Adding a new code to PanQEC

In this tutorial, we will learn how to create your own custom code using PanQEC’s template and structure, with the goal of visualizing it in the GUI and calculating its threshold.

Let’s start by making a few imports:

[1]:
from panqec.codes import StabilizerCode
from panqec.gui import GUI

Creating the code class

Let’s say you want to implement your own version of the 3D toric code. In PanQEC, a code is represented by a class that inherits from the base class StabilizerCode. More precisely, the class should have the following structure:

[2]:
class MyToric3DCode(StabilizerCode):
    dimension = 3
    deformation_names = ['XZZX']

    @property
    def label(self):
        pass

    def get_qubit_coordinates(self):
        pass

    def get_stabilizer_coordinates(self):
        pass

    def stabilizer_type(self, location):
        pass

    def get_stabilizer(self, location):
        pass

    def qubit_axis(self, location):
        pass

    def get_logicals_x(self):
        pass

    def get_logicals_z(self):
        pass

    def get_deformation(self, location, deformation_name, **kwargs):
        pass

    def stabilizer_representation(self, location, rotated_picture=False):
        pass

    def qubit_representation(self, location, rotated_picture=False):
        pass

The two class attributes represent the dimension of the code (useful mainly for the visualization) and a list of potential Clifford-deformations. For instance, we will show here how to implement the XZZX-type Clifford-deformation presented in this paper.

Let’s now consider the different class methods one-by-one.

Label

The label is a name that should uniquely identify a code and its parameters. It is used for instance to identify each code in the output generated when calculating a threshold. In our example, we can give it a name of the form 'My Toric {L_x}x{L_y}x{L_z}'

[3]:
def label(self):
    return 'My Toric {}x{}x{}'.format(*self.size)

Qubit and stabilizer coordinates

The first step when implementing a new code is to define a coordinate system. This coordinate system should contain both the qubits and the stabilizers and uniquely identify them. But apart from these requirements, the user is free to choose the most convenient system for the code. For instance, if several stabilizer (or qubit) sit at a given location (e.g. for the color code), one can add an extra dimension to the coordinates in order to uniquely specify each of those (e.g. (0,x,y,z) for the X plaquettes and (1,x,y,z) for the Z plaquettes in the color code). On the other hand, not all locations in the coordinate system have to be filled by qubit or stabilizers.

For our 3D toric code, the coordinate system will consist of cartesian coordinates (x,y,z), where horizontal qubits/faces sit at even z (starting at z=0), and vertical qubits/faces at odd z.

To construct the code, PanQEC then simply takes as input the list of coordinates for both qubits and stabilizers:

[4]:
def get_qubit_coordinates(self):
    coordinates = []
    Lx, Ly, Lz = self.size

    # Qubits along e_x
    for x in range(1, 2*Lx, 2):
        for y in range(0, 2*Ly, 2):
            for z in range(0, 2*Lz, 2):
                coordinates.append((x, y, z))

    # Qubits along e_y
    for x in range(0, 2*Lx, 2):
        for y in range(1, 2*Ly, 2):
            for z in range(0, 2*Lz, 2):
                coordinates.append((x, y, z))

    # Qubits along e_z
    for x in range(0, 2*Lx, 2):
        for y in range(0, 2*Ly, 2):
            for z in range(1, 2*Lz, 2):
                coordinates.append((x, y, z))

    return coordinates
[5]:
def get_stabilizer_coordinates(self):
    coordinates = []
    Lx, Ly, Lz = self.size

    # Vertices
    for x in range(0, 2*Lx, 2):
        for y in range(0, 2*Ly, 2):
            for z in range(0, 2*Lz, 2):
                coordinates.append((x, y, z))

    # Face in xy plane
    for x in range(1, 2*Lx, 2):
        for y in range(1, 2*Ly, 2):
            for z in range(0, 2*Lz, 2):
                coordinates.append((x, y, z))

    # Face in yz plane
    for x in range(0, 2*Lx, 2):
        for y in range(1, 2*Ly, 2):
            for z in range(1, 2*Lz, 2):
                coordinates.append((x, y, z))

    # Face in xz plane
    for x in range(1, 2*Lx, 2):
        for y in range(0, 2*Ly, 2):
            for z in range(1, 2*Lz, 2):
                coordinates.append((x, y, z))

    return coordinates

Defining stabilizers

The next step is to define stabilizers. For that, we first define a helper method stabilizer_type that specifies whether a stabilizer at a given location is a vertex or a face.

[6]:
def stabilizer_type(self, location):
    x, y, z = location
    if x % 2 == 0 and y % 2 == 0:
        return 'vertex'
    else:
        return 'face'

We then create a method get_stabilizer that takes a given location and returns an operator, in the form of a dictionary that assigns a Pauli (as a string ‘X’, ‘Y’ or ‘Z’) to each qubit location in the support of the stabilizer. For instance, if we are given the location (1,1,0) (which should correspond to a horizontal face), the method will return the following:

get_stabilizer((1,1,0)) -> {(1,0,0): 'X', (0,1,0): 'X', (2,1,0): 'X', (1,2,0): 'X'}
[7]:
def get_stabilizer(self, location):
    if self.stabilizer_type(location) == 'vertex':
        pauli = 'Z'
    else:
        pauli = 'X'

    x, y, z = location

    # delta specifies the positions of the qubits involved in the stabilizer
    # relative to the stabilizer position
    if self.stabilizer_type(location) == 'vertex':
        delta = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
    else:
        # Face in xy-plane.
        if z % 2 == 0:
            delta = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0)]
        # Face in yz-plane.
        elif (x % 2 == 0):
            delta = [(0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
        # Face in zx-plane.
        elif (y % 2 == 0):
            delta = [(-1, 0, 0), (1, 0, 0), (0, 0, -1), (0, 0, 1)]

    operator = dict()
    for d in delta:
        Lx, Ly, Lz = self.size
        qubit_location = ((x + d[0]) % (2*Lx), (y + d[1]) % (2*Ly), (z + d[2]) % (2*Lz))

        if self.is_qubit(qubit_location):
            operator[qubit_location] = pauli

    return operator

Qubit axis

The method qubit_axis takes the location of a qubit in the coordinate system and returns its orientation axis. For instance, in the 3D toric code, qubits are represented as edges, that can be oriented along the x, y or z axis. Therefore, qubit_axis will return the string 'x', 'y' or 'z'. This method is useful for defining Clifford deformations (XZZX-type transformations of the code) along a given axis.

[8]:
def qubit_axis(self, location):
    x, y, z = location

    if (z % 2 == 0) and (x % 2 == 1) and (y % 2 == 0):
        axis = 'x'
    elif (z % 2 == 0) and (x % 2 == 0) and (y % 2 == 1):
        axis = 'y'
    else:
        axis = 'z'

    return axis

Logical operators

We now need to define the logical operators of the code. Like for the stabilizers, a logical operator is represented as a dictionary that associates a Pauli string ('X', 'Y' or 'Z') to each qubit location. The methods get_logicals_x and get_logicals_z return a list of such dictionaries, whose length should be the number of logical qubits of our code.

[9]:
def get_logicals_x(self):
    """The 3 logical X operators."""

    Lx, Ly, Lz = self.size
    logicals = []

    # X operators along x edges in x direction.
    operator = dict()
    for x in range(1, 2*Lx, 2):
        operator[(x, 0, 0)] = 'X'
    logicals.append(operator)

    # X operators along y edges in y direction.
    operator = dict()
    for y in range(1, 2*Ly, 2):
        operator[(0, y, 0)] = 'X'
    logicals.append(operator)

    # X operators along z edges in z direction
    operator = dict()
    for z in range(1, 2*Lz, 2):
        operator[(0, 0, z)] = 'X'
    logicals.append(operator)

    return logicals
[10]:
def get_logicals_z(self):
    """Get the 3 logical Z operators."""
    Lx, Ly, Lz = self.size
    logicals = []

    # Z operators on x edges forming surface normal to x (yz plane).
    operator = dict()
    for y in range(0, 2*Ly, 2):
        for z in range(0, 2*Lz, 2):
            operator[(1, y, z)] = 'Z'
    logicals.append(operator)

    # Z operators on y edges forming surface normal to y (zx plane).
    operator = dict()
    for z in range(0, 2*Lz, 2):
        for x in range(0, 2*Lx, 2):
            operator[(x, 1, z)] = 'Z'
    logicals.append(operator)

    # Z operators on z edges forming surface normal to z (xy plane).
    operator = dict()
    for x in range(0, 2*Lx, 2):
        for y in range(0, 2*Ly, 2):
            operator[(x, y, 1)] = 'Z'
    logicals.append(operator)

    return logicals

Clifford-deformations

PanQEC has been designed to handle Clifford deformations and biased noise in a simple way. The method get_deformation takes at least two parameters: the location of the qubit and the name of the deformation. For the 3D toric code, we will consider an XZZX-type deformation where every qubit along one of the axis is Hadamared. Therefore, the method also takes a deformation_axis parameter that indicates which axis should be Hadamared. The method then returns a dictionary with the Pauli mapping on the current qubit. In our case, we check that the qubit location given as parameter lives on the deformation axis, and if so, we return the Hadamard mapping {'X': 'Z', 'Y': 'Y', 'Z': 'X'}. The deformation can then be used in two way:

  1. Through code.deform(deformation_name, **kwargs) function, which turns the code into its deformed version inplace. This is used in the GUI to visualize the Clifford-deformation.

  2. Through the error model. A PauliErrorModel takes as parameter deformation_name and deformation_kwargs, which are used to Clifford-deform the error model instead of the code. This is the most common approach in simulations.

[11]:
def get_deformation(self, location, deformation_name, deformation_axis='y'):
    if deformation_name == 'XZZX':
        undeformed_dict = {'X': 'X', 'Y': 'Y', 'Z': 'Z'}
        deformed_dict = {'X': 'Z', 'Y': 'Y', 'Z': 'X'}

        if self.qubit_axis(location) == deformation_axis:
            deformation = deformed_dict
        else:
            deformation = undeformed_dict

    else:
        raise ValueError(f"The deformation {deformation_name}"
                         "does not exist")

    return deformation

Qubit and stabilizer visual representation

With all those methods defined, we are ready to simulate the code. If that’s all you want to do, you can skip this section. However, if you can visualize the code in the GUI, you can spot bugs and intuititively understand what is actually going on. To visualize it, we need to tell PanQEC how to visually represent qubits and stabilizers, such as what shapes and colors to use. The way to do this is to make a JSON file that specifies all the visual parameters of the code. Here is an example of JSON file that we can use for our 3D toric code:

[12]:
{
    "MyToric3DCode": {
        "qubits": {
            "kitaev": {
                "object": "cylinder",
                "color": {
                    "I": "pink",
                    "X": "red",
                    "Y": "green",
                    "Z": "blue"
                },
                "opacity": {
                    "activated": {
                        "min": 1,
                        "max": 1
                    },
                    "deactivated": {
                        "min": 0.1,
                        "max": 0.6
                    }
                },
                "params": {
                    "length": 2,
                    "radius": 0.1,
                    "angle": 0
                }
            },
            "rotated": {
                "object": "sphere",
                "color": {
                    "I": "white",
                    "X": "red",
                    "Y": "green",
                    "Z": "blue"
                },
                "opacity": {
                    "activated": {
                        "min": 1,
                        "max": 1
                    },
                    "deactivated": {
                        "min": 0.1,
                        "max": 0.4
                    }
                },
                "params": {
                    "radius": 0.2
                }
            }
        },
        "stabilizers": {
            "kitaev": {
                "vertex": {
                    "object": "sphere",
                    "color": {
                        "activated": "gold",
                        "deactivated": "white"
                    },
                    "opacity": {
                        "activated": {
                            "min": 1,
                            "max": 1
                        },
                        "deactivated": {
                            "min": 0.1,
                            "max": 0.6
                        }
                    },
                    "params": {"radius": 0.2}
                },
                "face": {
                    "object": "rectangle",
                    "color": {
                        "activated": "gold",
                        "deactivated": "blue"
                    },
                    "opacity": {
                        "activated": {
                            "min": 0.6,
                            "max": 0.6
                        },
                        "deactivated": {
                            "min": 0,
                            "max": 0
                        }
                    },
                    "params": {
                        "w": 1.5,
                        "h": 1.5,
                        "normal": [1, 0, 0],
                        "angle": 0
                    }
                }
            },
            "rotated": {
                "vertex": {
                    "object": "octahedron",
                    "color": {
                        "activated": "orange",
                        "deactivated": "orange"
                    },
                    "opacity": {
                        "activated": {
                            "min": 0.9,
                            "max": 0.9
                        },
                        "deactivated": {
                            "min": 0.1,
                            "max": 0.3
                        }
                    },
                    "params": {
                        "length": 1,
                        "angle": 0
                    }
                },
                "face": {
                    "object": "rectangle",
                    "color": {
                        "activated": "gold",
                        "deactivated": "gold"
                    },
                    "opacity": {
                        "activated": {
                            "min": 0.9,
                            "max": 0.9
                        },
                        "deactivated": {
                            "min": 0.1,
                            "max": 0.3
                        }
                    },
                    "params": {
                        "w": 1.4142,
                        "h": 1.4142,
                        "normal": [0,0,1],
                        "angle": 0.7854
                    }
                }
            }
        }
    }
}
[12]:
{'MyToric3DCode': {'qubits': {'kitaev': {'object': 'cylinder',
    'color': {'I': 'pink', 'X': 'red', 'Y': 'green', 'Z': 'blue'},
    'opacity': {'activated': {'min': 1, 'max': 1},
     'deactivated': {'min': 0.1, 'max': 0.6}},
    'params': {'length': 2, 'radius': 0.1, 'angle': 0}},
   'rotated': {'object': 'sphere',
    'color': {'I': 'white', 'X': 'red', 'Y': 'green', 'Z': 'blue'},
    'opacity': {'activated': {'min': 1, 'max': 1},
     'deactivated': {'min': 0.1, 'max': 0.4}},
    'params': {'radius': 0.2}}},
  'stabilizers': {'kitaev': {'vertex': {'object': 'sphere',
     'color': {'activated': 'gold', 'deactivated': 'white'},
     'opacity': {'activated': {'min': 1, 'max': 1},
      'deactivated': {'min': 0.1, 'max': 0.6}},
     'params': {'radius': 0.2}},
    'face': {'object': 'rectangle',
     'color': {'activated': 'gold', 'deactivated': 'blue'},
     'opacity': {'activated': {'min': 0.6, 'max': 0.6},
      'deactivated': {'min': 0, 'max': 0}},
     'params': {'w': 1.5, 'h': 1.5, 'normal': [1, 0, 0], 'angle': 0}}},
   'rotated': {'vertex': {'object': 'octahedron',
     'color': {'activated': 'orange', 'deactivated': 'orange'},
     'opacity': {'activated': {'min': 0.9, 'max': 0.9},
      'deactivated': {'min': 0.1, 'max': 0.3}},
     'params': {'length': 1, 'angle': 0}},
    'face': {'object': 'rectangle',
     'color': {'activated': 'gold', 'deactivated': 'gold'},
     'opacity': {'activated': {'min': 0.9, 'max': 0.9},
      'deactivated': {'min': 0.1, 'max': 0.3}},
     'params': {'w': 1.4142,
      'h': 1.4142,
      'normal': [0, 0, 1],
      'angle': 0.7854}}}}}}

Let’s dissect the structure of this file. Since a single JSON file can describe several codes, the highest-level key simply indicates the name of the class.

The second level should contain two keys, "qubits" and "stabilizers", which will respectively describe the qubits and stabilizer representations. Then, for each of those, we have to indicate two representations: one for the Kitaev picture (where qubits are usually edges) and one of the rotated picture (where qubits are usually vertices). It’s mostly useful for codes that actually have those two representations (such as the 2D and 3D toric codes), but if you only want to describe one representation, you’re free to copy-paste the same content in both the "kitaev" and the "rotated" fields.

Now comes the core of the representation. For each qubit/stabilizer in the Kitaev/rotated picture, we have to specify four elements: "object", "color", "opacity", "params". The "object" key indicates the general type of object we want. The definition of the different objects can be found in the file panqec/gui/js/shape.js of the repository, and currently contains the following possibilities: cylinder, sphere, rectangle, cube and octahedron.

We then have to indicate the color of each object. For the qubits, we have to specify four colors, depending on whether there is an error ("X", "Y" or "Z") or no error ("I") on this qubit. For the stabilizers, we indicate a color for when it is activated (i.e. when there is an excitation) or not. The full list of color names can be found by printing the StabilizerCode property code.colormap. Currently, direct RBG values are not supported, but will come in subsequent versions of PanQEC.

We can also specify the opacity of our object, in four different cases, depending on whether the stabilizer/qubit is activated or not and if we currently are at the minimum or maximum opacity (this can be changed interactively in the GUI). The opacity is a number between 0 and 1, where 0 is completely transparent and 1 completely opaque.

Finally, we specify the parameters of the object through the key params. Those parameters are object-dependent and can also be extracted from panqec/gui/js/shape.js. Many parameters correspond to sizes (such as the radius of a sphere or the lengths of a cube), and for those, the scale is defined following the convention of the coordinate system defined above.

Once the JSON is written (let’s say in a file called toric3d.json, located in the same folder as your python file), we have to link it to our class. For that we override two methods qubit_representation and stabilizer_representation, that take a location and return the representation (as a Python dictionary) corresponding to the given qubit or stabilizer (and whose highest-level keys are object, color, opacity, params and a new key location that we will discuss below). We start by calling the base method using super(), with the json file we have just created as a parameter:

[13]:
def stabilizer_representation(self, location, rotated_picture=False):
    representation = super().stabilizer_representation(location, rotated_picture, json_file='toric3d.json')

    return representation

def qubit_representation(self, location, rotated_picture=False):
    representation = super().qubit_representation(location, rotated_picture, json_file='toric3d.json')

    return representation

Those base methods will automatically parse the json file and return the dictionary in the correct format. In particular, they fill the key "location" using the location we have given as a parameter, and for the qubits, use the method qubit_axis to automatically orient the cylinder. However, overriding those methods can be useful not only to specify a new JSON file, but also to modify the parameters of the different objects dynamically depending on their location. In our case, we want the orientation of the faces to depend on the position. We can therefore override the parameter normal of the rectangle (specifying its normal axis):

[14]:
def stabilizer_representation(self, location, rotated_picture=False):
    representation = super().stabilizer_representation(location, rotated_picture, json_file='toric3d.json')

    x, y, z = location
    if not rotated_picture and self.stabilizer_type(location) == 'face':
        if z % 2 == 0:  # xy plane
            representation['params']['normal'] = [0, 0, 1]
        elif x % 2 == 0:  # yz plane
            representation['params']['normal'] = [1, 0, 0]
        else:  # xz plane
            representation['params']['normal'] = [0, 1, 0]

    if rotated_picture and self.stabilizer_type(location) == 'face':
        if z % 2 == 0:
            representation['params']['normal'] = [0, 0, 1]
        elif x % 2 == 0:
            representation['params']['normal'] = [1, 0, 0]
        else:
            representation['params']['normal'] = [0, 1, 0]

    return representation

Another common key that we could want to override is the location parameter. Indeed, in some coordinate system, the coordinates of a qubit or stabilizer can be different from its spatial location. For instance, we previously discussed how if two qubit or two stabilizers are sitting at the same location, we can add a new coordinate to specify it uniquely. An example where this appears is the Haah code, where each site contains two qubits. Qubit coordinates are therefore specified as a 4-tuple (i,x,y,z). where i is either 0 or 1. In this case, we need to override the location parameter to specify the actual spatial location of each qubit:

[15]:
def qubit_representation(self, location, rotated_picture=False):
    representation = super().qubit_representation(location, rotated_picture)

    i, x, y, z = location

    d = representation['params']['radius']
    if i == 0:
        representation['location'] = [x-d, y, z]
    else:
        representation['location'] = [x+d, y, z]

    return representation

Complete class

Here is what our class now looks like:

[16]:
class MyToric3DCode(StabilizerCode):
    dimension = 3

    @property
    def label(self):
        return 'My Toric {}x{}x{}'.format(*self.size)

    def get_qubit_coordinates(self):
        coordinates = []
        Lx, Ly, Lz = self.size

        # Qubits along e_x
        for x in range(1, 2*Lx, 2):
            for y in range(0, 2*Ly, 2):
                for z in range(0, 2*Lz, 2):
                    coordinates.append((x, y, z))

        # Qubits along e_y
        for x in range(0, 2*Lx, 2):
            for y in range(1, 2*Ly, 2):
                for z in range(0, 2*Lz, 2):
                    coordinates.append((x, y, z))

        # Qubits along e_z
        for x in range(0, 2*Lx, 2):
            for y in range(0, 2*Ly, 2):
                for z in range(1, 2*Lz, 2):
                    coordinates.append((x, y, z))

        return coordinates

    def get_stabilizer_coordinates(self):
        coordinates = []
        Lx, Ly, Lz = self.size

        # Vertices
        for x in range(0, 2*Lx, 2):
            for y in range(0, 2*Ly, 2):
                for z in range(0, 2*Lz, 2):
                    coordinates.append((x, y, z))

        # Face in xy plane
        for x in range(1, 2*Lx, 2):
            for y in range(1, 2*Ly, 2):
                for z in range(0, 2*Lz, 2):
                    coordinates.append((x, y, z))

        # Face in yz plane
        for x in range(0, 2*Lx, 2):
            for y in range(1, 2*Ly, 2):
                for z in range(1, 2*Lz, 2):
                    coordinates.append((x, y, z))

        # Face in xz plane
        for x in range(1, 2*Lx, 2):
            for y in range(0, 2*Ly, 2):
                for z in range(1, 2*Lz, 2):
                    coordinates.append((x, y, z))

        return coordinates

    def stabilizer_type(self, location):
        x, y, z = location
        if x % 2 == 0 and y % 2 == 0:
            return 'vertex'
        else:
            return 'face'

    def get_stabilizer(self, location, deformed_axis=None):
        if self.stabilizer_type(location) == 'vertex':
            pauli = 'Z'
        else:
            pauli = 'X'

        x, y, z = location

        if self.stabilizer_type(location) == 'vertex':
            delta = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
        else:
            # Face in xy-plane.
            if z % 2 == 0:
                delta = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0)]
            # Face in yz-plane.
            elif (x % 2 == 0):
                delta = [(0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1)]
            # Face in zx-plane.
            elif (y % 2 == 0):
                delta = [(-1, 0, 0), (1, 0, 0), (0, 0, -1), (0, 0, 1)]

        operator = dict()
        for d in delta:
            Lx, Ly, Lz = self.size
            qubit_location = ((x + d[0]) % (2*Lx), (y + d[1]) % (2*Ly), (z + d[2]) % (2*Lz))

            if self.is_qubit(qubit_location):
                operator[qubit_location] = pauli

        return operator

    def qubit_axis(self, location):
        x, y, z = location

        if (z % 2 == 0) and (x % 2 == 1) and (y % 2 == 0):
            axis = 'x'
        elif (z % 2 == 0) and (x % 2 == 0) and (y % 2 == 1):
            axis = 'y'
        else:
            axis = 'z'

        return axis

    def get_logicals_x(self):
        Lx, Ly, Lz = self.size
        logicals = []

        # X operators along x edges in x direction.
        operator = dict()
        for x in range(1, 2*Lx, 2):
            operator[(x, 0, 0)] = 'X'
        logicals.append(operator)

        # X operators along y edges in y direction.
        operator = dict()
        for y in range(1, 2*Ly, 2):
            operator[(0, y, 0)] = 'X'
        logicals.append(operator)

        # X operators along z edges in z direction
        operator = dict()
        for z in range(1, 2*Lz, 2):
            operator[(0, 0, z)] = 'X'
        logicals.append(operator)

        return logicals

    def get_logicals_z(self):
        Lx, Ly, Lz = self.size
        logicals = []

        # Z operators on x edges forming surface normal to x (yz plane).
        operator = dict()
        for y in range(0, 2*Ly, 2):
            for z in range(0, 2*Lz, 2):
                operator[(1, y, z)] = 'Z'
        logicals.append(operator)

        # Z operators on y edges forming surface normal to y (zx plane).
        operator = dict()
        for z in range(0, 2*Lz, 2):
            for x in range(0, 2*Lx, 2):
                operator[(x, 1, z)] = 'Z'
        logicals.append(operator)

        # Z operators on z edges forming surface normal to z (xy plane).
        operator = dict()
        for x in range(0, 2*Lx, 2):
            for y in range(0, 2*Ly, 2):
                operator[(x, y, 1)] = 'Z'
        logicals.append(operator)

        return logicals

    def stabilizer_representation(self, location, rotated_picture=False):
        representation = super().stabilizer_representation(location, rotated_picture, json_file='toric3d.json')

        x, y, z = location
        if not rotated_picture and self.stabilizer_type(location) == 'face':
            if z % 2 == 0:  # xy plane
                representation['params']['normal'] = [0, 0, 1]
            elif x % 2 == 0:  # yz plane
                representation['params']['normal'] = [1, 0, 0]
            else:  # xz plane
                representation['params']['normal'] = [0, 1, 0]

        if rotated_picture and self.stabilizer_type(location) == 'face':
            if z % 2 == 0:
                representation['params']['normal'] = [0, 0, 1]
            elif x % 2 == 0:
                representation['params']['normal'] = [1, 0, 0]
            else:
                representation['params']['normal'] = [0, 1, 0]

        return representation

    def qubit_representation(self, location, rotated_picture=False):
        representation = super().qubit_representation(location, rotated_picture, json_file='toric3d.json')

        return representation

    def get_deformation(self, location, deformation_name, deformation_axis='y'):
        if deformation_name == 'XZZX':
            undeformed_dict = {'X': 'X', 'Y': 'Y', 'Z': 'Z'}
            deformed_dict = {'X': 'Z', 'Y': 'Y', 'Z': 'X'}

            if self.qubit_axis(location) == deformation_axis:
                deformation = deformed_dict
            else:
                deformation = undeformed_dict

        else:
            raise ValueError(f"The deformation {deformation_name}"
                             "does not exist")

        return deformation

Visualizing the code in the GUI

Now that the code has been implemented, we can add it to the GUI in order to visualize it.

gui = GUI()
gui.add_code(MyToric3DCode, 'My Toric 3D')
gui.run(port=5000)

This code should start a server at the address http://127.0.0.1:5000. To see your code, open the address on your favorite web browser, click on “3D codes”, and select “My Toric 3D” as a code in the contextual menu on the right. Congratulations, you have added a new code in the GUI!

Once you’ve made sure that the code is correct by testing it on the GUI, you can start doing research with it!