Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 11 additions & 45 deletions qiskit_ibm_runtime/noise_learner_v3/noise_learner_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
from ..batch import Batch
from ..fake_provider.local_service import QiskitRuntimeLocalService
from ..options.noise_learner_v3_options import NoiseLearnerV3Options
from ..qiskit_runtime_service import QiskitRuntimeService
from ..runtime_job_v2 import RuntimeJobV2

# pylint: disable=unused-import,cyclic-import
Expand Down Expand Up @@ -77,48 +76,16 @@ def __init__(
mode: Optional[Union[BackendV2, Session, Batch]] = None,
options: Optional[NoiseLearnerV3Options] = None,
):
self._session: BackendV2 | None = None
self._backend: BackendV2
self._service: QiskitRuntimeService

self._options = options or NoiseLearnerV3Options()
if (
isinstance(self._options.experimental, UnsetType)
or self._options.experimental.get("image") is None
):
self._options.experimental = {}

if isinstance(mode, (Session, Batch)):
self._session = mode
self._backend = self._session._backend
self._service = self._session.service
elif open_session := get_cm_session():
if open_session != mode:
if open_session._backend != mode:
raise ValueError(
"The backend passed in to the primitive is different from the session "
"backend. Please check which backend you intend to use or leave the mode "
"parameter empty to use the session backend."
)
logger.warning(
"A backend was passed in as the mode but a session context manager "
"is open so this job will run inside this session/batch "
"instead of in job mode."
)
self._session = open_session
self._backend = self._session._backend
self._service = self._session.service
elif isinstance(mode, BackendV2):
self._backend = mode
self._service = self._backend.service
else:
raise ValueError(
"A backend or session/batch must be specified, or a session/batch must be open."
)
self._mode, self._service, self._backend = _get_mode_service_backend(mode)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the part above redundant, because_get_mode_service_backend just undoes what was done above?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The part above was some copying of the content of _get_mode_service_backend, with some editing, that introduced bugs. Some cases were addressed (correctly or incorrectly) by the part above, without ever reaching the call to _get_mode_service_backend. Other cases did reach the call, and went through the same process again inside the function.


if isinstance(self._service, QiskitRuntimeLocalService): # type: ignore[unreachable]
raise ValueError("``NoiseLearner`` not currently supported in local mode.")
self._session, self._service, self._backend = _get_mode_service_backend(mode)
if isinstance(self._service, QiskitRuntimeLocalService):
raise ValueError("``NoiseLearnerV3`` is currently not supported in local mode.")

@property
def options(self) -> NoiseLearnerV3Options:
Expand All @@ -144,15 +111,14 @@ def run(self, instructions: Iterable[CircuitInstruction]) -> RuntimeJobV2:
IBMInputValueError: If an instruction cannot be learned by any of the supported learning
protocols.
"""
if self._backend:
target = getattr(self._backend, "target", None)
if target and not is_simulator(self._backend):
for instruction in instructions:
validate_instruction(instruction, target)

configuration = getattr(self._backend, "configuration", None)
if configuration and not is_simulator(self._backend):
validate_options(self.options, configuration())
target = getattr(self._backend, "target", None)
if target and not is_simulator(self._backend):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what happens if self._backend is a simulator, I would expect it to raise somewhere (init or run? 🤷). The truth is that we never supported simulators for NoiseLearner, and the same applies to NoiseLearnerV3.

If this is indeed the case, I would prefer to add a check to the init along the lines of:

if is_simulator(self._backend):
    raise ValueError("Simulator backend is not supported.")

And then here:

  • We would not need to check is_simulator anymore
  • We would need to raise ValueError if target or configuration is None, because real backends do have a target and a config

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • We already raise, in init, if we fall on the QiskitRuntimeLocalService, perhaps it covers the case of a simulator?
  • If real backends always have a target and a config then why check it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it can be a simulator at this point, so I removed the check in 4b98826. I also think the check if target and config are None is redundant, but why not. We can also revert and think about all this in a separate issue.

for instruction in instructions:
validate_instruction(instruction, target)

configuration = getattr(self._backend, "configuration", None)
if configuration and not is_simulator(self._backend):
validate_options(self.options, configuration())

inputs = noise_learner_v3_inputs_to_0_1(instructions, self.options).model_dump()
inputs["version"] = 3 # TODO: this is a work-around for the dispatch
Expand Down
92 changes: 92 additions & 0 deletions test/unit/noise_learner_v3/test_noise_learner_v3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2025.
#
# 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 the `NoiseLearnerV3` class."""

