MitRes¶
A typical process for running experiments on quantum devices is as follows: first a logical quantum circuit is constructed, next it is compiled to fit to the constraints of the quantum device of choice (such as the connectivity graph or set of gate primitives) before being sent to the device via the hardware manufacturers language of choice. Finally results are returned, typically as a sequence of Shots, each shot being a single set of bit corresponding to the measurement result of each qubit.
In its basic capacity the MitRes.run method will run
each of these tasks sequentially.
from qermit import MitRes, CircuitShots
from pytket import Circuit
from pytket.extensions.qiskit import AerBackend
mitres = MitRes(backend = AerBackend())
c = Circuit(2,2).H(0).Rz(0.25,0).CX(1,0).measure_all()
results = mitres.run([CircuitShots(Circuit = c, Shots = 50)])
print(results[0].get_counts())
Counter({(0, 0): 25, (1, 0): 25})
In the above code snippet a Bell circuit is simulated on the AerBackend, returning a
pytket BackendResult object.
The MitRes object holds a graph of MitTask (or TaskGraph, see later clarification) objects (a TaskGraph).
A MitTask object is a pure function that computes some basic step in a typical experiment.
When the run function is called, a topological sort is applied to the graph to order these tasks. Each is then run sequentially.
The MitTask objects held in a MitRes can be visualised with the MitRes.get_task_graph method.
mitres.get_task_graph()
It is also possible to import and use the MitTask objects directly.
from qermit.taskgraph import backend_handle_task_gen, backend_res_task_gen
sim_backend = AerBackend()
circuits_to_handles_task = backend_handle_task_gen(sim_backend)
handles_to_results_task = backend_res_task_gen(sim_backend)
print(circuits_to_handles_task)
print(handles_to_results_task)
handles = circuits_to_handles_task(([CircuitShots(Circuit = c, Shots = 50)],))
results = handles_to_results_task(handles)
print(results[0][0].get_counts())
<MitTask::CircuitsToHandles>
<MitTask::HandlesToResults>
Counter({(1, 0): 30, (0, 0): 20})
You may notice that the arguments of the MitTask objects are different, requiring
Tuple type objects. This is necessary for the execution in the TaskGraph.run method.
All typing information is accessible via the qermit documentation.
Error-mitigation with MitRes¶
To produce a MitRes object that executes an error-mitigation protocol when the MitRes.run
method is called, additional MitTask objects need to be added to its task graph.
The defining characteristic of a MitRes object is that the first MitTask object
in its sorted graph requires a List[CircuitShots] object as its sole argument and that
the final MitTask object in its sorted graph returns a List[BackendResult] object.
This is a crucial type constraint required for the combining of error-mitigation methods.
Given this, there are two viable approaches to producing error-mitigation MitRes objects, either
extending a MitRes object with new MitTask objects under strict type constraints, or constructing
a TaskGraph object with relaxed type constraints on internal tasks and then casting to a MitRes object at completion.
Extending MitRes with MitTask¶
We have already seen that the basic construction of a MitRes object has two MitTask objects, one
submitting Circuit to a pytket Backend to retrieve unique handles, the other submitting these handles
back to the Backend to retrieve BackendResult.
The MitRes.append and MitRes.prepend methods can be used to extend the
MitTask objects the MitRes._task_graph attribute holds.
As an example, Let’s construct a basic MitRes object and then prepend a task that compiles circuits. While a common
task in any experiment workflow, the basic MitRes constructor does not add a MitTask for compilation.
This is because error-mitigation methods often produce circuits with very specific structures that need to be retained.
Even basic compilation may destroy these structures.
from qermit import MitTask
A MitTask object is defined by four attributes: a name, a function it computes, the number of arguments to said
function and the number of elements in a Tuple it returns.
Let’s define a basic function that compiles Circuits to a given Backend.
from pytket import Circuit
from pytket.backends import Backend
from typing import List
def compile_circuits(backend: Backend, circuits: List[Circuit]) -> List[Circuit]:
for c in circuits:
backend.compile_circuit(c)
return circuits
This is a straightforward function definition for those familiar with pytket and completes our goal.
However, this will require some minor modification before we can add it to our MitRes object.
We have already noted that a MitRes object is defined by the property that the MitRes.run method
takes a List[CircuitShots] as an argument and returns a List[BackendResult]. Considering this,
the MitRes.append and MitRes.prepend methods have restrictions on what MitTask objects they can
add to a task graph.
The MitRes.append method can only append tasks that have one out wire and return Tuple[List[BackendResult]] from
the internal method, while the MitRes.prepend method can only prepend tasks that have one in wire and expects a
single argument of type List[CircuitShots] to its internal method. The Tuple type is necessary for piping data
through the internal graph.
Let’s rework the compile_circuits method to fit the List[CircuitShots] constraint.
from qermit import CircuitShots
from typing import Tuple
def compile_circuit_shots(backend: Backend, circuit_shots: List[CircuitShots]) -> Tuple[List[CircuitShots]]:
compiled_circuit_shots = []
for cs in circuit_shots:
compiled_circuit = backend.get_compiled_circuit(cs.Circuit)
compiled_circuit_shots.append((compiled_circuit, cs.Shots))
return (compiled_circuit_shots,)
Notice that MitRes.prepend does not allow tasks that pass Backend objects as arguments. When the MitRes
class constructor is called with a given Backend, this same Backend is used to construct <MitTask::CircuitsToHandles>
and <MitTask::HandlesToResults> objects via MitTask generator functions. In this way each method references the same
Backend object, meaning it can be used to store and pass basic information such as device characteristics.
Let’s work the compile_circuit_shots method here into a similar generator function for a MitTask object.
def backend_compile_circuit_shots_task_gen(
backend: Backend
) -> MitTask:
def compile_circuit_shots(obj, circuit_shots: List[CircuitShots]) -> Tuple[List[CircuitShots]]:
compiled_circuit_shots = []
for cs in circuit_shots:
compiled_circuit = backend.get_compiled_circuit(cs.Circuit)
compiled_circuit_shots.append((compiled_circuit, cs.Shots))
return (compiled_circuit_shots,)
return MitTask(
_label="CompileCircuitShots", _n_in_wires=1, _n_out_wires=1, _method=compile_circuit_shots
)
When called, backend_compile_circuit_shots_task_gen will return a MitTask object with a callable that
compiles a List[CircuitShots] to the defined backend.
from pytket.extensions.qiskit import AerBackend
sim_backend = AerBackend()
mit_task = backend_compile_circuit_shots_task_gen(sim_backend)
print(mit_task)
<MitTask::CompileCircuitShots>
The callable expects a Tuple of the arguments to the MitTask._method attribute.
test_circuit_shots = [CircuitShots(Circuit = Circuit(2).CZ(0,1).measure_all(), Shots = 10)]
test_results = mit_task((test_circuit_shots,))
print(test_results)
([CircuitShots(Circuit=[TK1(0.5, 0.5, 0.5) q[1]; CX q[0], q[1]; Measure q[0] --> c[0]; TK1(0.5, 0.5, 0.5) q[1]; Measure q[1] --> c[1]; ], Shots=10)],)
We can see that the circuit has been compiled to the AerBackend gate set primitive and returned a suitable type - Let’s prepend it to
the basic MitRes object.
mit_res = MitRes(sim_backend)
mit_res.prepend(mit_task)
mit_res.get_task_graph()
If we were to use the MitRes.run method now to run an experiment, all circuits would be compiled with sim_backend before
being executed on the hardware. The MitRes.append method works similarly - later examples will show it in use.
Constructing MitRes from TaskGraph¶
Error-mitigation methods can involve complicated sequences of tasks to work. The strict type requirements of the
MitRes prepend and append rules preserve properties required for combining error-mitigation methods, but restrict
the possible structure and order of tasks.
If a more complicated structure of tasks is required to perform the mitigation, a MitRes object can be cast
as a TaskGraph object without the same restrictions. If the final object after construction respects the
MitRes type constraints then it can be cast back as a MitRes object later.
from qermit import TaskGraph
sim_backend = AerBackend()
task_graph = TaskGraph().from_TaskGraph(MitRes(sim_backend))
task_graph.get_task_graph()
Additionally to the relaxed type constraints of TaskGraph.prepend and TaskGraph.append in relation to MitRes,
the TaskGraph class has additional construction methods: TaskGraph.add_n_wires and TaskGraph.parallel.
task_graph.add_n_wires(2)
task_graph.get_task_graph()
The TaskGraph.add_n_wires method adds n new edges between the input and output vertices.
Similarly, the TaskGraph.parallel method adds a new path between the input and output vertices,
but with a MitTask or TaskGraph object (or child) inserted.
We can add another MitRes object in parallel:
task_graph.parallel(MitRes(sim_backend))
task_graph.get_task_graph()
The MitRes object is added to the graph as its own callable. The MitRes.decompose_TaskGraph_nodes method will
recursively substitute any graph node with a _task_graph attribute with said _task_graph, adding unique names
to aid understanding.
task_graph.decompose_TaskGraph_nodes()
task_graph.get_task_graph()
Any MitTask object we want to pass to TaskGraph.prepend must have four output wires and any number of input wires, while
any MitTask object we want to pass to TaskGraph.append must have four input wires and any number of output wires. Every wire
must have a type defined Wire.
Furthermore, each MitTask would also be expected to match the types of the edges being added to them, though TaskGraph only checks this during
TaskGraph.run and not at graph construction as with MitRes and MitEx.
from qermit.taskgraph import Wire
def prepend_task_gen() -> MitTask:
def task(obj, wire0: Wire, wire1: Wire) -> Tuple[List[CircuitShots], Wire, Wire, List[CircuitShots]]:
c0 = Circuit(3).X(0).measure_all()
c1 = Circuit(3).X(2).CX(2,0).X(2).measure_all()
return ([CircuitShots(Circuit=c0, Shots=15)], wire0, wire1, [CircuitShots(Circuit=c1, Shots=10)])
return MitTask(
_label="PrependTask", _n_in_wires=2, _n_out_wires=4, _method=task
)
prepend_task = prepend_task_gen()
print(prepend_task)
for r in prepend_task(("nO nietsniE", "hcaeB ehT")):
print(r)
<MitTask::PrependTask>
[CircuitShots(Circuit=[Measure q[1] --> c[1]; Measure q[2] --> c[2]; X q[0]; Measure q[0] --> c[0]; ], Shots=15)]
nO nietsniE
hcaeB ehT
[CircuitShots(Circuit=[Measure q[1] --> c[1]; X q[2]; CX q[2], q[0]; Measure q[0] --> c[0]; X q[2]; Measure q[2] --> c[2]; ], Shots=10)]
We can add this ``MitTask`` to our ``TaskGraph`` object.
task_graph.prepend(prepend_task)
task_graph.get_task_graph()
We can construct a similar MitTask for TaskGraph.append.
from functools import reduce
import operator
from typing import Counter
from pytket.backends.backendresult import BackendResult
def append_task_gen() -> MitTask:
def task(obj, results0: List[BackendResult], wire0: Wire, wire1: Wire, results1: List[BackendResult]) -> Tuple[Wire, Counter]:
both_counts = [results0[0].get_counts(), results1[0].get_counts()]
combined_counts = reduce(operator.add, both_counts)
return (wire0[::-1] + " " + wire1[::-1], combined_counts)
return MitTask(
_label="AppendTask", _n_in_wires=4, _n_out_wires=2, _method=task
)
task_graph.append(append_task_gen())
task_graph.get_task_graph()
print(task_graph.run(("nO nietsniE", "hcaeB ehT")))
('Einstein On The Beach', Counter({(1, 0, 0): 25}))
While this example is nonsensical in regards to actual, useful, experiments, it displays how more useful
structures of TaskGraph can be produced.
In its current format however, we can not cast this TaskGraph object as a MitRes.
MitRes(sim_backend).from_TaskGraph(task_graph)
TypeError: Type signature of passed task_graph.run method does not equal MitRun.run type signature. Number of in and out wires does not match.
To produce a MitRes object from this TaskGraph we need to append and prepend MitTask in such a way that
the MitRes type constraints are respected.
from pytket.utils.outcomearray import OutcomeArray
def type_constraint_prepend_task_gen() -> MitTask:
def task(obj, cs: List[CircuitShots]) -> Tuple[Wire, Wire]:
return ("nO nietsniE", "hcaeB ehT")
return MitTask(
_label="TypePrependTask", _n_in_wires=1, _n_out_wires=2, _method=task
)
def type_constraint_append_task_gen() -> MitTask:
def task(obj, anything_comb: Wire, counter: Counter) -> Tuple[List[BackendResult]]:
counter = Counter(
{
OutcomeArray.from_readouts([key]): val
for key, val in counter.items()
}
)
return([BackendResult(counts = counter)],)
return MitTask(
_label="TypeAppendTask", _n_in_wires=2, _n_out_wires=1, _method=task
)
task_graph.prepend(type_constraint_prepend_task_gen())
task_graph.append(type_constraint_append_task_gen())
cast_mitres = MitRes(sim_backend).from_TaskGraph(task_graph)
cast_mitres.get_task_graph()
print(cast_mitres.run(([],)))
[BackendResult(q_bits={},c_bits={c[0]: 0, c[1]: 1, c[2]: 2},counts=Counter({OutcomeArray([[128]], dtype=uint8): 25}),shots=None,state=None,unitary=None,density_matrix=None)]
With this illustrative introduction to constructing advanced TaskGraph objects complete, Let’s move on to
actual error-mitigation techniques available in qermit.
There are two MitRes error-mitigation methods available in qermit; SPAM correction and Frame Randomisation. Error-mitigation methods
are available via a selection of generator functions. When called, the error-mitigation method of choice is constructed by
acting on a MitRes object with the construction methods just discussed. In this manner composition is facilitated, as each generator
method allows the error-mitigation method to be constructed around custom MitRes objects using keyword arguments.
SPAM Mitigation in qermit¶
A prominent source of noise is that occurring during State Preparation and Measurement (SPAM).
SPAM error-mitigation methods can correct for such noise through a post-processing step that modifies the output distribution measured from repeatedly sampling shots. This is possible given the assumption that SPAM noise is not dependent on the quantum computation run.
By repeatedly preparing and measuring a basis state, a distribution over basis states is procured. While for a perfect device the distribution would be the prepared basis state with probability 1, for devices prone to SPAM noise this distribution is perturbed and other basis states may be returned with (expected) small probability.
If this process is repeated for all (or a suitable subset given many qubits won’t experience correlated SPAM errors) basis states, a transition matrix can be derived that describes the noisy SPAM process. Simply applying the inverse of this transition matrix to the distribution of a quantum state from some desired quantum computation can effectively uncompute the errors caused by SPAM noise.
Generators for SPAM MitRes objects are available in the qermit.spam SPAM module.
from qermit.spam import gen_UnCorrelated_SPAM_MitRes
from pytket.extensions.qiskit import IBMQEmulatorBackend
lagos_backend = IBMQEmulatorBackend(
"ibm_lagos",
hub='',
group='',
project='',
)
uc_spam_mitres = gen_UnCorrelated_SPAM_MitRes(
backend = lagos_backend,
calibration_shots = 500
)
uc_spam_mitres.get_task_graph()
The gen_UnCorrelated_SPAM_MitRes generator function returns a MitRes object for performing SPAM mitigation with
the assumption that readout errors are not correlated between qubits. As in practice this is not always the case,
another generator function gen_FullyCorrelated_SPAM_MitRes allows correlations to be specified using Node
from the Backend.device attribute.
from qermit.spam import gen_FullyCorrelated_SPAM_MitRes
lagos_nodes = lagos_backend.backend_info.architecture.nodes
correlated_nodes = [lagos_nodes[:3], lagos_nodes[3:]]
spam_mitres_fc = gen_FullyCorrelated_SPAM_MitRes(backend = lagos_backend,
correlations = correlated_nodes,
calibration_shots = 500)
The correlation keyword argument expects a List[List[Node]] object, where each sub-list specifies correlated device qubits.
The method will raise an error if a qubit is multiple sub-list.
The uc_spam_mitres object can run experiments like any other MitRes. Let’s run an experiment both with and without both error-mitigation
and none to compare usage and results.
from qermit.taskgraph import gen_compiled_MitRes
compile_mitres = gen_compiled_MitRes(backend = lagos_backend)
compile_mitres.get_task_graph()
The gen_compiled_MitRes generator function returns a MitRes object with a compilation task prepended, as with the MitRes we constructed earlier.
from pytket import Circuit
from qermit import CircuitShots
test_c_0 = Circuit(4).X(0).X(2).measure_all()
test_c_1 = Circuit(4).X(1).X(3).measure_all()
test_experiment = [CircuitShots(Circuit = test_c_0, Shots = 1000), CircuitShots(Circuit = test_c_1, Shots = 1000)]
basic_results = compile_mitres.run(test_experiment)
print(basic_results[0].get_counts())
print(basic_results[1].get_counts())
Counter({(1, 0, 1, 0): 916, (1, 1, 1, 0): 31, (0, 0, 1, 0): 21, (1, 0, 0, 0): 18, (1, 0, 1, 1): 13, (0, 1, 1, 0): 1})
Counter({(0, 1, 0, 1): 910, (0, 0, 0, 1): 54, (0, 1, 0, 0): 17, (0, 1, 1, 1): 12, (1, 1, 0, 1): 4, (0, 0, 0, 0): 3})
While the circuits constructed should have deterministic outputs, (1, 0, 1, 0) and (0, 1, 0, 1) respectively, we can
see that the counts are returning some shots for other basis states.
The lagos_backend used for these examples is a simulator Backend run with a noise model to emulate
the properties of the Lagos device available through IBMQ, including readout errors.
spam_mitigated_results = uc_spam_mitres.run(test_experiment)
print(spam_mitigated_results[0].get_counts())
print(spam_mitigated_results[1].get_counts())
Counter({(1, 0, 1, 0): 981, (1, 0, 1, 1): 15, (1, 1, 1, 1): 2, (0, 0, 0, 0): 1, (0, 1, 0, 1): 1, (0, 1, 1, 0): 1, (1, 0, 0, 1): 1, (1, 1, 0, 0): 1})
Counter({(0, 1, 0, 1): 992, (1, 1, 0, 1): 7, (0, 1, 1, 1): 2, (0, 0, 0, 0): 1, (0, 0, 1, 0): 1, (1, 0, 0, 0): 1, (1, 0, 1, 1): 1, (1, 1, 1, 0): 1})
The device SPAM characterisation produced is stored inside the characterisation attribute the MitRes object. If a
characterisation is already available for a given method when MitRes.run is called, then it will not be characterised again. This characterisation
can be accessed by calling MitRes.get_characterisation().
Naively comparing counts, we can see that by using the MitRes object returned by gen_UnCorrelated_SPAM_MitRes a greater proportion of the returned
shots are the deterministic outputs we expected. We can not make any grand peformance claims based off this example alone, but we can see how
SPAM error-mitigation can improve results. However as emphasised earlier, MitRes objects can work with any pytket Backend object, meaning
we can easily run this experiment again using the real IBMQ Lagos device simply by switching the Backend object passed to the generator function.
Also note that there is some statistical noise and the returned set of counts for SPAM error-mitigation has slightly more counts than specified. This is an artifact of the correction procedure, but importantly we can see that the returned distribution is closer to the ideal.
from pytket.extensions.qiskit import IBMQBackend
lagos_real = IBMQBackend(
"ibm_lagos",
hub='',
group='',
project='',
)
compile_mitres_real = gen_compiled_MitRes(backend = lagos_real)
uc_spam_mitres_real = gen_UnCorrelated_SPAM_MitRes(backend = lagos_real, calibration_shots = 500)
basic_results_real = compile_mitres_real.run(test_experiment)
spam_mitigated_results_real = uc_spam_mitres_real.run(test_experiment)
To complete our comparison on real hardware, results from the Lagos device without SPAM error-mitigation:
print(basic_results_real[0].get_counts())
print(basic_results_real[1].get_counts())
Counter({(1, 0, 1, 0): 808, (1, 0, 0, 0): 98, (1, 1, 1, 0): 50, (0, 0, 1, 0): 22, (1, 0, 1, 1): 8, (1, 1, 0, 0): 4, (0, 0, 0, 0): 3, (1, 1, 1, 1): 3, (0, 1, 1, 0): 2, (0, 0, 1, 1): 1, (0, 1, 0, 0): 1})
Counter({(0, 1, 0, 1): 891, (0, 0, 0, 1): 60, (0, 1, 0, 0): 26, (0, 1, 1, 1): 11, (1, 1, 0, 1): 9, (0, 0, 0, 0): 2, (0, 1, 1, 0): 1})
Results from the Lagos device with SPAM error-mitigation:
print(spam_mitigated_results_real[0].get_counts())
print(spam_mitigated_results_real[1].get_counts())
Counter({(1, 0, 1, 0): 993, (0, 1, 1, 0): 4, (1, 0, 0, 0): 2, (0, 0, 0, 0): 1, (0, 0, 0, 1): 1, (0, 0, 1, 0): 1, (0, 1, 0, 0): 1, (1, 1, 0, 1): 1})
Counter({(0, 1, 0, 1): 984, (1, 1, 0, 1): 7, (0, 0, 0, 0): 4, (0, 0, 1, 1): 3, (0, 1, 1, 0): 2, (1, 0, 0, 0): 2, (1, 0, 0, 1): 1, (1, 1, 1, 0): 1})
As with the emulator Backend, the distribution of results returned with SPAM error-mitigation for this simple test case is improved.
Frame-Randomisation in qermit¶
While it is not possible to efficiently characterise and suppress all device noise, it can be advantageous to transform some adverse type of noise into a less damaging type.
Coherent errors are additional unwanted unitary rotations that may appear throughout a quantum computation. Their effect can be damaging due to a possible faster rate of error accumulation than in the case of probabilistic (incoherent) errors.
Randomisation protocols can be used to tailor the form of the noise profile. By averaging the n-qubit noise channel over all elements from a group (specifically some subgroup of the full unitary group on n qubits), the resulting noise is invariant under the action of any element from this group.
For example, averaging a noise channel over the n-qubit Pauli group has the effect of producing an n-qubit stochastic Pauli channel – this is a probabilistic linear combination of n-qubit Pauli unitary errors.
In this manner, an n-qubit coherent noise channel can be tailored into an n-qubit stochastic Pauli noise channel. For Pauli channels, the worst case error rate is similar to the average error rate, whilst for coherent noise the worst case error rate scales as a square root of the average error rate.
Generator functions in the qermit.frame_randomisation frame randomisation module return MitRes objects for automatically using randomised protocols
when running experiments.
Randomised compilation [Wallman2015] is a well known example of such a procedure.
from qermit.frame_randomisation import gen_Frame_Randomisation_MitRes
fr_mitres = gen_Frame_Randomisation_MitRes(lagos_backend, samples = 200)
fr_mitres.get_task_graph()
The returned MitRes object uses FrameRandomisation methods as available
in the pytket.tailoring tailoring module.
The frame randomisation method used can be changed with the frame_randomisation keyword argument,
which accepts methods defined in the FrameRandomisation enum class, supporting PauliFrameRandomisation
and UniversalFrameRandomisation as defined in pytket. Without specification, the
method will default use UniversalFrameRandomisation.
An extended explanation of these methods is available in the pytket manual.
from qermit.frame_randomisation import FrameRandomisation
pfr_mitres = gen_Frame_Randomisation_MitRes(lagos_real,
samples = 200,
frame_randomisation = FrameRandomisation.PauliFrameRandomisation,
optimisation_level = 0)
ufr_mitres = gen_Frame_Randomisation_MitRes(lagos_real,
samples = 200,
frame_randomisation = FrameRandomisation.UniversalFrameRandomisation,
optimisation_level = 0)
The gen_Frame_Randomisation_MitRes generator has an additional keyword argument for the optimisation_level
used in the internal compilation task. For the purpose of this example we will keep this to its minimum 0, meaning
that any Circuit objects will only be compiled to fit basic device constraints i.e. the gate set and fixed
physical qubit connectivity.
Let’s compare performance, between a noiseless simulator, the Lagos device without any mitigation and the Lagos device with universal frame-randomisation.
from pytket.extensions.qiskit import AerBackend
test_fr_circuit = Circuit(2)
test_fr_circuit.X(0).H(1).CX(0,1).Rz(0.3, 1)
test_fr_circuit.CX(0,1).X(0).H(1).CX(0,1).Rz(0.3, 1).CX(0,1).measure_all()
test_fr_experiment = [CircuitShots(Circuit = test_fr_circuit, Shots = 3000)]
ideal_mitres = gen_compiled_MitRes(AerBackend())
ideal_res = ideal_mitres.run(test_fr_experiment)
print(ideal_res[0].get_counts())
Counter({(0, 0): 2366, (0, 1): 634})
compile_mitres_0 = gen_compiled_MitRes(lagos_real, optimisation_level = 0)
basic_results = compile_mitres_0.run(test_fr_experiment)
print(basic_results[0].get_counts())
Counter({(0, 0): 2415, (0, 1): 501, (1, 0): 63, (1, 1): 21})
ufr_results = ufr_mitres.run(test_fr_experiment)
print(ufr_results[0].get_counts())
Counter({(0, 0): 2521, (1, 0): 616, (0, 1): 52, (1, 1): 11})
pfr_results = pfr_mitres.run(test_fr_experiment)
print(pfr_results[0].get_counts())
Counter({(0, 0): 2490, (1, 0): 634, (0, 1): 59, (1, 1): 17})
Universal and Pauli Frame-Randomisation are expected to help suppress coherent errors that would usually build up over
large circuits during their execution on some device. Considering these results, we could speculate that
the results returned when using the mitigation MitRes object are closer to the ideal, but there
aren’t enough shots here to say anything conclusive.
However, we have shown that it is as straightforward to execute any Circuit with frame-randomisation as it is without, or as it was with SPAM error-mitigation.
Combining MitRes methods¶
One of the key features of qermit is how it easily facilitates running combinations of error-mitigation protocols.
While each error-mitigated MitRes generator will have different options for what combinations can be done, for
this example we will consider combining the frame-randomisation and SPAM mitigation methods we’ve previously looked at.
As we have seen, when a generator function for a mitigated MitRes method is called, it builds the desired mitigation
scheme by passing various combinations of MitTask to ``TaskGraph construction methods. In both the SPAM
and frame-randomisation MitRes generators, the starting building block is a basic MitRes object - combining
mitigation methods is possible by simply starting this construction from a mitigated MitRes object.
The gen_UnCorrelated_SPAM_MitRes generator has two keywords for achieving this, calibration_mitres and
correction_mitres. The calibration_mitres keyword corresponds to the MitRes object through which
characteriastion circuits are executed on the backend for characterisating SPAM errors, the correction_mitres
keyword corresponds to the MitRes object through which experiment circuits are run (i.e. those passed to the MitRes.run method).
To show this, Let’s create a SPAM mitigation MitRes object that also runs frame-randomisation on experiment circuits.
ufr_mitres = gen_Frame_Randomisation_MitRes(lagos_real,
samples = 400)
ufr_spam_mitres = gen_UnCorrelated_SPAM_MitRes(lagos_real,
calibration_shots = 500,
correction_mitres = ufr_mitres)
ufr_spam_mitres.decompose_TaskGraph_nodes()
ufr_spam_mitres.get_task_graph()
Let’s compare this task graph to the SPAM and frame-randomisation task graphs:
Comparing both images, we can see that where the default SPAM MitRes has a subgraph
with a <MitTask::CircuitsToHandles> followed by a <MitTask::HandlesToResults>, the
combined MitRes here has this subgraph subsituted with the frame-randomisation MitRes.
Running experiments with this combined MitRes is identical to any other MitRes.
ufr_spam_res = ufr_spam_mitres.run(test_fr_experiment)
print(ufr_spam_res[0].get_counts())
Counter({(0, 0): 2526, (1, 0): 614, (0, 1): 41, (1, 1): 21})
Wallman, J., Emerson, J., 2015. Noise tailoring for scalable quantum computation via randomized compiling. Phys. Rev. A 94, 052325 (2016).