.. _examp-qecc:

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:

.. code-block:: python

    """
    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 ``ZRepetition`` to represent our ``qecc``:

.. doctest::
   :options: +SKIP

    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:


.. doctest:: python
   :options: +SKIP

    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 :math:`Z` checks;
therefore, the code will not detect any :math:`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.

.. doctest:: python
    :options: +SKIP

    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.

.. doctest:: python
    :options: +SKIP

    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.

.. doctest:: python
    :options: +SKIP

    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 ``ZRepetition`` 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``.

.. doctest:: python
    :options: +SKIP

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

            # The following are convenient 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:

.. doctest:: python
    :options: +SKIP

    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.

.. doctest:: python
   :options: +SKIP

   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 :math:`+1`, then the element of
``logical_signs`` is 0. If the phase of the operators is :math:`-`, 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:

.. doctest:: python
   :options: +SKIP

   class InstrInitZero(LogicalInstruction):
       def __init__(self, qecc, symbol, **gate_params):
           super().__init__(qecc, symbol, **gate_params)
           # The following are convenient 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:


.. doctest:: python
   :options: +SKIP

   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]``.

.. doctest:: python
   :options: +SKIP

   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``:

.. doctest:: python
   :options: +SKIP

   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:

.. doctest:: python
   :options: +SKIP

   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:

.. doctest::
   :options: +SKIP

   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:

.. doctest:: python
   :options: +SKIP

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

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

.. image:: ../images/qecc_zrep_syn_extract.png
   :align: center
   :width: 400px

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

.. doctest:: python
   :options: +SKIP

         >>> import pecos as pc
         >>> depolar = pc.error_gens.DepolarModel(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}}])}}}

         >>> import pecos as pc
         >>> depolar = pc.error_gens.DepolarModel(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}}])}}}

      >>> import pecos as pc
      >>> depolar = pc.error_gens.DepolarModel(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}}])}}}

         >>> import pecos as pc
         >>> depolar = pc.error_gens.DepolarModel(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}}])}}}

         >>> import pecos as pc
         >>> depolar = pc.error_gens.DepolarModel(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}}])}}}

      >>> import pecos as pc
      >>> depolar = pc.error_gens.DepolarModel(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}}])}}}

      >>> import pecos as pc
      >>> depolar = pc.error_gens.DepolarModel(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}}])}}}

      >>> import pecos as pc
      >>> depolar = pc.error_gens.DepolarModel(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}}])}}}

   >>> 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}}])}}}
