Gate Sets Tutorial

This tutorial will show you how to create and use GateSet objects. GateSet objects are fundamental to pyGSTi, as each represents a set of quantum gates along with state preparation and measurement (i.e. POVM effect) 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 this Hilbert-Schmidt 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.

The basis used for Hilbert-Schmidt space can be any set of $d\times d$ matrices which span the density matrix space. pyGSTi constains support for three basis sets:

  • the matrix unit, or "standard" basis, consisting of the matrices with a single unit (1.0) element and otherwise zero. This basis is selected by passing "std" to appropriate function arguments.
  • the Pauli-product basis, consisting of tensor products of the four Pauli matrices {I, X, Y, Z} normalized so that $Tr(B_i B_j) = \delta_{ij}$. All of these matrices are Hermitian, so that Hilbert-Schmidt vectors and matrices are real when this basis is used. This basis can only be used when the $d = 4^i$ for integer $i$, and is selected using the string "pp".
  • the Gell-Mann basis, consisting of the normalized Gell-Mann matrices (see Wikipedia if you don't know what these are). Similar to the Pauli-product case, these matrices are also Hermitian, so that Hilbert-Schmidt vectors and matrices are real when this basis is used. Unlike the Pauli-product case, since Gell-Mann matrices are well defined in any dimension, the Gell-Mann basis is not restricted to cases when $d=4^i$. This basis is selected using the string "gm".

GateSet objects have the look and feel of Python dictionaries which hold $d^2\times d^2$ gate matrices and length-$d^2$ state preparation and POVM effect vectors (collectively referred to as "SPAM" vectors).

In [1]:
from __future__ import print_function
In [2]:
#Import the pyGSTi module -- you probably want this at the beginning of every notebook
import pygsti
import as pc

Creating gate sets

There are more or less three ways to create GateSet objects in pyGSTi:

  • By creating an empty GateSet and setting its elements directly, possibly with the help of's build_gate and build_vector functions.
  • By a single call to build_gateset, which automates the above approach.
  • By loading from a text-format gateset file using

Creating a GateSet from scratch

Gates and SPAM vectors can be assigned to a GateSet object as to an ordinary python dictionary. Internally a GateSet holds these quantities as Gate- and SPAMVec-derived objects, but you may assign lists, Numpy arrays, or other types of Python iterables to a GateSet key and a conversion will be performed automatically. To keep gates, state preparations, and POVM effects separate, the GateSet object looks at the beginning of the dictionary key being assigned: keys beginning with rho, E, and G are categorized as state preparations, POVM effects, and gates, respectively. To avoid ambiguity, each key must begin with one of these three prefixes with the exception of identity which we will mention later.

To separately access the state preparations, POVM effects, and gates contained in a GateSet use the preps, effects, and gates members respectively. Each one provides dictionary-like access to the underlying objects. For example, myGateset.gates['Gx'] accesses the same underlying Gate object as myGateset['Gx'], and similarly for myGateset.preps['rho0'] and myGateset['rho0']. The values of gates and SPAM vectors can be read and written in this way.

In addition to SPAM vectors and gate matrices, a GateSet holds a mapping between (state preparation, POVM effect) pairs and strings called "SPAM labels". Each SPAM label identifies an experimental outcome, meaning "I prepared state A and then measured outcome X". Experimental data is tabulated according to SPAM label, that is, each experimental count is assigned a particular SPAM label (this is explained further in the Dataset tutorial). The map between (state preparation, POVM effect) pairs and "SPAM labels" is a dictionary called spamdefs whose keys are the SPAM labels and whose values are 2-tuples containing a state preparation label and POVM effect label. The special POVM effect label "remainder" can be used to mean the identity minus all of the other POVM effects. This "remainder" POVM effect is not properly contained within the GateSet (it is not parameterized during optimizations), but should rather be thought of as a quantity that can straightforwardly be computed from GateSet quantities.

When the "remainder" label is used, the GateSet must then know what the identity vector is, and so one must set the special 'identity' key of the GateSet to the identity vector in whatever basis is being used for the SPAM vectors and gate matrices.

(Aside: Usually there is only a single state preparation, in which case the SPAM labels correspond directly with the POVM effects typically thought of as experimental outcomes. However, if there are multiple state preparations, it is important that we treat the experiment counts for "preparing state A and measuring outcome X" and "preparing state B and measuring outcome X" differently.)

In [3]:
from math import sqrt
import numpy as np

#Initialize an empty GateSet object
gateset1 = pygsti.objects.GateSet()

#Populate the GateSet object with states, effects, gates,
# all in the *normalized* Pauli basis: { I/sqrt(2), X/sqrt(2), Y/sqrt(2), Z/sqrt(2) }
# where I, X, Y, and Z are the standard Pauli matrices.
gateset1['rho0'] = [ 1/sqrt(2), 0, 0, 1/sqrt(2) ] # density matrix [[1, 0], [0, 0]] in Pauli basis
gateset1['E0'] = [ 1/sqrt(2), 0, 0, -1/sqrt(2) ]  # projector onto [[0, 0], [0, 1]] in Pauli basis
gateset1['Gi'] = np.identity(4,'d') # 4x4 identity matrix
gateset1['Gx'] = [[1, 0, 0, 0],
                  [0, 1, 0, 0],
                  [0, 0, 0,-1],
                  [0, 0, 1, 0]] # pi/2 X-rotation in Pauli basis

gateset1['Gy'] = [[1, 0, 0, 0],
                  [0, 0, 0, 1],
                  [0, 0, 1, 0],
                  [0,-1, 0, 0]] # pi/2 Y-rotation in Pauli basis

#Create SPAM labels "plus" and "minus" using the special "remainder" label,
# and set the then-needed identity vector.
gateset1.spamdefs['plus'] = ('rho0','E0')
gateset1.spamdefs['minus'] = ('rho0','remainder')
gateset1['identity'] = [ sqrt(2), 0, 0, 0 ]  # [[1, 0], [0, 1]] in Pauli basis

Creating a GateSet from scratch using build_gate and build_vector

The build_gate and build_vector functions take a human-readable string representation of a gate or SPAM vector, and return a Gate or SPAMVector object that gets stored in the dictionary-like GateSet object. To use these functions, you must specify what state space you're working with. This is done via two quantities:

  1. State space dimensions: a list of integers specifying the dimension of each block in a direct-sum decomposition of the total state space. For example, [2] means just a 2-dimensional Hilbert space, and [2,2] means the direct sum of two 2-dimensional Hilbert spaces.
  2. State space labels: a list of tuples of (string) labels. Each tuple describes how to label the corresponding term of the direct-sum decomposition of the state space. Thus, the length of the state-space-labels list must be equal to the length of the state-space-dimensions list. The elements of a tuple must be strings that start with either "Q" or "L", and are followed by any letters or numbers of your choosing. A label beginning with "Q" denotes a 2-dimensional space, whereas a label beginning with "L" denotes a 1-dimensional space. The tuple itself represents a tensor product of the spaces denoted by it's elements, and so describes how to interpret a given dimension Hilbert space as the tensor product of 1- and 2-dimensional spaces. For example, the tuple ('Q0',) describes a 2-dimensional Hilbert space as that of a single qubit, and the tuple ('Q0','Q1') describes a 4-dimensional Hilbert space as that of two qubit spaces tensored together. Each tuple describes a single Hilbert-space term in the direct-sum decomposition of the entire Hilbert space, so the list [('Q0','Q1'),('L0',)] represents a Hilbert space that is the direct sum of a 4-dimensional and a 1-dimensional space; the 4-dimensional space is the a tensor product of two qubit spaces labelled 'Q0' and 'Q1' while the 1-dimensional space is labeled 'L0'. (In this case, the corresponding state space dimensions list must be [4,1], and is required as an argument to build_vector and build_gate just as a consistency check.)

While specifying the state space in this way can seem overly cumbersome for small Hilbert spaces, it allows for great flexibility when moving to more complex spaces. It is worthwhile to note that the state space labels described above are only used when interpreting the human-readable string used to specify gates and SPAM vectors in calls to build_gate and build_vector, respectively.

build_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.

build_gate accepts a wider range of descriptor strings, which take the form of functionName(args) and include:

  • I(label0, label1, ...) : the identity on the spaces labeled by label0, label1, etc.
  • 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).
  • 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).

When the special "remainder" label is used, the needed identity vector can be generated by a call to build_identity_vec.

In [4]:
#Specify the state space
stateSpace = [2] # Hilbert space has dimension 2; density matrix is a 2x2 matrix
spaceLabels = [('Q0',)] #interpret the 2x2 density matrix as a single qubit named 'Q0'

#Initialize an empty GateSet object
gateset2 = pygsti.objects.GateSet()

#Populate the GateSet object with states, effects, and gates using 
# build_vector, build_gate, and build_identity_vec.   
gateset2['rho0'] = pc.build_vector(stateSpace,spaceLabels,"0")
gateset2['E0'] = pc.build_vector(stateSpace,spaceLabels,"1")
gateset2['Gi'] = pc.build_gate(stateSpace,spaceLabels,"I(Q0)")
gateset2['Gx'] = pc.build_gate(stateSpace,spaceLabels,"X(pi/2,Q0)")
gateset2['Gy'] = pc.build_gate(stateSpace,spaceLabels,"Y(pi/2,Q0)")
gateset2['identity'] = pc.build_identity_vec(stateSpace)

#Create SPAM labels "plus" and "minus" using the special "remainder" label.
gateset2.spamdefs['plus'] = ('rho0','E0')
gateset2.spamdefs['minus'] = ('rho0','remainder')

