Creating a QECC Class

To facilitate the evaluation of QECC protocols not included in PECOS, this appendix shows how to represent a QECC with a Python so can be used with PECOS. In particular, we look at representing the repetition code.

To begin, we create an empty Python file called zrepetition.py and import some useful classes:

"""
A representation of the Z-check repetition code.
"""
from pecos.circuits import QuantumCircuit
from pecos.qeccs import QECC, LogicalGate, LogicalInstruction

Subclasses of QECC, LogicalGate, and LogicalInstruction inherit numerous methods and attributes that simplify the creation of new qeccs. If some of the inherited methods and attributes are not appropriate for a QECC, one can typically override them.

The QECC class

We now create a class ZReptition to represent our qecc:

 class ZRepetition(QECC):
    def __init__(self, **qecc_params):
         # Pass qecc_params to the parent class:
         super().__init__(**qecc_params)
         # Set variables that describe the QECC:
         self._set_qecc_description()
         # Create a lattice for placing qubits:
         self.layout = self._generate_layout()
         # Identify the sides of the QECC:
         self._determine_sides()
         # Identify symbols with gate/instruction classes:
         self._set_symbols()

Here, the dict called qecc_params will be used to specify parameters that identify a member of the QECC’s family. We will discuss later the method calls see in the __init__ method.

Next, we write the _set_qecc_description, which sets class attributes that describe the QECC:

 def _set_qecc_description(self):
     self.name = 'Z Repetition Code'
     # Size of the repetition code:
     self.length = self.qecc_params['length']
     self.distance = 1
     self.num_data_qudits = self.length
     self.num_logical_qudits = 1
     self.num_ancillas = self.num_data_qudits - 1

The name attribute identifies the code. The length attribute we will use to define how long the QECC is. We use distance to determine the size of the QECC. We will be describing a repetition that only has \(Z\) checks; therefore, the code will not detect any \(Z\) errors. For this reason, the distance is one no matter the length of the QECC. num_data_qudits is the number of data qubits. The attribute num_logical_qudits is the number of logical qubits we will encode with this QECC. The total number of ancillas used in all the qecc’s procedures is equal to num_ancillas. The total number of qubits is equal to the num_qudits attribute. This attribute is determined by the parent class QECC.

Next, we construct _set_symbols, which contain dictionaries that associate symbols to LogicalInstructions and LogicalGates. We will describe these classes later.

def _set_symbols(self):
    # instruction symbol => instr. class:
    self.sym2instruction_class = {
        'instr_syn_extract': InstrSynExtraction,
        'instr_init_zero': InstrInitZero, }
    # gate symbol => gate class:
    self.sym2gate_class = {
        'I': GateIdentity,
        'init |0>': GateInitZero, }

Now we write the method _generate_layout, which generates the physical layout of qubits. As we will see later, a physical layout is useful for defining the quantum circuits of the QECC protocol.

def _generate_layout(self):
    self.lattice_width = self.num_qudits
    data_ids = self._data_id_iter()
    ancilla_ids = self._ancilla_id_iter()
    y = 1
    for x in range(self.lattice_width):
        if x%2 == 0: # Even (ancilla qubit)
            self._add_node(x, y, data_ids)
        else: # Odd (data qubit)
            self._add_node(x, y, ancilla_ids)
    # `add_nodes` updates an attribute called `layout.`
    return self.layout

Finally for the qecc, we will add the method _determine_sides to create a dictionary that defines the physical boundary of the QECC. This information can be used by decoders to understand the geometry of the code.

def _determine_sides(self):
    self.sides = {
        'length': set(self.data_qudit_set)
        }

Logical Instruction Classes

Now that we have created a class to represent the QECC, we will now create classes to represent logical instructions. First create an logical instruction class, called InstrSynExtraction, that represents one round of syndrome extraction. Similar to the ZRepitition class, we will subclass our class off of the LogicalInstruction, which is provided by PECOS. After we do this, we will write an initialization method that receives as arguments the qecc instance the instruction belongs to, the associated symbol, and a dictionary of logical gate parameters called gate_params. This dictionary will come from the LogicalGate that contains the LogicalInstruction and may alter the LogicalGate and the QuantumCircuit contained in the LogicalInstruction.

class InstrSynExtraction(LogicalInstruction):
    def __init__(self, qecc, symbol, **gate_params):
        super().__init__(qecc, symbol, **gate_params)

        # The following are convienent for plotting:
        self.ancilla_x_check = set()
        self.ancilla_z_check = qecc.ancilla_qudit_set
        self._create_checks()
        self.set_logical_ops()
        self._compile_circuit(self.abstract_circuit) # Call at end

We now include the _create_checks method, which we will use to define the checks of the QECC:

   def _create_checks(self):
       self.abstract_circuit = QuantumCircuit(**self.gate_params)
       for qid in self.qecc.ancilla_qudit_set:
           x, y = qecc.layout[qid]

           # Get the data qubits to each side.
           d1 = qecc.position2qudit[(x-1, y)]
           d2 = qecc.position2qudit[(x+1, y)]
           self.abstract_circuit.append('Z check', {qid, d1, d2}, datas=[d1, d2], ancillas=[qid])

Here we use the physical layout of the QECC to construct checks. A ``QuantumCircuit`` called ``abstract_circuit`` is used to register each :math:`Z`-type check, the qubits it acts on, and whether the qubits are used as data or ancilla qubits. Note, check circuits such as the ones seen in Fig~\ref{fig:surf-checks} are used to implement the checks. The order of the data qubits in the ``datas`` keyword indicates the order which the data qubits are acted on by the check circuits. The checks registered by ``abstract_circuit`` are later compiled into quantum circuits.

Now we will write the method set_logical_ops, which define the logical operators of the QECCs.

  def set_logical_ops(self):
      data_qubits = set(self.qecc.data_qudit_set)
      logical_ops = [
          {'X': QuantumCircuit([{'X': {0}}]),
           'Z': QuantumCircuit([{'Z': data_qubits}])}
           ]
      self.initial_logical_ops = logical_ops
      self.final_logical_ops = logical_ops

      # The final logical sign and stabilizer
      self.logical_stabilizers = None
      self.logical_signs = None

Here, the variables ``initial_logical_ops`` and ``final_logical_ops`` that represent the initial and final logical operators, respectively, are set. Each of these variables are a list where each element represents a collection of logical operators of an encoded qudit. In particular, each element is a dictionary where the keys are symbols identified with the logical operator and the values are ``QuantumCircuits`` representing the unitaries of logical operators.

If a logical operator encodes a stabilizer state then logical_stabilizers is a list of the strings representing the logical operators that stabilizer the state. If the logical operator does not specifically encode a stabilizer state, then logical_stabilizers is set to None. The variable logical_signs is a list of signs the corresponding logical operators in logical_stabilizers. If the phase of the operators is \(+1\), then the element of logical_signs is 0. If the phase of the operators is \(-\), then the element of logical_signs is 1. If logical_stabilizers is None, then logical_signs is None.

We now define the initialization of the logical zero-stat:

  class InstrInitZero(LogicalInstruction):
      def __init__(self, qecc, symbol, **gate_params):
          super().__init__(qecc, symbol, **gate_params)
          # The following are convienent for plotting:
          self.ancilla_x_check = set()
          self.ancilla_z_check = qecc.ancilla_qudit_set
          self._create_checks()
          self.set_logical_ops()
          # Must be called at the end of initiation.
          self._compile_circuit(self.abstract_circuit)

Here, the method ``_create_checks`` is used to create check by first making a shallow copy of the ``abstract_circuit`` of the ``InstrSynExtraction`` class. After doing this we add :math:`|0\rangle` initialization of the data qubits on the 0th tick.

The _create_checks method is as follows:

def _create_checks(self):
    # Get an instance of the syndrome extraction instruction
    syn_ext = qecc.instruction('instr_syn_extract', **self.gate_params)
    # Make a shallow copy of the abstract circuits.
    self.abstract_circuit = syn_ext.abstract_circuit.copy()
    # Add it the initialization of the data qubits
    data_qudits = set(qecc.data_qudit_set)
    self.abstract_circuit.append('init |0>', locations=data_qudits, tick=0)
}

