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/howtos/runtime_sessions.rst b/docs/howtos/runtime_sessions.rst index 90aff4ff3c..4b0899f796 100644 --- a/docs/howtos/runtime_sessions.rst +++ b/docs/howtos/runtime_sessions.rst @@ -1,29 +1,30 @@ -Use Experiments with Runtime sessions -===================================== +Use Experiments with Sampler +============================= Problem ------- -You want to run experiments in a `Runtime session -`_ so that jobs can run in close temporal proximity. +You want to run experiments with a custom :class:`qiskit.primitives.BaseSamplerV2` service. +A sampler can be instantiated with a backend, session or batch, which allows one to +run an experiment in different execution modes. + +.. note:: + All jobs, by default, run using the :class:`qiskit_ibm_runtime.SamplerV2` class. When calling ``exp.run`` a + :class:`qiskit_ibm_runtime.SamplerV2` object will be automatically generated to wrap the specified backend. Solution -------- -.. note:: - This guide requires :external+qiskit_ibm_runtime:doc:`qiskit-ibm-runtime ` version 0.15 and up, which can be installed with ``python -m pip install qiskit-ibm-runtime``. - For how to migrate from the older ``qiskit-ibm-provider`` to :external+qiskit_ibm_runtime:doc:`qiskit-ibm-runtime `, - consult the `migration guide `_.\ - -Use the :class:`~qiskit_ibm_runtime.IBMBackend` object in :external+qiskit_ibm_runtime:doc:`index`, which supports sessions. +In this example, we will pass in a :class:`qiskit_ibm_runtime.SamplerV2` object to a tomography experiment. -In this example, we will set the ``max_circuits`` property to an artificially low value so that the experiment will be -split into multiple jobs that run sequentially in a single session. When running real experiments with a -large number of circuits that can't fit in a single job, it may be helpful to follow this usage pattern: +.. note:: + If a sampler object is passed to :meth:`qiskit_experiments.framework.BaseExperiment.run` then the `run options + `_ of the + sampler object are used. The execution options set by the experiment are ignored. .. jupyter-input:: - from qiskit_ibm_runtime import QiskitRuntimeService + from qiskit_ibm_runtime import SamplerV2 as Sampler from qiskit_experiments.library.tomography import ProcessTomography from qiskit import QuantumCircuit @@ -32,13 +33,14 @@ large number of circuits that can't fit in a single job, it may be helpful to fo qc = QuantumCircuit(1) qc.x(0) - backend.open_session() + sampler = Sampler(backed) + # set the shots in the sampler object + sampler.options.default_shots = 300 exp = ProcessTomography(qc) # Artificially lower circuits per job, adjust value for your own application exp.set_experiment_options(max_circuits=3) - exp_data = exp.run(backend) - # This will prevent further jobs from being submitted without terminating current jobs - backend.close_session() + # pass the sampler into the experiment + exp_data = exp.run(sampler=sampler) + + -Note that runtime primitives are not currently supported natively in Qiskit Experiments, so -the ``backend.run()`` path is required to run experiments. 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/backend_data.py b/qiskit_experiments/framework/backend_data.py index 53544cb47d..3eea026cfe 100644 --- a/qiskit_experiments/framework/backend_data.py +++ b/qiskit_experiments/framework/backend_data.py @@ -15,6 +15,7 @@ Since `BackendV1` and `BackendV2` do not share the same interface, this class unifies data access for various data fields. """ +import warnings from qiskit.providers.models import PulseBackendConfiguration # pylint: disable=no-name-in-module from qiskit.providers import BackendV1, BackendV2 @@ -24,11 +25,17 @@ class BackendData: def __init__(self, backend): """Inits the backend and verifies version""" + self._backend = backend self._v1 = isinstance(backend, BackendV1) self._v2 = isinstance(backend, BackendV2) + if self._v2: - self._parse_additional_data() + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", message=".*qiskit.qobj.pulse_qobj.*", category=DeprecationWarning + ) + self._parse_additional_data() def _parse_additional_data(self): # data specific parsing not done yet in upstream qiskit diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 41240df41c..4338b851c2 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -23,6 +23,8 @@ from qiskit.exceptions import QiskitError from qiskit.qobj.utils import MeasLevel from qiskit.providers.options import Options +from qiskit.primitives.base import BaseSamplerV2 +from qiskit_ibm_runtime import SamplerV2 as Sampler from qiskit_experiments.framework import BackendData from qiskit_experiments.framework.store_init_args import StoreInitArgs from qiskit_experiments.framework.base_analysis import BaseAnalysis @@ -40,6 +42,7 @@ def __init__( analysis: Optional[BaseAnalysis] = None, backend: Optional[Backend] = None, experiment_type: Optional[str] = None, + backend_run: Options[bool] = False, ): """Initialize the experiment object. @@ -48,7 +51,7 @@ def __init__( analysis: Optional, the analysis to use for the experiment. backend: Optional, the backend to run the experiment on. experiment_type: Optional, the experiment type string. - + backend_run: Optional, use backend run vs the sampler (temporary) Raises: QiskitError: If qubits contains duplicates. """ @@ -82,6 +85,7 @@ def __init__( # attributes created during initialization self._backend = None self._backend_data = None + self._backend_run = backend_run if isinstance(backend, Backend): self._set_backend(backend) @@ -197,22 +201,26 @@ def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "BaseExperiment": def run( self, backend: Optional[Backend] = None, + sampler: Optional[BaseSamplerV2] = None, analysis: Optional[Union[BaseAnalysis, None]] = "default", timeout: Optional[float] = None, + backend_run: Optional[bool] = None, **run_options, ) -> ExperimentData: """Run an experiment and perform analysis. Args: - backend: Optional, the backend to run the experiment on. This - will override any currently set backends for the single - execution. + backend: Optional, the backend to run on. Will override existing backend settings. + sampler: Optional, the sampler to run the experiment on. + If None then a sampler will be invoked from previously + set backend analysis: Optional, a custom analysis instance to use for performing analysis. If None analysis will not be run. If ``"default"`` the experiments :meth:`analysis` instance will be used if it contains one. timeout: Time to wait for experiment jobs to finish running before cancelling. + backend_run: Use backend run (temp option for testing) run_options: backend runtime options used for circuit execution. Returns: @@ -223,11 +231,31 @@ def run( ExperimentData container. """ - if backend is not None or analysis != "default" or run_options: + if ( + (backend is not None) + or (sampler is not None) + or analysis != "default" + or run_options + or (backend_run is not None) + ): # Make a copy to update analysis or backend if one is provided at runtime experiment = self.copy() - if backend: - experiment._set_backend(backend) + if backend_run is not None: + experiment._backend_run = backend_run + # we specified a backend OR a sampler + if (backend is not None) or (sampler is not None): + if sampler is None: + # backend only specified + experiment._set_backend(backend) + elif backend is None: + # sampler only specifid + experiment._set_backend(sampler._backend) + else: + # we specified both a sampler and a backend + if self._backend_run: + experiment._set_backend(backend) + else: + experiment._set_backend(sampler._backend) if isinstance(analysis, BaseAnalysis): experiment.analysis = analysis if run_options: @@ -251,7 +279,7 @@ def run( run_opts = experiment.run_options.__dict__ # Run jobs - jobs = experiment._run_jobs(transpiled_circuits, **run_opts) + jobs = experiment._run_jobs(transpiled_circuits, sampler=sampler, **run_opts) experiment_data.add_jobs(jobs, timeout=timeout) # Optionally run analysis @@ -333,7 +361,9 @@ def job_info(self, backend: Backend = None): "Total number of jobs": num_jobs, } - def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[Job]: + def _run_jobs( + self, circuits: List[QuantumCircuit], sampler: BaseSamplerV2 = None, **run_options + ) -> List[Job]: """Run circuits on backend as 1 or more jobs.""" max_circuits = self._max_circuits(self.backend) @@ -348,7 +378,43 @@ def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[Job]: job_circuits = [circuits] # Run jobs - jobs = [self.backend.run(circs, **run_options) for circs in job_circuits] + if not self._backend_run: + if sampler is None: + # instantiate a sampler from the backend + sampler = Sampler(self.backend) + + # have to hand set some of these options + # see https://docs.quantum.ibm.com/api/qiskit-ibm-runtime + # /qiskit_ibm_runtime.options.SamplerExecutionOptionsV2 + if "init_qubits" in run_options: + sampler.options.execution.init_qubits = run_options["init_qubits"] + if "rep_delay" in run_options: + sampler.options.execution.rep_delay = run_options["rep_delay"] + if "meas_level" in run_options: + if run_options["meas_level"] == 2: + sampler.options.execution.meas_type = "classified" + elif run_options["meas_level"] == 1: + if "meas_return" in run_options: + if run_options["meas_return"] == "avg": + sampler.options.execution.meas_type = "avg_kerneled" + else: + sampler.options.execution.meas_type = "kerneled" + else: + # assume this is what is wanted if no meas return specified + sampler.options.execution.meas_type = "kerneled" + else: + 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") + + jobs = [sampler.run(circs) for circs in job_circuits] + else: + jobs = [self.backend.run(circs, **run_options) for circs in job_circuits] return jobs diff --git a/qiskit_experiments/framework/composite/batch_experiment.py b/qiskit_experiments/framework/composite/batch_experiment.py index 19ffaaa87d..351f7c3a42 100644 --- a/qiskit_experiments/framework/composite/batch_experiment.py +++ b/qiskit_experiments/framework/composite/batch_experiment.py @@ -18,6 +18,7 @@ from qiskit import QuantumCircuit from qiskit.providers import Job, Backend, Options +from qiskit_ibm_runtime import SamplerV2 as Sampler from .composite_experiment import CompositeExperiment, BaseExperiment from .composite_analysis import CompositeAnalysis @@ -134,7 +135,11 @@ def _remap_qubits(self, circuit, qubit_mapping): return new_circuit def _run_jobs_recursive( - self, circuits: List[QuantumCircuit], truncated_metadata: List[Dict], **run_options + self, + circuits: List[QuantumCircuit], + truncated_metadata: List[Dict], + sampler: Sampler = None, + **run_options, ) -> List[Job]: # The truncated metadata is a truncation of the original composite metadata. # During the recursion, the current experiment (self) will be at the head of the truncated @@ -161,19 +166,21 @@ def _run_jobs_recursive( # even if they run in different jobs if isinstance(exp, BatchExperiment): new_jobs = exp._run_jobs_recursive( - circs_by_subexps[index], truncated_metadata, **run_options + circs_by_subexps[index], truncated_metadata, sampler, **run_options ) else: - new_jobs = exp._run_jobs(circs_by_subexps[index], **run_options) + new_jobs = exp._run_jobs(circs_by_subexps[index], sampler, **run_options) jobs.extend(new_jobs) else: - jobs = super()._run_jobs(circuits, **run_options) + jobs = super()._run_jobs(circuits, sampler, **run_options) return jobs - def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[Job]: + def _run_jobs( + self, circuits: List[QuantumCircuit], sampler: Sampler = None, **run_options + ) -> List[Job]: truncated_metadata = [circ.metadata for circ in circuits] - jobs = self._run_jobs_recursive(circuits, truncated_metadata, **run_options) + jobs = self._run_jobs_recursive(circuits, truncated_metadata, sampler, **run_options) return jobs @classmethod diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index b1795afff1..db71273ffd 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -38,6 +38,7 @@ from qiskit.exceptions import QiskitError from qiskit.providers import Job, Backend, Provider from qiskit.utils.deprecation import deprecate_arg +from qiskit.primitives import BitArray, SamplerPubResult, BasePrimitiveJob from qiskit_ibm_experiment import ( IBMExperimentService, @@ -45,6 +46,7 @@ AnalysisResultData as AnalysisResultDataclass, ResultQuality, ) + from qiskit_experiments.framework.json import ExperimentEncoder, ExperimentDecoder from qiskit_experiments.database_service.utils import ( plot_to_svg_bytes, @@ -314,8 +316,26 @@ def completion_times(self) -> Dict[str, datetime]: """Returns the completion times of the jobs.""" job_times = {} for job_id, job in self._jobs.items(): - if job is not None and "COMPLETED" in job.time_per_step(): - job_times[job_id] = job.time_per_step().get("COMPLETED") + 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: + job_times[job_id] = execution["execution_spans"].stop + 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: + job_times[job_id] = datetime.fromisoformat(finished) + if job_id not in job_times: + warnings.warn( + "Could not determine job completion time. Using current timestamp.", + UserWarning, + ) + job_times[job_id] = datetime.now() return job_times @@ -738,7 +758,7 @@ def add_data( def add_jobs( self, - jobs: Union[Job, List[Job]], + jobs: Union[Job, List[Job], BasePrimitiveJob, List[BasePrimitiveJob]], timeout: Optional[float] = None, ) -> None: """Add experiment data. @@ -769,19 +789,20 @@ def add_jobs( # Add futures for extracting finished job data timeout_ids = [] for job in jobs: - if self.backend is not None: - backend_name = BackendData(self.backend).name - job_backend_name = BackendData(job.backend()).name - if self.backend and backend_name != job_backend_name: - LOG.warning( - "Adding a job from a backend (%s) that is different " - "than the current backend (%s). " - "The new backend will be used, but " - "service is not changed if one already exists.", - job.backend(), - self.backend, - ) - self.backend = job.backend() + if hasattr(job, "backend"): + if self.backend is not None: + backend_name = BackendData(self.backend).name + job_backend_name = BackendData(job.backend()).name + if self.backend and backend_name != job_backend_name: + LOG.warning( + "Adding a job from a backend (%s) that is different " + "than the current backend (%s). " + "The new backend will be used, but " + "service is not changed if one already exists.", + job.backend(), + self.backend, + ) + self.backend = job.backend() jid = job.job_id() if jid in self._jobs: @@ -869,7 +890,7 @@ def _add_job_data( LOG.error( "Job data not added for errored job [Job ID: %s]\nError message: %s", jid, - job.error_message(), + job.error_message() if hasattr(job, "error_message") else "n/a", ) return jid, False LOG.warning("Adding data from job failed [Job ID: %s]", job.job_id()) @@ -985,27 +1006,105 @@ def _add_result_data(self, result: Result, job_id: Optional[str] = None) -> None job_id: The id of the job the result came from. If `None`, the job id in `result` is used. """ - if job_id is None: - job_id = result.job_id - if job_id not in self._jobs: - self._jobs[job_id] = None - self.job_ids.append(job_id) - with self._result_data.lock: - # Lock data while adding all result data - for i, _ in enumerate(result.results): - data = result.data(i) - data["job_id"] = job_id - if "counts" in data: - # Format to Counts object rather than hex dict - data["counts"] = result.get_counts(i) - expr_result = result.results[i] - if hasattr(expr_result, "header") and hasattr(expr_result.header, "metadata"): - data["metadata"] = expr_result.header.metadata - data["shots"] = expr_result.shots - data["meas_level"] = expr_result.meas_level - if hasattr(expr_result, "meas_return"): - data["meas_return"] = expr_result.meas_return - self._result_data.append(data) + if hasattr(result, "results"): + # backend run results + if job_id is None: + job_id = result.job_id + if job_id not in self._jobs: + self._jobs[job_id] = None + self.job_ids.append(job_id) + with self._result_data.lock: + # Lock data while adding all result data + for i, _ in enumerate(result.results): + data = result.data(i) + data["job_id"] = job_id + if "counts" in data: + # Format to Counts object rather than hex dict + data["counts"] = result.get_counts(i) + expr_result = result.results[i] + if hasattr(expr_result, "header") and hasattr(expr_result.header, "metadata"): + data["metadata"] = expr_result.header.metadata + data["shots"] = expr_result.shots + data["meas_level"] = expr_result.meas_level + if hasattr(expr_result, "meas_return"): + data["meas_return"] = expr_result.meas_return + self._result_data.append(data) + else: + # sampler results + if job_id is None: + raise QiskitError("job_id must be provided, not available in the sampler result") + if job_id not in self._jobs: + self._jobs[job_id] = None + self.job_ids.append(job_id) + with self._result_data.lock: + # Lock data while adding all result data + # Sampler results are a list + for i, _ in enumerate(result): + data = {} + # convert to a Sampler Pub Result (can remove this later when the bug is fixed) + testres = SamplerPubResult(result[i].data, result[i].metadata) + data["job_id"] = job_id + if 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(joined_data, BitArray): + # bit results so has counts + data["meas_level"] = 2 + # The sampler result always contains bitstrings. At + # this point, we have lost track of whether the job + # requested memory/meas_return=single. Here we just + # hope that nothing breaks if we always return single + # shot results since the counts dict is also returned + # any way. + data["meas_return"] = "single" + # join the data + 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"] = joined_data.num_shots + elif isinstance(joined_data, np.ndarray): + data["meas_level"] = 1 + if joined_data.ndim == 1: + data["meas_return"] = "avg" + # TODO: we either need to track shots in the + # circuit metadata and pull it out here or get + # upstream to report the number of shots in the + # sampler result for level 1 avg data. + data["shots"] = 1 + data["memory"] = np.zeros((len(joined_data), 2), dtype=float) + data["memory"][:, 0] = np.real(joined_data) + data["memory"][:, 1] = np.imag(joined_data) + else: + data["meas_return"] = "single" + data["shots"] = joined_data.shape[0] + data["memory"] = np.zeros((*joined_data.shape, 2), dtype=float) + data["memory"][:, :, 0] = np.real(joined_data) + data["memory"][:, :, 1] = np.imag(joined_data) + else: + 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"] + 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 + + self._result_data.append(data) def _retrieve_data(self): """Retrieve job data if missing experiment data.""" 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 0fcf06f626..d67a259ab4 100644 --- a/qiskit_experiments/test/fake_backend.py +++ b/qiskit_experiments/test/fake_backend.py @@ -43,6 +43,7 @@ def __init__( num_qubits=1, max_experiments=100, ): + self.simulator = True super().__init__(provider=provider, name=backend_name) self._target = Target(num_qubits=num_qubits) # Add a measure for each qubit so a simple measure circuit works @@ -63,12 +64,13 @@ def target(self) -> Target: return self._target def run(self, run_input, **options): + shots = options.get("shots", 100) if not isinstance(run_input, list): run_input = [run_input] results = [ { - "data": {"0": 100}, - "shots": 100, + "data": {"0": shots}, + "shots": shots, "success": True, "header": {"metadata": circ.metadata}, "meas_level": 2, diff --git a/qiskit_experiments/test/mock_iq_backend.py b/qiskit_experiments/test/mock_iq_backend.py index f59eecac0d..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() @@ -239,6 +244,7 @@ def __init__( self._experiment_helper = experiment_helper self._rng = np.random.default_rng(rng_seed) + self.simulator = True super().__init__() @@ -456,6 +462,10 @@ def _generate_data( result_in_str = str(format(result, "b").zfill(output_length)) counts[result_in_str] = num_occurrences 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) + ] else: # Phase has meaning only for IQ shot, so we calculate it here phase = self.experiment_helper.iq_phase([circuit])[0] 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 9f9f4e7d38..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: @@ -319,7 +325,7 @@ def _state_to_measurement_data( centers = self._iq_cluster_centers(circuit=circuit) measurement_data = self._iq_data(state.probabilities(), shots, centers, 0.2) if meas_return == "avg": - measurement_data = np.average(np.array(measurement_data), axis=0) + measurement_data = np.average(np.array(measurement_data), axis=0).tolist() else: raise QiskitError(f"Unsupported measurement level {meas_level}.") diff --git a/qiskit_experiments/test/t2hahn_backend.py b/qiskit_experiments/test/t2hahn_backend.py index eda6ef8bcc..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", @@ -198,6 +203,6 @@ def run( sim = AerSimulator(noise_model=noise_model, seed_simulator=self._seed) - job = sim.run(new_circuits, shots=shots) + job = sim.run(new_circuits, shots=shots, **options) return FakeJob(self, job.result()) diff --git a/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml b/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml new file mode 100644 index 0000000000..7e716ce507 --- /dev/null +++ b/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml @@ -0,0 +1,15 @@ +--- +prelude: > + In this release we added support for the Qiskit primitives. + Qiskit Experiments will execute circuits using :class:`qiskit_ibm_runtime.SamplerV2` by default. +upgrade: + - | + When only a ``backend`` is set on an experiment, :meth:`qiskit_experiments.framework.BaseExperiment.run` + now defaults to wrapping the ``backend`` in a :class:`qiskit_ibm_runtime.SamplerV2` and + using that to execute the circuits. A new ``sampler`` argument is also + accepted by ``run()`` to allow for a custom :class:`qiskit.primitives.BaseSamplerV2` + instance to be used for circuit execution. ``run()`` also accepts a ``backend_run`` + option which will cause the old ``backend.run`` path to be used for circuit execution. + However, the ``backend.run()`` method is scheduled to be removed from + qiskit-ibm-runtime in the near future. + diff --git a/requirements.txt b/requirements.txt index dde901d97e..99a3ea9ae6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ numpy>=1.17 scipy>=1.4 qiskit>=0.45 qiskit-ibm-experiment>=0.4.6 +qiskit_ibm_runtime>=0.29.0 matplotlib>=3.4 uncertainties lmfit diff --git a/test/base.py b/test/base.py index 999395d1cf..d7fa6b763f 100644 --- a/test/base.py +++ b/test/base.py @@ -106,6 +106,11 @@ def setUpClass(cls): """Set-up test class.""" super().setUpClass() + # 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 # warnings. In that case, the test should catch the warning @@ -127,6 +132,19 @@ def setUpClass(cls): message=".*The curve data representation has been replaced by the `DataFrame` format.*", category=PendingDeprecationWarning, ) + warnings.filterwarnings( + "default", + module="qiskit_experiments", + 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 @@ -161,7 +179,7 @@ def assertExperimentDone( timeout: The maximum time in seconds to wait for executor to complete. Defaults to the value of ``TEST_TIMEOUT``. """ - if timeout is None: + if timeout is None and TEST_TIMEOUT != 0: timeout = TEST_TIMEOUT experiment_data.block_for_results(timeout=timeout) diff --git a/test/data_processing/test_restless_experiment.py b/test/data_processing/test_restless_experiment.py index 7a02e3768e..ac53f81c5a 100644 --- a/test/data_processing/test_restless_experiment.py +++ b/test/data_processing/test_restless_experiment.py @@ -82,7 +82,7 @@ def test_end_to_end_restless_standard_processor(self, pi_ratio): amp_exp = FineXAmplitude([0], backend) # standard data processor. - standard_processor = DataProcessor("counts", [Probability("01")]) + standard_processor = DataProcessor("counts", [Probability("1")]) amp_exp.analysis.set_options(data_processor=standard_processor) # enable a restless measurement setting. amp_exp.enable_restless(rep_delay=1e-6, override_processor_by_restless=False) diff --git a/test/framework/test_composite.py b/test/framework/test_composite.py index c1667a3f60..bf6e7cbfd0 100644 --- a/test/framework/test_composite.py +++ b/test/framework/test_composite.py @@ -454,6 +454,7 @@ def test_composite_subexp_data(self): "1111": 5, }, { + "0000": 6, "0001": 3, "0010": 4, "0011": 5, @@ -499,7 +500,7 @@ def test_composite_subexp_data(self): "1111": 3, }, { - "0000": 3, + "0000": 12, "0001": 6, "0010": 7, "0011": 1, @@ -524,10 +525,17 @@ def run(self, run_input, **options): for circ, cnt in zip(run_input, counts): results.append( { - "shots": -1, + "shots": sum(cnt.values()), "success": True, "header": {"metadata": circ.metadata}, - "data": {"counts": cnt}, + "data": { + "counts": cnt, + "memory": [ + format(int(f"0b{s}", 2), "x") + for s, n in cnt.items() + for _ in range(n) + ], + }, } ) @@ -573,7 +581,7 @@ def circuits(self): ], flatten_results=False, ) - expdata = par_exp.run(Backend(num_qubits=4)) + expdata = par_exp.run(Backend(num_qubits=4), shots=sum(counts[0].values())) self.assertExperimentDone(expdata) self.assertEqual(len(expdata.data()), len(counts)) @@ -583,17 +591,17 @@ def circuits(self): counts1 = [ [ {"00": 14, "10": 19, "11": 11, "01": 8}, - {"01": 14, "10": 7, "11": 13, "00": 12}, + {"01": 14, "10": 7, "11": 13, "00": 18}, {"00": 14, "01": 5, "10": 16, "11": 17}, {"00": 4, "01": 16, "10": 19, "11": 13}, - {"00": 12, "01": 15, "10": 11, "11": 5}, + {"00": 21, "01": 15, "10": 11, "11": 5}, ], [ {"00": 10, "01": 10, "10": 12, "11": 20}, - {"00": 12, "01": 10, "10": 7, "11": 17}, + {"00": 18, "01": 10, "10": 7, "11": 17}, {"00": 17, "01": 7, "10": 14, "11": 14}, {"00": 9, "01": 14, "10": 22, "11": 7}, - {"00": 17, "01": 10, "10": 9, "11": 7}, + {"00": 26, "01": 10, "10": 9, "11": 7}, ], ] @@ -604,11 +612,11 @@ def circuits(self): self.assertDictEqual(circ_data["counts"], circ_counts) counts2 = [ - [{"00": 10, "01": 10, "10": 12, "11": 20}, {"00": 12, "01": 10, "10": 7, "11": 17}], + [{"00": 10, "01": 10, "10": 12, "11": 20}, {"00": 18, "01": 10, "10": 7, "11": 17}], [ {"00": 17, "01": 7, "10": 14, "11": 14}, {"00": 9, "01": 14, "10": 22, "11": 7}, - {"00": 17, "01": 10, "10": 9, "11": 7}, + {"00": 26, "01": 10, "10": 9, "11": 7}, ], ] @@ -618,8 +626,8 @@ def circuits(self): self.assertDictEqual(circ_data["counts"], circ_counts) counts3 = [ - [{"0": 22, "1": 30}, {"0": 19, "1": 27}], - [{"0": 20, "1": 32}, {"0": 22, "1": 24}], + [{"0": 22, "1": 30}, {"0": 25, "1": 27}], + [{"0": 20, "1": 32}, {"0": 28, "1": 24}], ] self.assertEqual(len(expdata.child_data(1).child_data(0).child_data()), len(counts3)) @@ -947,7 +955,7 @@ def test_batch_transpile_options_integrated(self): noise_model = noise.NoiseModel() noise_model.add_all_qubit_quantum_error(noise.depolarizing_error(0.5, 2), ["cx", "swap"]) - expdata = self.batch2.run(backend, noise_model=noise_model, shots=1000) + expdata = self.batch2.run(backend, noise_model=noise_model, shots=1000, memory=True) self.assertExperimentDone(expdata) self.assertEqual(expdata.child_data(0).analysis_results("non-zero counts").value, 8) diff --git a/test/library/characterization/test_cross_resonance_hamiltonian.py b/test/library/characterization/test_cross_resonance_hamiltonian.py index 3ed70b85e7..da23fb6320 100644 --- a/test/library/characterization/test_cross_resonance_hamiltonian.py +++ b/test/library/characterization/test_cross_resonance_hamiltonian.py @@ -151,8 +151,9 @@ def test_integration(self, ix, iy, iz, zx, zy, zz): dt = 0.222e-9 sigma = 64 + shots = 2000 - backend = AerSimulator(seed_simulator=123, shots=2000) + backend = AerSimulator(seed_simulator=123, shots=shots) backend._configuration.dt = dt # Note that Qiskit is Little endian, i.e. [q1, q0] @@ -179,7 +180,7 @@ def test_integration(self, ix, iy, iz, zx, zy, zz): ) expr.backend = backend - exp_data = expr.run() + exp_data = expr.run(shots=shots) self.assertExperimentDone(exp_data, timeout=1000) self.assertEqual(exp_data.analysis_results("omega_ix").quality, "good") diff --git a/test/library/tomography/test_process_tomography.py b/test/library/tomography/test_process_tomography.py index 45df711e01..18ae55dac8 100644 --- a/test/library/tomography/test_process_tomography.py +++ b/test/library/tomography/test_process_tomography.py @@ -55,7 +55,7 @@ def test_full_qpt_random_unitary(self, num_qubits): backend = AerSimulator(seed_simulator=seed, shots=shots) target = qi.random_unitary(2**num_qubits, seed=seed) exp = ProcessTomography(target) - expdata = exp.run(backend, analysis=None) + expdata = exp.run(backend, analysis=None, shots=shots) self.assertExperimentDone(expdata) # Run each tomography fitter analysis as a subtest so diff --git a/test/library/tomography/test_state_tomography.py b/test/library/tomography/test_state_tomography.py index 2b7ae0b7da..b0ec4b52c8 100644 --- a/test/library/tomography/test_state_tomography.py +++ b/test/library/tomography/test_state_tomography.py @@ -55,7 +55,7 @@ def test_full_qst(self, num_qubits): backend = AerSimulator(seed_simulator=seed, shots=shots) target = qi.random_statevector(2**num_qubits, seed=seed) exp = StateTomography(target) - expdata = exp.run(backend, analysis=None) + expdata = exp.run(backend, analysis=None, shots=shots) self.assertExperimentDone(expdata) # Run each tomography fitter analysis as a subtest so @@ -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: