Implicit Models

This tutorial explains how to create and use the implicit-layer-operation models present in pyGSTi. It doesn't show you how to build your own model, which will be the topic of a future tutorial.

"Implicit models", as we'll refer to implicit-layer-operation models from now on, store building blocks needed to construct layer operations but not usually the layer operations themselves. When simulating a circuit, an implicit model creates on the fly, from its building blocks, an operator for each circuit layer in turn. It therefore only creates operators for the layers that are actually needed for the circuit simulation. Thus simulating a circuit with an implicit model is similar to building an explicit model with just the needed layer-operations based on some building blocks and some rules.

Implicit models are very useful within multi-qubit contexts, where there are so many possible circuit layers one cannot easily create and store separate operators for every possible layer. It is much more convenient to instead specify a smaller set of building-block-operators and rules for combining them into full $n$-qubit layer operations.

PyGSTi currently contains two types of implicit models, both derived from ImplicitOpModel (which is derived from Model):

  • LocalNoiseModel objects are noise models where "noise" (the departure or deviance from perfection) of a given gate is localized to only the qubits where that gate is intended to act. Said another way, the key assumption of a LocalNoiseModel is that gates act as the perfect identity everywhere except on their target qubits - the qubits they are supposed to act nontrivially upon. Because errors on non-target qubits can broadly be interpreted as "crosstalk", we can think of a LocalNoiseModel as a crosstalk-free model. For concreteness, some examples of local noise are:
    • a rotation gate over-rotates that qubit it's supposed to rotate
    • a controlled-not gate acts imperfectly on its control and target qubits but perfectly on all other qubits
  • CloudNoiseModel objects allow imperfections in a gate to involve qubits in a neighborhood of or cloud around the gate's target qubits. When the neighborhood is shrunk to just the target qubits themselves this reduced to a local noise model. What exactly constitutes a neighborhood or cloud is based on a number of "hops" (edge-traversals) on a graph of qubit connectivity that is supplied by the user.

Inside an implicit model: .prep_blks, .povm_blks, .operation_blks, and .instrument_blks

Whereas an ExplicitModel contains the dictionaries .preps, .povms, .operations, and .instruments (which hold layer operators), an ImplicitModel contains the dictionaries .prep_blks, .povm_blks, .operation_blks, and .instrument_blks. Each of these dictionaries contains a second level dictionaries, and it is this second level which hold actual operators (LinearOperator- and SPAMVec-derived objects) - the building blocks of the model. The keys of the top-level dictionary are category names, and the keys of the second-level dictionaries are typically gate names or circuit layer labels. For example, a LocalNoiseModel has two categories within its .operation_blks: "gates", and "layers", which we'll see more of below.

To begin, we'll import pyGSTi and define a function which prints the 1st and 2nd level keys of any ImplicitModel:

In [ ]:
import pygsti
import numpy as np

def print_implicit_model_blocks(mdl, showSPAM=False):
    if showSPAM:
        print('State prep building blocks (.prep_blks):')
        for blk_lbl,blk in mdl.prep_blks.items():
            print(" " + blk_lbl, ": ", ', '.join(map(str,blk.keys())))
        print()

        print('POVM building blocks (.povm_blks):')
        for blk_lbl,blk in mdl.povm_blks.items():
            print(" "  + blk_lbl, ": ", ', '.join(map(str,blk.keys())))
        print()
    
    print('Operation building blocks (.operation_blks):')
    for blk_lbl,blk in mdl.operation_blks.items():
        print(" " + blk_lbl, ": ", ', '.join(map(str,blk.keys())))
    print()

Local-noise implicit models

The LocalNoiseModel class represents a model whose gates are only have local noise (described above) applied to them. This makes it trivial to combine gate-operations into layer-operations because within a layer gates act on disjoint sets of qubits and therefore so does the (local) noise. We can create a LocalNoiseModel by passing build_standard_localnoise_model a number of qubits and set of gate names (see the docstring for what gate names are recognized and how to add your own):

