This notebook demonstrates how to generate sets of fiducial and germ sequences which form the building blocks of the gate sequences used by long-sequence GST. As described in previous tutorials, by structuring the GST sequences as
preparation_fiducial + repeated_germ + measurement_fiducial
long-sequence GST is highly sensitive to all possible (within the space of allowed gate sets). Furthermore, by iteratively increase the number of germ repetitions in repeated_germ
, pyGSTi's iterative algorithms are able to avoid (usually!) local optima.
Both germ and fiducial sets are determined for a given "target" gate set. We currently assume that this gate set contains unitary gates, such that infinitely many gates may be performed without moving away from a pure state. It is almost always the case that the desired "target" operations are unitary, so this isn't a burdensome assumption. If this isn't the case, one should find perform fiducial and germ selection on the nearest unitary gate set.
The purpose of the preparation and measurement fiducial sequences, $\{F_i\}$ and $\{H_i\}$, is to prepare a sufficiently diverse set of input states, and a sufficiently diverse set of measurements, to completely probe an operation of interest - defined as the map that lies between the fiducials. This is achieved if (and only if) the input states $\{\rho_i\}\equiv \{F_i|\rho\rangle\rangle\}$ and the measurement effects $\{E_j\} \equiv \{\langle\langle E|H_j\}$ are both informationally complete (IC). A set of matrices is IC if and only if it spans the vector space of density matrices. For a Hilbert space of dimension $d$ this requires at least $d^2$ linearly independent elements.
In general, any randomly chosen set of $d^2$ states or effects will be IC. So, for single-qubit GST, we could choose $d^2=4$ random fiducial sequences. However, while the resulting $\{\rho_i\}$ and $\{E_j\}$ will almost certainly be linearly independent, they may be close to linearly dependent.
To evaluate a set of fiducials we form a matrix $M$, which will allow us to quantify how linearly independent the resulting $\{\rho_i\}$ or $\{E_j\}$ will be. If we are evaluating a set of preparation fiducials, then the i^th column of $M$ is $F_i|\rho\rangle\rangle$; if measurement fiducials, the columns are $\langle\langle E|H_i$. This notation assumes a single native preparation or measurement effect; in the case when there are more this simply adds more columns $M$.
We then form the square matrix $MM^T$. We either then score the fiducial set as the number of fiducials times the sum of the reciprocals of the eigenvalues of $MM^T$ (scoreFunc = 'all'
) or by the number of fiducials times the reciprocal of the smallest eigenvalue of $MM^T$ (scoreFunc = 'worst'
). In both cases, a lower score is better.
In the 'all'
case, we are attempting to make all the fiducials as uniformly informationally complete as possible, that is, we are trying to make our fiducial set as sensitive as possible to all directions in Hilbert-Schmidt space. In the 'worst'
case, we are instead attempting minimize our insensitivity to the direction in Hilbert-Schmidt space that we are least sensitive to.
The defining property which makes a set of gate sequences a "complete germ set" is the amplification of all possible gate errors. More precisely, the repetition of a complete set of germs, sandwiched between assumed-IC preparation and measurement fiducial sequences, will yield a sensitivity that scales with the number of times each germ is repeated to any direction in the space of GateSets
(defined by the gate set's parameterization). This completeness is relative a "target" gate set, just as in fiducial selection. While the detailed mathematics behind germ selection is beyond the scope of this tutorial, the essence of the algorithm is as follows. The Jacobian $J$ of a potential set of germs relative to the target GateSet
's parameters is constructed and the eigenvalues of $J^\dagger J$ are computed. If the number of large eigenvalues values equals its maximum - the number of non-gauge GateSet
parameters - then the germ set is deemed "amplificationally complete", and will amplify any gate error. More specifically, a germ set is scored using a combination of 1) the number of eigenvalues values above some threshold and 2) either the 'all'
or 'worst'
scoring function applied to the eigenvalues of $J^\dagger J$. Several technical points make this slightly more complicated:
GateSet
before performing the Jacobian analysis to mitigate the possibility of mischaracterizing a direction as being amplified when it isn't for all GateSet
s except the perfect one.If not all that made perfect sense, do not worry. The remainder of this tutorial focuses on how to do fiducial or germ selection using pyGSTi, and does not rely on a rock solid theoretical understanding of the methods.
The selection of fiducial and germ sequences in pyGSTi is similar in that each uses a numerical optimization which considers different possible "candidate" sets, and tries to find the one which scores the best. The modules pygsti.algorithms.fiducialselection
and pygsti.algorithms.germselection
contain the algorithms relevant to each type of sequence selection.
import pygsti
import pygsti.algorithms.fiducialselection as fidsel
import pygsti.algorithms.germselection as germsel
We'll begin by constructing a 1-qubit $X(\pi/2)$, $Y(\pi/2)$, $I$ gateset for which we will find germs and fiducials.
gs_target = pygsti.construction.build_gateset(
[2], [('Q0',)], ['Gi', 'Gx', 'Gy'],
["I(Q0)", "X(pi/2,Q0)", "Y(pi/2,Q0)"])
We begin by demonstrating the most autmoated and hands-off approach to computing fiducials and germs -- by just providing the target gate set and accepting the defaults for all remaining optinal arguments. Note that one may compute these in either order - fiducial selection is usually much faster, since the required computation is significantly less.
prepFiducials, measFiducials = fidsel.generate_fiducials(gs_target)
Using GRASP algorithm. Preparation fiducials: ['{}', 'Gx', 'Gy', 'GxGx'] Score: 31.999999999999986 Measurement fiducials: ['{}', 'Gx', 'Gy'] Score: 9.999999999999996
germs = germsel.generate_germs(gs_target)
Using greedy algorithm. Constructed germ set: ['Gi', 'Gx', 'Gy', 'GxGy', 'GiGiGiGiGiGx', 'GiGiGiGyGxGx', 'GiGiGiGxGyGx', 'GiGiGiGxGxGy', 'GxGxGyGxGyGy', 'GiGyGxGyGy'] Score: Score: major=-34.0 minor=184.78943191686915, N: 34
Now that we have germs and fiducials, we can construct the list of experiments we need to perform in order to do GST. The only new things to provide at this point are the sizes for the experiments we want to perform (in this case we want to perform between 0 and 256 gates between fiducial pairs, going up by a factor of 2 at each stage).
maxLengths = [2**n for n in range(8 + 1)]
listOfExperiments = pygsti.construction.make_lsgst_experiment_list(
gs_target, prepFiducials, measFiducials, germs, maxLengths)
There are many ways you can assume more control over the experiment design process. We'll only demonstrate a few here, but all options are discussed in the documentation for the various functions we've used.
There are a number of different algorithms available for germ selection. You can choose a non-default
algorithm by specifying the algorithm
keyword argument. Each of the available algorithms has a set of keyword arguments of its own with which you can more precisely specify how you want it to behave. These keyword arguments can be passed as a dictionary to generate_germs
through the keyword argument algorithm_kwargs
.
graspGerms = germsel.generate_germs(gs_target, algorithm='grasp', algorithm_kwargs={'iterations': 1})
Using GRASP algorithm. Constructed germ set: ['Gi', 'Gx', 'Gy', 'GxGy', 'GiGiGiGiGiGy', 'GiGiGiGiGxGy', 'GiGiGiGiGyGx', 'GiGiGxGxGxGy', 'GiGiGyGyGyGx', 'GxGxGyGyGxGy'] Score: Score: major=-34.0 minor=82.51221271869544, N: 34
Fiducial selection can be controlled in much the same way, using the same algorithms (except for 'greedy'
).
slackPrepFids, slackMeasFids = fidsel.generate_fiducials(gs_target, algorithm='slack',
algorithm_kwargs={'slackFrac': 0.25})
Using slack algorithm. Preparation fiducials: ['{}', 'GxGy', 'GyGx', 'GyGy'] Score: 32.00000000000001 Measurement fiducials: ['{}', 'GxGy', 'GyGx'] Score: 9.999999999999996
We can also adjust some algorithm-independent parameters for germ and fiducial selection. For instance, all of the algorithms currently rely on having a pool of gatestring from which they construct germs and fiducials. The size of this pool is set by specifying the longest germ or fiducial to include in this pool.
For germ selection, the default maximum germ length is 6, and we can see that our original construction indeed makes use of germs of length 6.
max([len(germ) for germ in germs])
6
We can try and set the maximum germ length to 5 and see what we get.
germsMaxLength5 = germsel.generate_germs(gs_target, candidateGermCounts={5: 'all upto'})
Using greedy algorithm. Constructed germ set: ['Gi', 'Gx', 'Gy', 'GiGiGiGiGy', 'GxGy', 'GiGxGiGyGx', 'GiGiGiGxGy', 'GiGiGiGyGx', 'GiGxGyGxGx', 'GiGyGyGxGy'] Score: Score: major=-34.0 minor=55266.3128015056, N: 34
max([len(germ) for germ in germsMaxLength5])
5
Sure enough, we now have a germ set with a shorter longest germ. If we get too ambitious in shortening the maximum
germ length, germ selection won't be able to find an amplificationally complete germ set. It will send a warning
message to stderr
if this happens and return None
.
germsMaxLength3 = germsel.generate_germs(gs_target, candidateGermCounts={3: 'all upto'})
print(germsMaxLength3)
Using greedy algorithm. None
WARNING: Complete initial germ set FAILS on gateset 0. WARNING: Aborting search.
Fiducial selection defaults to a maximum fiducial length of 2. This allows us to construct an informationally
complete set of states and measurements, but for this gateset we know that there is a uniformly
informationally complete set of states and measurements that require fiducials of up to length 3. We can find
that set of fiducials by telling generate_fiducials
to consider longer fiducials.
uniformPrepFids, uniformMeasFids = fidsel.generate_fiducials(gs_target, maxFidLength=3,
algorithm='grasp',
algorithm_kwargs={'iterations': 100})
Using GRASP algorithm. Preparation fiducials: ['{}', 'GxGx', 'Gy', 'Gx'] Score: 31.999999999999975 Measurement fiducials: ['{}', 'Gx', 'Gy'] Score: 9.999999999999996
As was the case with germ selection, if you are too aggressive in limiting fiducial length you may
constrain the algorithm to the extent that it cannot even find a set of fiducials to generate an
informationally complete set of states and measurements. In that case, it will also send a warning
message to stderr
and return None
for the preparation and measurement fiducial sets.
incompletePrepFids, incompleteMeasFids = fidsel.generate_fiducials(gs_target, maxFidLength=1)
Using GRASP algorithm. Measurement fiducials: ['{}', 'Gx', 'Gy'] Score: 9.999999999999996
WARNING: Complete initial fiducial set FAILS. WARNING: Aborting search.
print(incompleteMeasFids, incompletePrepFids)
[GateString({}), GateString(Gx), GateString(Gy)] None
There are several natural things to require of the returned germ and fiducial sets. For germ sets, you will usually
want the individual gates to be included as germs. If for some reason you don't want this, you can set the
force keyword argument to None
.
nonSingletonGerms = germsel.generate_germs(gs_target, force=None, candidateGermCounts={4: 'all upto'},
algorithm='grasp', algorithm_kwargs={'iterations': 5})
Using GRASP algorithm. Constructed germ set:
WARNING: Complete initial germ set FAILS on gateset 2. WARNING: Aborting search.
In fiducial selection, it is likewise natural to require the empty gate string to be in the
fiducial set. This requirement may be disabled by setting forceEmpty to False
. It is also
often desireable for identity gates to be left out of fiducials, since they add no diversity
to the set of states and measurements generated. You can allow identity gates in fiducials by
setting omitIdentity to False
.
A more common modification to the fiducial set requirements is to leave out additional gates from fiducials. This might be desireable if you have a multi-qubit system and you expect your 2-qubit gates to be of lower fidelity than your single-qubit gates. In this case you might want to construct fiducials from only single-qubit gates. A list of gates that you would like to omit from your fiducials can be provided as a list of gate labels to the gatesToOmit keyword argument.
Our gateset doesn't have multi-qubit gates, but we can demonstrate several pieces of this
functionality by setting omitIdentity to False
and omitting the identity manually using
gatesToOmit.
omitIdentityPrepFids, omitIdentityMeasFids = fidsel.generate_fiducials(gs_target, omitIdentity=False,
gatesToOmit=['Gi'])
Using GRASP algorithm. Preparation fiducials: ['{}', 'GxGx', 'Gx', 'Gy'] Score: 31.999999999999975 Measurement fiducials: ['{}', 'Gx', 'Gy'] Score: 9.999999999999996
omitIdentityPrepFids
[GateString({}), GateString(GxGx), GateString(Gx), GateString(Gy)]
omitIdentityMeasFids
[GateString({}), GateString(Gx), GateString(Gy)]
The various algorithms can tell you something of what's going on with them while they're running. By default, this output is silenced, but it can be turned on using the verbosity keyword argument.
verbosePrepFids, verboseMeasFids = fidsel.generate_fiducials(gs_target, verbosity=2)
Using GRASP algorithm. Complete initial fiducial set succeeds. Now searching for best fiducial set. Starting fiducial list optimization. Lower score is better. Starting iteration 1 of 5. Initial construction: ['{}', 'GxGx', 'GyGx', 'GxGy'] Local optimum: ['{}', 'Gx', 'Gy', 'GxGx'] Finished iteration 1 of 5. Starting iteration 2 of 5. Initial construction: ['{}', 'GxGx', 'Gy', 'Gx'] Local optimum: ['{}', 'GxGx', 'Gy', 'Gx'] Finished iteration 2 of 5. Starting iteration 3 of 5. Initial construction: ['{}', 'GxGx', 'Gy', 'GxGy'] Local optimum: ['{}', 'Gx', 'Gy', 'GxGx'] Finished iteration 3 of 5. Starting iteration 4 of 5. Initial construction: ['{}', 'GyGy', 'GxGy', 'GyGx'] Local optimum: ['{}', 'Gx', 'Gy', 'GxGx'] Finished iteration 4 of 5. Starting iteration 5 of 5. Initial construction: ['{}', 'GxGx', 'GyGx', 'GxGy'] Local optimum: ['{}', 'Gx', 'Gy', 'GxGx'] Finished iteration 5 of 5. Preparation fiducials: ['{}', 'GxGx', 'Gy', 'Gx'] Score: 31.999999999999975 Complete initial fiducial set succeeds. Now searching for best fiducial set. Starting fiducial list optimization. Lower score is better. Starting iteration 1 of 5. Initial construction: ['{}', 'GyGx', 'GxGy'] Local optimum: ['{}', 'Gx', 'Gy'] Finished iteration 1 of 5. Starting iteration 2 of 5. Initial construction: ['{}', 'Gy', 'Gx'] Local optimum: ['{}', 'Gy', 'Gx'] Finished iteration 2 of 5. Starting iteration 3 of 5. Initial construction: ['{}', 'GxGy', 'Gx'] Local optimum: ['{}', 'Gx', 'Gy'] Finished iteration 3 of 5. Starting iteration 4 of 5. Initial construction: ['{}', 'GxGy', 'GyGx'] Local optimum: ['{}', 'Gx', 'Gy'] Finished iteration 4 of 5. Starting iteration 5 of 5. Initial construction: ['{}', 'GxGy', 'GyGx'] Local optimum: ['{}', 'Gx', 'Gy'] Finished iteration 5 of 5. Measurement fiducials: ['{}', 'Gx', 'Gy'] Score: 9.999999999999996
silentGerms = germsel.generate_germs(gs_target, algorithm='slack', algorithm_kwargs={'maxIter': 5},
verbosity=0)