The set_logical_ops method is similar to the of method of the same name in InstrSynExtraction. The difference for this class is that a logical zero-state is encoded by the logical operator. Because of this, logical_stabilizers is set to ['Z'] and logical_signs is set to [0].

def set_logical_ops(self):
    data_qubits = set(self.qecc.data_qudit_set)
    self.initial_logical_ops = [
        {'X': QuantumCircuit([{'X': {0}}]),
         'Z': QuantumCircuit([{'Z': {0}}])}  ]
    self.final_logical_ops = [
        {'X': QuantumCircuit([{'X': {0}}]),
         'Z': QuantumCircuit([{'Z': data_qubits}])}  ]
    self.logical_stabilizers = ['Z']
    self.logical_signs = [0]

Logical Gate Classes

We now construct the LogicalClass classes. The construction of these classes is relatively simple compared to the create of LogicalInstruction classes.

To begin, we write the class representing the logical identity called GateIdentity:

  class GateIdentity(LogicalGate):
      def __init__(self, qecc, symbol, **gate_params):
          super().__init__(qecc, symbol, **gate_params)
          self.expected_params(gate_params, {'num_syn_extract', 'error_free', 'random_outcome'})
          self.num_syn_extract = gate_params.get('num_syn_extract', qecc.length)
          self.instr_symbols = ['instr_syn_extract'] * self.num_syn_extract


Here, the initialization method includes the argument ``qecc`` and the argument ``symbol``. These are the ``qecc`` instance of the ``LogicalGate`` class and the string used to represent the ``LogicalGate``, respectively. The initialization method also accepts a keyword arguments, which are stored in the dictionary ``gate_params`` and may be used to alter the ``LogicalGate`` and associated ``LogicalInstructions``.

The method expected_params determines the keyword arguments that are accepted from gate_params. The number of syndrome extraction rounds equal to 'num_syn_extract'. in the gate_params dictionary. Finally, a list of LogicalInstruction symbols is stored in the variable instr_symbols. The instr_symbols indicates the order of LogicalInstructions that the gate represents. The correspondence between the LogicalInstruction classes and symbols was established by the sym2instruction_class method of the ZRepetition class.

We will also create a LogicalGate class the represents the initialization of logical zero:

  class GateInitZero(LogicalGate):
      def __init__(self, qecc, symbol, **gate_params):
          super().__init__(qecc, symbol, **gate_params)
          self.expected_params(gate_params, {'num_syn_extract', 'error_free', 'random_outcome'})
          self.num_syn_extract = gate_params.get('num_syn_extract', 0)
          self.instr_symbols = ['instr_init_zero']
          syn_extract = ['instr_syn_extract'] * self.num_syn_extract
          self.instr_symbols.extend(syn_extract)

Here, all the methods function the same way as those in the ``GateIdentity`` class.

Example Usage

Now we will look at a small example of using the ZRepetition class that we created. We begin by importing the class from the zrepetition.py script and creating an instance of length three:

from zrepetition import ZRepetition
qecc = ZRepetition(length=3)

Now that we have created an instance, we will use the plot method that is inherited by the syndrome-extraction instruction:

qecc.instruction('instr_syn_extract').plot()

This code results in the plot of the length three repetition code:

../_images/qecc_zrep_syn_extract.png

The ZRepetition class can be used just like any other qecc that comes with PECOS. For example, we can run the following simulation:

>>> import pecos as pc
>>> depolar = pc.error_gens.DepolarGen(model_level='code_capacity')
>>> logic = pc.circuits.LogicalCircuit()
>>> logic.append(qecc.gate('ideal init |0>'))
>>> logic.append(qecc.gate('I'))
>>> circ_runner = pc.circuit_runners.Standard(seed=3)
>>> state = circ_runner.init(qecc.num_qudits)
>>> meas, err = circ_runner.run_logic(state, logic, error_gen=depolar, error_params={'p': 0.1})
>>> meas
{(1, 2): {3: {3: 1}}}
>>> err
{(1, 2): {0: {'after': QuantumCircuit([{'X': {4}}])}}}