{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Essential Objects: Circuits, Models, and DataSets\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import pygsti\n",
"from pygsti.objects import Circuit, Model, DataSet"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Circuits\n",
"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.\n",
"\n",
"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\".\n",
"\n",
"A `Circuit` can be created from a list of gate labels:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"c = Circuit( [('Gx',0),('Gcnot',0,1),(),('Gy',3)], line_labels=[0,1,2,3])\n",
"print(c)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"If you want multiple gates in a single layer, just put those gate labels in their own nested list:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"c = Circuit( [('Gx',0),[('Gcnot',0,1),('Gy',3)],()] , line_labels=[0,1,2,3])\n",
"print(c)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"c = Circuit( ['rho',('Gz',1),[('Gswap',0,1),('Gy',2)],'Mz'] , line_labels=[0,1,2])\n",
"print(c)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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:"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"c = Circuit( ['Gx','Gy','Gi'] )\n",
"print(c)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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).\n",
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Models\n",
"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\"). \n",
"\n",
"#### Explicit layer-operation models\n",
"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 `build_explicit_model` function: "
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"mdl = pygsti.construction.build_explicit_model((0,1),\n",
" [(), ('Gx',0), ('Gy',0), ('Gx',1), ('Gy',1), ('Gcnot',0,1)],\n",
" [\"I(0,1)\",\"X(pi/2,0)\", \"Y(pi/2,0)\", \"X(pi/2,1)\", \"Y(pi/2,1)\", \"CNOT(0,1)\"]) "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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): "
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"print(\"Preparations: \", ', '.join(map(str,mdl.preps.keys())))\n",
"print(\"Measurements: \", ', '.join(map(str,mdl.povms.keys())))\n",
"print(\"Layer Ops: \", ', '.join(map(str,mdl.operations.keys())))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now use this model to do what models were made to do: compute the outcome probabilities of circuits."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"c = Circuit( [('Gx',0),('Gcnot',0,1),('Gy',1)] , line_labels=[0,1])\n",
"print(c)\n",
"mdl.probs(c) # Compute the outcome probabilities of circuit `c`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"An `ExplictOpModel` only \"knows\" how to operate on circuit layers it explicitly contains in its dictionaries,\n",
"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)]`:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"c = Circuit( [[('Gx',0),('Gx',1)],('Gy',1)] , line_labels=[0,1])\n",
"print(c)\n",
"\n",
"try: \n",
" p = mdl.probs(c)\n",
"except KeyError as e:\n",
" print(\"!!KeyError: missing\",str(e))\n",
" \n",
" #Create an operation for two parallel X-gates & rerun (now it works!)\n",
" mdl.operations[ [('Gx',0),('Gx',1)] ] = np.dot(mdl.operations[('Gx',0)], mdl.operations[('Gx',1)])\n",
" p = mdl.probs(c)\n",
" \n",
"print(\"Probability_of_outcome(00) = \", p['00']) # p is like a dictionary of outcomes"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"mdl.probs((('Gx',0),('Gcnot',0,1)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Implicit layer-operation models\n",
"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. \n",
""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Data Sets\n",
"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:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"dataset_txt = \\\n",
"\"\"\"## Columns = 00 count, 01 count, 10 count, 11 count\n",
"{} 100 0 0 0\n",
"Gx:0 55 5 40 0\n",
"Gx:0Gy:1 20 27 23 30\n",
"Gx:0^4 85 3 10 2\n",
"Gx:0Gcnot:0:1 45 1 4 50\n",
"[Gx:0Gx:1]Gy:0 25 32 17 26\n",
"\"\"\"\n",
"with open(\"tutorial_files/Example_Short_Dataset.txt\",\"w\") as f:\n",
" f.write(dataset_txt)\n",
"ds = pygsti.io.load_dataset(\"tutorial_files/Example_Short_Dataset.txt\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The second is by simulating a `Model` and thereby generating \"fake data\". This essentially calls `mdl.probs(c)` for each circuit in a given list, and samples from the output probability distribution to obtain outcome counts:"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"circuit_list = pygsti.construction.circuit_list([ (), \n",
" (('Gx',0),),\n",
" (('Gx',0),('Gy',1)),\n",
" (('Gx',0),)*4,\n",
" (('Gx',0),('Gcnot',0,1)),\n",
" ((('Gx',0),('Gx',1)),('Gy',0)) ], line_labels=(0,1))\n",
"ds_fake = pygsti.construction.generate_fake_data(mdl, circuit_list, nSamples=100,\n",
" sampleError='multinomial', seed=8675309)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Outcome counts are accessible by indexing a `DataSet` as if it were a dictionary with `Circuit` keys:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"c = Circuit( (('Gx',0),('Gy',1)), line_labels=(0,1) )\n",
"print(ds[c]) # index using a Circuit\n",
"print(ds[ [('Gx',0),('Gy',1)] ]) # or with something that can be converted to a Circuit "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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:"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
"row = ds[c]\n",
"row['00'] # this is ok\n",
"for outlbl, cnt in row.counts.items(): # Note: `row` doesn't have .items(), need \".counts\"\n",
" print(outlbl, cnt)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Another thing to note is that `DataSet` objects are \"sparse\" in that 0-counts are not typically stored:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
"c = Circuit([('Gx',0)], line_labels=(0,1))\n",
"print(\"No 01 or 11 outcomes here: \",ds_fake[c])\n",
"for outlbl, cnt in ds_fake[c].counts.items():\n",
" print(\"Item: \",outlbl, cnt) # Note: this loop never loops over 01 or 11!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You can manipulate `DataSets` in a variety of ways, including:\n",
"- adding and removing rows\n",
"- \"trucating\" a `DataSet` to include only a subset of it's string\n",
"- \"filtering\" a $n$-qubit `DataSet` to a $m < n$-qubit dataset\n",
"\n",
"To find out more about these and other operations, see our [data set tutorial](objects/DataSet.ipynb)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## What's next?\n",
"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:\n",
"- [Circuit](objects/Circuit.ipynb) - how to build circuits ([GST circuits](objects/advanced/GSTCircuitConstruction.ipynb) in particular)\n",
"- [ExplicitModel](objects/ExplicitModel.ipynb) - constructing explicit layer-operation models\n",
"- [ImplicitModel](objects/ImplicitModel.ipynb) - constructing implicit layer-operation models\n",
"- [DataSet](objects/DataSet.ipynb) - constructing data sets ([timestamped data](objects/advanced/TimestampedDataSets.ipynb) in particular)\n",
"- [Basis](objects/advanced/MatrixBases.ipynb) - defining matrix and vector bases\n",
"- [Results](objects/advanced/Results.ipynb) - the container object for model-based results\n",
"- [ProcessorSpec](objects/advanced/ProcessorSpec.ipynb) - represents a QIP as a collection of models and meta information. \n",
"- [Instrument](objects/advanced/Instruments.ipynb) - allows for circuits with intermediate measurements\n",
"- [Operation Factories](objects/advanced/OperationFactories.ipynb) - allows continuously parameterized gates"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.0"
}
},
"nbformat": 4,
"nbformat_minor": 2
}