Breaking into error mitigation with Mitiq’s calibration module#

../_images/calibration.png

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 backend

  • Run calibration with some special settings, RBSettings, using the cal.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, and mitiq.zne.scaling.folding.fold_all), and ZNE factories for extrapolation (such as mitiq.zne.inference.RichardsonFactory and mitiq.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.