Breaking into error mitigation with Mitiq’s calibration module#

This tutorial helps answer the question: “What quantum error mitigation technique should I use for my problem?”.
The newly introduced mitiq.calibration
module helps answer that in an optimized way, through Benchmarks
and Strategies
.
More specifically, this tutorial covers:
Getting started with Mitiq’s calibration module with ZNE
Use Qiskit noisy simulator with
FakeJakarta
as backendRun calibration with some special settings,
RBSettings
, using thecal.run(log=True)
option
Getting started with Mitiq#
from mitiq.benchmarks import generate_rb_circuits
from mitiq.zne import execute_with_zne
from mitiq.zne.scaling import (
fold_gates_at_random,
fold_global,
fold_all
)
from mitiq.zne.inference import LinearFactory, RichardsonFactory
from mitiq import (
Calibrator,
Settings,
execute_with_mitigation,
MeasurementResult,
)
from qiskit.providers.fake_provider import FakeJakarta # Fake (simulated) QPU
Define the circuit to study#
Global variables#
Define global variables for the quantum circuit of interest: number of qubits, depth of the quantum circuit and number of shots.
n_qubits = 2
depth_circuit = 100
shots = 10 ** 4
Quantum circuit: Randomized benchmarking (RB)#
We now use Mitiq’s built-in generate_rb_circuits
from the mitiq.benchmarks
module to define the quantum circuit.
circuit = generate_rb_circuits(n_qubits, depth_circuit,return_type="qiskit")[0]
circuit.measure_all()
print(len(circuit))
1054
We define a function that executes the quantum circuits and returns the expectation value.
This is consumed by Mitiq’s execute_with_zne
. In this example, the expectation value is the probability of measuring the ground state, which is what one would expect from an ideal randomized benchmarking circuit.
def execute_circuit(circuit):
"""Execute the input circuit and return the expectation value of |00..0><00..0|"""
noisy_backend = FakeJakarta()
noisy_result = noisy_backend.run(circuit, shots=shots).result()
noisy_counts = noisy_result.get_counts(circuit)
noisy_expectation_value = noisy_counts[n_qubits * "0"] / shots
return noisy_expectation_value
mitigated = execute_with_zne(circuit, execute_circuit, factory=LinearFactory([1, 3, 5]))
unmitigated = execute_circuit(circuit)
ideal = 1 #property of RB circuits
print("ideal = \t \t",ideal)
print("unmitigated = \t \t", "{:.5f}".format(unmitigated))
print("mitigated = \t \t", "{:.5f}".format(mitigated))
ideal = 1
unmitigated = 0.82640
mitigated = 0.89608
Using calibration to improve the results#
Let’s consider a noisy backend using the Qiskit noisy simulator, FakeJakarta
. Note that the executor passed to the Calibrator
object must return counts, as opposed to expectation values.
def execute_calibration(qiskit_circuit):
"""Execute the input circuits and return the measurement results."""
noisy_backend = FakeJakarta()
noisy_result = noisy_backend.run(qiskit_circuit, shots=shots).result()
noisy_counts = noisy_result.get_counts(qiskit_circuit)
noisy_counts = { k.replace(" ",""):v for k, v in noisy_counts.items()}
measurements = MeasurementResult.from_counts(noisy_counts)
return measurements
We import from the calibration module the key ingredients to use mitiq.calibration
: the Calibrator
class, the mitiq.calibration.settings.Settings
class and the execute_with_mitigation
function.
Currently mitiq.calibration
supports ZNE as a technique to calibrate from, tuning different scale factors, extrapolation methods and circuit scaling methods.
Let’s run the calibration using an ad-hoc RBSettings and using the log=True
option in order to print the list of experiments run.
benchmarks: Circuit type: “rb”
strategies: use various “zne” strategies, testing various “scale_noise” methods (such as
mitiq.zne.scaling.folding.fold_global
,mitiq.zne.scaling.folding.fold_gates_at_random
, andmitiq.zne.scaling.folding.fold_all
), and ZNE factories for extrapolation (such asmitiq.zne.inference.RichardsonFactory
andmitiq.zne.inference.LinearFactory
)
RBSettings = Settings(
benchmarks=[
{
"circuit_type": "rb",
"num_qubits": 2,
"circuit_depth": int(depth_circuit / 2),
},
],
strategies=[
{
"technique": "zne",
"scale_noise": fold_global,
"factory": RichardsonFactory([1.0, 2.0, 3.0]),
},
{
"technique": "zne",
"scale_noise": fold_global,
"factory": RichardsonFactory([1.0, 3.0, 5.0]),
},
{
"technique": "zne",
"scale_noise": fold_global,
"factory": LinearFactory([1.0, 2.0, 3.0]),
},
{
"technique": "zne",
"scale_noise": fold_global,
"factory": LinearFactory([1.0, 3.0, 5.0]),
},
{
"technique": "zne",
"scale_noise": fold_gates_at_random,
"factory": RichardsonFactory([1.0, 2.0, 3.0]),
},
{
"technique": "zne",
"scale_noise": fold_gates_at_random,
"factory": RichardsonFactory([1.0, 3.0, 5.0]),
},
{
"technique": "zne",
"scale_noise": fold_gates_at_random,
"factory": LinearFactory([1.0, 2.0, 3.0]),
},
{
"technique": "zne",
"scale_noise": fold_gates_at_random,
"factory": LinearFactory([1.0, 3.0, 5.0]),
},
{
"technique": "zne",
"scale_noise": fold_all,
"factory": RichardsonFactory([1.0, 2.0, 3.0]),
},
{
"technique": "zne",
"scale_noise": fold_all,
"factory": RichardsonFactory([1.0, 3.0, 5.0]),
},
{
"technique": "zne",
"scale_noise": fold_all,
"factory": LinearFactory([1.0, 2.0, 3.0]),
},
{
"technique": "zne",
"scale_noise": fold_all,
"factory": LinearFactory([1.0, 3.0, 5.0]),
},
],
)
cal = Calibrator(execute_calibration, frontend="qiskit", settings=RBSettings)
cal.run(log=True)
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
Cell In[8], line 2
1 cal = Calibrator(execute_calibration, frontend="qiskit", settings=RBSettings)
----> 2 cal.run(log=True)
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/calibration/calibrator.py:329, in Calibrator.run(self, log)
327 with warnings.catch_warnings():
328 warnings.simplefilter("ignore", UserWarning)
--> 329 mitigated_value = strategy.mitigation_function(
330 circuit, expval_executor
331 )
332 self.results.add_result(
333 strategy,
334 problem,
(...)
337 mitigated_val=mitigated_value,
338 )
340 self.results.ensure_full()
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/zne/zne.py:62, in execute_with_zne(circuit, executor, observable, factory, scale_noise, num_to_average)
59 if num_to_average < 1:
60 raise ValueError("Argument `num_to_average` must be a positive int.")
---> 62 return factory.run(
63 circuit, executor, observable, scale_noise, int(num_to_average)
64 ).reduce()
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/zne/inference.py:567, in BatchedFactory.run(self, qp, executor, observable, scale_noise, num_to_average)
560 res.extend(
561 executor.evaluate(
562 circuit, observable, force_run_all=True, **kwargs
563 )
564 )
565 else:
566 # Else, run all circuits.
--> 567 res = executor.evaluate(
568 to_run, observable, force_run_all=True, **kwargs_list[0]
569 )
571 # Reshape "res" to have "num_to_average" columns
572 reshaped = np.array(res).reshape((-1, num_to_average))
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/executor/executor.py:178, in Executor.evaluate(self, circuits, observable, force_run_all, **kwargs)
175 result_step = 1
177 # Run all required circuits.
--> 178 all_results = self.run(all_circuits, force_run_all, **kwargs)
180 # Parse the results.
181 if self._executor_return_type in FloatLike:
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/executor/executor.py:253, in Executor.run(self, circuits, force_run_all, **kwargs)
251 if not self.can_batch:
252 for circuit in to_run:
--> 253 self._call_executor(circuit, **kwargs)
255 else:
256 stop = len(to_run)
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/executor/executor.py:290, in Executor._call_executor(self, to_run, **kwargs)
280 def _call_executor(
281 self, to_run: Union[QPROGRAM, Sequence[QPROGRAM]], **kwargs: Any
282 ) -> None:
283 """Calls the executor on the input circuit(s) to run. Stores the
284 executed circuits in ``self._executed_circuits`` and the quantum
285 results in ``self._quantum_results``.
(...)
288 to_run: Circuit(s) to run.
289 """
--> 290 result = self._executor(to_run, **kwargs)
291 self._calls_to_executor += 1
293 if self.can_batch:
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/calibration/calibrator.py:396, in convert_to_expval_executor.<locals>.expval_executor(circuit)
392 if not cirq.is_measurement(circuit_with_meas):
393 circuit_with_meas.append(
394 cirq.measure(circuit_with_meas.all_qubits())
395 )
--> 396 raw = cast(MeasurementResult, executor.run([circuit_with_meas])[0])
397 distribution = raw.prob_distribution()
398 return distribution.get(bitstring, 0.0)
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/executor/executor.py:260, in Executor.run(self, circuits, force_run_all, **kwargs)
258 for i in range(int(np.ceil(stop / step))):
259 batch = to_run[i * step : (i + 1) * step]
--> 260 self._call_executor(batch, **kwargs)
262 results = self._quantum_results[start_result_index:]
264 if not force_run_all:
265 # Expand computed results to all results using counts.
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/executor/executor.py:290, in Executor._call_executor(self, to_run, **kwargs)
280 def _call_executor(
281 self, to_run: Union[QPROGRAM, Sequence[QPROGRAM]], **kwargs: Any
282 ) -> None:
283 """Calls the executor on the input circuit(s) to run. Stores the
284 executed circuits in ``self._executed_circuits`` and the quantum
285 results in ``self._quantum_results``.
(...)
288 to_run: Circuit(s) to run.
289 """
--> 290 result = self._executor(to_run, **kwargs)
291 self._calls_to_executor += 1
293 if self.can_batch:
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/calibration/calibrator.py:273, in Calibrator.__init__.<locals>.cirq_execute(circuits)
268 def cirq_execute(
269 circuits: Sequence[cirq.Circuit],
270 ) -> Sequence[MeasurementResult]:
271 q_programs = [convert_from_mitiq(c, frontend) for c in circuits]
272 results = cast(
--> 273 Sequence[MeasurementResult], self.executor.run(q_programs)
274 )
275 return results
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/executor/executor.py:253, in Executor.run(self, circuits, force_run_all, **kwargs)
251 if not self.can_batch:
252 for circuit in to_run:
--> 253 self._call_executor(circuit, **kwargs)
255 else:
256 stop = len(to_run)
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/mitiq/executor/executor.py:290, in Executor._call_executor(self, to_run, **kwargs)
280 def _call_executor(
281 self, to_run: Union[QPROGRAM, Sequence[QPROGRAM]], **kwargs: Any
282 ) -> None:
283 """Calls the executor on the input circuit(s) to run. Stores the
284 executed circuits in ``self._executed_circuits`` and the quantum
285 results in ``self._quantum_results``.
(...)
288 to_run: Circuit(s) to run.
289 """
--> 290 result = self._executor(to_run, **kwargs)
291 self._calls_to_executor += 1
293 if self.can_batch:
Cell In[6], line 4, in execute_calibration(qiskit_circuit)
2 """Execute the input circuits and return the measurement results."""
3 noisy_backend = FakeJakarta()
----> 4 noisy_result = noisy_backend.run(qiskit_circuit, shots=shots).result()
5 noisy_counts = noisy_result.get_counts(qiskit_circuit)
6 noisy_counts = { k.replace(" ",""):v for k, v in noisy_counts.items()}
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/qiskit_aer/jobs/utils.py:42, in requires_submit.<locals>._wrapper(self, *args, **kwargs)
40 if self._future is None:
41 raise JobError("Job not submitted yet!. You have to .submit() first!")
---> 42 return func(self, *args, **kwargs)
File ~/checkouts/readthedocs.org/user_builds/mitiq/envs/stable/lib/python3.10/site-packages/qiskit_aer/jobs/aerjob.py:114, in AerJob.result(self, timeout)
96 @requires_submit
97 def result(self, timeout=None):
98 # pylint: disable=arguments-differ
99 """Get job result. The behavior is the same as the underlying
100 concurrent Future objects,
101
(...)
112 concurrent.futures.CancelledError: if job cancelled before completed.
113 """
--> 114 return self._future.result(timeout=timeout)
File ~/.asdf/installs/python/3.10.13/lib/python3.10/concurrent/futures/_base.py:453, in Future.result(self, timeout)
450 elif self._state == FINISHED:
451 return self.__get_result()
--> 453 self._condition.wait(timeout)
455 if self._state in [CANCELLED, CANCELLED_AND_NOTIFIED]:
456 raise CancelledError()
File ~/.asdf/installs/python/3.10.13/lib/python3.10/threading.py:320, in Condition.wait(self, timeout)
318 try: # restore state no matter what (e.g., KeyboardInterrupt)
319 if timeout is None:
--> 320 waiter.acquire()
321 gotit = True
322 else:
KeyboardInterrupt:
As you can see above, several experiments were run, and each one has either a red cross (❌) or a green check (✅) to signal whether the error mitigation experiment obtained an expectation value that is better than the non-mitigated one.
calibrated_mitigated=execute_with_mitigation(circuit, execute_circuit, calibrator=cal)
mitigated=execute_with_zne(circuit, execute_circuit, factory=LinearFactory([1, 3, 5]))
unmitigated=execute_circuit(circuit)
print("ideal = \t \t",ideal)
print("unmitigated = \t \t", "{:.5f}".format(unmitigated))
print("mitigated = \t \t", "{:.5f}".format(mitigated))
print("calibrated_mitigated = \t", "{:.5f}".format(calibrated_mitigated))
We can see that the mitigated and calibrated-mitigated values show improvement over the unmitigated value, and that the calibrated value shows a larger improvement, achieving the objective of the calibration process.