From 2c9dec511620579d60512e475912477680022c27 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Tue, 15 Oct 2024 09:23:22 -0400 Subject: [PATCH] Refactor monkey-patching so it is active during docs build --- docs/howtos/artifacts.rst | 7 + docs/manuals/characterization/t1.rst | 7 + docs/manuals/characterization/t2ramsey.rst | 7 + docs/manuals/characterization/tphi.rst | 7 + .../measurement/readout_mitigation.rst | 7 + .../measurement/restless_measurements.rst | 7 + docs/manuals/verification/quantum_volume.rst | 7 + .../verification/randomized_benchmarking.rst | 7 + .../manuals/verification/state_tomography.rst | 7 + docs/tutorials/custom_experiment.rst | 7 + docs/tutorials/getting_started.rst | 7 + .../correlated_readout_error.py | 4 + .../characterization/fine_frequency.py | 4 + .../characterization/local_readout_error.py | 4 + .../multi_state_discrimination.py | 4 + .../library/characterization/ramsey_xy.py | 4 + .../library/characterization/t1.py | 4 + .../library/characterization/t2ramsey.py | 4 + .../library/characterization/tphi.py | 4 + .../library/characterization/zz_ramsey.py | 4 + qiskit_experiments/test/mock_iq_backend.py | 5 + qiskit_experiments/test/patching.py | 260 ++++++++++++++++++ qiskit_experiments/test/pulse_backend.py | 6 + qiskit_experiments/test/t2hahn_backend.py | 5 + test/base.py | 258 +---------------- 25 files changed, 400 insertions(+), 247 deletions(-) create mode 100644 qiskit_experiments/test/patching.py diff --git a/docs/howtos/artifacts.rst b/docs/howtos/artifacts.rst index b3075507f4..9ad3112d72 100644 --- a/docs/howtos/artifacts.rst +++ b/docs/howtos/artifacts.rst @@ -25,6 +25,13 @@ Viewing artifacts Here we run a parallel experiment consisting of two :class:`.T1` experiments in parallel and then view the output artifacts as a list of :class:`.ArtifactData` objects accessed by :meth:`.ExperimentData.artifacts`: +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: from qiskit_ibm_runtime.fake_provider import FakePerth diff --git a/docs/manuals/characterization/t1.rst b/docs/manuals/characterization/t1.rst index 1fefa30371..97af51b743 100644 --- a/docs/manuals/characterization/t1.rst +++ b/docs/manuals/characterization/t1.rst @@ -34,6 +34,13 @@ for qubit 0. packages to run simulations. You can install them with ``python -m pip install qiskit-aer qiskit-ibm-runtime``. +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: import numpy as np diff --git a/docs/manuals/characterization/t2ramsey.rst b/docs/manuals/characterization/t2ramsey.rst index c65f5a434c..fe2a82c9a2 100644 --- a/docs/manuals/characterization/t2ramsey.rst +++ b/docs/manuals/characterization/t2ramsey.rst @@ -62,6 +62,13 @@ pure T1/T2 relaxation noise model. packages to run simulations. You can install them with ``python -m pip install qiskit-aer qiskit-ibm-runtime``. +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: # A T1 simulator diff --git a/docs/manuals/characterization/tphi.rst b/docs/manuals/characterization/tphi.rst index 43e117b657..0f04c9a34f 100644 --- a/docs/manuals/characterization/tphi.rst +++ b/docs/manuals/characterization/tphi.rst @@ -25,6 +25,13 @@ From the :math:`T_1` and :math:`T_2` estimates, we compute the results for packages to run simulations. You can install them with ``python -m pip install qiskit-aer qiskit-ibm-runtime``. +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: import numpy as np diff --git a/docs/manuals/measurement/readout_mitigation.rst b/docs/manuals/measurement/readout_mitigation.rst index 1a6b8d54d7..c0672a6118 100644 --- a/docs/manuals/measurement/readout_mitigation.rst +++ b/docs/manuals/measurement/readout_mitigation.rst @@ -35,6 +35,13 @@ experiments to generate the corresponding mitigators. packages to run simulations. You can install them with ``python -m pip install qiskit-aer qiskit-ibm-runtime``. +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: import numpy as np diff --git a/docs/manuals/measurement/restless_measurements.rst b/docs/manuals/measurement/restless_measurements.rst index 86f2143357..313d4cde94 100644 --- a/docs/manuals/measurement/restless_measurements.rst +++ b/docs/manuals/measurement/restless_measurements.rst @@ -62,6 +62,13 @@ they use always starts with the qubits in the ground state. This tutorial requires the :external+qiskit_ibm_runtime:doc:`qiskit-ibm-runtime ` package to model a backend. You can install it with ``python -m pip install qiskit-ibm-runtime``. +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: from qiskit_ibm_runtime.fake_provider import FakePerth diff --git a/docs/manuals/verification/quantum_volume.rst b/docs/manuals/verification/quantum_volume.rst index f73cc89ac2..4d2ca9ec5c 100644 --- a/docs/manuals/verification/quantum_volume.rst +++ b/docs/manuals/verification/quantum_volume.rst @@ -29,6 +29,13 @@ z_value = 2), and at least 100 trials have been ran. packages to run simulations. You can install them with ``python -m pip install qiskit-aer qiskit-ibm-runtime``. +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: from qiskit_experiments.framework import BatchExperiment diff --git a/docs/manuals/verification/randomized_benchmarking.rst b/docs/manuals/verification/randomized_benchmarking.rst index d38cbe02b7..15f0fa215f 100644 --- a/docs/manuals/verification/randomized_benchmarking.rst +++ b/docs/manuals/verification/randomized_benchmarking.rst @@ -16,6 +16,13 @@ explanation on the RB method, which is based on Refs. [1]_ [2]_. packages to run simulations. You can install them with ``python -m pip install qiskit-aer qiskit-ibm-runtime``. +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: import numpy as np diff --git a/docs/manuals/verification/state_tomography.rst b/docs/manuals/verification/state_tomography.rst index 3e25f8b884..cc481ade36 100644 --- a/docs/manuals/verification/state_tomography.rst +++ b/docs/manuals/verification/state_tomography.rst @@ -14,6 +14,13 @@ complete basis of measurement operators. We first initialize a simulator to run the experiments on. +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: from qiskit_aer import AerSimulator diff --git a/docs/tutorials/custom_experiment.rst b/docs/tutorials/custom_experiment.rst index 0a1a50b4f9..74ba533e59 100644 --- a/docs/tutorials/custom_experiment.rst +++ b/docs/tutorials/custom_experiment.rst @@ -562,6 +562,13 @@ To test our code, we first simulate a noisy backend with asymmetric readout erro You can install it with ``python -m pip install qiskit-aer``. +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: from qiskit_aer import AerSimulator, noise diff --git a/docs/tutorials/getting_started.rst b/docs/tutorials/getting_started.rst index 72062fde4a..2eb3b093e2 100644 --- a/docs/tutorials/getting_started.rst +++ b/docs/tutorials/getting_started.rst @@ -86,6 +86,13 @@ backend, real or simulated, that you can access through Qiskit. packages to run simulations. You can install them with ``python -m pip install qiskit-aer qiskit-ibm-runtime``. +.. jupyter-execute:: + :hide-code: + + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + .. jupyter-execute:: from qiskit_ibm_runtime.fake_provider import FakePerth diff --git a/qiskit_experiments/library/characterization/correlated_readout_error.py b/qiskit_experiments/library/characterization/correlated_readout_error.py index 617feb523f..3fe55efa7b 100644 --- a/qiskit_experiments/library/characterization/correlated_readout_error.py +++ b/qiskit_experiments/library/characterization/correlated_readout_error.py @@ -77,6 +77,10 @@ class CorrelatedReadoutError(BaseExperiment): .. jupyter-execute:: :hide-code: + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + from qiskit.providers.fake_provider import GenericBackendV2 from qiskit_aer import AerSimulator diff --git a/qiskit_experiments/library/characterization/fine_frequency.py b/qiskit_experiments/library/characterization/fine_frequency.py index ce39417d67..c01b17d5c7 100644 --- a/qiskit_experiments/library/characterization/fine_frequency.py +++ b/qiskit_experiments/library/characterization/fine_frequency.py @@ -53,6 +53,10 @@ class FineFrequency(BaseExperiment): .. jupyter-execute:: :hide-code: + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + # backend from qiskit_ibm_runtime.fake_provider import FakePerth from qiskit_aer import AerSimulator diff --git a/qiskit_experiments/library/characterization/local_readout_error.py b/qiskit_experiments/library/characterization/local_readout_error.py index 8830f2270f..329a7b0234 100644 --- a/qiskit_experiments/library/characterization/local_readout_error.py +++ b/qiskit_experiments/library/characterization/local_readout_error.py @@ -66,6 +66,10 @@ class LocalReadoutError(BaseExperiment): .. jupyter-execute:: :hide-code: + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + # backend from qiskit_aer import AerSimulator from qiskit_ibm_runtime.fake_provider import FakePerth diff --git a/qiskit_experiments/library/characterization/multi_state_discrimination.py b/qiskit_experiments/library/characterization/multi_state_discrimination.py index 183d0707be..b0a33b9313 100644 --- a/qiskit_experiments/library/characterization/multi_state_discrimination.py +++ b/qiskit_experiments/library/characterization/multi_state_discrimination.py @@ -57,6 +57,10 @@ class MultiStateDiscrimination(BaseExperiment): .. jupyter-execute:: :hide-code: + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + # backend from qiskit_experiments.test.pulse_backend import SingleTransmonTestBackend backend = SingleTransmonTestBackend(5.2e9,-.25e9, 1e9, 0.8e9, 1e4, noise=False, seed=199) diff --git a/qiskit_experiments/library/characterization/ramsey_xy.py b/qiskit_experiments/library/characterization/ramsey_xy.py index 40566580f9..f60a276c0c 100644 --- a/qiskit_experiments/library/characterization/ramsey_xy.py +++ b/qiskit_experiments/library/characterization/ramsey_xy.py @@ -85,6 +85,10 @@ class RamseyXY(BaseExperiment, RestlessMixin): .. jupyter-execute:: :hide-code: + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + # backend from qiskit_aer import AerSimulator from qiskit_ibm_runtime.fake_provider import FakePerth diff --git a/qiskit_experiments/library/characterization/t1.py b/qiskit_experiments/library/characterization/t1.py index 6f3c02cfc1..c8ed55d558 100644 --- a/qiskit_experiments/library/characterization/t1.py +++ b/qiskit_experiments/library/characterization/t1.py @@ -41,6 +41,10 @@ class T1(BaseExperiment): .. jupyter-execute:: :hide-code: + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + # backend from qiskit_ibm_runtime.fake_provider import FakeManilaV2 from qiskit_aer import AerSimulator diff --git a/qiskit_experiments/library/characterization/t2ramsey.py b/qiskit_experiments/library/characterization/t2ramsey.py index b4b06be794..38312d4d4b 100644 --- a/qiskit_experiments/library/characterization/t2ramsey.py +++ b/qiskit_experiments/library/characterization/t2ramsey.py @@ -63,6 +63,10 @@ class T2Ramsey(BaseExperiment): .. jupyter-execute:: :hide-code: + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + # backend from qiskit_ibm_runtime.fake_provider import FakeManilaV2 from qiskit_aer import AerSimulator diff --git a/qiskit_experiments/library/characterization/tphi.py b/qiskit_experiments/library/characterization/tphi.py index 5e1755326d..6b67c2b771 100644 --- a/qiskit_experiments/library/characterization/tphi.py +++ b/qiskit_experiments/library/characterization/tphi.py @@ -54,6 +54,10 @@ class Tphi(BatchExperiment): .. jupyter-execute:: :hide-code: + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + # backend from qiskit_ibm_runtime.fake_provider import FakeManilaV2 from qiskit_aer import AerSimulator diff --git a/qiskit_experiments/library/characterization/zz_ramsey.py b/qiskit_experiments/library/characterization/zz_ramsey.py index 69cc0a1306..b9e84265f5 100644 --- a/qiskit_experiments/library/characterization/zz_ramsey.py +++ b/qiskit_experiments/library/characterization/zz_ramsey.py @@ -129,6 +129,10 @@ class ZZRamsey(BaseExperiment): .. jupyter-execute:: :hide-code: + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + patch_sampler_test_support() + # backend from qiskit_ibm_runtime.fake_provider import FakePerth from qiskit_aer import AerSimulator diff --git a/qiskit_experiments/test/mock_iq_backend.py b/qiskit_experiments/test/mock_iq_backend.py index 4436d05754..02ea3bbde1 100644 --- a/qiskit_experiments/test/mock_iq_backend.py +++ b/qiskit_experiments/test/mock_iq_backend.py @@ -47,6 +47,11 @@ def __init__( backend_version: str = None, **fields, ): + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + + patch_sampler_test_support() + super().__init__(provider, name, description, online_date, backend_version, **fields) backend_v1 = FakeOpenPulse2Q() diff --git a/qiskit_experiments/test/patching.py b/qiskit_experiments/test/patching.py new file mode 100644 index 0000000000..f0a6b56409 --- /dev/null +++ b/qiskit_experiments/test/patching.py @@ -0,0 +1,260 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Temporary monkey-patching test support for BackednSamplerV2""" +from __future__ import annotations + +import copy +import math +import warnings +from dataclasses import dataclass +from typing import Any, Literal + +import numpy as np + +import qiskit.primitives.backend_sampler_v2 +from qiskit.circuit import QuantumCircuit +from qiskit.exceptions import QiskitError +from qiskit.primitives import ( + BackendEstimatorV2, + BackendSamplerV2, +) +from qiskit.primitives.containers import ( + BitArray, + DataBin, + SamplerPubResult, +) +from qiskit.primitives.containers.sampler_pub import SamplerPub +from qiskit.primitives.primitive_job import PrimitiveJob +from qiskit.providers.backend import BackendV1, BackendV2 +from qiskit.result import Result +from qiskit_ibm_runtime.fake_provider.local_service import QiskitRuntimeLocalService + + +# The rest of this file contains definitions for monkey patching support for +# level 1 data and a noise model run option into BackendSamplerV2 +def _patched_run_circuits( + circuits: QuantumCircuit | list[QuantumCircuit], + backend: BackendV1 | BackendV2, + **run_options, +) -> tuple[list[Result], list[dict]]: + """Remove metadata of circuits and run the circuits on a backend. + Args: + circuits: The circuits + backend: The backend + monitor: Enable job minotor if True + **run_options: run_options + Returns: + The result and the metadata of the circuits + """ + if isinstance(circuits, QuantumCircuit): + circuits = [circuits] + metadata = [] + for circ in circuits: + metadata.append(circ.metadata) + # Commenting out this line is only change from qiskit.primitives.backend_estimator._run_circuits + # circ.metadata = {} + if isinstance(backend, BackendV1): + max_circuits = getattr(backend.configuration(), "max_experiments", None) + elif isinstance(backend, BackendV2): + max_circuits = backend.max_circuits + else: + raise RuntimeError("Backend version not supported") + if max_circuits: + jobs = [ + backend.run(circuits[pos : pos + max_circuits], **run_options) + for pos in range(0, len(circuits), max_circuits) + ] + result = [x.result() for x in jobs] + else: + result = [backend.run(circuits, **run_options).result()] + return result, metadata + + +def _patched_run_backend_primitive_v2( + self, # pylint: disable=unused-argument + backend: BackendV1 | BackendV2, + primitive: Literal["sampler", "estimator"], + options: dict, + inputs: dict, +) -> PrimitiveJob: + """Run V2 backend primitive. + + Args: + backend: The backend to run the primitive on. + primitive: Name of the primitive. + options: Primitive options to use. + inputs: Primitive inputs. + + Returns: + The job object of the result of the primitive. + """ + options_copy = copy.deepcopy(options) + + prim_options = {} + sim_options = options_copy.get("simulator", {}) + if seed_simulator := sim_options.pop("seed_simulator", None): + prim_options["seed_simulator"] = seed_simulator + if noise_model := sim_options.pop("noise_model", None): + prim_options["noise_model"] = noise_model + if not sim_options: + options_copy.pop("simulator", None) + if primitive == "sampler": + if default_shots := options_copy.pop("default_shots", None): + prim_options["default_shots"] = default_shots + if meas_type := options_copy.get("execution", {}).pop("meas_type", None): + if meas_type == "classified": + prim_options["meas_level"] = 2 + prim_options["meas_return"] = "single" + elif meas_type == "kerneled": + prim_options["meas_level"] = 1 + prim_options["meas_return"] = "single" + elif meas_type == "avg_kerneled": + prim_options["meas_level"] = 1 + prim_options["meas_return"] = "avg" + else: + options_copy["execution"]["meas_type"] = meas_type + + if not options_copy["execution"]: + del options_copy["execution"] + + primitive_inst = BackendSamplerV2(backend=backend, options=prim_options) + else: + if default_shots := options_copy.pop("default_shots", None): + inputs["precision"] = 1 / math.sqrt(default_shots) + if default_precision := options_copy.pop("default_precision", None): + prim_options["default_precision"] = default_precision + primitive_inst = BackendEstimatorV2(backend=backend, options=prim_options) + + if options_copy: + warnings.warn(f"Options {options_copy} have no effect in local testing mode.") + + return primitive_inst.run(**inputs) + + +@dataclass +class Options: + """Options for :class:`~.BackendSamplerV2`""" + + default_shots: int = 1024 + """The default shots to use if none are specified in :meth:`~.run`. + Default: 1024. + """ + + seed_simulator: int | None = None + """The seed to use in the simulator. If None, a random seed will be used. + Default: None. + """ + + noise_model: Any | None = None + meas_level: int | None = None + meas_return: str | None = None + + +def _patched_run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[SamplerPubResult]: + """Compute results for pubs that all require the same value of ``shots``.""" + # prepare circuits + bound_circuits = [pub.parameter_values.bind_all(pub.circuit) for pub in pubs] + flatten_circuits = [] + for circuits in bound_circuits: + flatten_circuits.extend(np.ravel(circuits).tolist()) + + # run circuits + run_opts = { + k: getattr(self._options, k) + for k in ("noise_model", "meas_return", "meas_level") + if getattr(self._options, k) is not None + } + results, _ = _patched_run_circuits( + flatten_circuits, + self._backend, + memory=True, + shots=shots, + seed_simulator=self._options.seed_simulator, + **run_opts, + ) + result_memory = qiskit.primitives.backend_sampler_v2._prepare_memory(results) + + # pack memory to an ndarray of uint8 + results = [] + start = 0 + for pub, bound in zip(pubs, bound_circuits): + meas_info, max_num_bytes = qiskit.primitives.backend_sampler_v2._analyze_circuit( + pub.circuit + ) + end = start + bound.size + results.append( + self._postprocess_pub( + result_memory[start:end], + shots, + bound.shape, + meas_info, + max_num_bytes, + pub.circuit.metadata, + meas_level=self._options.meas_level, + ) + ) + start = end + + return results + + +def _patched_postprocess_pub( + self, # pylint: disable=unused-argument + result_memory: list[list[str]], + shots: int, + shape: tuple[int, ...], + meas_info: list[qiskit.primitives.backend_sampler_v2._MeasureInfo], + max_num_bytes: int, + circuit_metadata: dict, + meas_level: int | None = None, +) -> SamplerPubResult: + """Converts the memory data into an array of bit arrays with the shape of the pub.""" + if meas_level == 2 or meas_level is None: + arrays = { + item.creg_name: np.zeros(shape + (shots, item.num_bytes), dtype=np.uint8) + for item in meas_info + } + memory_array = qiskit.primitives.backend_sampler_v2._memory_array( + result_memory, max_num_bytes + ) + + for samples, index in zip(memory_array, np.ndindex(*shape)): + for item in meas_info: + ary = qiskit.primitives.backend_sampler_v2._samples_to_packed_array( + samples, item.num_bits, item.start + ) + arrays[item.creg_name][index] = ary + + meas = { + item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info + } + elif meas_level == 1: + raw = np.array(result_memory) + cplx = raw[..., 0] + 1j * raw[..., 1] + cplx = np.reshape(cplx, (*shape, *cplx.shape[1:])) + meas = {item.creg_name: cplx for item in meas_info} + else: + raise QiskitError(f"Unsupported meas_level: {meas_level}") + return SamplerPubResult( + DataBin(**meas, shape=shape), + metadata={"shots": shots, "circuit_metadata": circuit_metadata}, + ) + + +def patch_sampler_test_support(): + """Monkey-patching to pass metadata through to test backends and support level 1""" + warnings.filterwarnings("ignore", ".*Could not determine job completion time.*", UserWarning) + qiskit.primitives.backend_sampler_v2.Options = Options + QiskitRuntimeLocalService._run_backend_primitive_v2 = _patched_run_backend_primitive_v2 + BackendSamplerV2._run_pubs = _patched_run_pubs + BackendSamplerV2._postprocess_pub = _patched_postprocess_pub diff --git a/qiskit_experiments/test/pulse_backend.py b/qiskit_experiments/test/pulse_backend.py index ab3f85d317..adeaa835e1 100644 --- a/qiskit_experiments/test/pulse_backend.py +++ b/qiskit_experiments/test/pulse_backend.py @@ -90,6 +90,11 @@ def __init__( atol: Absolute tolerance during solving. rtol: Relative tolerance during solving. """ + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + + patch_sampler_test_support() + from qiskit_dynamics import Solver super().__init__( @@ -307,6 +312,7 @@ def _state_to_measurement_data( if memory: memory_data = state.sample_memory(shots) measurement_data = dict(zip(*np.unique(memory_data, return_counts=True))) + memory_data = memory_data.tolist() else: measurement_data = state.sample_counts(shots) else: diff --git a/qiskit_experiments/test/t2hahn_backend.py b/qiskit_experiments/test/t2hahn_backend.py index 1a688c864c..b1af4957cc 100644 --- a/qiskit_experiments/test/t2hahn_backend.py +++ b/qiskit_experiments/test/t2hahn_backend.py @@ -104,6 +104,11 @@ def __init__( Initialize the T2Hahn backend """ + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + + patch_sampler_test_support() + super().__init__( name="T2Hahn_simulator", backend_version="0", diff --git a/test/base.py b/test/base.py index afa798ca6f..d7fa6b763f 100644 --- a/test/base.py +++ b/test/base.py @@ -12,8 +12,6 @@ """ Qiskit Experiments test case class """ -# Needed for the monkey-patching at the bottom of the file -from __future__ import annotations import os import json @@ -36,34 +34,6 @@ from qiskit_experiments.framework.experiment_data import ExperimentStatus from .extended_equality import is_equivalent -# The imports from here to the next blank line are just for the monkey-patching -# at the end of the file. -# pylint: disable=wrong-import-order,ungrouped-imports -import copy -import math -from dataclasses import dataclass -from typing import Literal -import numpy as np -import qiskit.primitives.backend_sampler_v2 -from qiskit.circuit import QuantumCircuit -from qiskit.exceptions import QiskitError -from qiskit.primitives import ( - BackendEstimatorV2, - BackendSamplerV2, -) -from qiskit.primitives.containers import ( - BitArray, - DataBin, - SamplerPubResult, -) -from qiskit.primitives.containers.sampler_pub import SamplerPub -from qiskit.primitives.primitive_job import PrimitiveJob -from qiskit.providers.backend import BackendV1, BackendV2 -from qiskit.result import Result -from qiskit_ibm_runtime.fake_provider.local_service import QiskitRuntimeLocalService - -# pylint: enable=wrong-import-order,ungrouped-imports - # Workaround until https://github.com/Qiskit/qiskit-aer/pull/2142 is released try: @@ -136,12 +106,10 @@ def setUpClass(cls): """Set-up test class.""" super().setUpClass() - # Monkey-patching hacks to pass metadata through to test backends - # and support level 1 - qiskit.primitives.backend_sampler_v2.Options = Options - QiskitRuntimeLocalService._run_backend_primitive_v2 = _patched_run_backend_primitive_v2 - BackendSamplerV2._run_pubs = _patched_run_pubs - BackendSamplerV2._postprocess_pub = _patched_postprocess_pub + # Temporary workaround for missing support in Qiskit and qiskit-ibm-runtime + from qiskit_experiments.test.patching import patch_sampler_test_support + + patch_sampler_test_support() warnings.filterwarnings("error", category=DeprecationWarning) # Tests should not generate any warnings unless testing those @@ -170,6 +138,13 @@ def setUpClass(cls): message=".*Could not determine job completion time.*", category=UserWarning, ) + # Generated by restless tests using BackendSamplerV2 + warnings.filterwarnings( + "default", + module="qiskit_experiments", + message=".*have no effect in local testing mode.*", + category=UserWarning, + ) # Some functionality may be deprecated in Qiskit Experiments. If # the deprecation warnings aren't filtered, the tests will fail as @@ -364,214 +339,3 @@ def experiment_data_equiv(cls, data1, data2): QiskitExperimentsTestCase = create_base_test_case(USE_TESTTOOLS) - - -# The rest of this file contains definitions for monkey patching support for -# level 1 data and a noise model run option into BackendSamplerV2 -def _patched_run_circuits( - circuits: QuantumCircuit | list[QuantumCircuit], - backend: BackendV1 | BackendV2, - **run_options, -) -> tuple[list[Result], list[dict]]: - """Remove metadata of circuits and run the circuits on a backend. - Args: - circuits: The circuits - backend: The backend - monitor: Enable job minotor if True - **run_options: run_options - Returns: - The result and the metadata of the circuits - """ - if isinstance(circuits, QuantumCircuit): - circuits = [circuits] - metadata = [] - for circ in circuits: - metadata.append(circ.metadata) - # Commenting out this line is only change from qiskit.primitives.backend_estimator._run_circuits - # circ.metadata = {} - if isinstance(backend, BackendV1): - max_circuits = getattr(backend.configuration(), "max_experiments", None) - elif isinstance(backend, BackendV2): - max_circuits = backend.max_circuits - else: - raise RuntimeError("Backend version not supported") - if max_circuits: - jobs = [ - backend.run(circuits[pos : pos + max_circuits], **run_options) - for pos in range(0, len(circuits), max_circuits) - ] - result = [x.result() for x in jobs] - else: - result = [backend.run(circuits, **run_options).result()] - return result, metadata - - -def _patched_run_backend_primitive_v2( - self, # pylint: disable=unused-argument - backend: BackendV1 | BackendV2, - primitive: Literal["sampler", "estimator"], - options: dict, - inputs: dict, -) -> PrimitiveJob: - """Run V2 backend primitive. - - Args: - backend: The backend to run the primitive on. - primitive: Name of the primitive. - options: Primitive options to use. - inputs: Primitive inputs. - - Returns: - The job object of the result of the primitive. - """ - options_copy = copy.deepcopy(options) - - prim_options = {} - sim_options = options_copy.get("simulator", {}) - if seed_simulator := sim_options.pop("seed_simulator", None): - prim_options["seed_simulator"] = seed_simulator - if noise_model := sim_options.pop("noise_model", None): - prim_options["noise_model"] = noise_model - if not sim_options: - options_copy.pop("simulator", None) - if primitive == "sampler": - if default_shots := options_copy.pop("default_shots", None): - prim_options["default_shots"] = default_shots - if meas_type := options_copy.get("execution", {}).pop("meas_type", None): - if meas_type == "classified": - prim_options["meas_level"] = 2 - prim_options["meas_return"] = "single" - elif meas_type == "kerneled": - prim_options["meas_level"] = 1 - prim_options["meas_return"] = "single" - elif meas_type == "avg_kerneled": - prim_options["meas_level"] = 1 - prim_options["meas_return"] = "avg" - else: - options_copy["execution"]["meas_type"] = meas_type - - if not options_copy["execution"]: - del options_copy["execution"] - - primitive_inst = BackendSamplerV2(backend=backend, options=prim_options) - else: - if default_shots := options_copy.pop("default_shots", None): - inputs["precision"] = 1 / math.sqrt(default_shots) - if default_precision := options_copy.pop("default_precision", None): - prim_options["default_precision"] = default_precision - primitive_inst = BackendEstimatorV2(backend=backend, options=prim_options) - - if options_copy: - warnings.warn(f"Options {options_copy} have no effect in local testing mode.") - - return primitive_inst.run(**inputs) - - -@dataclass -class Options: - """Options for :class:`~.BackendSamplerV2`""" - - default_shots: int = 1024 - """The default shots to use if none are specified in :meth:`~.run`. - Default: 1024. - """ - - seed_simulator: int | None = None - """The seed to use in the simulator. If None, a random seed will be used. - Default: None. - """ - - noise_model: Any | None = None - meas_level: int | None = None - meas_return: str | None = None - - -def _patched_run_pubs(self, pubs: list[SamplerPub], shots: int) -> list[SamplerPubResult]: - """Compute results for pubs that all require the same value of ``shots``.""" - # prepare circuits - bound_circuits = [pub.parameter_values.bind_all(pub.circuit) for pub in pubs] - flatten_circuits = [] - for circuits in bound_circuits: - flatten_circuits.extend(np.ravel(circuits).tolist()) - - # run circuits - run_opts = { - k: getattr(self._options, k) - for k in ("noise_model", "meas_return", "meas_level") - if getattr(self._options, k) is not None - } - results, _ = _patched_run_circuits( - flatten_circuits, - self._backend, - memory=True, - shots=shots, - seed_simulator=self._options.seed_simulator, - **run_opts, - ) - result_memory = qiskit.primitives.backend_sampler_v2._prepare_memory(results) - - # pack memory to an ndarray of uint8 - results = [] - start = 0 - for pub, bound in zip(pubs, bound_circuits): - meas_info, max_num_bytes = qiskit.primitives.backend_sampler_v2._analyze_circuit( - pub.circuit - ) - end = start + bound.size - results.append( - self._postprocess_pub( - result_memory[start:end], - shots, - bound.shape, - meas_info, - max_num_bytes, - pub.circuit.metadata, - meas_level=self._options.meas_level, - ) - ) - start = end - - return results - - -def _patched_postprocess_pub( - self, # pylint: disable=unused-argument - result_memory: list[list[str]], - shots: int, - shape: tuple[int, ...], - meas_info: list[qiskit.primitives.backend_sampler_v2._MeasureInfo], - max_num_bytes: int, - circuit_metadata: dict, - meas_level: int | None = None, -) -> SamplerPubResult: - """Converts the memory data into an array of bit arrays with the shape of the pub.""" - if meas_level == 2 or meas_level is None: - arrays = { - item.creg_name: np.zeros(shape + (shots, item.num_bytes), dtype=np.uint8) - for item in meas_info - } - memory_array = qiskit.primitives.backend_sampler_v2._memory_array( - result_memory, max_num_bytes - ) - - for samples, index in zip(memory_array, np.ndindex(*shape)): - for item in meas_info: - ary = qiskit.primitives.backend_sampler_v2._samples_to_packed_array( - samples, item.num_bits, item.start - ) - arrays[item.creg_name][index] = ary - - meas = { - item.creg_name: BitArray(arrays[item.creg_name], item.num_bits) for item in meas_info - } - elif meas_level == 1: - raw = np.array(result_memory) - cplx = raw[..., 0] + 1j * raw[..., 1] - cplx = np.reshape(cplx, (*shape, *cplx.shape[1:])) - meas = {item.creg_name: cplx for item in meas_info} - else: - raise QiskitError(f"Unsupported meas_level: {meas_level}") - return SamplerPubResult( - DataBin(**meas, shape=shape), - metadata={"shots": shots, "circuit_metadata": circuit_metadata}, - )