From a5a01eb23cb2b3721f893af7210f1820ade8c05a Mon Sep 17 00:00:00 2001 From: David McKay Date: Sat, 21 Sep 2024 15:38:22 -0400 Subject: [PATCH 01/14] initial changes --- .../framework/base_experiment.py | 37 ++++++++- .../framework/experiment_data.py | 75 +++++++++++++------ 2 files changed, 90 insertions(+), 22 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 41240df41c..7dd8b715cb 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -30,6 +30,8 @@ from qiskit_experiments.framework.configs import ExperimentConfig from qiskit_experiments.database_service import Qubit +from qiskit_ibm_runtime import SamplerV2 as Sampler + class BaseExperiment(ABC, StoreInitArgs): """Abstract base class for experiments.""" @@ -40,6 +42,7 @@ def __init__( analysis: Optional[BaseAnalysis] = None, backend: Optional[Backend] = None, experiment_type: Optional[str] = None, + use_sampler: Options[bool] = False ): """Initialize the experiment object. @@ -82,6 +85,7 @@ def __init__( # attributes created during initialization self._backend = None self._backend_data = None + self._use_sampler = use_sampler if isinstance(backend, Backend): self._set_backend(backend) @@ -199,6 +203,7 @@ def run( backend: Optional[Backend] = None, analysis: Optional[Union[BaseAnalysis, None]] = "default", timeout: Optional[float] = None, + use_sampler: Optional[bool] = None, **run_options, ) -> ExperimentData: """Run an experiment and perform analysis. @@ -235,6 +240,9 @@ def run( else: experiment = self + if use_sampler is not None: + self._use_sampler = use_sampler + if experiment.backend is None: raise QiskitError("Cannot run experiment, no backend has been set.") @@ -348,7 +356,34 @@ 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 self._use_sampler: + 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') + + + jobs = [sampler.run(circs, shots=run_options.get('shots', None)) 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/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index b1795afff1..93628e9b03 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -68,6 +68,8 @@ from .containers.figure_data import FigureData, FigureType +from qiskit.primitives import BitArray, SamplerPubResult + if TYPE_CHECKING: # There is a cyclical dependency here, but the name needs to exist for # Sphinx on Python 3.9+ to link type hints correctly. The gating on @@ -985,27 +987,58 @@ 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 type(testres.data[next(iter(testres.data))]) is BitArray: + #bit results so has counts + data["meas_level"] = 2 + data["meas_return"] = 'avg' + #join the data + data["counts"] = testres.join_data(testres.data.keys()).get_counts() + #number of shots + data["shots"] = testres.data[next(iter(testres.data))].num_shots + else: + raise QiskitError("Sampler with meas level 1 support TBD") + + data["metadata"] = testres.metadata['circuit_metadata'] + + self._result_data.append(data) def _retrieve_data(self): """Retrieve job data if missing experiment data.""" From 06b0bdffc39d530bb7ed6cb7ebb12c434cef1c6d Mon Sep 17 00:00:00 2001 From: David McKay Date: Fri, 27 Sep 2024 03:12:02 -0400 Subject: [PATCH 02/14] fit lint errors, upgrade pylint, fix tomo bug --- .../framework/base_experiment.py | 38 ++++++++++--------- .../framework/experiment_data.py | 32 ++++++++-------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 7dd8b715cb..551fcce58c 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -23,6 +23,7 @@ from qiskit.exceptions import QiskitError from qiskit.qobj.utils import MeasLevel from qiskit.providers.options import Options +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 @@ -30,8 +31,6 @@ from qiskit_experiments.framework.configs import ExperimentConfig from qiskit_experiments.database_service import Qubit -from qiskit_ibm_runtime import SamplerV2 as Sampler - class BaseExperiment(ABC, StoreInitArgs): """Abstract base class for experiments.""" @@ -42,7 +41,7 @@ def __init__( analysis: Optional[BaseAnalysis] = None, backend: Optional[Backend] = None, experiment_type: Optional[str] = None, - use_sampler: Options[bool] = False + use_sampler: Options[bool] = False, ): """Initialize the experiment object. @@ -218,6 +217,7 @@ def run( it contains one. timeout: Time to wait for experiment jobs to finish running before cancelling. + use_sampler: Use the SamplerV2 to run the experiment run_options: backend runtime options used for circuit execution. Returns: @@ -359,29 +359,31 @@ def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[Job]: if self._use_sampler: 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: + # 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': + 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 + # 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') + raise QiskitError("Only meas level 1 + 2 supported by sampler") - - jobs = [sampler.run(circs, shots=run_options.get('shots', None)) for circs in job_circuits] + jobs = [ + sampler.run(circs, shots=run_options.get("shots", None)) for circs in job_circuits + ] else: jobs = [self.backend.run(circs, **run_options) for circs in job_circuits] diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index 93628e9b03..8c2662e892 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 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, @@ -68,8 +70,6 @@ from .containers.figure_data import FigureData, FigureType -from qiskit.primitives import BitArray, SamplerPubResult - if TYPE_CHECKING: # There is a cyclical dependency here, but the name needs to exist for # Sphinx on Python 3.9+ to link type hints correctly. The gating on @@ -987,8 +987,8 @@ 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 hasattr(result, 'results'): - #backend run results + if hasattr(result, "results"): + # backend run results if job_id is None: job_id = result.job_id if job_id not in self._jobs: @@ -1011,9 +1011,9 @@ def _add_result_data(self, result: Result, job_id: Optional[str] = None) -> None data["meas_return"] = expr_result.meas_return self._result_data.append(data) else: - #sampler results + # sampler results if job_id is None: - raise QiskitError('job_id must be provided, not available in the sampler result') + 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) @@ -1022,22 +1022,22 @@ def _add_result_data(self, result: Result, job_id: Optional[str] = None) -> None # 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) + # 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 type(testres.data[next(iter(testres.data))]) is BitArray: - #bit results so has counts + if isinstance(testres.data[next(iter(testres.data))], BitArray): + # bit results so has counts data["meas_level"] = 2 - data["meas_return"] = 'avg' - #join the data + data["meas_return"] = "avg" + # join the data data["counts"] = testres.join_data(testres.data.keys()).get_counts() - #number of shots + # number of shots data["shots"] = testres.data[next(iter(testres.data))].num_shots else: - raise QiskitError("Sampler with meas level 1 support TBD") - - data["metadata"] = testres.metadata['circuit_metadata'] - + raise QiskitError("Sampler with meas level 1 support TBD") + + data["metadata"] = testres.metadata["circuit_metadata"] + self._result_data.append(data) def _retrieve_data(self): From 743d6a7796008f888ed3534a10c98dd0454ba0a5 Mon Sep 17 00:00:00 2001 From: David McKay Date: Mon, 7 Oct 2024 00:58:34 -0600 Subject: [PATCH 03/14] update to pass in session --- .../framework/base_experiment.py | 91 +++++++++++-------- .../library/quantum_volume/qv_analysis.py | 3 +- 2 files changed, 52 insertions(+), 42 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 551fcce58c..bf54fd7261 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -41,7 +41,7 @@ def __init__( analysis: Optional[BaseAnalysis] = None, backend: Optional[Backend] = None, experiment_type: Optional[str] = None, - use_sampler: Options[bool] = False, + backend_run: Options[bool] = False, ): """Initialize the experiment object. @@ -50,7 +50,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. """ @@ -84,7 +84,7 @@ def __init__( # attributes created during initialization self._backend = None self._backend_data = None - self._use_sampler = use_sampler + self._backend_run = backend_run if isinstance(backend, Backend): self._set_backend(backend) @@ -199,25 +199,26 @@ def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "BaseExperiment": def run( self, - backend: Optional[Backend] = None, + run_obj: Optional[Union[Backend, Sampler]] = None, analysis: Optional[Union[BaseAnalysis, None]] = "default", timeout: Optional[float] = None, - use_sampler: Optional[bool] = 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. + run_obj: Optional, the element to run the experiment on. Either a + backend or a session. Will override existing backend settings. + If None then a session 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. - use_sampler: Use the SamplerV2 to run the experiment + backend_run: Use backend run (temp option for testing) run_options: backend runtime options used for circuit execution. Returns: @@ -228,11 +229,11 @@ def run( ExperimentData container. """ - if backend is not None or analysis != "default" or run_options: + if isinstance(run_obj, Backend) or analysis != "default" or run_options: # Make a copy to update analysis or backend if one is provided at runtime experiment = self.copy() - if backend: - experiment._set_backend(backend) + if isinstance(run_obj, Backend): + experiment._set_backend(run_obj) if isinstance(analysis, BaseAnalysis): experiment.analysis = analysis if run_options: @@ -240,8 +241,8 @@ def run( else: experiment = self - if use_sampler is not None: - self._use_sampler = use_sampler + if backend_run is not None: + self._backend_run = backend_run if experiment.backend is None: raise QiskitError("Cannot run experiment, no backend has been set.") @@ -258,8 +259,14 @@ def run( # Run options run_opts = experiment.run_options.__dict__ + # see if sampler was sent in + if isinstance(run_obj, Sampler): + sampler = run_obj + else: + sampler = None + # 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 @@ -341,7 +348,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: Sampler = None, **run_options + ) -> List[Job]: """Run circuits on backend as 1 or more jobs.""" max_circuits = self._max_circuits(self.backend) @@ -356,34 +365,36 @@ def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[Job]: job_circuits = [circuits] # Run jobs - if self._use_sampler: - 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" + 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: - # 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") + raise QiskitError("Only meas level 1 + 2 supported by sampler") - jobs = [ - sampler.run(circs, shots=run_options.get("shots", None)) for circs in job_circuits - ] + sampler.options.default_shots = run_options.get("shots", None) + + jobs = [sampler.run(circs) for circs in job_circuits] else: jobs = [self.backend.run(circs, **run_options) for circs in job_circuits] diff --git a/qiskit_experiments/library/quantum_volume/qv_analysis.py b/qiskit_experiments/library/quantum_volume/qv_analysis.py index 92bc1a207a..8097789fd3 100644 --- a/qiskit_experiments/library/quantum_volume/qv_analysis.py +++ b/qiskit_experiments/library/quantum_volume/qv_analysis.py @@ -214,8 +214,7 @@ def _calc_ideal_heavy_output(probabilities_vector, depth): format_spec = f"{{0:0{depth}b}}" # Keys are bit strings and values are probabilities of observing those strings all_output_prob_ideal = { - format_spec.format(b): float(np.real(probabilities_vector[b])) - for b in range(2**depth) + format_spec.format(b): float(np.real(probabilities_vector[b])) for b in range(2**depth) } median_probabilities = float(np.real(np.median(probabilities_vector))) From 4cb91019be7a53237bd676a64f2d646011cc862a Mon Sep 17 00:00:00 2001 From: David McKay Date: Mon, 7 Oct 2024 01:03:03 -0600 Subject: [PATCH 04/14] revert black for qv --- qiskit_experiments/library/quantum_volume/qv_analysis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/library/quantum_volume/qv_analysis.py b/qiskit_experiments/library/quantum_volume/qv_analysis.py index 8097789fd3..92bc1a207a 100644 --- a/qiskit_experiments/library/quantum_volume/qv_analysis.py +++ b/qiskit_experiments/library/quantum_volume/qv_analysis.py @@ -214,7 +214,8 @@ def _calc_ideal_heavy_output(probabilities_vector, depth): format_spec = f"{{0:0{depth}b}}" # Keys are bit strings and values are probabilities of observing those strings all_output_prob_ideal = { - format_spec.format(b): float(np.real(probabilities_vector[b])) for b in range(2**depth) + format_spec.format(b): float(np.real(probabilities_vector[b])) + for b in range(2**depth) } median_probabilities = float(np.real(np.median(probabilities_vector))) From e18c8a6d05f716ae67c5683a113b46e5d2a7b099 Mon Sep 17 00:00:00 2001 From: David McKay Date: Mon, 7 Oct 2024 01:08:05 -0600 Subject: [PATCH 05/14] fix neko --- qiskit_experiments/framework/base_experiment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index bf54fd7261..7bdaa60296 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -392,7 +392,8 @@ def _run_jobs( else: raise QiskitError("Only meas level 1 + 2 supported by sampler") - sampler.options.default_shots = run_options.get("shots", None) + 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: From f7f7c73e7fc1cd73797e04259653a9a46cd40e6b Mon Sep 17 00:00:00 2001 From: David McKay Date: Mon, 7 Oct 2024 01:30:30 -0600 Subject: [PATCH 06/14] updated batch --- .../framework/composite/batch_experiment.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) 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 From 032abbf03f79afae9a85c54fef15f3a5fe5bb8e0 Mon Sep 17 00:00:00 2001 From: David McKay Date: Mon, 7 Oct 2024 13:54:57 -0600 Subject: [PATCH 07/14] updates --- .../framework/base_experiment.py | 13 ++++---- .../framework/experiment_data.py | 33 ++++++++++--------- requirements.txt | 1 + 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 7bdaa60296..9c7f4a6dfc 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -23,6 +23,7 @@ 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 @@ -199,7 +200,7 @@ def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "BaseExperiment": def run( self, - run_obj: Optional[Union[Backend, Sampler]] = None, + run_obj: Optional[Union[Backend, BaseSamplerV2]] = None, analysis: Optional[Union[BaseAnalysis, None]] = "default", timeout: Optional[float] = None, backend_run: Optional[bool] = None, @@ -229,6 +230,9 @@ def run( ExperimentData container. """ + if backend_run is not None: + self._backend_run = backend_run + if isinstance(run_obj, Backend) or analysis != "default" or run_options: # Make a copy to update analysis or backend if one is provided at runtime experiment = self.copy() @@ -241,9 +245,6 @@ def run( else: experiment = self - if backend_run is not None: - self._backend_run = backend_run - if experiment.backend is None: raise QiskitError("Cannot run experiment, no backend has been set.") @@ -260,7 +261,7 @@ def run( run_opts = experiment.run_options.__dict__ # see if sampler was sent in - if isinstance(run_obj, Sampler): + if isinstance(run_obj, BaseSamplerV2): sampler = run_obj else: sampler = None @@ -349,7 +350,7 @@ def job_info(self, backend: Backend = None): } def _run_jobs( - self, circuits: List[QuantumCircuit], sampler: Sampler = None, **run_options + 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) diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index 8c2662e892..2473cf33df 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -38,7 +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 +from qiskit.primitives import BitArray, SamplerPubResult, BasePrimitiveJob from qiskit_ibm_experiment import ( IBMExperimentService, @@ -740,7 +740,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. @@ -771,19 +771,22 @@ 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() + else: + self.backend = None jid = job.job_id() if jid in self._jobs: 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 From 61c10dfbd719e31f468515733baee643f0099383 Mon Sep 17 00:00:00 2001 From: David McKay Date: Mon, 7 Oct 2024 17:02:28 -0600 Subject: [PATCH 08/14] remove run_obj --- .../framework/base_experiment.py | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 9c7f4a6dfc..433557d46d 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -200,7 +200,8 @@ def from_config(cls, config: Union[ExperimentConfig, Dict]) -> "BaseExperiment": def run( self, - run_obj: Optional[Union[Backend, BaseSamplerV2]] = None, + backend: Optional[Backend] = None, + sampler: Optional[BaseSamplerV2] = None, analysis: Optional[Union[BaseAnalysis, None]] = "default", timeout: Optional[float] = None, backend_run: Optional[bool] = None, @@ -209,9 +210,9 @@ def run( """Run an experiment and perform analysis. Args: - run_obj: Optional, the element to run the experiment on. Either a - backend or a session. Will override existing backend settings. - If None then a session will be invoked from previously + 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"`` @@ -233,11 +234,23 @@ def run( if backend_run is not None: self._backend_run = backend_run - if isinstance(run_obj, Backend) or analysis != "default" or run_options: + if (backend is not None) or (sampler is not None) or analysis != "default" or run_options: # Make a copy to update analysis or backend if one is provided at runtime experiment = self.copy() - if isinstance(run_obj, Backend): - experiment._set_backend(run_obj) + # 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: @@ -260,12 +273,6 @@ def run( # Run options run_opts = experiment.run_options.__dict__ - # see if sampler was sent in - if isinstance(run_obj, BaseSamplerV2): - sampler = run_obj - else: - sampler = None - # Run jobs jobs = experiment._run_jobs(transpiled_circuits, sampler=sampler, **run_opts) experiment_data.add_jobs(jobs, timeout=timeout) From 50bb9720ba22fd6e43f0c2427eba896d497deaee Mon Sep 17 00:00:00 2001 From: David McKay Date: Mon, 7 Oct 2024 17:14:31 -0600 Subject: [PATCH 09/14] update for wills comment --- qiskit_experiments/framework/base_experiment.py | 13 +++++++++---- qiskit_experiments/framework/experiment_data.py | 2 -- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index 433557d46d..f0bc9b28d7 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -231,12 +231,17 @@ def run( ExperimentData container. """ - if backend_run is not None: - self._backend_run = backend_run - - if (backend is not None) or (sampler 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_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: diff --git a/qiskit_experiments/framework/experiment_data.py b/qiskit_experiments/framework/experiment_data.py index 2473cf33df..94e4826416 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -785,8 +785,6 @@ def add_jobs( self.backend, ) self.backend = job.backend() - else: - self.backend = None jid = job.job_id() if jid in self._jobs: From fcb43dc49812c8c5de2c551d8042ba2e879797c4 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Sat, 12 Oct 2024 11:59:45 -0400 Subject: [PATCH 10/14] Updates to SamplerV2 support (#1472) * Patch ExperimentData.completion_times to handle Job.time_per_step not existing * Pass noise_model run option through to Sampler * Add support for level 1 data to Sampler execution * Handle case where the Sampler strips circuit metadata from results * Mark some test backends as simulators so the sampler does not try to validate circuits * Change some tests to be more consistent about shots since the Sampler can not handle the requested number of shots differing from the shots in the actual results. * Support level 2 bitstrings in MockIQBackend * Fix PulseBackend returning data as numpy array instead of a list * Pass run options through to backend in T2HahnBackend * Do not set block_for_results timeout to 0 when TEST_TIMEOUT is 0 (TEST_TIMEOUT=0 indicates no timeout, not immediate timeout). * Monkey-patch BackendSamplerV2's run circuits function in the tests so that it does not strip circuit metadata that the test backends need to generate results. * Fix inconsistency between bitstring format and number of qubits in restless test. * Handle case where job class does not have error_message --- .../framework/base_experiment.py | 2 + .../framework/experiment_data.py | 74 +++++++++++++++++-- qiskit_experiments/test/fake_backend.py | 7 +- qiskit_experiments/test/mock_iq_backend.py | 7 ++ qiskit_experiments/test/pulse_backend.py | 2 +- qiskit_experiments/test/t2hahn_backend.py | 2 +- test/base.py | 55 +++++++++++++- .../test_restless_experiment.py | 2 +- test/framework/test_composite.py | 30 ++++---- .../test_cross_resonance_hamiltonian.py | 5 +- .../tomography/test_process_tomography.py | 2 +- .../tomography/test_state_tomography.py | 2 +- 12 files changed, 158 insertions(+), 32 deletions(-) diff --git a/qiskit_experiments/framework/base_experiment.py b/qiskit_experiments/framework/base_experiment.py index f0bc9b28d7..0975481f18 100644 --- a/qiskit_experiments/framework/base_experiment.py +++ b/qiskit_experiments/framework/base_experiment.py @@ -404,6 +404,8 @@ def _run_jobs( 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 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 94e4826416..b80609c950 100644 --- a/qiskit_experiments/framework/experiment_data.py +++ b/qiskit_experiments/framework/experiment_data.py @@ -316,8 +316,22 @@ 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 @@ -872,7 +886,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()) @@ -1026,18 +1040,62 @@ def _add_result_data(self, result: Result, job_id: Optional[str] = None) -> None # 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 isinstance(testres.data[next(iter(testres.data))], BitArray): + if testres.data: + inner_data = testres.data[next(iter(testres.data))] + if not testres.data: + # No data, usually this only happens in tests + pass + elif isinstance(inner_data, BitArray): # bit results so has counts data["meas_level"] = 2 - data["meas_return"] = "avg" + # 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"] = testres.data[next(iter(testres.data))].num_shots + data["shots"] = inner_data.num_shots + elif isinstance(inner_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 + # 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("Sampler with meas level 1 support TBD") + raise QiskitError(f"Unexpected result format: {type(inner_data)}") - data["metadata"] = testres.metadata["circuit_metadata"] + # 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] + circuit = corresponding_pub[0] + data["metadata"] = circuit.metadata self._result_data.append(data) diff --git a/qiskit_experiments/test/fake_backend.py b/qiskit_experiments/test/fake_backend.py index 0fcf06f626..db26c75ef4 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 @@ -62,13 +63,13 @@ def _default_options(cls): def target(self) -> Target: return self._target - def run(self, run_input, **options): + def run(self, run_input, shots=100, **options): 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..702a69bac6 100644 --- a/qiskit_experiments/test/mock_iq_backend.py +++ b/qiskit_experiments/test/mock_iq_backend.py @@ -239,6 +239,7 @@ def __init__( self._experiment_helper = experiment_helper self._rng = np.random.default_rng(rng_seed) + self.simulator = True super().__init__() @@ -456,6 +457,12 @@ 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/pulse_backend.py b/qiskit_experiments/test/pulse_backend.py index 9f9f4e7d38..ab3f85d317 100644 --- a/qiskit_experiments/test/pulse_backend.py +++ b/qiskit_experiments/test/pulse_backend.py @@ -319,7 +319,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..1a688c864c 100644 --- a/qiskit_experiments/test/t2hahn_backend.py +++ b/qiskit_experiments/test/t2hahn_backend.py @@ -198,6 +198,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/test/base.py b/test/base.py index 999395d1cf..6b4bb876f6 100644 --- a/test/base.py +++ b/test/base.py @@ -25,6 +25,11 @@ 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, @@ -106,6 +111,9 @@ 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 + 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 +135,13 @@ 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, + ) + # Some functionality may be deprecated in Qiskit Experiments. If # the deprecation warnings aren't filtered, the tests will fail as @@ -161,7 +176,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) @@ -321,3 +336,41 @@ 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/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..4d6d3aa69d 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,13 @@ 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 +577,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 +587,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 +608,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 +622,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 +951,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..7e01d4e2db 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..4bc9a904a0 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 From 325f99d1be9a91a10e80bf8bff09362d4806a6c8 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Tue, 15 Oct 2024 11:37:46 -0400 Subject: [PATCH 11/14] 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: From 00981bc4ef8f26ec69437b5ee37f9d61bf9d3e8d Mon Sep 17 00:00:00 2001 From: David McKay Date: Wed, 23 Oct 2024 00:59:11 -0400 Subject: [PATCH 12/14] warning filter, add release note, update doc --- docs/howtos/runtime_sessions.rst | 47 +++++++++++++++++-- qiskit_experiments/framework/backend_data.py | 4 ++ .../primitives_add-1a3bcbb2f189d18e.yaml | 10 ++++ 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml diff --git a/docs/howtos/runtime_sessions.rst b/docs/howtos/runtime_sessions.rst index 90aff4ff3c..be644dd9ae 100644 --- a/docs/howtos/runtime_sessions.rst +++ b/docs/howtos/runtime_sessions.rst @@ -1,5 +1,45 @@ -Use Experiments with Runtime sessions -===================================== +Use Experiments with Runtime sessions and sampler +================================================= + +Problem +------- + +You want to run experiments with a custom `SamplerV2 +`_ service. + +.. note:: + All jobs, by default, run using the ``SamplerV2`` service. When calling ``exp.run`` a + ``SamplerV2`` object will be automatically generated from the specified backend. + +Solution +-------- + +In this example, we will pass in a ``SamplerV2`` object to a tomography experiment. + +.. note:: + If a sampler object is passed to ``exp.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 SamplerV2 as Sampler + from qiskit_experiments.library.tomography import ProcessTomography + from qiskit import QuantumCircuit + + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.backend("ibm_osaka") + qc = QuantumCircuit(1) + qc.x(0) + + 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) + # pass the sampler into the experiment + exp_data = exp.run(sampler) Problem ------- @@ -40,5 +80,4 @@ large number of circuits that can't fit in a single job, it may be helpful to fo # This will prevent further jobs from being submitted without terminating current jobs backend.close_session() -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/qiskit_experiments/framework/backend_data.py b/qiskit_experiments/framework/backend_data.py index 53544cb47d..d28cba1f20 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,6 +25,9 @@ class BackendData: def __init__(self, backend): """Inits the backend and verifies version""" + warnings.filterwarnings( + "ignore", message=".*qiskit.qobj.pulse_qobj.*", category=DeprecationWarning + ) self._backend = backend self._v1 = isinstance(backend, BackendV1) self._v2 = isinstance(backend, BackendV2) diff --git a/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml b/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml new file mode 100644 index 0000000000..546737396a --- /dev/null +++ b/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml @@ -0,0 +1,10 @@ +--- +prelude: > + In this release we added support for the qiskit primitives so that + qiskit-experiments will use the SamplerV2 run path. +upgrade: + - | + Upgrade qiskit-experiments to use the SamplerV2 path. An option is + left in to use the old backend.run path, however, this is scheduled + to be deprecated by ibm-runtime in the near future. + From ebe7a33a1142c2cd83d96c0e6b9a4b66f3d448a6 Mon Sep 17 00:00:00 2001 From: David McKay Date: Wed, 23 Oct 2024 20:16:01 -0400 Subject: [PATCH 13/14] will comments --- docs/howtos/runtime_sessions.rst | 57 ++++--------------- .../primitives_add-1a3bcbb2f189d18e.yaml | 15 +++-- 2 files changed, 20 insertions(+), 52 deletions(-) diff --git a/docs/howtos/runtime_sessions.rst b/docs/howtos/runtime_sessions.rst index be644dd9ae..4b0899f796 100644 --- a/docs/howtos/runtime_sessions.rst +++ b/docs/howtos/runtime_sessions.rst @@ -1,23 +1,24 @@ -Use Experiments with Runtime sessions and sampler -================================================= +Use Experiments with Sampler +============================= Problem ------- -You want to run experiments with a custom `SamplerV2 -`_ service. +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 ``SamplerV2`` service. When calling ``exp.run`` a - ``SamplerV2`` object will be automatically generated from the specified backend. + 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 -------- -In this example, we will pass in a ``SamplerV2`` object to a tomography experiment. +In this example, we will pass in a :class:`qiskit_ibm_runtime.SamplerV2` object to a tomography experiment. .. note:: - If a sampler object is passed to ``exp.run`` then the `run options + 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. @@ -39,45 +40,7 @@ In this example, we will pass in a ``SamplerV2`` object to a tomography experime # Artificially lower circuits per job, adjust value for your own application exp.set_experiment_options(max_circuits=3) # pass the sampler into the experiment - exp_data = exp.run(sampler) + exp_data = exp.run(sampler=sampler) -Problem -------- - -You want to run experiments in a `Runtime session -`_ so that jobs can run in close temporal proximity. - -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 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: - -.. jupyter-input:: - - from qiskit_ibm_runtime import QiskitRuntimeService - from qiskit_experiments.library.tomography import ProcessTomography - from qiskit import QuantumCircuit - - service = QiskitRuntimeService(channel="ibm_quantum") - backend = service.backend("ibm_osaka") - qc = QuantumCircuit(1) - qc.x(0) - - backend.open_session() - 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() diff --git a/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml b/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml index 546737396a..01877f3abc 100644 --- a/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml +++ b/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml @@ -1,10 +1,15 @@ --- prelude: > - In this release we added support for the qiskit primitives so that - qiskit-experiments will use the SamplerV2 run path. + In this release we added support for the Qiskit primitives. + Qiskit Experiments will execute circuits using :class:`qiskit_ibm_runtime.SamplerV2` by default. upgrade: - | - Upgrade qiskit-experiments to use the SamplerV2 path. An option is - left in to use the old backend.run path, however, this is scheduled - to be deprecated by ibm-runtime in the near future. + 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.SamplerV2` + 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 deprecated by + qiskit-ibm-runtime in the near future. From cc66417cbb4053d73fa9933d99367b8e88848ec9 Mon Sep 17 00:00:00 2001 From: David McKay Date: Fri, 25 Oct 2024 10:33:07 -0400 Subject: [PATCH 14/14] small updates --- qiskit_experiments/framework/backend_data.py | 11 +++++++---- .../notes/primitives_add-1a3bcbb2f189d18e.yaml | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/qiskit_experiments/framework/backend_data.py b/qiskit_experiments/framework/backend_data.py index d28cba1f20..3eea026cfe 100644 --- a/qiskit_experiments/framework/backend_data.py +++ b/qiskit_experiments/framework/backend_data.py @@ -25,14 +25,17 @@ class BackendData: def __init__(self, backend): """Inits the backend and verifies version""" - warnings.filterwarnings( - "ignore", message=".*qiskit.qobj.pulse_qobj.*", category=DeprecationWarning - ) + 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/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml b/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml index 01877f3abc..7e716ce507 100644 --- a/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml +++ b/releasenotes/notes/primitives_add-1a3bcbb2f189d18e.yaml @@ -7,9 +7,9 @@ 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.SamplerV2` + 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 deprecated by + However, the ``backend.run()`` method is scheduled to be removed from qiskit-ibm-runtime in the near future.