Create a GateSet in a single call to build_gateset

The approach illustrated above using calls to build_vector, build_gate, and build_identity_vec can be performed in a single call to build_gateset. You will notice that all of the arguments to build_gateset corrspond to those used to construct a gate set using build_vector and build_gate; the build_gateset function is merely a convenience function which allows you to specify everything at once. These arguments are:

  • Args 1 & 2 : the state-space-dimensions and state-space-labels, familiar from before.
  • Args 3 & 4 : list-of-gate-labels, list-of-gate-expressions (labels must begin with 'G'; "expressions" being the descriptor strings passed to build_gate)
  • Args 5 & 6 : list-of-prep-labels, list-of-prep-expressions (labels must begin with 'rho'; "expressions" being the descriptor strings passed to build_vector)
  • Args 7 & 8 : list-of-effect-labels, list-of-effect-expressions (labels must begin with 'E'; "expressions" being the descriptor strings passed to build_vector)
  • Arg 9 : the dictionary of SPAM label definitions.

Note that the optional parameter basis can be set to "gm" (the default), "pp", or "std" to select the basis for the gate matrices and SPAM vectors.

In [5]:
gateset3 = pc.build_gateset( [2], [('Q0',)],
                             ['Gi','Gx','Gy'], [ "I(Q0)","X(pi/2,Q0)", "Y(pi/2,Q0)"],
                             prepLabels = ['rho0'], prepExpressions=["0"], 
                             effectLabels = ['E0'], effectExpressions=["1"], 
                             spamdefs={'plus': ('rho0','E0'), 'minus': ('rho0','remainder') }) 

Load a GateSet from a file

You can also construct a GateSet object from a file using The format of the text file should be fairly self-evident given the above discussion. Note that vector and matrix elements need not be simple numbers, but can be any mathematical expression parseable by the Python interpreter, and in addition to numbers can include "sqrt" and "pi".

In [6]:
#3) Write a text-format gateset file and read it in.
gateset4_txt = \
# Example text file describing a gateset

# State prepared, specified as a state in the Pauli basis (I,X,Y,Z)
1/sqrt(2) 0 0 1/sqrt(2)

# State measured as yes outcome, also specified as a state in the Pauli basis
1/sqrt(2) 0 0 -1/sqrt(2)

1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1

1 0 0 0
0 1 0 0
0 0 0 -1
0 0 1 0

1 0 0 0
0 0 0 1
0 0 1 0
0 -1 0 0

IDENTITYVEC sqrt(2) 0 0 0
SPAMLABEL plus = rho0 E0
SPAMLABEL minus = rho0 remainder
with open("tutorial_files/Example_Gateset.txt","w") as gsetfile:

gateset4 ="tutorial_files/Example_Gateset.txt")
In [7]:
#All four of the above gatesets are identical.  See this by taking the frobenius differences between them:
assert(gateset1.frobeniusdist(gateset2) < 1e-8)
assert(gateset1.frobeniusdist(gateset3) < 1e-8)
assert(gateset1.frobeniusdist(gateset4) < 1e-8)

Viewing gate sets

In the cells below, we demonstrate how to print and access information within a GateSet.

In [8]:
#Printing the contents of a GateSet is easy
print("Gateset 1:\n", gateset1)
Gateset 1:
 rho0 =    0.7071        0        0   0.7071

E0 =    0.7071        0        0  -0.7071

Gi = 
   1.0000        0        0        0
        0   1.0000        0        0
        0        0   1.0000        0
        0        0        0   1.0000

Gx = 
   1.0000        0        0        0
        0   1.0000        0        0
        0        0        0  -1.0000
        0        0   1.0000        0

Gy = 
   1.0000        0        0        0
        0        0        0   1.0000
        0        0   1.0000        0
        0  -1.0000        0        0

In [9]:
#You can also access individual gates like they're numpy arrays:
Gx = gateset1['Gx'] # a Gate object, but behaves like a numpy array

#By printing a gate, you can see that it's not just a numpy array
print("Gx = ", Gx)

#But can be accessed as one:
print("Array-like printout\n", Gx[:,:],"\n")
print("First row\n", Gx[0,:],"\n")
print("Element [2,3] = ",Gx[2,3], "\n")

Id = np.identity(4,'d')
Id_dot_Gx =,Gx)
print("Id_dot_Gx\n", Id_dot_Gx, "\n")
Gx =  Fully Parameterized gate with shape (4, 4)
 1.00   0   0   0
   0 1.00   0   0
   0   0   0-1.00
   0   0 1.00   0

