**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

- a rotation gate over-rotates that qubit it's

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

`.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()
```

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

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

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

.

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

Now the gates are `FullDenseOp`

objects (which can be modified as we please):

In [5]:

```
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 [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')
```

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

Out[7]:

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

Out[8]:

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

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

Out[11]:

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

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

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

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

In [15]:

```
print(c)
mdl_cloudnoise.probs(c)
```

Out[15]:

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

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

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

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

In [ ]:

```
```