<jupyter_start><jupyter_text>Tutorial on creating a custom operator (e.g. gate)This tutorial demonstrates the process of creating your own gate operation.  One can view gate (or layer) operations in pyGSTi as simply parameterized process matrices: a mapping that associates with any given set of parameter values a process matrix.  This mapping is encapsulated by a `LinearOperator`-derived class in pyGSTi, and in addition to using the ones included with pyGSTi (e.g. `FullDenseOp`, see the [Operator tutorial](Operators.ipynb) for more examples) you're free to make your own.  That's exactly what we'll be doing here.There are lots of good reasons for doing this, the foremost being that you have a specific way you want to model a gate operation that is specific to your system's physics and not captured by pyGSTi's more generic built-in operation classes.  You also may want to make an operation whose parameters are exactly the "knobs" that you have access to in the lab.  Whatever the reason, pyGSTi has been designed to make the creation of custom operator types easy and straightforward.In this example, we'll be creating a custom 1-qubit gate operation.  It will be a $X(\pi/2)$-rotation that may have some amount of depolarization and "on-axis" overrotation, but no other imperfections.  Thus, it will only have to parameters: the depolarization and the overrotation amounts.Here's a class which implements this operation.  The comments explain what different parts do.<jupyter_code>import pygsti
import numpy as np

class MyXPi2Operator(pygsti.obj.DenseOperator):
    def __init__(self):
        #initialize with no noise
        super(MyXPi2Operator,self).__init__(np.identity(4,'d'), "densitymx") # this is *super*-operator, so "densitymx"
        self.from_vector([0,0]) 
        
    def num_params(self): 
        return 2 # we have two parameters
    
    def to_vector(self):
        return np.array([self.depol_amt, self.over_rotation],'d') #our parameter vector
        
    def from_vector(self, v, close=False, nodirty=False):
        #initialize from parameter vector v
        self.depol_amt = v[0]
        self.over_rotation = v[1]
        
        theta = (np.pi/2 + self.over_rotation)/2
        a = 1.0-self.depol_amt
        b = a*2*np.cos(theta)*np.sin(theta)
        c = a*(np.sin(theta)**2 - np.cos(theta)**2)
        
        # .base is a member of DenseOperator and is a numpy array that is 
        # the dense Pauli transfer matrix of this operator
        # Technical note: use [:,:] instead of direct assignment so id of self.base doesn't change
        self.base[:,:] = np.array([[1,   0,   0,   0],
                                  [0,   a,   0,   0],
                                  [0,   0,   c,  -b],
                                  [0,   0,   b,   c]],'d')
        if not nodirty: self.dirty = True  # mark that parameter vector may have changed
        
    def transform(self, S):
        # Update self with inverse(S) * self * S (used in gauge optimization)
        raise NotImplementedError("MyXPi2Operator cannot be transformed!")<jupyter_output><empty_output><jupyter_text>We'll add a `MyXPi2Operator` instance as the `"Gx"` gate in pyGSTi's standard {Idle, $X(\pi/2)$, $Y(\pi/2)$} model (see the [standard modules tutorial](StandardModules.ipynb) for more information on standard models).<jupyter_code>from pygsti.modelpacks import smq1Q_XYI
mdl = smq1Q_XYI.target_model()
mdl.operations[('Gxpi2',0)] = MyXPi2Operator()
print(mdl)<jupyter_output><empty_output><jupyter_text>Next, to demonstrate everything is working like it should, we'll optimize this model using Gate Set tomography (see the [GST overview tutorial](../../algorithms/GST-Overview.ipynb) for the details on what all this stuff does).  GST by default attempts to gauge optimize its final estimate to look like the target model (see the [gauge optimization tutorial](../../algorithms/advanced/GaugeOpt.ipynb) for details).  This would requires all of the operators in our model to implement the (gauge) `transform` method.  Because `MyXPi2Operator` doesn't, we tell GST not to perform any gauge optimization by setting `gaugeOptParams=False` below.<jupyter_code># Generate "fake" data from a depolarized version of the target (ideal) model
maxLengths = [1,2,4,8,16]
mdl_datagen = smq1Q_XYI.target_model().depolarize(op_noise=0.01, spam_noise=0.001)
listOfExperiments = pygsti.construction.make_lsgst_experiment_list(
    mdl_datagen, smq1Q_XYI.prep_fiducials(), smq1Q_XYI.meas_fiducials(), smq1Q_XYI.germs(), maxLengths)
ds = pygsti.construction.generate_fake_data(mdl_datagen, listOfExperiments, nSamples=1000,
                                            sampleError="binomial", seed=1234)

#Run GST *without* gauge optimization
results = pygsti.do_long_sequence_gst(ds, mdl, smq1Q_XYI.prep_fiducials(), smq1Q_XYI.meas_fiducials(),
                                      smq1Q_XYI.germs(), maxLengths, gaugeOptParams=False)<jupyter_output><empty_output><jupyter_text>**That's it!  We just ran GST with a custom operation.**Our `MyXPi2Operator`-containing model fits the data pretty well (compare the actual and expected $2\Delta \log \mathcal{L}$ values printed above).  This makes sense because the data was generated by a model containing only depolarization errors on the gates, and our custom gate class can model this type of noise.  We expect, since we know how the data was generated, that the `"Gx"` gate depolarizes with magnitude $0.01$ and has no (zero) over-rotation.  Indeed, this is what we find when we look at `"Gx"` of the estimated model:<jupyter_code>mdl_estimate = results.estimates['default'].models['final iteration estimate']
print(mdl_estimate[('Gxpi2',0)])
est_depol, est_overrotation = mdl_estimate[('Gxpi2',0)].to_vector()
print("Estimated Gx depolarization =",est_depol)
print("Estimated Gx over-rotation =",est_overrotation)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09144792548687553, 'fraction_numerical': 0.012701100762066046, 'mean_word_length': 3.7899432278994323, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 9, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '124488', 'license': 'Apache-2.0', 'n_tokens_mistral': 1822, 'n_tokens_neox': 1730, 'n_words': 638, 'path': 'jupyter_notebooks/Tutorials/objects/advanced/CustomOperator.ipynb', 'repo_name': 'lnmaurer/pyGSTi', 'size': '5905'}