#!/usr/bin/env python # coding: utf-8 # # Essential Objects: Circuits, Models, and DataSets # Three object types form the foundational of all that pyGSTi does: [circuits](#circuits), [models](#models), and [data sets](#datasets). 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](objects/advanced/Instruments.ipynb). 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](objects/Circuit.ipynb). # # ## 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](objects/ExplicitModel.ipynb). 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](objects/ImplicitModel.ipynb) 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) 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, 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](objects/advanced/TimestampedDataSets.ipynb), 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](objects/DataSet.ipynb). # ## What's next? # 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](02-Applications.ipynb). Alternatively, if you're interested in learning more about the above-described or other objects, here are some links to relevant tutorials: # - [Circuit](objects/Circuit.ipynb) - how to build circuits ([GST circuits](objects/advanced/GSTCircuitConstruction.ipynb) in particular) # - [ExplicitModel](objects/ExplicitModel.ipynb) - constructing explicit layer-operation models # - [ImplicitModel](objects/ImplicitModel.ipynb) - constructing implicit layer-operation models # - [DataSet](objects/DataSet.ipynb) - constructing data sets ([timestamped data](objects/advanced/TimestampedDataSets.ipynb) in particular) # - [Basis](objects/advanced/MatrixBases.ipynb) - defining matrix and vector bases # - [Results](objects/advanced/Results.ipynb) - the container object for model-based results # - [ProcessorSpec](objects/advanced/ProcessorSpec.ipynb) - represents a QIP as a collection of models and meta information. # - [Instrument](objects/advanced/Instruments.ipynb) - allows for circuits with intermediate measurements # - [Operation Factories](objects/advanced/OperationFactories.ipynb) - allows continuously parameterized gates # In[ ]: