diff --git a/README.md b/README.md index 892e7d493..5a2b8cb15 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ All quantum applications and algorithms level are fundamentally built using thre The IBM Runtime service offers these primitives with additional features, such as built-in error suppression and mitigation. -There are several different options you can specify when calling the primitives. See [`qiskit_ibm_runtime.Options`](https://github.com/Qiskit/qiskit-ibm-runtime/blob/main/qiskit_ibm_runtime/options.py#L103) class for more information. +There are several different options you can specify when calling the primitives. See [`qiskit_ibm_runtime.Options`](https://github.com/Qiskit/qiskit-ibm-runtime/blob/main/qiskit_ibm_runtime/options/options.py#L33) class for more information. ### Sampler @@ -256,6 +256,35 @@ with Session(service=service, backend="ibmq_qasm_simulator") as session: This code returns `Job result is [4.] at theta = 1.575674623307102` using only nine iterations. This is a very powerful extension to the primitives. However, using too much code between iterative calls can lock the QPU and use excessive QPU time, which is expensive. We recommend only using sessions when needed. The Sampler can also be used within a session, but there are not any well-defined examples for this. +## Instances + +Access to IBM Quantum Platform services is controlled by the instances (previously called providers) to which you are assigned. An instance is defined by a hierarchical organization of hub, group, and project. A hub is the top level of a given hierarchy (organization) and contains within it one or more groups. These groups are in turn populated with projects. The combination of hub/group/project is called an instance. Users can belong to more than one instance at any time. + +> **_NOTE:_** IBM Cloud instances are different from IBM Quantum Platform instances. IBM Cloud does not use the hub/group/project structure for user management. To view and create IBM Cloud instances, visit the [IBM Cloud Quantum Instances page](https://cloud.ibm.com/quantum/instances). + +To view a list of your instances, visit your [account settings page](https://www-dev.quantum-computing.ibm.com/account) or use the `instances()` method. + +You can specify an instance when initializing the service or provider, or when picking a backend: + +```python + +# Optional: Specify the instance at service level. This becomes the default unless overwritten. +service = QiskitRuntimeService(channel='ibm_quantum', instance="hub1/group1/project1") +backend1 = service.backend("ibmq_manila") + +# Optional: Specify the instance at the backend level, which overwrites the service-level specification when this backend is used. +backend2 = service.backend("ibmq_manila", instance="hub2/group2/project2") + +sampler1 = Sampler(backend=backend1) # this will use hub1/group1/project1 +sampler2 = Sampler(backend=backend2) # this will use hub2/group2/project2 +``` + +If you do not specify an instance, then the code will select one in the following order: + +1. If your account only has access to one instance, it is selected by default. +2. If your account has access to multiple instances, but only one can access the requested backend, the instance with access is selected. +3. In all other cases, the code selects the first instance other than ibm-q/open/main that has access to the backend. + ## Access your IBM Quantum backends A **backend** is a quantum device or simulator capable of running quantum circuits or pulse schedules. diff --git a/qiskit_ibm_runtime/accounts/management.py b/qiskit_ibm_runtime/accounts/management.py index 6e80c9e03..fd65c6e9b 100644 --- a/qiskit_ibm_runtime/accounts/management.py +++ b/qiskit_ibm_runtime/accounts/management.py @@ -26,8 +26,6 @@ ) _QISKITRC_CONFIG_FILE = os.path.join(os.path.expanduser("~"), ".qiskit", "qiskitrc") _DEFAULT_ACCOUNT_NAME = "default" -_DEFAULT_ACCOUNT_NAME_LEGACY = "default-legacy" -_DEFAULT_ACCOUNT_NAME_CLOUD = "default-cloud" _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM = "default-ibm-quantum" _DEFAULT_ACCOUNT_NAME_IBM_CLOUD = "default-ibm-cloud" _DEFAULT_CHANNEL_TYPE: ChannelType = "ibm_cloud" @@ -50,9 +48,9 @@ def save( verify: Optional[bool] = None, overwrite: Optional[bool] = False, channel_strategy: Optional[str] = None, + set_as_default: Optional[bool] = None, ) -> None: """Save account on disk.""" - cls.migrate(filename=filename) channel = channel or os.getenv("QISKIT_IBM_CHANNEL") or _DEFAULT_CHANNEL_TYPE name = name or cls._get_default_account_name(channel) filename = filename if filename else _DEFAULT_ACCOUNT_CONFIG_JSON_FILE @@ -72,6 +70,7 @@ def save( ) # avoid storing invalid accounts .validate().to_saved_format(), + set_as_default=set_as_default, ) @staticmethod @@ -84,7 +83,6 @@ def list( """List all accounts in a given filename, or in the default account file.""" filename = filename if filename else _DEFAULT_ACCOUNT_CONFIG_JSON_FILE filename = os.path.expanduser(filename) - AccountManager.migrate(filename) def _matching_name(account_name: str) -> bool: return name is None or name == account_name @@ -139,8 +137,23 @@ def get( Args: filename: Full path of the file from which to get the account. - name: Account name. Takes precedence if `auth` is also specified. + name: Account name. channel: Channel type. + Order of precedence for selecting the account: + 1. If name is specified, get account with that name + 2. If the environment variables define an account, get that one + 3. If the channel parameter is defined, + a. get the account of this channel type defined as "is_default_account" + b. get the account of this channel type with default name + c. get any account of this channel type + 4. If the channel is defined in "QISKIT_IBM_CHANNEL" + a. get the account of this channel type defined as "is_default_account" + b. get the account of this channel type with default name + c. get any account of this channel type + 5. If a default account is defined in the json file, get that account + 6. Get any account that is defined in the json file with + preference for _DEFAULT_CHANNEL_TYPE. + Returns: Account information. @@ -150,7 +163,6 @@ def get( """ filename = filename if filename else _DEFAULT_ACCOUNT_CONFIG_JSON_FILE filename = os.path.expanduser(filename) - cls.migrate(filename) if name: saved_account = read_config(filename=filename, name=name) if not saved_account: @@ -162,18 +174,20 @@ def get( if env_account is not None: return env_account - if channel: - saved_account = read_config( - filename=filename, - name=cls._get_default_account_name(channel=channel), - ) - if saved_account is None: - if os.path.isfile(_QISKITRC_CONFIG_FILE): - return cls._from_qiskitrc_file() - raise AccountNotFoundError(f"No default {channel} account saved.") + all_config = read_config(filename=filename) + # Get the default account for the given channel. + # If channel == None, get the default account, for any channel, if it exists + saved_account = cls._get_default_account(all_config, channel) + + if saved_account is not None: return Account.from_saved_format(saved_account) - all_config = read_config(filename=filename) + # Get the default account from the channel defined in the environment variable + account = cls._get_default_account(all_config, channel=channel_) + if account is not None: + return Account.from_saved_format(account) + + # check for any account for channel_type in _CHANNEL_TYPES: account_name = cls._get_default_account_name(channel=channel_type) if account_name in all_config: @@ -194,54 +208,12 @@ def delete( """Delete account from disk.""" filename = filename if filename else _DEFAULT_ACCOUNT_CONFIG_JSON_FILE filename = os.path.expanduser(filename) - cls.migrate(filename=filename) name = name or cls._get_default_account_name(channel) return delete_config( filename=filename, name=name, ) - @classmethod - def migrate(cls, filename: Optional[str] = None) -> None: - """Migrate accounts on disk by removing `auth` and adding `channel`.""" - filename = filename if filename else _DEFAULT_ACCOUNT_CONFIG_JSON_FILE - filename = os.path.expanduser(filename) - data = read_config(filename=filename) - for key, value in data.items(): - if key == _DEFAULT_ACCOUNT_NAME_CLOUD: - value.pop("auth", None) - value.update(channel="ibm_cloud") - delete_config(filename=filename, name=key) - save_config( - filename=filename, - name=_DEFAULT_ACCOUNT_NAME_IBM_CLOUD, - config=value, - overwrite=False, - ) - elif key == _DEFAULT_ACCOUNT_NAME_LEGACY: - value.pop("auth", None) - value.update(channel="ibm_quantum") - delete_config(filename=filename, name=key) - save_config( - filename=filename, - name=_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM, - config=value, - overwrite=False, - ) - else: - if isinstance(value, dict) and "auth" in value: - if value["auth"] == "cloud": - value.update(channel="ibm_cloud") - elif value["auth"] == "legacy": - value.update(channel="ibm_quantum") - value.pop("auth", None) - save_config( - filename=filename, - name=key, - config=value, - overwrite=True, - ) - @classmethod def _from_env_variables(cls, channel: Optional[ChannelType]) -> Optional[Account]: """Read account from environment variable.""" @@ -256,6 +228,34 @@ def _from_env_variables(cls, channel: Optional[ChannelType]) -> Optional[Account channel=channel, ) + @classmethod + def _get_default_account( + cls, all_config: dict, channel: Optional[str] = None + ) -> Optional[dict]: + default_channel_account = None + any_channel_account = None + + for account_name in all_config: + account = all_config[account_name] + if channel: + if account.get("channel") == channel and account.get("is_default_account"): + return account + if account.get( + "channel" + ) == channel and account_name == cls._get_default_account_name(channel): + default_channel_account = account + if account.get("channel") == channel: + any_channel_account = account + else: + if account.get("is_default_account"): + return account + + if default_channel_account: + return default_channel_account + elif any_channel_account: + return any_channel_account + return None + @classmethod def _get_default_account_name(cls, channel: ChannelType) -> str: return ( diff --git a/qiskit_ibm_runtime/accounts/storage.py b/qiskit_ibm_runtime/accounts/storage.py index db463de27..256432997 100644 --- a/qiskit_ibm_runtime/accounts/storage.py +++ b/qiskit_ibm_runtime/accounts/storage.py @@ -22,7 +22,9 @@ logger = logging.getLogger(__name__) -def save_config(filename: str, name: str, config: dict, overwrite: bool) -> None: +def save_config( + filename: str, name: str, config: dict, overwrite: bool, set_as_default: Optional[bool] = None +) -> None: """Save configuration data in a JSON file under the given name.""" logger.debug("Save configuration data for '%s' in '%s'", name, filename) _ensure_file_exists(filename) @@ -35,8 +37,24 @@ def save_config(filename: str, name: str, config: dict, overwrite: bool) -> None f"Named account ({name}) already exists. " f"Set overwrite=True to overwrite." ) + data[name] = config + + # if set_as_default, but another account is defined as default, user must specify overwrite to change + # the default account. + if set_as_default: + data[name]["is_default_account"] = True + for account_name in data: + account = data[account_name] + if account_name != name and account.get("is_default_account"): + if overwrite: + del account["is_default_account"] + else: + raise AccountAlreadyExistsError( + f"default_account ({name}) already exists. " + f"Set overwrite=True to overwrite." + ) + with open(filename, mode="w", encoding="utf-8") as json_out: - data[name] = config json.dump(data, json_out, sort_keys=True, indent=4) diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index 13a30e262..db8308eec 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -16,9 +16,9 @@ import os from typing import Optional, Dict, Sequence, Any, Union import logging +import typing from qiskit.circuit import QuantumCircuit -from qiskit.opflow import PauliSumOp from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.primitives import BaseEstimator @@ -32,6 +32,9 @@ # pylint: disable=unused-import,cyclic-import from .session import Session +if typing.TYPE_CHECKING: + from qiskit.opflow import PauliSumOp + logger = logging.getLogger(__name__) diff --git a/qiskit_ibm_runtime/exceptions.py b/qiskit_ibm_runtime/exceptions.py index 9137b4307..3902a53d1 100644 --- a/qiskit_ibm_runtime/exceptions.py +++ b/qiskit_ibm_runtime/exceptions.py @@ -13,6 +13,7 @@ """Exceptions related to the IBM Runtime service.""" from qiskit.exceptions import QiskitError +from qiskit.providers.exceptions import JobTimeoutError class IBMError(QiskitError): @@ -87,7 +88,7 @@ class RuntimeInvalidStateError(IBMRuntimeError): pass -class RuntimeJobTimeoutError(IBMRuntimeError): +class RuntimeJobTimeoutError(JobTimeoutError): """Error raised when waiting for job times out.""" pass diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index a1ea28103..ba22c8c9f 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -34,7 +34,7 @@ from qiskit_ibm_provider.utils.backend_decoder import configuration_from_server_data from qiskit_ibm_runtime import ibm_backend -from .accounts import AccountManager, Account, AccountType, ChannelType +from .accounts import AccountManager, Account, ChannelType from .api.clients import AuthClient, VersionClient from .api.clients.runtime import RuntimeClient from .api.exceptions import RequestsApiError @@ -139,6 +139,7 @@ def __init__( - Account with the input `name`, if specified. - Default account for the `channel` type, if `channel` is specified but `token` is not. - Account defined by the input `channel` and `token`, if specified. + - Account defined by the `default_channel` if defined in filename - Account defined by the environment variables, if defined. - Default account for the ``ibm_cloud`` account, if one is available. - Default account for the ``ibm_quantum`` account, if one is available. @@ -230,7 +231,6 @@ def _discover_account( url: Optional[str] = None, instance: Optional[str] = None, channel: Optional[ChannelType] = None, - auth: Optional[AccountType] = None, filename: Optional[str] = None, name: Optional[str] = None, proxies: Optional[ProxyConfiguration] = None, @@ -250,27 +250,24 @@ def _discover_account( ) if name: if filename: - if any([auth, channel, token, url]): + if any([channel, token, url]): logger.warning( - "Loading account from file %s with name %s. Any input 'auth', " + "Loading account from file %s with name %s. Any input " "'channel', 'token' or 'url' are ignored.", filename, name, ) else: - if any([auth, channel, token, url]): + if any([channel, token, url]): logger.warning( - "Loading account with name %s. Any input 'auth', " + "Loading account with name %s. Any input " "'channel', 'token' or 'url' are ignored.", name, ) account = AccountManager.get(filename=filename, name=name) - elif auth or channel: - if auth and auth not in ["legacy", "cloud"]: - raise ValueError("'auth' can only be 'cloud' or 'legacy'") + elif channel: if channel and channel not in ["ibm_cloud", "ibm_quantum"]: raise ValueError("'channel' can only be 'ibm_cloud' or 'ibm_quantum'") - channel = channel or self._get_channel_for_auth(auth=auth) if token: account = Account( channel=channel, @@ -288,9 +285,10 @@ def _discover_account( elif any([token, url]): # Let's not infer based on these attributes as they may change in the future. raise ValueError( - "'channel' or 'auth' is required if 'token', or 'url' is specified but 'name' is not." + "'channel' is required if 'token', or 'url' is specified but 'name' is not." ) + # channel is not defined yet, get it from the AccountManager if account is None: account = AccountManager.get(filename=filename) @@ -681,13 +679,6 @@ def delete_account( """ return AccountManager.delete(filename=filename, name=name, channel=channel) - @staticmethod - def _get_channel_for_auth(auth: str) -> str: - """Returns channel type based on auth""" - if auth == "legacy": - return "ibm_quantum" - return "ibm_cloud" - @staticmethod def save_account( token: Optional[str] = None, @@ -700,6 +691,7 @@ def save_account( verify: Optional[bool] = None, overwrite: Optional[bool] = False, channel_strategy: Optional[str] = None, + set_as_default: Optional[bool] = None, ) -> None: """Save the account to disk for future use. @@ -720,6 +712,8 @@ def save_account( verify: Verify the server's TLS certificate. overwrite: ``True`` if the existing account is to be overwritten. channel_strategy: Error mitigation strategy. + set_as_default: If ``True``, the account is saved in filename, + as the default account. """ AccountManager.save( @@ -733,6 +727,7 @@ def save_account( verify=verify, overwrite=overwrite, channel_strategy=channel_strategy, + set_as_default=set_as_default, ) @staticmethod @@ -1510,15 +1505,6 @@ def instances(self) -> List[str]: return list(self._hgps.keys()) return [] - @property - def auth(self) -> str: - """Return the authentication type used. - - Returns: - The authentication type used. - """ - return "cloud" if self._channel == "ibm_cloud" else "legacy" - @property def channel(self) -> str: """Return the channel type used. diff --git a/qiskit_ibm_runtime/runtime_job.py b/qiskit_ibm_runtime/runtime_job.py index b6d9eeb8d..eff9c14fd 100644 --- a/qiskit_ibm_runtime/runtime_job.py +++ b/qiskit_ibm_runtime/runtime_job.py @@ -458,12 +458,15 @@ def _set_status(self, job_response: Dict) -> None: """ try: reason = job_response["state"].get("reason") + reason_code = job_response["state"].get("reason_code") if reason: # TODO remove this in https://github.com/Qiskit/qiskit-ibm-runtime/issues/989 if reason.upper() == "RAN TOO LONG": self._reason = reason.upper() else: self._reason = reason + if reason_code: + self._reason = f"Error code {reason_code}; {self._reason}" self._status = self._status_from_job_response(job_response) except KeyError: raise IBMError(f"Unknown status: {job_response['state']['status']}") @@ -489,6 +492,7 @@ def _error_msg_from_job_response(self, response: Dict) -> str: Error message. """ status = response["state"]["status"].upper() + job_result_raw = self._download_external_result( self._api_client.job_results(job_id=self.job_id()) ) diff --git a/qiskit_ibm_runtime/session.py b/qiskit_ibm_runtime/session.py index 60054d61d..ac54809ed 100644 --- a/qiskit_ibm_runtime/session.py +++ b/qiskit_ibm_runtime/session.py @@ -90,7 +90,7 @@ def __init__( max_time: (EXPERIMENTAL setting, can break between releases without warning) Maximum amount of time, a runtime session can be open before being forcibly closed. Can be specified as seconds (int) or a string like "2h 30m 40s". - This value must be in between 300 seconds and the + This value must be less than the `system imposed maximum `_. diff --git a/qiskit_ibm_runtime/utils/qctrl.py b/qiskit_ibm_runtime/utils/qctrl.py index 221ae65b0..f3acd1ec2 100644 --- a/qiskit_ibm_runtime/utils/qctrl.py +++ b/qiskit_ibm_runtime/utils/qctrl.py @@ -57,7 +57,7 @@ def _raise_if_error_in_options(options: Dict[str, Any]) -> None: arguments={}, ) - optimization_level = options.get("optimization_level", 1) + optimization_level = options.get("optimization_level", 3) _check_argument( optimization_level > 0, description="Q-CTRL Primitives do not support optimization level 0. Please\ diff --git a/releasenotes/notes/default_account-13d86d50f5b1d972.yaml b/releasenotes/notes/default_account-13d86d50f5b1d972.yaml new file mode 100644 index 000000000..47d3bbe7e --- /dev/null +++ b/releasenotes/notes/default_account-13d86d50f5b1d972.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added the option to define a default account in the account json file. + The select an account as default, define ``set_as_default=True`` in + ``QiskitRuntimeService.save_account()``. diff --git a/releasenotes/notes/error-codes-82a392efad5963da.yaml b/releasenotes/notes/error-codes-82a392efad5963da.yaml new file mode 100644 index 000000000..5f95903a0 --- /dev/null +++ b/releasenotes/notes/error-codes-82a392efad5963da.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Job error messages now include the error code. Error codes can be found in + https://docs.quantum-computing.ibm.com/errors. \ No newline at end of file diff --git a/test/account.py b/test/account.py index 1987eb591..65da49c73 100644 --- a/test/account.py +++ b/test/account.py @@ -152,6 +152,7 @@ def get_account_config_contents( instance=None, verify=None, proxies=None, + set_default=None, ): """Generate qiskitrc content""" if instance is None: @@ -177,4 +178,6 @@ def get_account_config_contents( out[name]["verify"] = verify if proxies is not None: out[name]["proxies"] = proxies + if set_default: + out[name]["is_default_account"] = True return out diff --git a/test/integration/test_job.py b/test/integration/test_job.py index 2e2f966e9..444d420f2 100644 --- a/test/integration/test_job.py +++ b/test/integration/test_job.py @@ -278,6 +278,7 @@ def test_job_creation_date(self, service): for rjob in rjobs: self.assertTrue(rjob.creation_date) + @unittest.skip("Skipping until primitives add more logging") @run_integration_test def test_job_logs(self, service): """Test job logs.""" diff --git a/test/unit/test_account.py b/test/unit/test_account.py index dce40f931..d389123f9 100644 --- a/test/unit/test_account.py +++ b/test/unit/test_account.py @@ -29,8 +29,6 @@ ) from qiskit_ibm_runtime.accounts.account import IBM_CLOUD_API_URL, IBM_QUANTUM_API_URL from qiskit_ibm_runtime.accounts.management import ( - _DEFAULT_ACCOUNT_NAME_LEGACY, - _DEFAULT_ACCOUNT_NAME_CLOUD, _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM, _DEFAULT_ACCOUNT_NAME_IBM_CLOUD, ) @@ -61,25 +59,6 @@ ), ) -_TEST_LEGACY_ACCOUNT = { - "auth": "legacy", - "token": "token-x", - "url": "https://auth.quantum-computing.ibm.com/api", - "instance": "ibm-q/open/main", -} - -_TEST_CLOUD_ACCOUNT = { - "auth": "cloud", - "token": "token-y", - "url": "https://cloud.ibm.com", - "instance": "crn:v1:bluemix:public:quantum-computing:us-east:a/...::", - "proxies": { - "username_ntlm": "bla", - "password_ntlm": "blub", - "urls": {"https": "127.0.0.1"}, - }, -} - _TEST_FILENAME = "/tmp/temp_qiskit_account.json" @@ -229,138 +208,6 @@ def test_save_without_overwrite(self): overwrite=False, ) - # TODO remove test when removing auth parameter - @temporary_account_config_file(contents={_DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT}) - @no_envs(["QISKIT_IBM_TOKEN"]) - def test_save_channel_ibm_cloud_over_auth_cloud_without_overwrite(self): - """Test to overwrite an existing auth "cloud" account with channel "ibm_cloud" - and without setting overwrite=True.""" - with self.assertRaises(AccountAlreadyExistsError): - AccountManager.save( - token=_TEST_IBM_CLOUD_ACCOUNT.token, - url=_TEST_IBM_CLOUD_ACCOUNT.url, - instance=_TEST_IBM_CLOUD_ACCOUNT.instance, - channel="ibm_cloud", - name=None, - overwrite=False, - ) - - # TODO remove test when removing auth parameter - @temporary_account_config_file(contents={_DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT}) - @no_envs(["QISKIT_IBM_TOKEN"]) - def test_save_channel_ibm_quantum_over_auth_legacy_without_overwrite(self): - """Test to overwrite an existing auth "legacy" account with channel "ibm_quantum" - and without setting overwrite=True.""" - with self.assertRaises(AccountAlreadyExistsError): - AccountManager.save( - token=_TEST_IBM_QUANTUM_ACCOUNT.token, - url=_TEST_IBM_QUANTUM_ACCOUNT.url, - instance=_TEST_IBM_QUANTUM_ACCOUNT.instance, - channel="ibm_quantum", - name=None, - overwrite=False, - ) - - # TODO remove test when removing auth parameter - @temporary_account_config_file(contents={_DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT}) - @no_envs(["QISKIT_IBM_TOKEN"]) - def test_save_channel_ibm_quantum_over_auth_legacy_with_overwrite(self): - """Test to overwrite an existing auth "legacy" account with channel "ibm_quantum" - and with setting overwrite=True.""" - AccountManager.save( - token=_TEST_IBM_QUANTUM_ACCOUNT.token, - url=_TEST_IBM_QUANTUM_ACCOUNT.url, - instance=_TEST_IBM_QUANTUM_ACCOUNT.instance, - channel="ibm_quantum", - name=None, - overwrite=True, - ) - self.assertEqual(_TEST_IBM_QUANTUM_ACCOUNT, AccountManager.get(channel="ibm_quantum")) - - # TODO remove test when removing auth parameter - @temporary_account_config_file(contents={_DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT}) - @no_envs(["QISKIT_IBM_TOKEN"]) - def test_save_channel_ibm_cloud_over_auth_cloud_with_overwrite(self): - """Test to overwrite an existing auth "cloud" account with channel "ibm_cloud" - and with setting overwrite=True.""" - AccountManager.save( - token=_TEST_IBM_CLOUD_ACCOUNT.token, - url=_TEST_IBM_CLOUD_ACCOUNT.url, - instance=_TEST_IBM_CLOUD_ACCOUNT.instance, - channel="ibm_cloud", - 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")) - - # TODO remove test when removing auth parameter - @temporary_account_config_file(contents={"personal-account": _TEST_CLOUD_ACCOUNT}) - def test_save_channel_ibm_cloud_with_name_over_auth_cloud_with_overwrite(self): - """Test to overwrite an existing named auth "cloud" account with channel "ibm_cloud" - and with setting overwrite=True.""" - AccountManager.save( - token=_TEST_IBM_CLOUD_ACCOUNT.token, - url=_TEST_IBM_CLOUD_ACCOUNT.url, - instance=_TEST_IBM_CLOUD_ACCOUNT.instance, - channel="ibm_cloud", - proxies=_TEST_IBM_CLOUD_ACCOUNT.proxies, - name="personal-account", - overwrite=True, - ) - self.assertEqual(_TEST_IBM_CLOUD_ACCOUNT, AccountManager.get(name="personal-account")) - - # TODO remove test when removing auth parameter - @temporary_account_config_file(contents={"personal-account": _TEST_CLOUD_ACCOUNT}) - def test_save_channel_ibm_cloud_with_name_over_auth_cloud_without_overwrite(self): - """Test to overwrite an existing named auth "cloud" account with channel "ibm_cloud" - and without setting overwrite=True.""" - with self.assertRaises(AccountAlreadyExistsError): - AccountManager.save( - token=_TEST_IBM_CLOUD_ACCOUNT.token, - url=_TEST_IBM_CLOUD_ACCOUNT.url, - instance=_TEST_IBM_CLOUD_ACCOUNT.instance, - channel="ibm_cloud", - proxies=_TEST_IBM_CLOUD_ACCOUNT.proxies, - name="personal-account", - overwrite=False, - ) - - # TODO remove test when removing auth parameter - @temporary_account_config_file(contents={"personal-account": _TEST_LEGACY_ACCOUNT}) - def test_save_channel_ibm_quantum_with_name_over_auth_legacy_with_overwrite(self): - """Test to overwrite an existing named auth "legacy" account with channel "ibm_quantum" - and with setting overwrite=True.""" - AccountManager.save( - token=_TEST_IBM_QUANTUM_ACCOUNT.token, - url=_TEST_IBM_QUANTUM_ACCOUNT.url, - instance=_TEST_IBM_QUANTUM_ACCOUNT.instance, - channel="ibm_quantum", - proxies=_TEST_IBM_QUANTUM_ACCOUNT.proxies, - name="personal-account", - overwrite=True, - ) - self.assertEqual(_TEST_IBM_QUANTUM_ACCOUNT, AccountManager.get(name="personal-account")) - - # TODO remove test when removing auth parameter - @temporary_account_config_file(contents={"personal-account": _TEST_LEGACY_ACCOUNT}) - def test_save_channel_ibm_quantum_with_name_over_auth_legacy_without_overwrite( - self, - ): - """Test to overwrite an existing named auth "legacy" account with channel "ibm_quantum" - and without setting overwrite=True.""" - with self.assertRaises(AccountAlreadyExistsError): - AccountManager.save( - token=_TEST_IBM_QUANTUM_ACCOUNT.token, - url=_TEST_IBM_QUANTUM_ACCOUNT.url, - instance=_TEST_IBM_QUANTUM_ACCOUNT.instance, - channel="ibm_quantum", - proxies=_TEST_IBM_QUANTUM_ACCOUNT.proxies, - name="personal-account", - overwrite=False, - ) - @temporary_account_config_file(contents={"conflict": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format()}) def test_get_none(self): """Test to get an account with an invalid name.""" @@ -457,21 +304,6 @@ def test_list(self): self.assertEqual(accounts["key1"], _TEST_IBM_CLOUD_ACCOUNT) self.assertTrue(accounts["key2"], _TEST_IBM_QUANTUM_ACCOUNT) - with temporary_account_config_file( - contents={ - _DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT, - _DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_CLOUD_ACCOUNT, - } - ), self.subTest("non-empty list of auth accounts"): - accounts = AccountManager.list() - - self.assertEqual(len(accounts), 2) - self.assertEqual(accounts[_DEFAULT_ACCOUNT_NAME_IBM_CLOUD], _TEST_IBM_CLOUD_ACCOUNT) - self.assertTrue(accounts[_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM], _TEST_IBM_QUANTUM_ACCOUNT) - - with temporary_account_config_file(contents={}), self.subTest("empty list of accounts"): - self.assertEqual(len(AccountManager.list()), 0) - with temporary_account_config_file( contents={ "key1": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format(), @@ -504,35 +336,6 @@ def test_list(self): self.assertEqual(len(accounts), 1) self.assertListEqual(accounts, ["key1"]) - # TODO remove test when removing auth parameter - with temporary_account_config_file( - contents={ - "key1": _TEST_CLOUD_ACCOUNT, - "key2": _TEST_LEGACY_ACCOUNT, - _DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT, - _DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT, - } - ), self.subTest("filtered list of auth accounts"): - accounts = list(AccountManager.list(channel="ibm_cloud").keys()) - self.assertEqual(len(accounts), 2) - self.assertListEqual(accounts, [_DEFAULT_ACCOUNT_NAME_IBM_CLOUD, "key1"]) - - accounts = list(AccountManager.list(channel="ibm_quantum").keys()) - self.assertEqual(len(accounts), 2) - self.assertListEqual(accounts, [_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM, "key2"]) - - accounts = list(AccountManager.list(channel="ibm_cloud", default=True).keys()) - self.assertEqual(len(accounts), 1) - self.assertListEqual(accounts, [_DEFAULT_ACCOUNT_NAME_IBM_CLOUD]) - - accounts = list(AccountManager.list(channel="ibm_cloud", default=False).keys()) - self.assertEqual(len(accounts), 1) - self.assertListEqual(accounts, ["key1"]) - - accounts = list(AccountManager.list(name="key1").keys()) - self.assertEqual(len(accounts), 1) - self.assertListEqual(accounts, ["key1"]) - @temporary_account_config_file( contents={ "key1": _TEST_IBM_CLOUD_ACCOUNT.to_saved_format(), @@ -555,32 +358,10 @@ def test_delete(self): self.assertTrue(len(AccountManager.list()) == 0) - @temporary_account_config_file( - contents={ - "key1": _TEST_CLOUD_ACCOUNT, - _DEFAULT_ACCOUNT_NAME_LEGACY: _TEST_LEGACY_ACCOUNT, - _DEFAULT_ACCOUNT_NAME_CLOUD: _TEST_CLOUD_ACCOUNT, - } - ) - def test_delete_auth(self): - """Test delete accounts already saved using auth.""" - - with self.subTest("delete named account"): - self.assertTrue(AccountManager.delete(name="key1")) - self.assertFalse(AccountManager.delete(name="key1")) - - with self.subTest("delete default auth='legacy' account using channel"): - self.assertTrue(AccountManager.delete(channel="ibm_quantum")) - - with self.subTest("delete default auth='cloud' account using channel"): - self.assertTrue(AccountManager.delete()) - - self.assertTrue(len(AccountManager.list()) == 0) - def test_delete_filename(self): """Test delete accounts with filename parameter.""" - filename = "~/account_to_delete.json" + filename = _TEST_FILENAME name = "key1" channel = "ibm_quantum" AccountManager.save(channel=channel, filename=filename, name=name, token="temp_token") @@ -606,6 +387,180 @@ def test_account_with_filename(self): ) self.assertEqual(account.token, dummy_token) + @temporary_account_config_file() + def test_default_env_channel(self): + """Test that if QISKIT_IBM_CHANNEL is set in the environment, this channel will be used""" + token = uuid.uuid4().hex + # unset default_channel in the environment + with temporary_account_config_file(token=token), no_envs("QISKIT_IBM_CHANNEL"): + service = FakeRuntimeService() + self.assertEqual(service.channel, "ibm_cloud") + + # set channel to default channel in the environment + subtests = ["ibm_quantum", "ibm_cloud"] + for channel in subtests: + channel_env = {"QISKIT_IBM_CHANNEL": channel} + with temporary_account_config_file(channel=channel, token=token), custom_envs( + channel_env + ): + service = FakeRuntimeService() + self.assertEqual(service.channel, channel) + + def test_save_default_account(self): + """Test that if a default_account is defined in the qiskit-ibm.json file, + this account will be used""" + AccountManager.save( + filename=_TEST_FILENAME, + name=_DEFAULT_ACCOUNT_NAME_IBM_CLOUD, + token=_TEST_IBM_CLOUD_ACCOUNT.token, + url=_TEST_IBM_CLOUD_ACCOUNT.url, + instance=_TEST_IBM_CLOUD_ACCOUNT.instance, + channel="ibm_cloud", + overwrite=True, + set_as_default=True, + ) + AccountManager.save( + filename=_TEST_FILENAME, + name=_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM, + token=_TEST_IBM_QUANTUM_ACCOUNT.token, + url=_TEST_IBM_QUANTUM_ACCOUNT.url, + instance=_TEST_IBM_QUANTUM_ACCOUNT.instance, + channel="ibm_quantum", + overwrite=True, + ) + + with no_envs("QISKIT_IBM_CHANNEL"), no_envs("QISKIT_IBM_TOKEN"): + account = AccountManager.get(filename=_TEST_FILENAME) + self.assertEqual(account.channel, "ibm_cloud") + self.assertEqual(account.token, _TEST_IBM_CLOUD_ACCOUNT.token) + + AccountManager.save( + filename=_TEST_FILENAME, + name=_DEFAULT_ACCOUNT_NAME_IBM_QUANTUM, + token=_TEST_IBM_QUANTUM_ACCOUNT.token, + url=_TEST_IBM_QUANTUM_ACCOUNT.url, + instance=_TEST_IBM_QUANTUM_ACCOUNT.instance, + channel="ibm_quantum", + overwrite=True, + set_as_default=True, + ) + with no_envs("QISKIT_IBM_CHANNEL"), no_envs("QISKIT_IBM_TOKEN"): + account = AccountManager.get(filename=_TEST_FILENAME) + self.assertEqual(account.channel, "ibm_quantum") + self.assertEqual(account.token, _TEST_IBM_QUANTUM_ACCOUNT.token) + + @temporary_account_config_file() + def test_set_channel_precedence(self): + """Test the precedence of the various methods to set the account: + account name > env_variables > channel parameter default account + > default account > default account from default channel""" + cloud_token = uuid.uuid4().hex + default_token = uuid.uuid4().hex + preferred_token = uuid.uuid4().hex + any_token = uuid.uuid4().hex + channel_env = {"QISKIT_IBM_CHANNEL": "ibm_cloud"} + contents = { + _DEFAULT_ACCOUNT_NAME_IBM_CLOUD: { + "channel": "ibm_cloud", + "token": cloud_token, + "instance": "some_instance", + }, + _DEFAULT_ACCOUNT_NAME_IBM_QUANTUM: { + "channel": "ibm_quantum", + "token": default_token, + }, + "preferred-ibm-quantum": { + "channel": "ibm_quantum", + "token": preferred_token, + "is_default_account": True, + }, + "any-quantum": { + "channel": "ibm_quantum", + "token": any_token, + }, + } + + # 'name' parameter + with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs( + "QISKIT_IBM_TOKEN" + ): + service = FakeRuntimeService(name="any-quantum") + self.assertEqual(service.channel, "ibm_quantum") + self.assertEqual(service._account.token, any_token) + + # No name or channel params, no env vars, get the account specified as "is_default_account" + with temporary_account_config_file(contents=contents), no_envs( + "QISKIT_IBM_CHANNEL" + ), no_envs("QISKIT_IBM_TOKEN"): + service = FakeRuntimeService() + self.assertEqual(service.channel, "ibm_quantum") + self.assertEqual(service._account.token, preferred_token) + + # parameter 'channel' is specified, it overrides channel in env + # account specified as "is_default_account" + with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs( + "QISKIT_IBM_TOKEN" + ): + service = FakeRuntimeService(channel="ibm_quantum") + self.assertEqual(service.channel, "ibm_quantum") + self.assertEqual(service._account.token, preferred_token) + + # account with default name for the channel + contents["preferred-ibm-quantum"]["is_default_account"] = False + with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs( + "QISKIT_IBM_TOKEN" + ): + service = FakeRuntimeService(channel="ibm_quantum") + self.assertEqual(service.channel, "ibm_quantum") + self.assertEqual(service._account.token, default_token) + + # any account for this channel + del contents["default-ibm-quantum"] + # channel_env = {"QISKIT_IBM_CHANNEL": "ibm_quantum"} + with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs( + "QISKIT_IBM_TOKEN" + ): + service = FakeRuntimeService(channel="ibm_quantum") + self.assertEqual(service.channel, "ibm_quantum") + self.assertEqual(service._account.token, any_token) + + # no channel param, get account that is specified as "is_default_account" + # for channel from env + contents["preferred-ibm-quantum"]["is_default_account"] = True + with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs( + "QISKIT_IBM_TOKEN" + ): + service = FakeRuntimeService() + self.assertEqual(service.channel, "ibm_quantum") + self.assertEqual(service._account.token, preferred_token) + + # no channel param, account with default name for the channel from env + del contents["preferred-ibm-quantum"]["is_default_account"] + contents["default-ibm-quantum"] = { + "channel": "ibm_quantum", + "token": default_token, + } + channel_env = {"QISKIT_IBM_CHANNEL": "ibm_quantum"} + with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs( + "QISKIT_IBM_TOKEN" + ): + service = FakeRuntimeService() + self.assertEqual(service.channel, "ibm_quantum") + self.assertEqual(service._account.token, default_token) + + # no channel param, any account for the channel from env + del contents["default-ibm-quantum"] + with temporary_account_config_file(contents=contents), custom_envs(channel_env), no_envs( + "QISKIT_IBM_TOKEN" + ): + service = FakeRuntimeService() + self.assertEqual(service.channel, "ibm_quantum") + self.assertEqual(service._account.token, any_token) + # default channel + with temporary_account_config_file(contents=contents), no_envs("QISKIT_IBM_CHANNEL"): + service = FakeRuntimeService() + self.assertEqual(service.channel, "ibm_cloud") + def tearDown(self) -> None: """Test level tear down.""" super().tearDown() @@ -735,7 +690,10 @@ def test_enable_account_both_channel(self): token = uuid.uuid4().hex contents = get_account_config_contents(channel="ibm_cloud", token=token) contents.update(get_account_config_contents(channel="ibm_quantum", token=uuid.uuid4().hex)) - with temporary_account_config_file(contents=contents), no_envs(["QISKIT_IBM_TOKEN"]): + + with temporary_account_config_file(contents=contents), no_envs( + ["QISKIT_IBM_TOKEN", "QISKIT_IBM_CHANNEL"] + ): service = FakeRuntimeService() self.assertTrue(service._account) self.assertEqual(service._account.token, token) @@ -754,7 +712,7 @@ def test_enable_account_by_env_channel(self): "QISKIT_IBM_URL": url, "QISKIT_IBM_INSTANCE": "h/g/p" if channel == "ibm_quantum" else "crn:12", } - with custom_envs(envs): + with custom_envs(envs), no_envs("QISKIT_IBM_CHANNEL"): service = FakeRuntimeService(channel=channel) self.assertTrue(service._account) @@ -871,7 +829,7 @@ def test_enable_account_by_env_pref(self): "QISKIT_IBM_URL": url, "QISKIT_IBM_INSTANCE": "my_crn", } - with custom_envs(envs): + with custom_envs(envs), no_envs("QISKIT_IBM_CHANNEL"): service = FakeRuntimeService(**extra) self.assertTrue(service._account) diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index 3d4786029..f4845e55e 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -20,46 +20,14 @@ from datetime import datetime import numpy as np -import scipy.sparse -from qiskit.algorithms.optimizers import ( - ADAM, - GSLS, - SPSA, - QNSPSA, - L_BFGS_B, - NELDER_MEAD, -) + from qiskit.circuit import Parameter, QuantumCircuit from qiskit.test.reference_circuits import ReferenceCircuits from qiskit.circuit.library import EfficientSU2, CXGate, PhaseGate, U2Gate -from qiskit.opflow import ( - PauliSumOp, - MatrixOp, - PauliOp, - CircuitOp, - EvolvedOp, - TaperedPauliSumOp, - Z2Symmetries, - I, - X, - Y, - Z, - StateFn, - CircuitStateFn, - DictStateFn, - VectorStateFn, - OperatorStateFn, - SparseVectorStateFn, - CVaRMeasurement, - ComposedOp, - SummedOp, - TensoredOp, -) from qiskit.providers.fake_provider import FakeNairobi from qiskit.quantum_info import SparsePauliOp, Pauli, Statevector from qiskit.result import Result from qiskit_aer.noise import NoiseModel - from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder from .mock.fake_runtime_client import CustomResultRuntimeJob from .mock.fake_runtime_service import FakeRuntimeService @@ -125,73 +93,47 @@ def test_coder_qc(self): def test_coder_operators(self): """Test runtime encoder and decoder for operators.""" + + # filter warnings triggered by opflow imports + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", category=DeprecationWarning, module=r"qiskit\.opflow\." + ) + from qiskit.opflow import PauliSumOp # pylint: disable=import-outside-toplevel + + # catch warnings triggered by opflow use + with warnings.catch_warnings(record=True) as w_log: + deprecated_op = PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2])) + self.assertTrue(len(w_log) > 0) + coeff_x = Parameter("x") coeff_y = coeff_x + 1 - quantum_circuit = QuantumCircuit(1) - quantum_circuit.h(0) - operator = 2.0 * I ^ I - z2_symmetries = Z2Symmetries( - [Pauli("IIZI"), Pauli("ZIII")], - [Pauli("IIXI"), Pauli("XIII")], - [1, 3], - [-1, 1], - ) - isqrt2 = 1 / np.sqrt(2) - sparse = scipy.sparse.csr_matrix([[0, isqrt2, 0, isqrt2]]) subtests = ( - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1]), coeff=coeff_y), - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1 + 2j]), coeff=3 - 2j), - PauliSumOp.from_list([("II", -1.052373245772859), ("IZ", 0.39793742484318045)]), - MatrixOp(primitive=np.array([[0, -1j], [1j, 0]]), coeff=coeff_x), - PauliOp(primitive=Pauli("Y"), coeff=coeff_x), - CircuitOp(quantum_circuit, coeff=coeff_x), - EvolvedOp(operator, coeff=coeff_x), - TaperedPauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), z2_symmetries), - StateFn(quantum_circuit, coeff=coeff_x), - CircuitStateFn(quantum_circuit, is_measurement=True), - DictStateFn("1" * 3, is_measurement=True), - VectorStateFn(np.ones(2**3, dtype=complex)), - OperatorStateFn(CircuitOp(QuantumCircuit(1))), - SparseVectorStateFn(sparse), - Statevector([1, 0]), - CVaRMeasurement(Z, 0.2), - ComposedOp([(X ^ Y ^ Z), (Z ^ X ^ Y ^ Z).to_matrix_op()]), - SummedOp([X ^ X * 2, Y ^ Y], 2), - TensoredOp([(X ^ Y), (Z ^ I)]), - (Z ^ Z) ^ (I ^ 2), + SparsePauliOp(Pauli("XYZX"), coeffs=[2]), + SparsePauliOp(Pauli("XYZX"), coeffs=[coeff_y]), + SparsePauliOp(Pauli("XYZX"), coeffs=[1 + 2j]), + deprecated_op, ) + for operator in subtests: with self.subTest(operator=operator): encoded = json.dumps(operator, cls=RuntimeEncoder) self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertEqual(operator, decoded) - def test_coder_optimizers(self): - """Test runtime encoder and decoder for optimizers.""" - subtests = ( - (ADAM, {"maxiter": 100, "amsgrad": True}), - (GSLS, {"maxiter": 50, "min_step_size": 0.01}), - (SPSA, {"maxiter": 10, "learning_rate": 0.01, "perturbation": 0.1}), - (QNSPSA, {"fidelity": 123, "maxiter": 25, "resamplings": {1: 100, 2: 50}}), - # some SciPy optimizers only work with default arguments due to Qiskit/qiskit-terra#6682 - (L_BFGS_B, {}), - (NELDER_MEAD, {}), - # Enable when https://github.com/scikit-quant/scikit-quant/issues/24 is fixed - # (IMFIL, {"maxiter": 20}), - # (SNOBFIT, {"maxiter": 200, "maxfail": 20}), - ) - for opt_cls, settings in subtests: - with self.subTest(opt_cls=opt_cls): - optimizer = opt_cls(**settings) - encoded = json.dumps(optimizer, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertTrue(isinstance(decoded, opt_cls)) - for key, value in settings.items(): - self.assertEqual(decoded.settings[key], value) + with warnings.catch_warnings(): + # filter warnings triggered by opflow imports + # in L146 of utils/json.py + warnings.filterwarnings( + "ignore", category=DeprecationWarning, module=r"qiskit\.opflow\." + ) + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + module=r"qiskit_ibm_runtime\.utils\.json", + ) + decoded = json.loads(encoded, cls=RuntimeDecoder) + self.assertEqual(operator, decoded) def test_coder_noise_model(self): """Test encoding and decoding a noise model.""" @@ -268,8 +210,7 @@ def test_decoder_import(self): temp_fp.close() subtests = ( - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), - DictStateFn("1" * 3, is_measurement=True), + SparsePauliOp(Pauli("XYZX"), coeffs=[2]), Statevector([1, 0]), ) for operator in subtests: