Three object types form the foundational of all that pyGSTi does: circuits, models, and data sets. This tutorial's objective is to explain what these objects are and how they relate to one another at a very high level while providing links to other notebooks that cover the details we gloss over here.

In [ ]:

```
import pygsti
from pygsti.objects import Circuit, Model, DataSet
```

The `Circuit`

object encapsulates a quantum circuit as a sequence of *layers*, each of which contains zero or more non-identity *gates*. A `Circuit`

has some number of labeled *lines* and each gate label is assigned to one or more lines. Line labels can be integers or strings. Gate labels have two parts: a `str`

-type name and a tuple of line labels. A gate name typically begins with 'G' because this is expected when we parse circuits from text files.

For example, `('Gx',0)`

is a gate label that means "do the Gx gate on qubit 0", and `('Gcnot',(2,3))`

means "do the Gcnot gate on qubits 2 and 3".

A `Circuit`

can be created from a list of gate labels:

In [ ]:

```
c = Circuit( [('Gx',0),('Gcnot',0,1),(),('Gy',3)], line_labels=[0,1,2,3])
print(c)
```

If you want multiple gates in a single layer, just put those gate labels in their own nested list:

In [ ]:

```
c = Circuit( [('Gx',0),[('Gcnot',0,1),('Gy',3)],()] , line_labels=[0,1,2,3])
print(c)
```

We distinguish three basic types of circuit layers. We call layers containing quantum gates *operation layers*. All the circuits we've seen so far just have operation layers. It's also possible to have a *preparation layer* at the beginning of a circuit and a *measurement layer* at the end of a circuit. There can also be a fourth type of layer called an *instrument layer* which we dicuss in a separate tutorial on Instruments. Assuming that `'rho'`

labels a (n-qubit) state preparation and `'Mz'`

labels a (n-qubit) measurement, here's a circuit with all three types of layers:

In [ ]:

```
c = Circuit( ['rho',('Gz',1),[('Gswap',0,1),('Gy',2)],'Mz'] , line_labels=[0,1,2])
print(c)
```

Finally, when dealing with small systems (e.g. 1 or 2 qubits), we typically just use a `str`

-type label (without any line-labels) to denote every possible layer. In this case, all the labels operate on the entire state space so we don't need the notion of 'lines' in a `Circuit`

. When there are no line-labels, a `Circuit`

assumes a single default **'*'-label**, which you can usually just ignore:

In [ ]:

```
c = Circuit( ['Gx','Gy','Gi'] )
print(c)
```

Pretty simple, right? The `Circuit`

object allows you to easily manipulate its labels (similar to a NumPy array) and even perform some basic operations like depth reduction and simple compiling. For lots more details on how to create, modify, and use circuit objects see the circuit tutorial.

An instance of the `Model`

class represents something that can predict the outcome probabilities of quantum circuits. We define any such thing to be a "QIP model", or just a "model", as these probabilities define the behavior of some real or virtual QIP. Because there are so many types of models, the `Model`

class in pyGSTi is just a base class and is never instaniated directly. Classes `ExplicitOpModel`

and `ImplicitOpModel`

derive from `Model`

and define two broad categories of models, both of which sequentially operate on circuit *layers* (the "Op" in the class names is short for "layer operation").

An `ExplicitOpModel`

is a container object. Its `.preps`

, `.povms`

, and `.operations`

members are essentially dictionaires of state preparation, measurement, and layer-operation objects, respectively. How to create these objects and build up explicit models from scratch is a central capability of pyGSTi and a topic of the explicit-model tutorial. Presently, we'll create a 2-qubit model using the convenient `create_explicit_model`

function:

In [ ]:

```
mdl = pygsti.construction.create_explicit_model((0,1),
[(), ('Gx',0), ('Gy',0), ('Gx',1), ('Gy',1), ('Gcnot',0,1)],
["I(0,1)","X(pi/2,0)", "Y(pi/2,0)", "X(pi/2,1)", "Y(pi/2,1)", "CNOT(0,1)"])
```

This creates an `ExplicitOpModel`

with a default preparation (prepares all qubits in the zero-state) labeled `'rho0'`

, a default measurement labeled `'Mdefault'`

in the Z-basis and with 5 layer-operations given by the labels in the 2nd argument (the first argument is akin to a circuit's line labels and the third argument contains special strings that the function understands):

In [ ]:

```
print("Preparations: ", ', '.join(map(str,mdl.preps.keys())))
print("Measurements: ", ', '.join(map(str,mdl.povms.keys())))
print("Layer Ops: ", ', '.join(map(str,mdl.operations.keys())))
```

We can now use this model to do what models were made to do: compute the outcome probabilities of circuits.

In [ ]:

```
c = Circuit( [('Gx',0),('Gcnot',0,1),('Gy',1)] , line_labels=[0,1])
print(c)
mdl.probabilities(c) # Compute the outcome probabilities of circuit `c`
```

An `ExplictOpModel`

only "knows" how to operate on circuit layers it explicitly contains in its dictionaries,
so, for example, a circuit layer with two X gates in parallel (layer-label = `[('Gx',0),('Gx',1)]`

) cannot be used with our model until we explicitly associate an operation with the layer-label `[('Gx',0),('Gx',1)]`

:

In [ ]:

```
import numpy as np
c = Circuit( [[('Gx',0),('Gx',1)],('Gy',1)] , line_labels=[0,1])
print(c)
try:
p = mdl.probabilities(c)
except KeyError as e:
print("!!KeyError: missing",str(e))
#Create an operation for two parallel X-gates & rerun (now it works!)
mdl.operations[ [('Gx',0),('Gx',1)] ] = np.dot(mdl.operations[('Gx',0)], mdl.operations[('Gx',1)])
p = mdl.probabilities(c)
print("Probability_of_outcome(00) = ", p['00']) # p is like a dictionary of outcomes
```

In [ ]:

```
mdl.probabilities((('Gx',0),('Gcnot',0,1)))
```

In the above example, you saw how it is possible to manually add a layer-operation to an `ExplicitOpModel`

based on its other, more primitive layer operations. This often works fine for a few qubits, but can quickly become tedious as the number of qubits increases (since the number of potential layers that involve a given set of gates grows exponentially with qubit number). This is where `ImplicitOpModel`

objects come into play: these models contain rules for building up arbitrary layer-operations based on more primitive operations. PyGSTi offers several "built-in" types of implicit models and a rich set of tools for building your own custom ones. See the tutorial on implicit models for details.

The `DataSet`

object is a container for tabulated outcome counts. It behaves like a dictionary whose keys are `Circuit`

objects and whose values are dictionaries that associate *outcome labels* with (usually) integer counts. There are two primary ways you go about getting a `DataSet`

. The first is by reading in a simply formatted text file:

In [ ]:

```
dataset_txt = \
"""## Columns = 00 count, 01 count, 10 count, 11 count
{} 100 0 0 0
Gx:0 55 5 40 0
Gx:0Gy:1 20 27 23 30
Gx:0^4 85 3 10 2
Gx:0Gcnot:0:1 45 1 4 50
[Gx:0Gx:1]Gy:0 25 32 17 26
"""
with open("tutorial_files/Example_Short_Dataset.txt","w") as f:
f.write(dataset_txt)
ds = pygsti.io.load_dataset("tutorial_files/Example_Short_Dataset.txt")
```

The second is by simulating a `Model`

and thereby generating "fake data". This essentially calls `mdl.probabilities(c)`

for each circuit in a given list, and samples from the output probability distribution to obtain outcome counts:

In [ ]:

```
circuit_list = pygsti.construction.to_circuits([ (),
(('Gx',0),),
(('Gx',0),('Gy',1)),
(('Gx',0),)*4,
(('Gx',0),('Gcnot',0,1)),
((('Gx',0),('Gx',1)),('Gy',0)) ], line_labels=(0,1))
ds_fake = pygsti.construction.simulate_data(mdl, circuit_list, n_samples=100,
sample_error='multinomial', seed=8675309)
```

Outcome counts are accessible by indexing a `DataSet`

as if it were a dictionary with `Circuit`

keys:

In [ ]:

```
c = Circuit( (('Gx',0),('Gy',1)), line_labels=(0,1) )
print(ds[c]) # index using a Circuit
print(ds[ [('Gx',0),('Gy',1)] ]) # or with something that can be converted to a Circuit
```

Because `DataSet`

object can also store *timestamped* data (see the time-dependent data tutorial, the values or "rows" of a `DataSet`

aren't simple dictionary objects. When you'd like a `dict`

of counts use the `.counts`

member of a data set row:

In [ ]:

```
row = ds[c]
row['00'] # this is ok
for outlbl, cnt in row.counts.items(): # Note: `row` doesn't have .items(), need ".counts"
print(outlbl, cnt)
```

Another thing to note is that `DataSet`

objects are "sparse" in that 0-counts are not typically stored:

In [ ]:

```
c = Circuit([('Gx',0)], line_labels=(0,1))
print("No 01 or 11 outcomes here: ",ds_fake[c])
for outlbl, cnt in ds_fake[c].counts.items():
print("Item: ",outlbl, cnt) # Note: this loop never loops over 01 or 11!
```

You can manipulate `DataSets`

in a variety of ways, including:

- adding and removing rows
- "trucating" a
`DataSet`

to include only a subset of it's string - "filtering" a $n$-qubit
`DataSet`

to a $m < n$-qubit dataset

To find out more about these and other operations, see our data set tutorial.

You've learned about the three main object types in pyGSTi! The next step is to learn about how these objects are used within pyGSTi, which is the topic of the next overview tutorial on applications. Alternatively, if you're interested in learning more about the above-described or other objects, here are some links to relevant tutorials:

- Circuit - how to build circuits (GST circuits in particular)
- ExplicitModel - constructing explicit layer-operation models
- ImplicitModel - constructing implicit layer-operation models
- DataSet - constructing data sets (timestamped data in particular)
- Basis - defining matrix and vector bases
- Results - the container object for model-based results
- ProcessorSpec - represents a QIP as a collection of models and meta information.
- Instrument - allows for circuits with intermediate measurements
- Operation Factories - allows continuously parameterized gates

In [ ]:

```
```