In [ ]:
mdl_locnoise = pygsti.construction.build_localnoise_model(n_qubits=4, gate_names=['Gxpi','Gypi','Gcnot'])
print(type(mdl_locnoise))
print_implicit_model_blocks(mdl_locnoise, showSPAM=True)

Here we've created a model on 4 qubits with $X(\pi)$, $Y(\pi)$ and CNOT gates. The default qubit labelling (see the qubit_labels argument) is the integers starting at 0, so in this case our qubits are labelled $0$, $1$, $2$, and $3$. The default qubit connectivity (see the geometry argument) is a line, so there are CNOT gates between each adjacent pair of qubits when arranged as $0-1-2-3$.

Let's take a look at what's inside the model:

  • There is just a single "layers" category within .prep_blks and .povm_blks, each containing just a single operator (a state preparation or POVM) which prepares or measures the entire 4-qubit register. Currently, the preparation and measurement portions of both a LocalNoiseModel and a CloudNoiseModel are not divided into components (e.g. 1-qubit factors) and so .prep_blks["layers"] and .povm_blks["layers"] behave similarly to an ExplicitOpModel's .preps and .povms dictionaries. Because there's nothing special going on here, we'll omit printing .prep_blks and .povm_blks for the rest of this tutorial by leaving the default showSPAM=False in future calls to print_implicit_model_blocks.

    (Aside) The question may come to mind: why does the model create these layer operations now? Why not just create these on an as-needed basis? The answer is for efficiency - it takes some nontrivial amount of work to "embed" a 1- or 2-qubit process matrix within a larger (e.g. 4-qubit) process matrix, and we perform this work once up front so it doesn't need to be repeated later on.

  • There are two categories within .operation_blks: "gates" and "layers". The former contains three elements which are just the gate names ("Gxpi", "Gypi", and "Gcnot"), which hold the 1- and 2-qubit gate operations. The "layers" category contains holds (4-qubit) primitive layer operations which give the action of layers containing just a single gate (called "primitive layers"). From their labels we can see which gate is placed where within the layer.
  • Gate operations are linked to all of the layer operations containing that gate. For example, the Gxpi element of .operation_blks["gates"] is linked to the Gxpi:0, Gxpi:1, Gxpi:2, Gxpi:3 members of .operation_blks["layers"]. Technically, this means that these layer operations contain a reference to (not a copy of) the .operation_blks["gates"]["Gxpi"] object. Functionally, this means that whatever noise or imperfections are present in the "Gxpi" gate operation will be manifest in all of the corresponding layer operations, as we'll see below. This behavior is specified by the independent_gates argument, whose default value is True. We'll see what happens when we change this farther down below.

Let's print the "Gxpi" operator:

In [ ]:
print(mdl_locnoise.operation_blks['gates']['Gxpi']) # Static!

Notice that is a StaticDenseOp object. The gate operations in .operation_blks["gates"] are static operators (they have no adjustable parameters - see the Operators tutorial for an explanation of the different kinds of operators). This is because the default value of the parameterization argument is "static".

Creating a modifiable LocalNoiseModel

Since we'd like to modify these gate operations, let's make a new model with parameterization="full". We'll also set the availability argument to demonstrate how we can specify where the CNOT gates should go - they'll occur in only one "direction", from left to right, across the $0-1-2-3$ chain:

In [ ]:
mdl_locnoise = pygsti.construction.build_localnoise_model(n_qubits=4, gate_names=['Gxpi','Gypi','Gcnot'],
                                                                  availability={'Gcnot': [(0,1),(1,2),(2,3)]},
                                                                  parameterization='full')
print_implicit_model_blocks(mdl_locnoise)

Now the gates are FullDenseOp objects (which can be modified as we please):

In [ ]:
print(mdl_locnoise.operation_blks['gates']['Gxpi'])

Let's set the process matrix (more accurately, this is the Pauli-transfer-matrix of the gate) of "Gxpi" to include some depolarization:

In [ ]:
mdl_locnoise.operation_blks['gates']['Gxpi'] = np.array([[1,   0,   0,   0],
                                                         [0, 0.9,   0,   0],
                                                         [0,   0,-0.9,   0],
                                                         [0,   0,   0,-0.9]],'d')