Array-like printout
 [[ 1.  0.  0.  0.]
 [ 0.  1.  0.  0.]
 [ 0.  0.  0. -1.]
 [ 0.  0.  1.  0.]] 

First row
 [ 1.  0.  0.  0.] 

Element [2,3] =  -1.0 

 [[ 1.  0.  0.  0.]
 [ 0.  1.  0.  0.]
 [ 0.  0.  0. -1.]
 [ 0.  0.  1.  0.]] 

Basic Operations with Gatesets

GateSet objects have a number of methods that support a variety of operations, including:

  • Depolarizing or rotating every gate
  • Writing the gate set to a file
  • Computing products of gate matrices
  • Printing more information about the gate set
In [10]:
#Add 10% depolarization noise to the gates
depol_gateset3 = gateset3.depolarize(gate_noise=0.1)

#Add 10% depolarization noise to the gates
rot_gateset3 = gateset3.rotate(rotate=0.1)
In [11]:
#Writing a gateset as a text file, "tutorial_files/Example_depolarizedGateset.txt", title="My Gateset")
In [12]:
#Computing the product of gate matrices (more on this in the next tutorial on gate strings)
print("Product of Gx * Gx = \n",depol_gateset3.product(("Gx", "Gx")), end='\n\n')
print("Probability of 'plus' spam label of gate string GxGx = ",'plus', ("Gx", "Gx")))
print("Probability of 'minus' spam label of gate string GxGx = ",'minus', ("Gx", "Gx")))
print("Probabilities as a dict = ",depol_gateset3.probs(("Gx", "Gx")))
Product of Gx * Gx = 
 [[  1.00000000e+00   0.00000000e+00   1.51390444e-16  -2.78344767e-16]
 [  0.00000000e+00   8.10000000e-01   0.00000000e+00   0.00000000e+00]
 [ -1.09201969e-16   0.00000000e+00  -8.10000000e-01  -3.17943723e-16]
 [  2.63428889e-16   0.00000000e+00   3.17943723e-16  -8.10000000e-01]]

Probability of 'plus' spam label of gate string GxGx =  0.9049999999999999
Probability of 'minus' spam label of gate string GxGx =  0.09499999999999997
Probabilities as a dict =  {'minus': 0.09499999999999997, 'plus': 0.9049999999999999}
In [13]:
#Printing more detailed information about a gateset
rho0 =    0.7071        0        0   0.7071

E0 =    0.7071        0        0  -0.7071

Gi = 
   1.0000        0        0        0
        0   0.9000        0        0
        0        0   0.9000        0
        0        0        0   0.9000

Gx = 
   1.0000        0        0        0
        0   0.9000        0        0
        0        0        0  -0.9000
        0        0   0.9000        0

Gy = 
   1.0000        0        0        0
        0        0        0   0.9000
        0        0   0.9000        0
        0  -0.9000        0        0

Choi Matrices:
('Choi(Gi) in pauli basis = \n', '   0.9250       +0j        0       +0j        0       +0j        0       +0j\n        0       +0j   0.0250       +0j        0       +0j        0       +0j\n        0       +0j        0       +0j   0.0250       +0j        0       +0j\n        0       +0j        0       +0j        0       +0j   0.0250       +0j\n')
('  --eigenvals = ', [0.024999999999999977, 0.024999999999999998, 0.024999999999999998, 0.92500000000000027], '\n')
('Choi(Gx) in pauli basis = \n', '   0.4750       +0j        0  +0.4500j        0       +0j        0       +0j\n        0  -0.4500j   0.4750       +0j        0       +0j        0       +0j\n        0       +0j        0       +0j   0.0250       +0j        0       +0j\n        0       +0j        0       +0j        0       +0j   0.0250       +0j\n')
('  --eigenvals = ', [0.024999999999999974, 0.024999999999999991, 0.025000000000000026, 0.92500000000000016], '\n')
('Choi(Gy) in pauli basis = \n', '   0.4750       +0j        0       +0j        0  +0.4500j        0       +0j\n        0       +0j   0.0250       +0j        0       +0j        0       +0j\n        0  -0.4500j        0       +0j   0.4750       +0j        0       +0j\n        0       +0j        0       +0j        0       +0j   0.0250       +0j\n')
('  --eigenvals = ', [0.024999999999999932, 0.025000000000000008, 0.025000000000000099, 0.92500000000000082], '\n')
('Sum of negative Choi eigenvalues = ', 0.0)
('rhoVec Penalty (>0 if invalid rhoVecs) = ', 1.1102230246251565e-16)
('EVec Penalty (>0 if invalid EVecs) = ', 0)
In [ ]: