Download
Download this file as Jupyter notebook: custom.ipynb.
Example: Writing Custom Noise Sources
This example demonstrates how to write a custom noise source for the simulator.
[2]:
import numpy as np
import trueq as tq
import trueq.simulation as tqs
A custom noise source can be implemented by subclassing
NoiseSource
and overriding its
apply()
method.
The apply()
method will be passed
three arguments: cycle_wrapper
, backend
, and circuit_cache
. The job of
apply is to give instructions to the backend
based on the contents of
cycle_wrapper
. The backend
is an instance of
SimulationBackend
whose runner contains methods
like process_gate()
and
process_superop()
which
the noise source’s apply()
can
use.
Note that cycle_wrapper
is not a Cycle
instance as one
might expect, but instead a thin wrapper called
CycleWrapper
which in turn wraps each cycle
operation as OpWrapper
.
These wrapper types are required to store metadata on each operation (also discussed
further below). However, noise sources will rarely need to deal with these wrapper
objects directly. This is because noise sources (by strongly encouraged convention,
though not by contract) own a Match
instance which
takes ownership of the mechanics of these wrappers. This is seen in the example below,
where the iter_gates
method is used to yield
gate objects with their corresponding gate labels.
predefine noise matrices to be applied to qubits following ideal gates note that rowstack_subsys is the superoperator convention required by the simulator
[3]:
p = 0.01
s1 = tq.math.Superop.from_kraus(
[np.sqrt(1 - p) * np.eye(2), np.sqrt(p) * tq.Gate.x.mat]
)
p = 0.02
s2 = tq.math.Superop.from_kraus(
[np.sqrt(1 - p) * np.eye(4), np.sqrt(p) * tq.math.random_unitary(4)]
)
class ExampleNoise(tqs.NoiseSource):
def __init__(self, match=None):
# this simulator hardcodes the subsystem dimension to 2
super().__init__(dim=2, match=match)
def make_circuit_cache(self, circuit):
# this return will be made available to apply() as circuit_cache for every
# cycle in the circuit
return set(circuit.labels)
def apply(self, cycle_wrappers, backend, circuit_cache):
# in this method we ask the backend to process certain operations
used_labels = set()
# loop through the gates in the cycle and ask the backend to process them
# note that we must set noise_only=False since we are simulating each gate
for labels, gate in self.match.iter_gates(cycle_wrappers, noise_only=False):
used_labels.update(labels)
# first do ideal simulation of this gate
backend.process_gate(labels, gate)
# now add some superoperator noise to each qubit the gate acts on
if len(labels) == 1:
backend.process_superop(labels, s1)
elif len(labels) == 2:
backend.process_superop(labels, s2)
# apply a 20 degree Z rotation to every qubit without a gate in this cycle
for label in circuit_cache.difference(used_labels):
backend.process_gate((label,), tq.Gate.rp("Z", 20))
Now we can instantiate a simulator that uses this noise source and do any simulator calculation. Here, we use the simulator to predict the output of a hypothetical KNR experiment.
[4]:
sim = tq.Simulator().append_noise_source(ExampleNoise())
cycle = {(0, 1): tq.Gate.cnot, 2: tq.Gate.x}
fit = sim.predict_knr(cycle, twirl=tq.Twirl("P", range(5)))
fit.plot.knr_heatmap()
Caching
There are two distinct caches available to a noise source. The first is a private
noise source attribute that is called _cache
(by convention, not by contract) in
built-in noise models which persists throughout the lifetime of the noise source. For
example, a gate-dependent noise model may consider caching gate noise if it is
expensive to recompute everytime the same gate is encountered.
The second cache comes as the third argument to the
apply()
method. This cache is
instantiated by
make_circuit_cache()
just before a
new circuit is simulated and is presented as the third argument to the apply method
for every cycle within that circuit. It typically stores information about the circuit
that is not available in every cycle, such as all of the labels the circuit acts on,
or which measurements it performs at the end.
Wrappers and metadata
The variable cycle_wrapper
in the example above has type
CycleWrapper
which is a thin wrapper for a cycle
and also wraps every cycle operation with
OpWrapper
. During simulation, everytime a new
cycle is encountered it is wrapped once, and the same wrapped cycle instance is passed
to all noise sources. The cycle wrapper exists to enable efficient looping logic used
by Match
and to persist operation wrappers for
multiple noise sources. The operation wrapper exists to store
two important pieces of information:
Whether some noise source has previously simulated the same operation of the cycle. This is stored as a boolean called
has_been_simulated
and is set to true whenever a match method is called with the argumentnoise_only=False
, as it is in the above example.Whether no noise sources (except the ideal final simulation if relevant) should touch the operation again. This is stored as a boolean called
no_more_noise
and is set to true whenever it is yielded by a match method whoseexclusive
value is true.
Download
Download this file as Jupyter notebook: custom.ipynb.