From 325f99d1be9a91a10e80bf8bff09362d4806a6c8 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Tue, 15 Oct 2024 11:37:46 -0400 Subject: [PATCH] Monkey-patching to get tests to pass for sampler update (#1473) These changes could be merged into #1470 (PR could be redirected to that branch). For now I am just opening a PR to see how the tests run in CI. --- 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 + .../framework/base_experiment.py | 2 + .../framework/experiment_data.py | 39 +-- .../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/fake_backend.py | 3 +- qiskit_experiments/test/mock_iq_backend.py | 9 +- qiskit_experiments/test/patching.py | 260 ++++++++++++++++++ qiskit_experiments/test/pulse_backend.py | 6 + qiskit_experiments/test/t2hahn_backend.py | 5 + test/base.py | 57 +--- test/framework/test_composite.py | 6 +- .../test_cross_resonance_hamiltonian.py | 2 +- .../tomography/test_state_tomography.py | 2 +- 31 files changed, 435 insertions(+), 69 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/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 0975481f18..4338b851c2 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -406,6 +406,8 @@ def _run_jobs( raise QiskitError("Only meas level 1 + 2 supported by sampler") if "noise_model" in run_options: sampler.options.simulator.noise_model = run_options["noise_model"] + if "seed_simulator" in run_options: + sampler.options.simulator.seed_simulator = run_options["seed_simulator"] if run_options.get("shots") is not None: sampler.options.default_shots = run_options.get("shots") diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index b80609c950..db71273ffd 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -319,9 +319,13 @@ def completion_times(self) -> Dict[str, datetime]: if job is not None: if hasattr(job, "time_per_step") and "COMPLETED" in job.time_per_step(): job_times[job_id] = job.time_per_step().get("COMPLETED") - elif (execution := job.result().metadata.get("execution")) and "execution_spans" in execution: + elif ( + execution := job.result().metadata.get("execution") + ) and "execution_spans" in execution: job_times[job_id] = execution["execution_spans"].stop - elif (client := getattr(job, "_api_client", None)) and hasattr(client, "job_metadata"): + elif (client := getattr(job, "_api_client", None)) and hasattr( + client, "job_metadata" + ): metadata = client.job_metadata(job.job_id()) finished = metadata.get("timestamps", {}).get("finished", {}) if finished: @@ -1041,11 +1045,20 @@ def _add_result_data(self, result: Result, job_id: Optional[str] = None) -> None testres = SamplerPubResult(result[i].data, result[i].metadata) data["job_id"] = job_id if testres.data: - inner_data = testres.data[next(iter(testres.data))] - if not testres.data: + joined_data = testres.join_data() + outer_shape = testres.data.shape + if outer_shape: + raise QiskitError( + f"Outer PUB dimensions {outer_shape} found in result. " + "Only unparameterized PUBs are currently supported by " + "qiskit-experiments." + ) + else: + joined_data = None + if joined_data is None: # No data, usually this only happens in tests pass - elif isinstance(inner_data, BitArray): + elif isinstance(joined_data, BitArray): # bit results so has counts data["meas_level"] = 2 # The sampler result always contains bitstrings. At @@ -1059,15 +1072,9 @@ def _add_result_data(self, result: Result, job_id: Optional[str] = None) -> None data["counts"] = testres.join_data(testres.data.keys()).get_counts() data["memory"] = testres.join_data(testres.data.keys()).get_bitstrings() # number of shots - data["shots"] = inner_data.num_shots - elif isinstance(inner_data, np.ndarray): + data["shots"] = joined_data.num_shots + elif isinstance(joined_data, np.ndarray): data["meas_level"] = 1 - joined_data = testres.join_data(testres.data.keys()) - # Need to split off the pub dimension representing - # different parameter binds which is trivial because - # qiskit-experiments does not support parameter binding - # to pubs currently. - joined_data = joined_data[0] if joined_data.ndim == 1: data["meas_return"] = "avg" # TODO: we either need to track shots in the @@ -1085,15 +1092,15 @@ def _add_result_data(self, result: Result, job_id: Optional[str] = None) -> None data["memory"][:, :, 0] = np.real(joined_data) data["memory"][:, :, 1] = np.imag(joined_data) else: - raise QiskitError(f"Unexpected result format: {type(inner_data)}") + raise QiskitError(f"Unexpected result format: {type(joined_data)}") # Some Sampler implementations remove the circuit metadata # which some experiment Analysis classes need. Here we try # to put it back from the circuits themselves. if "circuit_metadata" in testres.metadata: data["metadata"] = testres.metadata["circuit_metadata"] - else: - corresponding_pub = job.inputs["pubs"][i] + elif self._jobs[job_id] is not None: + corresponding_pub = self._jobs[job_id].inputs["pubs"][i] circuit = corresponding_pub[0] data["metadata"] = circuit.metadata 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/fake_backend.py b/qiskit_experiments/test/fake_backend.py index db26c75ef4..d67a259ab4 100644 --- a/qiskit_experiments/test/fake_backend.py +++ b/qiskit_experiments/test/fake_backend.py @@ -63,7 +63,8 @@ def _default_options(cls): def target(self) -> Target: return self._target - def run(self, run_input, shots=100, **options): + def run(self, run_input, **options): + shots = options.get("shots", 100) if not isinstance(run_input, list): run_input = [run_input] results = [ diff --git a/qiskit_experiments/test/mock_iq_backend.py b/qiskit_experiments/test/mock_iq_backend.py index 702a69bac6..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() @@ -459,9 +464,7 @@ def _generate_data( run_result["counts"] = counts if meas_return == "single" or self.options.get("memory"): run_result["memory"] = [ - format(result, "x") - for result, num in enumerate(results) - for _ in range(num) + format(result, "x") for result, num in enumerate(results) for _ in range(num) ] else: # Phase has meaning only for IQ shot, so we calculate it here 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 6b4bb876f6..d7fa6b763f 100644 --- a/test/base.py +++ b/test/base.py @@ -25,11 +25,6 @@ import uncertainties from qiskit.utils.deprecation import deprecate_func import qiskit_aer.backends.aerbackend -# The following imports are just for _patched_run_circuits -import qiskit.primitives.backend_sampler_v2 -from qiskit.circuit import QuantumCircuit -from qiskit.providers import BackendV1, BackendV2 -from qiskit.result import Result from qiskit_experiments.framework import ( ExperimentDecoder, @@ -111,8 +106,10 @@ def setUpClass(cls): """Set-up test class.""" super().setUpClass() - # Hack to pass metadata through to test backends - qiskit.primitives.backend_sampler_v2._run_circuits = _patched_run_circuits + # 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 @@ -141,7 +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 @@ -336,41 +339,3 @@ def experiment_data_equiv(cls, data1, data2): QiskitExperimentsTestCase = create_base_test_case(USE_TESTTOOLS) - - -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 diff --git a/test/framework/test_composite.py b/test/framework/test_composite.py index 4d6d3aa69d..bf6e7cbfd0 100644 --- a/test/framework/test_composite.py +++ b/test/framework/test_composite.py @@ -530,7 +530,11 @@ def run(self, run_input, **options): "header": {"metadata": circ.metadata}, "data": { "counts": cnt, - "memory": [format(int(f"0b{s}", 2), "x") for s, n in cnt.items() for _ in range(n)] + "memory": [ + format(int(f"0b{s}", 2), "x") + for s, n in cnt.items() + for _ in range(n) + ], }, } ) diff --git a/test/library/characterization/test_cross_resonance_hamiltonian.py b/test/library/characterization/test_cross_resonance_hamiltonian.py index 7e01d4e2db..da23fb6320 100644 --- a/test/library/characterization/test_cross_resonance_hamiltonian.py +++ b/test/library/characterization/test_cross_resonance_hamiltonian.py @@ -151,7 +151,7 @@ def test_integration(self, ix, iy, iz, zx, zy, zz): dt = 0.222e-9 sigma = 64 - shots=2000 + shots = 2000 backend = AerSimulator(seed_simulator=123, shots=shots) backend._configuration.dt = dt diff --git a/test/library/tomography/test_state_tomography.py b/test/library/tomography/test_state_tomography.py index 4bc9a904a0..b0ec4b52c8 100644 --- a/test/library/tomography/test_state_tomography.py +++ b/test/library/tomography/test_state_tomography.py @@ -374,7 +374,7 @@ def test_mitigated_full_qst(self, qubits): target = qi.random_statevector(2 ** len(qubits), seed=seed) exp = MitigatedStateTomography(target, physical_qubits=qubits, backend=backend) exp.analysis.set_options(unmitigated_fit=True) - expdata = exp.run(analysis=None) + expdata = exp.run(analysis=None, shots=shots) self.assertExperimentDone(expdata) for fitter in FITTERS: