# Essential Objects: Circuits, Models, and DataSets¶

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


## Circuits¶

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.

## Models¶

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").

#### Explicit layer-operation models¶

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


#### Implicit layer-operation models¶

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.

## Data Sets¶

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)


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, num_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:

• "trucating" a DataSet to include only a subset of it's string
• "filtering" a $n$-qubit DataSet to a $m < n$-qubit dataset