While pyGSTi is able to support several common types of 2-qubit gates, the space of all possible 2-qubit gates is so large that some users will need to construct their own particular 2-qubit gate "from scratch". In this example, we look at how to construct a 2-qubit model manually. We'll use pygsti.construction.create_explicit_model
to construct the single-qubit gates and we'll create a "non-standard" 2-qubit gate by specifying the unitary operation it performs as a $4\times 4$ matrix.
To perform GST on such a model, one may need to compute new sets of fiducials and/or germ sequences. Once these are obtained, 2-qubit GST can be run just as in the example which uses one of pyGSTi's built-in 2Q models.
import pygsti
Since the space of single-qubit gates is relatively small, we'll assume that the single-qubit gates in our model are able to be specified using pygsti.construction.create_explicit_model
. So we'll start by construct a Model
object containing all but the two-qubit gate(s) using create_explicit_model
.
target_model = pygsti.models.modelconstruction.create_explicit_model_from_expressions(
[('Q0','Q1')],['Gii','Gix','Giy','Gxi','Gyi'],
[ "I(Q0):I(Q1)", "X(pi/2,Q1)", "Y(pi/2,Q1)", "X(pi/2,Q0)", "Y(pi/2,Q0)" ],
effect_labels=['00','01','10','11'], effect_expressions=["0","1","2","3"])
There are lots of arguments to this function, so let's review what they mean:
[('Q0','Q1')]
= interpret this 4-d space as that of two qubits 'Q0', and 'Q1' (note these labels must begin with 'Q'!)"Gix"
= operation label; can be anything that begins with 'G' and is followed by lowercase letters"X(pi/2,Q1)"
= pi/2 single-qubit x-rotation gate on the qubit labeled Q1"rho0"
= prep label; can be anything that begins with "rho"'10'
= designates a POVM effect label whose corresponding vector is given by the effectExpressions
argument."2"
= a prep or effect expression indicating a projection/preparation of the 3rd (b/c 0-based) computational basis elementYou can also explicity add identity operations, e.g. "I(Q0)"
, to the rotation gates to get the same model (see mdl_targetB
below), and this same syntax can be used for non-entangling 2-qubit gates, e.g. "X(pi/2,Q0):X(pi/2,Q1)"
.
mdl_targetB = pygsti.models.modelconstruction.create_explicit_model_from_expressions(
[('Q0','Q1')],['Gii','Gix','Giy','Gxi','Gyi','Gcnot'],
[ "I(Q0):I(Q1)", "I(Q0):X(pi/2,Q1)", "I(Q0):Y(pi/2,Q1)", "X(pi/2,Q0):I(Q1)", "Y(pi/2,Q0):I(Q1)", "CNOT(Q0,Q1)" ],
effect_labels=['00','01','10','11'], effect_expressions=["0","1","2","3"])
assert(abs(target_model.frobeniusdist(mdl_targetB)) < 1e-6)
If our 2-qubit gate happens to be one that can be specified using create_explicit_model
then we can just use it to construct the entire Model
and be done. Currently, create_explicit_model
can create any controlled $X$, $Y$, or $Z$ rotation using CX
, CY
and CZ
, as well as the standard CNOT
and CPHASE
gates. Below we demonstrate creation with the CNOT gate. The resulting Model
is then identical to pygsti.modelpacks.legacy.std2Q_XYCNOT.target_model()
.
mdl_withCNOT = pygsti.models.modelconstruction.create_explicit_model_from_expressions(
[('Q0','Q1')],['Gii','Gix','Giy','Gxi','Gyi','Gcnot'],
[ "I(Q0):I(Q1)", "I(Q0):X(pi/2,Q1)", "I(Q0):Y(pi/2,Q1)", "X(pi/2,Q0):I(Q1)", "Y(pi/2,Q0):I(Q1)", "CNOT(Q0,Q1)" ],
effect_labels=['00','01','10','11'], effect_expressions=["0","1","2","3"])
#Note this is the same model as one of pyGSTi's standard models:
from pygsti.modelpacks.legacy import std2Q_XYICNOT
assert(abs(mdl_withCNOT.frobeniusdist(std2Q_XYICNOT.target_model())) < 1e-6)
Thus, since our target_model
just contains the 1-qubit gates of std2Q_XYCNOT.target_model
, we could also create has all the 1-qubit gates are the same a third way to obtain target_model
is to just remove Gcnot
from the standard model:
mdl_targetC = std2Q_XYICNOT.target_model()
del mdl_targetC.operations['Gcnot']
assert(abs(target_model.frobeniusdist(mdl_targetC)) < 1e-6)
We're assuming that create_explicit_model
can't make the 2-qubit gate we want, so we'll need to create our own. Below we demonstrate how to create a 2-qubit gate from a given unitary which acts on the 2-qubit, 4-dimensional, state space.
import numpy as np
#Unitary in acting on the state-space { |A>, |B>, |C>, |D> } == { |00>, |01>, |10>, |11> }.
# This unitary rotates the second qubit by pi/2 in either the (+) or (-) direction based on
# the state of the first qubit.
myUnitary = 1./np.sqrt(2) * np.array([[1,-1j,0,0],
[-1j,1,0,0],
[0,0,1,1j],
[0,0,1j,1]])
#Convert this unitary into a "superoperator", which acts on the
# space of vectorized density matrices instead of just the state space.
# These superoperators are what GST calls "gates".
mySuperOp_stdbasis = pygsti.unitary_to_std_process_mx(myUnitary)
#After the call to unitary_to_process_mx, the superoperator is a complex matrix
# in the "standard" or "matrix unit" basis given by { |A><A|, |A><B|, etc }.
# For use in GST, we want to work with a *real* matrix in either the
# Gell-Mann or Pauli-product basis. Here we choose the Pauli-product basis,
# which is typically more intuitive when working with 2 qubits.
mySuperOp_ppbasis = pygsti.change_basis(mySuperOp_stdbasis, "std", "pp")
#The resulting superoperator in the Pauli-product basis is exactly
# what goes into the Model object, which can be set using
# dictionary syntax. The line below names our two-qubit gate 'Gtq'
target_model['Gtq'] = mySuperOp_ppbasis
We're done creating our 2-qubit model, target_model
, printed below. To run 2-qubit GST with this model we ideally would generate fiducials and germs specifically for it. Actually, since the 1-qubit gates are the same as other standard 2Q models, and the fiducial seqeuences for these standard sets only contain 1-qubit gates, we can just use the fiducial sets from a standard set (e.g. std2Q_XYCNOT
). The germs should be computed, though typically you can get away with just using the germ set of a standard model and replacing it's 2-qubit gate with the custom one - so it can be worth checking whether such a set is complete before running a full germ-selection procedure.
print(target_model)