{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Model Parameterization\n",
    "The fundamental role of Model objects in pyGSTi is to simulate circuits, that is, to map circuits to outcome probability distributions.  This mapping is *parameterized* by some set of real-valued parameters, meaning that the mapping between circuits and outcome distribution depends on the values of a `Model`'s parameters.  Model objects have a `num_params` attribute holding its parameter count, and `to_vector` and `from_vector` methods which get or set a model's vector of parameters.\n",
    "\n",
    "`ModelMember` objects such as state prepations, operations, and measurements (POVMs) are also parameterized, and similarly possess a `num_params` attribute and `to_vector` and `from_vector` methods.  For models that hold member objects to implement their operations (e.g., both explicit and implicit models), the model's parameterization the result of combining the parameterizations of all its members.\n",
    "\n",
    "In explicit models, the parameterization is properly viewed as a mapping between the model's parameter space and the space of $d^2 \\times d^2$ operation matrices and length-$d^2$ SPAM vectors.  A `Model`'s contents always correspond to a valid set of parameters, which can be obtained by its `to_vector` method, and can always be initialized from a vector of parameters via its `from_vector` method.  The number of parameters (obtained via `num_params`) is independent (and need not equal!) the total number of gate-matrix and SPAM-vector elements comprising the `Model`.  For example, in a \"TP-parameterized\" model, the first row of each operation matrix is fixed at `[1,0,...0]`, regardless to what the `Model`'s underlying parameters are.  One of pyGSTi's primary capabilities is model optimization: the optimization of a fit function (often the log-likelihood) over the parameter space of an initial `Model` (often times the \"target\" model).  Thus, specifying a model's parameterization specifies the constraints under which the model is optimized, or equivalently the space of possible circuit-to-outcome-distribution mappings that are searched for a best-fit estimate.  \n",
    "\n",
    "In the simplest case, each gate and SPAM vector within a `ExplicitOpModel` have independent paramterizations, so that each `pygsti.modelmembers.ModelMember`-derived object has its own separate parameters accessed by its `to_vector` and `from_vector` methods.  The `ExplictOpModel`'s parameter vector, in this case, is just the concatenation of the parameter vectors of its contents, usually in the order: 1) state preparation vectors, 2) measurement vectors, 3) gates.\n",
    "\n",
    "## Operation types\n",
    "\n",
    "Operations on quantum states exist within the `pygsti.modelmembers.operations` subpackage.  Most of the classes therein represent a unique combination of a:\n",
    "\n",
    "a. category of operation that can be represented, and\n",
    "b. parameterization of that category of operations.\n",
    "\n",
    "For example, the `FullArbitraryOp` class can represent an arbitrary (Markovian) operation, and \"fully\" parameterizes the operation by exposing every element of the operation's dense process matrix as a parameter.  The `StaticCliffordOp` class can only represent Clifford operations, and is \"static\", meaning it exposes no parameters and so cannot be changed in an optimization.  Here are brief descriptions of several of the most commonly used operation types:\n",
    "\n",
    "- The `FullArbitraryOp` class represents a arbitrary process matrix which has a parameter for every element, and thus optimizations using this gate class allow the operation matrix to be varied completely.\n",
    "- The `StaticArbitraryOp` class also represents an arbitrary process matrix but has no parameters, and thus is not optimized at all.\n",
    "- The `FullTPOp` class represents a process matrix whose first row must be `[1,0,...0]`.  This corresponds to a trace-preserving (TP) gate in the Gell-Mann and Pauli-product bases.  Each element in the remaining rows is a separate parameter, similar to a fully parameterized gate.  Optimizations using this gate type are used to constrain the estimated gate to being trace preserving.\n",
    "- The `LindbladErrorgen` class defines an error generator that takes a particular Lindblad form.  This class is fairly flexible, but is predominantly used to constrain optimizations to the set of infinitesimally-generated CPTP maps.  To produce a gate or layer operation, error generators must be exponentiated using the `ExpErrorgenOp` class.\n",
    "\n",
    "Similarly, there classes represnting quantum states in `pygsti.modelmembers.states` and those for POVMs and POVM effects in `pygsti.modelmembers.povms`.  Many of these classes run parallel to those for operations.  For example, there exist `FullState` and `TPState` classes, the latter which fixes its first element to $\\sqrt{d}$, where $d^2$ is the vector length, as this is the appropriate value for a unit-trace state preparation.\n",
    "\n",
    "There are other operation types that simply combine or modify other operations.  These types don't correspond to a particular category of operations or parameterization, they simply inherit these from the operations they act upon.  The are:\n",
    "\n",
    "- The `ComposedOp` class combines zero or more other operations by acting them one after the other.  This has the effect of producing a map whose process matrix would be the product of the process matrices of the factor operations. \n",
    "- The `ComposedErrorgen` class combines zero or more error generators by effectively summing them together.\n",
    "- The `EmbeddedOp` class embeds a lower-dimensional operation (e.g. a 1-qubit gate) into a higer-dimensional space (e.g. a 3-qubit space).\n",
    "- The `EmbeddedErrorgen` class embeds a lower-dimensional error generator into a higher-dimensional space.\n",
    "- The `ExpErrorgenOp` class exponentiates an error generator operation, making it into a map on quantum states.\n",
    "- The `RepeatedOp` class simply repeats a single operation $k$ times.\n",
    "\n",
    "These operations act as critical building blocks when constructing complex gate and circuit-layer operations, especially on a many-qubit spaces.  Again, there are analogous classes for states, POVMs, etc., within the other sub-packages beneath `pygsti.modelmembers`.\n",
    "\n",
    "\n",
    "## Specifying operation types when creating models\n",
    "\n",
    "Many of the model construction functions take arguments dictating the type of modelmember objects to create.  As described above, by changing the type of a gate you select how that gate is represented (e.g. Clifford gates can be represented more efficiently than arbitrary gates) and how it is parameterized.  This in turn dictates how the overall model is paramterized.\n",
    "\n",
    "For a brief overview of the available options, here is an incomplete list of parameterization arguments and their associated `pygsti.modelmember` class.  Most types start with either `\"full\"` or `\"static\"` - these indicate whether the model members have parameters or not, respectively. Parameterizations without a prefix are \"full\" by default. See the related [ForwardSimulation tutorial](../algorithms/advanced/ForwardSimulationTypes.ipynb) for how each parameterization relates to the allowed types of forward simulation in PyGSTi.\n",
    "\n",
    "- `gate_type` for `modelmember.operations`:\n",
    "  - `\"static\"` $\\rightarrow$ `StaticArbitraryOp`\n",
    "  - `\"full\"` $\\rightarrow$ `FullArbitraryOp`\n",
    "  - `\"static standard\"` $\\rightarrow$ `StaticStandardOp`\n",
    "  - `\"static clifford\"` $\\rightarrow$ `StaticCliffordOp`\n",
    "  - `\"static unitary\"` $\\rightarrow$ `StaticUnitaryOp`\n",
    "  - `\"full unitary\"` $\\rightarrow$ `FullUnitaryOp`\n",
    "  - `\"full TP\"` $\\rightarrow$ `FullTPOp`\n",
    "  - `\"CPTP\"`, `\"H+S\"`, etc. $\\rightarrow$ `ExpErrorgenOp` + `LindbladErrorgen`\n",
    "\n",
    "\n",
    "- `prep_type` for `modelmember.states`:\n",
    "  - `\"computational\"` $\\rightarrow$ `ComputationalBasisState`\n",
    "  - `\"static pure\"` $\\rightarrow$ `StaticPureState`\n",
    "  - `\"full pure\"` $\\rightarrow$ `FullPureState`\n",
    "  - `\"static\"` $\\rightarrow$ `StaticState`\n",
    "  - `\"full\"` $\\rightarrow$ `FullState`\n",
    "  - `\"full TP\"` $\\rightarrow$ `TPState`\n",
    "\n",
    "\n",
    "- `povm_type` for `modelmember.povms`:\n",
    "  - `\"computational\"` $\\rightarrow$ `ComputationalBasisPOVM`\n",
    "  - `\"static pure\"` $\\rightarrow$ `UnconstrainedPOVM` + `StaticPureEffect`\n",
    "  - `\"full pure\"` $\\rightarrow$ `UnconstrainedPOVM` + `FullPureEffect`\n",
    "  - `\"static\"` $\\rightarrow$ `UnconstrainedPOVM` + `StaticEffect`\n",
    "  - `\"full\"` $\\rightarrow$ `UnconstrainedPOVM` + `FullEffect`\n",
    "  - `\"full TP\"` $\\rightarrow$ `TPPOVM`\n",
    "  \n",
    "For convenience, the `prep_type` and `povm_type` arguments also accept `\"auto\"`, which will try to set the parameterization based on the given `gate_type`. An incomplete list of this `gate_type` $\\rightarrow$ `prep_type` / `povm_type` mapping is:\n",
    "\n",
    "- `\"auto\"`, `\"static standard\"`, `\"static clifford\"` $\\rightarrow$ `\"computational\"`\n",
    "- `\"unitary\"` $\\rightarrow$ `\"pure\"`\n",
    "- All others map directly\n",
    "\n",
    "### Explicit Models\n",
    "We now illustrate how one may specify the type of paramterization in `create_explicit_model`, and change the object types of all of a `ExplicitOpModel`'s contents using its `set_all_parameterizaions` method.  The `create_explicit_model` function builds (layer) operations that are compositions of the ideal operations and added noise (see the [model noise tutorial](ModelNoise.ipynb)).  By setting `ideal_gate_type` and similar arguments, the object type used for the initial \"ideal\" part of the operations is decided."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pygsti\n",
    "from pygsti.processors import QubitProcessorSpec\n",
    "from pygsti.models import modelconstruction as mc"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pspec = QubitProcessorSpec(1, ['Gi', 'Gxpi2', 'Gypi2'])  # simple single qubit processor\n",
    "model = mc.create_explicit_model(pspec)\n",
    "model.print_modelmembers()\n",
    "print(\"%d parameters\" % model.num_params)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By default, an explicit model creates static (zero parameter) operations of types `StaticUnitaryOp`.  If we specify an `ideal_gate_type` we can change this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = mc.create_explicit_model(pspec, ideal_gate_type=\"full TP\")\n",
    "model.print_modelmembers()\n",
    "print(\"%d parameters\" % model.num_params)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Switching the parameterizatio to \"CPTP\" gates changes the gate type accordingly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "model.set_all_parameterizations('CPTP')\n",
    "model.print_modelmembers()\n",
    "print(\"%d parameters\" % model.num_params)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To alter an *individual* gate or SPAM vector's parameterization, one can simply construct a replacement object of the desired type and assign it to the `Model`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Turning ComposedOp into a dense matrix for conversion into a dense FullTPOp\n",
    "newOp = pygsti.modelmembers.operations.FullTPOp(model[('Gi', 0)].to_dense())\n",
    "model['Gi'] = newOp\n",
    "print(\"model['Gi'] =\",model['Gi'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**NOTE:** When a `LinearOperator` or `SPAMVec`-derived object is assigned as an element of an `ExplicitOpModel` (as above), the object *replaces* any existing object with the given key.  However, if any other type of object is assigned to an `ExplicitOpModel` element, an attempt is made to initialize or update the existing existing gate using the assigned data (using its `set_matrix` function internally).  For example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "numpy_array = np.array( [[1, 0, 0, 0],\n",
    "                         [0, 0.5, 0, 0],\n",
    "                         [0, 0, 0.5, 0],\n",
    "                         [0, 0, 0, 0.5]], 'd')\n",
    "model['Gi'] = numpy_array # after assignment with a numpy array...\n",
    "print(\"model['Gi'] =\",model['Gi']) # this is STILL a FullTPOp object\n",
    "\n",
    "#If you try to assign a gate to something that is either invalid or it doesn't know how\n",
    "# to deal with, it will raise an exception\n",
    "invalid_TP_array = np.array( [[2, 1, 3, 0],\n",
    "                              [0, 0.5, 0, 0],\n",
    "                              [0, 0, 0.5, 0],\n",
    "                              [0, 0, 0, 0.5]], 'd')\n",
    "try:\n",
    "    model['Gi'] = invalid_TP_array\n",
    "except ValueError as e:\n",
    "    print(\"ERROR!! \" + str(e))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Implicit models\n",
    "\n",
    "The story is similar with implicit models.  Operations are built as compositions of ideal operations and noise, and by specifying the `ideal_gate_type` and similar arguments, you can set what type of ideal operation is created.  Below we show some examples with a `LocalNoiseModel`.  Let's start with the default static operation type:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mdl_locnoise = pygsti.models.create_crosstalk_free_model(pspec)\n",
    "mdl_locnoise.print_modelmembers()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Suppose we'd like to modify the gate operations.  Then we should make a model with `ideal_gate_type=\"full\"`, so the operations are `FullArbitraryOp` objects:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mdl_locnoise = pygsti.models.create_crosstalk_free_model(pspec, ideal_gate_type='full')\n",
    "mdl_locnoise.print_modelmembers()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "These can now be modified by matrix assignment, since their parameters allow them to take on any other process matrix.  Let's set the process matrix (more accurately, this is the Pauli-transfer-matrix of the gate) of `\"Gxpi\"` to include some depolarization: "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mdl_locnoise.operation_blks['gates']['Gxpi2'] = np.array([[1,   0,   0,   0],\n",
    "                                                          [0, 0.9,   0,   0],\n",
    "                                                          [0,   0,-0.9,   0],\n",
    "                                                          [0,   0,   0,-0.9]],'d')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In `CloudNoiseModel` objects, all of the model's parameterization is inherited from its noise operations, and so there are no `ideal_gate_type` and similar arguments in `create_cloud_crosstalk_model`.  All of the ideal operations are always static (have no parameters) in a cloud noise model.  See the [tutorial on model noise](ModelNoise.ipynb) to see how the types of the noise objects can be set."
   ]
  }
 ],
 "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": 4
}
