diff --git a/docs/apidocs/index.rst b/docs/apidocs/index.rst index c8031745a..280a5a7d0 100644 --- a/docs/apidocs/index.rst +++ b/docs/apidocs/index.rst @@ -9,6 +9,7 @@ qiskit-ibm-runtime API reference runtime_service noise_learner + noise_learner_result options transpiler qiskit_ibm_runtime.transpiler.passes.scheduling diff --git a/docs/apidocs/noise_learner_result.rst b/docs/apidocs/noise_learner_result.rst new file mode 100644 index 000000000..b530306a2 --- /dev/null +++ b/docs/apidocs/noise_learner_result.rst @@ -0,0 +1,4 @@ +.. automodule:: qiskit_ibm_runtime.utils.noise_learner_result + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/qiskit_ibm_runtime/options/resilience_options.py b/qiskit_ibm_runtime/options/resilience_options.py index 0b22d06da..550b05aca 100644 --- a/qiskit_ibm_runtime/options/resilience_options.py +++ b/qiskit_ibm_runtime/options/resilience_options.py @@ -12,11 +12,12 @@ """Resilience options.""" -from typing import Literal, Union +from typing import List, Literal, Union from dataclasses import asdict from pydantic import model_validator, Field +from ..utils.noise_learner_result import LayerError from .utils import Unset, UnsetType, Dict, primitive_dataclass from .measure_noise_learning_options import MeasureNoiseLearningOptions from .zne_options import ZneOptions @@ -65,6 +66,12 @@ 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``. Layers whose information is missing in ``layer_noise_model`` + are treated as noiseless and their noise is not mitigated. """ measure_mitigation: Union[UnsetType, bool] = Unset @@ -78,6 +85,7 @@ class ResilienceOptionsV2: layer_noise_learning: Union[LayerNoiseLearningOptions, Dict] = Field( default_factory=LayerNoiseLearningOptions ) + layer_noise_model: Union[UnsetType, List[LayerError]] = Unset @model_validator(mode="after") def _validate_options(self) -> "ResilienceOptionsV2": diff --git a/qiskit_ibm_runtime/utils/noise_learner_result.py b/qiskit_ibm_runtime/utils/noise_learner_result.py index e59b6313d..c85259d5f 100644 --- a/qiskit_ibm_runtime/utils/noise_learner_result.py +++ b/qiskit_ibm_runtime/utils/noise_learner_result.py @@ -10,7 +10,17 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""NoiseLearner result class""" +""" +================================================================================== +NoiseLearner result classes (:mod:`qiskit_ibm_runtime.utils.noise_learner_result`) +================================================================================== + +.. autosummary:: + :toctree: ../stubs/ + + PauliLindbladError + LayerError +""" from __future__ import annotations diff --git a/release-notes/unreleased/1858.feat.rst b/release-notes/unreleased/1858.feat.rst new file mode 100644 index 000000000..1aaf41a04 --- /dev/null +++ b/release-notes/unreleased/1858.feat.rst @@ -0,0 +1,3 @@ +``ResilienceOptionsV2`` has a new field ``layer_noise_model``. When this field is set, all the +mitigation strategies that require noise data skip the noise learning stage, and instead gather +the required information from ``layer_noise_model``. \ No newline at end of file diff --git a/test/integration/test_noise_learner.py b/test/integration/test_noise_learner.py index 6a92f2390..ae09d592f 100644 --- a/test/integration/test_noise_learner.py +++ b/test/integration/test_noise_learner.py @@ -17,12 +17,11 @@ from qiskit.circuit import QuantumCircuit from qiskit.providers.jobstatus import JobStatus -from qiskit.compiler import transpile -from qiskit_ibm_runtime import RuntimeJob, Session +from qiskit_ibm_runtime import RuntimeJob, Session, EstimatorV2 from qiskit_ibm_runtime.noise_learner import NoiseLearner from qiskit_ibm_runtime.utils.noise_learner_result import PauliLindbladError, LayerError -from qiskit_ibm_runtime.options import NoiseLearnerOptions +from qiskit_ibm_runtime.options import NoiseLearnerOptions, EstimatorOptions from ..decorators import run_integration_test from ..ibm_test_case import IBMIntegrationTestCase @@ -39,12 +38,12 @@ def setUp(self) -> None: raise SkipTest("test_eagle not available in this environment") c1 = QuantumCircuit(2) - c1.cx(0, 1) + c1.ecr(0, 1) c2 = QuantumCircuit(3) - c2.cx(0, 1) - c2.cx(1, 2) - c2.cx(0, 1) + c2.ecr(0, 1) + c2.ecr(1, 2) + c2.ecr(0, 1) self.circuits = [c1, c2] @@ -65,10 +64,9 @@ def test_with_default_options(self, service): # pylint: disable=unused-argument options = NoiseLearnerOptions() learner = NoiseLearner(mode=backend, options=options) - circuits = transpile(self.circuits, backend=backend) - job = learner.run(circuits) + job = learner.run(self.circuits) - self._verify(job, self.default_input_options) + self._verify(job, self.default_input_options, 3) @run_integration_test def test_with_non_default_options(self, service): # pylint: disable=unused-argument @@ -80,43 +78,12 @@ def test_with_non_default_options(self, service): # pylint: disable=unused-argu options.layer_pair_depths = [0, 1] learner = NoiseLearner(mode=backend, options=options) - circuits = transpile(self.circuits, backend=backend) - job = learner.run(circuits) + job = learner.run(self.circuits) input_options = deepcopy(self.default_input_options) input_options["max_layers_to_learn"] = 1 input_options["layer_pair_depths"] = [0, 1] - self._verify(job, input_options) - - @run_integration_test - def test_in_session(self, service): - """Test noise learner when used within a session.""" - backend = self.backend - - options = NoiseLearnerOptions() - options.max_layers_to_learn = 1 - options.layer_pair_depths = [0, 1] - - input_options = deepcopy(self.default_input_options) - input_options["max_layers_to_learn"] = 1 - input_options["layer_pair_depths"] = [0, 1] - - circuits = transpile(self.circuits, backend=backend) - - with Session(service, backend) as session: - options.twirling_strategy = "all" - learner1 = NoiseLearner(mode=session, options=options) - job1 = learner1.run(circuits) - - input_options["twirling_strategy"] = "all" - self._verify(job1, input_options) - - options.twirling_strategy = "active-circuit" - learner2 = NoiseLearner(mode=session, options=options) - job2 = learner2.run(circuits) - - input_options["twirling_strategy"] = "active-circuit" - self._verify(job2, input_options) + self._verify(job, input_options, 1) @run_integration_test def test_with_no_layers(self, service): # pylint: disable=unused-argument @@ -127,20 +94,52 @@ def test_with_no_layers(self, service): # pylint: disable=unused-argument options.max_layers_to_learn = 0 learner = NoiseLearner(mode=backend, options=options) - circuits = transpile(self.circuits, backend=backend) - job = learner.run(circuits) + job = learner.run(self.circuits) self.assertEqual(job.result().data, []) input_options = deepcopy(self.default_input_options) input_options["max_layers_to_learn"] = 0 - self._verify(job, input_options) + self._verify(job, input_options, 0) + + @run_integration_test + def test_learner_plus_estimator(self, service): # pylint: disable=unused-argument + """Test feeding noise learner data to estimator.""" + backend = self.backend + + options = EstimatorOptions() + options.resilience.zne_mitigation = True # pylint: disable=assigning-non-slot + options.resilience.zne.amplifier = "pea" + options.resilience.layer_noise_learning.layer_pair_depths = [0, 1] - def _verify(self, job: RuntimeJob, expected_input_options: dict) -> None: + pubs = [(c, "Z" * c.num_qubits) for c in self.circuits] + + with Session(service, backend) as session: + learner = NoiseLearner(mode=session, options=options) + learner_job = learner.run(self.circuits) + noise_model = learner_job.result() + self.assertEqual(len(noise_model), 3) + + estimator = EstimatorV2(mode=session, options=options) + estimator.options.resilience.layer_noise_model = noise_model + + estimator_job = estimator.run(pubs) + result = estimator_job.result() + + noise_model_metadata = result.metadata["resilience"]["layer_noise_model"] + for x, y in zip(noise_model, noise_model_metadata): + self.assertEqual(x.circuit, y.circuit) + self.assertEqual(x.qubits, y.qubits) + self.assertEqual(x.error.generators, y.error.generators) + self.assertEqual(x.error.rates.tolist(), y.error.rates.tolist()) + + 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) + for datum in result.data: circuit = datum.circuit qubits = datum.qubits