Implicit Models

**Notice: this topic describes "beta level" functionality in pyGSTi!** It may contain bugs and holes in its implementation, which will be addressed in future releases.

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 [1]:
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 [2]:
mdl_locnoise = pygsti.construction.build_standard_localnoise_model(nQubits=4, gate_names=['Gxpi','Gypi','Gcnot'])
print(type(mdl_locnoise))
print_implicit_model_blocks(mdl_locnoise, showSPAM=True)
<class 'pygsti.objects.localnoisemodel.LocalNoiseModel'>
State prep building blocks (.prep_blks):
 layers :  rho0

POVM building blocks (.povm_blks):
 layers :  Mdefault

Operation building blocks (.operation_blks):
 layers :  Gxpi:0, Gxpi:1, Gxpi:2, Gxpi:3, Gypi:0, Gypi:1, Gypi:2, Gypi:3, Gcnot:0:1, Gcnot:1:0, Gcnot:1:2, Gcnot:2:1, Gcnot:2:3, Gcnot:3:2
 gates :  Gxpi, Gypi, Gcnot

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 [3]:
print(mdl_locnoise.operation_blks['gates']['Gxpi']) # Static!
StaticDenseOp with shape (4, 4)
 1.00   0   0   0
   0 1.00   0   0
   0   0-1.00   0
   0   0   0-1.00

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 [4]:
mdl_locnoise = pygsti.construction.build_standard_localnoise_model(nQubits=4, gate_names=['Gxpi','Gypi','Gcnot'],
                                                                  availability={'Gcnot': [(0,1),(1,2),(2,3)]},
                                                                  parameterization='full')
print_implicit_model_blocks(mdl_locnoise)
Operation building blocks (.operation_blks):
 layers :  Gxpi:0, Gxpi:1, Gxpi:2, Gxpi:3, Gypi:0, Gypi:1, Gypi:2, Gypi:3, Gcnot:0:1, Gcnot:1:2, Gcnot:2:3
 gates :  Gxpi, Gypi, Gcnot

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

In [5]:
print(mdl_locnoise.operation_blks['gates']['Gxpi'])
FullDenseOp with shape (4, 4)
 1.00   0   0   0
   0 1.00   0   0
   0   0-1.00   0
   0   0   0-1.00

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

In [6]:
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 [7]:
c = pygsti.obj.Circuit( [('Gxpi',0),('Gxpi',1),('Gxpi',2),('Gxpi',3)], num_lines=4)
print(c)
mdl_locnoise.probs(c)
Qubit 0 ---|Gxpi|-|    |-|    |-|    |---
Qubit 1 ---|    |-|Gxpi|-|    |-|    |---
Qubit 2 ---|    |-|    |-|Gxpi|-|    |---
Qubit 3 ---|    |-|    |-|    |-|Gxpi|---

Out[7]:
OutcomeLabelDict([(('0000',), 6.250000000020128e-06),
                  (('0001',), 0.0001187500000000008),
                  (('0010',), 0.00011874999999999386),
                  (('0011',), 0.0022562499999999874),
                  (('0100',), 0.00011874999999999386),
                  (('0101',), 0.0022562499999999874),
                  (('0110',), 0.0022562499999999874),
                  (('0111',), 0.042868749999999976),
                  (('1000',), 0.00011874999999999386),
                  (('1001',), 0.0022562500000000013),
                  (('1010',), 0.0022562499999999944),
                  (('1011',), 0.042868749999999976),
                  (('1100',), 0.0022562499999999944),
                  (('1101',), 0.042868749999999976),
                  (('1110',), 0.042868750000000004),
                  (('1111',), 0.8145062500000005)])

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 [8]:
c2 = c.parallelize()
print(c2)

mdl_locnoise.probs(c2)
Qubit 0 ---|Gxpi|---
Qubit 1 ---|Gxpi|---
Qubit 2 ---|Gxpi|---
Qubit 3 ---|Gxpi|---

Out[8]:
OutcomeLabelDict([(('0000',), 6.250000000020128e-06),
                  (('0001',), 0.0001187500000000008),
                  (('0010',), 0.00011874999999999386),
                  (('0011',), 0.0022562499999999874),
                  (('0100',), 0.00011874999999999386),
                  (('0101',), 0.0022562499999999874),
                  (('0110',), 0.0022562499999999874),
                  (('0111',), 0.042868749999999976),
                  (('1000',), 0.00011874999999999386),
                  (('1001',), 0.0022562500000000013),
                  (('1010',), 0.0022562499999999944),
                  (('1011',), 0.042868749999999976),
                  (('1100',), 0.0022562499999999944),
                  (('1101',), 0.042868749999999976),
                  (('1110',), 0.042868750000000004),
                  (('1111',), 0.8145062500000005)])

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 [9]:
mdl_locnoise2 = pygsti.construction.build_standard_localnoise_model(nQubits=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)
Operation building blocks (.operation_blks):
 layers :  Gxpi:0, Gxpi:1, Gxpi:2, Gxpi:3, Gypi:0, Gypi:1, Gypi:2, Gypi:3, Gcnot:0:1, Gcnot:1:2, Gcnot:2:3
 gates :  Gxpi:0, Gxpi:1, Gxpi:2, Gxpi:3, Gypi:0, Gypi:1, Gypi:2, Gypi:3, Gcnot:0:1, Gcnot:1:2, Gcnot:2:3

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 [10]:
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 [11]:
print(c)
mdl_locnoise2.probs(c)
Qubit 0 ---|Gxpi|-|    |-|    |-|    |---
Qubit 1 ---|    |-|Gxpi|-|    |-|    |---
Qubit 2 ---|    |-|    |-|Gxpi|-|    |---
Qubit 3 ---|    |-|    |-|    |-|Gxpi|---

Out[11]:
OutcomeLabelDict([(('0000',), 0.0),
                  (('0001',), 0.0),
                  (('0010',), 0.0),
                  (('0011',), -1.3877787807814457e-17),
                  (('0100',), 0.0),
                  (('0101',), 0.0),
                  (('0110',), 0.0),
                  (('0111',), 0.049999999999999926),
                  (('1000',), 0.0),
                  (('1001',), 0.0),
                  (('1010',), 0.0),
                  (('1011',), 1.3877787807814457e-17),
                  (('1100',), 0.0),
                  (('1101',), 0.0),
                  (('1110',), 0.0),
                  (('1111',), 0.9500000000000003)])

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: tutoral on graphs & example here

In [12]:
mdl_locnoise3 = pygsti.construction.build_standard_localnoise_model(nQubits=9, gate_names=['Gxpi','Gypi','Gcnot'],
                                                                    geometry='grid')
