#!/usr/bin/env python # coding: utf-8 # # Gate String Tutorial # This tutorial will show you how to create and use `GateString` objects, which represent an ordered sequence (or "string") of gate set operations. In most cases, a `GateString` may optionally begin with a state-preparation label and end with a POVM label, and contains gate labels as all its other elements. In almost all cases, you'll be using a list (or even a list of lists!) of `GateString`s, so we'll often be talking about "gate string lists". # # A `GateString` object is nearly identical, and sometimes interchangeable, with a Python tuple of *gate labels* (i.e. the names beginning with `G` that label the gate operations in a `GateSet`) or *instrument labels* (beginning with `I`), sometimes sandwiched between a state preparation and POVM (measurement) label. `GateString`s can be accessed and operated upon just as a standard Python tuple. The primary difference between a `GateString` and a tuple is that a `GateString` also contains a "string representation" of the gate sequence. This string representation gets carried along for the ride until it's needed, typically when writing a the gate string to a file. The string representation must *evaluate*, using pyGSTi's allowed text format for gate strings (see below), to the tuple-of-gate-labels, or "tuple representation". The string representation is intended to contain a compact and intuitive human-readable form of the gate sequence that is used for display purposes. For example, the gate string `('Gx','Gx','Gx','Gx','Gx')` might have the string representation `"Gx^5"`. If needed, the tuple and string representations of any `GateString` `g` can be accessed via `g.tup` or `tuple(g)` and `g.str` or `str(g)` respectively. # # Gate strings are central to Gate Set Tomography, as they describe both real and "simulated" experiments. A `GateString`'s ordered sequence tells the experimentalist which gates they must execute on their hardware and likewise what order to compose (i.e. multiply) the gate matrices contained in a `GateSet`. When a state preparation or POVM label is omitted from the beginning or end of a gate string, respectively, one must be *inferred*. This is currently only possible when the relevant `GateSet` object holds only a single state preparation or POVM. The outcomes of an experiment are labeled by 1-tuples of the given (or inferred) POVM effect labels. When the gate string contains instruments (which produce intermediate measurement outcomes), the "outcome label" is a tuple of length greater than one, where each element correponds to the outcome of a single instrument or the final POVM. Thus, by repeating an experiment one obtains counts and thereby frequencies for each outcome label. Given a `GateSet` one can obtain corresponding probabilities by muliplying gate matrices and contracting the product between a state preparation and POVM effect vector. # # The **ordering direction** is important. The elements of a `GateString` are read from **left-to-right**, meaning the first (left-most) gate label is performed first. This is very natural for experiments since one can read the gate string as a script, executing each gate as one reads from left to right. However, since we insist on "normal" matrix multiplication conventions, the ordering of the matrix product is *reversed* from that of the gate string. For example, the gate string `('Ga','Gb','Gc')`, in which Ga is performed first, corresponds to the matrix product $G_c G_b G_a$. The probability of this gate string for a SPAM label associated with the (column) vectors ($\rho_0$,$E_0$) is given by $E_0^T G_c G_b G_a \rho_0$, which can be interpreted as "prepare state 0 first, then apply gate A, then B, then C, and finally measure effect 0". While this nuance is typically hidden from the user (the `GateSet` functions which compute products and probabilities from `GateString`s perform the order reversal internally), it becomes very important if you plan to perform such products by hand. # # We'll now go over some examples of how to create and use a single `GateString`. # In[1]: from __future__ import print_function # In[2]: import pygsti # the main pyGSTi module import pygsti.construction as pc #shorthand from pygsti.construction import std1Q_XY #a standard gateset & peripherals # ## A simple example: the single `GateString` # The cell below show how to create a `GateString` object from a tuple, optionally with a corresponding string representation. It demonstrates how to access the tuple and string representations directly, and the tuple-like operations that can be performed on a `GateString`. # In[3]: #Construction of a GateString s1 = pygsti.objects.GateString( ('Gx','Gx') ) # from a tuple s2 = pygsti.objects.GateString( ('Gx','Gx'), "Gx^2" ) # from tuple and string representations (must match!) s3 = pygsti.objects.GateString( None, "Gx^2" ) # from just a string representation #All of these are equivalent (even though their string representations aren't -- only tuples are compared) assert(s1 == s2 == s3) #Printing displays the string representation print("Printing") print("s1 = %s" % s1) print("s2 = %s" % s2) print("s3 = %s" % s3, end='\n\n') #Casting to tuple displays the tuple representation print("Printing tuple(.)") print("s1 =", tuple(s1)) print("s2 =", tuple(s2)) print("s3 =", tuple(s3), end='\n\n') #Access to tuple or string representation directly: print("s1.tup =", s1.tup, ", s1.str = ", s1.str) print("tuple(s1) =", tuple(s1), ", str(s1) = ", str(s1), end='\n\n') #Operations assert(s1 == ('Gx','Gx')) #can compare with tuples s4 = s1+s2 #addition (note this concatenates string reps) s5 = s1*3 #integer-multplication (note this exponentiates in string rep) print("s1 + s2 = ",s4, ", tuple = ", tuple(s4)) print("s1*3 = ",s5, ", tuple = ", tuple(s5), end='\n\n') # ## List Construction Functions: `pygsti.construction` and `create_gatestring_list` # Usually you'll be working with entire lists of `GateString` objects which define some part of the experiments utilized by Gate Set Tomography. pyGSTi provides several functions for constructing gate string lists, which we not demonstrate. # # The workhorse function is `pygsti.construction.create_gatestring_list`, which executes its positional arguments within a nested loop given by iterable keyword arguments. That's a mouthful, so let's look at a few examples: # In[4]: As = [('a1',),('a2',)] Bs = [('b1','b2'), ('b3','b4')] def rep2(x): return x+x list1 = pc.create_gatestring_list("a", a=As) list2 = pc.create_gatestring_list("a+b", a=As, b=Bs, order=['a','b']) list3 = pc.create_gatestring_list("R(a)+c", a=As, c=[('c',)], R=rep2) print("list1 = %s" % list(map(tuple, list1))) print("list2 = %s" % list2) print("list3 = %s" % list(map(str,list3))) # Many of the gate sequences used by Gate Set Tomography are composed of three parts. A "preparation fiducial" sequence is followed by a "repeated germ" sequence, which is followed by a "measurement fiducial" sequence. We won't get into why this structure is used, but simply use this fact to motivate looking at gate strings of the form $f_1 + R(g) + f_2$, where the $f_1$ and $f_2$ fiducial sequences are simple short sequences are $R(g)$ is a possibly long sequence that is generated by repeating a short sequence $g$ called a "germ". # # It is possible to generate "repeated germ" sequences in several ways using the functions **`pygsti.construction.repeat_`*xxx* **. In modern GST, germ sequences are always repeated an *integer* number of times rather than being truncated to a precise length, so `repeat_with_max_length` is used instead of `repeat_and_truncate`. Below we demonstrate the use of these functions. # In[5]: print(pc.repeat_and_truncate(('A', 'B', 'C'), 5)) #args (x,N): repeat x until it is exactly length N print(pc.repeat_with_max_length(('A', 'B', 'C'), 5)) #args (x,N): repeat x the maximum integer number of times so len(x) < N print(pc.repeat_count_with_max_length(('A', 'B', 'C'), 5)) #args (x,N): the maximum integer number of times so len(x) < N # We can combine a repeated germ sequence between two fiducial sequences using `create_gatestring_list`. This demonstrates the power of the `create_gatestring_list` to perform nested loops. We also introduce the "bulk-conversion" function `gatestring_list`, which creates a list of `GateString` objects from a list of tuples. # In[6]: fids = pc.gatestring_list( [ ('Gf0',), ('Gf1',) ] ) #fiducial strings germs = pc.gatestring_list( [ ('G0',), ('G1a','G1b')] ) #germ strings gateStrings1 = pc.create_gatestring_list("f0+germ*e+f1", f0=fids, f1=fids, germ=germs, e=2, order=["germ","f0","f1"]) print("gateStrings1 = \n", "\n".join(map(str,gateStrings1)),"\n") gateStrings2 = pc.create_gatestring_list("f0+T(germ,N)+f1", f0=fids, f1=fids, germ=germs, N=3, T=pc.repeat_and_truncate, order=["germ","f0","f1"]) print("gateStrings2 = \n", "\n".join(map(str,gateStrings2)),"\n") gateStrings3 = pc.create_gatestring_list("f0+T(germ,N)+f1", f0=fids, f1=fids, germ=germs, N=3, T=pc.repeat_with_max_length, order=["germ","f0","f1"]) print("gateStrings3 = \n", "\n".join(map(str,gateStrings3)), "\n") # In addition to `create_gatestring_list`, the **`pygsti.construction.list_`*xxx* ** functions provide ways of constructing common gate string lists. The example below shows how to construct all possible gate strings within a certain length range, as well as how to construct the set of gate strings needed to run Linear Gate Set Tomography given a set of fiducial strings. # In[7]: myGates = [ 'Gx', 'Gy' ] #gate labels -- often just gateset.gates.keys() allStringsInLengthRange = pc.list_all_gatestrings(myGates, minlength=0, maxlength=2) print("\nAll strings using %s up to length 2 = \n" \ % str(myGates), "\n".join(map(str,allStringsInLengthRange))) # In[8]: myFiducialList = pc.gatestring_list([ ('Gf1',), ('Gf2',) ]) #list of fiducials lgstStrings = pc.list_lgst_gatestrings(myFiducialList,myFiducialList,myGates) print("\nLGST strings = \n","\n".join(map(str,lgstStrings))) # ## Manipulating `GateStrings` # Sometimes it is useful to manipulate a `GateString` (or a list of them) via find & replace operations. The `manipulate_gatestring` and `manipulate_gatestring_list` functions take as input a set of replacement "rules" and process one or more `GateString` objects accordingly. For example, the rules # # - ab $\rightarrow$ AB' (if B follows A, prime B) # - BA $\rightarrow$ B''A (if B precedes A, double-prime B) # - CA $\rightarrow$ CA' (if A follows C, prime A) # - BC $\rightarrow$ BC' (if C follows B, prime C) # # are specified by the dictionary: # In[9]: sequenceRules = [ (("A", "B"), ("A", "B'")), (("B", "A"), ("B''", "A")), (("C", "A"), ("C", "A'")), (("B", "C"), ("B", "C'"))] # Will produce the output: # - BAB $\rightarrow$ B''AB' # - ABA $\rightarrow$ AB'A (frustrated!) # - CAB $\rightarrow$ CA'B' # - ABC $\rightarrow$ AB'C' # In[10]: from pygsti.objects import GateString from pygsti.construction import manipulate_gatestring print(manipulate_gatestring(GateString(tuple('BAB')), sequenceRules)) print(manipulate_gatestring(GateString(tuple('ABA')), sequenceRules)) print(manipulate_gatestring(GateString(tuple('CAB')), sequenceRules)) print(manipulate_gatestring(GateString(tuple('ABC')), sequenceRules)) # In[11]: # You can also process an entire list of gate strings in bulk orig_lst = pygsti.construction.gatestring_list([ tuple('BAB'), tuple('ABA'), tuple('CAB'), tuple('ABC')]) lst = pygsti.construction.manipulate_gatestring_list(orig_lst, sequenceRules) print('\n'.join([str(s) for s in lst])) # ## Gate Label "Aliases" # A similar but simpler type of manipulation called "gate label aliasing" is used in pyGSTi to map a gate label into another gate label **only for `DataSet` lookups**. The mapping is similar to `manipulate_gatestring`'s find & replace functionality, except that (at least currently) the string to find can be only a single gate label (and so isn't even a string at all). The support for gate label aliasing within pyGSTi's algorithms aids in mapping many `GateSet` models onto the same data (often with simpler gate labelling). # In[12]: #TODO: remove Aliasing or provide examples?