In this notebook, we'll demonstrate how to select preparation and measurement fiducials for the standard single-qubit $\{X_{\pi/2}, Y_{\pi/2}, I\}$ (or $\{X_{\pi/2}, Y_{\pi/2}\}$) gate set. The results are very straightforward to generalize for a non $\{X_{\pi/2}, Y_{\pi/2}, I\}$ single-qubit gateset or a multiqubit gate set.

Fiducial selection is not quite as much of a "dark art" as germ selection is, but there are nonetheless equally valid choices one can make in terms of inputs to the fiducial selection function. At present, we demonstrate only a subset of the functionality, but we endeavor to explain the remaining functionality as well.

In [1]:
from __future__ import print_function
In [2]:
#Import relevant modules.

import pygsti
import numpy as _np

from pygsti.algorithms import fiducialselection as FS

from pygsti.construction import std1Q_XYI
from pygsti.construction import std1Q_XY

import matplotlib.pyplot as plt
%matplotlib inline

import time

Different target gatesets require different fiducials. Thus, we must first define the target gateset.

It's worth noting that unlike with germ selection, we will use perfect gates. (In fact, imperfect gates for fiducial selection can introduce errors that we don't want.)

In [3]:
#Define the target gate set we will select fiducials for.
#Here, it is the standard X pi/2, Y pi/2, I gate set.

gs_target = std1Q_XYI.gs_target

Let's now try to actually pick out a fiducial set, using the function pygsti.algorithms.fiducialselection.optimize_integer_fiducials_slack.

To motivate fiducial selection, consider the following passage from quant-ph:1605.07674:

The purpose of the fiducials is to prepare a sufficiently diverse set of input states, and a sufficiently diverse set of measurements, to completely probe the operation of interest. 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|F_j\}$ are both informationally complete (IC). A set of matrices is IC if and only if it spans the vector space $\mathcal{B}(H)$ of matrices. 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.

We determine how linearly independent a fiducial set is in the following manner:

To evaluate a set of fiducials $\{F_i\}_{i=1}^k$, we start by forming 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$.

While we assume that there is only one preparation state $|\rho\rangle\rangle$, there may be multiple measurement effects (e.g., if we're dealing with a two-qubit system). When dealing with a collection of measurement effects $\{E_j\}$, the rows of $M$ are $\langle\langle E_j|F_i$ (see algorithms.fiducialselection.make_meas_mxs for full indexing explanation).

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 singular values of $MM^T$ (kwarg scoreFunc = 'all') or by the number of fiducials times the reciprocal of the smallest singular value of $MM^T$ (kwarg scoreFunc = 'worst'). In both cases, a lower score is better.

In the former 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 latter case, we are instead attempting minimize our insensitivity to the direction in Hilbert-Schmidt space that we are least sensitive to.

In order to actually select a good set of fiducials, we either:

  1. Generate a collection of many different candidate fiducials, and score subsets of these candidate fiducials in an effort to find the "best" subset of many fiducial sets, and then attempt to pick the best one.
    Or
  2. Generate a collection of sets of candidate fiducials, all of the same size.

In case 2 we exhaustively check all fiducial sets. For case 1, exhaustive search would typically be prohibitively expensive. Therefore, we use an integer program that starts by checking a single case, and then comparing it to "adjacent" fiducial sets (ones that are the same as the first, save for the addition or omission of a single fiducial). The integer program starts by essentially being a greedy algorithm that just tries iteratively in this fashion to find a fiducial set that has the best (lowest) score. However, it is easy to get stuck in a bad local minimum. Therefore, once a local minimum is found, we change our search method. First, we only allow fiducials sets that are smaller than the one at the local minimum. This is to expedite the process. Additionally, we relax our score-checking constraints, allowing the optimizer to "tunnel" out of the local minimum. This relaxation is quantified via either the fixedSlack or slackFrac parameter, explained below.

Before demonstrating fiducial selection, we briefly discuss a few relevant keyword arguments (some mentioned already above). This is because the fiducial selection output will strongly depend on several different inputs to the function optimize_integer_fiducials_slack. These inputs include:

  • prepOrMeas: Whether we are attempting to compute preparation fiducials or measurement fiducials.

  • fiducialList : The list of candidate germs from which the fiducials set will be chosen.

  • initialWeights : The initial subset of candidate fiducials which the optimizer tests. The default here is None, meaning that all candidate fiducials are included in the first fiducial set test.

  • fixedSlack : The absolute score a fiducial set is allowed to achieve. (That is, as higher scores are worse, once relaxation has begun, any fiducial set with a score that is higher than fixedSlack is rejected.)

  • slackFrac : The relative score a fiducial set is allowed to achieve. (That is, as higher scores are worse, once relaxation has begun, any fiducial set with a score that is higher than [current best score] * (1 + slackFrac) is rejected.)

Note: Only one of fixedSlack or slackFrac may be set. Typically we will use fixedSlack, and we find that fixedSlack ~1 is sufficient. These arguments determine the relaxation scheme used to reduce the fiducial list size.

  • forceEmpty: Whether or not the fiducial set must contain the empty string.

  • fixedNum : Whether or not we are forcing the fiducial set to be a fixed size

  • scoreFunc : Whether the objective function only tries to minimize how insensitive we are for our most insensitive direction in Hilbert-Schmidt space, or if it tries to make us as sensitive as possible to all directions in Hilbert-Schmidt space

Here we demonstrate particular choices for the above inputs. By parameter counting, one can see that these particular instances yield "optimal" results (as there are four elements of a state or measurement effect, and we choose four fiducials).

However, we make no claims of optimality for these choices in general (particularly when 2 or more qubits are considered).

End users are encouraged to experiment themselves with these inputs. They are also welcome to contact the pygsti development team at [email protected]

In [4]:
#Let's try to pick out a fiducial set.  

#First, we generate a candidate set which we'll attempt to prune.
#Here, we're looking at all gate string sequences of maximum length 2.

max_length = 2
gates = ['Gx','Gy']#We omit any identity operations here, as we don't want them in our fiducials.

#Important for the minlength arg to equal 0, so we include the empty string.
testFidList = pygsti.construction.list_all_gatestrings(gates,0,max_length)

Don't worry if the optimize_integer_fiducials_slack function below throws a divide by zero warning; this just means one of the tested cases was really bad.

In [5]:
#Compute the preparation fiducials

start = time.time()
prepFidList1 = FS.optimize_integer_fiducials_slack(gs_target,testFidList,prepOrMeas='prep',\
                                                   initialWeights=None,slackFrac=1)
end = time.time()
print('')
print("Fiducial selection completed in {0} seconds.".format(round(end-start, 7)))
print(prepFidList1)
Complete initial fiducial set succeeds.
Now searching for best fiducial set.
Starting fiducial set optimization. Lower score is better.
score = 64.0
weights = [1 0 0 0 1 1 1]
L1(weights) = 4
Final fiducial set succeeds.

Fiducial selection completed in 0.0173471 seconds.
[GateString({}), GateString(GxGy), GateString(GyGx), GateString(GyGy)]
In [6]:
# Compute the measurement fiducials 

start = time.time()
measFidList1 = FS.optimize_integer_fiducials_slack(gs_target,testFidList,prepOrMeas='meas',\
                                                   initialWeights=None,slackFrac=1)
end = time.time()
print('')
print("Fiducial selection completed in {0} seconds.".format(round(end-start, 7)))
print(measFidList1)
Complete initial fiducial set succeeds.
Now searching for best fiducial set.
Starting fiducial set optimization. Lower score is better.
score = 64.0
weights = [1 0 0 0 1 1 1]
L1(weights) = 4
Final fiducial set succeeds.

Fiducial selection completed in 0.0251639 seconds.
[GateString({}), GateString(GxGy), GateString(GyGx), GateString(GyGy)]
In [7]:
print("We have selected our preparation fiducials to be:")
for fid in prepFidList1:
    print('\t', fid)
We have selected our preparation fiducials to be:
	 {}
	 GxGy
	 GyGx
	 GyGy
In [8]:
print("We have selected our measurement fiducials to be:")
for fid in measFidList1:
    print('\t', fid)
We have selected our measurement fiducials to be:
	 {}
	 GxGy
	 GyGx
	 GyGy

The key property we wish for our fiducials to have is informational completeness. Below, we test whether our selected preparation and measurement fiducuials are in fact so.

In [9]:
FS.test_fiducial_list(gs_target,prepFidList1,'prep',returnAll=True)
Out[9]:
(True,
 [0.21922359359558488,
  0.49999999999999922,
  0.99999999999999967,
  2.2807764064044149],
 Score: 32.00000000000001, N: 4)
In [10]:
FS.test_fiducial_list(gs_target,measFidList1,'meas',returnAll=True)
Out[10]:
(True,
 [0.21922359359558485,
  0.49999999999999994,
  0.99999999999999978,
  2.2807764064044149],
 Score: 32.0, N: 4)

Note that in practice, we use a set of six fiducials both for prep and measure (which can be created using std1Q_XYI.fiducials). This is for greater numerical stability; for single-qubit GST, we recommend 6 preparation and 6 measurement fiducials, as the added cost is not too great, and this provides prep and measure fiducials corresponding to all 6 antipodal points on the Bloch sphere, providing a nice degree of symmetry.

However, for multiqubit GST, experimental resource constraints become more relevant, and we recommend simply directly using the outputs of the fiducial selection code.

That said, one can, for one or more quibits, force optimize_integer_fiducials_slack to return a fiducial set of fixed size. Instead of running an integer program over fiducial sets of different sizes, we can instead score all fiducial sets of a fixed size (that are subsets of the input set) and select the best one.
This can become expensive quickly, but it is very feasible for at least single-qubit gate sets, as exhibited below.

To turn this functionality on, set the fixedNum keyword argument to be equal to the fiducial set size you want.

If there does not exist an informationally complete fiducial set of the desired size (with score cutoff determined by the "threshold" kwarg"), fiducial selection should explicitly fail.

In [11]:
#Again, we'll try to pick out a fiducial set, but now we will insist that the set be of size 6.
#We'll look at all gate string sequences of maximum length 3.

max_length = 3
gates = ['Gx','Gy']

testFidList_force6 = pygsti.construction.list_all_gatestrings(gates,0,max_length)
In [12]:
#Let's again forceEmpty to be True, and see what we get for preparation fiducials.
start = time.time()
prepFidList1_force6 = FS.optimize_integer_fiducials_slack(gs_target,testFidList_force6,prepOrMeas='prep',\
                                                          initialWeights=None,slackFrac=1,fixedNum=6,\
                                                          forceEmpty=True)
end = time.time()
print('')
print("Fiducial selection completed in {0} seconds.".format(round(end-start, 7)))
print(prepFidList1_force6)
Complete initial fiducial set succeeds.
Now searching for best fiducial set.
Starting fiducial set optimization. Lower score is better.
Output set is required to be of size6
Total number of fiducial sets to be checked is2002.0
WARNING: If this is very large, you may wish to abort.
Fiducial selection completed in 0.3374081 seconds.
[GateString({}), GateString(Gx), GateString(Gy), GateString(GxGx), GateString(GxGxGx), GateString(GxGxGy)]
In [13]:
#Let's set forceEmpty = False
start = time.time()
prepFidList1_force6 = FS.optimize_integer_fiducials_slack(gs_target,testFidList_force6,prepOrMeas='prep',\
                                                          initialWeights=None,slackFrac=1,fixedNum=6,\
                                                          forceEmpty=False)
end = time.time()
print('')
print("Fiducial selection completed in {0} seconds.".format(round(end-start, 7)))
print(prepFidList1_force6)
#Conveniently, we get the same results as above!
Complete initial fiducial set succeeds.
Now searching for best fiducial set.
Starting fiducial set optimization. Lower score is better.
Output set is required to be of size6
Total number of fiducial sets to be checked is5005.0
WARNING: If this is very large, you may wish to abort.
Fiducial selection completed in 0.714879 seconds.
[GateString({}), GateString(Gx), GateString(Gy), GateString(GxGx), GateString(GxGxGx), GateString(GxGxGy)]
In [14]:
#Now let's make a measurement fiducial set with forceEmpty = True
start = time.time()
measFidList1_force6 = FS.optimize_integer_fiducials_slack(gs_target,testFidList_force6,prepOrMeas='meas',\
                                                          initialWeights=None,slackFrac=1,fixedNum=6,\
                                                          forceEmpty=True)
end = time.time()
print('')
print("Fiducial selection completed in {0} seconds.".format(round(end-start, 7)))
print(measFidList1_force6)
Complete initial fiducial set succeeds.
Now searching for best fiducial set.
Starting fiducial set optimization. Lower score is better.
Output set is required to be of size6
Total number of fiducial sets to be checked is2002.0
WARNING: If this is very large, you may wish to abort.
Fiducial selection completed in 0.2921982 seconds.
[GateString({}), GateString(Gx), GateString(Gy), GateString(GxGx), GateString(GxGxGx), GateString(GyGxGx)]
In [15]:
#Now let's make a measurement fiducial set with forceEmpty = False
start = time.time()
measFidList1_force6 = FS.optimize_integer_fiducials_slack(gs_target,testFidList_force6,prepOrMeas='meas',\
                                                          initialWeights=None,slackFrac=1,fixedNum=6,\
                                                          forceEmpty=False)
end = time.time()
print('')
print("Fiducial selection completed in {0} seconds.".format(round(end-start, 7)))
print(measFidList1_force6)
Complete initial fiducial set succeeds.
Now searching for best fiducial set.
Starting fiducial set optimization. Lower score is better.
Output set is required to be of size6
Total number of fiducial sets to be checked is5005.0
WARNING: If this is very large, you may wish to abort.
Fiducial selection completed in 0.7187691 seconds.
[GateString({}), GateString(Gx), GateString(Gy), GateString(GxGx), GateString(GxGxGx), GateString(GyGxGx)]
In [16]:
print("We have selected our size-6 preparation fiducials to be:")
for fid in prepFidList1_force6:
    print('\t', fid)
We have selected our size-6 preparation fiducials to be:
	 {}
	 Gx
	 Gy
	 GxGx
	 GxGxGx
	 GxGxGy
In [17]:
print("We have selected our size-6 measurement fiducials to be:")
for fid in measFidList1_force6:
    print('\t', fid)
We have selected our size-6 measurement fiducials to be:
	 {}
	 Gx
	 Gy
	 GxGx
	 GxGxGx
	 GyGxGx
In [18]:
FS.test_fiducial_list(gs_target,prepFidList1_force6,'prep',returnAll=True)
Out[18]:
(True,
 [0.99999999999999944, 0.99999999999999978, 1.0, 2.9999999999999996],
 Score: 20.000000000000007, N: 4)
In [19]:
FS.test_fiducial_list(gs_target,measFidList1_force6,'meas',returnAll=True)
Out[19]:
(True,
 [0.99999999999999833,
  0.99999999999999967,
  1.0000000000000002,
  2.9999999999999996],
 Score: 20.00000000000001, N: 4)
In [20]:
#Lastly, let's compare to the "standard" 6-fiducial set we use as our default:
print("std1Q_XYI.fiducials =", std1Q_XYI.fiducials)
print(FS.test_fiducial_list(gs_target,std1Q_XYI.fiducials,'prep',returnAll=True))
print(FS.test_fiducial_list(gs_target,std1Q_XYI.fiducials,'meas',returnAll=True))
std1Q_XYI.fiducials = [GateString({}), GateString(Gx), GateString(Gy), GateString(GxGx), GateString(GxGxGx), GateString(GyGyGy)]
(True, [0.99999999999999933, 1.0, 1.0000000000000007, 2.9999999999999991], Score: 20.0, N: 4)
(True, [0.99999999999999911, 1.0, 1.0000000000000004, 2.9999999999999996], Score: 20.000000000000004, N: 4)

The "standard" fiducials are very similar to the automatically selected ones, and score the same! The notable difference is that prep and measurement fiducials are different when automatically selected; our default sets are the same for both prep and measure. This is because each "standard" fiducial is symmetric; the automated fiducials reverse gate order between preparation and measure.

In [ ]: