{
 "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
}