Circuit simulation

Now that we have a model, we'll simulate a circuit with four "primitive $X(\pi)$" layers. Notice from the outcome probabilities that all for layers have imperfect (depolarized) $X(\pi)$ gates:

In [ ]:
c = pygsti.obj.Circuit( [('Gxpi',0),('Gxpi',1),('Gxpi',2),('Gxpi',3)], num_lines=4)
print(c)
mdl_locnoise.probs(c)

If we compress the circuit's depth (to 1) we can also simulate this circuit, since a LocalNoiseModel knows how to automatically create this single non-primitive (contains 4 $X(\pi)$ gates) layer from its gate and primitive-layer building blocks. Note that the probabilities are identical to the above case.

In [ ]:
c2 = c.parallelize()
print(c2)

mdl_locnoise.probs(c2)

Creating a LocalNoiseModel with independent gates

As we've just seen, by default build_standard_localnoise_model creates a LocalNoiseModel that contains just a single gate operation for each gate name (e.g. "Gxpi"). This is convenient when we expect the same gate acting on different qubits will have identical (or very similar) noise properties. What if, however, we expect that the $X(\pi)$ gate on qubit $0$ has a different type of noise than the $X(\pi)$ gate on qubit $1$? In this case, we want gates on different qubits to have independent noise, so we set independent_gates=True:

In [ ]:
mdl_locnoise2 = pygsti.construction.build_localnoise_model(n_qubits=4, gate_names=['Gxpi','Gypi','Gcnot'],
                                                                  availability={'Gcnot': [(0,1),(1,2),(2,3)]},
                                                                  parameterization='full', independent_gates=True)
print_implicit_model_blocks(mdl_locnoise2)

Notice that now there are separate .operation_blks["gates"] elements for each primitive layer. Now we can add some noise just to the $X(\pi)$ gate on qubit $0$, for instance:

In [ ]:
mdl_locnoise2.operation_blks['gates'][('Gxpi',0)] = np.array([[1,   0,   0,   0],
                                                              [0, 0.9,   0,   0],
                                                              [0,   0,-0.9,   0],
                                                              [0,   0,   0,-0.9]],'d')

When we simulate the same circuit as above, we find that only the first (on qubit $0$) $X(\pi)$ gate has depolarization error on it now:

In [ ]:
print(c)
mdl_locnoise2.probs(c)

Other geometries

Finally, note that we can specify other qubit connectivities using the geometry argument of build_standard_localnoise_model. You can specify a builtin name like "line" or "grid", or any pygsti.obj.QubitGraph object to specify which 2-qubit gates are available as primitive layers. Here's an example of 9 qubits on a grid (note that edges of builtin graphs like "grid" are undirected, so the 2Q gates occur in both directions):

0-1-2
| | |
3-4-5
| | |
6-7-8

TODO: tutorial on graphs & example here

In [ ]:
mdl_locnoise3 = pygsti.construction.build_localnoise_model(n_qubits=9, gate_names=['Gxpi','Gypi','Gcnot'],
                                                                    geometry='grid')
print_implicit_model_blocks(mdl_locnoise3)

Crosstalk-free models

A crosstalk free model is essentially the same as a local noise model - it's a model where each gate can be represented as a process matrix which only acts nontrivially on the gate's designated target qubits.) The construction routines we've seen so far construct models with perfect gates. The build_crosstalk_free_model function constructs a noisy LocalNoiseModel whose per-gate noise can be described in a simple way via a dictionary of gate errors. Here are the types of noise you can specify via the elements of this error dictionary:

  1. a single floating point number specifies that a gate should have a given depolarization rate.
  2. a tuple of floating point numbers specifies (potentially) anisotropic Pauli stochastic noise.
  3. a dictionary specifies the error rates associated with different standard error generators.

Here's an example that illustrates the above three ways:

In [ ]:
n_qubits = 2
cf_mdl = pygsti.construction.build_crosstalk_free_model(
            n_qubits, ('Gx','Gy','Gcnot'), 
            {'Gx': 0.1,  # float => single depolarization rate
             'Gy': (0.02,0.02,0.02), # tuple of floats => pauli stochastic rates 
             'Gcnot': {('H','ZZ'): 0.01, ('S','IX'): 0.01} #error generators
            })

The first two error types should be pretty self-explanatory. The third, "error generators", needs a bit more explanation: Some common types of gate noise can be represented in terms of an error generator. If $G$ is a noisy gate (a CPTP map) and $G_0$ is it's ideal counterpart, then if we write $G = e^{\Lambda}G_0$ then $\Lambda$ is called the gate's error generator. LindbladOp objects in pyGSTi represent this type of gate decomposition. If we write $\Lambda$ as a sum of terms, $\Lambda = \sum_i \alpha_i F_i$ then, when the $F_i$ are specific generators for well-known errors (e.g. rotations or stochastic errors), the $\alpha_i$ can roughly be interpreted as the error rates corresponding to the well-known error types. PyGSTi has three specific generator types (where $P_i$ is a Pauli operator or tensor product of Pauli operators):

  • Hamiltonian: $F_i = H_i$ where $H_i : \rho \rightarrow -i[P_i,\rho]$
  • Stochastic: $F_i = S_i$ where $S_i : \rho \rightarrow P_i \rho P_i - \rho$
  • Affine: $F_i = A_i$ where $A_i : \rho \rightarrow \mathrm{Tr}(\rho_{target})P_i \otimes \rho_{non-target}$

When we have an implicit model whose gates are LindbladOp operators, then we can specify the errors on the gates via a dictionary of the $\alpha_i$ coefficients. The dictionary given as a value of the error_rates argument of build_crosstalk_free_model lists the $\alpha_i$ using the syntax (<type>,<basis_element>), where "H", "S", and "A" are used to designate the Hamiltonian, stochastic, and affine types, and strings of I, X, Y, and Z can be used to label a Pauli basis element. See the Lindblad operator section of the tutorial on operators for more details.

More options

The above example creates a crosstalk free model whereby each gate, regardless of which qubit(s) it acts on, has the same noise. To override this behavior for a specific set of target qubits, you can include elements in the error dictionary which include qubit labels (e.g. ('Gx',0) below). To add noise to the $n$-qubit idle, state preparation, or measurement operations, you can also specify 'idle', 'prep', and 'povm' as keys in the error dictionary. The below cell illustrates this:

In [ ]:
cf_mdl2 = pygsti.construction.build_crosstalk_free_model(
    n_qubits, ('Gx','Gy','Gcnot'), 
    {'Gx': 0.1,  #depol
     ('Gx',0): 0.2, #more depolarization on qubit 0
     'Gy': (0.02,0.02,0.02), # pauli stochastic 
     'Gcnot': {('H','ZZ'): 0.01, ('S','IX'): 0.01}, # error generator rates 
     'idle': 0.01, 'prep': 0.01, 'povm': 0.01 # depolarization on the idle, prep, and measurement
    })

For convenience, you can use shorthand to describe the basis-elements for the error generator rates (using 'HZZ' instead of ('H','ZZ') for instance).

In [ ]:
cf_mdl3 = pygsti.construction.build_crosstalk_free_model(
    n_qubits, ('Gx','Gy','Gcnot'), 
    {'Gx': 0.1,  #depol
     ('Gx',0): 0.2, #more depolarization on qubit 0
     'Gy': (0.02,0.02,0.02), # pauli stochastic 
     'Gcnot': {'HZZ': 0.01, 'SIX': 0.01}, # error generator rates 
     'idle': 0.01, 'prep': 0.01, 'povm': 0.01 # depolarization on the idle, prep, and measurement
    })

Construction by appending

The build_crosstalk_free_model demonstrated above allows you to build a wide variety of local noise models fairly easily and quickly - but what if you need something just a little different? By setting ensure_composed_gates=True, all of the output gates (the elements of .operation_blks['gates']) will be composed gates - i.e. ComposedOp objects. This is nice because composed operations allow you to easily tag on additional operations - to add additional elements to whatever is being composed. Here's an example of how to create a noiseless crosstalk-free model and then add an arbitrary additional error term to both the Gx and Gy gates:

