{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Explicit Models Tutorial\n", "This tutorial will show you how to create and use `ExplicitOpModel` objects. `Model` objects are fundamental to pyGSTi, as each represents a set of quantum gates along with state preparation and measurement (i.e. POVM) operations. In pyGSTi, a \"state space\" refers to a Hilbert space of *pure* quantum states (often thought of as length-$d$ vectors, where $d=2^N$ for $N$ qubits). A \"density matrix space\" refers to a Hilbert space of density matrices, which while often thought of as $d \\times d$ matrices can also be represented by length $d^2$ vectors. Mathematically, these vectors live in Hilbert-Schmidt space, the space of linear operators on the original $d\\times d$ density matrix space. pyGSTi uses the \"Liouville\" vector-representation for density matrices and POVM effects, since this allows quantum gates to be represented by $d^2 \\times d^2$ matrices which act on Hilbert-Schmidt vectors.\n", "\n", "`ExplicitOpModel` objects are the simplest type of `Model` objects in pyGSTi. They have the look and feel of Python dictionaries which hold $d^2\\times d^2$ operation matrices, length-$d^2$ state preparation vectors, and sets of length-$d^2$ effect vectors which encode positive operator value measures (POVMs). State preparation and POVM effect vectors are both generically referred to as \"SPAM\" (state preparation and measurement) vectors. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pygsti" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creating models\n", "Before getting to `ExplicitOpModels` in particular, lets explain two quantites that *all* `Model` objects posess: a *basis* and *state space labels*:\n", "- A model's `.state_space` member (a `StateSpace` object) describes the model's state space as the direct sum and tensor product of labelled *factors*. Typically, this is just a tensor product of one or more 2-dimensional qubit spaces labelled by the integers 0 to $N_{qubits}-1$ or `\"Q0\"`, `\"Q1\"`, etc. We specify a 1-qubit state space using `[\"Q0\"]` below (the \"Q\" tells pyGSTi it's a 2-dimensional *qubit* space). If you had two qubits you could use `[\"Q0\",\"Q1\"]` or `[0,1]` to describe the tensor product of two qubit spaces, as pyGSTi assumes integer labels stand for qubit spaces too. To learn more about the `StateSpace` object, see the [state space tutorial](advanced/StateSpace.ipynb).\n", "- A model's `.basis` member (a `Basis` object) describes how any dense representations (matrices or vectors) of the the operations in a `Model` should be interpreted. We'll be using the \"Pauli product\" basis, which is named `\"pp\"` in pyGSTi and consists of the tensor products of Pauli matrices (since our example has just a 1-qubit state space the `\"pp\"` basis is just the 4 Pauli matrices $\\{\\sigma_0,\\sigma_X,\\sigma_Y,\\sigma_Z\\}$). To learn more about `Basis` objects see the [Basis object tutorial](advanced/MatrixBases.ipynb)).\n", "\n", "\n", "## Creating explicit models\n", "There are more or less four ways to create `ExpicitOpModel` objects in pyGSTi:\n", "\n", "* By creating an empty `ExpicitOpModel` and setting its elements directly.\n", "* By a single call to a `pygsti.models.modelconstruction` function, which automates the above approach.\n", "* By loading from a text-format model file using `pygsti.io.read_model` (see the [File IO tutorial](../other/FileIO.ipynb)).\n", "* By loading one from the `pygsti.modelpacks` module (see the [ModelPacks tutorial](advanced/ModelPacks.ipynb)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating a `ExplicitOpModel` from scratch\n", "\n", "Layer operations (often called \"gates\" in a 1- or even 2-qubit context) and SPAM vectors can be assigned to a `ExplicitOpModel` object as to an ordinary python dictionary. Internally a `ExpicitOpModel` holds these quantities as `LinearOperator`- and `SPAMVec`- and `POVM`-derived objects (all types of `ModelMember` objects from `pygsti.modelmembers`), but you may assign lists, Numpy arrays, or other types of Python iterables to a `ExplicitOpModel` key and a conversion will be performed automatically. To keep gates, state preparations, and POVMs separate, the `ExplicitOpModel` object looks at the beginning of the dictionary key being assigned: keys beginning with `rho`, `M`, and `G` are categorized as state preparations, POVMs, and gates, respectively. To avoid ambiguity, each key *must* begin with one of these three prefixes.\n", "\n", "To separately access (set or get) the state preparations, POVMs, and operations contained in a `ExplicitOpModel` use the `preps`, `povms`, and `operations` members respectively. Each one provides dictionary-like access to the underlying objects. For example, `myModel.operations['Gx']` accesses the same underlying `LinearOperator` object as `myModel['Gx']`, and similarly for `myModel.preps['rho0']` and `myModel['rho0']`. The values of operations and state preparation vectors can be read and written in this way. \n", "\n", "A POVM object acts similarly to dictionary of `SPAMVec`-derived effect vectors, but typically requires all such vectors to be initialized at once, that is, you cannot assign individual effect vectors to a `POVM`. The string-valued keys of a `POVM` label the outcome associated with each effect vector, and are therefore termed *effect labels* or *outcome labels*. The outcome labels also designate data within a `DataSet` object (see the [DataSet tutorial](DataSet.ipynb)), and thereby associate modeled POVMs with experimental measurements. \n", "\n", "\n", "\n", "The below cell illustrates how to create a `ExplicitOpModel` from scratch." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from math import sqrt\n", "import numpy as np\n", "\n", "import pygsti.modelmembers as mm\n", "\n", "#Initialize an empty Model object\n", "#Designate the basis being used for the matrices and vectors below \n", "# as the \"Pauli product\" basis of dimension 2 - i.e. the four 2x2 Pauli matrices I,X,Y,Z\n", "model1 = pygsti.models.ExplicitOpModel(['Q0'],'pp')\n", "\n", "#Populate the Model object with states, effects, gates,\n", "# all in the *normalized* Pauli basis: { I/sqrt(2), X/sqrt(2), Y/sqrt(2), Z/sqrt(2) }\n", "# where I, X, Y, and Z are the standard Pauli matrices.\n", "model1['rho0'] = [ 1/sqrt(2), 0, 0, 1/sqrt(2) ] # density matrix [[1, 0], [0, 0]] in Pauli basis\n", "model1['Mdefault'] = mm.povms.UnconstrainedPOVM(\n", " {'0': [ 1/sqrt(2), 0, 0, 1/sqrt(2) ], # projector onto [[1, 0], [0, 0]] in Pauli basis\n", " '1': [ 1/sqrt(2), 0, 0, -1/sqrt(2) ] },# projector onto [[0, 0], [0, 1]] in Pauli basis\n", " evotype='densitymx') # Specify the evolution type when initializing from NumPy arrays.\n", " # densitymx is the default\n", "\n", "model1['Gi'] = np.identity(4,'d') # 4x4 identity matrix\n", "model1['Gx'] = [[1, 0, 0, 0],\n", " [0, 1, 0, 0],\n", " [0, 0, 0,-1],\n", " [0, 0, 1, 0]] # pi/2 X-rotation in Pauli basis\n", "\n", "model1['Gy'] = [[1, 0, 0, 0],\n", " [0, 0, 0, 1],\n", " [0, 0, 1, 0],\n", " [0,-1, 0, 0]] # pi/2 Y-rotation in Pauli basis" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating a `ExplicitOpModel` from scratch using `modelconstruction.create_operation` and `modelconstruction.create_spam_vector`\n", "The `modelconstruction.create_operation` and `modelconstruction.create_spam_vector` functions take a human-readable string representation of a gate or SPAM vector, and return a `LinearOperator` or `SPAMVector` object that gets stored in a dictionary-like `ExplicitOpModel` or `POVM` object. To use these functions, you must specify what state space you're working with, and the basis for that space - so the `.state_space_labels` and `.basis` member of your `Model` object, as described above.\n", "\n", "`create_spam_vector` currently only understands strings which are integers (e.g. \"1\"), for which it creates a vector performing state preparation of (or, equivalently, a state projection onto) the $i^{th}$ state of the Hilbert space, that is, the state corresponding to the $i^{th}$ row and column of the $d\\times d$ density matrix.\n", "\n", "`create_operation` accepts a wider range of descriptor strings, which take the form of *functionName*(*args*) and include:\n", "- `I(label0, label1, ...)` : the identity on the spaces labeled by `label0`, `label1`, etc.\n", "- `X(theta,Qlabel)`, `Y(theta,Qlabel)`, `Z(theta,Qlabel)` : single qubit X-, Y-, and Z-axis rotations by angle `theta` (in radians) on the qubit labeled by `Qlabel`. Note that `pi` can be used within an expression for `theta`, e.g. `X(pi/2,Q0)`.\n", "- `CX(theta, Qlabel1, Qlabel2)`, `CY(theta, Qlabel1, Qlabel2)`, `CZ(theta, Qlabel1, Qlabel2)` : two-qubit controlled rotations by angle `theta` (in radians) on qubits `Qlabel1` (the control) and `Qlabel2` (the target)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Initialize an empty Model object\n", "model2 = pygsti.models.ExplicitOpModel(['Q0'],'pp') # single qubit labelled 'Q0'; Pauli basis\n", "statespace = model2.state_space\n", "basis = model2.basis\n", "\n", "from pygsti.models import modelconstruction as mc\n", "\n", "#Populate the Model object with states, effects, and gates using \n", "# build_vector, build_operation, and create_identity_vec. \n", "model2['rho0'] = mc.create_spam_vector(\"0\", statespace, basis)\n", "model2['Mdefault'] = mm.povms.UnconstrainedPOVM(\n", " { '0': mc.create_spam_vector(\"0\", statespace, basis),\n", " '1': mc.create_spam_vector(\"1\", statespace, basis) },\n", " evotype='densitymx')\n", "model2['Gi'] = mc.create_operation(\"I(Q0)\", statespace, basis)\n", "model2['Gx'] = mc.create_operation(\"X(pi/2,Q0)\", statespace, basis)\n", "model2['Gy'] = mc.create_operation(\"Y(pi/2,Q0)\", statespace, basis)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create a `ExplicitOpModel` in a single call to `create_explicit_model_from_expressions`\n", "The approach illustrated above using calls to `create_spam_vector` and `create_operation` can be performed in a single call to `create_explicit_model_from_expresssions`. You will notice that all of the arguments to `create_explicit_model_from_expressions` correspond to those used to construct a model using `create_spam_vector` and `create_operation`; the `create_explicit_model_from_expressions` function is merely a convenience function which allows you to specify everything at once. These arguments are:\n", "- Arg 1 : the state-space-labels, as described above.\n", "- Args 2 & 3 : list-of-gate-labels, list-of-gate-expressions (labels *must* begin with 'G'; \"expressions\" being the descriptor strings passed to `create_operation`)\n", "- Args 4 & 5 : list-of-prep-labels, list-of-prep-expressions (labels *must* begin with 'rho'; \"expressions\" being the descriptor strings passed to `create_spam_vector`)\n", "- Args 6 & 7 : list-of-effect-labels, list-of-effect-expressions (labels can be anything; \"expressions\" being the descriptor strings passed to `create_spam_vector`). These effect vectors will comprise a single POVM named `\"Mdefault\"` by default, but which can be changed via the `povmLabels` argument (see doc string for details).\n", "\n", "The optional argument `basis` can be set to any of the known built-in basis *names* (e.g. `\"gm\"`, `\"pp\"`, `\"qt\"`, or `\"std\"`) to select the basis for the Model as described above. By default, `\"pp\"` is used when possible (if the state space corresponds to an integer number of qubits), `\"qt\"` if the state space has dimension 3, and `\"gm\"` otherwise. Optional arguments `gate_type`, `prep_type`, and `povm_type` are used to specify the type of created gate, state, and POVM objects respectively (see below)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "model3 = mc.create_explicit_model_from_expressions(['Q0'],\n", " ['Gi','Gx','Gy'], [ \"I(Q0)\",\"X(pi/2,Q0)\", \"Y(pi/2,Q0)\"],\n", " prep_labels=['rho0'], prep_expressions=[\"0\"], \n", " effect_labels=['0','1'], effect_expressions=[\"0\",\"1\"] ) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this case, the parameters to `create_explicit_model_from_expressions`, specify:\n", "\n", " - The state space has dimension 2 and is interpreted as that of a single qubit labeled \"Q0\" (label must begin with 'Q' or be an integer if we don't want to create a full `StateSpace` object that contains the dimensions too.)\n", " \n", " - there are three gates: Idle, $\\pi/2$ x-rotation, $\\pi/2$ y-rotation, labeled `Gi`, `Gx`, and `Gy`.\n", " \n", " - there is one state prep operation, labeled `rho0`, which prepares the 0-state (the first basis element of the 2D state space)\n", " \n", " - there is one POVM (~ measurement), named `Mdefault` with two effect vectors: `'0'` projects onto the 0-state (the first basis element of the 2D state space) and `'1'` projects onto the 1-state.\n", " \n", "Note that **by default**, there is a single state prep, `\"rho0\"`, that prepares the 0-state and a single POVM, `\"Mdefault\"`, which consists of projectors onto each standard basis state that are labelled by their integer indices (so just `'0'` and `'1'` in the case of 1-qubit). Thus, all but the first four arguments used above just specify the default behavior and can be omitted:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "model4 = mc.create_explicit_model_from_expressions( ['Q0'],\n", " ['Gi','Gx','Gy'], [ \"I(Q0)\",\"X(pi/2,Q0)\", \"Y(pi/2,Q0)\"] )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The arguments generalize straightforwardly to multiple qubits, though explicit models become impractical (due to the large amount of memory required) with more than two or three qubits. Here's a 2-qubit example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "model4_2qubit = pygsti.models.create_explicit_model_from_expressions((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": [ "## Create a `ExplicitOpModel` in a single call to `create_explicit_model`\n", "\n", "While `create_explicit_model_from_expressions` allows the user to define gates based on X, Y, and Z rotations, it is often the case that a set of \"standard\" gates want to be used. Some of these standard gates are defined by PyGSTi (such as X/Y/Z or $\\sqrt{X/Y/Z}$) and can be used directly without specifying their explicit expression. The construction routine for using these standard names is `create_explicit_model`, which takes its information from a `QubitProcessorSpec`.\n", "\n", "A `QubitProcessorSpec` is an object that contains information for some experimental device, including:\n", "- Number of qubits\n", "- Names of standard gates\n", "- Qubit labels and topology/availability of gates.\n", "To learn more, see the [processor specification tutorial](ProcessorSpec.ipynb). For our purposes here, it is sufficient to view this as a container for qubit/gate information, and is a common input to most model construction routines." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pygsti.processors import QubitProcessorSpec\n", "\n", "pspec = QubitProcessorSpec(1, ['Gi', 'Gxpi2', 'Gypi2'], qubit_labels=['Q0']) # single qubit with idle, X(pi/2), and Y(pi/2) gates\n", "\n", "model5 = mc.create_explicit_model(pspec)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# There is one difference with this function - the gate labels are automatically generated\n", "# based on the base gate name and qubit labels.\n", "# To exactly match the other models, we can rename the operations\n", "model5.operations['Gi'] = model5.operations['Gi', 'Q0']\n", "model5.operations['Gx'] = model5.operations['Gxpi2', 'Q0']\n", "model5.operations['Gy'] = model5.operations['Gypi2', 'Q0']\n", "del model5.operations['Gi', 'Q0']\n", "del model5.operations['Gxpi2', 'Q0']\n", "del model5.operations['Gypi2', 'Q0']" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#All six of the above models are identical. See this by taking the frobenius differences between them:\n", "assert(model1.frobeniusdist(model2) < 1e-8)\n", "assert(model1.frobeniusdist(model3) < 1e-8)\n", "assert(model1.frobeniusdist(model4) < 1e-8)\n", "assert(model1.frobeniusdist(model5) < 1e-8)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Viewing models\n", "Next, we demonstrate how to print and access information within a `ExplicitOpModel`. We can print the matrix and vector contents of an explicit model by just printing the object: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"Model 1:\\n\", model1)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#You can also access individual gates like they're numpy arrays:\n", "Gx = model1['Gx'] # a LinearOperator object, but behaves like a numpy array\n", "\n", "#By printing a gate, you can see that it's not just a numpy array\n", "print(\"Gx = \", Gx)\n", "\n", "#But can be accessed as one:\n", "print(\"Array-like printout\\n\", Gx[:,:],\"\\n\")\n", "print(\"First row\\n\", Gx[0,:],\"\\n\")\n", "print(\"Element [2,3] = \",Gx[2,3], \"\\n\")\n", "\n", "Id = np.identity(4,'d')\n", "Id_dot_Gx = np.dot(Id,Gx)\n", "print(\"Id_dot_Gx\\n\", Id_dot_Gx, \"\\n\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also print the members (state preparations, operations, POVMs) of a model using the `print_modelmembers` method. This shows the model contents in a more condensed format that includes the type of object each member and a short summary of what it holds." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "model1.print_modelmembers()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Basic Operations with Explicit Models\n", "\n", "`ExplicitOpModel` objects have a number of methods that support a variety of operations, including:\n", "\n", "* Depolarizing or rotating every gate\n", "* Writing the model to a JSON file\n", "* Computing products of operation matrices\n", "* Printing more information about the model" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Add 10% depolarization noise to the gates\n", "depol_model3 = model3.depolarize(op_noise=0.1)\n", "\n", "#Add a Y-axis rotation uniformly to all the gates\n", "rot_model3 = model3.rotate(rotate=(0,0.1,0))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#Writing a model as a text file\n", "depol_model3.write(\"../tutorial_files/Example_depolarizedModel.json\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"Probabilities of outcomes of the gate\\n sequence GxGx (rho0 and Mdefault assumed)= \",\n", " depol_model3.probabilities( (\"Gx\", \"Gx\")))\n", "print(\"Probabilities of outcomes of the \\\"complete\\\" gate\\n sequence rho0+GxGx+Mdefault = \",\n", " depol_model3.probabilities( (\"rho0\", \"Gx\", \"Gx\", \"Mdefault\")))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It is also possible to manipulate the underlying operations by accessing the forward simulator in the `Model` class. For example, using the `matrix` simulator type, one can compute the product of two gate operations. For more details, see the [ForwardSimulationTypes tutorial](../algorithms/advanced/ForwardSimulationTypes.ipynb)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Computing the product of operation matrices (only allowed with the matrix simulator type)\n", "print(\"Product of Gx * Gx = \\n\",depol_model3.sim.product((\"Gx\", \"Gx\")), end='\\n\\n')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Next steps\n", "Next, you may want to take a look a [implicit models](ImplicitModel.ipynb), which are similar to explicit models but more powerful in ways relevant to multi-qubit modeling. You may also be interested to check out the [model parameterizations tutorial](ModelParameterization.ipynb) and the [model noise tutoria](ModelNoise.ipynb), which have information relevant to explicit and implicit models." ] } ], "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": 1 }