From a78c733427d2166c9c5cd33fb17ddf68df4a6754 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Wed, 2 Jul 2025 15:00:52 -0400 Subject: [PATCH 1/4] remove RuntimeOptions --- qiskit_ibm_runtime/__init__.py | 1 - .../fake_provider/local_service.py | 11 +- qiskit_ibm_runtime/options/options.py | 8 +- qiskit_ibm_runtime/qiskit_runtime_service.py | 39 +++--- qiskit_ibm_runtime/runtime_options.py | 127 ------------------ test/unit/test_options.py | 21 --- 6 files changed, 22 insertions(+), 185 deletions(-) delete mode 100644 qiskit_ibm_runtime/runtime_options.py diff --git a/qiskit_ibm_runtime/__init__.py b/qiskit_ibm_runtime/__init__.py index 33acca3c3..935ec8c28 100644 --- a/qiskit_ibm_runtime/__init__.py +++ b/qiskit_ibm_runtime/__init__.py @@ -205,7 +205,6 @@ from .ibm_backend import IBMBackend from .runtime_job import RuntimeJob from .runtime_job_v2 import RuntimeJobV2 -from .runtime_options import RuntimeOptions from .utils.json import RuntimeEncoder, RuntimeDecoder from .session import Session # pylint: disable=cyclic-import from .batch import Batch # pylint: disable=cyclic-import diff --git a/qiskit_ibm_runtime/fake_provider/local_service.py b/qiskit_ibm_runtime/fake_provider/local_service.py index 0e78dcccc..bcb562af7 100644 --- a/qiskit_ibm_runtime/fake_provider/local_service.py +++ b/qiskit_ibm_runtime/fake_provider/local_service.py @@ -18,8 +18,7 @@ import copy import logging import warnings -from dataclasses import asdict -from typing import Callable, Dict, List, Literal, Optional, Union +from typing import Callable, Dict, List, Literal, Optional from qiskit.primitives import ( BackendEstimatorV2, @@ -34,7 +33,6 @@ from .fake_provider import FakeProviderForBackendV2 # pylint: disable=unused-import, cyclic-import from .local_runtime_job import LocalRuntimeJob from ..ibm_backend import IBMBackend -from ..runtime_options import RuntimeOptions logger = logging.getLogger(__name__) @@ -148,7 +146,7 @@ def _run( self, program_id: Literal["sampler", "estimator"], inputs: Dict, - options: Union[RuntimeOptions, Dict], + options: Dict, ) -> PrimitiveJob: """Execute the runtime program. @@ -165,10 +163,7 @@ def _run( ValueError: If input is invalid. NotImplementedError: If using V2 primitives. """ - if isinstance(options, Dict): - qrt_options = copy.deepcopy(options) - else: - qrt_options = asdict(options) + qrt_options = copy.deepcopy(options) backend = qrt_options.pop("backend", None) diff --git a/qiskit_ibm_runtime/options/options.py b/qiskit_ibm_runtime/options/options.py index 1fba169fd..bfd068a14 100644 --- a/qiskit_ibm_runtime/options/options.py +++ b/qiskit_ibm_runtime/options/options.py @@ -14,7 +14,7 @@ from abc import abstractmethod from typing import Iterable, Tuple, Union, Any -from dataclasses import dataclass, fields, asdict, is_dataclass +from dataclasses import dataclass, asdict, is_dataclass import copy from qiskit.transpiler import CouplingMap @@ -31,7 +31,6 @@ ) from .environment_options import EnvironmentOptions from .simulator_options import SimulatorOptions -from ..runtime_options import RuntimeOptions def _make_data_row(indent: int, name: str, value: Any, is_section: bool) -> Iterable[str]: @@ -93,9 +92,8 @@ def _get_runtime_options(options: dict) -> dict: environment = options_copy.get("environment") or {} out = {"max_execution_time": options_copy.get("max_execution_time", None)} - for fld in fields(RuntimeOptions): - if fld.name in environment: - out[fld.name] = environment[fld.name] + for fld in environment: + out[fld] = environment[fld] if "image" in options_copy: out["image"] = options_copy["image"] diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index 105715f69..952f5d791 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -42,7 +42,6 @@ from .runtime_job_v2 import RuntimeJobV2 from .utils import validate_job_tags from .api.client_parameters import ClientParameters -from .runtime_options import RuntimeOptions from .ibm_backend import IBMBackend from .models import QasmBackendConfiguration @@ -1062,7 +1061,7 @@ def _run( self, program_id: str, inputs: Dict, - options: Optional[Union[RuntimeOptions, Dict]] = None, + options: Optional[Dict] = None, callback: Optional[Callable] = None, result_decoder: Optional[Union[Type[ResultDecoder], Sequence[Type[ResultDecoder]]]] = None, session_id: Optional[str] = None, @@ -1097,30 +1096,23 @@ def _run( RuntimeProgramNotFound: If the program cannot be found. IBMRuntimeError: An error occurred running the program. """ - - qrt_options: RuntimeOptions = options - if options is None: - qrt_options = RuntimeOptions() - elif isinstance(options, Dict): - qrt_options = RuntimeOptions(**options) - - qrt_options.validate(channel=self.channel) + backend_name = options["backend"].name if self._channel == "ibm_quantum": # Find the right hgp hgp_name = self._get_hgp( - instance=qrt_options.instance, backend_name=qrt_options.get_backend_name() + instance=options.get("instance"), backend_name=backend_name ).name if hgp_name != self._current_instance: self._current_instance = hgp_name logger.info("Instance selected: %s", self._current_instance) else: hgp_name = None - backend = qrt_options.backend + backend = options["backend"] if isinstance(backend, str) or ( hgp_name and isinstance(backend, IBMBackend) and backend._instance != hgp_name ): - backend = self.backend(name=qrt_options.get_backend_name(), instance=hgp_name) + backend = self.backend(name=backend_name, instance=hgp_name) status = backend.status() if status.operational is True and status.status_msg != "active": warnings.warn( @@ -1134,17 +1126,17 @@ def _run( try: response = self._active_api_client.program_run( program_id=program_id, - backend_name=qrt_options.get_backend_name(), + backend_name=backend_name, params=inputs, - image=qrt_options.image, + image=options.get("image"), hgp=hgp_name, - log_level=qrt_options.log_level, + log_level=options.get("log_level"), session_id=session_id, - job_tags=qrt_options.job_tags, - max_execution_time=qrt_options.max_execution_time, + job_tags=options.get("job_tags"), + max_execution_time=options.get("max_execution_time"), start_session=start_session, - session_time=qrt_options.session_time, - private=qrt_options.private, + session_time=options.get("session_time"), + private=options.get("private"), ) if self._channel == "ibm_quantum": messages = response.get("messages") @@ -1157,7 +1149,7 @@ def _run( raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None raise IBMRuntimeError(f"Failed to run program: {ex}") from None - if response["backend"] and response["backend"] != qrt_options.get_backend_name(): + if response["backend"] and response["backend"] != backend_name: backend = self.backend(name=response["backend"], instance=hgp_name) return RuntimeJobV2( @@ -1167,10 +1159,11 @@ def _run( program_id=program_id, user_callback=callback, result_decoder=result_decoder, - image=qrt_options.image, + image=options.get("image"), + tags=options.get("job_tags"), service=self, version=version, - private=qrt_options.private, + private=options.get("private"), ) def check_pending_jobs(self) -> None: diff --git a/qiskit_ibm_runtime/runtime_options.py b/qiskit_ibm_runtime/runtime_options.py deleted file mode 100644 index 7dd427345..000000000 --- a/qiskit_ibm_runtime/runtime_options.py +++ /dev/null @@ -1,127 +0,0 @@ -# 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. - -"""Runtime options that control the execution environment.""" - -from __future__ import annotations - -import re -import logging -from dataclasses import dataclass -from typing import Optional, List - -from qiskit.providers.backend import Backend - -from .exceptions import IBMInputValueError -from .utils import validate_job_tags - - -@dataclass(init=False) -class RuntimeOptions: - """Class for representing generic runtime execution options.""" - - backend: Optional[str | Backend] = None - image: Optional[str] = None - log_level: Optional[str] = None - instance: Optional[str] = None - job_tags: Optional[List[str]] = None - max_execution_time: Optional[int] = None - session_time: Optional[int] = None - private: Optional[bool] = False - - def __init__( - self, - backend: Optional[str | Backend] = None, - image: Optional[str] = None, - log_level: Optional[str] = None, - instance: Optional[str] = None, - job_tags: Optional[List[str]] = None, - max_execution_time: Optional[int] = None, - session_time: Optional[int] = None, - private: Optional[bool] = False, - ) -> None: - """RuntimeOptions constructor. - - Args: - backend: target backend to run on. This is required for ``ibm_quantum`` channel. - image: the runtime image used to execute the primitive, specified in - the form of ``image_name:tag``. Not all accounts are - authorized to select a different image. - log_level: logging level to set in the execution environment. The valid - log levels are: ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, and ``CRITICAL``. - The default level is ``WARNING``. - instance: The hub/group/project to use, in that format. This is only supported - for ``ibm_quantum`` channel. If ``None``, a hub/group/project that provides - access to the target backend is randomly selected. - job_tags: Tags to be assigned to the job. The tags can subsequently be used - as a filter in the :meth:`jobs()` function call. - max_execution_time: Maximum execution time in seconds, which is based - on system execution time (not wall clock time). System execution time is the - amount of time that the system is dedicated to processing your job. If a job exceeds - this time limit, it is forcibly cancelled. Simulator jobs continue to use wall - clock time. - session_time: Length of session in seconds. - private: Boolean that indicates whether the job is marked as private. When set to true, - input parameters are not returned, and the results can only be read once. - After the job is completed, input parameters are deleted from the service. - After the results are read, these are also deleted from the service. - When set to false, the input parameters and results follow the - standard retention behavior of the API. - """ - self.backend = backend - self.image = image - self.log_level = log_level - self.instance = instance - 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. - - Args: - channel: channel type. - - Raises: - IBMInputValueError: If one or more option is invalid. - """ - if self.image and not re.match( - "[a-zA-Z0-9]+([/.\\-_][a-zA-Z0-9]+)*:[a-zA-Z0-9]+([.\\-_][a-zA-Z0-9]+)*$", - self.image, - ): - raise IBMInputValueError('"image" needs to be in form of image_name:tag') - - if channel == "ibm_quantum" and not self.backend: - raise IBMInputValueError( - '"backend" is required field in "options" for "ibm_quantum" channel.' - ) - - if self.instance and channel != "ibm_quantum": - raise IBMInputValueError('"instance" is only supported for "ibm_quantum" channel.') - - if self.log_level and not isinstance(logging.getLevelName(self.log_level.upper()), int): - raise IBMInputValueError( - f"{self.log_level} is not a valid log level. The valid log levels are: `DEBUG`, " - f"`INFO`, `WARNING`, `ERROR`, and `CRITICAL`." - ) - - if self.job_tags: - validate_job_tags(self.job_tags) - - def get_backend_name(self) -> str: - """Get backend name.""" - if isinstance(self.backend, str): - return self.backend - if self.backend: - return self.backend.name if self.backend.version == 2 else self.backend.name() - return None diff --git a/test/unit/test_options.py b/test/unit/test_options.py index 433256f33..9c9a5dad2 100644 --- a/test/unit/test_options.py +++ b/test/unit/test_options.py @@ -20,7 +20,6 @@ from qiskit.transpiler import CouplingMap from qiskit_aer.noise import NoiseModel -from qiskit_ibm_runtime import RuntimeOptions from qiskit_ibm_runtime.options import EstimatorOptions, SamplerOptions from qiskit_ibm_runtime.fake_provider import FakeManilaV2, FakeNairobiV2 @@ -32,26 +31,6 @@ class TestOptionsV2(IBMTestCase): """Class for testing the v2 Options class.""" - @data(EstimatorOptions, SamplerOptions) - def test_runtime_options(self, opt_cls): - """Test converting runtime options.""" - full_options = RuntimeOptions( - backend="ibm_gotham", - image="foo:bar", - log_level="DEBUG", - instance="h/g/p", - job_tags=["foo", "bar"], - max_execution_time=600, - ) - partial_options = RuntimeOptions(backend="foo", log_level="DEBUG") - - for rt_options in [full_options, partial_options]: - with self.subTest(rt_options=rt_options): - self.assertGreaterEqual( - vars(rt_options).items(), - opt_cls._get_runtime_options(vars(rt_options)).items(), - ) - @data(EstimatorOptions, SamplerOptions) def test_kwargs_options(self, opt_cls): """Test specifying arbitrary options.""" From 6684b898261fa969116a5112716b1ecc23fe86e4 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Wed, 2 Jul 2025 15:21:49 -0400 Subject: [PATCH 2/4] unit tests --- qiskit_ibm_runtime/qiskit_runtime_service.py | 4 +++- test/unit/test_jobs.py | 6 ------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index 952f5d791..af46764ac 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -1096,7 +1096,9 @@ def _run( RuntimeProgramNotFound: If the program cannot be found. IBMRuntimeError: An error occurred running the program. """ - backend_name = options["backend"].name + backend_name = ( + options["backend"] if isinstance(options["backend"], str) else options["backend"].name + ) if self._channel == "ibm_quantum": # Find the right hgp diff --git a/test/unit/test_jobs.py b/test/unit/test_jobs.py index 2edb55a1a..379b008d7 100644 --- a/test/unit/test_jobs.py +++ b/test/unit/test_jobs.py @@ -59,12 +59,6 @@ def test_run_program_phantom_backend(self, service): with self.assertRaises(QiskitBackendNotFoundError): _ = run_program(service=service, backend_name="phantom_backend") - def test_run_program_missing_backend_ibm_quantum(self): - """Test running an ibm_quantum program with no backend.""" - service = FakeRuntimeService(channel="ibm_quantum", token="my_token") - with self.assertRaises(IBMInputValueError): - _ = run_program(service=service, backend_name="") - def test_run_program_default_hgp_backend(self): """Test running a program with a backend in default hgp.""" service = FakeRuntimeService(channel="ibm_quantum", token="my_token") From 15ebd4c480b53b1d17b0f55801ca042231702fa2 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Tue, 12 Aug 2025 13:22:15 -0400 Subject: [PATCH 3/4] formatting --- qiskit_ibm_runtime/qiskit_runtime_service.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index e509dfae9..3f50fbb2b 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -824,7 +824,7 @@ def _run( ) backend = options["backend"] - + status = backend.status() if status.operational is True and status.status_msg != "active": warnings.warn( @@ -838,7 +838,6 @@ def _run( backend_name=backend_name, params=inputs, image=options.get("image"), - hgp=hgp_name, log_level=options.get("log_level"), session_id=session_id, job_tags=options.get("job_tags"), @@ -854,7 +853,7 @@ def _run( raise IBMRuntimeError(f"Failed to run program: {ex}") from None if response["backend"] and response["backend"] != backend_name: - backend = self.backend(name=response["backend"], instance=hgp_name) + backend = self.backend(name=response["backend"]) return RuntimeJobV2( backend=backend, From a5c8d041c5500fbcdb64d3b82c055cb7e9acdcdd Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Mon, 8 Sep 2025 14:13:33 -0400 Subject: [PATCH 4/4] Fix unit tests --- qiskit_ibm_runtime/qiskit_runtime_service.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index b9f1ff9bb..47599856b 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -876,11 +876,9 @@ def _run( RuntimeProgramNotFound: If the program cannot be found. IBMRuntimeError: An error occurred running the program. """ - backend_name = ( - options["backend"] if isinstance(options["backend"], str) else options["backend"].name - ) - backend = options["backend"] + if isinstance(backend, str): + backend = self.backend(name=backend) status = backend.status() if status.operational is True and status.status_msg != "active": @@ -892,7 +890,7 @@ def _run( try: response = self._active_api_client.program_run( program_id=program_id, - backend_name=backend_name, + backend_name=backend.name, params=inputs, image=options.get("image"), log_level=options.get("log_level"), @@ -909,7 +907,7 @@ def _run( raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None raise IBMRuntimeError(f"Failed to run program: {ex}") from None - if response["backend"] and response["backend"] != backend_name: + if response["backend"] and response["backend"] != backend.name: backend = self.backend(name=response["backend"]) return RuntimeJobV2(