This tutorial will show you how to create and use `ExplicitOpModel`

objects. `Model`

objects are fundamental to pyGSTi, as each represents a set of quantum gates along with state preparation and measurement (i.e. POVM) operations. In pyGSTi, a "state space" refers to a Hilbert space of *pure* quantum states (often thought of as length-$d$ vectors, where $d=2^N$ for $N$ qubits). A "density matrix space" refers to a Hilbert space of density matrices, which while often thought of as $d \times d$ matrices can also be represented by length $d^2$ vectors. Mathematically, these vectors live in Hilbert-Schmidt space, the space of linear operators on the original $d\times d$ density matrix space. pyGSTi uses the "Liouville" vector-representation for density matrices and POVM effects, since this allows quantum gates to be represented by $d^2 \times d^2$ matrices which act on Hilbert-Schmidt vectors.

`ExplicitOpModel`

objects are the simplest type of `Model`

objects in pyGSTi. They have the look and feel of Python dictionaries which hold $d^2\times d^2$ operation matrices, length-$d^2$ state preparation vectors, and sets of length-$d^2$ effect vectors which encode positive operator value measures (POVMs). State preparation and POVM effect vectors are both generically referred to as "SPAM" (state preparation and measurement) vectors.

In [ ]:

```
import pygsti
import pygsti.construction as pc
```

Before getting to `ExplicitOpModels`

in particular, lets explain two quantites that *all* `Model`

objects posess: a *basis* and *state space labels*:

- A model's
`.state_space_labels`

member (a`StateSpaceLabels`

object) describes the model's state space as the direct sum and tensor product of labelled*factors*. Typically, this is just a tensor product of one or more 2-dimensional qubit spaces labelled by the integers 0 to $N_{qubits}-1$ or`"Q0"`

,`"Q1"`

, etc. We specify a 1-qubit state space using`["Q0"]`

below (the "Q" tells pyGSTi it's a 2-dimensional*qubit*space). If you had two qubits you could use`["Q0","Q1"]`

or`[0,1]`

to describe the tensor product of two qubit spaces, as pyGSTi assumes integer labels stand for qubit spaces too. To learn more about the`StateSpaceLabels`

object, see the state space labelling tutorial. - A model's
`.basis`

member (a`Basis`

object) describes how any dense representations (matrices or vectors) of the the operations in a`Model`

should be interpreted. We'll be using the "Pauli product" basis, which is named`"pp"`

in pyGSTi and consists of the tensor products of Pauli matrices (since our example has just a 1-qubit state space the`"pp"`

basis is just the 4 Pauli matrices $\{\sigma_0,\sigma_X,\sigma_Y,\sigma_Z\}$). To learn more about`Basis`

objects see the Basis object tutorial).

There are more or less four ways to create `ExpicitOpModel`

objects in pyGSTi:

- By creating an empty
`ExpicitOpModel`

and setting its elements directly, possibly with the help of`pygsti.construction`

's`build_operation`

and`build_vector`

functions. - By a single call to
`create_explicit_model`

, which automates the above approach. - By loading from a text-format model file using
`pygsti.io.load_model`

. - By copying one from one of the
`pygsti.construction.std*`

modules.

`ExplicitOpModel`

from scratch¶Layer operations (often called "gates" in a 1- or even 2-qubit context) and SPAM vectors can be assigned to a `ExplicitOpModel`

object as to an ordinary python dictionary. Internally a `ExpicitOpModel`

holds these quantities as `LinearOperator`

- and `SPAMVec`

- and `POVM`

-derived objects, but you may assign lists, Numpy arrays, or other types of Python iterables to a `ExplicitOpModel`

key and a conversion will be performed automatically. To keep gates, state preparations, and POVMs separate, the `ExplicitOpModel`

object looks at the beginning of the dictionary key being assigned: keys beginning with `rho`

, `M`

, and `G`

are categorized as state preparations, POVMs, and gates, respectively. To avoid ambiguity, each key *must* begin with one of these three prefixes.

To separately access (set or get) the state preparations, POVMs, and operations contained in a `ExplicitOpModel`

use the `preps`

, `povms`

, and `operations`

members respectively. Each one provides dictionary-like access to the underlying objects. For example, `myModel.operations['Gx']`

accesses the same underlying `LinearOperator`

object as `myModel['Gx']`

, and similarly for `myModel.preps['rho0']`

and `myModel['rho0']`

. The values of operations and state preparation vectors can be read and written in this way.

A POVM object acts similarly to dictionary of `SPAMVec`

-derived effect vectors, but typically requires all such vectors to be initialized at once, that is, you cannot assign individual effect vectors to a `POVM`

. The string-valued keys of a `POVM`

label the outcome associated with each effect vector, and are therefore termed *effect labels* or *outcome labels*. The outcome labels also designate data within a `DataSet`

object (see the DataSet tutorial), and thereby associate modeled POVMs with experimental measurements.

The below cell illustrates how to create a `ExplicitOpModel`

from scratch.

In [ ]:

```
from math import sqrt
import numpy as np
#Initialize an empty Model object
#Designate the basis being used for the matrices and vectors below
# as the "Pauli product" basis of dimension 2 - i.e. the four 2x2 Pauli matrices I,X,Y,Z
model1 = pygsti.objects.ExplicitOpModel(['Q0'],'pp')
#Populate the Model object with states, effects, gates,
# all in the *normalized* Pauli basis: { I/sqrt(2), X/sqrt(2), Y/sqrt(2), Z/sqrt(2) }
# where I, X, Y, and Z are the standard Pauli matrices.
model1['rho0'] = [ 1/sqrt(2), 0, 0, 1/sqrt(2) ] # density matrix [[1, 0], [0, 0]] in Pauli basis
model1['Mdefault'] = pygsti.objects.UnconstrainedPOVM(
{'0': [ 1/sqrt(2), 0, 0, 1/sqrt(2) ], # projector onto [[1, 0], [0, 0]] in Pauli basis
'1': [ 1/sqrt(2), 0, 0, -1/sqrt(2) ] }) # projector onto [[0, 0], [0, 1]] in Pauli basis
model1['Gi'] = np.identity(4,'d') # 4x4 identity matrix
model1['Gx'] = [[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0,-1],
[0, 0, 1, 0]] # pi/2 X-rotation in Pauli basis
model1['Gy'] = [[1, 0, 0, 0],
[0, 0, 0, 1],
[0, 0, 1, 0],
[0,-1, 0, 0]] # pi/2 Y-rotation in Pauli basis
pygsti.io.write_model(model1, "../tutorial_files/Example_gatesetFromScratch.txt", title="My Model")
```

Check out the model file that was written here.

`ExplicitOpModel`

from scratch using `modelconstruction._basis_create_operation`

and `modelconstruction._basis_create_spam_vector`

¶The `modelconstruction._basis_create_operation`

and `modelconstruction._basis_create_spam_vector`

functions take a human-readable string representation of a gate or SPAM vector, and return a `LinearOperator`

or `SPAMVector`

object that gets stored in a dictionary-like `ExplicitOpModel`

or `POVM`

object. To use these functions, you must specify what state space you're working with, and the basis for that space - so the `.state_space_labels`

and `.basis`

member of your `Model`

object, as described above.

`build_vector`

currently only understands strings which are integers (e.g. "1"), for which it creates a vector performing state preparation of (or, equivalently, a state projection onto) the $i^{th}$ state of the Hilbert space, that is, the state corresponding to the $i^{th}$ row and column of the $d\times d$ density matrix.

`build_operation`

accepts a wider range of descriptor strings, which take the form of *functionName*(*args*) and include:

`I(label0, label1, ...)`

: the identity on the spaces labeled by`label0`

,`label1`

, etc.`X(theta,Qlabel)`

,`Y(theta,Qlabel)`

,`Z(theta,Qlabel)`

: single qubit X-, Y-, and Z-axis rotations by angle`theta`

(in radians) on the qubit labeled by`Qlabel`

. Note that`pi`

can be used within an expression for`theta`

, e.g.`X(pi/2,Q0)`

.`CX(theta, Qlabel1, Qlabel2)`

,`CY(theta, Qlabel1, Qlabel2)`

,`CZ(theta, Qlabel1, Qlabel2)`

: two-qubit controlled rotations by angle`theta`

(in radians) on qubits`Qlabel1`

(the control) and`Qlabel2`

(the target).

In [ ]:

```
#Initialize an empty Model object
model2 = pygsti.objects.ExplicitOpModel(['Q0'],'pp') # single qubit labelled 'Q0'; Pauli basis
spaceLabels = model2.state_space_labels
basis = model2.basis
#Populate the Model object with states, effects, and gates using
# build_vector, build_operation, and create_identity_vec.
model2['rho0'] = pc.modelconstruction._basis_create_spam_vector("0",basis)
model2['Mdefault'] = pygsti.objects.UnconstrainedPOVM(
{ '0': pc.modelconstruction._basis_create_spam_vector("0",basis),
'1': pc.modelconstruction._basis_create_spam_vector("1",basis) } )
model2['Gi'] = pc.modelconstruction._basis_create_operation(spaceLabels,"I(Q0)",basis)
model2['Gx'] = pc.modelconstruction._basis_create_operation(spaceLabels,"X(pi/2,Q0)",basis)
model2['Gy'] = pc.modelconstruction._basis_create_operation(spaceLabels,"Y(pi/2,Q0)",basis)
```

`ExplicitOpModel`

in a single call to create_explicit_model¶The approach illustrated above using calls to `build_vector`

and `build_operation`

can be performed in a single call to `create_explicit_model`

. You will notice that all of the arguments to `create_explicit_model`

corrspond to those used to construct a model using `build_vector`

and `build_operation`

; the `create_explicit_model`

function is merely a convenience function which allows you to specify everything at once. These arguments are:

- Arg 1 : the state-space-labels, as described above.
- Args 2 & 3 : list-of-gate-labels, list-of-gate-expressions (labels
*must*begin with 'G'; "expressions" being the descriptor strings passed to`build_operation`

) - Args 4 & 5 : list-of-prep-labels, list-of-prep-expressions (labels
*must*begin with 'rho'; "expressions" being the descriptor strings passed to`build_vector`

) - Args 6 & 7 : list-of-effect-labels, list-of-effect-expressions (labels can be anything; "expressions" being the descriptor strings passed to
`build_vector`

). These effect vectors will comprise a single POVM named`"Mdefault"`

by default, but which can be changed via the`povmLabels`

argument (see doc string for details).

The optional argument `basis`

can be set to any of the known built-in basis *names* (e.g. `"gm"`

, `"pp"`

, `"qt"`

, or `"std"`

) to select the basis for the Model as described above. By default, `"pp"`

is used when possible (if the state space corresponds to an integer number of qubits), `"qt"`

if the state space has dimension 3, and `"gm"`

otherwise. The optional argument `parameterization`

is used to specify the parameterization used for the created gates (see below).

In [ ]:

```
model3 = pc.create_explicit_model(['Q0'],
['Gi','Gx','Gy'], [ "I(Q0)","X(pi/2,Q0)", "Y(pi/2,Q0)"],
prep_labels=['rho0'], prep_expressions=["0"],
effect_labels=['0','1'], effect_expressions=["0","1"] )
```

In this case, the parameters to `create_explicit_model`

, specify:

The state space has dimension 2 and is interpreted as that of a single qubit labeled "Q0" (label must begin with 'Q' or be an integer if we don't want to create a full

`StateSpaceLabels`

object that contains the dimensions too.)there are three gates: Idle, $\pi/2$ x-rotation, $\pi/2$ y-rotation, labeled

`Gi`

,`Gx`

, and`Gy`

.there is one state prep operation, labeled

`rho0`

, which prepares the 0-state (the first basis element of the 2D state space)there is one POVM (~ measurement), named

`Mdefault`

with two effect vectors:`'0'`

projects onto the 0-state (the first basis element of the 2D state space) and`'1'`

projects onto the 1-state.

Note that **by default**, there is a single state prep, `"rho0"`

, that prepares the 0-state and a single POVM, `"Mdefault"`

, which consists of projectors onto each standard basis state that are labelled by their integer indices (so just `'0'`

and `'1'`

in the case of 1-qubit). Thus, all but the first four arguments used above just specify the default behavior and can be omitted:

In [ ]:

```
model4 = pc.create_explicit_model( ['Q0'], ['Gi','Gx','Gy'], [ "I(Q0)","X(pi/2,Q0)", "Y(pi/2,Q0)"] )
```

`ExplicitOpModel`

from a file¶You can also construct a `ExplicitOpModel`

object from a file using `pygsti.io.load_model`

. The format of the text file should be fairly self-evident given the above discussion. Note that vector and matrix elements need not be simple numbers, but can be any mathematical expression parseable by the Python interpreter, and in addition to numbers can include "sqrt" and "pi".

In [ ]:

```
#3) Write a text-format model file and read it in.
model5_txt = \
"""
# Example text file describing a model
PREP: rho0
LiouvilleVec
1/sqrt(2) 0 0 1/sqrt(2)
POVM: Mdefault
EFFECT: 0
LiouvilleVec
1/sqrt(2) 0 0 1/sqrt(2)
EFFECT: 1
LiouvilleVec
1/sqrt(2) 0 0 -1/sqrt(2)
END POVM
GATE: Gi
LiouvilleMx
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
GATE: Gx
LiouvilleMx
1 0 0 0
0 1 0 0
0 0 0 -1
0 0 1 0
GATE: Gy
LiouvilleMx
1 0 0 0
0 0 0 1
0 0 1 0
0 -1 0 0
STATESPACE: Q0(4)
BASIS: pp
"""
with open("../tutorial_files/Example_ExplicitModel.txt","w") as gsetfile:
gsetfile.write(model5_txt)
model5 = pygsti.io.load_model("../tutorial_files/Example_ExplicitModel.txt")
```

In [ ]:

```
#All four of the above models are identical. See this by taking the frobenius differences between them:
assert(model1.frobeniusdist(model2) < 1e-8)
assert(model1.frobeniusdist(model3) < 1e-8)
assert(model1.frobeniusdist(model4) < 1e-8)
assert(model1.frobeniusdist(model5) < 1e-8)
```

In the cells below, we demonstrate how to print and access information within a `ExplicitOpModel`

.

In [ ]:

```
#Printing the contents of a Model is easy
print("Model 1:\n", model1)
```

In [ ]:

```
#You can also access individual gates like they're numpy arrays:
Gx = model1['Gx'] # a LinearOperator object, but behaves like a numpy array
#By printing a gate, you can see that it's not just a numpy array
print("Gx = ", Gx)
#But can be accessed as one:
print("Array-like printout\n", Gx[:,:],"\n")
print("First row\n", Gx[0,:],"\n")
print("Element [2,3] = ",Gx[2,3], "\n")
Id = np.identity(4,'d')
Id_dot_Gx = np.dot(Id,Gx)
print("Id_dot_Gx\n", Id_dot_Gx, "\n")
```

`ExplicitOpModel`

objects have a number of methods that support a variety of operations, including:

- Depolarizing or rotating every gate
- Writing the model to a file
- Computing products of operation matrices
- Printing more information about the model

In [ ]:

```
#Add 10% depolarization noise to the gates
depol_model3 = model3.depolarize(op_noise=0.1)
#Add a Y-axis rotation uniformly to all the gates
rot_model3 = model3.rotate(rotate=(0,0.1,0))
```

In [ ]:

```
#Writing a model as a text file
pygsti.io.write_model(depol_model3, "../tutorial_files/Example_depolarizedModel.txt", title="My Model")
```

In [ ]:

```
print("Probabilities of outcomes of the gate\n sequence GxGx (rho0 and Mdefault assumed)= ",
depol_model3.probabilities( ("Gx", "Gx")))
print("Probabilities of outcomes of the \"complete\" gate\n sequence rho0+GxGx+Mdefault = ",
depol_model3.probabilities( ("rho0", "Gx", "Gx", "Mdefault")))
```

In [ ]:

```
#Printing more detailed information about a model
depol_model3.print_info()
```

It is also possible to manipulate the underlying operations by accessing the forward simulator in the `Model`

class. For example, using the `matrix`

simulator type, one can compute the product of two gate operations. For more details, see the ForwardSimulationTypes tutorial.

In [ ]:

```
# Computing the product of operation matrices (only allowed with the matrix simulator type)
print("Product of Gx * Gx = \n",depol_model3.sim.product(("Gx", "Gx")), end='\n\n')
```

In addition to specifying a set of $d^2 \times d^2$ operation matrices and length-$d^2$ SPAM vectors, every `Model`

encapsulates a *parametrization*, that is, a function mapping a set of real-valued *parameters* to its set of operation matrices and SPAM vectors. A `Model`

's contents must always correspond to a valid set of parameters, which can be obtained by its `to_vector`

method, and can always be initialized from a vector of parameters via its `from_vector`

method. The number of parameters (obtained via `num_params`

) is independent (and need not equal!) the total number of gate-matrix and SPAM-vector elements comprising the `Model`

. For example, in a "TP-parameterized" model, the first row of each operation matrix is fixed at `[1,0,...0]`

, regardless to what the `Model`

's underlying parameters are. When pyGSTi generates `Model`

estimates the *parameters* of an initial `Model`

(often times the "target" model) supplied by the caller are optimized. Thus, by its parameterization a single `Model`

