Skip to content

Commit

Permalink
Merge branch 'main' into fix-errors
Browse files Browse the repository at this point in the history
  • Loading branch information
kt474 authored Sep 21, 2023
2 parents 8e19bcd + b7fef82 commit a0c55bd
Show file tree
Hide file tree
Showing 15 changed files with 362 additions and 407 deletions.
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
116 changes: 58 additions & 58 deletions qiskit_ibm_runtime/accounts/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -72,6 +70,7 @@ def save(
)
# avoid storing invalid accounts
.validate().to_saved_format(),
set_as_default=set_as_default,
)

@staticmethod
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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."""
Expand All @@ -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 (
Expand Down
22 changes: 20 additions & 2 deletions qiskit_ibm_runtime/accounts/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)


Expand Down
5 changes: 4 additions & 1 deletion qiskit_ibm_runtime/estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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__)


Expand Down
3 changes: 2 additions & 1 deletion qiskit_ibm_runtime/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -87,7 +88,7 @@ class RuntimeInvalidStateError(IBMRuntimeError):
pass


class RuntimeJobTimeoutError(IBMRuntimeError):
class RuntimeJobTimeoutError(JobTimeoutError):
"""Error raised when waiting for job times out."""

pass
Expand Down
Loading

0 comments on commit a0c55bd

Please sign in to comment.