from test.utils import get_mocked_backend, get_mocked_session

from qiskit_ibm_runtime import Session
from qiskit_ibm_runtime.noise_learner_v3 import NoiseLearnerV3

from ...ibm_test_case import IBMTestCase


class TestNoiseLearnerV3(IBMTestCase):
"""Tests the ``NoiseLearnerV3`` class."""

def test_init_with_backend(self):
"""Test ``NoiseLearnerV3.init`` when the input mode is an ``IBMBackend``."""
backend = get_mocked_backend()
service = backend.service
service.reset_mock()
noise_learner = NoiseLearnerV3(mode=backend)
self.assertEqual(noise_learner._session, None)
self.assertEqual(noise_learner._backend, backend)
self.assertEqual(noise_learner._service, service)

def test_init_with_session(self):
"""Test ``NoiseLearnerV3.init`` when the input mode is a session."""
backend_name = "ibm_hello"
session = get_mocked_session(get_mocked_backend(backend_name))
session.reset_mock()
session.service.reset_mock()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why do we need to reset (twice) something that we just initialized. We should check (in another issue) if we can improve this mock

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied it from the primitives v2 tests. We should check what happens in this regard when tests run in parallel.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed I don't see a reason for any of the calls to reset_mock in test_noise_learner_v3.py, and for most of the calls in test_ibm_primitives_v2.py. To be on the safe side, as you say, we'll face it in another issue.

noise_learner = NoiseLearnerV3(mode=session)
self.assertEqual(noise_learner._session, session)
self.assertEqual(noise_learner._backend.name, backend_name)
self.assertEqual(noise_learner._service, session.service)

def test_session_context_manager(self):
"""Test ``NoiseLearnerV3.init`` inside a session context manager."""
backend = get_mocked_backend()
service = backend.service
service.reset_mock()
with Session(backend=backend) as session:
noise_learner = NoiseLearnerV3()
self.assertEqual(noise_learner._session, session)
self.assertEqual(noise_learner._backend, backend)
self.assertEqual(noise_learner._service, service)

def test_init_with_backend_inside_session_context_manager(self):
"""Test ``NoiseLearnerV3.init`` inside a session context manager,
when the input mode is an ``IBMBackend``."""
backend = get_mocked_backend()
service = backend.service
service.reset_mock()
with Session(backend=backend) as session:
noise_learner = NoiseLearnerV3(mode=backend)
self.assertEqual(noise_learner._session, session)
self.assertEqual(noise_learner._backend, backend)
self.assertEqual(noise_learner._service, service)

def test_run_of_session_is_selected(self):
"""Test that ``NoiseLearner.run`` selects the ``run`` method
of the session, if a session is specified."""
backend_name = "ibm_hello"
session = get_mocked_session(get_mocked_backend(backend_name))
session.reset_mock()
session.service.reset_mock()
noise_learner = NoiseLearnerV3(mode=session)
session._run = lambda *args, **kwargs: "session"
session.service._run = lambda *args, **kwargs: "service"
selected_run = noise_learner.run([])
self.assertEqual(selected_run, "session")

def test_run_of_service_is_selected(self):
"""Test that ``NoiseLearner.run`` selects the ``run`` method
of the service, if a session is not specified."""
backend = get_mocked_backend()
service = backend.service
service.reset_mock()
noise_learner = NoiseLearnerV3(mode=backend)
service._run = lambda *args, **kwargs: "service"
selected_run = noise_learner.run([])
self.assertEqual(selected_run, "service")