can determine the space of possible `Model`

s that are searched for a best-fit estimate.

Each gate and SPAM vector within a `ExplicitOpModel`

have independent paramterizations, so that each `pygsti.objects.LinearOperator`

-derived gate object and `pygsti.objects.SPAMVec`

-derived SPAM vector has its own `to_vector`

, `from_vector`

, and `num_params`

method. A `Model`

's parameter vector is just the concatenation of the parameter vectors of its contents, in the order: 1) state preparation vectors, 2) measurement vectors, 3) gates.

Users are able to create their own gate parameterizations by deriving from `pygsti.objects.LinearOperator`

or `pygsti.objects.DenseOperator`

(which itself derives from `LinearOperator`

). Included in pyGSTi are several convenient gate parameterizations which are worth knowing about:

- The
`FullDenseOp`

class defines a gate which has a parameter for every element, and thus optimizations using this gate class allow the operation matrix to be completely arbitrary. - The
`TPDenseOp`

class defines a gate whose first row must be`[1,0,...0]`

. This corresponds to a trace-preserving (TP) gate in the Gell-Mann and Pauli-product bases. Each element in the remaining rows is a separate parameter, similar to a fully parameterized gate. Optimizations using this gate type are used to constrain the estimated gate to being trace preserving. - The
`LindbladOp`

(or`LindbladDenseOp`

) class defines a gate whose logarithm take a particular Lindblad form. This class is fairly flexible, but is predominantly used to constrain optimizations to the set of infinitesimally-generated CPTP maps.

Similarly, there are `FullSPAMVec`

and `TPSPAMVec`

classes, the latter which fixes its first element to $\sqrt{d}$, where $d^2$ is the vector length, as this is the appropriate value for a unit-trace state preparation.

We now illustrate how one map specify the type of paramterization in `create_explicit_model`

, and change the parameterizations of all of a `ExplicitOpModel`

's contents using its `set_all_parameterizaions`

method.

In [ ]:

```
# Speciy basis as 'gm' for Gell-Mann (could also be 'pp' for Pauli-Product)
# and parameterization to 'TP', so that gates are TPParameterizedGates
model6 = pc.create_explicit_model(['Q0'], ['Gi',], [ "I(Q0)"],
basis='pp', parameterization="TP")
#See that gates and prep vectors are TP, whereas previous Model's have
# fully parameterized elements
print("model6 gate type = ", type(model6['Gi']))
print("model6 prep type = ", type(model6['rho0']))
print("model5 gate type = ", type(model5['Gi']))
print("model5 prep type = ", type(model5['rho0']))
#Switch parameterization to CPTP gates
model6.set_all_parameterizations('CPTP')
print("\nAfter setting all parameterizations to CPTP:")
print("model6 gate type = ", type(model6['Gi']))
print("model6 prep type = ", type(model6['rho0']))
```

To alter an individual gate or SPAM vector's parameterization, one can simply construct a `LinearOperator`

or `SPAMVec`

object with the desired parameterization and assign it to the `Model`

.

In [ ]:

```
newOp = pygsti.objects.TPDenseOp(model6['Gi'])
model6['Gi'] = newOp
print("model6['Gi'] =",model6['Gi'])
```

**NOTE:** When a `LinearOperator`

or `SPAMVec`

-derived object is assigned as an element of an `ExplicitOpModel`

(as above), the object *replaces* any existing object with the given key. However, if any other type of object is assigned to an `ExplicitOpModel`

element, an attempt is made to initialize or update the existing existing gate using the assigned data (using its `set_matrix`

function internally). For example:

In [ ]:

```
import numpy as np
numpy_array = np.array( [[1, 0, 0, 0],
[0, 0.5, 0, 0],
[0, 0, 0.5, 0],
[0, 0, 0, 0.5]], 'd')
model6['Gi'] = numpy_array # after assignment with a numpy array...
print("model6['Gi'] =",model6['Gi']) # this is STILL a TPDenseOp object
#If you try to assign a gate to something that is either invalid or it doesn't know how
# to deal with, it will raise an exception
invalid_TP_array = np.array( [[2, 1, 3, 0],
[0, 0.5, 0, 0],
[0, 0, 0.5, 0],
[0, 0, 0, 0.5]], 'd')
try:
model6['Gi'] = invalid_TP_array
except ValueError as e:
print("ERROR!! " + str(e))
```

In [ ]:

```
```