In [ ]:
mdl = pygsti.construction.build_crosstalk_free_model(n_qubits, ('Gi','Gx','Gy','Gcnot'), {}, ensure_composed_gates=True, independent_gates=False)

additional_error = pygsti.obj.TPDenseOp(np.identity(4,'d')) # this could be *any* operator

#ComposedOp objects support .append( operation )
mdl.operation_blks['gates']['Gx'].append(additional_error)
mdl.operation_blks['gates']['Gy'].append(additional_error)

This has advantages over simply setting gates to numpy arrays after using parameterization="full" (as demonstrated above) because you're adding a custom operation object, which can posess a custom parameterization.

Cloud-noise implicit models

Note: cloud-noise models are an advanced feature in pyGSTi, and as such this portion of the tutorial is less complete and more confusing than our tutorials on other topics

CloudNnoiseModel objects are designed to represent gates whose imperfections affect only the qubits in a neighborhood, or cloud, around a gate's target qubits. This notion of a gate's cloud is fairly flexible, but typically defined as the set of qubits that can be reached by some number ($k$, say) of edge traversals (or hops) from the gate's target qubits along a globally given connectivity graph.

For instance, if the graph specifies four qubits in a line: $0-1-2-3$ and we allow at most 1 hop along the graph, then the noise cloud of a single-qubit gate on qubit $1$ is the set of qubits $\{0,1,2\}$ and the cloud for a two-qubit gate on qubits $1$ and $2$ is the set $\{0,1,2,3\}$.

A CloudNoiseModel also contains a single (noisy) "global" or "background" idle operation that is intended to specify noise that affects all the qubits during the time of a circuit layer regardless of whether they participate in any gates. This noisy idle operation (acting on all the qubits) and the noise on each gate (acting on the gate's cloud) is taken to have the form $\exp{\mathcal{L}}$, where $\mathcal{L}$ is a Lindbladian which contains error terms only up to some maximum weight (typically 1 or 2, so we call this a "low weight" approximation or constraint). Thus, a CloudNoiseModel describes noise that is geometrically-local and low-weight but not strictly local (i.e. crosstalk-free) as a LocalNoiseModel does.

Each circuit layer is modeled as the global idle composed with gate operation(s) corresponding to that gate(s) in the layer. Thus, the noise from the global idle and from the gate cloud(s) must be combined when constructing a layer operation. By default this is done by simply composing the different error maps. The errcomp_type argument, however, can change this behavior so that the Lindbladian error generators are composed instead of the maps. When errcomp_type="gates" the noise maps for the components are of a gate layer are composed; when errcomp_type="errorgens" the error generators of the noise maps are added and used as the error generator for the final operation (this is an advanced topic which isn't covered in this tutorial yet).

We can create a CloudNnoiseModel using the build_standard_cloudnoise_model function which resembles build_standard_localnoise_model but contains some extra arguments dealing with cloud construction and the maximum error weights used:

  • maxhops specifies how many hops from a gate's target qubits (along the qubit graph given by the geometry argument ,which defaults to "line") describe which qubits comprise the gate's cloud.
  • max_idle_weight specifies the maximum-weight of error terms in the global idle operation.
  • max_spam_weight specifies the maximum-weight of error terms in the state preparation and measurement operations.
  • extra_gate_weight specifies the maximum-weight of error terms in gates' clouds relative to the number of target qubits of the gate. For instance, if extra_gate_weight=0 then 1-qubit gates can have up to weight-1 error terms in their clouds and 2-qubit gates can have up to weight-2 error terms. If extra_gate_weight=1 then this changes to weight-2 errors for 1Q gates and weight-3 errors for 2Q gates.
  • extra_weight_1_hops specifies an additional number of hops (added to maxhops) that applies only to weight-1 error terms. For example, in a 8-qubit line example, if maxhops=1, extra_gate_weight=0, and extra_weight_1_hops=1 then a 2-qubit gate on qubits $4$ and $5$ can have up-to-weight-2 errors on qubits $\{3,4,5,6\}$ and additionally weight-1 errors on qubits $2$ and $7$.
  • errcomp_type specifes how errors are composed when creating layer operations. An advanced topic that we don't explore here.

That's a lot to take in, so let's look at a concrete example. Here's how to create a cloud noise model on a 4-qubit line:

In [ ]:
import pygsti
mdl_cloudnoise = pygsti.construction.build_cloudnoise_model_from_hops_and_weights(
     n_qubits=4, gate_names=['Gxpi','Gypi','Gcnot'],
     availability={'Gcnot': [(0,1),(1,2),(2,3)]},
     max_idle_weight=1, max_spam_weight=1, maxhops=1,
     extra_weight_1_hops=0, extra_gate_weight=0)
print_implicit_model_blocks(mdl_cloudnoise)

We see that a CloudNoiseModel has three operation categories: "gates", "layers", and "cloudnoise". The first two serve a similar function as in a LocalNoiseModel, and hold the (1- and 2-qubit) gate operations and the (4-qubit) layer operations, respectively. The "cloudnoise" category contains layer operations corresponding to the "cloud-noise" associated with each primitive layer, i.e. each single-gate layer. The "layers" category contains the special "globalIdle" operation (described above) and perfect layer operations for each primitive layer. Let's take a look at the structure of some of these operations:

In [ ]:
print(mdl_cloudnoise.operation_blks['gates']['Gxpi']) # just a static 1-qubit operator
print(mdl_cloudnoise.operation_blks['layers'][('Gxpi',0)]) # perfect layer operator: 1Q Gxpi gate on qubit 0
print(mdl_cloudnoise.operation_blks['layers']['globalIdle']) # composition of wt-1 error terms on each (of 4) qubits
print(mdl_cloudnoise.operation_blks['cloudnoise'][('Gxpi',0)]) # wt-1 error terms on "cloud of Gxpi:0" == qubits 0 & 1
print(mdl_cloudnoise.operation_blks['cloudnoise'][('Gxpi',1)]) # wt-1 error tersm on "cloud of Gxpi:1" == qubits 0,1,2

We can simultate the same circuit as above using our (currently noise-free) cloud noise model:

In [ ]:
print(c)
mdl_cloudnoise.probs(c)

Now let's add some noise to our model. Whereas in a LocalNoiseModel one typically inserts noise by modifying the operations in the "gates" category (after making them non-static gates), in a CloudNoiseModel one should modify the "globalIdle" layer or the "cloudnoise" operations. Here, we'll add some noise (make the error-term coefficients nonzero) to the portion global idle which acts on the first qubit "Q0" (see the printed structure of "globalIdle above). For details on what exactly is going on here, checkout the Operators tutorial.

In [ ]:
# parameters of mdl_cloudnoise.operation_blks['layers']['globalIdle'].factorops[0].embedded_op
# are the coefficients of HX, HY, HZ error terms, then the squares of the coefficients of SX, SY, SZ error terms.
noisevec = np.array([0,0,0,np.sqrt(0.1),np.sqrt(0.1),np.sqrt(0.1)])
mdl_cloudnoise.operation_blks['layers']['globalIdle'].factorops[0].embedded_op.from_vector(noisevec)

#Print out the coefficients of the error terms, to make sure we did what we wanted:
errs = mdl_cloudnoise.operation_blks['layers']['globalIdle'].factorops[0].embedded_op.get_errgen_coeffs()
for err,val in errs.items():
    print(":".join(map(str,err)),"=",val)

Now let's calculate the probabilities using the noisy model. The resulting probabilities show the affect of depolarization on the first qubit over each (all 4) circuit layers:

In [ ]:
mdl_cloudnoise.probs(c)

If we compress the circuit down to depth 1, then there's only a single layer and thus just a single "globalIdle" affects the outcome probabilities:

In [ ]:
mdl_cloudnoise.probs(c2)

Cloud-crosstalk models

CloudNoiseModel objects can capture crosstalk errors - errors which violate either the locality of a gate (if it operates non-trivially on non-target qubits) or its independence from its "environment" (the other gates it appears with in a circuit layer). The construction routines we've seen so far construct models with perfect gates. The build_cloud_crosstalk_model function constructs a noisy CloudNoiseModel whose noise can be described in a (relatively) simple way via a dictionary of errors. The error_rates argument is a dictionary whose values are themselves dictionaries of error-generator rates (similar to what is allowed in crosstalk-free model construction) and whose keys describe which gate the noise is applied to and upon which qubits the noise is "centered".

Here's an example that illustrates this:

In [ ]:
n_qubits = 2
cc_mdl1 = pygsti.construction.build_cloud_crosstalk_model(
            n_qubits, ('Gx','Gy','Gcnot'), 
            { ('Gx',0): { ('H','X'): 0.01, ('S','XY:0,1'): 0.01},
              ('Gcnot',0,1): { ('H','ZZ'): 0.02, ('S','XX:0,1'): 0.02 },
              'idle': { ('S','XX:0,1'): 0.01 },
              'prep': { ('S','XX:0,1'): 0.01 },
              'povm': { ('S','XX:0,1'): 0.01 }
            })

Notice that the keys of the error dictionary give specific qubits, e.g. ('Gx',0), but that the basis elements given in the corresponding dictionary of error-generator rates by contain other/additional qubits (e.g. 'XY:0,1', which is the 2-qubit Pauli operator that acts as $X$ on qubit 0 and $Y$ on qubit 1). Furthermore, note that when a basis element does not have any qubit specification then the target qubit(s) of the current gate is/are assumed (e.g. the 'X' in ('H','X') for key ('Gx',0) is the $X$ Pauli on qubit 0).

Stencils

You might be thinking that this makes it difficult to describe "cloud" noise because you'd need to describe the action of the Gx gate, for instance, on each qubit separately. You can certainly do this, but "stencils" have been developed precisely for this purpose.

A QubitGraph object may have included within it the notion of one or more directions. For example, the built-in (and default) linear chain graph has two directions, "left" and "right". When creating a cloud-crosstalk model, you can specify as a key in the error_rates argument just a simple gate name, e.g. "Gx", and use stencil notation to describe the noise for all ('Gx',#) operations. There are two aspects of this notation:

  • it allows the use of @<targetIndex> to specify one of the operation's target qubits (e.g. for ('Gcnot',0,1), @0 would evaluate to 0 and @1 would evaluate to 1 whereas for ('Gcnot',3,2), @0 would evaluate to 3 and @1 would evaluate to 2)
  • you can follow a @<targetIndex> specifier with any number of +<direction> directives. For example, on a linear-chain graph, you could specify a qubit index by @0+left+left to mean the qubit that is two qubits to the left of the first target qubit. If a qubit doesn't exist in that location (e.g. if you've hit the end of the chain) then the specified error rate is ignored.

Here's a simple example:

In [ ]:
cc_mdl2 = pygsti.construction.build_cloud_crosstalk_model(
            n_qubits, ('Gx','Gy','Gcnot'), 
           { 'Gx': { ('H','X'): 0.01, ('S','X:@0+left'): 0.01, ('S','XX:@0,@0+right'): 0.02},
             'Gcnot': { ('H','ZZ'): 0.02, ('S','XX:@1+right,@0+left'): 0.02 },
             'idle': { ('S','XX:0,1'): 0.01 }
           })

Basis-element notation can also be abbreviated for convenience (this builds the same model a above):

In [ ]:
cc_mdl3 = pygsti.construction.build_cloud_crosstalk_model(
           n_qubits, ('Gx','Gy','Gcnot'), 
           { 'Gx': { 'HX': 0.01, 'SX:@0+left': 0.01, ('SXX:@0,@0+right'): 0.02},
             'Gcnot': { 'HZZ': 0.02, 'SXX:@1+right,@0+left': 0.02 },
             'idle': { 'SXX:0,1': 0.01 }
           })

Additional resources

Getting a list of the gate names recognized by pyGSTi:

In [ ]:
known_gate_names = list(pygsti.tools.internalgates.get_standard_gatename_unitaries().keys())
print(known_gate_names)