From 79fb1674f44b98823f1898ae443a4f5812499ad8 Mon Sep 17 00:00:00 2001 From: Samuele Ferracin Date: Thu, 17 Oct 2024 10:48:42 -0400 Subject: [PATCH 1/3] added (#1978) --- CONTRIBUTING.md | 2 +- docs/apidocs/debug_tools.rst | 4 ++++ docs/apidocs/index.rst | 1 + qiskit_ibm_runtime/debug_tools/__init__.py | 21 ++++++++++++++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 docs/apidocs/debug_tools.rst diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dedd8448d..e2d905eae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -155,7 +155,7 @@ Finally, preview the docs build by following the instructions in Building The release notes are part of the standard qiskit-ibm-runtime documentation builds. To check what the rendered html output of the release notes will look like for the current state of the repo you can run: -`tox -edocs` which will build all the documentation into `docs/_build/html` +`tox -e docs` which will build all the documentation into `docs/_build/html` and the release notes in particular will be located at `docs/_build/html/release_notes.html`. diff --git a/docs/apidocs/debug_tools.rst b/docs/apidocs/debug_tools.rst new file mode 100644 index 000000000..01d3b9788 --- /dev/null +++ b/docs/apidocs/debug_tools.rst @@ -0,0 +1,4 @@ +.. automodule:: qiskit_ibm_runtime.debug_tools + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/apidocs/index.rst b/docs/apidocs/index.rst index 9653e753f..e90dc0e0b 100644 --- a/docs/apidocs/index.rst +++ b/docs/apidocs/index.rst @@ -15,4 +15,5 @@ qiskit-ibm-runtime API reference qiskit_ibm_runtime.transpiler.passes.scheduling fake_provider execution_span + debug_tools visualization diff --git a/qiskit_ibm_runtime/debug_tools/__init__.py b/qiskit_ibm_runtime/debug_tools/__init__.py index 7ebc2c6ef..ca86bc95b 100644 --- a/qiskit_ibm_runtime/debug_tools/__init__.py +++ b/qiskit_ibm_runtime/debug_tools/__init__.py @@ -10,7 +10,26 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Functions and classes for debugging and analyzing qiskit-ibm-runtime jobs.""" +""" +======================================================= +Debugging tools (:mod:`qiskit_ibm_runtime.debug_tools`) +======================================================= + +.. currentmodule:: qiskit_ibm_runtime.debug_tools + +The tools for debugging and analyzing qiskit-ibm-runtime jobs. + +Classes +======= + +.. autosummary:: + :toctree: ../stubs/ + + Neat + NeatResult + NeatPubResult + +""" from .neat import Neat from .neat_results import NeatPubResult, NeatResult From f7ad24ce9bce3aaa233aef2b1e97e675fbbf7f26 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:56:49 +0900 Subject: [PATCH 2/3] Add skip_reset_qubits option (#1981) --- .../options/dynamical_decoupling_options.py | 8 ++++++++ test/integration/test_sampler_v2.py | 1 + 2 files changed, 9 insertions(+) diff --git a/qiskit_ibm_runtime/options/dynamical_decoupling_options.py b/qiskit_ibm_runtime/options/dynamical_decoupling_options.py index 9a8eb0800..2476976f7 100644 --- a/qiskit_ibm_runtime/options/dynamical_decoupling_options.py +++ b/qiskit_ibm_runtime/options/dynamical_decoupling_options.py @@ -53,3 +53,11 @@ class DynamicalDecouplingOptions: Default: "alap". """ + skip_reset_qubits: Union[UnsetType, bool] = Unset + r"""Whether to insert DD on idle periods that immediately follow initialized/reset qubits. + + Since qubits in the ground state are less susceptible to decoherence, it can be beneficial + to let them be while they are known to be in this state. + + Default: False. + """ diff --git a/test/integration/test_sampler_v2.py b/test/integration/test_sampler_v2.py index 5e3454e35..ad8251a7a 100644 --- a/test/integration/test_sampler_v2.py +++ b/test/integration/test_sampler_v2.py @@ -491,6 +491,7 @@ def test_sampler_v2_dd(self, service): sampler.options.dynamical_decoupling.sequence_type = "XX" sampler.options.dynamical_decoupling.extra_slack_distribution = "middle" sampler.options.dynamical_decoupling.scheduling_method = "asap" + sampler.options.dynamical_decoupling.skip_reset_qubits = True bell, _, _ = self._cases[1] bell = transpile(bell, real_device) From f67531ecd3dbdd95f9a85814878ecd05379dc0aa Mon Sep 17 00:00:00 2001 From: Kevin Tian Date: Fri, 18 Oct 2024 10:22:19 -0400 Subject: [PATCH 3/3] Remove channel strategy & q-ctrl (#1966) * Remove channel strategy & q-ctrl * Add release note * merge conflicts again * Address comments --- .github/workflows/q-ctrl-tests.yml | 54 ------- qiskit_ibm_runtime/accounts/account.py | 28 +--- qiskit_ibm_runtime/accounts/management.py | 2 - qiskit_ibm_runtime/api/clients/runtime.py | 17 +-- qiskit_ibm_runtime/api/rest/runtime.py | 17 --- qiskit_ibm_runtime/estimator.py | 7 - .../fake_provider/local_service.py | 1 - qiskit_ibm_runtime/qiskit_runtime_service.py | 75 +-------- qiskit_ibm_runtime/sampler.py | 9 +- qiskit_ibm_runtime/utils/qctrl.py | 143 ------------------ release-notes/unreleased/1966.other.rst | 11 ++ test/decorators.py | 10 +- test/integration/test_account.py | 24 --- test/qctrl/test_qctrl.py | 113 -------------- test/unit/mock/fake_runtime_client.py | 13 +- test/unit/test_account.py | 27 ---- test/unit/test_ibm_primitives_v2.py | 40 ----- test/utils.py | 1 - 18 files changed, 23 insertions(+), 569 deletions(-) delete mode 100644 .github/workflows/q-ctrl-tests.yml delete mode 100644 qiskit_ibm_runtime/utils/qctrl.py create mode 100644 release-notes/unreleased/1966.other.rst delete mode 100644 test/qctrl/test_qctrl.py diff --git a/.github/workflows/q-ctrl-tests.yml b/.github/workflows/q-ctrl-tests.yml deleted file mode 100644 index c138e9289..000000000 --- a/.github/workflows/q-ctrl-tests.yml +++ /dev/null @@ -1,54 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -name: Q-CTRL Tests -on: - schedule: - - cron: '0 1 * * *' - push: - tags: - - "*" - workflow_dispatch: -jobs: - integration-tests: - name: Run integration tests - ${{ matrix.environment }} - runs-on: ${{ matrix.os }} - strategy: - # avoid cancellation of in-progress jobs if any matrix job fails - fail-fast: false - matrix: - python-version: [ 3.9 ] - os: [ "ubuntu-latest" ] - environment: [ "ibm-cloud-staging" ] - environment: ${{ matrix.environment }} - env: - QISKIT_IBM_TOKEN: ${{ secrets.QISKIT_IBM_TOKEN_QCTRL }} - QISKIT_IBM_URL: ${{ secrets.QISKIT_IBM_URL }} - QISKIT_IBM_INSTANCE: ${{ secrets.QISKIT_IBM_INSTANCE_QCTRL }} - CHANNEL_STRATEGY: q-ctrl - LOG_LEVEL: DEBUG - STREAM_LOG: True - QISKIT_IN_PARALLEL: True - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -c constraints.txt -r requirements-dev.txt -e . - - name: Run q-ctrl tests - run: python -m unittest test/qctrl/test_qctrl.py diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index cb3793365..f369f02ac 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -42,7 +42,6 @@ def __init__( instance: Optional[str] = None, proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = True, - channel_strategy: Optional[str] = None, ): """Account constructor. @@ -53,7 +52,6 @@ def __init__( instance: Service instance to use. proxies: Proxy configuration. verify: Whether to verify server's TLS certificate. - channel_strategy: Error mitigation strategy. """ self.channel: str = None self.url: str = None @@ -61,7 +59,6 @@ def __init__( self.instance = instance self.proxies = proxies self.verify = verify - self.channel_strategy = channel_strategy self.private_endpoint: bool = False def to_saved_format(self) -> dict: @@ -81,7 +78,6 @@ def from_saved_format(cls, data: dict) -> "Account": token = data.get("token") instance = data.get("instance") verify = data.get("verify", True) - channel_strategy = data.get("channel_strategy") private_endpoint = data.get("private_endpoint", False) return cls.create_account( channel=channel, @@ -90,7 +86,6 @@ def from_saved_format(cls, data: dict) -> "Account": instance=instance, proxies=proxies, verify=verify, - channel_strategy=channel_strategy, private_endpoint=private_endpoint, ) @@ -103,7 +98,6 @@ def create_account( instance: Optional[str] = None, proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = True, - channel_strategy: Optional[str] = None, private_endpoint: Optional[bool] = False, ) -> "Account": """Creates an account for a specific channel.""" @@ -114,7 +108,6 @@ def create_account( instance=instance, proxies=proxies, verify=verify, - channel_strategy=channel_strategy, ) elif channel == "ibm_cloud": return CloudAccount( @@ -123,7 +116,6 @@ def create_account( instance=instance, proxies=proxies, verify=verify, - channel_strategy=channel_strategy, private_endpoint=private_endpoint, ) else: @@ -167,20 +159,8 @@ def validate(self) -> "Account": self._assert_valid_url(self.url) self._assert_valid_instance(self.instance) self._assert_valid_proxies(self.proxies) - self._assert_valid_channel_strategy(self.channel_strategy) return self - @staticmethod - def _assert_valid_channel_strategy(channel_strategy: str) -> None: - """Assert that the channel strategy is valid.""" - # add more strategies as they are implemented - strategies = ["q-ctrl", "default"] - if channel_strategy and channel_strategy not in strategies: - raise InvalidAccountError( - f"Invalid `channel_strategy` value. Expected one of " - f"{strategies}, got '{channel_strategy}'." - ) - @staticmethod def _assert_valid_channel(channel: ChannelType) -> None: """Assert that the channel parameter is valid.""" @@ -229,7 +209,6 @@ def __init__( instance: Optional[str] = None, proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = True, - channel_strategy: Optional[str] = None, ): """Account constructor. @@ -239,9 +218,8 @@ def __init__( instance: Service instance to use. proxies: Proxy configuration. verify: Whether to verify server's TLS certificate. - channel_strategy: Error mitigation strategy. """ - super().__init__(token, instance, proxies, verify, channel_strategy) + super().__init__(token, instance, proxies, verify) resolved_url = url or IBM_QUANTUM_API_URL self.channel = "ibm_quantum" self.url = resolved_url @@ -272,7 +250,6 @@ def __init__( instance: Optional[str] = None, proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = True, - channel_strategy: Optional[str] = None, private_endpoint: Optional[bool] = False, ): """Account constructor. @@ -283,10 +260,9 @@ def __init__( instance: Service instance to use. proxies: Proxy configuration. verify: Whether to verify server's TLS certificate. - channel_strategy: Error mitigation strategy. private_endpoint: Connect to private API URL. """ - super().__init__(token, instance, proxies, verify, channel_strategy) + super().__init__(token, instance, proxies, verify) resolved_url = url or IBM_CLOUD_API_URL self.channel = "ibm_cloud" self.url = resolved_url diff --git a/qiskit_ibm_runtime/accounts/management.py b/qiskit_ibm_runtime/accounts/management.py index b6a84bd3c..4adc4406a 100644 --- a/qiskit_ibm_runtime/accounts/management.py +++ b/qiskit_ibm_runtime/accounts/management.py @@ -45,7 +45,6 @@ def save( proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = None, overwrite: Optional[bool] = False, - channel_strategy: Optional[str] = None, set_as_default: Optional[bool] = None, private_endpoint: Optional[bool] = False, ) -> None: @@ -61,7 +60,6 @@ def save( instance=instance, proxies=proxies, verify=verify, - channel_strategy=channel_strategy, private_endpoint=private_endpoint, ) return save_config( diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index 36f617ea7..494411dfd 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -61,7 +61,6 @@ def program_run( start_session: Optional[bool] = False, session_time: Optional[int] = None, private: Optional[bool] = False, - channel_strategy: Optional[str] = None, ) -> Dict: """Run the specified program. @@ -78,7 +77,6 @@ def program_run( 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: JSON response. @@ -99,7 +97,6 @@ def program_run( start_session=start_session, session_time=session_time, private=private, - channel_strategy=channel_strategy, **hgp_dict, ) @@ -274,27 +271,17 @@ def session_details(self, session_id: str) -> Dict[str, Any]: """ return self._api.runtime_session(session_id=session_id).details() - def list_backends( - self, hgp: Optional[str] = None, channel_strategy: Optional[str] = None - ) -> List[str]: + def list_backends(self, hgp: Optional[str] = None) -> List[str]: """Return IBM backends available for this service instance. Args: hgp: Filter by hub/group/project. - channel_strategy: Filter by channel strategy. Returns: IBM backends available for this service instance. """ - return self._api.backends(hgp=hgp, channel_strategy=channel_strategy)["devices"] - def is_qctrl_enabled(self) -> bool: - """Returns a boolean of whether or not the instance has q-ctrl enabled. - - Returns: - Boolean value. - """ - return self._api.is_qctrl_enabled() + return self._api.backends(hgp=hgp)["devices"] def backend_configuration(self, backend_name: str) -> Dict[str, Any]: """Return the configuration of the IBM backend. diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index c3fadd33a..45d39e77b 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -77,7 +77,6 @@ def program_run( start_session: Optional[bool] = False, session_time: Optional[int] = None, private: Optional[bool] = False, - channel_strategy: Optional[str] = None, ) -> Dict: """Execute the program. @@ -96,7 +95,6 @@ def program_run( 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: JSON response. @@ -125,8 +123,6 @@ def program_run( payload["hub"] = hub payload["group"] = group payload["project"] = project - if channel_strategy: - payload["channel_strategy"] = channel_strategy if private: payload["private"] = True data = json.dumps(payload, cls=RuntimeEncoder) @@ -216,14 +212,12 @@ def backends( self, hgp: Optional[str] = None, timeout: Optional[float] = None, - channel_strategy: Optional[str] = None, ) -> Dict[str, List[str]]: """Return a list of IBM backends. Args: hgp: The service instance to use, only for ``ibm_quantum`` channel, in h/g/p format. timeout: Number of seconds to wait for the request. - channel_strategy: Error mitigation strategy. Returns: JSON response. @@ -232,19 +226,8 @@ def backends( params = {} if hgp: params["provider"] = hgp - if channel_strategy: - params["channel_strategy"] = channel_strategy return self.session.get(url, params=params, timeout=timeout).json() - def is_qctrl_enabled(self) -> bool: - """Return boolean of whether or not the instance has q-ctrl enabled. - - Returns: - Boolean value. - """ - url = self.get_url("cloud_instance") - return self.session.get(url).json().get("qctrl_enabled") - def usage(self) -> Dict[str, Any]: """Return monthly open plan usage information. diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index 8ecdbc274..bbb6276d2 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -27,7 +27,6 @@ from .options.estimator_options import EstimatorOptions from .base_primitive import BasePrimitiveV2 from .utils.deprecation import issue_deprecation_msg -from .utils.qctrl import validate_v2 as qctrl_validate_v2 from .utils import validate_estimator_pubs # pylint: disable=unused-import,cyclic-import @@ -119,8 +118,6 @@ def __init__( options: Estimator options, see :class:`EstimatorOptions` for detailed description. - Raises: - NotImplementedError: If "q-ctrl" channel strategy is used. """ BaseEstimatorV2.__init__(self) Estimator.__init__(self) @@ -167,10 +164,6 @@ def _validate_options(self, options: dict) -> None: ValueError: if validation fails. """ - if self._service._channel_strategy == "q-ctrl": - qctrl_validate_v2(options) - return - if ( options.get("resilience", {}).get("pec_mitigation", False) is True and self._backend is not None diff --git a/qiskit_ibm_runtime/fake_provider/local_service.py b/qiskit_ibm_runtime/fake_provider/local_service.py index de0a6adfa..438c60ad7 100644 --- a/qiskit_ibm_runtime/fake_provider/local_service.py +++ b/qiskit_ibm_runtime/fake_provider/local_service.py @@ -49,7 +49,6 @@ def __init__(self) -> None: An instance of QiskitRuntimeService. """ - self._channel_strategy = None def backend( self, name: str = None, instance: str = None # pylint: disable=unused-argument diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index 319a00172..4089ad6fa 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -75,7 +75,6 @@ def __init__( instance: Optional[str] = None, proxies: Optional[dict] = None, verify: Optional[bool] = None, - channel_strategy: Optional[str] = None, private_endpoint: Optional[bool] = None, url_resolver: Optional[Callable[[str, str, Optional[bool]], str]] = None, ) -> None: @@ -115,7 +114,6 @@ def __init__( ``username_ntlm``, ``password_ntlm`` (username and password to enable NTLM user authentication) verify: Whether to verify the server's TLS certificate. - channel_strategy: (DEPRECATED) Error mitigation strategy. private_endpoint: Connect to private API URL. url_resolver: Function used to resolve the runtime url. @@ -127,20 +125,6 @@ def __init__( """ super().__init__() - if channel_strategy: - warnings.warn( - ( - "As of qiskit-ibm-runtime version 0.30.0, the channel_strategy parameter is " - "deprecated. Q-CTRL Performance Management strategy currently offered " - "on the Qiskit Runtime Service will be removed on 18 October, 2024. " - "To continue using Q-CTRL in your workflow, use one of the following options: " - "Qiskit Functions Catalog: https://quantum.ibm.com/functions, or " - "Fire Opal: https://q-ctrl.com/fire-opal" - ), - DeprecationWarning, - stacklevel=2, - ) - self._account = self._discover_account( token=token, url=url, @@ -150,7 +134,6 @@ def __init__( name=name, proxies=ProxyConfiguration(**proxies) if proxies else None, verify=verify, - channel_strategy=channel_strategy, ) if private_endpoint is not None: @@ -167,7 +150,6 @@ def __init__( url_resolver=url_resolver, ) - self._channel_strategy = channel_strategy or self._account.channel_strategy self._channel = self._account.channel self._backend_allowed_list: List[str] = [] self._url_resolver = url_resolver @@ -175,7 +157,6 @@ def __init__( if self._channel == "ibm_cloud": self._api_client = RuntimeClient(self._client_params) self._backend_allowed_list = self._discover_cloud_backends() - self._validate_channel_strategy() else: auth_client = self._authenticate_ibm_quantum_account(self._client_params) # Update client parameters to use authenticated values. @@ -209,19 +190,11 @@ def _discover_account( name: Optional[str] = None, proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = None, - channel_strategy: Optional[str] = None, ) -> Account: """Discover account.""" account = None verify_ = verify or True - if channel_strategy: - if channel_strategy not in ["q-ctrl", "default"]: - raise ValueError(f"{channel_strategy} is not a valid channel strategy.") - if channel and channel != "ibm_cloud": - raise ValueError( - f"The channel strategy {channel_strategy} is " - "only supported on the ibm_cloud channel." - ) + if name: if filename: if any([channel, token, url]): @@ -250,7 +223,6 @@ def _discover_account( instance=instance, proxies=proxies, verify=verify_, - channel_strategy=channel_strategy, ) else: if url: @@ -281,35 +253,13 @@ def _discover_account( return account - def _validate_channel_strategy(self) -> None: - """Raise an error if the passed in channel_strategy and - instance do not match. - - """ - qctrl_enabled = self._api_client.is_qctrl_enabled() - if self._channel_strategy == "q-ctrl": - if not qctrl_enabled: - raise IBMNotAuthorizedError( - "The instance passed in is not compatible with Q-CTRL channel strategy. " - "Please switch to or create an instance with the Q-CTRL strategy enabled. " - "See https://cloud.ibm.com/docs/quantum-computing?" - "topic=quantum-computing-get-started for more information" - ) - else: - if qctrl_enabled: - raise IBMNotAuthorizedError( - "The instance passed in is only compatible with Q-CTRL performance " - "management strategy. " - "To use this instance, set channel_strategy='q-ctrl'." - ) - def _discover_cloud_backends(self) -> List[str]: """Return the remote backends available for this service instance. Returns: A list of the remote backend names. """ - return self._api_client.list_backends(channel_strategy=self._channel_strategy) + return self._api_client.list_backends() def _resolve_crn(self, account: Account) -> None: account.resolve_crn() @@ -691,7 +641,6 @@ def save_account( proxies: Optional[dict] = None, verify: Optional[bool] = None, overwrite: Optional[bool] = False, - channel_strategy: Optional[str] = None, set_as_default: Optional[bool] = None, private_endpoint: Optional[bool] = False, ) -> None: @@ -713,26 +662,11 @@ def save_account( authentication) verify: Verify the server's TLS certificate. overwrite: ``True`` if the existing account is to be overwritten. - channel_strategy: (DEPRECATED) Error mitigation strategy. set_as_default: If ``True``, the account is saved in filename, as the default account. private_endpoint: Connect to private API URL. """ - if channel_strategy: - warnings.warn( - ( - "As of qiskit-ibm-runtime version 0.30.0, the channel_strategy parameter is " - "deprecated. Q-CTRL Performance Management strategy currently offered " - "on the Qiskit Runtime Service will be removed on 18 October, 2024." - "To continue using Q-CTRL in your workflow, use one of the following options: " - "Qiskit Functions Catalog: https://quantum.ibm.com/functions, or " - "Fire Opal: https://q-ctrl.com/fire-opal" - ), - DeprecationWarning, - stacklevel=2, - ) - AccountManager.save( token=token, url=url, @@ -743,7 +677,6 @@ def save_account( proxies=ProxyConfiguration(**proxies) if proxies else None, verify=verify, overwrite=overwrite, - channel_strategy=channel_strategy, set_as_default=set_as_default, private_endpoint=private_endpoint, ) @@ -908,9 +841,6 @@ def _run( 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 - ), ) if self._channel == "ibm_quantum": messages = response.get("messages") @@ -1247,5 +1177,4 @@ def __eq__(self, other: Any) -> bool: self._channel == other._channel and self._account.instance == other._account.instance and self._account.token == other._account.token - and self._channel_strategy == other._channel_strategy ) diff --git a/qiskit_ibm_runtime/sampler.py b/qiskit_ibm_runtime/sampler.py index d2391aaee..34650896a 100644 --- a/qiskit_ibm_runtime/sampler.py +++ b/qiskit_ibm_runtime/sampler.py @@ -29,8 +29,7 @@ # pylint: disable=unused-import,cyclic-import from .session import Session from .batch import Batch -from .utils.deprecation import deprecate_arguments, issue_deprecation_msg -from .utils.qctrl import validate_v2 as qctrl_validate_v2 +from .utils.deprecation import issue_deprecation_msg from .utils import validate_classical_registers from .options import SamplerOptions @@ -80,8 +79,6 @@ def __init__( options: Sampler options, see :class:`SamplerOptions` for detailed description. - Raises: - NotImplementedError: If "q-ctrl" channel strategy is used. """ self.options: SamplerOptions BaseSamplerV2.__init__(self) @@ -129,9 +126,7 @@ def _validate_options(self, options: dict) -> None: ValidationError: if validation fails. """ - if self._service._channel_strategy == "q-ctrl": - qctrl_validate_v2(options) - return + pass @classmethod def _program_id(cls) -> str: diff --git a/qiskit_ibm_runtime/utils/qctrl.py b/qiskit_ibm_runtime/utils/qctrl.py deleted file mode 100644 index 4210d0711..000000000 --- a/qiskit_ibm_runtime/utils/qctrl.py +++ /dev/null @@ -1,143 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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. - -"""Qctrl validation functions and helpers.""" - -import logging -from typing import Any, Optional, Dict, List - -from ..options import EnvironmentOptions, SimulatorOptions -from ..options.utils import UnsetType - -logger = logging.getLogger(__name__) - - -def validate_v2(options: Dict[str, Any]) -> None: - """Validates the options for qctrl""" - - # Raise error on bad options. - _raise_if_error_in_options_v2(options) - # Override options and warn. - _warn_and_clean_options_v2(options) - - # Default validation otherwise. - - EnvironmentOptions(**options.get("environment", {})) - # ExecutionOptions(**options.get("execution", {})) - SimulatorOptions(**options.get("simulator", {})) - - -def _raise_if_error_in_options_v2(options: Dict[str, Any]) -> None: - """Checks for settings that produce errors and raise a ValueError""" - - # Fail on resilience_level set to 0 - resilience_level = options.get("resilience_level", 1) - if isinstance(resilience_level, UnsetType): - resilience_level = 1 - _check_argument( - resilience_level > 0, - description=( - "Q-CTRL Primitives do not support resilience level 0. Please " - "set resilience_level to 1 and re-try" - ), - arguments={}, - ) - - optimization_level = options.get("optimization_level", 1) - if isinstance(optimization_level, UnsetType): - optimization_level = 1 - _check_argument( - optimization_level > 0, - description="Q-CTRL Primitives do not support optimization level 0. Please\ - set optimization_level to 1 and re-try", - arguments={}, - ) - - -def _warn_and_clean_options_v2(options: Dict[str, Any]) -> None: - """ - Validate and update transpilation settings - """ - # Issue a warning and override if any of these setting is not None - # or a different value than the default below - expected_options = { - "optimization_level": 1, - "resilience_level": 1, - "resilience": { - "measure_mitigation": None, - "measure_noise_learning": None, - "zne_mitigation": None, - "zne": None, - "pec_mitigation": None, - "pec": None, - "layer_noise_learning": None, - }, - "twirling": None, - "dynamical_decoupling": None, - } - - # Collect keys with mis-matching values - different_keys = _validate_values(expected_options, options) - # Override options - _update_values(expected_options, options) - if different_keys: - logger.warning( - "The following settings cannot be customized and will be overwritten: %s", - ",".join(sorted(different_keys)), - ) - - -def _validate_values( - expected_options: Dict[str, Any], current_options: Optional[Dict[str, Any]] -) -> List[str]: - """Validates expected_options and current_options have the same values if the - keys of expected_options are present in current_options""" - - if current_options is None: - return [] - - different_keys = [] - for expected_key, expected_value in expected_options.items(): - if isinstance(expected_value, dict): - different_keys.extend( - _validate_values(expected_value, current_options.get(expected_key, None)) - ) - else: - current_value = current_options.get(expected_key, None) - if (current_value not in (None, UnsetType)) and expected_value != current_value: - different_keys.append(expected_key) - return different_keys - - -def _update_values( - expected_options: Dict[str, Any], current_options: Optional[Dict[str, Any]] -) -> None: - - if current_options is None: - return - - for expected_key, expected_value in expected_options.items(): - if isinstance(expected_value, dict): - _update_values(expected_value, current_options.get(expected_key, None)) - else: - if expected_key in current_options: - current_options[expected_key] = expected_value - - -def _check_argument( - condition: bool, - description: str, - arguments: Dict[str, str], -) -> None: - if not condition: - error_str = f"{description} arguments={arguments}" - raise ValueError(error_str) diff --git a/release-notes/unreleased/1966.other.rst b/release-notes/unreleased/1966.other.rst new file mode 100644 index 000000000..440dec91d --- /dev/null +++ b/release-notes/unreleased/1966.other.rst @@ -0,0 +1,11 @@ +The ``channel_strategy`` parameter in ``QiskitRuntimeService`` has been removed. +To continue using Q-CTRL in your workflow, please explore the following options: + + * If your organization has an existing IBM Quantum Premium Plan instance: migrate to + the Q-CTRL Performance Management Function, found in the + `Qiskit Functions Catalog `__. + + * To continue using Qiskit Runtime with IBM Cloud: migrate to Q-CTRL Fire Opal, + the same performance management product accessible directly through Q-CTRL. + You can `connect your IBM Cloud API key and Qiskit Runtime CRN `__ + to Fire Opal. \ No newline at end of file diff --git a/test/decorators.py b/test/decorators.py index 32cc0c464..0ed3d3184 100644 --- a/test/decorators.py +++ b/test/decorators.py @@ -69,15 +69,14 @@ def _wrapper(self, *args, **kwargs): def _get_integration_test_config(): - token, url, instance, qpu, channel_strategy = ( + token, url, instance, qpu = ( os.getenv("QISKIT_IBM_TOKEN"), os.getenv("QISKIT_IBM_URL"), os.getenv("QISKIT_IBM_INSTANCE"), os.getenv("QISKIT_IBM_QPU"), - os.getenv("CHANNEL_STRATEGY"), ) channel: Any = "ibm_quantum" if url.find("quantum-computing.ibm.com") >= 0 else "ibm_cloud" - return channel, token, url, instance, qpu, channel_strategy + return channel, token, url, instance, qpu def run_integration_test(func): @@ -117,7 +116,7 @@ def _wrapper(self, *args, **kwargs): ["ibm_cloud", "ibm_quantum"] if supported_channel is None else supported_channel ) - channel, token, url, instance, qpu, channel_strategy = _get_integration_test_config() + channel, token, url, instance, qpu = _get_integration_test_config() if not all([channel, token, url]): raise Exception("Configuration Issue") # pylint: disable=broad-exception-raised @@ -133,7 +132,6 @@ def _wrapper(self, *args, **kwargs): channel=channel, token=token, url=url, - channel_strategy=channel_strategy, ) dependencies = IntegrationTestDependencies( channel=channel, @@ -142,7 +140,6 @@ def _wrapper(self, *args, **kwargs): instance=instance, qpu=qpu, service=service, - channel_strategy=channel_strategy, ) kwargs["dependencies"] = dependencies func(self, *args, **kwargs) @@ -162,7 +159,6 @@ class IntegrationTestDependencies: token: str channel: str url: str - channel_strategy: str def integration_test_setup_with_backend( diff --git a/test/integration/test_account.py b/test/integration/test_account.py index 829f33f3d..521509a60 100644 --- a/test/integration/test_account.py +++ b/test/integration/test_account.py @@ -25,7 +25,6 @@ get_resource_controller_api_url, get_iam_api_url, ) -from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError from ..ibm_test_case import IBMIntegrationTestCase from ..decorators import IntegrationTestDependencies @@ -53,28 +52,6 @@ def _skip_on_ibm_quantum(self): if self.dependencies.channel == "ibm_quantum": self.skipTest("Not supported on ibm_quantum") - def test_channel_strategy(self): - """Test passing in a channel strategy.""" - self._skip_on_ibm_quantum() - # test when channel strategy not supported by instance - with self.assertRaises(IBMNotAuthorizedError): - QiskitRuntimeService( - channel="ibm_cloud", - url=self.dependencies.url, - token=self.dependencies.token, - instance=self.dependencies.instance, - channel_strategy="q-ctrl", - ) - # test passing in default - service = QiskitRuntimeService( - channel="ibm_cloud", - url=self.dependencies.url, - token=self.dependencies.token, - instance=self.dependencies.instance, - channel_strategy="default", - ) - self.assertTrue(service) - def test_local_channel(self): """Test local channel mode""" local_service = QiskitRuntimeService( @@ -85,7 +62,6 @@ def test_local_channel(self): url=self.dependencies.url, token=self.dependencies.token, instance=self.dependencies.instance, - channel_strategy="default", ) self.assertIsInstance(local_service, QiskitRuntimeLocalService) self.assertIsInstance(local_service1, QiskitRuntimeLocalService) diff --git a/test/qctrl/test_qctrl.py b/test/qctrl/test_qctrl.py deleted file mode 100644 index c56414247..000000000 --- a/test/qctrl/test_qctrl.py +++ /dev/null @@ -1,113 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# 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 for job functions using real runtime service.""" - -from qiskit.quantum_info import SparsePauliOp -from qiskit.circuit.library import RealAmplitudes -from qiskit.primitives.containers import PrimitiveResult, PubResult, DataBin, BitArray -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager - -from qiskit_ibm_runtime import ( - SamplerV2, - EstimatorV2, - Batch, -) - - -from ..ibm_test_case import IBMIntegrationTestCase -from ..decorators import run_integration_test -from ..utils import bell - -FIDELITY_THRESHOLD = 0.8 -DIFFERENCE_THRESHOLD = 0.35 - - -class TestV2PrimitivesQCTRL(IBMIntegrationTestCase): - """Integration tests for V2 primitives using QCTRL.""" - - def setUp(self) -> None: - super().setUp() - self.bell = bell() - self.backend = self.service.least_busy(simulator=False) - - @run_integration_test - def test_sampler_v2_qctrl(self, service): - """Test qctrl bell state with samplerV2""" - shots = 1 - - pm = generate_preset_pass_manager(backend=self.backend, optimization_level=1) - isa_circuit = pm.run(self.bell) - - with Batch(service, backend=self.backend): - sampler = SamplerV2() - - result = sampler.run([isa_circuit], shots=shots).result() - self._verify_sampler_result(result, num_pubs=1) - - @run_integration_test - def test_estimator_v2_qctrl(self, service): - """Test simple circuit with estimatorV2 using qctrl.""" - pass_mgr = generate_preset_pass_manager(backend=self.backend, optimization_level=1) - - psi1 = pass_mgr.run(RealAmplitudes(num_qubits=2, reps=2)) - psi2 = pass_mgr.run(RealAmplitudes(num_qubits=2, reps=3)) - - # pylint: disable=invalid-name - H1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]).apply_layout(psi1.layout) - H2 = SparsePauliOp.from_list([("IZ", 1)]).apply_layout(psi2.layout) - H3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]).apply_layout(psi1.layout) - - theta1 = [0, 1, 1, 2, 3, 5] - theta2 = [0, 1, 1, 2, 3, 5, 8, 13] - theta3 = [1, 2, 3, 4, 5, 6] - - with Batch(service, self.backend): - estimator = EstimatorV2() - - job = estimator.run([(psi1, H1, [theta1])]) - result = job.result() - self._verify_estimator_result(result, num_pubs=1, shapes=[(1,)]) - - job2 = estimator.run([(psi1, [H1, H3], [theta1, theta3]), (psi2, H2, theta2)]) - result2 = job2.result() - self._verify_estimator_result(result2, num_pubs=2, shapes=[(2,), (1,)]) - - job3 = estimator.run([(psi1, H1, theta1), (psi2, H2, theta2), (psi1, H3, theta3)]) - result3 = job3.result() - self._verify_estimator_result(result3, num_pubs=3, shapes=[(1,), (1,), (1,)]) - - def _verify_sampler_result(self, result, num_pubs, targets=None): - """Verify result type.""" - self.assertIsInstance(result, PrimitiveResult) - self.assertIsInstance(result.metadata, dict) - self.assertEqual(len(result), num_pubs) - for idx, pub_result in enumerate(result): - # TODO: We need to update the following test to check `SamplerPubResult` - # when the server side is upgraded to Qiskit 1.1. - self.assertIsInstance(pub_result, PubResult) - self.assertIsInstance(pub_result.data, DataBin) - self.assertIsInstance(pub_result.metadata, dict) - if targets: - self.assertIsInstance(result[idx].data.meas, BitArray) - self._assert_allclose(result[idx].data.meas, targets[idx]) - - def _verify_estimator_result(self, result, num_pubs, shapes): - """Verify result type.""" - self.assertIsInstance(result, PrimitiveResult) - self.assertEqual(len(result), num_pubs) - for idx, pub_result in enumerate(result): - self.assertIsInstance(pub_result, PubResult) - self.assertIsInstance(pub_result.data, DataBin) - self.assertTrue(pub_result.metadata) - self.assertEqual(pub_result.data.evs.shape, shapes[idx]) - self.assertEqual(pub_result.data.stds.shape, shapes[idx]) diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py index 3738c6684..3ace2145d 100644 --- a/test/unit/mock/fake_runtime_client.py +++ b/test/unit/mock/fake_runtime_client.py @@ -105,7 +105,6 @@ def __init__( session_id=None, max_execution_time=None, start_session=None, - channel_strategy=None, ): """Initialize a fake job.""" self._job_id = job_id @@ -131,7 +130,6 @@ def __init__( elif final_status == "COMPLETED": self._result = json.dumps({"quasi_dists": [{0: 0.5, 3: 0.5}], "metadata": []}) self._final_status = final_status - self._channel_strategy = channel_strategy def _auto_progress(self): """Automatically update job status.""" @@ -292,10 +290,6 @@ def set_job_classes(self, classes): classes = [classes] self._job_classes = classes - def is_qctrl_enabled(self): - """Return whether or not channel_strategy q-ctrl is enabled.""" - return False - def set_final_status(self, final_status): """Set job status to passed in final status instantly.""" self._final_status = final_status @@ -314,7 +308,6 @@ def program_run( 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.""" job_id = uuid.uuid4().hex @@ -344,7 +337,6 @@ def program_run( job_tags=job_tags, max_execution_time=max_execution_time, start_session=start_session, - channel_strategy=channel_strategy, **self._job_kwargs, ) self.session_time = session_time @@ -440,10 +432,7 @@ def _get_job(self, job_id: str, exclude_params: bool = None) -> Any: raise RequestsApiError("Job not found", status_code=404) return self._jobs[job_id] - # pylint: disable=unused-argument - def list_backends( - self, hgp: Optional[str] = None, channel_strategy: Optional[str] = None - ) -> List[str]: + def list_backends(self, hgp: Optional[str] = None) -> List[str]: """Return IBM backends available for this service instance.""" return [back.name for back in self._backends if back.has_access(hgp)] diff --git a/test/unit/test_account.py b/test/unit/test_account.py index eed11f982..02ff03b4d 100644 --- a/test/unit/test_account.py +++ b/test/unit/test_account.py @@ -128,22 +128,6 @@ def test_invalid_instance(self): ).validate() self.assertIn("Invalid `instance` value.", str(err.exception)) - def test_invalid_channel_strategy(self): - """Test invalid values for channel_strategy""" - subtests = [ - {"channel": "ibm_cloud", "channel_strategy": "test"}, - ] - for params in subtests: - with self.subTest(params=params): - with self.assertRaises(InvalidAccountError) as err: - Account.create_account( - **params, - token=self.dummy_token, - url=self.dummy_ibm_cloud_url, - instance="crn:v1:bluemix:public:quantum-computing:us-east:a/...::", - ).validate() - self.assertIn("Invalid `channel_strategy` value.", str(err.exception)) - def test_invalid_proxy_config(self): """Test invalid values for proxy configuration.""" @@ -784,17 +768,6 @@ def test_enable_account_bad_channel(self): _ = FakeRuntimeService(channel=channel) self.assertIn("channel", str(err.exception)) - def test_enable_account_bad_channel_strategy(self): - """Test initializing account by bad channel strategy.""" - subtests = [ - {"channel_strategy": "q-ctrl", "channel": "ibm_quantum"}, - {"channel_strategy": "test"}, - ] - for test in subtests: - with temporary_account_config_file() as _, self.assertRaises(ValueError) as err: - _ = FakeRuntimeService(**test) - self.assertIn("channel", str(err.exception)) - def test_enable_account_by_name_pref(self): """Test initializing account by name and preferences.""" name = "foo" diff --git a/test/unit/test_ibm_primitives_v2.py b/test/unit/test_ibm_primitives_v2.py index fb63ea61f..d3c9d730d 100644 --- a/test/unit/test_ibm_primitives_v2.py +++ b/test/unit/test_ibm_primitives_v2.py @@ -674,49 +674,9 @@ def test_qctrl_supported_values_for_options_estimator(self): {"dynamical_decoupling": {"sequence_type": "XY4"}}, ] session = get_mocked_session() - session.service._channel_strategy = "q-ctrl" session.service.backend().configuration().simulator = False for options in options_good: with self.subTest(msg=f"EstimatorV2, {options}"): print(options) inst = EstimatorV2(mode=session, options=options) _ = inst.run(**get_primitive_inputs(inst)) - - def test_qctrl_supported_values_for_options_sampler(self): - """Test exception when options levels not supported for Sampler V2.""" - - options_good = [ - # Minimum working settings - {}, - # Dynamical_decoupling (issue warning) - {"dynamical_decoupling": {"sequence_type": "XY4"}}, - ] - session = get_mocked_session() - session.service._channel_strategy = "q-ctrl" - session.service.backend().configuration().simulator = False - for options in options_good: - with self.subTest(msg=f"SamplerV2, {options}"): - print(options) - inst = SamplerV2(mode=session, options=options) - _ = inst.run(**get_primitive_inputs(inst)) - - def test_qctrl_unsupported_values_for_options(self): - """Test exception when options levels are not supported.""" - options_bad = [ - # Bad resilience levels - ({"resilience_level": 0}, "resilience level"), - # Bad optimization level - ({"optimization_level": 0}, "optimization level"), - ] - session = get_mocked_session() - session.service._channel_strategy = "q-ctrl" - session.service.backend().configuration().simulator = False - primitives = [SamplerV2, EstimatorV2] - for cls in primitives: - for bad_opt, expected_message in options_bad: - with self.subTest(msg=bad_opt): - with self.assertRaises(ValueError) as exc: - inst = cls(mode=session, options=bad_opt) - _ = inst.run(**get_primitive_inputs(inst)) - - self.assertIn(expected_message, str(exc.exception)) diff --git a/test/utils.py b/test/utils.py index 5e6c91a41..735a66a10 100644 --- a/test/utils.py +++ b/test/utils.py @@ -314,7 +314,6 @@ def get_mocked_backend( """Return a mock backend.""" mock_service = mock.MagicMock(spec=QiskitRuntimeService) - mock_service._channel_strategy = None mock_api_client = mock.MagicMock() mock_service._api_client = mock_api_client