print_implicit_model_blocks(mdl_locnoise3)
Operation building blocks (.operation_blks):
 layers :  Gxpi:0, Gxpi:1, Gxpi:2, Gxpi:3, Gxpi:4, Gxpi:5, Gxpi:6, Gxpi:7, Gxpi:8, Gypi:0, Gypi:1, Gypi:2, Gypi:3, Gypi:4, Gypi:5, Gypi:6, Gypi:7, Gypi:8, Gcnot:0:1, Gcnot:0:3, Gcnot:1:0, Gcnot:1:2, Gcnot:1:4, Gcnot:2:1, Gcnot:2:5, Gcnot:3:0, Gcnot:3:4, Gcnot:3:6, Gcnot:4:1, Gcnot:4:3, Gcnot:4:5, Gcnot:4:7, Gcnot:5:2, Gcnot:5:4, Gcnot:5:8, Gcnot:6:3, Gcnot:6:7, Gcnot:7:4, Gcnot:7:6, Gcnot:7:8, Gcnot:8:5, Gcnot:8:7
 gates :  Gxpi, Gypi, Gcnot

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_types argument, however, can change this behavior so that the Lindbladian error generators are composed instead of the maps (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.
  • maxIdleWeight specifies the maximum-weight of error terms in the global idle operation.
  • maxSpamWeight specifies the maximum-weight of error terms in the state preparation and measurement operations.
  • extraGateWeight specifies the maximum-weight of error terms in gates' clouds relative to the number of target qubits of the gate. For instance, if extraGateWeight=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 extraGateWeight=1 then this changes to weight-2 errors for 1Q gates and weight-3 errors for 2Q gates.
  • extraWeight1Hops 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, extraGateWeight=0, and extraWeight1Hops=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 [13]:
import pygsti
mdl_cloudnoise = pygsti.construction.build_standard_cloudnoise_model(nQubits=4, gate_names=['Gxpi','Gypi','Gcnot'],
                                                                     availability={'Gcnot': [(0,1),(1,2),(2,3)]},
                                                                     maxIdleWeight=1, maxSpamWeight=1, maxhops=1,
                                                                     extraWeight1Hops=0, extraGateWeight=0)
print_implicit_model_blocks(mdl_cloudnoise)
Operation building blocks (.operation_blks):
 layers :  globalIdle, Gxpi:0, Gxpi:1, Gxpi:2, Gxpi:3, Gypi:0, Gypi:1, Gypi:2, Gypi:3, Gcnot:0:1, Gcnot:1:2, Gcnot:2:3
 gates :  Gxpi, Gypi, Gcnot
 cloudnoise :  Gxpi:0, Gxpi:1, Gxpi:2, Gxpi:3, Gypi:0, Gypi:1, Gypi:2, Gypi:3, Gcnot:0:1, Gcnot:1:2, Gcnot:2:3

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 [14]:
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
StaticDenseOp with shape (4, 4)
 1.00   0   0   0
   0 1.00   0   0
   0   0-1.00   0
   0   0   0-1.00

Embedded gate with full dimension 256 and state space 0(2)*1(2)*2(2)*3(2)
 that embeds the following 4-dimensional gate into acting on the [0] space
StaticDenseOp with shape (4, 4)
 1.00   0   0   0
   0 1.00   0   0
   0   0-1.00   0
   0   0   0-1.00

Composed gate of 4 factors:
Factor 0:
Embedded gate with full dimension 256 and state space Q0(2)*Q1(2)*Q2(2)*Q3(2)
 that embeds the following 4-dimensional gate into acting on the ['Q0'] space
Lindblad Parameterized gate map with dim = 4, num params = 6
Factor 1:
Embedded gate with full dimension 256 and state space Q0(2)*Q1(2)*Q2(2)*Q3(2)
 that embeds the following 4-dimensional gate into acting on the ['Q1'] space
Lindblad Parameterized gate map with dim = 4, num params = 6
Factor 2:
Embedded gate with full dimension 256 and state space Q0(2)*Q1(2)*Q2(2)*Q3(2)
 that embeds the following 4-dimensional gate into acting on the ['Q2'] space
Lindblad Parameterized gate map with dim = 4, num params = 6
Factor 3:
Embedded gate with full dimension 256 and state space Q0(2)*Q1(2)*Q2(2)*Q3(2)
 that embeds the following 4-dimensional gate into acting on the ['Q3'] space
Lindblad Parameterized gate map with dim = 4, num params = 6

Composed gate of 2 factors:
Factor 0:
Embedded gate with full dimension 256 and state space Q0(2)*Q1(2)*Q2(2)*Q3(2)
 that embeds the following 4-dimensional gate into acting on the ['Q0'] space
Lindblad Parameterized gate map with dim = 4, num params = 6
Factor 1:
Embedded gate with full dimension 256 and state space Q0(2)*Q1(2)*Q2(2)*Q3(2)
 that embeds the following 4-dimensional gate into acting on the ['Q1'] space
Lindblad Parameterized gate map with dim = 4, num params = 6

Composed gate of 3 factors:
Factor 0:
Embedded gate with full dimension 256 and state space Q0(2)*Q1(2)*Q2(2)*Q3(2)
 that embeds the following 4-dimensional gate into acting on the ['Q0'] space
Lindblad Parameterized gate map with dim = 4, num params = 6
Factor 1:
Embedded gate with full dimension 256 and state space Q0(2)*Q1(2)*Q2(2)*Q3(2)
 that embeds the following 4-dimensional gate into acting on the ['Q1'] space
Lindblad Parameterized gate map with dim = 4, num params = 6
Factor 2:
Embedded gate with full dimension 256 and state space Q0(2)*Q1(2)*Q2(2)*Q3(2)
 that embeds the following 4-dimensional gate into acting on the ['Q2'] space
Lindblad Parameterized gate map with dim = 4, num params = 6

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

In [15]:
print(c)
mdl_cloudnoise.probs(c)
Qubit 0 ---|Gxpi|-|    |-|    |-|    |---
Qubit 1 ---|    |-|Gxpi|-|    |-|    |---
Qubit 2 ---|    |-|    |-|Gxpi|-|    |---
Qubit 3 ---|    |-|    |-|    |-|Gxpi|---

Out[15]:
OutcomeLabelDict([(('0000',), 0.0),
                  (('0001',), 0.0),
                  (('0010',), 0.0),
                  (('0011',), 0.0),
                  (('0100',), 0.0),
                  (('0101',), 0.0),
                  (('0110',), -6.938893903907228e-18),
                  (('0111',), 9.922618282587337e-16),
                  (('1000',), 0.0),
                  (('1001',), 0.0),
                  (('1010',), 0.0),
                  (('1011',), 1.0061396160665481e-15),
                  (('1100',), 0.0),
                  (('1101',), 1.0061396160665481e-15),
                  (('1110',), 1.061650767297806e-15),
                  (('1111',), 0.9999999999999952)])

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 [16]:
# 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,basisDict = 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)
H:0 = 0.0
H:1 = 0.0
H:2 = 0.0
S:0 = 0.1
S:1 = 0.1
S:2 = 0.1

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 [17]:
mdl_cloudnoise.probs(c)
Out[17]:
OutcomeLabelDict([(('0000',), -1.0408340855860843e-17),
                  (('0001',), -3.469446951953614e-18),
                  (('0010',), -3.469446951953614e-18),
                  (('0011',), 2.671474153004283e-16),
                  (('0100',), 1.0408340855860843e-17),
                  (('0101',), 2.671474153004283e-16),
                  (('0110',), 2.671474153004283e-16),
                  (('0111',), 0.27533551794138833),
                  (('1000',), 1.0408340855860843e-17),
                  (('1001',), 3.469446951953614e-18),
                  (('1010',), 3.469446951953614e-18),
                  (('1011',), 7.320533068622126e-16),
                  (('1100',), 3.469446951953614e-18),
                  (('1101',), 7.181755190543981e-16),
                  (('1110',), 7.4593109467002705e-16),
                  (('1111',), 0.724664482058608)])

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 [18]:
mdl_cloudnoise.probs(c2)
Out[18]:
OutcomeLabelDict([(('0000',), 1.3877787807814457e-17),
                  (('0001',), 0.0),
                  (('0010',), 0.0),
                  (('0011',), 6.938893903907228e-17),
                  (('0100',), -6.938893903907228e-18),
                  (('0101',), 6.245004513516506e-17),
                  (('0110',), 7.632783294297951e-17),
                  (('0111',), 0.09063462346100905),
                  (('1000',), -1.3877787807814457e-17),
                  (('1001',), 0.0),
                  (('1010',), 0.0),
                  (('1011',), 4.3021142204224816e-16),
                  (('1100',), -1.3877787807814457e-17),
                  (('1101',), 4.3021142204224816e-16),
                  (('1110',), 4.579669976578771e-16),
                  (('1111',), 0.909365376538989)])

Additional resources

Getting a list of the gate names recognized by pyGSTi:

In [19]:
known_gate_names = list(pygsti.tools.internalgates.get_standard_gatename_unitaries().keys())
print(known_gate_names)
['Gi', 'Gx', 'Gxpi2', 'Gy', 'Gypi2', 'Gz', 'Gzpi2', 'Gxpi', 'Gypi', 'Gzpi', 'Gxmpi2', 'Gympi2', 'Gzmpi2', 'Gh', 'Gp', 'Gpdag', 'Gt', 'Gtdag', 'Gc0', 'Gc1', 'Gc2', 'Gc3', 'Gc4', 'Gc5', 'Gc6', 'Gc7', 'Gc8', 'Gc9', 'Gc10', 'Gc11', 'Gc12', 'Gc13', 'Gc14', 'Gc15', 'Gc16', 'Gc17', 'Gc18', 'Gc19', 'Gc20', 'Gc21', 'Gc22', 'Gc23', 'Gcphase', 'Gcnot', 'Gswap']
In [ ]: