From 1f4c96375ea374921d05e254c927b0a013369f77 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Mon, 14 Aug 2023 15:38:49 -0400 Subject: [PATCH 1/6] q-ctrl support --- qiskit_ibm_runtime/accounts/account.py | 16 +++++++++++++++- qiskit_ibm_runtime/accounts/management.py | 2 ++ qiskit_ibm_runtime/api/clients/runtime.py | 3 +++ qiskit_ibm_runtime/api/rest/runtime.py | 5 +++++ qiskit_ibm_runtime/qiskit_runtime_service.py | 17 +++++++++++++++++ 5 files changed, 42 insertions(+), 1 deletion(-) diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index 7d0e3b5bb..72893bd22 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -44,6 +44,7 @@ def __init__( instance: Optional[str] = None, proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = True, + channel_strategy: Optional[str] = None, ): """Account constructor. @@ -54,17 +55,18 @@ def __init__( instance: Service instance to use. proxies: Proxy configuration. verify: Whether to verify server's TLS certificate. + channel_strategy: Error mitigation strategy. """ resolved_url = url or ( IBM_QUANTUM_API_URL if channel == "ibm_quantum" else IBM_CLOUD_API_URL ) - self.channel = channel self.token = token self.url = resolved_url self.instance = instance self.proxies = proxies self.verify = verify + self.channel_strategy = channel_strategy def to_saved_format(self) -> dict: """Returns a dictionary that represents how the account is saved on disk.""" @@ -84,6 +86,7 @@ def from_saved_format(cls, data: dict) -> "Account": instance=data.get("instance"), proxies=ProxyConfiguration(**proxies) if proxies else None, verify=data.get("verify", True), + channel_strategy=data.get("channel_strategy"), ) def resolve_crn(self) -> None: @@ -155,8 +158,19 @@ def validate(self) -> "Account": self._assert_valid_url(self.url) self._assert_valid_instance(self.channel, 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 + if channel_strategy and channel_strategy not in ["q-ctrl"]: + raise InvalidAccountError( + f"Invalid `channel_strategy` value. Expected one of " + f"{['q-ctrl']}, got '{channel_strategy}'." + ) + @staticmethod def _assert_valid_channel(channel: ChannelType) -> None: """Assert that the channel parameter is valid.""" diff --git a/qiskit_ibm_runtime/accounts/management.py b/qiskit_ibm_runtime/accounts/management.py index 151563b81..6e80c9e03 100644 --- a/qiskit_ibm_runtime/accounts/management.py +++ b/qiskit_ibm_runtime/accounts/management.py @@ -49,6 +49,7 @@ def save( proxies: Optional[ProxyConfiguration] = None, verify: Optional[bool] = None, overwrite: Optional[bool] = False, + channel_strategy: Optional[str] = None, ) -> None: """Save account on disk.""" cls.migrate(filename=filename) @@ -67,6 +68,7 @@ def save( channel=channel, proxies=proxies, verify=verify, + channel_strategy=channel_strategy, ) # avoid storing invalid accounts .validate().to_saved_format(), diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index 2450c9bde..0176cd849 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -128,6 +128,7 @@ def program_run( max_execution_time: Optional[int] = None, start_session: Optional[bool] = False, session_time: Optional[int] = None, + channel_strategy: Optional[str] = None, ) -> Dict: """Run the specified program. @@ -143,6 +144,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. + channel_strategy: Error mitigation strategy. Returns: JSON response. @@ -162,6 +164,7 @@ def program_run( max_execution_time=max_execution_time, start_session=start_session, session_time=session_time, + 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 f11e752e6..f6205a16e 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -138,6 +138,7 @@ def program_run( max_execution_time: Optional[int] = None, start_session: Optional[bool] = False, session_time: Optional[int] = None, + channel_strategy: Optional[str] = None, ) -> Dict: """Execute the program. @@ -155,6 +156,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. + channel_strategy: Error mitigation strategy. Returns: JSON response. @@ -183,6 +185,9 @@ def program_run( payload["hub"] = hub payload["group"] = group payload["project"] = project + if channel_strategy: + # TODO this will be changed to "channel_strategy" + payload["performance_strategy"] = channel_strategy data = json.dumps(payload, cls=RuntimeEncoder) return self.session.post(url, data=data, timeout=900).json() diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index f7a9ec91b..1e89afa9b 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -128,6 +128,7 @@ def __init__( instance: Optional[str] = None, proxies: Optional[dict] = None, verify: Optional[bool] = None, + channel_strategy: Optional[str] = None, ) -> None: """QiskitRuntimeService constructor @@ -179,6 +180,7 @@ def __init__( name=name, proxies=ProxyConfiguration(**proxies) if proxies else None, verify=verify, + channel_strategy=channel_strategy, ) self._client_params = ClientParameters( @@ -190,6 +192,7 @@ def __init__( verify=self._account.verify, ) + self._channel_strategy = channel_strategy self._channel = self._account.channel self._programs: Dict[str, RuntimeProgram] = {} self._backends: Dict[str, "ibm_backend.IBMBackend"] = {} @@ -227,10 +230,19 @@ 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"]: + raise IBMInputValueError(f"{channel_strategy} is not a valid channel strategy.") + if channel and channel != "ibm_cloud": + raise IBMInputValueError( + f"The channel strategy {channel_strategy} is " + "only supported on the ibm_cloud channel." + ) if name: if filename: if any([auth, channel, token, url]): @@ -262,6 +274,7 @@ def _discover_account( instance=instance, proxies=proxies, verify=verify_, + channel_strategy=channel_strategy, ) else: if url: @@ -681,6 +694,7 @@ def save_account( proxies: Optional[dict] = None, verify: Optional[bool] = None, overwrite: Optional[bool] = False, + channel_strategy: Optional[str] = None, ) -> None: """Save the account to disk for future use. @@ -700,6 +714,7 @@ def save_account( authentication) verify: Verify the server's TLS certificate. overwrite: ``True`` if the existing account is to be overwritten. + channel_strategy: Error mitigation strategy. """ AccountManager.save( @@ -712,6 +727,7 @@ def save_account( proxies=ProxyConfiguration(**proxies) if proxies else None, verify=verify, overwrite=overwrite, + channel_strategy=channel_strategy, ) @staticmethod @@ -1002,6 +1018,7 @@ def run( max_execution_time=qrt_options.max_execution_time, start_session=start_session, session_time=qrt_options.session_time, + channel_strategy=self._channel_strategy or self._account.channel_strategy, ) except RequestsApiError as ex: if ex.status_code == 404: From 0e4a266d42b60be27a9464ca973885eb27ebd72f Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Mon, 14 Aug 2023 15:57:08 -0400 Subject: [PATCH 2/6] add reno, fix unit test --- releasenotes/notes/q-ctrl-support-157170386477dfbd.yaml | 9 +++++++++ test/unit/mock/fake_runtime_client.py | 4 ++++ 2 files changed, 13 insertions(+) create mode 100644 releasenotes/notes/q-ctrl-support-157170386477dfbd.yaml diff --git a/releasenotes/notes/q-ctrl-support-157170386477dfbd.yaml b/releasenotes/notes/q-ctrl-support-157170386477dfbd.yaml new file mode 100644 index 000000000..7adf9eb74 --- /dev/null +++ b/releasenotes/notes/q-ctrl-support-157170386477dfbd.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + There is a new parameter, ``channel_strategy`` that can be set in the initialization of + :class:`qiskit_ibm_runtime.QiskitRuntimeService` or saved in + :meth:`qiskit_ibm_runtime.QiskitRuntimeService.save_account`. If ``channel_strategy`` + is set to ``q-ctrl``, all jobs within the service will use + the Q-CTRL error mitigation strategy. + diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py index 989905ef9..e59052cc0 100644 --- a/test/unit/mock/fake_runtime_client.py +++ b/test/unit/mock/fake_runtime_client.py @@ -104,6 +104,7 @@ def __init__( session_id=None, max_execution_time=None, start_session=None, + channel_strategy=None, ): """Initialize a fake job.""" self._job_id = job_id @@ -129,6 +130,7 @@ def __init__( elif final_status == "COMPLETED": self._result = json.dumps("foo") self._final_status = final_status + self._channel_strategy = channel_strategy def _auto_progress(self): """Automatically update job status.""" @@ -365,6 +367,7 @@ def program_run( max_execution_time: Optional[int] = None, start_session: Optional[bool] = None, session_time: Optional[int] = None, + channel_strategy: Optional[str] = None, ) -> Dict[str, Any]: """Run the specified program.""" _ = self._get_program(program_id) @@ -396,6 +399,7 @@ 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 From 37879d90e052fd97bee9a401df7b931a05850d05 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Mon, 14 Aug 2023 23:20:44 -0400 Subject: [PATCH 3/6] add unit tests --- qiskit_ibm_runtime/qiskit_runtime_service.py | 4 +-- test/unit/test_account.py | 28 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index 1e89afa9b..885cdf48d 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -237,9 +237,9 @@ def _discover_account( verify_ = verify or True if channel_strategy: if channel_strategy not in ["q-ctrl"]: - raise IBMInputValueError(f"{channel_strategy} is not a valid channel strategy.") + raise ValueError(f"{channel_strategy} is not a valid channel strategy.") if channel and channel != "ibm_cloud": - raise IBMInputValueError( + raise ValueError( f"The channel strategy {channel_strategy} is " "only supported on the ibm_cloud channel." ) diff --git a/test/unit/test_account.py b/test/unit/test_account.py index e3e5879cc..dce40f931 100644 --- a/test/unit/test_account.py +++ b/test/unit/test_account.py @@ -150,6 +150,22 @@ 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( + **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.""" @@ -275,6 +291,7 @@ def test_save_channel_ibm_cloud_over_auth_cloud_with_overwrite(self): proxies=_TEST_IBM_CLOUD_ACCOUNT.proxies, name=None, overwrite=True, + channel_strategy="q-ctrl", ) self.assertEqual(_TEST_IBM_CLOUD_ACCOUNT, AccountManager.get(channel="ibm_cloud")) @@ -793,6 +810,17 @@ 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" From 50d2644885929c89eef4017f9fe2a202a5174c75 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Tue, 15 Aug 2023 14:48:18 -0400 Subject: [PATCH 4/6] change payload name to channel_strategy --- qiskit_ibm_runtime/api/rest/runtime.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index f6205a16e..1c9866883 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -186,8 +186,7 @@ def program_run( payload["group"] = group payload["project"] = project if channel_strategy: - # TODO this will be changed to "channel_strategy" - payload["performance_strategy"] = channel_strategy + payload["channel_strategy"] = channel_strategy data = json.dumps(payload, cls=RuntimeEncoder) return self.session.post(url, data=data, timeout=900).json() From 20460f5b6920ea9123d7eb27192c89b3b2e364aa Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Tue, 22 Aug 2023 14:35:00 -0400 Subject: [PATCH 5/6] init channel strategy correctly --- qiskit_ibm_runtime/qiskit_runtime_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index 885cdf48d..2d6b2806e 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -192,7 +192,7 @@ def __init__( verify=self._account.verify, ) - self._channel_strategy = channel_strategy + self._channel_strategy = channel_strategy or self._account.channel_strategy self._channel = self._account.channel self._programs: Dict[str, RuntimeProgram] = {} self._backends: Dict[str, "ibm_backend.IBMBackend"] = {} @@ -1018,7 +1018,7 @@ def run( max_execution_time=qrt_options.max_execution_time, start_session=start_session, session_time=qrt_options.session_time, - channel_strategy=self._channel_strategy or self._account.channel_strategy, + channel_strategy=self._channel_strategy, ) except RequestsApiError as ex: if ex.status_code == 404: From f664de80bc80251ac4d01b20f91e69bb69482c71 Mon Sep 17 00:00:00 2001 From: kevin-tian Date: Mon, 28 Aug 2023 13:56:29 -0400 Subject: [PATCH 6/6] add docstring --- 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 a63eb2fb6..e56cd3818 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -164,6 +164,7 @@ 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: Error mitigation strategy. Returns: An instance of QiskitRuntimeService.