{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Essential Objects\n", "This tutorial covers several object types that are foundational to much of what pyGSTi does: [circuits](#circuits), [processor specifications](#pspecs), [models](#models), and [data sets](#datasets). Our objective is to explain what these objects are and how they relate to one another at a high level while providing links to other notebooks that cover details we skip over here." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pygsti\n", "from pygsti.circuits import Circuit\n", "from pygsti.models import Model\n", "from pygsti.data import 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": null, "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": null, "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": null, "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": null, "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": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Processor Specifications\n", "A processor specification describes the interface that a quantum processor exposes to the outside world. Actual quantum processors often have a \"native\" interface associated with them, but can also be viewed as implementing various other derived interfaces. For example, while a 1-qubit quantum processor may natively implement the $X(\\pi/2)$ and $Z(\\pi/2)$ gates, it can also implement the set of all 1-qubit Clifford gates. Both of these interfaces would correspond to a processor specification in pyGSTi.\n", "\n", "Currently pyGSTi only supports processor specifications having an integral number of qubits. The `QubitProcessorSpec` object describes the number of qubits and what gates are available on them. For example," ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pspec = pygsti.processors.QubitProcessorSpec(num_qubits=2, gate_names=['Gxpi2', 'Gypi2', 'Gcnot'],\n", " geometry=\"line\")\n", "print(\"Qubit labels are\", pspec.qubit_labels)\n", "print(\"X(pi/2) gates on qubits: \", pspec.resolved_availability('Gxpi2'))\n", "print(\"CNOT gates on qubits: \", pspec.resolved_availability('Gcnot'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "creates a processor specification for a 2-qubits with $X(\\pi/2)$, $Y(\\pi/2)$, and CNOT gates. Setting the geometry to `\"line\"` causes 1-qubit gates to be available on each qubit and the CNOT between the two qubits (in either control/target direction). Processor specifications are used to build experiment designs and models, and so defining or importing an appropriate processor specification is often the first step in many analyses. To learn more about processor specification objects, see the [processor specification tutorial](objects/ProcessorSpec.ipynb)." ] }, { "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` (subclasses of `Model`) 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 processor specification above via the `create_explicit_model` function: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mdl = pygsti.models.create_explicit_model(pspec)" ] }, { "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": null, "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": null, "metadata": {}, "outputs": [], "source": [ "c = Circuit( [('Gxpi2',0),('Gcnot',0,1),('Gypi2',1)] , line_labels=[0,1])\n", "print(c)\n", "mdl.probabilities(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 = `[('Gxpi2',0),('Gxpi2',1)]`) cannot be used with our model until we explicitly associate an operation with the layer-label `[('Gxpi2',0),('Gxpi2',1)]`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "c = Circuit( [[('Gxpi2',0),('Gxpi2',1)],('Gxpi2',1)] , line_labels=[0,1])\n", "print(c)\n", "\n", "try: \n", " p = mdl.probabilities(c)\n", "except KeyError as e:\n", " print(\"!!KeyError: \",str(e))\n", " \n", " #Create an operation for two parallel X-gates & rerun (now it works!)\n", " mdl.operations[ [('Gxpi2',0),('Gxpi2',1)] ] = np.dot(mdl.operations[('Gxpi2',0)].to_dense(),\n", " mdl.operations[('Gxpi2',1)].to_dense())\n", " p = mdl.probabilities(c)\n", " \n", "print(\"Probability_of_outcome(00) = \", p['00']) # p is like a dictionary of outcomes" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mdl.probabilities((('Gxpi2',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": null, "metadata": {}, "outputs": [], "source": [ "dataset_txt = \\\n", "\"\"\"## Columns = 00 count, 01 count, 10 count, 11 count\n", "{} 100 0 0 0\n", "Gxpi2:0 55 5 40 0\n", "Gxpi2:0Gypi2:1 20 27 23 30\n", "Gxpi2:0^4 85 3 10 2\n", "Gxpi2:0Gcnot:0:1 45 1 4 50\n", "[Gxpi2:0Gxpi2:1]Gypi2: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.read_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.probabilities(c)` for each circuit in a given list, and samples from the output probability distribution to obtain outcome counts:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "circuit_list = pygsti.circuits.to_circuits([ (), \n", " (('Gxpi2',0),),\n", " (('Gxpi2',0),('Gypi2',1)),\n", " (('Gxpi2',0),)*4,\n", " (('Gxpi2',0),('Gcnot',0,1)),\n", " ((('Gxpi2',0),('Gxpi2',1)),('Gxpi2',0)) ], line_labels=(0,1))\n", "ds_fake = pygsti.data.simulate_data(mdl, circuit_list, num_samples=100,\n", " sample_error='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": null, "metadata": {}, "outputs": [], "source": [ "c = Circuit( (('Gxpi2',0),('Gypi2',1)), line_labels=(0,1) )\n", "print(ds[c]) # index using a Circuit\n", "print(ds[ [('Gxpi2',0),('Gypi2',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": null, "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": null, "metadata": {}, "outputs": [], "source": [ "c = Circuit([('Gxpi2',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", "- [QubitProcessorSpec](objects/advanced/QubitProcessorSpec.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.8.5" } }, "nbformat": 4, "nbformat_minor": 2 }