{ "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": [ { "name": "stdout", "output_type": "stream", "text": [ "Qubit 0 ---|Gx|-|C1|-| |-| |---\n", "Qubit 1 ---| |-|T0|-| |-| |---\n", "Qubit 2 ---| |-| |-| |-| |---\n", "Qubit 3 ---| |-| |-| |-|Gy|---\n", "\n" ] } ], "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": [ { "name": "stdout", "output_type": "stream", "text": [ "Qubit 0 ---|Gx|-|C1|-| |---\n", "Qubit 1 ---| |-|T0|-| |---\n", "Qubit 2 ---| |-| |-| |---\n", "Qubit 3 ---| |-|Gy|-| |---\n", "\n" ] } ], "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": [ { "name": "stdout", "output_type": "stream", "text": [ "Qubit 0 ---|rho|-| |-|Gswap:0:1|-|Mz|---\n", "Qubit 1 ---|rho|-|Gz|-|Gswap:0:1|-|Mz|---\n", "Qubit 2 ---|rho|-| |-| Gy |-|Mz|---\n", "\n" ] } ], "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": [ { "name": "stdout", "output_type": "stream", "text": [ "Qubit * ---|Gx|-|Gy|-|Gi|---\n", "\n" ] } ], "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": [ { "name": "stdout", "output_type": "stream", "text": [ "Preparations: rho0\n", "Measurements: Mdefault\n", "Layer Ops: [], Gx:0, Gy:0, Gx:1, Gy:1, Gcnot:0:1\n" ] } ], "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": [ { "name": "stdout", "output_type": "stream", "text": [ "Qubit 0 ---|Gx|-|C1|-| |---\n", "Qubit 1 ---| |-|T0|-|Gy|---\n", "\n" ] }, { "data": { "text/plain": [ "OutcomeLabelDict([(('00',), 0.25000000000000033),\n", " (('01',), 0.25000000000000006),\n", " (('10',), 0.25),\n", " (('11',), 0.25000000000000006)])" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "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": [ { "name": "stdout", "output_type": "stream", "text": [ "Qubit 0 ---|Gx|-| |---\n", "Qubit 1 ---|Gx|-|Gy|---\n", "\n", "!!KeyError: missing Label[[Gx:0Gx:1]]\n", "Probability_of_outcome(00) = 0.2500000000000002\n" ] } ], "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": [ { "data": { "text/plain": [ "OutcomeLabelDict([(('00',), 0.5000000000000004),\n", " (('01',), 0.0),\n", " (('10',), 0.0),\n", " (('11',), 0.5)])" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "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": [ { "name": "stdout", "output_type": "stream", "text": [ "Loading tutorial_files/Example_Dataset.txt: 100%\n" ] } ], "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_Dataset.txt\",\"w\") as f:\n", " f.write(dataset_txt)\n", "ds = pygsti.io.load_dataset(\"tutorial_files/Example_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": [ { "name": "stdout", "output_type": "stream", "text": [ "{('00',): 20.0, ('01',): 27.0, ('10',): 23.0, ('11',): 30.0}\n", "{('00',): 20.0, ('01',): 27.0, ('10',): 23.0, ('11',): 30.0}\n" ] } ], "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": [ { "name": "stdout", "output_type": "stream", "text": [ "('00',) 20.0\n", "('01',) 27.0\n", "('10',) 23.0\n", "('11',) 30.0\n" ] } ], "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": [ { "name": "stdout", "output_type": "stream", "text": [ "No 01 or 11 outcomes here: {('00',): 50.0, ('10',): 50.0}\n", "Item: ('00',) 50.0\n", "Item: ('10',) 50.0\n" ] } ], "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" ] }, { "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 }