From 6a9f38c0eaa11179a854cd90d222f98cb16bc282 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Mon, 26 Aug 2024 16:39:14 -0400 Subject: [PATCH 01/17] Port `Noise_model.from_dict()` (#1890) * port nosie model class * port from_dict * add release note --- qiskit_ibm_runtime/utils/json.py | 3 +- qiskit_ibm_runtime/utils/noise_model.py | 187 ++++++++++++++++++++++++ release-notes/unreleased/1890.bug.rst | 2 + 3 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 qiskit_ibm_runtime/utils/noise_model.py create mode 100644 release-notes/unreleased/1890.bug.rst diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index 6446140f6..b8881c160 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -71,6 +71,7 @@ PrimitiveResult, ) from qiskit_ibm_runtime.options.zne_options import ExtrapolatorType +from qiskit_ibm_runtime.utils.noise_model import from_dict _TERRA_VERSION = tuple( int(x) for x in re.match(r"\d+\.\d+\.\d", _terra_version_string).group(0).split(".")[:3] @@ -429,7 +430,7 @@ def object_hook(self, obj: Any) -> Any: return obj_val if obj_type == "NoiseModel": if HAS_AER: - return qiskit_aer.noise.NoiseModel.from_dict(obj_val) + return from_dict(obj_val) warnings.warn("Qiskit Aer is needed to restore noise model.") return obj_val return obj diff --git a/qiskit_ibm_runtime/utils/noise_model.py b/qiskit_ibm_runtime/utils/noise_model.py new file mode 100644 index 000000000..25da6c15e --- /dev/null +++ b/qiskit_ibm_runtime/utils/noise_model.py @@ -0,0 +1,187 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +"""Utility functions for the qiskit aer Noise Model class.""" + +from warnings import catch_warnings, filterwarnings +from typing import Any, Dict, List +import numpy as np + +from qiskit.circuit import Instruction +from qiskit.circuit import QuantumCircuit +from qiskit.circuit import Reset +from qiskit.circuit.library.generalized_gates import PauliGate, UnitaryGate + +from qiskit_aer.noise.errors.quantum_error import QuantumError +from qiskit_aer.noise.errors.readout_error import ReadoutError +from qiskit_aer.noise.noiseerror import NoiseError +from qiskit_aer.noise.noise_model import NoiseModel + + +def from_dict(noise_dict: Dict[str, Any]) -> NoiseModel: + """ + Load NoiseModel from a dictionary. + + Args: + noise_dict (dict): A serialized noise model. + + Returns: + NoiseModel: the noise model. + + Raises: + NoiseError: if dict cannot be converted to NoiseModel. + """ + + def inst_dic_list_to_circuit(dic_list: List[Any]) -> QuantumCircuit: + num_qubits = max(max(dic["qubits"]) for dic in dic_list) + 1 + circ = QuantumCircuit(num_qubits) + for dic in dic_list: + if dic["name"] == "reset": + circ.append(Reset(), qargs=dic["qubits"]) + elif dic["name"] == "kraus": + circ.append( + Instruction( + name="kraus", + num_qubits=len(dic["qubits"]), + num_clbits=0, + params=dic["params"], + ), + qargs=dic["qubits"], + ) + elif dic["name"] == "unitary": + circ.append(UnitaryGate(data=dic["params"][0]), qargs=dic["qubits"]) + elif dic["name"] == "pauli": + circ.append(PauliGate(dic["params"][0]), qargs=dic["qubits"]) + else: + with catch_warnings(): + filterwarnings( + "ignore", + category=DeprecationWarning, + module="qiskit_aer.noise.errors.errorutils", + ) + circ.append( + UnitaryGate(label=dic["name"], data=_standard_gate_unitary(dic["name"])), + qargs=dic["qubits"], + ) + return circ + + # Return noise model + noise_model = NoiseModel() + + # Get error terms + errors = noise_dict.get("errors", []) + + for error in errors: + error_type = error["type"] + + # Add QuantumError + if error_type == "qerror": + circuits = [inst_dic_list_to_circuit(dics) for dics in error["instructions"]] + noise_ops = tuple(zip(circuits, error["probabilities"])) + qerror = QuantumError(noise_ops) + qerror._id = error.get("id", None) or qerror.id + instruction_names = error["operations"] + all_gate_qubits = error.get("gate_qubits", None) + if all_gate_qubits is not None: + for gate_qubits in all_gate_qubits: + # Add local quantum error + noise_model.add_quantum_error( + qerror, instruction_names, gate_qubits, warnings=False + ) + else: + # Add all-qubit quantum error + noise_model.add_all_qubit_quantum_error(qerror, instruction_names, warnings=False) + + # Add ReadoutError + elif error_type == "roerror": + probabilities = error["probabilities"] + all_gate_qubits = error.get("gate_qubits", None) + roerror = ReadoutError(probabilities) + # Add local readout error + if all_gate_qubits is not None: + for gate_qubits in all_gate_qubits: + noise_model.add_readout_error(roerror, gate_qubits, warnings=False) + # Add all-qubit readout error + else: + noise_model.add_all_qubit_readout_error(roerror, warnings=False) + # Invalid error type + else: + raise NoiseError("Invalid error type: {}".format(error_type)) + return noise_model + + +def _standard_gate_unitary(name: str) -> np.ndarray: + # To be removed with from_dict + unitary_matrices = { + ("id", "I"): np.eye(2, dtype=complex), + ("x", "X"): np.array([[0, 1], [1, 0]], dtype=complex), + ("y", "Y"): np.array([[0, -1j], [1j, 0]], dtype=complex), + ("z", "Z"): np.array([[1, 0], [0, -1]], dtype=complex), + ("h", "H"): np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2), + ("s", "S"): np.array([[1, 0], [0, 1j]], dtype=complex), + ("sdg", "Sdg"): np.array([[1, 0], [0, -1j]], dtype=complex), + ("t", "T"): np.array([[1, 0], [0, np.exp(1j * np.pi / 4)]], dtype=complex), + ("tdg", "Tdg"): np.array([[1, 0], [0, np.exp(-1j * np.pi / 4)]], dtype=complex), + ("cx", "CX", "cx_01"): np.array( + [[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]], dtype=complex + ), + ("cx_10",): np.array( + [[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]], dtype=complex + ), + ("cz", "CZ"): np.array( + [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]], dtype=complex + ), + ("swap", "SWAP"): np.array( + [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dtype=complex + ), + ("ccx", "CCX", "ccx_012", "ccx_102"): np.array( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + ], + dtype=complex, + ), + ("ccx_021", "ccx_201"): np.array( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + ], + dtype=complex, + ), + ("ccx_120", "ccx_210"): np.array( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0], + ], + dtype=complex, + ), + } + + return next((value for key, value in unitary_matrices.items() if name in key), None) diff --git a/release-notes/unreleased/1890.bug.rst b/release-notes/unreleased/1890.bug.rst new file mode 100644 index 000000000..4b9089ca1 --- /dev/null +++ b/release-notes/unreleased/1890.bug.rst @@ -0,0 +1,2 @@ +Ported the ``Noise_model.from_dict()`` method from ``qiskit-aer`` because it was removed +in ``0.15.0``. \ No newline at end of file From d01b07dd24ac21bbfba00fa34cdaf7a239b68ba0 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Wed, 28 Aug 2024 08:44:15 -0400 Subject: [PATCH 02/17] Deprecate simulator `noise_model` option (#1892) * Deprecate simulator noise model option * Add release note --- qiskit_ibm_runtime/base_primitive.py | 10 ++++++++++ release-notes/unreleased/1892.deprecation.rst | 2 ++ 2 files changed, 12 insertions(+) create mode 100644 release-notes/unreleased/1892.deprecation.rst diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index 1cf7da14f..fb90cc1ec 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -172,6 +172,16 @@ def _run(self, pubs: Union[list[EstimatorPub], list[SamplerPub]]) -> RuntimeJobV logger.info("Submitting job using options %s", primitive_options) + if not isinstance(self._service, QiskitRuntimeLocalService): + if primitive_options.get("options", {}).get("simulator", {}).get("noise_model"): + issue_deprecation_msg( + msg="The noise_model option is deprecated", + version="0.29.0", + remedy="Use the local testing mode instead.", + period="3 months", + stacklevel=3, + ) + # Batch or Session if self._mode: return self._mode.run( diff --git a/release-notes/unreleased/1892.deprecation.rst b/release-notes/unreleased/1892.deprecation.rst new file mode 100644 index 000000000..7ef717638 --- /dev/null +++ b/release-notes/unreleased/1892.deprecation.rst @@ -0,0 +1,2 @@ +The simulator option ``noise_model`` is now deprecated for jobs running on real devices. +``noise_model`` will still be an acceptable option when using the local testing mode. \ No newline at end of file From 4e6769a61401a26f699634404d4765c9cdf02a90 Mon Sep 17 00:00:00 2001 From: Samuele Ferracin Date: Wed, 28 Aug 2024 13:23:57 -0400 Subject: [PATCH 03/17] docs (#1893) --- qiskit_ibm_runtime/options/resilience_options.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/qiskit_ibm_runtime/options/resilience_options.py b/qiskit_ibm_runtime/options/resilience_options.py index 550b05aca..6f07cc937 100644 --- a/qiskit_ibm_runtime/options/resilience_options.py +++ b/qiskit_ibm_runtime/options/resilience_options.py @@ -12,12 +12,12 @@ """Resilience options.""" -from typing import List, Literal, Union +from typing import Literal, Sequence, Union from dataclasses import asdict from pydantic import model_validator, Field -from ..utils.noise_learner_result import LayerError +from ..utils.noise_learner_result import LayerError, NoiseLearnerResult from .utils import Unset, UnsetType, Dict, primitive_dataclass from .measure_noise_learning_options import MeasureNoiseLearningOptions from .zne_options import ZneOptions @@ -67,9 +67,9 @@ class ResilienceOptionsV2: layer_noise_learning: Layer noise learning options. See :class:`LayerNoiseLearningOptions` for all options. - layer_noise_model: A list of :class:`LayerError` objects. - If set, all the mitigation strategies that require noise data (e.g., PEC and PEA) - skip the noise learning stage, and instead gather the required information from + layer_noise_model: A :class:`NoiseLearnerResult` or a sequence of :class:`LayerError` + objects. If set, all the mitigation strategies that require noise data (e.g., PEC and + PEA) skip the noise learning stage, and instead gather the required information from ``layer_noise_model``. Layers whose information is missing in ``layer_noise_model`` are treated as noiseless and their noise is not mitigated. """ @@ -85,7 +85,7 @@ class ResilienceOptionsV2: layer_noise_learning: Union[LayerNoiseLearningOptions, Dict] = Field( default_factory=LayerNoiseLearningOptions ) - layer_noise_model: Union[UnsetType, List[LayerError]] = Unset + layer_noise_model: Union[UnsetType, NoiseLearnerResult, Sequence[LayerError]] = Unset @model_validator(mode="after") def _validate_options(self) -> "ResilienceOptionsV2": From 63e029142f9138a0e3883e9873b605482b8b7e00 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Wed, 28 Aug 2024 14:28:01 -0400 Subject: [PATCH 04/17] Revert #1791 (#1891) * Revert #1791 * add release note --- qiskit_ibm_runtime/ibm_backend.py | 4 ++-- release-notes/unreleased/1891.bug.rst | 2 ++ test/unit/mock/fake_runtime_client.py | 2 ++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 release-notes/unreleased/1891.bug.rst diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py index 0be9e7c50..b9ef8efd0 100644 --- a/qiskit_ibm_runtime/ibm_backend.py +++ b/qiskit_ibm_runtime/ibm_backend.py @@ -334,9 +334,9 @@ def target(self) -> Target: Returns: Target """ - self._get_properties(datetime=python_datetime.now()) + self._get_properties() self._get_defaults() - self._convert_to_target(refresh=True) + self._convert_to_target() return self._target def target_history(self, datetime: Optional[python_datetime] = None) -> Target: diff --git a/release-notes/unreleased/1891.bug.rst b/release-notes/unreleased/1891.bug.rst new file mode 100644 index 000000000..77b1e34c2 --- /dev/null +++ b/release-notes/unreleased/1891.bug.rst @@ -0,0 +1,2 @@ +Revert a previous change to ``backend.target`` where the target was no longer being +cached. \ No newline at end of file diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py index f4679087b..97616a2fd 100644 --- a/test/unit/mock/fake_runtime_client.py +++ b/test/unit/mock/fake_runtime_client.py @@ -455,6 +455,8 @@ def backend_status(self, backend_name: str) -> Dict[str, Any]: def backend_properties(self, backend_name: str, datetime: Any = None) -> Dict[str, Any]: """Return the properties of a backend.""" + if datetime: + raise NotImplementedError("'datetime' is not supported.") if ret := self._find_backend(backend_name).properties: return ret.copy() return None From 72c16553d4ac082a228b5a81deaacf2b26ece60c Mon Sep 17 00:00:00 2001 From: Samuele Ferracin Date: Thu, 29 Aug 2024 14:47:05 -0400 Subject: [PATCH 05/17] Improving docs for extrapolators (#1894) Co-authored-by: Ian Hincks --- qiskit_ibm_runtime/options/zne_options.py | 37 +++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/qiskit_ibm_runtime/options/zne_options.py b/qiskit_ibm_runtime/options/zne_options.py index acab5f3a6..a67ec7de2 100644 --- a/qiskit_ibm_runtime/options/zne_options.py +++ b/qiskit_ibm_runtime/options/zne_options.py @@ -97,24 +97,37 @@ class ZneOptions: your circuits is amplified by probabilistically injecting single-qubit noise proportional to the corresponding learned noise model. - noise_factors: Noise factors to use for noise amplification. Default: (1, 1.5, 2) for PEA, - and (1, 3, 5) otherwise. + noise_factors: Noise factors to use for noise amplification. Default: ``(1, 1.5, 2)`` for + PEA, and ``(1, 3, 5)`` otherwise. extrapolated_noise_factors: Noise factors to evaluate the fit extrapolation models at. If unset, this will default to ``[0, *noise_factors]``. This option does not affect execution or model fitting in any way, it only determines the - points at which the ``extrapolator``s are evaluated to be returned in the data fields - called ``evs_extrapolated`` and ``stds_extrapolated``. + points at which the ``extrapolator``\\s are evaluated to be returned in the data + fields called ``evs_extrapolated`` and ``stds_extrapolated``. extrapolator: Extrapolator(s) to try (in order) for extrapolating to zero noise. - One or more of: - - * "linear" - * "exponential" - * "double_exponential" - * "polynomial_degree_(1 <= k <= 7)" - - Default: ("exponential", "linear"). + The available options are: + + * ``"exponential"``, which fits the data using an exponential decaying function defined + as :math:`f(x; A, \tau) = A e^{-x/\tau}`, where :math:`A = f(0; A, \tau)` is the + value at zero noise (:math:`x=0`\\) and :math:`\tau>0` is a positive rate. + * ``"double_exponential"``, which uses a sum of two exponential as in Ref. 1. + * ``"polynomial_degree_(1 <= k <= 7)"``, which uses a polynomial function defined as + :math:`f(x; c_0, c_1, \\ldots, c_k) = \\sum_{i=0, k} c_i x^i`. + * ``"linear"``, which is equivalent to ``"polynomial_degree_1"``. + + If more than one extrapolator is specified, the ``evs`` and ``stds`` reported in the + result's data refer to the first one, while the extrapolated values + (``evs_extrapolated`` and ``stds_extrapolated``) are sorted according to the order of + the extrapolators provided. + + Default: ``("exponential", "linear")``. + + References: + 1. Z. Cai, *Multi-exponential error extrapolation and combining error mitigation techniques + for NISQ applications*, + `npj Quantum Inf 7, 80 (2021) `_ """ amplifier: Union[ From 8f60ac106da5d000c130255738c541b1014b21fd Mon Sep 17 00:00:00 2001 From: mberna Date: Fri, 30 Aug 2024 10:43:49 -0400 Subject: [PATCH 06/17] Retrieve image info when decoding V2 jobs (#1900) --- qiskit_ibm_runtime/qiskit_runtime_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index 0f25ec589..ad40fd1f7 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -1170,6 +1170,7 @@ def _decode_job(self, raw_data: Dict) -> Union[RuntimeJob, RuntimeJobV2]: job_id=raw_data["id"], program_id=raw_data.get("program", {}).get("id", ""), creation_date=raw_data.get("created", None), + image=raw_data.get("runtime"), session_id=raw_data.get("session_id"), tags=raw_data.get("tags"), ) From 36ffab7158fb893adea702999d6467c9f24b8f20 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Tue, 3 Sep 2024 16:14:02 +0300 Subject: [PATCH 07/17] Execution span classes (#1833) * first draft of execution span classes * lint * a small change * updated proposal * update ExecutionSpanCollection definition * updates from PR comments * updates from PR comments * updates from PR comments * fixed SliceType definition incompatible with python 3.8 * changed SliceType * more adjustment to old python versions * more 3.8 * lint * Update qiskit_ibm_runtime/execution_span.py Co-authored-by: Ian Hincks * replacing vartypes with quotes * 3.8 and 3.9 * black * lint * wrote a encoder-decoder exec-span test (currently failing) * serilaization and deserialization of exec span * mypy * mypy * mypy * mypy * renamed ExecutionSpanCollection because it was too long * added useful functions related to exec spans * a small fix * lint * mypy * renamed method * shortened a few lines * ExecutionSpanSet.filter_by_pub * lint * added ExecutionSpanSet.duration * test execution span * release notes * lint * Update qiskit_ibm_runtime/execution_span.py Co-authored-by: Ian Hincks * Update qiskit_ibm_runtime/execution_span.py Co-authored-by: Ian Hincks * Update qiskit_ibm_runtime/execution_span.py Co-authored-by: Ian Hincks * Update qiskit_ibm_runtime/execution_span.py Co-authored-by: Ian Hincks * Update qiskit_ibm_runtime/execution_span.py Co-authored-by: Ian Hincks * changed slicetype * mypy * receive an iterable instead of a sequence * removed SliceType * wrote repr * continuation of replacement of pairs of integers by slices * black * Update qiskit_ibm_runtime/execution_span.py Co-authored-by: Ian Hincks * Update qiskit_ibm_runtime/utils/json.py Co-authored-by: Ian Hincks * Update qiskit_ibm_runtime/utils/validations.py Co-authored-by: Ian Hincks * removed some execution span methods * Update qiskit_ibm_runtime/execution_span.py Co-authored-by: Ian Hincks * Update qiskit_ibm_runtime/execution_span.py Co-authored-by: Ian Hincks * fix * Update qiskit_ibm_runtime/execution_span.py Co-authored-by: Ian Hincks * black * update exspan validation * mypy * improving validation accuracy * a slight change * fixed validation for the case of no measurements * fixed a bug in the validate function * black * removed debug print * add abstraction layer to ExecutionSpan * add sorting stuff * mypy * tuple type hint in python 3.8 * adjusted validation to new api --------- Co-authored-by: ptristan Co-authored-by: Ian Hincks Co-authored-by: Ian Hincks --- qiskit_ibm_runtime/execution_span.py | 283 ++++++++++++++++++++++++ qiskit_ibm_runtime/utils/json.py | 23 ++ qiskit_ibm_runtime/utils/validations.py | 50 +++++ release-notes/unreleased/1833.feat.rst | 1 + test/unit/test_data_serialization.py | 22 +- test/unit/test_execution_span.py | 197 +++++++++++++++++ 6 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 qiskit_ibm_runtime/execution_span.py create mode 100644 release-notes/unreleased/1833.feat.rst create mode 100644 test/unit/test_execution_span.py diff --git a/qiskit_ibm_runtime/execution_span.py b/qiskit_ibm_runtime/execution_span.py new file mode 100644 index 000000000..1598f5f32 --- /dev/null +++ b/qiskit_ibm_runtime/execution_span.py @@ -0,0 +1,283 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +"""Execution span classes.""" + +from __future__ import annotations + +import abc +from datetime import datetime +import math +from typing import overload, Iterable, Iterator, Tuple + +import numpy as np +import numpy.typing as npt + + +# Python 3.8 does not recognize tuple[ bool: + pass + + def __lt__(self, other: ExecutionSpan) -> bool: + return (self.start, self.stop) < (other.start, other.stop) + + def __repr__(self) -> str: + attrs = [ + f"start='{self.start:%Y-%m-%d %H:%M:%S}'", + f"stop='{self.stop:%Y-%m-%d %H:%M:%S}'", + f"size={self.size}", + ] + return f"{type(self).__name__}(<{', '.join(attrs)}>)" + + @property + def duration(self) -> float: + """The duration of this span, in seconds.""" + return (self.stop - self.start).total_seconds() + + @property + @abc.abstractmethod + def pub_idxs(self) -> list[int]: + """Which pubs, by index, have dependence on this execution span.""" + + @property + def start(self) -> datetime: + """The start time of the span, in UTC.""" + return self._start + + @property + def stop(self) -> datetime: + """The stop time of the span, in UTC.""" + return self._stop + + @property + def size(self) -> int: + """The total number of results with dependence on this execution span, across all pubs. + + This attribute is equivalent to the sum of the elements of all present :meth:`mask`\\s. + For sampler results, it represents the total number of shots with dependence on this + execution span. + + Combine this attribute with :meth:`filter_by_pub` to find the size of some particular pub: + + .. code:: python + + span.filter_by_pub(2).size + + """ + return sum(self.mask(pub_idx).sum() for pub_idx in self.pub_idxs) + + @abc.abstractmethod + def mask(self, pub_idx: int) -> npt.NDArray[np.bool_]: + """Return an array-valued mask specifying which parts of a pub result depend on this span. + + Args: + pub_idx: The index of the pub to return a mask for. + + Returns: + An array with the same shape as the pub data. + """ + + def contains_pub(self, pub_idx: int | Iterable[int]) -> bool: + """Return whether the pub with the given index has data with dependence on this span. + + Args: + pub_idx: One or more pub indices from the original primitive call. + + Returns: + Whether there is dependence on this span. + """ + pub_idx = {pub_idx} if isinstance(pub_idx, int) else set(pub_idx) + return not pub_idx.isdisjoint(self.pub_idxs) + + @abc.abstractmethod + def filter_by_pub(self, pub_idx: int | Iterable[int]) -> "ExecutionSpan": + """Return a new span whose slices are filtered to the provided pub indices. + + For example, if this span contains slice information for pubs with indices 1, 3, 4 and + ``[1, 4]`` is provided, then the span returned by this method will contain slice information + for only those two indices, but be identical otherwise. + + Args: + pub_idx: One or more pub indices from the original primitive call. + + Returns: + A new filtered span. + """ + + +class SliceSpan(ExecutionSpan): + """An :class:`~.ExecutionSpan` for data stored in a sliceable format. + + This type of execution span references pub result data by assuming that it is a sliceable + portion of the (row major) flattened data. Therefore, for each pub dependent on this span, the + constructor accepts a single :class:`slice` object, along with the corresponding shape of the + data to be sliced. + + Args: + start: The start time of the span, in UTC. + stop: The stop time of the span, in UTC. + data_slices: A map from pub indices to pairs ``(shape_tuple, slice)``. + """ + + def __init__( + self, start: datetime, stop: datetime, data_slices: dict[int, tuple[ShapeType, slice]] + ): + super().__init__(start, stop) + self._data_slices = data_slices + + def __eq__(self, other: object) -> bool: + return isinstance(other, SliceSpan) and ( + self.start == other.start + and self.stop == other.stop + and self._data_slices == other._data_slices + ) + + @property + def pub_idxs(self) -> list[int]: + return sorted(self._data_slices) + + @property + def size(self) -> int: + size = 0 + for shape, sl in self._data_slices.values(): + size += len(range(math.prod(shape))[sl]) + return size + + def mask(self, pub_idx: int) -> npt.NDArray[np.bool_]: + shape, sl = self._data_slices[pub_idx] + mask = np.zeros(shape, dtype=np.bool_) + mask.ravel()[sl] = True + return mask + + def filter_by_pub(self, pub_idx: int | Iterable[int]) -> "SliceSpan": + pub_idx = {pub_idx} if isinstance(pub_idx, int) else set(pub_idx) + slices = {idx: val for idx, val in self._data_slices.items() if idx in pub_idx} + return SliceSpan(self.start, self.stop, slices) + + +class ExecutionSpans: + """A collection of timings for pub results. + + This class is a list-like containing :class:`~.ExecutionSpan`\\s, where each execution span + represents a time window of data collection, and contains a reference to exactly which of the + data were collected during the window. + + .. code::python + + spans = sampler_job.result().metadata["execution"]["execution_spans"] + + for span in spans: + print(span) + + It is possible for distinct time windows to overlap. This is not because a QPU was performing + multiple executions at once, but is instead an artifact of certain classical processing + that may happen concurrently with quantum execution. The guarantee being made is that the + referenced data definitely occurred in the reported execution span, but not necessarily that + the limits of the time window are as tight as possible. + """ + + def __init__(self, spans: Iterable[ExecutionSpan]): + self._spans = list(spans) + + def __len__(self) -> int: + return len(self._spans) + + @overload + def __getitem__(self, idxs: int) -> ExecutionSpan: ... + + @overload + def __getitem__(self, idxs: slice | list[int]) -> "ExecutionSpans": ... + + def __getitem__(self, idxs: int | slice | list[int]) -> ExecutionSpan | "ExecutionSpans": + if isinstance(idxs, int): + return self._spans[idxs] + if isinstance(idxs, slice): + return ExecutionSpans(self._spans[idxs]) + return ExecutionSpans(self._spans[idx] for idx in idxs) + + def __iter__(self) -> Iterator[ExecutionSpan]: + return iter(self._spans) + + def __repr__(self) -> str: + return f"ExecutionSpans({repr(self._spans)})" + + def __eq__(self, other: object) -> bool: + return isinstance(other, ExecutionSpans) and self._spans == other._spans + + @property + def pub_idxs(self) -> list[int]: + """Which pubs, by index, have dependence on one or more execution spans present.""" + return sorted({idx for span in self for idx in span.pub_idxs}) + + @property + def start(self) -> datetime: + """The start time of the entire collection, in UTC.""" + return min(span.start for span in self) + + @property + def stop(self) -> datetime: + """The stop time of the entire collection, in UTC.""" + return max(span.stop for span in self) + + @property + def duration(self) -> float: + """The total duration of this collection, in seconds.""" + return (self.stop - self.start).total_seconds() + + def filter_by_pub(self, pub_idx: int | Iterable[int]) -> "ExecutionSpans": + """Return a new set of spans where each one has been filtered to the specified pubs. + + See also :meth:~.ExecutionSpan.filter_by_pub`. + + Args: + pub_idx: One or more pub indices to filter. + """ + return ExecutionSpans(span.filter_by_pub(pub_idx) for span in self) + + def sort(self, inplace: bool = True) -> "ExecutionSpans": + """Return the same execution spans, sorted. + + Sorting is done by the :attr:`~.ExecutionSpan.start` timestamp of each execution span. + + Args: + inplace: Whether to sort this instance in place, or return a copy. + + Returns: + This instance if ``inplace``, a new instance otherwise, sorted. + """ + obj = self if inplace else ExecutionSpans(self) + obj._spans.sort() + return obj diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index b8881c160..7274c994d 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -71,6 +71,7 @@ PrimitiveResult, ) from qiskit_ibm_runtime.options.zne_options import ExtrapolatorType +from qiskit_ibm_runtime.execution_span import SliceSpan, ExecutionSpans from qiskit_ibm_runtime.utils.noise_model import from_dict _TERRA_VERSION = tuple( @@ -318,6 +319,19 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ if isinstance(obj, PrimitiveResult): out_val = {"pub_results": obj._pub_results, "metadata": obj.metadata} return {"__type__": "PrimitiveResult", "__value__": out_val} + if isinstance(obj, SliceSpan): + out_val = { + "start": obj.start, + "stop": obj.stop, + "data_slices": { + idx: (shape, data_slice.start, data_slice.stop) + for idx, (shape, data_slice) in obj._data_slices.items() + }, + } + return {"__type__": "ExecutionSpan", "__value__": out_val} + if isinstance(obj, ExecutionSpans): + out_val = {"spans": list(obj)} + return {"__type__": "ExecutionSpanCollection", "__value__": out_val} if HAS_AER and isinstance(obj, qiskit_aer.noise.NoiseModel): return {"__type__": "NoiseModel", "__value__": obj.to_dict()} if hasattr(obj, "settings"): @@ -426,6 +440,15 @@ def object_hook(self, obj: Any) -> Any: return PubResult(**obj_val) if obj_type == "PrimitiveResult": return PrimitiveResult(**obj_val) + if obj_type == "ExecutionSpan": + new_slices = { + int(idx): (tuple(shape), slice(*sl_args)) + for idx, (shape, *sl_args) in obj_val["data_slices"].items() + } + obj_val["data_slices"] = new_slices + return SliceSpan(**obj_val) + if obj_type == "ExecutionSpanCollection": + return ExecutionSpans(**obj_val) if obj_type == "to_json": return obj_val if obj_type == "NoiseModel": diff --git a/qiskit_ibm_runtime/utils/validations.py b/qiskit_ibm_runtime/utils/validations.py index 80821cc1c..0216845fb 100644 --- a/qiskit_ibm_runtime/utils/validations.py +++ b/qiskit_ibm_runtime/utils/validations.py @@ -14,12 +14,16 @@ from typing import List, Sequence, Optional, Any import warnings import keyword +from math import prod + from qiskit import QuantumCircuit from qiskit.transpiler import Target +from qiskit.primitives.containers import PrimitiveResult from qiskit.primitives.containers.sampler_pub import SamplerPub from qiskit.primitives.containers.estimator_pub import EstimatorPub from qiskit_ibm_runtime.utils.utils import is_isa_circuit, are_circuits_dynamic from qiskit_ibm_runtime.exceptions import IBMInputValueError +from qiskit_ibm_runtime.execution_span import ExecutionSpans def validate_classical_registers(pubs: List[SamplerPub]) -> None: @@ -126,3 +130,49 @@ def validate_job_tags(job_tags: Optional[List[str]]) -> None: not isinstance(job_tags, list) or not all(isinstance(tag, str) for tag in job_tags) ): raise IBMInputValueError("job_tags needs to be a list of strings.") + + +def validate_exec_spans_in_result(result: PrimitiveResult) -> bool: + """Validate execution span section in result metadata. + + Args: + result: A primitive result to be validated + + Returns True if validation succeeds + """ + + if ( + "execution" not in result.metadata + or not isinstance(result.metadata["execution"], dict) + or "execution_spans" not in result.metadata["execution"] + or not isinstance(result.metadata["execution"]["execution_spans"], ExecutionSpans) + ): + return False + + slice_ends = [0] * len(result) + shapes = [(0,)] * len(result) + for exspan in result.metadata["execution"]["execution_spans"]: + # temporarily disable mypy for the next line, until we fix it + for task_id, task_data in exspan._data_slices.items(): # type: ignore + task_shape, task_slice = task_data + if task_slice.start != slice_ends[task_id]: + return False + slice_ends[task_id] = task_slice.stop + shapes[task_id] = task_shape + + for pub_length, tshape, res in zip(slice_ends, shapes, result): + res_vals = list(res.data.values()) + if len(res_vals) > 0: + shots = res_vals[0].num_shots + elif "num_randomization" in res.metadata: + shots = res.metadata["num_randomizations"] * res.metadata["shots_per_randomization"] + else: + shots = 0 + expected_length = prod(res.data.shape) * shots + if pub_length != expected_length: + return False + + if tshape != res.data.shape + (shots,): + return False + + return True diff --git a/release-notes/unreleased/1833.feat.rst b/release-notes/unreleased/1833.feat.rst new file mode 100644 index 000000000..1414fbd73 --- /dev/null +++ b/release-notes/unreleased/1833.feat.rst @@ -0,0 +1 @@ +We added new classes, :class:`.ExecutionSpan` and :class:`.ExecutionSpanSet`. These classes are used in the primitive result metadata, to convey information about start and stop times of batch jobs. diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index d9d9ac345..2b0041772 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -42,6 +42,7 @@ from qiskit_aer.noise import NoiseModel from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder from qiskit_ibm_runtime.fake_provider import FakeNairobi +from qiskit_ibm_runtime.execution_span import SliceSpan, ExecutionSpans from .mock.fake_runtime_client import CustomResultRuntimeJob from .mock.fake_runtime_service import FakeRuntimeService @@ -329,6 +330,7 @@ def assert_primitive_results_equal(self, primitive_result1, primitive_result2): self.assertEqual(len(primitive_result1), len(primitive_result2)) for pub_result1, pub_result2 in zip(primitive_result1, primitive_result2): self.assert_pub_results_equal(pub_result1, pub_result2) + self.assertEqual(primitive_result1.metadata, primitive_result2.metadata) # Data generation methods @@ -411,7 +413,25 @@ def make_test_primitive_results(self): PubResult(DataBin(alpha=alpha, beta=beta, shape=(10, 20))), PubResult(DataBin()), ] - result = PrimitiveResult(pub_results, {"1": 2}) + + metadata = { + "execution": { + "execution_spans": ExecutionSpans( + [ + SliceSpan( + datetime(2022, 1, 1), + datetime(2023, 1, 1), + {1: ((100,), slice(4, 9)), 0: ((2, 5), slice(5, 7))}, + ), + SliceSpan( + datetime(2024, 8, 20), datetime(2024, 8, 21), {0: ((14,), slice(2, 3))} + ), + ] + ) + } + } + + result = PrimitiveResult(pub_results, metadata) primitive_results.append(result) return primitive_results diff --git a/test/unit/test_execution_span.py b/test/unit/test_execution_span.py new file mode 100644 index 000000000..52e773254 --- /dev/null +++ b/test/unit/test_execution_span.py @@ -0,0 +1,197 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +"""Tests SliceSpan and ExecutionSpans classes.""" + + +from datetime import datetime, timedelta +import ddt + +import numpy as np +import numpy.testing as npt +from qiskit_ibm_runtime.execution_span import SliceSpan, ExecutionSpans + +from ..ibm_test_case import IBMTestCase + + +@ddt.ddt +class TestSliceSpan(IBMTestCase): + """Class for testing SliceSpan.""" + + def setUp(self) -> None: + super().setUp() + self.start1 = datetime(2023, 8, 22, 18, 45, 3) + self.stop1 = datetime(2023, 8, 22, 18, 45, 10) + self.slices1 = {1: ((100,), slice(4, 9)), 0: ((5, 2), slice(5, 7))} + self.span1 = SliceSpan(self.start1, self.stop1, self.slices1) + + self.start2 = datetime(2023, 8, 22, 18, 45, 9) + self.stop2 = datetime(2023, 8, 22, 18, 45, 11, 500000) + self.slices2 = {0: ((100,), slice(2, 3)), 2: ((32, 3), slice(6, 8))} + self.span2 = SliceSpan(self.start2, self.stop2, self.slices2) + + def test_limits(self): + """Test the start and stop properties""" + self.assertEqual(self.span1.start, self.start1) + self.assertEqual(self.span1.stop, self.stop1) + self.assertEqual(self.span2.start, self.start2) + self.assertEqual(self.span2.stop, self.stop2) + + def test_equality(self): + """Test the equality method.""" + self.assertEqual(self.span1, self.span1) + self.assertEqual(self.span1, SliceSpan(self.start1, self.stop1, self.slices1)) + self.assertNotEqual(self.span1, self.span2) + self.assertNotEqual(self.span1, "aoeu") + + def test_comparison(self): + """Test the comparison method.""" + self.assertLess(self.span1, self.span2) + + dt = timedelta(seconds=1) + span1_plus = SliceSpan(self.start1, self.stop1 + dt, self.slices1) + self.assertLess(self.span1, span1_plus) + + span1_minus = SliceSpan(self.start1, self.stop1 - dt, self.slices1) + self.assertGreater(self.span1, span1_minus) + + def test_duration(self): + """Test the duration property""" + self.assertEqual(self.span1.duration, 7) + self.assertEqual(self.span2.duration, 2.5) + + def test_repr(self): + """Test the repr method""" + expect = "start='2023-08-22 18:45:03', stop='2023-08-22 18:45:10', size=7" + self.assertEqual(repr(self.span1), f"SliceSpan(<{expect}>)") + + def test_size(self): + """Test the size property""" + self.assertEqual(self.span1.size, 5 + 2) + self.assertEqual(self.span2.size, 1 + 2) + + def test_pub_idxs(self): + """Test the pub_idxs property""" + self.assertEqual(self.span1.pub_idxs, [0, 1]) + self.assertEqual(self.span2.pub_idxs, [0, 2]) + + def test_mask(self): + """Test the mask() method""" + mask1 = np.zeros((100,), dtype=bool) + mask1[4:9] = True + npt.assert_array_equal(self.span1.mask(1), mask1) + + mask2 = [[0, 0], [0, 0], [0, 1], [1, 0], [0, 0]] + npt.assert_array_equal(self.span1.mask(0), np.array(mask2, dtype=bool)) + + @ddt.data( + (0, True, True), + ([0, 1], True, True), + ([0, 1, 2], True, True), + ([1, 2], True, True), + ([1], True, False), + (2, False, True), + ([0, 2], True, True), + ) + @ddt.unpack + def test_contains_pub(self, idx, span1_expected_res, span2_expected_res): + """The the contains_pub method""" + self.assertEqual(self.span1.contains_pub(idx), span1_expected_res) + self.assertEqual(self.span2.contains_pub(idx), span2_expected_res) + + def test_filter_by_pub(self): + """The the filter_by_pub method""" + self.assertEqual(self.span1.filter_by_pub([]), SliceSpan(self.start1, self.stop1, {})) + self.assertEqual(self.span2.filter_by_pub([]), SliceSpan(self.start2, self.stop2, {})) + + self.assertEqual( + self.span1.filter_by_pub([2, 0]), + SliceSpan(self.start1, self.stop1, {0: self.slices1[0]}), + ) + self.assertEqual(self.span2.filter_by_pub([2, 0]), self.span2) + + self.assertEqual( + self.span1.filter_by_pub(1), + SliceSpan(self.start1, self.stop1, {1: self.slices1[1]}), + ) + self.assertEqual(self.span2.filter_by_pub(1), SliceSpan(self.start2, self.stop2, {})) + + +@ddt.ddt +class TestExecutionSpans(IBMTestCase): + """Class for testing ExecutionSpans.""" + + def setUp(self) -> None: + super().setUp() + self.start1 = datetime(2023, 8, 22, 18, 45, 3) + self.stop1 = datetime(2023, 8, 22, 18, 45, 10) + self.slices1 = {1: ((100,), slice(4, 9)), 0: ((2, 5), slice(5, 7))} + self.span1 = SliceSpan(self.start1, self.stop1, self.slices1) + + self.start2 = datetime(2023, 8, 22, 18, 45, 9) + self.stop2 = datetime(2023, 8, 22, 18, 45, 11, 500000) + self.slices2 = {0: ((100,), slice(2, 3)), 2: ((32, 3), slice(6, 8))} + self.span2 = SliceSpan(self.start2, self.stop2, self.slices2) + + self.spans = ExecutionSpans([self.span1, self.span2]) + + def test_duration(self): + """Test the duration property""" + self.assertEqual(self.spans.duration, 8.5) + + def test_filter_by_pub(self): + """The the filter_by_pub method""" + self.assertEqual( + self.spans.filter_by_pub([]), + ExecutionSpans( + [ + SliceSpan(self.start1, self.stop1, {}), + SliceSpan(self.start2, self.stop2, {}), + ] + ), + ) + + self.assertEqual( + self.spans.filter_by_pub([2, 0]), + ExecutionSpans([SliceSpan(self.start1, self.stop1, {0: self.slices1[0]}), self.span2]), + ) + + self.assertEqual( + self.spans.filter_by_pub(1), + ExecutionSpans( + [ + SliceSpan(self.start1, self.stop1, {1: self.slices1[1]}), + SliceSpan(self.start2, self.stop2, {}), + ] + ), + ) + + def test_sequence_methods(self): + """Test __len__ and __get_item__""" + self.assertEqual(len(self.spans), 2) + self.assertEqual(self.spans[0], self.span1) + self.assertEqual(self.spans[1], self.span2) + self.assertEqual(self.spans[1, 0], ExecutionSpans([self.span2, self.span1])) + + def test_sort(self): + """Test the sort method.""" + spans = ExecutionSpans([self.span2, self.span1]) + self.assertLess(spans[1], spans[0]) + inplace_sort = spans.sort() + self.assertIs(inplace_sort, spans) + self.assertLess(spans[0], spans[1]) + + spans = ExecutionSpans([self.span2, self.span1]) + new_sort = spans.sort(inplace=False) + self.assertIsNot(inplace_sort, spans) + self.assertLess(spans[1], spans[0]) + self.assertLess(new_sort[0], new_sort[1]) From 773372475230ddb0e91e1ab99e31b521b08e1784 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Tue, 3 Sep 2024 09:29:08 -0400 Subject: [PATCH 08/17] Add "private" environment option (#1888) * Add "private" environment option * Add test and release note * move release note * fix unit test * Update unit tests to return RuntimeJobV2 * fix default param --------- Co-authored-by: ptristan3 <44805021+ptristan3@users.noreply.github.com> --- qiskit_ibm_runtime/api/clients/runtime.py | 3 ++ qiskit_ibm_runtime/api/rest/runtime.py | 4 ++ .../options/environment_options.py | 3 ++ qiskit_ibm_runtime/qiskit_runtime_service.py | 40 ++++++------------- qiskit_ibm_runtime/runtime_options.py | 4 ++ release-notes/unreleased/1888.feat.rst | 3 ++ test/integration/test_ibm_job_attributes.py | 15 +++++++ test/unit/mock/fake_runtime_client.py | 4 ++ test/unit/test_jobs.py | 23 +++++------ 9 files changed, 59 insertions(+), 40 deletions(-) create mode 100644 release-notes/unreleased/1888.feat.rst diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index 979ea9a80..36f617ea7 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -60,6 +60,7 @@ def program_run( max_execution_time: Optional[int] = None, start_session: Optional[bool] = False, session_time: Optional[int] = None, + private: Optional[bool] = False, channel_strategy: Optional[str] = None, ) -> Dict: """Run the specified program. @@ -76,6 +77,7 @@ def program_run( max_execution_time: Maximum execution time in seconds. start_session: Set to True to explicitly start a runtime session. Defaults to False. session_time: Length of session in seconds. + private: Marks job as private. channel_strategy: Error mitigation strategy. Returns: @@ -96,6 +98,7 @@ def program_run( max_execution_time=max_execution_time, start_session=start_session, session_time=session_time, + private=private, channel_strategy=channel_strategy, **hgp_dict, ) diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index 70f1af1f1..c3fadd33a 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -76,6 +76,7 @@ def program_run( max_execution_time: Optional[int] = None, start_session: Optional[bool] = False, session_time: Optional[int] = None, + private: Optional[bool] = False, channel_strategy: Optional[str] = None, ) -> Dict: """Execute the program. @@ -94,6 +95,7 @@ def program_run( max_execution_time: Maximum execution time in seconds. start_session: Set to True to explicitly start a runtime session. Defaults to False. session_time: Length of session in seconds. + private: Marks job as private. channel_strategy: Error mitigation strategy. Returns: @@ -125,6 +127,8 @@ def program_run( payload["project"] = project if channel_strategy: payload["channel_strategy"] = channel_strategy + if private: + payload["private"] = True data = json.dumps(payload, cls=RuntimeEncoder) return self.session.post(url, data=data, timeout=900).json() diff --git a/qiskit_ibm_runtime/options/environment_options.py b/qiskit_ibm_runtime/options/environment_options.py index 0f211de72..808050e18 100644 --- a/qiskit_ibm_runtime/options/environment_options.py +++ b/qiskit_ibm_runtime/options/environment_options.py @@ -45,8 +45,11 @@ class EnvironmentOptions: job_tags: Tags to be assigned to the job. The tags can subsequently be used as a filter in the :meth:`qiskit_ibm_runtime.qiskit_runtime_service.jobs()` function call. Default: ``None``. + + private: Boolean value for marking jobs as private. """ log_level: LogLevelType = "WARNING" callback: Optional[Callable] = None job_tags: Optional[List] = None + private: Optional[bool] = False diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index ad40fd1f7..8907f70c7 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -892,6 +892,7 @@ def run( max_execution_time=qrt_options.max_execution_time, start_session=start_session, session_time=qrt_options.session_time, + private=qrt_options.private, channel_strategy=( None if self._channel_strategy == "default" else self._channel_strategy ), @@ -910,33 +911,18 @@ def run( if response["backend"] and response["backend"] != qrt_options.get_backend_name(): backend = self.backend(name=response["backend"], instance=hgp_name) - if version == 2: - job = RuntimeJobV2( - backend=backend, - api_client=self._api_client, - client_params=self._client_params, - job_id=response["id"], - program_id=program_id, - user_callback=callback, - result_decoder=result_decoder, - image=qrt_options.image, - service=self, - version=version, - ) - else: - job = RuntimeJob( - backend=backend, - api_client=self._api_client, - client_params=self._client_params, - job_id=response["id"], - program_id=program_id, - user_callback=callback, - result_decoder=result_decoder, - image=qrt_options.image, - service=self, - version=version, - ) - return job + return RuntimeJobV2( + backend=backend, + api_client=self._api_client, + client_params=self._client_params, + job_id=response["id"], + program_id=program_id, + user_callback=callback, + result_decoder=result_decoder, + image=qrt_options.image, + service=self, + version=version, + ) def _run(self, *args: Any, **kwargs: Any) -> Union[RuntimeJob, RuntimeJobV2]: """Private run method""" diff --git a/qiskit_ibm_runtime/runtime_options.py b/qiskit_ibm_runtime/runtime_options.py index 485348427..19c56013a 100644 --- a/qiskit_ibm_runtime/runtime_options.py +++ b/qiskit_ibm_runtime/runtime_options.py @@ -36,6 +36,7 @@ class RuntimeOptions: job_tags: Optional[List[str]] = None max_execution_time: Optional[int] = None session_time: Optional[int] = None + private: Optional[bool] = False def __init__( self, @@ -46,6 +47,7 @@ def __init__( job_tags: Optional[List[str]] = None, max_execution_time: Optional[int] = None, session_time: Optional[int] = None, + private: Optional[bool] = False, ) -> None: """RuntimeOptions constructor. @@ -68,6 +70,7 @@ def __init__( this time limit, it is forcibly cancelled. Simulator jobs continue to use wall clock time. session_time: Length of session in seconds. + private: Boolean of whether or not the job is marked as private. """ self.backend = backend self.image = image @@ -76,6 +79,7 @@ def __init__( self.job_tags = job_tags self.max_execution_time = max_execution_time self.session_time = session_time + self.private = private def validate(self, channel: str) -> None: """Validate options. diff --git a/release-notes/unreleased/1888.feat.rst b/release-notes/unreleased/1888.feat.rst new file mode 100644 index 000000000..ca5549426 --- /dev/null +++ b/release-notes/unreleased/1888.feat.rst @@ -0,0 +1,3 @@ +Added a new ``private`` option under :class:`EnvironmentOptions`. When this option +is set to ``True``, the job will be returned without params and results can only +be retrieved once. \ No newline at end of file diff --git a/test/integration/test_ibm_job_attributes.py b/test/integration/test_ibm_job_attributes.py index 46eb89cb0..716dd11e6 100644 --- a/test/integration/test_ibm_job_attributes.py +++ b/test/integration/test_ibm_job_attributes.py @@ -160,3 +160,18 @@ def test_cost_estimation(self): """Test cost estimation is returned correctly.""" self.assertTrue(self.sim_job.usage_estimation) self.assertIn("quantum_seconds", self.sim_job.usage_estimation) + + def test_private_option(self): + """Test private option.""" + try: + backend = self.service.backend("test_eagle") + except: + raise SkipTest("test_eagle not available in this environment") + + sampler = Sampler(mode=backend) + sampler.options.environment.private = True + bell_circuit = transpile(bell(), backend) + job = sampler.run([bell_circuit]) + self.assertFalse(job.inputs) + self.assertTrue(job.result()) + self.assertFalse(job.result()) # private job results can only be retrieved once diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py index 97616a2fd..3738c6684 100644 --- a/test/unit/mock/fake_runtime_client.py +++ b/test/unit/mock/fake_runtime_client.py @@ -157,6 +157,9 @@ def to_dict(self): }, "program": {"id": self._program_id}, "image": self._image, + "params": { + "version": 2, + }, } def result(self): @@ -310,6 +313,7 @@ def program_run( max_execution_time: Optional[int] = None, start_session: Optional[bool] = None, session_time: Optional[int] = None, + private: Optional[int] = False, # pylint: disable=unused-argument channel_strategy: Optional[str] = None, ) -> Dict[str, Any]: """Run the specified program.""" diff --git a/test/unit/test_jobs.py b/test/unit/test_jobs.py index b95ebd66e..9ae1d9826 100644 --- a/test/unit/test_jobs.py +++ b/test/unit/test_jobs.py @@ -16,9 +16,8 @@ import time from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit.providers.jobstatus import JobStatus -from qiskit_ibm_runtime import RuntimeJob +from qiskit_ibm_runtime import RuntimeJobV2 from qiskit_ibm_runtime.constants import API_TO_JOB_ERROR_MESSAGE from qiskit_ibm_runtime.exceptions import ( RuntimeJobFailureError, @@ -48,11 +47,10 @@ def test_run_program(self, service): params = {"param1": "foo"} job = run_program(service=service, inputs=params) self.assertTrue(job.job_id()) - self.assertIsInstance(job, RuntimeJob) - self.assertIsInstance(job.status(), JobStatus) + self.assertIsInstance(job, RuntimeJobV2) with mock_wait_for_final_state(service, job): job.wait_for_final_state() - self.assertEqual(job.status(), JobStatus.DONE) + self.assertEqual(job.status(), "DONE") self.assertTrue(job.result()) @run_quantum_and_cloud_fake @@ -123,12 +121,11 @@ def test_run_program_with_custom_runtime_image(self, service): image = "name:tag" job = run_program(service=service, inputs=params, image=image) self.assertTrue(job.job_id()) - self.assertIsInstance(job, RuntimeJob) - self.assertIsInstance(job.status(), JobStatus) + self.assertIsInstance(job, RuntimeJobV2) with mock_wait_for_final_state(service, job): job.wait_for_final_state() self.assertTrue(job.result()) - self.assertEqual(job.status(), JobStatus.DONE) + self.assertEqual(job.status(), "DONE") self.assertEqual(job.image, image) @run_quantum_and_cloud_fake @@ -145,7 +142,7 @@ def test_run_program_failed(self, service): with mock_wait_for_final_state(service, job): job.wait_for_final_state() job_result_raw = service._api_client.job_results(job.job_id()) - self.assertEqual(JobStatus.ERROR, job.status()) + self.assertEqual("ERROR", job.status()) self.assertEqual( API_TO_JOB_ERROR_MESSAGE["FAILED"].format(job.job_id(), job_result_raw), job.error_message(), @@ -160,7 +157,7 @@ def test_run_program_failed_ran_too_long(self, service): with mock_wait_for_final_state(service, job): job.wait_for_final_state() job_result_raw = service._api_client.job_results(job.job_id()) - self.assertEqual(JobStatus.ERROR, job.status()) + self.assertEqual("ERROR", job.status()) self.assertEqual( API_TO_JOB_ERROR_MESSAGE["CANCELLED - RAN TOO LONG"].format( job.job_id(), job_result_raw @@ -176,9 +173,9 @@ def test_cancel_job(self, service): job = run_program(service, job_classes=CancelableRuntimeJob) time.sleep(1) job.cancel() - self.assertEqual(job.status(), JobStatus.CANCELLED) + self.assertEqual(job.status(), "CANCELLED") rjob = service.job(job.job_id()) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) + self.assertEqual(rjob.status(), "CANCELLED") with self.assertRaises(RuntimeInvalidStateError) as exc: rjob.result() self.assertIn("Job was cancelled", str(exc.exception)) @@ -212,7 +209,7 @@ def test_wait_for_final_state(self, service): job = run_program(service) with mock_wait_for_final_state(service, job): job.wait_for_final_state() - self.assertEqual(JobStatus.DONE, job.status()) + self.assertEqual("DONE", job.status()) @run_quantum_and_cloud_fake def test_delete_job(self, service): From b81b094b3e2deff7792a848331580565ef173b74 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Tue, 3 Sep 2024 09:37:41 -0400 Subject: [PATCH 09/17] Update backend configuration docstring (#1885) * Update backend configuration docstring * docs build --------- Co-authored-by: Samuele Ferracin Co-authored-by: ptristan3 <44805021+ptristan3@users.noreply.github.com> --- qiskit_ibm_runtime/ibm_backend.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py index b9ef8efd0..f2b66d6e2 100644 --- a/qiskit_ibm_runtime/ibm_backend.py +++ b/qiskit_ibm_runtime/ibm_backend.py @@ -461,6 +461,15 @@ def configuration( `Qiskit/ibm-quantum-schemas/backend_configuration `_. + More details about backend configuration properties can be found here `QasmBackendConfiguration + `_. + + IBM backends may also include the following properties: + * ``supported_features``: a list of strings of supported features like "qasm3" for dynamic + circuits support. + * ``parallel_compilation``: a boolean of whether or not the backend can process multiple + jobs at once. Parts of the classical computation will be parallelized. + Returns: The configuration for the backend. """ From 55321aa75a03336506a5a7355719acaa66cbdb07 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Tue, 3 Sep 2024 11:15:06 -0400 Subject: [PATCH 10/17] Move from_dict import (#1901) --- qiskit_ibm_runtime/utils/json.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index 7274c994d..26fd8583d 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -39,6 +39,7 @@ try: import qiskit_aer + from qiskit_ibm_runtime.utils.noise_model import from_dict HAS_AER = True except ImportError: @@ -70,9 +71,11 @@ SamplerPubResult, PrimitiveResult, ) -from qiskit_ibm_runtime.options.zne_options import ExtrapolatorType + +from qiskit_ibm_runtime.options.zne_options import ( # pylint: disable=ungrouped-imports + ExtrapolatorType, +) from qiskit_ibm_runtime.execution_span import SliceSpan, ExecutionSpans -from qiskit_ibm_runtime.utils.noise_model import from_dict _TERRA_VERSION = tuple( int(x) for x in re.match(r"\d+\.\d+\.\d", _terra_version_string).group(0).split(".")[:3] From 5d196b465605f23b9138fe9819494a8b61231469 Mon Sep 17 00:00:00 2001 From: Samuele Ferracin Date: Tue, 3 Sep 2024 14:57:15 -0400 Subject: [PATCH 11/17] Adding `fallback` option to ZNE extrapolators (#1902) * added fallback option * release notes * tests --- qiskit_ibm_runtime/options/zne_options.py | 4 ++++ release-notes/unreleased/1902.feat.rst | 1 + 2 files changed, 5 insertions(+) create mode 100644 release-notes/unreleased/1902.feat.rst diff --git a/qiskit_ibm_runtime/options/zne_options.py b/qiskit_ibm_runtime/options/zne_options.py index a67ec7de2..3bfbec565 100644 --- a/qiskit_ibm_runtime/options/zne_options.py +++ b/qiskit_ibm_runtime/options/zne_options.py @@ -29,6 +29,7 @@ "polynomial_degree_5", "polynomial_degree_6", "polynomial_degree_7", + "fallback", ] @@ -116,6 +117,8 @@ class ZneOptions: * ``"polynomial_degree_(1 <= k <= 7)"``, which uses a polynomial function defined as :math:`f(x; c_0, c_1, \\ldots, c_k) = \\sum_{i=0, k} c_i x^i`. * ``"linear"``, which is equivalent to ``"polynomial_degree_1"``. + * ``"fallback"``, which simply returns the raw data corresponding to the lowest noise + factor (typically ``1``) without performing any sort of extrapolation. If more than one extrapolator is specified, the ``evs`` and ``stds`` reported in the result's data refer to the first one, while the extrapolated values @@ -167,6 +170,7 @@ def _validate_options(self) -> "ZneOptions": "linear": 2, "exponential": 2, "double_exponential": 4, + "fallback": 1, } for idx in range(1, 8): required_factors[f"polynomial_degree_{idx}"] = idx + 1 diff --git a/release-notes/unreleased/1902.feat.rst b/release-notes/unreleased/1902.feat.rst new file mode 100644 index 000000000..f6f8e3fd7 --- /dev/null +++ b/release-notes/unreleased/1902.feat.rst @@ -0,0 +1 @@ +Added ``fallback`` option to ZNE extrapolators. \ No newline at end of file From 1d219a98e351df239a6f3cd3e55423752fdf43b7 Mon Sep 17 00:00:00 2001 From: Yael Ben-Haim Date: Wed, 4 Sep 2024 18:48:14 +0300 Subject: [PATCH 12/17] Remove a function that validates execution spans in primitive results (#1903) * Remove a function that validates execution spans in primitive results * lint --- qiskit_ibm_runtime/utils/validations.py | 49 ------------------------- 1 file changed, 49 deletions(-) diff --git a/qiskit_ibm_runtime/utils/validations.py b/qiskit_ibm_runtime/utils/validations.py index 0216845fb..c2a27df72 100644 --- a/qiskit_ibm_runtime/utils/validations.py +++ b/qiskit_ibm_runtime/utils/validations.py @@ -14,16 +14,13 @@ from typing import List, Sequence, Optional, Any import warnings import keyword -from math import prod from qiskit import QuantumCircuit from qiskit.transpiler import Target -from qiskit.primitives.containers import PrimitiveResult from qiskit.primitives.containers.sampler_pub import SamplerPub from qiskit.primitives.containers.estimator_pub import EstimatorPub from qiskit_ibm_runtime.utils.utils import is_isa_circuit, are_circuits_dynamic from qiskit_ibm_runtime.exceptions import IBMInputValueError -from qiskit_ibm_runtime.execution_span import ExecutionSpans def validate_classical_registers(pubs: List[SamplerPub]) -> None: @@ -130,49 +127,3 @@ def validate_job_tags(job_tags: Optional[List[str]]) -> None: not isinstance(job_tags, list) or not all(isinstance(tag, str) for tag in job_tags) ): raise IBMInputValueError("job_tags needs to be a list of strings.") - - -def validate_exec_spans_in_result(result: PrimitiveResult) -> bool: - """Validate execution span section in result metadata. - - Args: - result: A primitive result to be validated - - Returns True if validation succeeds - """ - - if ( - "execution" not in result.metadata - or not isinstance(result.metadata["execution"], dict) - or "execution_spans" not in result.metadata["execution"] - or not isinstance(result.metadata["execution"]["execution_spans"], ExecutionSpans) - ): - return False - - slice_ends = [0] * len(result) - shapes = [(0,)] * len(result) - for exspan in result.metadata["execution"]["execution_spans"]: - # temporarily disable mypy for the next line, until we fix it - for task_id, task_data in exspan._data_slices.items(): # type: ignore - task_shape, task_slice = task_data - if task_slice.start != slice_ends[task_id]: - return False - slice_ends[task_id] = task_slice.stop - shapes[task_id] = task_shape - - for pub_length, tshape, res in zip(slice_ends, shapes, result): - res_vals = list(res.data.values()) - if len(res_vals) > 0: - shots = res_vals[0].num_shots - elif "num_randomization" in res.metadata: - shots = res.metadata["num_randomizations"] * res.metadata["shots_per_randomization"] - else: - shots = 0 - expected_length = prod(res.data.shape) * shots - if pub_length != expected_length: - return False - - if tshape != res.data.shape + (shots,): - return False - - return True From 3032b9a744145eaee6a673c733c574deb2e8e38c Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Wed, 4 Sep 2024 14:56:14 -0400 Subject: [PATCH 13/17] Don't create new session in `from_id()` (#1896) * Don't create new session in `from_id()` * Add release note * change new_session to private * add unit test * Use class variable --------- Co-authored-by: ptristan3 <44805021+ptristan3@users.noreply.github.com> --- qiskit_ibm_runtime/batch.py | 4 +++- qiskit_ibm_runtime/session.py | 9 ++++++--- release-notes/unreleased/1896.bug.rst | 2 ++ test/unit/test_session.py | 2 ++ 4 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 release-notes/unreleased/1896.bug.rst diff --git a/qiskit_ibm_runtime/batch.py b/qiskit_ibm_runtime/batch.py index ce9a4d9b4..ee49e774a 100644 --- a/qiskit_ibm_runtime/batch.py +++ b/qiskit_ibm_runtime/batch.py @@ -83,6 +83,8 @@ class Batch(Session): `_" page. """ + _create_new_session = True + def __init__( self, service: Optional[QiskitRuntimeService] = None, @@ -131,7 +133,7 @@ def __init__( def _create_session(self) -> Optional[str]: """Create a session.""" - if isinstance(self._service, QiskitRuntimeService): + if isinstance(self._service, QiskitRuntimeService) and Batch._create_new_session: session = self._service._api_client.create_session( self.backend(), self._instance, self._max_time, self._service.channel, "batch" ) diff --git a/qiskit_ibm_runtime/session.py b/qiskit_ibm_runtime/session.py index 46efe039d..1f360b3f7 100644 --- a/qiskit_ibm_runtime/session.py +++ b/qiskit_ibm_runtime/session.py @@ -82,6 +82,8 @@ class Session: print(f"Counts: {pub_result.data.cr.get_counts()}") """ + _create_new_session = True + def __init__( self, service: Optional[QiskitRuntimeService] = None, @@ -104,7 +106,6 @@ def __init__( This value must be less than the `system imposed maximum `_. - Raises: ValueError: If an input value is invalid. """ @@ -165,7 +166,7 @@ def __init__( def _create_session(self) -> Optional[str]: """Create a session.""" - if isinstance(self._service, QiskitRuntimeService): + if isinstance(self._service, QiskitRuntimeService) and Session._create_new_session: session = self._service._api_client.create_session( self.backend(), self._instance, self._max_time, self._service.channel, "dedicated" ) @@ -367,7 +368,7 @@ def from_id(cls, session_id: str, service: QiskitRuntimeService) -> "Session": """ response = service._api_client.session_details(session_id) - backend = response.get("backend_name") + backend = service.backend(response.get("backend_name")) mode = response.get("mode") state = response.get("state") class_name = "dedicated" if cls.__name__.lower() == "session" else cls.__name__.lower() @@ -376,7 +377,9 @@ def from_id(cls, session_id: str, service: QiskitRuntimeService) -> "Session": f"Input ID {session_id} has execution mode {mode} instead of {class_name}." ) + cls._create_new_session = False session = cls(service, backend) + cls._create_new_session = True if state == "closed": session._active = False session._session_id = session_id diff --git a/release-notes/unreleased/1896.bug.rst b/release-notes/unreleased/1896.bug.rst new file mode 100644 index 000000000..ff6916e48 --- /dev/null +++ b/release-notes/unreleased/1896.bug.rst @@ -0,0 +1,2 @@ +Fixed an issue where ``Session.from_id()`` would create +a new empty session. \ No newline at end of file diff --git a/test/unit/test_session.py b/test/unit/test_session.py index 0b956187a..a19f0f3bb 100644 --- a/test/unit/test_session.py +++ b/test/unit/test_session.py @@ -151,6 +151,8 @@ def test_session_from_id(self): session_id = "123" session = Session.from_id(session_id=session_id, service=service) session.run(program_id="foo", inputs={}) + session._create_session = MagicMock() + self.assertTrue(session._create_session.assert_not_called) self.assertEqual(session.session_id, session_id) def test_correct_execution_mode(self): From b2319679d104a91c7524bd65089cb9ee227e3f40 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Wed, 4 Sep 2024 15:11:11 -0400 Subject: [PATCH 14/17] Prepare release 0.29.0 (#1904) --- release-notes/0.29.0.rst | 27 +++++++++++++++++++ release-notes/unreleased/1833.feat.rst | 1 - release-notes/unreleased/1888.feat.rst | 3 --- release-notes/unreleased/1890.bug.rst | 2 -- release-notes/unreleased/1891.bug.rst | 2 -- release-notes/unreleased/1892.deprecation.rst | 2 -- release-notes/unreleased/1896.bug.rst | 2 -- release-notes/unreleased/1902.feat.rst | 1 - 8 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 release-notes/0.29.0.rst delete mode 100644 release-notes/unreleased/1833.feat.rst delete mode 100644 release-notes/unreleased/1888.feat.rst delete mode 100644 release-notes/unreleased/1890.bug.rst delete mode 100644 release-notes/unreleased/1891.bug.rst delete mode 100644 release-notes/unreleased/1892.deprecation.rst delete mode 100644 release-notes/unreleased/1896.bug.rst delete mode 100644 release-notes/unreleased/1902.feat.rst diff --git a/release-notes/0.29.0.rst b/release-notes/0.29.0.rst new file mode 100644 index 000000000..06e829d41 --- /dev/null +++ b/release-notes/0.29.0.rst @@ -0,0 +1,27 @@ +0.29.0 (2024-09-04) +=================== + +Deprecation Notes +----------------- + +- The simulator option ``noise_model`` is now deprecated for jobs running on real devices. + ``noise_model`` will still be an acceptable option when using the local testing mode. (`1892 `__) + + +New Features +------------ + +- We added new classes, :class:`.ExecutionSpan` and :class:`.ExecutionSpanSet`. These classes are used in the primitive result metadata, to convey information about start and stop times of batch jobs. (`1833 `__) +- Added a new ``private`` option under :class:`EnvironmentOptions`. (`1888 `__) +- Added ``fallback`` option to ZNE extrapolators. (`1902 `__) + + +Bug Fixes +--------- + +- Ported the ``Noise_model.from_dict()`` method from ``qiskit-aer`` because it was removed + in ``0.15.0``. (`1890 `__) +- Revert a previous change to ``backend.target`` where the target was no longer being + cached. (`1891 `__) +- Fixed an issue where ``Session.from_id()`` would create + a new empty session. (`1896 `__) diff --git a/release-notes/unreleased/1833.feat.rst b/release-notes/unreleased/1833.feat.rst deleted file mode 100644 index 1414fbd73..000000000 --- a/release-notes/unreleased/1833.feat.rst +++ /dev/null @@ -1 +0,0 @@ -We added new classes, :class:`.ExecutionSpan` and :class:`.ExecutionSpanSet`. These classes are used in the primitive result metadata, to convey information about start and stop times of batch jobs. diff --git a/release-notes/unreleased/1888.feat.rst b/release-notes/unreleased/1888.feat.rst deleted file mode 100644 index ca5549426..000000000 --- a/release-notes/unreleased/1888.feat.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added a new ``private`` option under :class:`EnvironmentOptions`. When this option -is set to ``True``, the job will be returned without params and results can only -be retrieved once. \ No newline at end of file diff --git a/release-notes/unreleased/1890.bug.rst b/release-notes/unreleased/1890.bug.rst deleted file mode 100644 index 4b9089ca1..000000000 --- a/release-notes/unreleased/1890.bug.rst +++ /dev/null @@ -1,2 +0,0 @@ -Ported the ``Noise_model.from_dict()`` method from ``qiskit-aer`` because it was removed -in ``0.15.0``. \ No newline at end of file diff --git a/release-notes/unreleased/1891.bug.rst b/release-notes/unreleased/1891.bug.rst deleted file mode 100644 index 77b1e34c2..000000000 --- a/release-notes/unreleased/1891.bug.rst +++ /dev/null @@ -1,2 +0,0 @@ -Revert a previous change to ``backend.target`` where the target was no longer being -cached. \ No newline at end of file diff --git a/release-notes/unreleased/1892.deprecation.rst b/release-notes/unreleased/1892.deprecation.rst deleted file mode 100644 index 7ef717638..000000000 --- a/release-notes/unreleased/1892.deprecation.rst +++ /dev/null @@ -1,2 +0,0 @@ -The simulator option ``noise_model`` is now deprecated for jobs running on real devices. -``noise_model`` will still be an acceptable option when using the local testing mode. \ No newline at end of file diff --git a/release-notes/unreleased/1896.bug.rst b/release-notes/unreleased/1896.bug.rst deleted file mode 100644 index ff6916e48..000000000 --- a/release-notes/unreleased/1896.bug.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed an issue where ``Session.from_id()`` would create -a new empty session. \ No newline at end of file diff --git a/release-notes/unreleased/1902.feat.rst b/release-notes/unreleased/1902.feat.rst deleted file mode 100644 index f6f8e3fd7..000000000 --- a/release-notes/unreleased/1902.feat.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``fallback`` option to ZNE extrapolators. \ No newline at end of file From a41738a33a3c37ce6abdc72fc51d9ae424cc5c6f Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Wed, 4 Sep 2024 15:36:17 -0400 Subject: [PATCH 15/17] update main branch version 0.30.0 (#1905) --- docs/conf.py | 2 +- qiskit_ibm_runtime/VERSION.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 273ff589e..6b067580f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -release = '0.29.0' +release = '0.30.0' # -- General configuration --------------------------------------------------- diff --git a/qiskit_ibm_runtime/VERSION.txt b/qiskit_ibm_runtime/VERSION.txt index ae6dd4e20..c25c8e5b7 100644 --- a/qiskit_ibm_runtime/VERSION.txt +++ b/qiskit_ibm_runtime/VERSION.txt @@ -1 +1 @@ -0.29.0 +0.30.0 From e9f85246f12ef92f6100942c4bfaf8d1cff0f1b7 Mon Sep 17 00:00:00 2001 From: Samuele Ferracin Date: Thu, 5 Sep 2024 11:53:25 -0400 Subject: [PATCH 16/17] Adding pass to turn arbitrary circuit into Clifford circuit (#1887) Co-authored-by: joshuasn <53916441+joshuasn@users.noreply.github.com> --- .../transpiler/passes/__init__.py | 5 +- .../passes/cliffordization/__init__.py | 15 ++++ .../convert_isa_to_clifford.py | 81 +++++++++++++++++ release-notes/unreleased/1887.feat.rst | 1 + .../passes/cliffordization/__init__.py | 11 +++ .../cliffordization/test_to_clifford.py | 87 +++++++++++++++++++ 6 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 qiskit_ibm_runtime/transpiler/passes/cliffordization/__init__.py create mode 100644 qiskit_ibm_runtime/transpiler/passes/cliffordization/convert_isa_to_clifford.py create mode 100644 release-notes/unreleased/1887.feat.rst create mode 100644 test/unit/transpiler/passes/cliffordization/__init__.py create mode 100644 test/unit/transpiler/passes/cliffordization/test_to_clifford.py diff --git a/qiskit_ibm_runtime/transpiler/passes/__init__.py b/qiskit_ibm_runtime/transpiler/passes/__init__.py index eecb3fdf1..03f1a4306 100644 --- a/qiskit_ibm_runtime/transpiler/passes/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/__init__.py @@ -17,7 +17,7 @@ .. currentmodule:: qiskit_ibm_runtime.transpiler.passes -A collection of transpiler passes for IBM backends. Refer to +A collection of transpiler passes. Refer to https://docs.quantum.ibm.com/guides/transpile to learn more about transpilation and passes. @@ -25,6 +25,7 @@ :toctree: ../stubs/ ConvertIdToDelay + ConvertISAToClifford See :mod:`qiskit_ibm_runtime.transpiler.passes.scheduling` for a collection of scheduling passes. """ @@ -35,3 +36,5 @@ from .scheduling import ASAPScheduleAnalysis from .scheduling import PadDynamicalDecoupling from .scheduling import PadDelay + +from .cliffordization import ConvertISAToClifford diff --git a/qiskit_ibm_runtime/transpiler/passes/cliffordization/__init__.py b/qiskit_ibm_runtime/transpiler/passes/cliffordization/__init__.py new file mode 100644 index 000000000..207b4ec93 --- /dev/null +++ b/qiskit_ibm_runtime/transpiler/passes/cliffordization/__init__.py @@ -0,0 +1,15 @@ +# 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. + +"""Passes to transform a circuit into a Clifford circuit.""" + +from .convert_isa_to_clifford import ConvertISAToClifford diff --git a/qiskit_ibm_runtime/transpiler/passes/cliffordization/convert_isa_to_clifford.py b/qiskit_ibm_runtime/transpiler/passes/cliffordization/convert_isa_to_clifford.py new file mode 100644 index 000000000..2b980bc97 --- /dev/null +++ b/qiskit_ibm_runtime/transpiler/passes/cliffordization/convert_isa_to_clifford.py @@ -0,0 +1,81 @@ +# 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. + +""" +Pass to convert the gates of an ISA circuit to Clifford gates. +""" + +from random import choices +import numpy as np + +from qiskit.circuit import Barrier, Measure +from qiskit.circuit.library import CXGate, CZGate, ECRGate, IGate, RZGate, SXGate, XGate +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.basepasses import TransformationPass + +SUPPORTED_INSTRUCTIONS = (CXGate, CZGate, ECRGate, IGate, RZGate, SXGate, XGate, Barrier, Measure) +"""The set of instructions supported by the :class:`~.ConvertISAToClifford` pass.""" + + +class ConvertISAToClifford(TransformationPass): + """ + Convert the gates of an ISA circuit to Clifford gates. + + ISA circuits only contain Clifford gates from a restricted set or + :class:`qiskit.circuit.library.RZGate`\\s by arbitrary angles. To convert them to Clifford + circuits, this pass rounds the angle of every :class:`qiskit.circuit.library.RZGate` to the + closest multiple of `pi/2` (or to a random multiple of `pi/2` if the angle is unspecified), + while it skips every Clifford gate, measurement, and barrier. + + .. code-block:: python + + import numpy as np + + from qiskit.circuit import QuantumCircuit, Parameter + from qiskit.transpiler import PassManager + + from qiskit_ibm_runtime.transpiler.passes import ConvertISAToClifford + + # An ISA circuit ending with a Z rotation by pi/3 + qc = QuantumCircuit(2, 2) + qc.sx(0) + qc.rz(np.pi/2, 0) + qc.sx(0) + qc.barrier() + qc.cx(0, 1) + qc.rz(np.pi/3, 0) # non-Clifford Z rotation + qc.rz(Parameter("th"), 0) # Z rotation with unspecified angle + + # Turn into a Clifford circuit + pm = PassManager([ConvertISAToClifford()]) + clifford_qc = pm.run(qc) + + Raises: + ValueError: If the given circuit contains unsupported operations, such as non-ISA gates. + """ + + def run(self, dag: DAGCircuit) -> DAGCircuit: + for node in dag.op_nodes(): + if not isinstance(node.op, SUPPORTED_INSTRUCTIONS): + raise ValueError(f"Operation ``{node.op.name}`` not supported.") + + # Round the angle of `RZ`s to a multiple of pi/2 and skip the other instructions. + if isinstance(node.op, RZGate): + if isinstance(angle := node.op.params[0], float): + rem = angle % (np.pi / 2) + new_angle = angle - rem if rem < np.pi / 4 else angle + np.pi / 2 - rem + else: + # special handling of parametric gates + new_angle = choices([0, np.pi / 2, np.pi, 3 * np.pi / 2])[0] + dag.substitute_node(node, RZGate(new_angle), inplace=True) + + return dag diff --git a/release-notes/unreleased/1887.feat.rst b/release-notes/unreleased/1887.feat.rst new file mode 100644 index 000000000..a68beeff9 --- /dev/null +++ b/release-notes/unreleased/1887.feat.rst @@ -0,0 +1 @@ +Added ``ConvertISAToClifford`` transpilation pass to convert the gates of a circuit to Clifford gates. \ No newline at end of file diff --git a/test/unit/transpiler/passes/cliffordization/__init__.py b/test/unit/transpiler/passes/cliffordization/__init__.py new file mode 100644 index 000000000..139b265e1 --- /dev/null +++ b/test/unit/transpiler/passes/cliffordization/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. diff --git a/test/unit/transpiler/passes/cliffordization/test_to_clifford.py b/test/unit/transpiler/passes/cliffordization/test_to_clifford.py new file mode 100644 index 000000000..e436db9ff --- /dev/null +++ b/test/unit/transpiler/passes/cliffordization/test_to_clifford.py @@ -0,0 +1,87 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. + +"""Test the cliffordization pass.""" + +import numpy as np + +from qiskit.circuit import QuantumCircuit +from qiskit.transpiler.passmanager import PassManager + +from qiskit_ibm_runtime.transpiler.passes import ConvertISAToClifford + +from .....ibm_test_case import IBMTestCase + + +class TestConvertISAToClifford(IBMTestCase): + """Tests the ConvertISAToClifford pass.""" + + def test_clifford_isa_circuit(self): + """Test the pass on a Clifford circuit with ISA gates.""" + qc = QuantumCircuit(2, 2) + qc.id(0) + qc.sx(0) + qc.barrier() + qc.measure(0, 1) + qc.rz(0, 0) + qc.rz(np.pi / 2, 0) + qc.rz(np.pi, 0) + qc.rz(3 * np.pi / 2, 0) + qc.cx(0, 1) + qc.cz(0, 1) + qc.ecr(0, 1) + + pm = PassManager([ConvertISAToClifford()]) + transformed = pm.run(qc) + + self.assertEqual(qc, transformed) + + def test_error_clifford_non_isa_circuit(self): + """Test that the pass errors when run on a Clifford circuit with non-ISA gates.""" + qc = QuantumCircuit(2) + qc.id(0) + qc.sx(0) + qc.s(1) + qc.h(1) + qc.cx(0, 1) + qc.cz(0, 1) + qc.ecr(0, 1) + + pm = PassManager([ConvertISAToClifford()]) + with self.assertRaises(ValueError): + pm.run(qc) + + def test_error_non_clifford_isa_circuit(self): + """Test that the pass errors when run on a non-Clifford circuit with ISA gates.""" + qc = QuantumCircuit(2) + qc.id(0) + qc.sx(0) + qc.rx(np.pi / 2 - 0.1, 0) + qc.cx(0, 1) + qc.cz(0, 1) + qc.ecr(0, 1) + + pm = PassManager([ConvertISAToClifford()]) + with self.assertRaises(ValueError): + pm.run(qc) + + def test_error_reset(self): + """Test that the pass errors when run on circuits with resets.""" + qc = QuantumCircuit(2) + qc.x(1) + qc.barrier(0) + qc.reset(1) + qc.rx(np.pi / 2 - 0.1, 1) + + pm = PassManager([ConvertISAToClifford()]) + with self.assertRaises(ValueError): + pm.run(qc) From f12aa74621481c3a8427e5ed361f373b9d97efc7 Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 6 Sep 2024 12:04:47 -0400 Subject: [PATCH 17/17] Update noise learner tests (#1906) * Update noise learner tests * remove status check --- test/integration/test_noise_learner.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/test_noise_learner.py b/test/integration/test_noise_learner.py index ae09d592f..cad941aa8 100644 --- a/test/integration/test_noise_learner.py +++ b/test/integration/test_noise_learner.py @@ -16,7 +16,6 @@ from unittest import SkipTest from qiskit.circuit import QuantumCircuit -from qiskit.providers.jobstatus import JobStatus from qiskit_ibm_runtime import RuntimeJob, Session, EstimatorV2 from qiskit_ibm_runtime.noise_learner import NoiseLearner @@ -135,7 +134,6 @@ def test_learner_plus_estimator(self, service): # pylint: disable=unused-argume def _verify(self, job: RuntimeJob, expected_input_options: dict, n_results: int) -> None: job.wait_for_final_state() - self.assertEqual(job.status(), JobStatus.DONE, job.error_message()) result = job.result() self.assertEqual(len(result), n_results)