{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Implicit Models\n", "This tutorial explains how to create and use the implicit-layer-operation models present in pyGSTi. It *doesn't* show you how to build your own model, which will be the topic of a future tutorial.\n", "\n", "\"Implicit models\", as we'll refer to implicit-layer-operation models from now on, store building blocks needed to construct layer operations but not usually the layer operations themselves. When simulating a circuit, an implicit model creates on the fly, from its building blocks, an operator for each circuit layer in turn. It therefore only creates operators for the layers that are actually needed for the circuit simulation. Thus simulating a circuit with an implicit model is similar to building an *explicit* model with just the needed layer-operations based on some **building blocks** and some **rules**.\n", "\n", "Implicit models are very useful within multi-qubit contexts, where there are so many possible circuit layers one cannot easily create and store separate operators for every possible layer. It is much more convenient to instead specify a smaller set of building-block-operators and rules for combining them into full $n$-qubit layer operations.\n", "\n", "PyGSTi currently contains two types of implicit models, both derived from `ImplicitOpModel` (which is derived from `Model`):\n", "- `LocalNoiseModel` objects are noise models where \"noise\" (the departure or deviance from perfection) of a given gate is localized to *only* the qubits where that gate is intended to act. Said another way, the key assumption of a `LocalNoiseModel` is that gates act as the perfect identity everywhere except on their *target qubits* - the qubits they are supposed to act nontrivially upon. Because errors on non-target qubits can broadly be interpreted as \"crosstalk\", we can think of a `LocalNoiseModel` as a *crosstalk-free* model. For concreteness, some examples of local noise are:\n", " - a rotation gate over-rotates that qubit it's *supposed* to rotate\n", " - a controlled-not gate acts imperfectly on its control and target qubits but *perfectly* on all other qubits\n", " \n", " \n", "- `CloudNoiseModel` objects allow imperfections in a gate to involve qubits in a *neighborhood* of or *cloud* around the gate's target qubits. When the neighborhood is shrunk to just the target qubits themselves this reduced to a local noise model. What exactly constitutes a neighborhood or cloud is based on a number of \"hops\" (edge-traversals) on a graph of qubit connectivity that is supplied by the user.\n", "\n", "\n", "### Inside an implicit model: `.prep_blks`, `.povm_blks`, `.operation_blks`, and `.instrument_blks`\n", "\n", "Whereas an `ExplicitModel` contains the dictionaries `.preps`, `.povms`, `.operations`, and `.instruments` (which hold *layer* operators), an `ImplicitModel` contains the dictionaries `.prep_blks`, `.povm_blks`, `.operation_blks`, and `.instrument_blks`. Each of these dictionaries contains a second level dictionaries, and it is this second level which hold actual operators (`LinearOperator`- and `SPAMVec`-derived objects) - the **building blocks** of the model. The keys of the top-level dictionary are *category* names, and the keys of the second-level dictionaries are typically gate names or circuit layer labels. For example, a `LocalNoiseModel` has two categories within its `.operation_blks`: `\"gates\"`, and `\"layers\"`, which we'll see more of below. \n", "\n", "To begin, we'll import pyGSTi and define a function which prints the 1st and 2nd level keys of any `ImplicitModel`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pygsti\n", "import numpy as np\n", "\n", "def print_implicit_model_blocks(mdl, showSPAM=False):\n", " if showSPAM:\n", " print('State prep building blocks (.prep_blks):')\n", " for blk_lbl,blk in mdl.prep_blks.items():\n", " print(\" \" + blk_lbl, \": \", ', '.join(map(str,blk.keys())))\n", " print()\n", "\n", " print('POVM building blocks (.povm_blks):')\n", " for blk_lbl,blk in mdl.povm_blks.items():\n", " print(\" \" + blk_lbl, \": \", ', '.join(map(str,blk.keys())))\n", " print()\n", " \n", " print('Operation building blocks (.operation_blks):')\n", " for blk_lbl,blk in mdl.operation_blks.items():\n", " print(\" \" + blk_lbl, \": \", ', '.join(map(str,blk.keys())))\n", " print()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Local-noise implicit models\n", "The `LocalNoiseModel` class represents a model whose gates are only have *local noise* (described above) applied to them. This makes it trivial to combine gate-operations into layer-operations because within a layer gates act on disjoint sets of qubits and therefore so does the (local) noise. We can create a `LocalNoiseModel` by passing `build_standard_localnoise_model` a number of qubits and set of gate names (see the docstring for what gate names are recognized and how to add your own):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mdl_locnoise = pygsti.construction.create_localnoise_model(num_qubits=4, gate_names=['Gxpi','Gypi','Gcnot'])\n", "print(type(mdl_locnoise))\n", "print_implicit_model_blocks(mdl_locnoise, showSPAM=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we've created a model on 4 qubits with $X(\\pi)$, $Y(\\pi)$ and *CNOT* gates. The default qubit labelling (see the `qubit_labels` argument) is the integers starting at 0, so in this case our qubits are labelled $0$, $1$, $2$, and $3$. The default qubit connectivity (see the `geometry` argument) is a *line*, so there are *CNOT* gates between each adjacent pair of qubits when arranged as $0-1-2-3$. \n", "\n", "Let's take a look at what's inside the model:\n", "- There is just a single `\"layers\"` category within `.prep_blks` and `.povm_blks`, each containing just a single operator (a state preparation or POVM) which prepares or measures the entire 4-qubit register. Currently, the preparation and measurement portions of both a `LocalNoiseModel` and a `CloudNoiseModel` are not divided into components (e.g. 1-qubit factors) and so `.prep_blks[\"layers\"]` and `.povm_blks[\"layers\"]` behave similarly to an `ExplicitOpModel`'s `.preps` and `.povms` dictionaries. Because there's nothing special going on here, we'll omit printing `.prep_blks` and `.povm_blks` for the rest of this tutorial by leaving the default `showSPAM=False` in future calls to `print_implicit_model_blocks`. \n", "\n", " *(Aside)* The question may come to mind: *why does the model create these layer operations now?* Why not just create these on an as-needed basis? The answer is *for efficiency* - it takes some nontrivial amount of work to \"embed\" a 1- or 2-qubit process matrix within a larger (e.g. 4-qubit) process matrix, and we perform this work once up front so it doesn't need to be repeated later on.\n", "\n", "\n", "- There are two categories within `.operation_blks`: `\"gates\"` and `\"layers\"`. The former contains three elements which are just the gate names (`\"Gxpi\"`, `\"Gypi\"`, and `\"Gcnot\"`), which hold the 1- and 2-qubit gate operations. The `\"layers\"` category contains holds (4-qubit) *primitive layer operations* which give the action of layers containing just a single gate (called \"primitive layers\"). From their labels we can see which gate is placed where within the layer.\n", "\n", "\n", "- Gate operations are *linked* to all of the layer operations containing that gate. For example, the `Gxpi` element of `.operation_blks[\"gates\"]` is linked to the `Gxpi:0`, `Gxpi:1`, `Gxpi:2`, `Gxpi:3` members of `.operation_blks[\"layers\"]`. Technically, this means that these layer operations contain a *reference* to (not a *copy* of) the `.operation_blks[\"gates\"][\"Gxpi\"]` object. Functionally, this means that whatever noise or imperfections are present in the `\"Gxpi\"` gate operation will be manifest in all of the corresponding layer operations, as we'll see below. This behavior is specified by the `independent_gates` argument, whose default value is `True`. We'll see what happens when we change this farther down below.\n", "\n", "\n", "Let's print the `\"Gxpi\"` operator:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(mdl_locnoise.operation_blks['gates']['Gxpi']) # Static!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that is a `StaticDenseOp` object. The gate operations in `.operation_blks[\"gates\"]` are *static* operators (they have no adjustable parameters - see the [Operators tutorial](advanced/Operators.ipynb) for an explanation of the different kinds of operators). This is because the default value of the `parameterization` argument is `\"static\"`. \n", "\n", "### Creating a modifiable `LocalNoiseModel`\n", "\n", "Since we'd like to modify these gate operations, let's make a new model with `parameterization=\"full\"`. We'll also set the `availability` argument to demonstrate how we can specify where the *CNOT* gates should go - they'll occur in only one \"direction\", from left to right, across the $0-1-2-3$ chain:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mdl_locnoise = pygsti.construction.create_localnoise_model(num_qubits=4, gate_names=['Gxpi','Gypi','Gcnot'],\n", " availability={'Gcnot': [(0,1),(1,2),(2,3)]},\n", " parameterization='full')\n", "print_implicit_model_blocks(mdl_locnoise)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now the gates are `FullDenseOp` objects (which can be modified as we please):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(mdl_locnoise.operation_blks['gates']['Gxpi'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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']['Gxpi'] = 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": [ "### Circuit simulation\n", "Now that we have a model, we'll simulate a circuit with four \"primitive $X(\\pi)$\" layers. Notice from the outcome probabilities that all for layers have imperfect (depolarized) $X(\\pi)$ gates:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c = pygsti.obj.Circuit( [('Gxpi',0),('Gxpi',1),('Gxpi',2),('Gxpi',3)], num_lines=4)\n", "print(c)\n", "mdl_locnoise.probabilities(c)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we compress the circuit's depth (to 1) we can also simulate this circuit, since a `LocalNoiseModel` knows how to automatically create this single non-primitive (contains 4 $X(\\pi)$ gates) layer from its gate and primitive-layer building blocks. Note that the probabilities are identical to the above case." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c2 = c.parallelize()\n", "print(c2)\n", "\n", "mdl_locnoise.probabilities(c2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Creating a `LocalNoiseModel` with independent gates\n", "As we've just seen, by default `build_standard_localnoise_model` creates a `LocalNoiseModel` that contains just a single gate operation for each gate name (e.g. `\"Gxpi\"`). This is convenient when we expect the same gate acting on different qubits will have identical (or very similar) noise properties. What if, however, we expect that the $X(\\pi)$ gate on qubit $0$ has a different type of noise than the $X(\\pi)$ gate on qubit $1$? In this case, we want gates on different qubits to have *independent* noise, so we set `independent_gates=True`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mdl_locnoise2 = pygsti.construction.create_localnoise_model(num_qubits=4, gate_names=['Gxpi','Gypi','Gcnot'],\n", " availability={'Gcnot': [(0,1),(1,2),(2,3)]},\n", " parameterization='full', independent_gates=True)\n", "print_implicit_model_blocks(mdl_locnoise2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that now there are separate `.operation_blks[\"gates\"]` elements for each primitive layer. Now we can add some noise *just* to the $X(\\pi)$ gate on qubit $0$, for instance:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mdl_locnoise2.operation_blks['gates'][('Gxpi',0)] = 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": [ "When we simulate the same circuit as above, we find that only the first (on qubit $0$) $X(\\pi)$ gate has depolarization error on it now:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(c)\n", "mdl_locnoise2.probabilities(c)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Other geometries\n", "Finally, note that we can specify other qubit connectivities using the `geometry` argument of `build_standard_localnoise_model`. You can specify a builtin name like `\"line\"` or `\"grid\"`, or any `pygsti.obj.QubitGraph` object to specify which 2-qubit gates are available as primitive layers. Here's an example of 9 qubits on a grid (note that edges of builtin graphs like `\"grid\"` are *undirected*, so the 2Q gates occur in both directions):\n", "~~~\n", "0-1-2\n", "| | |\n", "3-4-5\n", "| | |\n", "6-7-8\n", "~~~\n", "**TODO: tutorial on graphs & example here**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mdl_locnoise3 = pygsti.construction.create_localnoise_model(num_qubits=9, gate_names=['Gxpi','Gypi','Gcnot'],\n", " geometry='grid')\n", "print_implicit_model_blocks(mdl_locnoise3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Crosstalk-free models\n", "A **crosstalk free** model is essentially the same as a local noise model - it's a model where each gate can be represented as a process matrix which only acts nontrivially on the gate's designated target qubits.) The construction routines we've seen so far construct models with perfect gates. The `create_crosstalk_free_model` function constructs a *noisy* `LocalNoiseModel` whose per-gate noise can be described in a simple way via a dictionary of gate errors. Here are the types of noise you can specify via the elements of this error dictionary:\n", "\n", "1. a *single floating point number* specifies that a gate should have a given depolarization rate.\n", "2. a *tuple of floating point numbers* specifies (potentially) anisotropic Pauli stochastic noise.\n", "3. a *dictionary* specifies the error rates associated with different standard error generators. \n", "\n", "Here's an example that illustrates the above three ways:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n_qubits = 2\n", "cf_mdl = pygsti.construction.create_crosstalk_free_model(\n", " n_qubits, ('Gx','Gy','Gcnot'), \n", " {'Gx': 0.1, # float => single depolarization rate\n", " 'Gy': (0.02,0.02,0.02), # tuple of floats => pauli stochastic rates \n", " 'Gcnot': {('H','ZZ'): 0.01, ('S','IX'): 0.01} #error generators\n", " })" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The first two error types should be pretty self-explanatory. The third, \"error generators\", needs a bit more explanation: Some common types of gate noise can be represented in terms of an *error generator*. If $G$ is a noisy gate (a CPTP map) and $G_0$ is it's ideal counterpart, then if we write $G = e^{\\Lambda}G_0$ then $\\Lambda$ is called the gate's *error generator*. `LindbladOp` objects in pyGSTi represent this type of gate decomposition. If we write $\\Lambda$ as a sum of terms, $\\Lambda = \\sum_i \\alpha_i F_i$ then, when the $F_i$ are specific generators for well-known errors (e.g. rotations or stochastic errors), the $\\alpha_i$ can roughly be interpreted as the error *rates* corresponding to the well-known error types. PyGSTi has three specific generator types (where $P_i$ is a Pauli operator or tensor product of Pauli operators):\n", "\n", "- **Hamiltonian**: $F_i = H_i$ where $H_i : \\rho \\rightarrow -i[P_i,\\rho]$\n", "- **Stochastic**: $F_i = S_i$ where $S_i : \\rho \\rightarrow P_i \\rho P_i - \\rho$\n", "- **Affine**: $F_i = A_i$ where $A_i : \\rho \\rightarrow \\mathrm{Tr}(\\rho_{target})P_i \\otimes \\rho_{non-target}$\n", "\n", "When we have an implicit model whose gates are `LindbladOp` operators, then we can specify the errors on the gates via a dictionary of the $\\alpha_i$ coefficients. The dictionary given as a value of the `error_rates` argument of `create_crosstalk_free_model` lists the $\\alpha_i$ using the syntax `(,)`, where `\"H\"`, `\"S\"`, and `\"A\"` are used to designate the Hamiltonian, stochastic, and affine types, and strings of `I`, `X`, `Y`, and `Z` can be used to label a Pauli basis element. See the Lindblad operator section of the [tutorial on operators](advanced/Operators.ipynb) for more details.\n", "\n", "#### More options\n", "The above example creates a crosstalk free model whereby each gate, regardless of which qubit(s) it acts on, has the same noise. To override this behavior for a specific set of target qubits, you can include elements in the error dictionary which include qubit labels (e.g. `('Gx',0)` below). To add noise to the $n$-qubit idle, state preparation, or measurement operations, you can also specify `'idle'`, `'prep'`, and `'povm'` as keys in the error dictionary. The below cell illustrates this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cf_mdl2 = pygsti.construction.create_crosstalk_free_model(\n", " n_qubits, ('Gx','Gy','Gcnot'), \n", " {'Gx': 0.1, #depol\n", " ('Gx',0): 0.2, #more depolarization on qubit 0\n", " 'Gy': (0.02,0.02,0.02), # pauli stochastic \n", " 'Gcnot': {('H','ZZ'): 0.01, ('S','IX'): 0.01}, # error generator rates \n", " 'idle': 0.01, 'prep': 0.01, 'povm': 0.01 # depolarization on the idle, prep, and measurement\n", " })" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For convenience, you can use shorthand to describe the basis-elements for the error generator rates (using `'HZZ'` instead of `('H','ZZ')` for instance). " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cf_mdl3 = pygsti.construction.create_crosstalk_free_model(\n", " n_qubits, ('Gx','Gy','Gcnot'), \n", " {'Gx': 0.1, #depol\n", " ('Gx',0): 0.2, #more depolarization on qubit 0\n", " 'Gy': (0.02,0.02,0.02), # pauli stochastic \n", " 'Gcnot': {'HZZ': 0.01, 'SIX': 0.01}, # error generator rates \n", " 'idle': 0.01, 'prep': 0.01, 'povm': 0.01 # depolarization on the idle, prep, and measurement\n", " })" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Construction by appending\n", "The `create_crosstalk_free_model` demonstrated above allows you to build a wide variety of local noise models fairly easily and quickly - but what if you need something just a little different? By setting `ensure_composed_gates=True`, all of the output gates (the elements of `.operation_blks['gates']`) will be *composed* gates - i.e. `ComposedOp` objects. This is nice because composed operations allow you to easily tag on additional operations - to add additional elements to whatever is being composed. Here's an example of how to create a noiseless crosstalk-free model and then add an arbitrary additional error term to both the `Gx` and `Gy` gates:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mdl = pygsti.construction.create_crosstalk_free_model(n_qubits, ('Gi','Gx','Gy','Gcnot'), {}, ensure_composed_gates=True, independent_gates=False)\n", "\n", "additional_error = pygsti.obj.TPDenseOp(np.identity(4,'d')) # this could be *any* operator\n", "\n", "#ComposedOp objects support .append( operation )\n", "mdl.operation_blks['gates']['Gx'].append(additional_error)\n", "mdl.operation_blks['gates']['Gy'].append(additional_error)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This has advantages over simply setting gates to numpy arrays after using `parameterization=\"full\"` (as demonstrated above) because you're adding a custom operation *object*, which can posess a custom *parameterization*." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Cloud-noise implicit models\n", "Note: cloud-noise models are an advanced feature in pyGSTi, and as such this portion of the tutorial is less complete and more confusing than our tutorials on other topics\n", "\n", "`CloudNnoiseModel` objects are designed to represent gates whose imperfections affect only the qubits in a neighborhood, or *cloud*, around a gate's target qubits. This notion of a gate's cloud is fairly flexible, but typically defined as the set of qubits that can be reached by some number ($k$, say) of edge traversals (or *hops*) from the gate's target qubits along a globally given connectivity graph. \n", "\n", "For instance, if the graph specifies four qubits in a line: $0-1-2-3$ and we allow at most 1 hop along the graph, then the noise cloud of a single-qubit gate on qubit $1$ is the set of qubits $\\{0,1,2\\}$ and the cloud for a two-qubit gate on qubits $1$ and $2$ is the set $\\{0,1,2,3\\}$.\n", "\n", "A `CloudNoiseModel` also contains a single (noisy) \"global\" or \"background\" idle operation that is intended to specify noise that affects all the qubits during the time of a circuit layer regardless of whether they participate in any gates. This noisy idle operation (acting on all the qubits) and the noise on each gate (acting on the gate's *cloud*) is taken to have the form $\\exp{\\mathcal{L}}$, where $\\mathcal{L}$ is a Lindbladian which contains error terms only up to some *maximum weight* (typically 1 or 2, so we call this a \"low weight\" approximation or constraint). Thus, **a `CloudNoiseModel` describes noise that is *geometrically-local* and *low-weight*** but not strictly local (i.e. crosstalk-free) as a `LocalNoiseModel` does.\n", "\n", "Each circuit layer is modeled as the global idle composed with gate operation(s) corresponding to that gate(s) in the layer. Thus, the noise from the global idle and from the gate cloud(s) must be combined when constructing a layer operation. By default this is done by simply composing the different error maps. The `errcomp_type` argument, however, can change this behavior so that the Lindbladian *error generators* are composed instead of the maps. When `errcomp_type=\"gates\"` the noise maps for the components are of a gate layer are composed; when `errcomp_type=\"errorgens\"` the error generators of the noise maps are added and used as the error generator for the final operation (this is an advanced topic which isn't covered in this tutorial yet). \n", "\n", "We can create a `CloudNnoiseModel` using the `build_standard_cloudnoise_model` function which resembles `build_standard_localnoise_model` but contains some extra arguments dealing with cloud construction and the maximum error weights used:\n", "- `maxhops` specifies how many hops from a gate's target qubits (along the qubit graph given by the `geometry` argument ,which defaults to `\"line\"`) describe which qubits comprise the gate's *cloud*.\n", "- `max_idle_weight` specifies the maximum-weight of error terms in the global idle operation.\n", "- `max_spam_weight` specifies the maximum-weight of error terms in the state preparation and measurement operations.\n", "- `extra_gate_weight` specifies the maximum-weight of error terms in gates' clouds *relative* to the number of target qubits of the gate. For instance, if `extra_gate_weight=0` then 1-qubit gates can have up to weight-1 error terms in their clouds and 2-qubit gates can have up to weight-2 error terms. If `extra_gate_weight=1` then this changes to weight-2 errors for 1Q gates and weight-3 errors for 2Q gates.\n", "- `extra_weight_1_hops` specifies an additional number of hops (added to `maxhops`) that applies only to weight-1 error terms. For example, in a 8-qubit line example, if `maxhops=1`, `extra_gate_weight=0`, and `extra_weight_1_hops=1` then a 2-qubit gate on qubits $4$ and $5$ can have up-to-weight-2 errors on qubits $\\{3,4,5,6\\}$ and additionally weight-1 errors on qubits $2$ and $7$.\n", "- `errcomp_type` specifes how errors are composed when creating layer operations. An advanced topic that we don't explore here.\n", "\n", "That's a lot to take in, so let's look at a concrete example. Here's how to create a cloud noise model on a 4-qubit line:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pygsti\n", "mdl_cloudnoise = pygsti.construction.create_cloudnoise_model_from_hops_and_weights(\n", " num_qubits=4, gate_names=['Gxpi','Gypi','Gcnot'],\n", " availability={'Gcnot': [(0,1),(1,2),(2,3)]},\n", " max_idle_weight=1, max_spam_weight=1, maxhops=1,\n", " extra_weight_1_hops=0, extra_gate_weight=0)\n", "print_implicit_model_blocks(mdl_cloudnoise)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that a `CloudNoiseModel` has *three operation categories*: `\"gates\"`, `\"layers\"`, and `\"cloudnoise\"`. The first two serve a similar function as in a `LocalNoiseModel`, and hold the (1- and 2-qubit) gate operations and the (4-qubit) layer operations, respectively. The `\"cloudnoise\"` category contains layer operations corresponding to the \"cloud-noise\" associated with each primitive layer, i.e. each single-gate layer. The `\"layers\"` category contains the special `\"globalIdle\"` operation (described above) and *perfect* layer operations for each primitive layer. Let's take a look at the structure of some of these operations: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(mdl_cloudnoise.operation_blks['gates']['Gxpi']) # just a static 1-qubit operator\n", "print(mdl_cloudnoise.operation_blks['layers'][('Gxpi',0)]) # perfect layer operator: 1Q Gxpi gate on qubit 0\n", "print(mdl_cloudnoise.operation_blks['layers']['globalIdle']) # composition of wt-1 error terms on each (of 4) qubits\n", "print(mdl_cloudnoise.operation_blks['cloudnoise'][('Gxpi',0)]) # wt-1 error terms on \"cloud of Gxpi:0\" == qubits 0 & 1\n", "print(mdl_cloudnoise.operation_blks['cloudnoise'][('Gxpi',1)]) # wt-1 error tersm on \"cloud of Gxpi:1\" == qubits 0,1,2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can simultate the same circuit as above using our (currently noise-free) cloud noise model:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(c)\n", "mdl_cloudnoise.probabilities(c)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's add some noise to our model. Whereas in a `LocalNoiseModel` one typically inserts noise by modifying the operations in the `\"gates\"` category (after making them non-static gates), in a `CloudNoiseModel` one should modify the `\"globalIdle\"` layer or the `\"cloudnoise\"` operations. Here, we'll add some noise (make the error-term coefficients nonzero) to the portion global idle which acts on the first qubit `\"Q0\"` (see the printed structure of `\"globalIdle` above). For details on what exactly is going on here, checkout the [Operators tutorial](advanced/Operators.ipynb)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# parameters of mdl_cloudnoise.operation_blks['layers']['globalIdle'].factorops[0].embedded_op\n", "# are the coefficients of HX, HY, HZ error terms, then the squares of the coefficients of SX, SY, SZ error terms.\n", "noisevec = np.array([0,0,0,np.sqrt(0.1),np.sqrt(0.1),np.sqrt(0.1)])\n", "mdl_cloudnoise.operation_blks['layers']['globalIdle'].factorops[0].embedded_op.from_vector(noisevec)\n", "\n", "#Print out the coefficients of the error terms, to make sure we did what we wanted:\n", "errs = mdl_cloudnoise.operation_blks['layers']['globalIdle'].factorops[0].embedded_op.errorgen_coefficients()\n", "for err,val in errs.items():\n", " print(\":\".join(map(str,err)),\"=\",val)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's calculate the probabilities using the noisy model. The resulting probabilities show the affect of depolarization on the first qubit over each (**all 4**) circuit layers:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mdl_cloudnoise.probabilities(c)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we compress the circuit down to depth 1, then there's only a single layer and thus just a single `\"globalIdle\"` affects the outcome probabilities:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mdl_cloudnoise.probabilities(c2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Cloud-crosstalk models\n", "`CloudNoiseModel` objects can capture **crosstalk errors** - errors which violate either the locality of a gate (if it operates non-trivially on non-target qubits) or its independence from its \"environment\" (the other gates it appears with in a circuit layer). The construction routines we've seen so far construct models with perfect gates. The `create_cloud_crosstalk_model` function constructs a *noisy* `CloudNoiseModel` whose noise can be described in a (relatively) simple way via a dictionary of errors. The `error_rates` argument is a dictionary whose values are themselves dictionaries of error-generator rates (similar to what is allowed in crosstalk-free model construction) and whose keys describe which gate the noise is applied to and upon which qubits the noise is \"centered\".\n", "\n", "Here's an example that illustrates this:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n_qubits = 2\n", "cc_mdl1 = pygsti.construction.create_cloud_crosstalk_model(\n", " n_qubits, ('Gx','Gy','Gcnot'), \n", " { ('Gx',0): { ('H','X'): 0.01, ('S','XY:0,1'): 0.01},\n", " ('Gcnot',0,1): { ('H','ZZ'): 0.02, ('S','XX:0,1'): 0.02 },\n", " 'idle': { ('S','XX:0,1'): 0.01 },\n", " 'prep': { ('S','XX:0,1'): 0.01 },\n", " 'povm': { ('S','XX:0,1'): 0.01 }\n", " })" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that the keys of the error dictionary give specific qubits, e.g. `('Gx',0)`, but that the basis elements given in the corresponding dictionary of error-generator rates by contain other/additional qubits (e.g. `'XY:0,1'`, which is the 2-qubit Pauli operator that acts as $X$ on qubit 0 and $Y$ on qubit 1). Furthermore, note that when a basis element does not have any qubit specification then the target qubit(s) of the current gate is/are assumed (e.g. the `'X'` in `('H','X')` for key `('Gx',0)` is the $X$ Pauli on qubit 0).\n", "\n", "#### Stencils\n", "You might be thinking that this makes it difficult to describe \"cloud\" noise because you'd need to describe the action of the `Gx` gate, for instance, on each qubit separately. You can certainly do this, but \"stencils\" have been developed precisely for this purpose. \n", "\n", "A `QubitGraph` object may have included within it the notion of one or more directions. For example, the built-in (and default) linear chain graph has two directions, \"left\" and \"right\". When creating a cloud-crosstalk model, you can specify as a key in the `error_rates` argument just a simple gate name, e.g. `\"Gx\"`, and use stencil notation to describe the noise for all `('Gx',#)` operations. There are two aspects of this notation:\n", "\n", "- it allows the use of `@` to specify one of the operation's target qubits (e.g. for `('Gcnot',0,1)`, `@0` would evaluate to `0` and `@1` would evaluate to `1` whereas for `('Gcnot',3,2)`, `@0` would evaluate to `3` and `@1` would evaluate to `2`)\n", "- you can follow a `@` specifier with any number of `+` directives. For example, on a linear-chain graph, you could specify a qubit index by `@0+left+left` to mean the qubit that is two qubits to the left of the first target qubit. If a qubit doesn't exist in that location (e.g. if you've hit the end of the chain) then the specified error rate is ignored.\n", "\n", "Here's a simple example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cc_mdl2 = pygsti.construction.create_cloud_crosstalk_model(\n", " n_qubits, ('Gx','Gy','Gcnot'), \n", " { 'Gx': { ('H','X'): 0.01, ('S','X:@0+left'): 0.01, ('S','XX:@0,@0+right'): 0.02},\n", " 'Gcnot': { ('H','ZZ'): 0.02, ('S','XX:@1+right,@0+left'): 0.02 },\n", " 'idle': { ('S','XX:0,1'): 0.01 }\n", " })" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Basis-element notation can also be abbreviated for convenience (this builds the same model a above):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cc_mdl3 = pygsti.construction.create_cloud_crosstalk_model(\n", " n_qubits, ('Gx','Gy','Gcnot'), \n", " { 'Gx': { 'HX': 0.01, 'SX:@0+left': 0.01, ('SXX:@0,@0+right'): 0.02},\n", " 'Gcnot': { 'HZZ': 0.02, 'SXX:@1+right,@0+left': 0.02 },\n", " 'idle': { 'SXX:0,1': 0.01 }\n", " })" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Additional resources\n", "\n", "Getting a list of the gate names recognized by pyGSTi:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "known_gate_names = list(pygsti.tools.internalgates.standard_gatename_unitaries().keys())\n", "print(known_gate_names)" ] }, { "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.6" } }, "nbformat": 4, "nbformat_minor": 2 }