diff --git a/.github/workflows/integration-tests-qiskit-main.yml b/.github/workflows/integration-tests-qiskit-main.yml new file mode 100644 index 0000000000..2aee5c0ecf --- /dev/null +++ b/.github/workflows/integration-tests-qiskit-main.yml @@ -0,0 +1,52 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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: Integration Tests (Qiskit Main) +on: + schedule: + # run integration tests against Qiskit main once a week + - cron: '0 0 * * 0' + workflow_dispatch: +jobs: + integration-tests: + if: github.repository_owner == 'Qiskit' + 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-quantum-production", "ibm-quantum-staging", "ibm-cloud-production", "ibm-cloud-staging" ] + environment: ${{ matrix.environment }} + env: + QISKIT_IBM_TOKEN: ${{ secrets.QISKIT_IBM_TOKEN }} + QISKIT_IBM_URL: ${{ secrets.QISKIT_IBM_URL }} + QISKIT_IBM_INSTANCE: ${{ secrets.QISKIT_IBM_INSTANCE }} + LOG_LEVEL: DEBUG + STREAM_LOG: True + QISKIT_IN_PARALLEL: True + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + 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 . git+https://github.com/Qiskit/qiskit.git + - name: Run integration tests + run: make integration-test diff --git a/.pylintrc b/.pylintrc index 5d5cb1eb0f..a455fd605e 100644 --- a/.pylintrc +++ b/.pylintrc @@ -379,6 +379,7 @@ function-naming-style=snake_case good-names=i, j, k, + dt, ex, Run, _ diff --git a/docs/apidocs/fake_provider.rst b/docs/apidocs/fake_provider.rst new file mode 100644 index 0000000000..357d305452 --- /dev/null +++ b/docs/apidocs/fake_provider.rst @@ -0,0 +1,4 @@ +.. automodule:: qiskit_ibm_runtime.fake_provider + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/apidocs/ibm-runtime.rst b/docs/apidocs/ibm-runtime.rst index 29b06f4628..cafc459a1a 100644 --- a/docs/apidocs/ibm-runtime.rst +++ b/docs/apidocs/ibm-runtime.rst @@ -9,3 +9,5 @@ qiskit-ibm-runtime API reference runtime_service options + transpiler + fake_provider diff --git a/docs/apidocs/transpiler.rst b/docs/apidocs/transpiler.rst new file mode 100644 index 0000000000..157dd40621 --- /dev/null +++ b/docs/apidocs/transpiler.rst @@ -0,0 +1,4 @@ +.. automodule:: qiskit_ibm_runtime.transpiler + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/conf.py b/docs/conf.py index e8f3e7eb01..b312c6c34e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,8 +25,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -docs_url_prefix = "ecosystem/ibm-runtime" -release = '0.18.1' +release = '0.19.2' # -- General configuration --------------------------------------------------- @@ -41,6 +40,7 @@ 'reno.sphinxext', 'nbsphinx', 'sphinxcontrib.katex', + 'matplotlib.sphinxext.plot_directive', ] templates_path = ['_templates'] diff --git a/program_source/circuit_runner/__init__.py b/program_source/circuit_runner/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/qiskit_ibm_runtime/VERSION.txt b/qiskit_ibm_runtime/VERSION.txt index 249afd517d..61e6e92d91 100644 --- a/qiskit_ibm_runtime/VERSION.txt +++ b/qiskit_ibm_runtime/VERSION.txt @@ -1 +1 @@ -0.18.1 +0.19.2 diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index 2e5ed6bbb4..eaa93ccc81 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -18,8 +18,8 @@ from urllib.parse import urlparse from requests.auth import AuthBase -from qiskit_ibm_provider.proxies import ProxyConfiguration -from qiskit_ibm_provider.utils.hgp import from_instance_format +from ..proxies import ProxyConfiguration +from ..utils.hgp import from_instance_format from .exceptions import InvalidAccountError, CloudResourceNameResolutionError from ..api.auth import QuantumAuth, CloudAuth diff --git a/qiskit_ibm_runtime/accounts/management.py b/qiskit_ibm_runtime/accounts/management.py index 4992998a6e..14aadd89b2 100644 --- a/qiskit_ibm_runtime/accounts/management.py +++ b/qiskit_ibm_runtime/accounts/management.py @@ -15,8 +15,7 @@ import os from typing import Optional, Dict -from qiskit_ibm_provider.proxies import ProxyConfiguration - +from ..proxies import ProxyConfiguration from .exceptions import AccountNotFoundError from .account import Account, ChannelType from .storage import save_config, read_config, delete_config diff --git a/qiskit_ibm_runtime/api/client_parameters.py b/qiskit_ibm_runtime/api/client_parameters.py index 3c722b7899..1cffd4948e 100644 --- a/qiskit_ibm_runtime/api/client_parameters.py +++ b/qiskit_ibm_runtime/api/client_parameters.py @@ -13,7 +13,7 @@ """Represent IBM Quantum account client parameters.""" from typing import Dict, Optional, Any, Union -from qiskit_ibm_provider.proxies import ProxyConfiguration +from ..proxies import ProxyConfiguration from ..utils import get_runtime_api_base_url from ..api.auth import QuantumAuth, CloudAuth diff --git a/qiskit_ibm_runtime/api/clients/__init__.py b/qiskit_ibm_runtime/api/clients/__init__.py index eba744e56c..58af1d4965 100644 --- a/qiskit_ibm_runtime/api/clients/__init__.py +++ b/qiskit_ibm_runtime/api/clients/__init__.py @@ -12,8 +12,8 @@ """IBM Quantum API clients.""" -from qiskit_ibm_provider.api.clients.base import BaseClient, WebsocketClientCloseCode -from qiskit_ibm_provider.api.clients.auth import AuthClient -from qiskit_ibm_provider.api.clients.version import VersionClient -from qiskit_ibm_provider.api.clients.runtime_ws import RuntimeWebsocketClient +from .base_websocket_client import WebsocketClientCloseCode +from .auth import AuthClient +from .version import VersionClient +from .runtime_ws import RuntimeWebsocketClient from .runtime import RuntimeClient diff --git a/qiskit_ibm_runtime/api/clients/auth.py b/qiskit_ibm_runtime/api/clients/auth.py new file mode 100644 index 0000000000..da27813def --- /dev/null +++ b/qiskit_ibm_runtime/api/clients/auth.py @@ -0,0 +1,172 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Client for accessing IBM Quantum authentication services.""" + +from typing import Dict, List, Optional, Any, Union +from requests.exceptions import RequestException + +from ..auth import QuantumAuth +from ..exceptions import AuthenticationLicenseError, RequestsApiError +from ..rest import Api +from ..session import RetrySession +from ..client_parameters import ClientParameters + + +class AuthClient: + """Client for accessing IBM Quantum authentication services.""" + + def __init__(self, client_params: ClientParameters) -> None: + """AuthClient constructor. + + Args: + client_params: Parameters used for server connection. + """ + self.api_token = client_params.token + self.auth_url = client_params.url + self._service_urls = {} # type: ignore[var-annotated] + + self.auth_api = Api(RetrySession(self.auth_url, **client_params.connection_parameters())) + self.base_api = self._init_service_clients(**client_params.connection_parameters()) + + def _init_service_clients(self, **request_kwargs: Any) -> Api: + """Initialize the clients used for communicating with the API. + + Args: + **request_kwargs: Arguments for the request ``Session``. + + Returns: + Client for the API server. + """ + # Request an access token. + self.access_token = self._request_access_token() + self.auth_api.session.auth = QuantumAuth(access_token=self.access_token) + self._service_urls = self.user_urls() + + # Create the api server client, using the access token. + base_api = Api( + RetrySession( + self._service_urls["http"], + auth=QuantumAuth(access_token=self.access_token), + **request_kwargs, + ) + ) + + return base_api + + def _request_access_token(self) -> str: + """Request a new access token from the API authentication service. + + Returns: + A new access token. + + Raises: + AuthenticationLicenseError: If the user hasn't accepted the license agreement. + RequestsApiError: If the request failed. + """ + try: + response = self.auth_api.login(self.api_token) + return response["id"] + except RequestsApiError as ex: + # Get the original exception that raised. + original_exception = ex.__cause__ + + if isinstance(original_exception, RequestException): + # Get the response from the original request exception. + error_response = ( + # pylint: disable=no-member + original_exception.response + ) + if error_response is not None and error_response.status_code == 401: + try: + error_code = error_response.json()["error"]["name"] + if error_code == "ACCEPT_LICENSE_REQUIRED": + message = error_response.json()["error"]["message"] + raise AuthenticationLicenseError(message) + except (ValueError, KeyError): + # the response did not contain the expected json. + pass + raise + + # User account-related public functions. + + def user_urls(self) -> Dict[str, str]: + """Retrieve the API URLs from the authentication service. + + Returns: + A dict with the base URLs for the services. Currently + supported keys are: + + * ``http``: The API URL for HTTP communication. + * ``ws``: The API URL for websocket communication. + * ``services`: The API URL for additional services. + """ + response = self.auth_api.user_info() + return response["urls"] + + def user_hubs(self) -> List[Dict[str, str]]: + """Retrieve the hub/group/project sets available to the user. + + The first entry in the list will be the default set, as indicated by + the ``isDefault`` field from the API. + + Returns: + A list of dictionaries with the hub, group, and project values keyed by + ``hub``, ``group``, and ``project``, respectively. + """ + response = self.base_api.hubs() + + hubs = [] # type: ignore[var-annotated] + for hub in response: + hub_name = hub["name"] + for group_name, group in hub["groups"].items(): + for project_name, project in group["projects"].items(): + entry = { + "hub": hub_name, + "group": group_name, + "project": project_name, + } + + # Move to the top if it is the default h/g/p. + if project.get("isDefault"): + hubs.insert(0, entry) + else: + hubs.append(entry) + + return hubs + + # Miscellaneous public functions. + + def api_version(self) -> Dict[str, Union[str, bool]]: + """Return the version of the API. + + Returns: + API version. + """ + return self.base_api.version() + + def current_access_token(self) -> Optional[str]: + """Return the current access token. + + Returns: + The access token in use. + """ + return self.access_token + + def current_service_urls(self) -> Dict: + """Return the current service URLs. + + Returns: + A dict with the base URLs for the services, in the same + format as :meth:`user_urls()`. + """ + return self._service_urls diff --git a/qiskit_ibm_runtime/api/clients/backend.py b/qiskit_ibm_runtime/api/clients/backend.py index d779ea7f59..b53996ff53 100644 --- a/qiskit_ibm_runtime/api/clients/backend.py +++ b/qiskit_ibm_runtime/api/clients/backend.py @@ -17,12 +17,10 @@ from datetime import datetime as python_datetime from abc import ABC, abstractmethod -from qiskit_ibm_provider.api.clients.base import BaseClient - logger = logging.getLogger(__name__) -class BaseBackendClient(BaseClient, ABC): +class BaseBackendClient(ABC): """Client for accessing backend information.""" @abstractmethod diff --git a/qiskit_ibm_runtime/api/clients/base_websocket_client.py b/qiskit_ibm_runtime/api/clients/base_websocket_client.py new file mode 100644 index 0000000000..d918eafc34 --- /dev/null +++ b/qiskit_ibm_runtime/api/clients/base_websocket_client.py @@ -0,0 +1,300 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. +# pylint: disable=unused-argument + +"""Base clients for accessing IBM Quantum.""" + +import logging +from typing import Optional, Any, Dict +from queue import Queue +from abc import ABC +from abc import abstractmethod +import traceback +import time +import enum + +from websocket import WebSocketApp, STATUS_NORMAL, STATUS_ABNORMAL_CLOSED + +from ..client_parameters import ClientParameters +from ..exceptions import WebsocketError, WebsocketTimeoutError + +logger = logging.getLogger(__name__) + + +class WebsocketClientCloseCode(enum.IntEnum): + """Possible values used for closing websocket connection.""" + + NORMAL = 1 + TIMEOUT = 2 + PROTOCOL_ERROR = 3 + CANCEL = 4 + + +class BaseWebsocketClient(ABC): + """Base class for websocket clients.""" + + BACKOFF_MAX = 8 + """Maximum time to wait between retries.""" + + def __init__( + self, + websocket_url: str, + client_params: ClientParameters, + job_id: str, + message_queue: Optional[Queue] = None, + ) -> None: + """BaseWebsocketClient constructor. + + Args: + websocket_url: URL for websocket communication with IBM Quantum. + client_params: Parameters used for server connection. + job_id: Job ID. + message_queue: Queue used to hold received messages. + """ + self._websocket_url = websocket_url.rstrip("/") + self._proxy_params = ( + client_params.proxies.to_ws_params(self._websocket_url) if client_params.proxies else {} + ) + self._access_token = client_params.token + self._job_id = job_id + self._message_queue = message_queue + self._header: Optional[Dict] = None + self._ws: Optional[WebSocketApp] = None + + self._authenticated = False + self._cancelled = False + self.connected = False + self._last_message: Any = None + self._current_retry = 0 + self._server_close_code = STATUS_ABNORMAL_CLOSED + self._client_close_code = None + self._error: Optional[str] = None + + def on_open(self, wsa: WebSocketApp) -> None: + """Called when websocket connection established. + + Args: + wsa: WebSocketApp object. + """ + logger.debug("Websocket connection established for job %s", self._job_id) + self.connected = True + if self._cancelled: + # Immediately disconnect if pre-cancelled. + self.disconnect(WebsocketClientCloseCode.CANCEL) + + def on_message(self, wsa: WebSocketApp, message: str) -> None: + """Called when websocket message received. + + Args: + wsa: WebSocketApp object. + message: Message received. + """ + try: + self._handle_message(message) + except Exception as err: # pylint: disable=broad-except + self._error = self._format_exception(err) + self.disconnect(WebsocketClientCloseCode.PROTOCOL_ERROR) + + @abstractmethod + def _handle_message(self, message: str) -> None: + """Handle received message. + + Args: + message: Message received. + """ + pass + + def on_close(self, wsa: WebSocketApp, status_code: int, msg: str) -> None: + """Called when websocket connection clsed. + + Args: + wsa: WebSocketApp object. + status_code: Status code. + msg: Close message. + """ + # Assume abnormal close if no code is given. + self._server_close_code = status_code or STATUS_ABNORMAL_CLOSED + self.connected = False + logger.debug( + "Websocket connection for job %s closed. status code=%s, message=%s", + self._job_id, + status_code, + msg, + ) + + def on_error(self, wsa: WebSocketApp, error: Exception) -> None: + """Called when a websocket error occurred. + + Args: + wsa: WebSocketApp object. + error: Encountered error. + """ + self._error = self._format_exception(error) + + def stream( + self, + url: str, + retries: int = 5, + backoff_factor: float = 0.5, + ) -> Any: + """Stream from the websocket. + + Args: + url: Websocket url to use. + retries: Max number of retries. + backoff_factor: Backoff factor used to calculate the + time to wait between retries. + + Returns: + The final message received. + + Raises: + WebsocketError: If the websocket connection ended unexpectedly. + WebsocketTimeoutError: If the operation timed out. + """ + self._reset_state() + self._cancelled = False + + while self._current_retry <= retries: + self._ws = WebSocketApp( + url, + header=self._header, + on_open=self.on_open, + on_message=self.on_message, + on_error=self.on_error, + on_close=self.on_close, + ) + try: + logger.debug( + "Starting new websocket connection: %s using proxy %s", + url, + self._proxy_params, + ) + self._reset_state() + self._ws.run_forever(ping_interval=60, ping_timeout=10, **self._proxy_params) + self.connected = False + + logger.debug("Websocket run_forever finished.") + + # Handle path-specific errors + self._handle_stream_iteration() + + if self._client_close_code in ( + WebsocketClientCloseCode.NORMAL, + WebsocketClientCloseCode.CANCEL, + ): + # If we closed the connection with a normal code. + return self._last_message + + if self._client_close_code == WebsocketClientCloseCode.TIMEOUT: + raise WebsocketTimeoutError( + "Timeout reached while getting job status." + ) from None + + if self._server_close_code == STATUS_NORMAL and self._error is None: + return self._last_message + + msg_to_log = ( + f"A websocket error occurred while streaming for job " + f"{self._job_id}. Connection closed with {self._server_close_code}." + ) + if self._error is not None: + msg_to_log += f"\n{self._error}" + logger.info(msg_to_log) + + self._current_retry += 1 + if self._current_retry > retries: + error_message = ( + "Max retries exceeded: Failed to establish a websocket connection." + ) + if self._error: + error_message += f" Error: {self._error}" + + raise WebsocketError(error_message) + finally: + self.disconnect(None) + + # Sleep then retry. + backoff_time = self._backoff_time(backoff_factor, self._current_retry) + logger.info( + "Retrying get_job_status via websocket after %s seconds: Attempt #%s", + backoff_time, + self._current_retry, + ) + time.sleep(backoff_time) + + # Execution should not reach here, sanity check. + exception_message = ( + "Max retries exceeded: Failed to establish a websocket " + "connection due to a network error." + ) + + logger.info(exception_message) + raise WebsocketError(exception_message) + + @abstractmethod + def _handle_stream_iteration(self) -> None: + """Called at the end of an iteration.""" + pass + + def _backoff_time(self, backoff_factor: float, current_retry_attempt: int) -> float: + """Calculate the backoff time to wait for. + + Exponential backoff time formula:: + {backoff_factor} * (2 ** (current_retry_attempt - 1)) + + Args: + backoff_factor: Backoff factor, in seconds. + current_retry_attempt: Current number of retry attempts. + + Returns: + The number of seconds to wait for, before making the next retry attempt. + """ + backoff_time = backoff_factor * (2 ** (current_retry_attempt - 1)) + return min(self.BACKOFF_MAX, backoff_time) + + def disconnect( + self, + close_code: Optional[WebsocketClientCloseCode] = WebsocketClientCloseCode.NORMAL, + ) -> None: + """Close the websocket connection. + + Args: + close_code: Disconnect status code. + """ + if self._ws is not None: + logger.debug("Client closing websocket connection with code %s.", close_code) + self._client_close_code = close_code + self._ws.close() + if close_code == WebsocketClientCloseCode.CANCEL: + self._cancelled = True + + def _format_exception(self, error: Exception) -> str: + """Format the exception. + + Args: + error: Exception to be formatted. + + Returns: + Formatted exception. + """ + return "".join( + traceback.format_exception(type(error), error, getattr(error, "__traceback__", "")) + ) + + def _reset_state(self) -> None: + """Reset state for a new connection.""" + self._authenticated = False + self.connected = False + self._error = None + self._server_close_code = None + self._client_close_code = None diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index e1e08c499b..ddbd41771a 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -17,13 +17,12 @@ from datetime import datetime as python_datetime from requests import Response -from qiskit_ibm_provider.utils.hgp import from_instance_format from qiskit_ibm_runtime.api.session import RetrySession from .backend import BaseBackendClient from ..rest.runtime import Runtime from ..client_parameters import ClientParameters - +from ...utils.hgp import from_instance_format logger = logging.getLogger(__name__) diff --git a/qiskit_ibm_runtime/api/clients/runtime_ws.py b/qiskit_ibm_runtime/api/clients/runtime_ws.py new file mode 100644 index 0000000000..bc88ed2768 --- /dev/null +++ b/qiskit_ibm_runtime/api/clients/runtime_ws.py @@ -0,0 +1,74 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Client for accessing IBM Quantum runtime service.""" + +import logging +from typing import Optional +from queue import Queue + +from .base_websocket_client import BaseWebsocketClient +from ..client_parameters import ClientParameters + +logger = logging.getLogger(__name__) + + +class RuntimeWebsocketClient(BaseWebsocketClient): + """Client for websocket communication with the IBM Quantum runtime service.""" + + def __init__( + self, + websocket_url: str, + client_params: ClientParameters, + job_id: str, + message_queue: Optional[Queue] = None, + ) -> None: + """WebsocketClient constructor. + + Args: + websocket_url: URL for websocket communication with IBM Quantum. + client_params: Parameters used for server connection. + job_id: Job ID. + message_queue: Queue used to hold received messages. + """ + super().__init__(websocket_url, client_params, job_id, message_queue) + self._header = client_params.get_auth_handler().get_headers() + + def _handle_message(self, message: str) -> None: + """Handle received message. + + Args: + message: Message received. + """ + if not self._authenticated: + self._authenticated = True # First message is an ACK + else: + self._message_queue.put_nowait(message) + self._current_retry = 0 + + def job_results(self, max_retries: int = 5, backoff_factor: float = 0.5) -> None: + """Return the interim result of a runtime job. + + Args: + max_retries: Max number of retries. + backoff_factor: Backoff factor used to calculate the + time to wait between retries. + + Raises: + WebsocketError: If a websocket error occurred. + """ + url = "{}/stream/jobs/{}".format(self._websocket_url, self._job_id) + self.stream(url=url, retries=max_retries, backoff_factor=backoff_factor) + + def _handle_stream_iteration(self) -> None: + """Handle a streaming iteration.""" + pass diff --git a/qiskit_ibm_runtime/api/clients/version.py b/qiskit_ibm_runtime/api/clients/version.py new file mode 100644 index 0000000000..c8628f04d5 --- /dev/null +++ b/qiskit_ibm_runtime/api/clients/version.py @@ -0,0 +1,46 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Client for determining the version of an IBM Quantum service.""" + +from typing import Dict, Union, Any + +from ..rest.root import Api +from ..session import RetrySession + + +class VersionClient: + """Client for determining the version of an IBM Quantum service.""" + + def __init__(self, url: str, **request_kwargs: Any) -> None: + """VersionClient constructor. + + Args: + url: URL of the service. + **request_kwargs: Arguments for the request ``Session``. + """ + self.client_version_finder = Api(RetrySession(url, **request_kwargs)) + + def version(self) -> Dict[str, Union[bool, str]]: + """Return the version information. + + Returns: + A dictionary with information about the API version, + with the following keys: + + * ``new_api`` (bool): Whether the new API is being used + + And the following optional keys: + + * ``api-*`` (str): The versions of each individual API component + """ + return self.client_version_finder.version() diff --git a/qiskit_ibm_runtime/api/rest/__init__.py b/qiskit_ibm_runtime/api/rest/__init__.py index 32092ddd98..f98193de6b 100644 --- a/qiskit_ibm_runtime/api/rest/__init__.py +++ b/qiskit_ibm_runtime/api/rest/__init__.py @@ -16,4 +16,4 @@ Job adaptor, for example, handles all /Jobs/{job id} endpoints. """ -from qiskit_ibm_provider.api.rest.root import Api +from .root import Api diff --git a/qiskit_ibm_runtime/api/rest/base.py b/qiskit_ibm_runtime/api/rest/base.py index 04e8ab0e60..809dc606fb 100644 --- a/qiskit_ibm_runtime/api/rest/base.py +++ b/qiskit_ibm_runtime/api/rest/base.py @@ -43,15 +43,3 @@ def get_url(self, identifier: str) -> str: The resolved URL of the endpoint (relative to the session base URL). """ return "{}{}".format(self.prefix_url, self.URL_MAP[identifier]) - - def get_prefixed_url(self, prefix: str, identifier: str) -> str: - """Return an adjusted URL for the specified identifier. - - Args: - prefix: string to be prepended to the URL. - identifier: Internal identifier of the endpoint. - - Returns: - The resolved facade URL of the endpoint. - """ - return "{}{}{}".format(prefix, self.prefix_url, self.URL_MAP[identifier]) diff --git a/qiskit_ibm_runtime/api/rest/cloud_backend.py b/qiskit_ibm_runtime/api/rest/cloud_backend.py index d15f2fc471..7964e5d58a 100644 --- a/qiskit_ibm_runtime/api/rest/cloud_backend.py +++ b/qiskit_ibm_runtime/api/rest/cloud_backend.py @@ -15,7 +15,7 @@ from typing import Dict, Any, Optional from datetime import datetime as python_datetime -from qiskit_ibm_provider.api.rest.base import RestAdapterBase +from qiskit_ibm_runtime.api.rest.base import RestAdapterBase from ..session import RetrySession diff --git a/qiskit_ibm_runtime/api/rest/program_job.py b/qiskit_ibm_runtime/api/rest/program_job.py new file mode 100644 index 0000000000..e50d10af70 --- /dev/null +++ b/qiskit_ibm_runtime/api/rest/program_job.py @@ -0,0 +1,109 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Program Job REST adapter.""" + +import json +from typing import Dict +from requests import Response + +from .base import RestAdapterBase +from ..session import RetrySession +from ...utils.json import RuntimeDecoder + + +class ProgramJob(RestAdapterBase): + """Rest adapter for program job related endpoints.""" + + URL_MAP = { + "self": "", + "results": "/results", + "cancel": "/cancel", + "logs": "/logs", + "interim_results": "/interim_results", + "metrics": "/metrics", + "tags": "/tags", + } + + def __init__(self, session: RetrySession, job_id: str, url_prefix: str = "") -> None: + """ProgramJob constructor. + + Args: + session: Session to be used in the adapter. + job_id: ID of the program job. + url_prefix: Prefix to use in the URL. + """ + super().__init__(session, "{}/jobs/{}".format(url_prefix, job_id)) + + def get(self, exclude_params: bool = None) -> Dict: + """Return program job information. + + Args: + exclude_params: If ``True``, the params will not be included in the response. + + Returns: + JSON response. + """ + payload = {} + if exclude_params: + payload["exclude_params"] = "true" + return self.session.get(self.get_url("self"), params=payload).json(cls=RuntimeDecoder) + + def delete(self) -> None: + """Delete program job.""" + self.session.delete(self.get_url("self")) + + def interim_results(self) -> str: + """Return program job interim results. + + Returns: + Interim results. + """ + response = self.session.get(self.get_url("interim_results")) + return response.text + + def results(self) -> str: + """Return program job results. + + Returns: + Job results. + """ + response = self.session.get(self.get_url("results")) + return response.text + + def cancel(self) -> None: + """Cancel the job.""" + self.session.post(self.get_url("cancel")) + + def logs(self) -> str: + """Retrieve job logs. + + Returns: + Job logs. + """ + return self.session.get(self.get_url("logs")).text + + def metadata(self) -> Dict: + """Retrieve job metadata. + + Returns: + Job Metadata. + """ + return self.session.get(self.get_url("metrics")).json() + + def update_tags(self, tags: list) -> Response: + """Update job tags. + + Returns: + API Response. + """ + return self.session.put(self.get_url("tags"), data=json.dumps({"tags": tags})) diff --git a/qiskit_ibm_runtime/api/rest/root.py b/qiskit_ibm_runtime/api/rest/root.py new file mode 100644 index 0000000000..f5f47af701 --- /dev/null +++ b/qiskit_ibm_runtime/api/rest/root.py @@ -0,0 +1,103 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Root REST adapter.""" + +import logging +from typing import Dict, List, Any, Union +import json + +from .base import RestAdapterBase +from .program_job import ProgramJob + +logger = logging.getLogger(__name__) + + +class Api(RestAdapterBase): + """Rest adapter for general endpoints.""" + + URL_MAP = { + "login": "/users/loginWithToken", + "user_info": "/users/me", + "hubs": "/Network", + "version": "/version", + "bookings": "/Network/bookings/v2", + } + + def job(self, job_id: str) -> ProgramJob: + """Return an adapter for the job. + + Args: + job_id: ID of the job. + + Returns: + The backend adapter. + """ + return ProgramJob(self.session, job_id) + + # Client functions. + + def hubs(self) -> List[Dict[str, Any]]: + """Return the list of hub/group/project sets available to the user. + + Returns: + JSON response. + """ + url = self.get_url("hubs") + return self.session.get(url).json() + + def version(self) -> Dict[str, Union[str, bool]]: + """Return the version information. + + Returns: + A dictionary with information about the API version, + with the following keys: + + * ``new_api`` (bool): Whether the new API is being used + + And the following optional keys: + + * ``api-*`` (str): The versions of each individual API component + """ + url = self.get_url("version") + response = self.session.get(url) + + try: + version_info = response.json() + version_info["new_api"] = True + except json.JSONDecodeError: + return {"new_api": False, "api": response.text} + + return version_info + + def login(self, api_token: str) -> Dict[str, Any]: + """Login with token. + + Args: + api_token: API token. + + Returns: + JSON response. + """ + url = self.get_url("login") + return self.session.post(url, json={"apiToken": api_token}).json() + + def user_info(self) -> Dict[str, Any]: + """Return user information. + + Returns: + JSON response of user information. + """ + url = self.get_url("user_info") + response = self.session.get(url).json() + + return response diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index b5a58648b0..354fc4e072 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -17,9 +17,9 @@ from typing import Dict, Any, List, Union, Optional import json -from qiskit_ibm_provider.api.rest.base import RestAdapterBase -from qiskit_ibm_provider.api.rest.program_job import ProgramJob -from qiskit_ibm_provider.utils import local_to_utc +from qiskit_ibm_runtime.api.rest.base import RestAdapterBase +from qiskit_ibm_runtime.api.rest.program_job import ProgramJob +from qiskit_ibm_runtime.utils import local_to_utc from .runtime_session import RuntimeSession from ...utils import RuntimeEncoder diff --git a/qiskit_ibm_runtime/base_primitive.py b/qiskit_ibm_runtime/base_primitive.py index ca93ebfde9..effaa68979 100644 --- a/qiskit_ibm_runtime/base_primitive.py +++ b/qiskit_ibm_runtime/base_primitive.py @@ -22,7 +22,7 @@ from qiskit.providers.options import Options as TerraOptions -from qiskit_ibm_provider.session import get_cm_session as get_cm_provider_session +from .provider_session import get_cm_session as get_cm_provider_session from .options import Options from .options.options import BaseOptions, OptionsV2 diff --git a/qiskit_ibm_runtime/estimator.py b/qiskit_ibm_runtime/estimator.py index 8d2ba9b80b..ba7e56f4d3 100644 --- a/qiskit_ibm_runtime/estimator.py +++ b/qiskit_ibm_runtime/estimator.py @@ -16,7 +16,6 @@ import os from typing import Optional, Dict, Sequence, Any, Union import logging -import typing from qiskit.circuit import QuantumCircuit from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -35,9 +34,6 @@ # pylint: disable=unused-import,cyclic-import from .session import Session -if typing.TYPE_CHECKING: - from qiskit.opflow import PauliSumOp - logger = logging.getLogger(__name__) @@ -236,7 +232,7 @@ def __init__( def run( # pylint: disable=arguments-differ self, circuits: QuantumCircuit | Sequence[QuantumCircuit], - observables: BaseOperator | PauliSumOp | Sequence[BaseOperator | PauliSumOp], + observables: BaseOperator | Sequence[BaseOperator], parameter_values: Sequence[float] | Sequence[Sequence[float]] | None = None, **kwargs: Any, ) -> RuntimeJob: @@ -272,7 +268,7 @@ def run( # pylint: disable=arguments-differ def _run( # pylint: disable=arguments-differ self, circuits: Sequence[QuantumCircuit], - observables: Sequence[BaseOperator | PauliSumOp], + observables: Sequence[BaseOperator], parameter_values: Sequence[Sequence[float]], **kwargs: Any, ) -> RuntimeJob: diff --git a/qiskit_ibm_runtime/exceptions.py b/qiskit_ibm_runtime/exceptions.py index 9b4b1ee1c4..f2fe7be110 100644 --- a/qiskit_ibm_runtime/exceptions.py +++ b/qiskit_ibm_runtime/exceptions.py @@ -28,12 +28,30 @@ class IBMAccountError(IBMError): pass -class IBMBackendApiProtocolError(IBMError): +class IBMBackendError(IBMError): + """Base class for errors raised by the backend modules.""" + + pass + + +class IBMBackendApiProtocolError(IBMBackendError): """Errors raised when an unexpected value is received from the server.""" pass +class IBMBackendValueError(IBMBackendError, ValueError): + """Value errors raised by the backend modules.""" + + pass + + +class IBMBackendApiError(IBMBackendError): + """Errors that occur unexpectedly when querying the server.""" + + pass + + class IBMInputValueError(IBMError): """Error raised due to invalid input value.""" diff --git a/qiskit_ibm_runtime/fake_provider/__init__.py b/qiskit_ibm_runtime/fake_provider/__init__.py index 722ed02dc1..c35804a291 100644 --- a/qiskit_ibm_runtime/fake_provider/__init__.py +++ b/qiskit_ibm_runtime/fake_provider/__init__.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. """ -====================================================== +======================================================= Fake Provider (:mod:`qiskit_ibm_runtime.fake_provider`) -====================================================== +======================================================= .. currentmodule:: qiskit_ibm_runtime.fake_provider @@ -48,11 +48,11 @@ circuit.cx(0,1) circuit.cx(0,2) circuit.measure_all() - circuit.draw('mpl') + circuit.draw('mpl', style="iqp") # Transpile the ideal circuit to a circuit that can be directly executed by the backend transpiled_circuit = transpile(circuit, backend) - transpiled_circuit.draw('mpl') + transpiled_circuit.draw('mpl', style="iqp") # Run the transpiled circuit using the simulated fake backend job = backend.run(transpiled_circuit) diff --git a/qiskit_ibm_runtime/fake_provider/fake_backend.py b/qiskit_ibm_runtime/fake_provider/fake_backend.py index 023d4beeee..58b7ac19b5 100644 --- a/qiskit_ibm_runtime/fake_provider/fake_backend.py +++ b/qiskit_ibm_runtime/fake_provider/fake_backend.py @@ -29,7 +29,6 @@ from qiskit import pulse from qiskit.exceptions import QiskitError from qiskit.utils import optionals as _optionals -from qiskit.providers import basicaer from qiskit.transpiler import Target from qiskit.providers import Options from qiskit.providers.backend_compat import convert_to_target @@ -39,6 +38,11 @@ decode_pulse_defaults, ) +try: + from qiskit.providers.basicaer import QasmSimulatorPy as BasicSimulator +except ImportError: + from qiskit.providers.basic_provider import BasicSimulator + class _Credentials: def __init__(self, token: str = "123456", url: str = "https://") -> None: @@ -128,7 +132,7 @@ def _setup_sim(self) -> None: self.set_options(noise_model=noise_model) else: - self.sim = basicaer.QasmSimulatorPy() + self.sim = BasicSimulator() def _get_conf_dict_from_json(self) -> dict: if not self.conf_filename: @@ -205,7 +209,7 @@ def _default_options(cls) -> Options: return AerSimulator._default_options() else: - return basicaer.QasmSimulatorPy._default_options() + return BasicSimulator._default_options() @property def dtm(self) -> float: @@ -304,12 +308,12 @@ def run(self, run_input, **options): # type: ignore This method runs circuit jobs (an individual or a list of QuantumCircuit ) and pulse jobs (an individual or a list of Schedule or ScheduleBlock) - using BasicAer or Aer simulator and returns a + using BasicAer simulator/ BasicSimulator or Aer simulator and returns a :class:`~qiskit.providers.Job` object. If qiskit-aer is installed, jobs will be run using AerSimulator with noise model of the fake backend. Otherwise, jobs will be run using - BasicAer simulator without noise. + BasicAer simulator/ BasicSimulator simulator without noise. Currently noisy simulation of a pulse job is not supported yet in FakeBackendV2. @@ -475,7 +479,7 @@ def _setup_sim(self) -> None: # it when run() is called self.set_options(noise_model=noise_model) else: - self.sim = basicaer.QasmSimulatorPy() + self.sim = BasicSimulator() def properties(self) -> BackendProperties: """Return backend properties""" @@ -536,7 +540,7 @@ def _default_options(cls) -> Options: return QasmSimulator._default_options() else: - return basicaer.QasmSimulatorPy._default_options() + return BasicSimulator._default_options() def run(self, run_input, **kwargs): # type: ignore """Main job in simulator""" diff --git a/qiskit_ibm_runtime/hub_group_project.py b/qiskit_ibm_runtime/hub_group_project.py index 1b213eadbc..6fe5a60c55 100644 --- a/qiskit_ibm_runtime/hub_group_project.py +++ b/qiskit_ibm_runtime/hub_group_project.py @@ -15,7 +15,6 @@ import logging from typing import Any, List -from qiskit_ibm_provider.utils.hgp import from_instance_format from qiskit_ibm_runtime import ( # pylint: disable=unused-import ibm_backend, qiskit_runtime_service, @@ -23,6 +22,7 @@ from .api.client_parameters import ClientParameters from .api.clients import RuntimeClient +from .utils.hgp import from_instance_format logger = logging.getLogger(__name__) diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py index 77374df2d4..accf571d09 100644 --- a/qiskit_ibm_runtime/ibm_backend.py +++ b/qiskit_ibm_runtime/ibm_backend.py @@ -40,39 +40,31 @@ ) from qiskit.transpiler.target import Target -from qiskit_ibm_provider.utils.backend_decoder import ( - defaults_from_server_data, - properties_from_server_data, -) -from qiskit_ibm_provider.utils import local_to_utc, are_circuits_dynamic -from qiskit_ibm_provider.utils.options import QASM2Options, QASM3Options -from qiskit_ibm_provider.exceptions import IBMBackendValueError, IBMBackendApiError -from qiskit_ibm_provider.api.exceptions import RequestsApiError - # temporary until we unite the 2 Session classes -from qiskit_ibm_provider.session import ( +from .provider_session import ( Session as ProviderSession, -) # temporary until we unite the 2 Session classes +) from .utils.utils import validate_job_tags from . import qiskit_runtime_service # pylint: disable=unused-import,cyclic-import from .runtime_job import RuntimeJob from .api.clients import RuntimeClient -from .api.clients.backend import BaseBackendClient -from .exceptions import IBMBackendApiProtocolError +from .exceptions import IBMBackendApiProtocolError, IBMBackendValueError, IBMBackendApiError from .utils.backend_converter import ( convert_to_target, ) from .utils.default_session import get_cm_session as get_cm_primitive_session +from .utils.backend_decoder import ( + defaults_from_server_data, + properties_from_server_data, +) +from .utils.options import QASM2Options, QASM3Options +from .api.exceptions import RequestsApiError +from .utils import local_to_utc, are_circuits_dynamic + +from .utils.pubsub import Publisher -# If using a new-enough version of the IBM Provider, access the pub/sub -# mechanism from it as a broker, but fall back to Qiskit if we're using -# an old version (in which case it will also be falling back to Qiskit). -try: - from qiskit_ibm_provider.utils.pubsub import Publisher -except ImportError: - from qiskit.tools.events.pubsub import Publisher # pylint: disable=ungrouped-imports logger = logging.getLogger(__name__) @@ -179,7 +171,7 @@ def __init__( self, configuration: Union[QasmBackendConfiguration, PulseBackendConfiguration], service: "qiskit_runtime_service.QiskitRuntimeService", - api_client: BaseBackendClient, + api_client: RuntimeClient, instance: Optional[str] = None, ) -> None: """IBMBackend constructor. diff --git a/qiskit_ibm_runtime/ibm_qubit_properties.py b/qiskit_ibm_runtime/ibm_qubit_properties.py new file mode 100644 index 0000000000..d6228bc4aa --- /dev/null +++ b/qiskit_ibm_runtime/ibm_qubit_properties.py @@ -0,0 +1,54 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Module for Qubit Properties of an IBM Quantum Backend.""" + +from qiskit.providers.backend import QubitProperties + + +class IBMQubitProperties(QubitProperties): + """A representation of the properties of a qubit on an IBM backend.""" + + __slots__ = ( # pylint: disable=redefined-slots-in-subclass + "t1", + "t2", + "frequency", + "anharmonicity", + "operational", + ) + + def __init__( # type: ignore[no-untyped-def] + self, + t1=None, + t2=None, + frequency=None, + anharmonicity=None, + operational=True, + ): + """Create a new ``IBMQubitProperties`` object + + Args: + t1: The T1 time for a qubit in secs + t2: The T2 time for a qubit in secs + frequency: The frequency of a qubit in Hz + anharmonicity: The anharmonicity of a qubit in Hz + operational: A boolean value representing if this qubit is operational. + """ + super().__init__(t1=t1, t2=t2, frequency=frequency) + self.anharmonicity = anharmonicity + self.operational = operational + + def __repr__(self): # type: ignore[no-untyped-def] + return ( + f"IBMQubitProperties(t1={self.t1}, t2={self.t2}, frequency={self.frequency}, " + f"anharmonicity={self.anharmonicity})" + ) diff --git a/qiskit_ibm_runtime/provider_session.py b/qiskit_ibm_runtime/provider_session.py new file mode 100644 index 0000000000..dad3dc4079 --- /dev/null +++ b/qiskit_ibm_runtime/provider_session.py @@ -0,0 +1,132 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Qiskit Runtime flexible session.""" + +from typing import Optional, Type, Union +from types import TracebackType +from contextvars import ContextVar + +from .utils.converters import hms_to_seconds + + +class Session: + """Class for creating a flexible Qiskit Runtime session. + + A Qiskit Runtime ``session`` allows you to group a collection of iterative calls to + the quantum computer. A session is started when the first job within the session + is started. Subsequent jobs within the session are prioritized by the scheduler. + Data used within a session, such as transpiled circuits, is also cached to avoid + unnecessary overhead. + + You can open a Qiskit Runtime session using this ``Session`` class + and submit one or more jobs. + + For example:: + + from qiskit.test.reference_circuits import ReferenceCircuits + from qiskit_ibm_runtime import QiskitRuntimeService + + circ = ReferenceCircuits.bell() + backend = QiskitRuntimeService().get_backend("ibmq_qasm_simulator") + + backend.open_session() + job = backend.run(circ) + print(f"Job ID: {job.job_id()}") + print(f"Result: {job.result()}") + # Close the session only if all jobs are finished and + # you don't need to run more in the session. + backend.cancel_session() + + Session can also be used as a context manager:: + + with backend.open_session() as session: + job = backend.run(ReferenceCircuits.bell()) + + """ + + def __init__( + self, + max_time: Optional[Union[int, str]] = None, + ): + """Session constructor. + + Args: + 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 + `system imposed maximum + `_. + + Raises: + ValueError: If an input value is invalid. + """ + self._instance = None + self._session_id: Optional[str] = None + self._active = True + + self._max_time = ( + max_time + if max_time is None or isinstance(max_time, int) + else hms_to_seconds(max_time, "Invalid max_time value: ") + ) + + @property + def session_id(self) -> str: + """Return the session ID. + + Returns: + Session ID. None until a job runs in the session. + """ + return self._session_id + + @property + def active(self) -> bool: + """Return the status of the session. + + Returns: + True if the session is active, False otherwise. + """ + return self._active + + def cancel(self) -> None: + """Set the session._active status to False""" + self._active = False + + def __enter__(self) -> "Session": + set_cm_session(self) + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + set_cm_session(None) + + +# Default session +_DEFAULT_SESSION: ContextVar[Optional[Session]] = ContextVar("_DEFAULT_SESSION", default=None) +_IN_SESSION_CM: ContextVar[bool] = ContextVar("_IN_SESSION_CM", default=False) + + +def set_cm_session(session: Optional[Session]) -> None: + """Set the context manager session.""" + _DEFAULT_SESSION.set(session) + _IN_SESSION_CM.set(session is not None) + + +def get_cm_session() -> Session: + """Return the context managed session.""" + return _DEFAULT_SESSION.get() diff --git a/qiskit_ibm_runtime/proxies/__init__.py b/qiskit_ibm_runtime/proxies/__init__.py new file mode 100644 index 0000000000..c0212bfb0d --- /dev/null +++ b/qiskit_ibm_runtime/proxies/__init__.py @@ -0,0 +1,17 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Proxy configuration. +""" + +from .configuration import ProxyConfiguration diff --git a/qiskit_ibm_runtime/proxies/configuration.py b/qiskit_ibm_runtime/proxies/configuration.py new file mode 100644 index 0000000000..0ed390dad3 --- /dev/null +++ b/qiskit_ibm_runtime/proxies/configuration.py @@ -0,0 +1,127 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Proxy related classes and functions.""" + + +from dataclasses import dataclass +from typing import Optional, Dict, Any +from urllib.parse import urlparse + +from requests_ntlm import HttpNtlmAuth + + +@dataclass +class ProxyConfiguration: + """Class for representing a proxy configuration. + + Args + urls: a dictionary mapping protocol or protocol and host to the URL of the proxy. Refer to + https://docs.python-requests.org/en/latest/api/#requests.Session.proxies for details. + username_ntlm: username used to enable NTLM user authentication. + password_ntlm: password used to enable NTLM user authentication. + """ + + urls: Optional[Dict[str, str]] = None + username_ntlm: Optional[str] = None + password_ntlm: Optional[str] = None + + def validate(self) -> None: + """Validate configuration. + + Raises: + ValueError: If configuration is invalid. + """ + if not any( + [ + isinstance(self.username_ntlm, str) and isinstance(self.password_ntlm, str), + self.username_ntlm is None and self.password_ntlm is None, + ] + ): + raise ValueError( + f"Invalid proxy configuration for NTLM authentication. None or both of username and " + f"password must be provided. Got username_ntlm={self.username_ntlm}, " + f"password_ntlm={self.password_ntlm}." + ) + + if self.urls is not None and not isinstance(self.urls, dict): + raise ValueError( + f"Invalid proxy configuration. Expected `urls` to contain a dictionary mapping protocol " + f"or protocol and host to the URL of the proxy. Got {self.urls}" + ) + + def to_dict(self) -> dict: + """Transform configuration to dictionary.""" + + return {k: v for k, v in self.__dict__.items() if v is not None} + + def to_request_params(self) -> dict: + """Transform configuration to request parameters. + + Returns: + A dictionary with proxy configuration parameters in the format + expected by ``requests``. The following keys can be present: + ``proxies``and ``auth``. + """ + + request_kwargs = {} + if self.urls: + request_kwargs["proxies"] = self.urls + + if self.username_ntlm and self.password_ntlm: + request_kwargs["auth"] = HttpNtlmAuth(self.username_ntlm, self.password_ntlm) + + return request_kwargs + + def to_ws_params(self, ws_url: str) -> dict: + """Extract proxy information for websocket. + + Args: + ws_url: Websocket URL. + + Returns: + A dictionary with proxy configuration parameters in the format expected by websocket. + The following keys can be present: ``http_proxy_host``and ``http_proxy_port``, + ``proxy_type``, ``http_proxy_auth``. + """ + out: Any = {} + + if self.urls: + proxies = self.urls + url_parts = urlparse(ws_url) + proxy_keys = [ + ws_url, + "wss", + "https://" + url_parts.hostname, + "https", + "all://" + url_parts.hostname, + "all", + ] + for key in proxy_keys: + if key in proxies: + proxy_parts = urlparse(proxies[key], scheme="http") + out["http_proxy_host"] = proxy_parts.hostname + out["http_proxy_port"] = proxy_parts.port + out["proxy_type"] = ( + "http" if proxy_parts.scheme.startswith("http") else proxy_parts.scheme + ) + if proxy_parts.username and proxy_parts.password: + out["http_proxy_auth"] = ( + proxy_parts.username, + proxy_parts.password, + ) + break + + if self.username_ntlm and self.password_ntlm: + out["http_proxy_auth"] = (self.username_ntlm, self.password_ntlm) + + return out diff --git a/qiskit_ibm_runtime/qiskit_runtime_service.py b/qiskit_ibm_runtime/qiskit_runtime_service.py index f3cbdda4c9..681c49ef91 100644 --- a/qiskit_ibm_runtime/qiskit_runtime_service.py +++ b/qiskit_ibm_runtime/qiskit_runtime_service.py @@ -29,10 +29,11 @@ QasmBackendConfiguration, ) -from qiskit_ibm_provider.proxies import ProxyConfiguration -from qiskit_ibm_provider.utils.hgp import to_instance_format, from_instance_format -from qiskit_ibm_provider.utils.backend_decoder import configuration_from_server_data from qiskit_ibm_runtime import ibm_backend +from .proxies import ProxyConfiguration +from .utils.hgp import to_instance_format, from_instance_format +from .utils.backend_decoder import configuration_from_server_data + from .utils.utils import validate_job_tags from .accounts import AccountManager, Account, ChannelType diff --git a/qiskit_ibm_runtime/runtime_job.py b/qiskit_ibm_runtime/runtime_job.py index e14843fb4a..5bd2e1d7b7 100644 --- a/qiskit_ibm_runtime/runtime_job.py +++ b/qiskit_ibm_runtime/runtime_job.py @@ -28,9 +28,10 @@ from qiskit.providers.job import JobV1 as Job # pylint: disable=unused-import,cyclic-import -from qiskit_ibm_provider.utils import utc_to_local + from qiskit_ibm_runtime import qiskit_runtime_service +from .utils import utc_to_local from .utils.utils import validate_job_tags from .utils.estimator_result_decoder import EstimatorResultDecoder from .utils.queueinfo import QueueInfo diff --git a/qiskit_ibm_runtime/session.py b/qiskit_ibm_runtime/session.py index cd9d665021..cb9da574da 100644 --- a/qiskit_ibm_runtime/session.py +++ b/qiskit_ibm_runtime/session.py @@ -17,14 +17,13 @@ from functools import wraps from threading import Lock -from qiskit_ibm_provider.utils.converters import hms_to_seconds - from qiskit_ibm_runtime import QiskitRuntimeService from .runtime_job import RuntimeJob from .utils.result_decoder import ResultDecoder from .ibm_backend import IBMBackend from .utils.default_session import set_cm_session from .utils.deprecation import deprecate_arguments +from .utils.converters import hms_to_seconds def _active_session(func): # type: ignore diff --git a/qiskit_ibm_runtime/transpiler/__init__.py b/qiskit_ibm_runtime/transpiler/__init__.py index d6e62daa4e..838b709267 100644 --- a/qiskit_ibm_runtime/transpiler/__init__.py +++ b/qiskit_ibm_runtime/transpiler/__init__.py @@ -12,7 +12,7 @@ """ ==================================================================== -IBM Backend Transpiler Tools (:mod:`qiskit_ibm_provider.transpiler`) +IBM Backend Transpiler Tools (:mod:`qiskit_ibm_runtime.transpiler`) ==================================================================== A collection of transpiler tools for working with IBM Quantum's diff --git a/qiskit_ibm_runtime/transpiler/passes/__init__.py b/qiskit_ibm_runtime/transpiler/passes/__init__.py index 2fe16514ca..2bd2bf1812 100644 --- a/qiskit_ibm_runtime/transpiler/passes/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/__init__.py @@ -12,10 +12,10 @@ """ ================================================================ -Transpiler Passes (:mod:`qiskit_ibm_provider.transpiler.passes`) +Transpiler Passes (:mod:`qiskit_ibm_runtime.transpiler.passes`) ================================================================ -.. currentmodule:: qiskit_ibm_provider.transpiler.passes +.. currentmodule:: qiskit_ibm_runtime.transpiler.passes A collection of transpiler passes for IBM backends. diff --git a/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py b/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py index 0a71af0102..e9e19bc135 100644 --- a/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/basis/__init__.py @@ -12,10 +12,10 @@ """ ========================================================== -Basis (:mod:`qiskit_ibm_provider.transpiler.passes.basis`) +Basis (:mod:`qiskit_ibm_runtime.transpiler.passes.basis`) ========================================================== -.. currentmodule:: qiskit_ibm_provider.transpiler.passes.basis +.. currentmodule:: qiskit_ibm_runtime.transpiler.passes.basis Passes to layout circuits to IBM backend's instruction sets. """ diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py index c3017e9bc7..90308b73c6 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/__init__.py @@ -12,10 +12,10 @@ """ ==================================================================== -Scheduling (:mod:`qiskit_ibm_provider.transpiler.passes.scheduling`) +Scheduling (:mod:`qiskit_ibm_runtime.transpiler.passes.scheduling`) ==================================================================== -.. currentmodule:: qiskit_ibm_provider.transpiler.passes.scheduling +.. currentmodule:: qiskit_ibm_runtime.transpiler.passes.scheduling A collection of scheduling passes for working with IBM Quantum's next-generation backends that support advanced "dynamic circuit" capabilities. Ie., @@ -38,11 +38,10 @@ from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager from qiskit.transpiler.passmanager import PassManager - from qiskit_ibm_provider.transpiler.passes.scheduling import DynamicCircuitInstructionDurations - from qiskit_ibm_provider.transpiler.passes.scheduling import ALAPScheduleAnalysis - from qiskit_ibm_provider.transpiler.passes.scheduling import PadDelay - from qiskit.providers.fake_provider import FakeJakarta - + from qiskit_ibm_runtime.transpiler.passes.scheduling import DynamicCircuitInstructionDurations + from qiskit_ibm_runtime.transpiler.passes.scheduling import ALAPScheduleAnalysis + from qiskit_ibm_runtime.transpiler.passes.scheduling import PadDelay + from qiskit_ibm_runtime.fake_provider import FakeJakarta backend = FakeJakarta() @@ -80,7 +79,7 @@ # Transpile. scheduled_teleport = pm.run(teleport) - scheduled_teleport.draw(output="mpl") + scheduled_teleport.draw(output="mpl", style="iqp") Instead of padding with delays we may also insert a dynamical decoupling sequence @@ -90,7 +89,7 @@ from qiskit.circuit.library import XGate - from qiskit_ibm_provider.transpiler.passes.scheduling import PadDynamicalDecoupling + from qiskit_ibm_runtime.transpiler.passes.scheduling import PadDynamicalDecoupling dd_sequence = [XGate(), XGate()] @@ -105,7 +104,7 @@ dd_teleport = pm.run(teleport) - dd_teleport.draw(output="mpl") + dd_teleport.draw(output="mpl", style="iqp") When compiling a circuit with Qiskit, it is more efficient and more robust to perform all the transformations in a single transpilation. This has been done above by extending Qiskit's preset @@ -123,7 +122,7 @@ qc_c_if = QuantumCircuit(1, 1) qc_c_if.x(0).c_if(0, 1) - qc_c_if.draw(output="mpl") + qc_c_if.draw(output="mpl", style="iqp") The :class:`.IBMBackend` configures a translation plugin :class:`.IBMTranslationPlugin` to automatically @@ -146,7 +145,7 @@ ) qc_if_dd = pm.run(qc_c_if, backend) - qc_if_dd.draw(output="mpl") + qc_if_dd.draw(output="mpl", style="iqp") If you are not using the transpiler plugin stages to @@ -168,7 +167,7 @@ ) qc_if_dd = pm.run(qc_c_if) - qc_if_dd.draw(output="mpl") + qc_if_dd.draw(output="mpl", style="iqp") Exploiting IBM backend's local parallel "fast-path" @@ -194,7 +193,7 @@ with qc.if_test((1, 1)): qc.x(1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") The circuit below will not use the fast-path as the conditional gate is @@ -207,7 +206,7 @@ with qc.if_test((0, 1)): qc.x(1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") Similarly, the circuit below contains gates on multiple qubits and will not be performed using the fast-path. @@ -220,7 +219,7 @@ qc.x(0) qc.x(1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") A fast-path block may contain multiple gates as long as they are on the fast-path qubit. If there are multiple fast-path blocks being performed in parallel each block will be @@ -238,7 +237,7 @@ with qc.if_test((1, 1)): qc.delay(1600, 1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") This behavior is also applied to the else condition of a fast-path eligible branch. @@ -253,7 +252,7 @@ with else_: qc.delay(1600, 0) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") If a single measurement result is used with several conditional blocks, if there is a fast-path @@ -272,7 +271,7 @@ # Does not use fast-path qc.x(1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") If you wish to prevent the usage of the fast-path you may insert a barrier between the measurement and the conditional branch. @@ -286,7 +285,7 @@ with qc.if_test((0, 1)): qc.x(0) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") Conditional measurements are not eligible for the fast-path. @@ -298,7 +297,7 @@ # Does not use the fast-path qc.measure(0, 1) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") Similarly nested control-flow is not eligible. @@ -312,7 +311,7 @@ with qc.if_test((0, 1)): qc.x(0) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") The scheduler is aware of the fast-path behavior and will not insert delays on idle qubits @@ -345,11 +344,11 @@ qc.delay(1000, 0) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") qc_dd = pm.run(qc) - qc_dd.draw(output="mpl") + qc_dd.draw(output="mpl", style="iqp") .. note:: If there are qubits that are *not* involved in a fast-path decision it is not @@ -374,7 +373,7 @@ # since the condition is compile time evaluated. qc.x(2) - qc.draw(output="mpl") + qc.draw(output="mpl", style="iqp") Scheduling & Dynamical Decoupling diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py index 006c53febc..77d893f573 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/dynamical_decoupling.py @@ -23,12 +23,16 @@ from qiskit.circuit.reset import Reset from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGInNode, DAGOpNode from qiskit.quantum_info.operators.predicates import matrix_equal -from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.passes.optimization import Optimize1qGates from qiskit.transpiler import CouplingMap +try: + from qiskit.quantum_info.synthesis import OneQubitEulerDecomposer +except ImportError: + from qiskit.synthesis import OneQubitEulerDecomposer + from .block_base_padder import BlockBasePadder @@ -56,8 +60,8 @@ class PadDynamicalDecoupling(BlockBasePadder): from qiskit.transpiler import PassManager, InstructionDurations from qiskit.visualization import timeline_drawer - from qiskit_ibm_provider.transpiler.passes.scheduling import ALAPScheduleAnalysis - from qiskit_ibm_provider.transpiler.passes.scheduling import PadDynamicalDecoupling + from qiskit_ibm_runtime.transpiler.passes.scheduling import ALAPScheduleAnalysis + from qiskit_ibm_runtime.transpiler.passes.scheduling import PadDynamicalDecoupling circ = QuantumCircuit(4) circ.h(0) @@ -103,7 +107,7 @@ def uhrig_pulse_location(k): .. note:: You need to call - :class:`~qiskit_ibm_provider.transpiler.passes.scheduling.ALAPScheduleAnalysis` + :class:`~qiskit_ibm_runtime.transpiler.passes.scheduling.ALAPScheduleAnalysis` before running dynamical decoupling to guarantee your circuit satisfies acquisition alignment constraints for dynamic circuit backends. """ @@ -321,7 +325,11 @@ def _pre_runhook(self, dag: DAGCircuit) -> None: self._dd_sequence_lengths[qubit] = [] physical_index = dag.qubits.index(qubit) - if self._qubits and physical_index not in self._qubits: + if ( + self._qubits + and physical_index not in self._qubits + or qubit in self._idle_qubits + ): continue for index, gate in enumerate(seq): diff --git a/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py b/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py index bf7665cd19..ec4710492c 100644 --- a/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py +++ b/qiskit_ibm_runtime/transpiler/passes/scheduling/utils.py @@ -21,7 +21,9 @@ InstructionDurations, InstructionDurationsType, ) +from qiskit.transpiler.target import Target from qiskit.transpiler.exceptions import TranspilerError +from qiskit.providers import Backend, BackendV1 def block_order_op_nodes(dag: DAGCircuit) -> Generator[DAGOpNode, None, None]: @@ -150,6 +152,75 @@ def __init__( self._enable_patching = enable_patching super().__init__(instruction_durations=instruction_durations, dt=dt) + @classmethod + def from_backend(cls, backend: Backend) -> "DynamicCircuitInstructionDurations": + """Construct a :class:`DynamicInstructionDurations` object from the backend. + Args: + backend: backend from which durations (gate lengths) and dt are extracted. + Returns: + DynamicInstructionDurations: The InstructionDurations constructed from backend. + """ + if isinstance(backend, BackendV1): + # TODO Remove once https://github.com/Qiskit/qiskit/pull/11727 gets released in qiskit 0.46.1 + # From here --------------------------------------- + def patch_from_backend(cls, backend: Backend): # type: ignore + """ + REMOVE me once https://github.com/Qiskit/qiskit/pull/11727 gets released in qiskit 0.46.1 + """ + instruction_durations = [] + backend_properties = backend.properties() + if hasattr(backend_properties, "_gates"): + for gate, insts in backend_properties._gates.items(): + for qubits, props in insts.items(): + if "gate_length" in props: + gate_length = props["gate_length"][ + 0 + ] # Throw away datetime at index 1 + instruction_durations.append((gate, qubits, gate_length, "s")) + for ( + q, # pylint: disable=invalid-name + props, + ) in backend.properties()._qubits.items(): + if "readout_length" in props: + readout_length = props["readout_length"][ + 0 + ] # Throw away datetime at index 1 + instruction_durations.append(("measure", [q], readout_length, "s")) + try: + dt = backend.configuration().dt + except AttributeError: + dt = None + + return cls(instruction_durations, dt=dt) + + return patch_from_backend(DynamicCircuitInstructionDurations, backend) + # To here --------------------------------------- (remove comment ignore annotations too) + return super( # type: ignore # pylint: disable=unreachable + DynamicCircuitInstructionDurations, cls + ).from_backend(backend) + + # Get durations from target if BackendV2 + return cls.from_target(backend.target) + + @classmethod + def from_target(cls, target: Target) -> "DynamicCircuitInstructionDurations": + """Construct a :class:`DynamicInstructionDurations` object from the target. + Args: + target: target from which durations (gate lengths) and dt are extracted. + Returns: + DynamicInstructionDurations: The InstructionDurations constructed from backend. + """ + + instruction_durations_dict = target.durations().duration_by_name_qubits + instruction_durations = [] + for instr_key, instr_value in instruction_durations_dict.items(): + instruction_durations += [(*instr_key, *instr_value)] + try: + dt = target.dt + except AttributeError: + dt = None + return cls(instruction_durations, dt=dt) + def update( self, inst_durations: Optional[InstructionDurationsType], dt: float = None ) -> "DynamicCircuitInstructionDurations": @@ -206,15 +277,23 @@ def _patch_instruction(self, key: InstrKey) -> None: elif name == "reset": self._patch_reset(key) + def _convert_and_patch_key(self, key: InstrKey) -> None: + """Convert duration to dt and patch key""" + prev_duration, unit = self._get_duration(key) + if unit != "dt": + prev_duration = self._convert_unit(prev_duration, unit, "dt") + # raise TranspilerError('Can currently only patch durations of "dt".') + odd_cycle_correction = self._get_odd_cycle_correction() + new_duration = prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction + if unit != "dt": # convert back to original unit + new_duration = self._convert_unit(new_duration, "dt", unit) + self._patch_key(key, new_duration, unit) + def _patch_measurement(self, key: InstrKey) -> None: """Patch measurement duration by extending duration by 160dt as temporarily required by the dynamic circuit backend. """ - prev_duration, unit = self._get_duration_dt(key) - if unit != "dt": - raise TranspilerError('Can currently only patch durations of "dt".') - odd_cycle_correction = self._get_odd_cycle_correction() - self._patch_key(key, prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction, unit) + self._convert_and_patch_key(key) # Enforce patching of reset on measurement update self._patch_reset(("reset", key[1], key[2])) @@ -227,31 +306,24 @@ def _patch_reset(self, key: InstrKey) -> None: # triggers the end of scheduling after the measurement pulse measure_key = ("measure", key[1], key[2]) try: - measure_duration, unit = self._get_duration_dt(measure_key) + measure_duration, unit = self._get_duration(measure_key) self._patch_key(key, measure_duration, unit) except KeyError: # Fall back to reset key if measure not available - prev_duration, unit = self._get_duration_dt(key) - if unit != "dt": - raise TranspilerError('Can currently only patch durations of "dt".') - odd_cycle_correction = self._get_odd_cycle_correction() - self._patch_key( - key, - prev_duration + self.MEASURE_PATCH_CYCLES + odd_cycle_correction, - unit, - ) + self._convert_and_patch_key(key) - def _get_duration_dt(self, key: InstrKey) -> Tuple[int, str]: + def _get_duration(self, key: InstrKey) -> Tuple[int, str]: """Handling for the complicated structure of this class. TODO: This class implementation should be simplified in Qiskit. Too many edge cases. """ if key[1] is None and key[2] is None: - return self.duration_by_name[key[0]] + duration = self.duration_by_name[key[0]] elif key[2] is None: - return self.duration_by_name_qubits[(key[0], key[1])] - - return self.duration_by_name_qubits_params[key] + duration = self.duration_by_name_qubits[(key[0], key[1])] + else: + duration = self.duration_by_name_qubits_params[key] + return duration def _patch_key(self, key: InstrKey, duration: int, unit: str) -> None: """Handling for the complicated structure of this class. diff --git a/qiskit_ibm_runtime/transpiler/plugin.py b/qiskit_ibm_runtime/transpiler/plugin.py index 75f70cfe4c..0775343955 100644 --- a/qiskit_ibm_runtime/transpiler/plugin.py +++ b/qiskit_ibm_runtime/transpiler/plugin.py @@ -20,7 +20,7 @@ from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes import ConvertConditionsToIfOps -from qiskit_ibm_provider.transpiler.passes.basis.convert_id_to_delay import ( +from qiskit_ibm_runtime.transpiler.passes.basis.convert_id_to_delay import ( ConvertIdToDelay, ) diff --git a/qiskit_ibm_runtime/utils/__init__.py b/qiskit_ibm_runtime/utils/__init__.py index 04e10428b7..985f05b3af 100644 --- a/qiskit_ibm_runtime/utils/__init__.py +++ b/qiskit_ibm_runtime/utils/__init__.py @@ -35,11 +35,13 @@ to_python_identifier """ -from qiskit_ibm_provider.utils.converters import ( +from .converters import ( utc_to_local, local_to_utc, seconds_to_duration, duration_difference, + are_circuits_dynamic, ) from .utils import to_python_identifier, is_crn, get_runtime_api_base_url, resolve_crn from .json import RuntimeEncoder, RuntimeDecoder, to_base64_string +from . import pubsub diff --git a/qiskit_ibm_runtime/utils/backend_converter.py b/qiskit_ibm_runtime/utils/backend_converter.py index 1bf390bfc5..3db605d100 100644 --- a/qiskit_ibm_runtime/utils/backend_converter.py +++ b/qiskit_ibm_runtime/utils/backend_converter.py @@ -36,7 +36,7 @@ PulseDefaults, ) -from qiskit_ibm_provider.ibm_qubit_properties import IBMQubitProperties +from ..ibm_qubit_properties import IBMQubitProperties def convert_to_target( diff --git a/qiskit_ibm_runtime/utils/backend_decoder.py b/qiskit_ibm_runtime/utils/backend_decoder.py new file mode 100644 index 0000000000..d02fb90fbb --- /dev/null +++ b/qiskit_ibm_runtime/utils/backend_decoder.py @@ -0,0 +1,167 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Utilities for working with IBM Quantum backends.""" + +from typing import List, Dict, Union, Optional +import logging +import traceback + +import dateutil.parser +from qiskit.providers.models import ( + BackendProperties, + PulseDefaults, + PulseBackendConfiguration, + QasmBackendConfiguration, +) + +from .converters import utc_to_local_all + +logger = logging.getLogger(__name__) + + +def configuration_from_server_data( + raw_config: Dict, + instance: str = "", +) -> Optional[Union[QasmBackendConfiguration, PulseBackendConfiguration]]: + """Create an IBMBackend instance from raw server data. + + Args: + raw_config: Raw configuration. + instance: Service instance. + + Returns: + Backend configuration. + """ + # Make sure the raw_config is of proper type + if not isinstance(raw_config, dict): + logger.warning( # type: ignore[unreachable] + "An error occurred when retrieving backend " + "information. Some backends might not be available." + ) + return None + try: + _decode_backend_configuration(raw_config) + try: + return PulseBackendConfiguration.from_dict(raw_config) + except (KeyError, TypeError): + return QasmBackendConfiguration.from_dict(raw_config) + except Exception: # pylint: disable=broad-except + logger.warning( + 'Remote backend "%s" for service instance %s could not be instantiated due ' + "to an invalid server-side configuration", + raw_config.get("backend_name", raw_config.get("name", "unknown")), + repr(instance), + ) + logger.debug("Invalid device configuration: %s", traceback.format_exc()) + return None + + +def defaults_from_server_data(defaults: Dict) -> PulseDefaults: + """Decode pulse defaults data. + + Args: + defaults: Raw pulse defaults data. + + Returns: + A ``PulseDefaults`` instance. + """ + for item in defaults["pulse_library"]: + _decode_pulse_library_item(item) + + for cmd in defaults["cmd_def"]: + if "sequence" in cmd: + for instr in cmd["sequence"]: + _decode_pulse_qobj_instr(instr) + + return PulseDefaults.from_dict(defaults) + + +def properties_from_server_data(properties: Dict) -> BackendProperties: + """Decode backend properties. + + Args: + properties: Raw properties data. + + Returns: + A ``BackendProperties`` instance. + """ + properties["last_update_date"] = dateutil.parser.isoparse(properties["last_update_date"]) + for qubit in properties["qubits"]: + for nduv in qubit: + nduv["date"] = dateutil.parser.isoparse(nduv["date"]) + for gate in properties["gates"]: + for param in gate["parameters"]: + param["date"] = dateutil.parser.isoparse(param["date"]) + for gen in properties["general"]: + gen["date"] = dateutil.parser.isoparse(gen["date"]) + + properties = utc_to_local_all(properties) + return BackendProperties.from_dict(properties) + + +def _decode_backend_configuration(config: Dict) -> None: + """Decode backend configuration. + + Args: + config: A ``QasmBackendConfiguration`` or ``PulseBackendConfiguration`` + in dictionary format. + """ + config["online_date"] = dateutil.parser.isoparse(config["online_date"]) + + if "u_channel_lo" in config: + for u_channel_list in config["u_channel_lo"]: + for u_channel_lo in u_channel_list: + u_channel_lo["scale"] = _to_complex(u_channel_lo["scale"]) + + +def _to_complex(value: Union[List[float], complex]) -> complex: + """Convert the input value to type ``complex``. + + Args: + value: Value to be converted. + + Returns: + Input value in ``complex``. + + Raises: + TypeError: If the input value is not in the expected format. + """ + if isinstance(value, list) and len(value) == 2: + return complex(value[0], value[1]) + elif isinstance(value, complex): + return value + + raise TypeError("{} is not in a valid complex number format.".format(value)) + + +def _decode_pulse_library_item(pulse_library_item: Dict) -> None: + """Decode a pulse library item. + + Args: + pulse_library_item: A ``PulseLibraryItem`` in dictionary format. + """ + pulse_library_item["samples"] = [ + _to_complex(sample) for sample in pulse_library_item["samples"] + ] + + +def _decode_pulse_qobj_instr(pulse_qobj_instr: Dict) -> None: + """Decode a pulse Qobj instruction. + + Args: + pulse_qobj_instr: A ``PulseQobjInstruction`` in dictionary format. + """ + if "val" in pulse_qobj_instr: + pulse_qobj_instr["val"] = _to_complex(pulse_qobj_instr["val"]) + if "parameters" in pulse_qobj_instr and "amp" in pulse_qobj_instr["parameters"]: + pulse_qobj_instr["parameters"]["amp"] = _to_complex(pulse_qobj_instr["parameters"]["amp"]) diff --git a/qiskit_ibm_runtime/utils/converters.py b/qiskit_ibm_runtime/utils/converters.py new file mode 100644 index 0000000000..2a48a3f449 --- /dev/null +++ b/qiskit_ibm_runtime/utils/converters.py @@ -0,0 +1,239 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Utilities related to conversion.""" + +import re +from datetime import datetime, timedelta, timezone +from math import ceil +from typing import Union, Tuple, Any, Optional, List + +from dateutil import tz, parser +from qiskit.circuit import QuantumCircuit, ControlFlowOp +from qiskit_ibm_runtime.exceptions import IBMInputValueError + + +def utc_to_local(utc_dt: Union[datetime, str]) -> datetime: + """Convert a UTC ``datetime`` object or string to a local timezone ``datetime``. + + Args: + utc_dt: Input UTC `datetime` or string. + + Returns: + A ``datetime`` with the local timezone. + + Raises: + TypeError: If the input parameter value is not valid. + """ + if isinstance(utc_dt, str): + utc_dt = parser.parse(utc_dt) + if not isinstance(utc_dt, datetime): + raise TypeError("Input `utc_dt` is not string or datetime.") + utc_dt = utc_dt.replace(tzinfo=timezone.utc) # type: ignore[arg-type] + local_dt = utc_dt.astimezone(tz.tzlocal()) # type: ignore[attr-defined] + return local_dt + + +def local_to_utc(local_dt: Union[datetime, str]) -> datetime: + """Convert a local ``datetime`` object or string to a UTC ``datetime``. + + Args: + local_dt: Input local ``datetime`` or string. + + Returns: + A ``datetime`` in UTC. + + Raises: + TypeError: If the input parameter value is not valid. + """ + if isinstance(local_dt, str): + local_dt = parser.parse(local_dt) + if not isinstance(local_dt, datetime): + raise TypeError("Input `local_dt` is not string or datetime.") + + # Input is considered local if it's ``utcoffset()`` is ``None`` or none-zero. + if local_dt.utcoffset() is None or local_dt.utcoffset() != timedelta(0): + local_dt = local_dt.replace(tzinfo=tz.tzlocal()) + return local_dt.astimezone(tz.UTC) + return local_dt # Already in UTC. + + +def local_to_utc_str(local_dt: Union[datetime, str], suffix: str = "Z") -> str: + """Convert a local ``datetime`` object or string to a UTC string. + + Args: + local_dt: Input local ``datetime`` or string. + suffix: ``Z`` or ``+``, indicating whether the suffix should be ``Z`` or + ``+00:00``. + + Returns: + UTC datetime in ISO format. + """ + utc_dt_str = local_to_utc(local_dt).isoformat() + if suffix == "Z": + utc_dt_str = utc_dt_str.replace("+00:00", "Z") + return utc_dt_str + + +def convert_tz(input_dt: Optional[datetime], to_utc: bool) -> Optional[datetime]: + """Convert input timestamp timezone. + + Args: + input_dt: Timestamp to be converted. + to_utc: True if to convert to UTC, otherwise to local timezone. + + Returns: + Converted timestamp, or ``None`` if input is ``None``. + """ + if input_dt is None: + return None + if to_utc: + return local_to_utc(input_dt) + return utc_to_local(input_dt) + + +def utc_to_local_all(data: Any) -> Any: + """Recursively convert all ``datetime`` in the input data from local time to UTC. + + Note: + Only lists and dictionaries are traversed. + + Args: + data: Data to be converted. + + Returns: + Converted data. + """ + if isinstance(data, datetime): + return utc_to_local(data) + elif isinstance(data, list): + return [utc_to_local_all(elem) for elem in data] + elif isinstance(data, dict): + return {key: utc_to_local_all(elem) for key, elem in data.items()} + return data + + +def str_to_utc(utc_dt: Optional[str]) -> Optional[datetime]: + """Convert a UTC string to a ``datetime`` object with UTC timezone. + + Args: + utc_dt: Input UTC string in ISO format. + + Returns: + A ``datetime`` with the UTC timezone, or ``None`` if the input is ``None``. + """ + if not utc_dt: + return None + parsed_dt = parser.isoparse(utc_dt) + return parsed_dt.replace(tzinfo=timezone.utc) + + +def seconds_to_duration(seconds: float) -> Tuple[int, int, int, int, int]: + """Converts seconds in a datetime delta to a duration. + + Args: + seconds: Number of seconds in time delta. + + Returns: + A tuple containing the duration in terms of days, + hours, minutes, seconds, and milliseconds. + """ + days = int(seconds // (3600 * 24)) + hours = int((seconds // 3600) % 24) + minutes = int((seconds // 60) % 60) + seconds = seconds % 60 + millisec = 0 + if seconds < 1: + millisec = int(ceil(seconds * 1000)) + seconds = 0 + else: + seconds = int(seconds) + return days, hours, minutes, seconds, millisec + + +def duration_difference(date_time: datetime) -> str: + """Compute the estimated duration until the given datetime. + + Args: + date_time: The input local datetime. + + Returns: + String giving the estimated duration. + """ + time_delta = date_time.replace(tzinfo=None) - datetime.now() + time_tuple = seconds_to_duration(time_delta.total_seconds()) + # The returned tuple contains the duration in terms of + # days, hours, minutes, seconds, and milliseconds. + time_str = "" + if time_tuple[0]: + time_str += "{} days".format(time_tuple[0]) + time_str += " {} hrs".format(time_tuple[1]) + elif time_tuple[1]: + time_str += "{} hrs".format(time_tuple[1]) + time_str += " {} min".format(time_tuple[2]) + elif time_tuple[2]: + time_str += "{} min".format(time_tuple[2]) + time_str += " {} sec".format(time_tuple[3]) + elif time_tuple[3]: + time_str += "{} sec".format(time_tuple[3]) + return time_str + + +def hms_to_seconds(hms: str, msg_prefix: str = "") -> int: + """Convert duration specified as hours minutes seconds to seconds. + + Args: + hms: The string input duration (in hours minutes seconds). Ex: 2h 10m 20s + msg_prefix: Additional message to prefix the error. + + Returns: + Total seconds (int) in the duration. + + Raises: + IBMInputValueError: when the given hms string is in an invalid format + """ + + parsed_time = re.findall(r"(\d+[dhms])", hms) + total_seconds = 0 + + if parsed_time: + for time_unit in parsed_time: + unit = time_unit[-1] + value = int(time_unit[:-1]) + if unit == "d": + total_seconds += value * 86400 + elif unit == "h": + total_seconds += value * 3600 + elif unit == "m": + total_seconds += value * 60 + elif unit == "s": + total_seconds += value + else: + raise IBMInputValueError(f"{msg_prefix} Invalid input: {unit}") + else: + raise IBMInputValueError(f"{msg_prefix} Invalid input: {parsed_time}") + + return total_seconds + + +def are_circuits_dynamic(circuits: List[QuantumCircuit]) -> bool: + """Checks if the input circuits are dynamic.""" + for circuit in circuits: + if isinstance(circuit, str): + return True + for inst in circuit: + if ( + isinstance(inst.operation, ControlFlowOp) + or getattr(inst.operation, "condition", None) is not None + ): + return True + return False diff --git a/qiskit_ibm_runtime/utils/hgp.py b/qiskit_ibm_runtime/utils/hgp.py new file mode 100644 index 0000000000..47438fc9b1 --- /dev/null +++ b/qiskit_ibm_runtime/utils/hgp.py @@ -0,0 +1,42 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Hub/group/project utility functions.""" + +from typing import Tuple +from ..exceptions import IBMInputValueError + + +def from_instance_format(instance: str) -> Tuple[str, str, str]: + """Convert the input instance to [hub, group, project]. + + Args: + instance: Service instance in hub/group/project format. + + Returns: + Hub, group, and project. + + Raises: + IBMInputValueError: If input is not in the correct format. + """ + try: + hub, group, project = instance.split("/") + return hub, group, project + except (ValueError, AttributeError): + raise IBMInputValueError( + f"Input instance value {instance} is not in the" f"correct hub/group/project format." + ) + + +def to_instance_format(hub: str, group: str, project: str) -> str: + """Convert input to hub/group/project format.""" + return f"{hub}/{group}/{project}" diff --git a/qiskit_ibm_runtime/utils/json.py b/qiskit_ibm_runtime/utils/json.py index fcc634c9e7..5bccfb3bf8 100644 --- a/qiskit_ibm_runtime/utils/json.py +++ b/qiskit_ibm_runtime/utils/json.py @@ -56,16 +56,15 @@ from qiskit.circuit.parametertable import ParameterView from qiskit.result import Result from qiskit.version import __version__ as _terra_version_string - -from qiskit_ibm_provider.qpy import ( - _write_parameter, +from qiskit.utils import optionals +from qiskit.qpy import ( _write_parameter_expression, _read_parameter_expression, _read_parameter_expression_v3, - _read_parameter, - dump, load, + dump, ) +from qiskit.qpy.binary_io.value import _write_parameter, _read_parameter # TODO: Remove when they are in terra from ..qiskit.primitives import ObservablesArray, BindingsArray @@ -221,10 +220,15 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ if hasattr(obj, "to_json"): return {"__type__": "to_json", "__value__": obj.to_json()} if isinstance(obj, QuantumCircuit): + kwargs: dict[str, object] = {"use_symengine": bool(optionals.HAS_SYMENGINE)} + if _TERRA_VERSION[0] >= 1: + # NOTE: This can be updated only after the server side has + # updated to a newer qiskit version. + kwargs["version"] = 10 value = _serialize_and_encode( data=obj, serializer=lambda buff, data: dump( - data, buff, RuntimeEncoder + data, buff, RuntimeEncoder, **kwargs ), # type: ignore[no-untyped-call] ) return {"__type__": "QuantumCircuit", "__value__": value} @@ -240,18 +244,26 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ data=obj, serializer=_write_parameter_expression, compress=False, + use_symengine=bool(optionals.HAS_SYMENGINE), ) return {"__type__": "ParameterExpression", "__value__": value} if isinstance(obj, ParameterView): return obj.data if isinstance(obj, Instruction): + kwargs = {"use_symengine": bool(optionals.HAS_SYMENGINE)} + if _TERRA_VERSION[0] >= 1: + # NOTE: This can be updated only after the server side has + # updated to a newer qiskit version. + kwargs["version"] = 10 # Append instruction to empty circuit quantum_register = QuantumRegister(obj.num_qubits) quantum_circuit = QuantumCircuit(quantum_register) quantum_circuit.append(obj, quantum_register) value = _serialize_and_encode( data=quantum_circuit, - serializer=lambda buff, data: dump(data, buff), # type: ignore[no-untyped-call] + serializer=lambda buff, data: dump( + data, buff, **kwargs + ), # type: ignore[no-untyped-call] ) return {"__type__": "Instruction", "__value__": value} if isinstance(obj, BasePub): diff --git a/qiskit_ibm_runtime/utils/options.py b/qiskit_ibm_runtime/utils/options.py new file mode 100644 index 0000000000..34874dfdbc --- /dev/null +++ b/qiskit_ibm_runtime/utils/options.py @@ -0,0 +1,58 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Backend run options.""" + +from dataclasses import asdict, dataclass +from typing import Dict, Union, Any, Optional +from qiskit.circuit import QuantumCircuit +from qiskit.qobj.utils import MeasLevel, MeasReturnType + + +@dataclass +class CommonOptions: + """Options common for both paths.""" + + shots: int = 4000 + meas_level: Union[int, MeasLevel] = MeasLevel.CLASSIFIED + init_qubits: bool = True + rep_delay: Optional[float] = None + memory: bool = False + meas_return: Union[str, MeasReturnType] = MeasReturnType.AVERAGE + + def to_transport_dict(self) -> Dict[str, Any]: + """Remove None values so runtime defaults are used.""" + dict_ = asdict(self) + for key in list(dict_.keys()): + if dict_[key] is None: + del dict_[key] + return dict_ + + +@dataclass +class QASM3Options(CommonOptions): + """Options for the QASM3 path.""" + + init_circuit: Optional[QuantumCircuit] = None + init_num_resets: Optional[int] = None + + +@dataclass +class QASM2Options(CommonOptions): + """Options for the QASM2 path.""" + + header: Optional[Dict] = None + init_qubits: bool = True + use_measure_esp: Optional[bool] = None + # Simulator only + noise_model: Any = None + seed_simulator: Optional[int] = None diff --git a/qiskit_ibm_runtime/utils/pubsub.py b/qiskit_ibm_runtime/utils/pubsub.py new file mode 100644 index 0000000000..e05fd211cb --- /dev/null +++ b/qiskit_ibm_runtime/utils/pubsub.py @@ -0,0 +1,182 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2024. +# +# 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. + +""" +Message broker for the Publisher / Subscriber mechanism +""" + +from __future__ import annotations + +import typing + +from qiskit.exceptions import QiskitError + +try: + from qiskit.tools.events.pubsub import _Broker as _QiskitBroker +except ImportError: + _QiskitBroker = None + +_Callback = typing.Callable[..., None] + + +class _Broker: + """The event/message broker. It's a singleton. + + In order to keep consistency across all the components, it would be great to + have a specific format for new events, documenting their usage. + It's the responsibility of the component emitting an event to document it's usage in + the component docstring. + + Event format:: + + ".." + + Examples: + + * "ibm.job.start" + """ + + _instance: _Broker | None = None + _subscribers: dict[str, list[_Subscription]] = {} + + @staticmethod + def __new__(cls: type[_Broker]) -> _Broker: + if _Broker._instance is None: + # Backwards compatibility for Qiskit pre-1.0; if the Qiskit-internal broker + # singleton exists then we use that instead of defining a new one, so that + # the event streams will be unified even if someone is still using the + # Qiskit entry points to subscribe. + # + # This dynamic switch assumes that the interface of this vendored `Broker` + # code remains identical to the Qiskit 0.45 version. + _Broker._instance = object.__new__(_QiskitBroker or cls) + return _Broker._instance + + class _Subscription: + def __init__(self, event: str, callback: _Callback): + self.event: str = event + self.callback: _Callback = callback + + def __eq__(self, other: object) -> bool: + """Overrides the default implementation""" + if isinstance(other, self.__class__): + return self.event == other.event and id(self.callback) == id( + other.callback + ) # Allow 1:N subscribers + return False + + def subscribe(self, event: str, callback: _Callback) -> bool: + """Subscribes to an event, so when it's emitted all the callbacks subscribed, + will be executed. We are not allowing double registration. + + Args: + event (string): The event to subscribed in the form of: + "terra..." + callback (callable): The callback that will be executed when an event is + emitted. + """ + if not callable(callback): + raise QiskitError("Callback is not a callable!") + + if event not in self._subscribers: + self._subscribers[event] = [] + + new_subscription = self._Subscription(event, callback) + if new_subscription in self._subscribers[event]: + # We are not allowing double subscription + return False + + self._subscribers[event].append(new_subscription) + return True + + def dispatch(self, event: str, *args: typing.Any, **kwargs: typing.Any) -> None: + """Emits an event if there are any subscribers. + + Args: + event (String): The event to be emitted + args: Arguments linked with the event + kwargs: Named arguments linked with the event + """ + # No event, no subscribers. + if event not in self._subscribers: + return + + for subscriber in self._subscribers[event]: + subscriber.callback(*args, **kwargs) + + def unsubscribe(self, event: str, callback: _Callback) -> bool: + """Unsubscribe the specific callback to the event. + + Args + event (String): The event to unsubscribe + callback (callable): The callback that won't be executed anymore + + Returns + True: if we have successfully unsubscribed to the event + False: if there's no callback previously registered + """ + + try: + self._subscribers[event].remove(self._Subscription(event, callback)) + except KeyError: + return False + + return True + + def clear(self) -> None: + """Unsubscribe everything, leaving the Broker without subscribers/events.""" + self._subscribers.clear() + + +class Publisher: + """Represents a "publisher". + + Every component (class) can become a :class:`Publisher` and send events by + inheriting this class. Functions can call this class like:: + + Publisher().publish("event", args, ... ) + """ + + def __init__(self) -> None: + self._broker: _Broker = _Broker() + + def publish(self, event: str, *args: typing.Any, **kwargs: typing.Any) -> None: + """Triggers an event, and associates some data to it, so if there are any + subscribers, their callback will be called synchronously.""" + return self._broker.dispatch(event, *args, **kwargs) + + +class Subscriber: + """Represents a "subscriber". + + Every component (class) can become a :class:`Subscriber` and subscribe to events, + that will call callback functions when they are emitted. + """ + + def __init__(self) -> None: + self._broker: _Broker = _Broker() + + def subscribe(self, event: str, callback: _Callback) -> bool: + """Subscribes to an event, associating a callback function to that event, so + when the event occurs, the callback will be called. + + This is a blocking call, so try to keep callbacks as lightweight as possible.""" + return self._broker.subscribe(event, callback) + + def unsubscribe(self, event: str, callback: _Callback) -> bool: + """Unsubscribe a pair event-callback, so the callback will not be called anymore + when the event occurs.""" + return self._broker.unsubscribe(event, callback) + + def clear(self) -> None: + """Unsubscribe everything""" + self._broker.clear() diff --git a/releasenotes/notes/0.19/consolidate-provider-code-b07fea8644aa8f43.yaml b/releasenotes/notes/0.19/consolidate-provider-code-b07fea8644aa8f43.yaml new file mode 100644 index 0000000000..5edb74f189 --- /dev/null +++ b/releasenotes/notes/0.19/consolidate-provider-code-b07fea8644aa8f43.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + `qiskit-ibm-provider` is pending deprecation, and therefore will no longer be a + dependency for `qiskit-ibm-runtime`. diff --git a/releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml b/releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml new file mode 100644 index 0000000000..bfe9cf25ec --- /dev/null +++ b/releasenotes/notes/0.19/fix-duration-patching-b80d45d77481dfa6.yaml @@ -0,0 +1,11 @@ +--- +fixes: + - | + Fix the patching of :class:`.DynamicCircuitInstructions` for instructions + with durations that are not in units of ``dt``. +upgrade: + - | + Extend :meth:`.DynamicCircuitInstructions.from_backend` to extract and + patch durations from both :class:`.BackendV1` and :class:`.BackendV2` + objects. Also add :meth:`.DynamicCircuitInstructions.from_target` to use a + :class:`.Target` object instead. diff --git a/releasenotes/notes/0.19/qiskit-1.0-compatible-6fbf17d2dd28cb48.yaml b/releasenotes/notes/0.19/qiskit-1.0-compatible-6fbf17d2dd28cb48.yaml new file mode 100644 index 0000000000..53deecd609 --- /dev/null +++ b/releasenotes/notes/0.19/qiskit-1.0-compatible-6fbf17d2dd28cb48.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + `qiskit-ibm-runtime` is now compatible with Qiskit versions `>= 0.45`, + including `1.0.0`. + + diff --git a/releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml b/releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml new file mode 100644 index 0000000000..8969fba285 --- /dev/null +++ b/releasenotes/notes/fix-qpy-bug-739cefc2c9018d0b.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed an issue with the :func:`.qpy.dump` function, when the + ``use_symengine`` flag was set to a truthy object that evaluated to + ``True`` but was not actually the boolean ``True`` the generated QPY + payload would be corrupt. + diff --git a/requirements-dev.txt b/requirements-dev.txt index 61ee53c7eb..03f4af164e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,7 +12,6 @@ websockets>=8 black~=22.0 coverage>=6.3 pylatexenc -mthree scikit-learn ddt>=1.2.0,!=1.4.0,!=1.4.3 diff --git a/requirements.txt b/requirements.txt index 9203f96325..15b6946d2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,3 @@ python-dateutil>=2.8.0 websocket-client>=1.5.1 typing-extensions>=4.0.0 ibm-platform-services>=0.22.6 -qiskit-ibm-provider>=0.8.0 diff --git a/setup.py b/setup.py index eb68ea7055..4dc75e1439 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,6 @@ "websocket-client>=1.5.1", "ibm-platform-services>=0.22.6", "pydantic", - "qiskit-ibm-provider>=0.8.0", ] # Handle version. diff --git a/test/fake_account_client.py b/test/fake_account_client.py deleted file mode 100644 index 5a2ec96546..0000000000 --- a/test/fake_account_client.py +++ /dev/null @@ -1,531 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# 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. - -"""Fake AccountClient.""" - -import copy - -# TODO This can probably be merged with the one in test_ibm_job_states -import time -import uuid -import warnings -from concurrent.futures import ThreadPoolExecutor, wait -from datetime import timedelta, datetime -from random import randrange -from typing import Dict, Any - -from qiskit.providers.fake_provider.backends.poughkeepsie.fake_poughkeepsie import ( - FakePoughkeepsie, -) - -from qiskit_ibm_provider.api.exceptions import ( - RequestsApiError, - UserTimeoutExceededError, -) -from qiskit_ibm_provider.apiconstants import ApiJobStatus, API_JOB_FINAL_STATES - -VALID_RESULT_RESPONSE = { - "backend_name": "ibmqx2", - "backend_version": "1.1.1", - "job_id": "XC1323XG2", - "qobj_id": "Experiment1", - "success": True, - "results": [], -} -"""A valid job result response.""" - -VALID_RESULT = { - "header": { - "name": "Bell state", - "creg_sizes": [["c", 2]], - "clbit_labels": [["c", 0], ["c", 1]], - "qubit_labels": [["q", 0], ["q", 1]], - }, - "shots": 1024, - "status": "DONE", - "success": True, - "data": {"counts": {"0x0": 484, "0x3": 540}}, -} - -API_STATUS_TO_INT = { - ApiJobStatus.CREATING: 0, - ApiJobStatus.VALIDATING: 1, - ApiJobStatus.QUEUED: 2, - ApiJobStatus.RUNNING: 3, - ApiJobStatus.COMPLETED: 4, - ApiJobStatus.ERROR_RUNNING_JOB: 4, - ApiJobStatus.ERROR_VALIDATING_JOB: 4, - ApiJobStatus.CANCELLED: 4, -} - - -class BaseFakeJob: - """Base class for faking a remote job.""" - - _job_progress = [ - ApiJobStatus.CREATING, - ApiJobStatus.VALIDATING, - ApiJobStatus.QUEUED, - ApiJobStatus.RUNNING, - ApiJobStatus.COMPLETED, - ] - - def __init__( - self, - executor, - job_id, - qobj, - backend_name, - job_tags=None, - job_name=None, - experiment_id=None, - run_mode=None, - progress_time=0.5, - **kwargs, - ): - """Initialize a fake job.""" - self._job_id = job_id - self._status = ApiJobStatus.CREATING - self.qobj = qobj - self._result = None - self._backend_name = backend_name - self._job_tags = job_tags - self._job_name = job_name - self._experiment_id = experiment_id - self._creation_date = datetime.now() - self._run_mode = run_mode - self._queue_pos = kwargs.pop("queue_pos", "auto") - self._comp_time = kwargs.pop("est_completion", "auto") - self._queue_info = None - self._progress_time = progress_time - self._future = executor.submit(self._auto_progress) - - def _auto_progress(self): - """Automatically update job status.""" - for status in self._job_progress: - time.sleep(self._progress_time) - self._status = status - - if self._status == ApiJobStatus.COMPLETED: - self._save_result() - elif self._status == ApiJobStatus.ERROR_RUNNING_JOB: - self._save_bad_result() - - def _save_result(self): - new_result = copy.deepcopy(VALID_RESULT_RESPONSE) - for _ in range(len(self.qobj["experiments"])): - valid_result = copy.deepcopy(VALID_RESULT) - counts = randrange(1024) - valid_result["data"]["counts"] = {"0x0": counts, "0x3": 1024 - counts} - new_result["results"].append(valid_result) - new_result["job_id"] = self._job_id - new_result["backend_name"] = self._backend_name - self._result = new_result - - def _save_bad_result(self): - new_result = copy.deepcopy(VALID_RESULT_RESPONSE) - new_result["job_id"] = self._job_id - new_result["backend_name"] = self._backend_name - new_result["success"] = False - new_result["error"] = {"message": "Kaboom", "code": 1234} - self._result = new_result - - def data(self): - """Return job data.""" - status = self._status - data = { - "job_id": self._job_id, - "kind": "q-object", - "status": status.value, - "creation_date": self._creation_date.isoformat(), - "_backend_info": {"name": self._backend_name}, - "client_info": {"qiskit": "0.23.5"}, - } - if self._job_tags: - data["tags"] = self._job_tags.copy() - if self._job_name: - data["name"] = self._job_name - if self._experiment_id: - data["experiment_id"] = self._experiment_id - if status == ApiJobStatus.ERROR_VALIDATING_JOB: - data["error"] = {"message": "Validation failed.", "code": 1234} - if status in [ApiJobStatus.RUNNING] + list(API_JOB_FINAL_STATES) and self._run_mode: - data["run_mode"] = self._run_mode - - time_per_step = {} - timestamp = self._creation_date - for api_stat in API_STATUS_TO_INT: # pylint: disable=consider-using-dict-items - if API_STATUS_TO_INT[status] > API_STATUS_TO_INT[api_stat]: - time_per_step[api_stat.value] = timestamp.isoformat() - timestamp += timedelta(seconds=30) - elif status == api_stat: - time_per_step[api_stat.value] = timestamp.isoformat() - timestamp += timedelta(seconds=30) - data["time_per_step"] = time_per_step - - return data - - def _get_info_queue(self): - self._queue_info = { - "status": "PENDING_IN_QUEUE", - "position": randrange(1, 10) if self._queue_pos == "auto" else self._queue_pos, - } - if self._queue_info["position"] is None: - return self._queue_info - - est_comp_ts = ( - self._creation_date + timedelta(minutes=10 * self._queue_info["position"]) - if self._comp_time == "auto" - else self._comp_time - ) - if est_comp_ts is None: - return self._queue_info - - self._queue_info["estimated_complete_time"] = est_comp_ts.isoformat() - self._queue_info["estimated_start_time"] = (est_comp_ts - timedelta(minutes=20)).isoformat() - - return self._queue_info - - def cancel(self): - """Cancel the job.""" - self._future.cancel() - wait([self._future]) - self._status = ApiJobStatus.CANCELLED - self._result = None - - def result(self): - """Return job result.""" - if not self._result: - raise RequestsApiError("Result is not available") - return self._result - - def status_data(self): - """Return job status data, including queue info.""" - status = self._status - data = {"status": status.value} - if status == ApiJobStatus.QUEUED: - data["info_queue"] = self._get_info_queue() - return data - - def status(self): - """Return job status.""" - return self._status - - def name(self): - """Return job name.""" - return self._job_name - - -class CancelableFakeJob(BaseFakeJob): - """Fake job that can be canceled.""" - - _job_progress = [ - ApiJobStatus.CREATING, - ApiJobStatus.VALIDATING, - ApiJobStatus.RUNNING, - ] - - -class NewFieldFakeJob(BaseFakeJob): - """Fake job that contains additional fields.""" - - def data(self): - """Return job data.""" - data = super().data() - data["new_field"] = "foo" - return data - - -class MissingFieldFakeJob(BaseFakeJob): - """Fake job that does not contain required fields.""" - - def data(self): - """Return job data.""" - data = super().data() - del data["job_id"] - return data - - -class FailedFakeJob(BaseFakeJob): - """Fake job that fails.""" - - _job_progress = [ApiJobStatus.CREATING, ApiJobStatus.VALIDATING] - - def __init__(self, *args, **kwargs): - # failure_type can be "validation", "result", or "partial" - self._failure_type = kwargs.pop("failure_type", "validation") - self._job_progress = FailedFakeJob._job_progress.copy() - if self._failure_type == "validation": - self._job_progress.append(ApiJobStatus.ERROR_VALIDATING_JOB) - else: - self._job_progress.extend([ApiJobStatus.RUNNING, ApiJobStatus.ERROR_RUNNING_JOB]) - super().__init__(*args, **kwargs) - - def _save_bad_result(self): - if self._failure_type != "partial": - super()._save_bad_result() - return - new_result = copy.deepcopy(VALID_RESULT_RESPONSE) - new_result["job_id"] = self._job_id - new_result["backend_name"] = self._backend_name - new_result["success"] = False - # Good first result. - valid_result = copy.deepcopy(VALID_RESULT) - counts = randrange(1024) - valid_result["data"]["counts"] = {"0x0": counts, "0x3": 1024 - counts} - new_result["results"].append(valid_result) - - for _ in range(1, len(self.qobj["experiments"])): - valid_result = copy.deepcopy(VALID_RESULT) - valid_result["success"] = False - valid_result["status"] = "This circuit failed." - new_result["results"].append(valid_result) - self._result = new_result - - -class FixedStatusFakeJob(BaseFakeJob): - """Fake job that stays in a specific status.""" - - def __init__(self, *args, **kwargs): - self._fixed_status = kwargs.pop("fixed_status") - super().__init__(*args, **kwargs) - - def _auto_progress(self): - """Automatically update job status.""" - for status in self._job_progress: - time.sleep(0.5) - self._status = status - if status == self._fixed_status: - break - - if self._status == ApiJobStatus.COMPLETED: - self._save_result() - - -class BaseFakeAccountClient: - """Base class for faking the AccountClient.""" - - def __init__( - self, - job_limit=-1, - job_class=BaseFakeJob, - job_kwargs=None, - props_count=None, - queue_positions=None, - est_completion=None, - run_mode=None, - ): - """Initialize a fake account client.""" - self._jobs = {} - self._results_retrieved = set() - self._job_limit = job_limit - self._executor = ThreadPoolExecutor() - self._job_class = job_class - if isinstance(self._job_class, list): - self._job_class.reverse() - self._job_kwargs = job_kwargs or {} - self._props_count = props_count or 0 - self._props_date = datetime.now().isoformat() - self._queue_positions = queue_positions.copy() if queue_positions else [] - self._queue_positions.reverse() - self._est_completion = est_completion.copy() if est_completion else [] - self._est_completion.reverse() - self._run_mode = run_mode - self._default_job_class = BaseFakeJob - - def list_jobs(self, limit, skip, descending=True, extra_filter=None): - """Return a list of jobs.""" - # pylint: disable=unused-argument - extra_filter = extra_filter or {} - if all(fil in extra_filter for fil in ["creationDate", "id"]): - return {} - tag = extra_filter.get("tags", None) - all_job_data = [] - for job in list(self._jobs.values())[skip : skip + limit]: - job_data = job.data() - if tag is None or tag in job_data["tags"]: - all_job_data.append(job_data) - if not descending: - all_job_data.reverse() - return all_job_data - - def job_submit( - self, - backend_name, - qobj_dict, - job_name, - job_tags, - experiment_id, - *_args, - **_kwargs, - ): - """Submit a Qobj to a device.""" - if self._job_limit != -1 and self._unfinished_jobs() >= self._job_limit: - raise RequestsApiError( - "400 Client Error: Bad Request for url: . Reached " - "maximum number of concurrent jobs, Error code: 3458." - ) - - new_job_id = uuid.uuid4().hex - if isinstance(self._job_class, list): - job_class = self._job_class.pop() if self._job_class else self._default_job_class - else: - job_class = self._job_class - job_kwargs = copy.copy(self._job_kwargs) - if self._queue_positions: - job_kwargs["queue_pos"] = self._queue_positions.pop() - if self._est_completion: - job_kwargs["est_completion"] = self._est_completion.pop() - - run_mode = self._run_mode - if run_mode == "dedicated_once": - run_mode = "dedicated" - self._run_mode = "fairshare" - - new_job = job_class( - executor=self._executor, - job_id=new_job_id, - qobj=qobj_dict, - backend_name=backend_name, - job_tags=job_tags, - job_name=job_name, - experiment_id=experiment_id, - run_mode=run_mode, - **job_kwargs, - ) - self._jobs[new_job_id] = new_job - return new_job.data() - - def job_download_qobj(self, job_id, *_args, **_kwargs): - """Retrieve and return a Qobj.""" - return copy.deepcopy(self._get_job(job_id).qobj) - - def job_result(self, job_id, *_args, **_kwargs): - """Return a random job result.""" - if job_id in self._results_retrieved: - warnings.warn(f"Result already retrieved for job {job_id}") - self._results_retrieved.add(job_id) - return self._get_job(job_id).result() - - def job_get(self, job_id, *_args, **_kwargs): - """Return information about a job.""" - return self._get_job(job_id).data() - - def job_status(self, job_id, *_args, **_kwargs): - """Return the status of a job.""" - return self._get_job(job_id).status_data() - - def job_final_status(self, job_id, *_args, **_kwargs): - """Wait until the job progress to a final state.""" - job = self._get_job(job_id) - status = job.status() - while status not in API_JOB_FINAL_STATES: - time.sleep(0.5) - status_data = job.status_data() - status = ApiJobStatus(status_data["status"]) - if _kwargs.get("status_queue", None): - data = {"status": status.value} - if status is ApiJobStatus.QUEUED: - data["infoQueue"] = {"status": "PENDING_IN_QUEUE", "position": 1} - _kwargs["status_queue"].put(status_data) - return self.job_status(job_id) - - def job_properties(self, *_args, **_kwargs): - """Return the backend properties of a job.""" - props = FakePoughkeepsie().properties().to_dict() - if self._props_count > 0: - self._props_count -= 1 - new_dt = datetime.now() + timedelta(hours=randrange(300)) - self._props_date = new_dt.isoformat() - props["last_update_date"] = self._props_date - return props - - def job_cancel(self, job_id, *_args, **_kwargs): - """Submit a request for cancelling a job.""" - self._get_job(job_id).cancel() - return {"cancelled": True} - - def backend_job_limit(self, *_args, **_kwargs): - """Return the job limit for the backend.""" - return {"maximumJobs": self._job_limit, "runningJobs": self._unfinished_jobs()} - - def job_update_attribute(self, job_id, attr_name, attr_value, *_args, **_kwargs): - """Update the specified job attribute with the given value.""" - job = self._get_job(job_id) - if attr_name == "name": - job._job_name = attr_value - if attr_name == "tags": - job._job_tags = attr_value.copy() - return {attr_name: attr_value} - - def backend_status(self, backend_name: str) -> Dict[str, Any]: - """Return the status of the backend.""" - return { - "backend_name": backend_name, - "backend_version": "0.0.0", - "operational": True, - "pending_jobs": 0, - "status_msg": "active", - } - - def tear_down(self): - """Clean up job threads.""" - for job_id in list(self._jobs.keys()): - try: - self._jobs[job_id].cancel() - except KeyError: - pass - - def _unfinished_jobs(self): - """Return the number of unfinished jobs.""" - return sum(1 for job in self._jobs.values() if job.status() not in API_JOB_FINAL_STATES) - - def _get_job(self, job_id): - """Return job if found.""" - if job_id not in self._jobs: - raise RequestsApiError("Job not found. Error code: 3250.") - return self._jobs[job_id] - - -class JobSubmitFailClient(BaseFakeAccountClient): - """Fake AccountClient used to fail a job submit.""" - - def __init__(self, failed_indexes): - """JobSubmitFailClient constructor.""" - if not isinstance(failed_indexes, list): - failed_indexes = [failed_indexes] - self._failed_indexes = failed_indexes - self._job_count = -1 - super().__init__() - - def job_submit(self, *_args, **_kwargs): # pylint: disable=arguments-differ - """Failing job submit.""" - self._job_count += 1 - if self._job_count in self._failed_indexes: - raise RequestsApiError("Job submit failed!") - return super().job_submit(*_args, **_kwargs) - - -class JobTimeoutClient(BaseFakeAccountClient): - """Fake AccountClient used to fail a job submit.""" - - def __init__(self, *args, max_fail_count=-1, **kwargs): - """JobTimeoutClient constructor.""" - self._fail_count = max_fail_count - super().__init__(*args, **kwargs) - - def job_final_status(self, job_id, *_args, **_kwargs): - """Wait until the job progress to a final state.""" - if self._fail_count != 0: - self._fail_count -= 1 - raise UserTimeoutExceededError("Job timed out!") - return super().job_final_status(job_id, *_args, **_kwargs) diff --git a/test/integration/test_auth_client.py b/test/integration/test_auth_client.py index 37c4d19bb4..9bab9ac5ee 100644 --- a/test/integration/test_auth_client.py +++ b/test/integration/test_auth_client.py @@ -14,7 +14,8 @@ import re -from qiskit_ibm_provider.api.exceptions import RequestsApiError +from qiskit_ibm_runtime.api.exceptions import RequestsApiError +from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError from qiskit_ibm_runtime.api.client_parameters import ClientParameters from qiskit_ibm_runtime.api.clients import AuthClient from ..ibm_test_case import IBMTestCase @@ -41,7 +42,7 @@ def test_url_404(self, dependencies: IntegrationTestDependencies) -> None: def test_invalid_token(self, dependencies: IntegrationTestDependencies) -> None: """Test login using invalid token.""" qe_token = "INVALID_TOKEN" - with self.assertRaises(RequestsApiError): + with self.assertRaises(IBMNotAuthorizedError): _ = self._init_auth_client(qe_token, dependencies.url) @integration_test_setup(supported_channel=["ibm_quantum"], init_service=False) diff --git a/test/integration/test_backend.py b/test/integration/test_backend.py index cc9d4c29d9..0e23415cdf 100644 --- a/test/integration/test_backend.py +++ b/test/integration/test_backend.py @@ -20,8 +20,8 @@ from qiskit import QuantumCircuit from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit_ibm_provider.ibm_qubit_properties import IBMQubitProperties -from qiskit_ibm_provider.exceptions import IBMBackendValueError +from qiskit_ibm_runtime.ibm_qubit_properties import IBMQubitProperties +from qiskit_ibm_runtime.exceptions import IBMBackendValueError from qiskit_ibm_runtime import QiskitRuntimeService diff --git a/test/integration/test_ibm_job.py b/test/integration/test_ibm_job.py index 45e56fcc11..09184b65a1 100644 --- a/test/integration/test_ibm_job.py +++ b/test/integration/test_ibm_job.py @@ -14,18 +14,13 @@ import copy import time from datetime import datetime, timedelta -from unittest import SkipTest, mock -from unittest import skip +from unittest import SkipTest from dateutil import tz from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.compiler import transpile from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES -from qiskit_ibm_provider.api.rest.job import Job as RestJob -from qiskit_ibm_provider.exceptions import IBMBackendApiError - -from qiskit_ibm_runtime.api.exceptions import RequestsApiError from qiskit_ibm_runtime.exceptions import RuntimeJobTimeoutError, RuntimeJobNotFound from ..ibm_test_case import IBMIntegrationTestCase @@ -226,7 +221,7 @@ def test_retrieve_jobs_order(self): job = self.sim_backend.run(self.bell) job.wait_for_final_state() newest_jobs = self.service.jobs( - limit=10, + limit=20, pending=False, descending=True, created_after=self.last_month, @@ -273,34 +268,6 @@ def test_wait_for_final_state_timeout(self): thread.join(0.1) cancel_job_safe(job, self.log) - @skip("not supported by api") - def test_job_submit_partial_fail(self): - """Test job submit partial fail.""" - job_id = [] - - def _side_effect(self, *args, **kwargs): - # pylint: disable=unused-argument - job_id.append(self.job_id) - raise RequestsApiError("Kaboom") - - fail_points = ["put_object_storage", "callback_upload"] - - for fail_method in fail_points: - with self.subTest(fail_method=fail_method): - with mock.patch.object( - RestJob, fail_method, side_effect=_side_effect, autospec=True - ): - with self.assertRaises(IBMBackendApiError): - self.sim_backend.run(self.bell) - - self.assertTrue(job_id, "Job ID not saved.") - job = self.service.job(job_id[0]) - self.assertEqual( - job.status(), - JobStatus.CANCELLED, - f"Job {job.job_id()} status is {job.status()} and not cancelled!", - ) - def test_job_circuits(self): """Test job circuits.""" self.assertEqual(str(self.bell), str(self.sim_job.inputs["circuits"][0])) diff --git a/test/integration/test_ibm_job_attributes.py b/test/integration/test_ibm_job_attributes.py index fd9ff1ad59..4469f4da69 100644 --- a/test/integration/test_ibm_job_attributes.py +++ b/test/integration/test_ibm_job_attributes.py @@ -22,7 +22,7 @@ from qiskit import QuantumCircuit from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES -from qiskit_ibm_provider.exceptions import IBMBackendValueError +from qiskit_ibm_runtime.exceptions import IBMBackendValueError from qiskit_ibm_runtime import IBMBackend, RuntimeJob from qiskit_ibm_runtime.exceptions import IBMInputValueError diff --git a/test/integration/test_options.py b/test/integration/test_options.py index 00db917746..89c90006eb 100644 --- a/test/integration/test_options.py +++ b/test/integration/test_options.py @@ -13,12 +13,12 @@ """Tests for job functions using real runtime service.""" from qiskit import QuantumCircuit -from qiskit.providers.fake_provider import FakeManila from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp from qiskit_aer.noise import NoiseModel from qiskit_ibm_runtime import Session, Sampler, Options, Estimator +from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime.exceptions import RuntimeJobFailureError from ..ibm_test_case import IBMIntegrationTestCase diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index a6c6c0ed52..41028ba8f5 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -17,13 +17,12 @@ from requests.exceptions import ProxyError -from qiskit_ibm_provider.proxies import ProxyConfiguration -from qiskit_ibm_provider.api.exceptions import RequestsApiError as ProviderRequestsApiError +from qiskit_ibm_runtime.proxies import ProxyConfiguration from qiskit_ibm_runtime import QiskitRuntimeService from qiskit_ibm_runtime.api.client_parameters import ClientParameters from qiskit_ibm_runtime.api.clients import AuthClient, VersionClient from qiskit_ibm_runtime.api.clients.runtime import RuntimeClient -from qiskit_ibm_runtime.api.exceptions import RequestsApiError as RuntimeRequestsApiError +from qiskit_ibm_runtime.api.exceptions import RequestsApiError from ..ibm_test_case import IBMTestCase from ..decorators import IntegrationTestDependencies, integration_test_setup @@ -160,7 +159,7 @@ def test_invalid_proxy_port_runtime_client( url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_PORT_PROXIES), ) - with self.assertRaises(RuntimeRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: client = RuntimeClient(params) client.jobs_get(limit=1) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -174,7 +173,7 @@ def test_invalid_proxy_port_authclient(self, dependencies: IntegrationTestDepend url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_PORT_PROXIES), ) - with self.assertRaises(ProviderRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: _ = AuthClient(params) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -184,7 +183,7 @@ def test_invalid_proxy_port_versionclient( self, dependencies: IntegrationTestDependencies ) -> None: """Should raise RequestApiError with ProxyError using VersionClient.""" - with self.assertRaises(ProviderRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: version_finder = VersionClient(dependencies.url, proxies=INVALID_PORT_PROXIES) version_finder.version() @@ -201,7 +200,7 @@ def test_invalid_proxy_address_runtime_client( url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_ADDRESS_PROXIES), ) - with self.assertRaises(RuntimeRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: client = RuntimeClient(params) client.jobs_get(limit=1) @@ -218,7 +217,7 @@ def test_invalid_proxy_address_authclient( url=dependencies.url, proxies=ProxyConfiguration(urls=INVALID_ADDRESS_PROXIES), ) - with self.assertRaises(ProviderRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: _ = AuthClient(params) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -228,7 +227,7 @@ def test_invalid_proxy_address_versionclient( self, dependencies: IntegrationTestDependencies ) -> None: """Should raise RequestApiError with ProxyError using VersionClient.""" - with self.assertRaises(ProviderRequestsApiError) as context_manager: + with self.assertRaises(RequestsApiError) as context_manager: version_finder = VersionClient(dependencies.url, proxies=INVALID_ADDRESS_PROXIES) version_finder.version() diff --git a/test/integration/test_results.py b/test/integration/test_results.py index ad02a24da6..6431e4a1d9 100644 --- a/test/integration/test_results.py +++ b/test/integration/test_results.py @@ -21,7 +21,6 @@ from ..unit.mock.proxy_server import MockProxyServer, use_proxies from ..ibm_test_case import IBMIntegrationJobTestCase from ..decorators import run_integration_test -from ..utils import cancel_job_safe, wait_for_status class TestIntegrationResults(IBMIntegrationJobTestCase): @@ -199,37 +198,6 @@ def result_callback(job_id, result): self.assertEqual(iterations - 1, final_it) self.assertIsNotNone(job._ws_client._server_close_code) - @run_integration_test - def test_callback_cancel_job(self, service): - """Test canceling a running job while streaming results.""" - - def result_callback(job_id, result): - # pylint: disable=unused-argument - nonlocal final_it - if "iteration" in result: - final_it = result["iteration"] - - final_it = 0 - iterations = 5 - sub_tests = [JobStatus.QUEUED, JobStatus.RUNNING] - - for status in sub_tests: - with self.subTest(status=status): - if status == JobStatus.QUEUED: - _ = self._run_program(service) - - job = self._run_program( - service=service, - interim_results="foo", - callback=result_callback, - ) - wait_for_status(job, status) - if not cancel_job_safe(job, self.log): - return - time.sleep(3) # Wait for cleanup - self.assertIsNotNone(job._ws_client._server_close_code) - self.assertLess(final_it, iterations) - @skip("skip until qiskit-ibm-runtime #933 is fixed") @run_integration_test def test_websocket_proxy(self, service): diff --git a/test/integration/test_retrieve_job.py b/test/integration/test_retrieve_job.py index 222fff7f70..3af9528e8c 100644 --- a/test/integration/test_retrieve_job.py +++ b/test/integration/test_retrieve_job.py @@ -184,7 +184,9 @@ def test_jobs_filter_by_date(self, service): job = self._run_program(service) job.wait_for_final_state() time_after_job = datetime.now(timezone.utc) - rjobs = service.jobs(created_before=time_after_job, created_after=current_time) + rjobs = service.jobs( + created_before=time_after_job, created_after=current_time, pending=False, limit=20 + ) self.assertTrue(job.job_id() in [j.job_id() for j in rjobs]) for job in rjobs: self.assertTrue(job.creation_date <= time_after_job) diff --git a/test/qctrl/__init__.py b/test/qctrl/__init__.py new file mode 100644 index 0000000000..139b265e1a --- /dev/null +++ b/test/qctrl/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. diff --git a/test/qctrl/test_qctrl.py b/test/qctrl/test_qctrl.py index 0eeebbdfbd..7d5a5bf6e9 100644 --- a/test/qctrl/test_qctrl.py +++ b/test/qctrl/test_qctrl.py @@ -12,8 +12,6 @@ """Tests for job functions using real runtime service.""" -import time - from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector, hellinger_fidelity from qiskit.providers.jobstatus import JobStatus diff --git a/test/unit/fake_provider/__init__.py b/test/unit/fake_provider/__init__.py new file mode 100644 index 0000000000..139b265e1a --- /dev/null +++ b/test/unit/fake_provider/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2024. +# +# 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. diff --git a/test/unit/fake_provider/test_fake_backends.py b/test/unit/fake_provider/test_fake_backends.py index 066583c7d3..e4ec088342 100644 --- a/test/unit/fake_provider/test_fake_backends.py +++ b/test/unit/fake_provider/test_fake_backends.py @@ -18,7 +18,7 @@ from qiskit.utils import optionals from qiskit_ibm_runtime.fake_provider import FakeAthens, FakePerth -from ..ibm_test_case import IBMTestCase +from ...ibm_test_case import IBMTestCase def get_test_circuit(): diff --git a/test/unit/mock/fake_api_backend.py b/test/unit/mock/fake_api_backend.py index 6ad2e4f5ca..75b06c9d00 100644 --- a/test/unit/mock/fake_api_backend.py +++ b/test/unit/mock/fake_api_backend.py @@ -15,7 +15,7 @@ from datetime import datetime as python_datetime from dataclasses import dataclass -from qiskit.providers.fake_provider import FakeLima +from qiskit_ibm_runtime.fake_provider import FakeLima @dataclass diff --git a/test/unit/mock/fake_runtime_client.py b/test/unit/mock/fake_runtime_client.py index 7be655d9a4..66e302282c 100644 --- a/test/unit/mock/fake_runtime_client.py +++ b/test/unit/mock/fake_runtime_client.py @@ -22,7 +22,7 @@ from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit_ibm_provider.utils.hgp import from_instance_format +from qiskit_ibm_runtime.utils.hgp import from_instance_format from qiskit_ibm_runtime.api.exceptions import RequestsApiError from qiskit_ibm_runtime.utils import RuntimeEncoder diff --git a/test/unit/mock/proxy_server.py b/test/unit/mock/proxy_server.py index 16f95c680a..a50b8141b5 100644 --- a/test/unit/mock/proxy_server.py +++ b/test/unit/mock/proxy_server.py @@ -15,7 +15,7 @@ import subprocess from contextlib import contextmanager -from qiskit_ibm_provider.proxies import ProxyConfiguration +from qiskit_ibm_runtime.proxies import ProxyConfiguration class MockProxyServer: diff --git a/test/unit/test_account.py b/test/unit/test_account.py index 5c04947848..cd730c0b43 100644 --- a/test/unit/test_account.py +++ b/test/unit/test_account.py @@ -19,7 +19,7 @@ from typing import Any from unittest import skipIf -from qiskit_ibm_provider.proxies import ProxyConfiguration +from qiskit_ibm_runtime.proxies import ProxyConfiguration from qiskit_ibm_runtime.accounts import ( AccountManager, Account, diff --git a/test/unit/test_backend.py b/test/unit/test_backend.py index d9f081823c..3eda26ed2b 100644 --- a/test/unit/test_backend.py +++ b/test/unit/test_backend.py @@ -16,11 +16,10 @@ import warnings from qiskit import transpile, qasm3, QuantumCircuit -from qiskit.providers.fake_provider import FakeManila from qiskit.providers.models import BackendStatus -from qiskit_ibm_provider.exceptions import IBMBackendValueError - +from qiskit_ibm_runtime.exceptions import IBMBackendValueError +from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime.ibm_backend import IBMBackend from ..ibm_test_case import IBMTestCase diff --git a/test/unit/test_backend_retrieval.py b/test/unit/test_backend_retrieval.py index de809d0964..0de18f164e 100644 --- a/test/unit/test_backend_retrieval.py +++ b/test/unit/test_backend_retrieval.py @@ -15,7 +15,7 @@ import uuid from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit.providers.fake_provider import FakeLima +from qiskit_ibm_runtime.fake_provider import FakeLima from .mock.fake_runtime_service import FakeRuntimeService from .mock.fake_api_backend import FakeApiBackendSpecs diff --git a/test/unit/test_client_parameters.py b/test/unit/test_client_parameters.py index 6b8716bd15..fe5019f9cd 100644 --- a/test/unit/test_client_parameters.py +++ b/test/unit/test_client_parameters.py @@ -16,7 +16,7 @@ from requests_ntlm import HttpNtlmAuth -from qiskit_ibm_provider.proxies import ProxyConfiguration +from qiskit_ibm_runtime.proxies import ProxyConfiguration from qiskit_ibm_runtime.api.client_parameters import ClientParameters from qiskit_ibm_runtime.api.auth import CloudAuth, QuantumAuth diff --git a/test/unit/test_data_serialization.py b/test/unit/test_data_serialization.py index ad5eeb42dc..ca6fcc19fa 100644 --- a/test/unit/test_data_serialization.py +++ b/test/unit/test_data_serialization.py @@ -26,12 +26,12 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import EfficientSU2, CXGate, PhaseGate, U2Gate -from qiskit.providers.fake_provider import FakeNairobi import qiskit.quantum_info as qi 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 qiskit_ibm_runtime.fake_provider import FakeNairobi # TODO: Remove when they are in terra from qiskit_ibm_runtime.qiskit.primitives import BindingsArray, ObservablesArray diff --git a/test/unit/test_ibm_primitives.py b/test/unit/test_ibm_primitives.py index cc70541075..e4b6889c7d 100644 --- a/test/unit/test_ibm_primitives.py +++ b/test/unit/test_ibm_primitives.py @@ -23,7 +23,6 @@ from qiskit.circuit import QuantumCircuit from qiskit.quantum_info import SparsePauliOp -from qiskit.providers.fake_provider import FakeManila from qiskit_aer.noise import NoiseModel from qiskit_ibm_runtime import ( @@ -34,6 +33,7 @@ ) from qiskit_ibm_runtime.ibm_backend import IBMBackend from qiskit_ibm_runtime.utils.default_session import _DEFAULT_SESSION +from qiskit_ibm_runtime.fake_provider import FakeManila from ..ibm_test_case import IBMTestCase from ..utils import ( diff --git a/test/unit/test_ibm_primitives_v2.py b/test/unit/test_ibm_primitives_v2.py index f8d9ceba14..438ad5f06c 100644 --- a/test/unit/test_ibm_primitives_v2.py +++ b/test/unit/test_ibm_primitives_v2.py @@ -24,7 +24,7 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import RealAmplitudes from qiskit.quantum_info import SparsePauliOp -from qiskit.providers.fake_provider import FakeManila +from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime import ( Sampler, diff --git a/test/unit/test_options.py b/test/unit/test_options.py index c670b5e66c..b6ac0f7686 100644 --- a/test/unit/test_options.py +++ b/test/unit/test_options.py @@ -17,7 +17,7 @@ from ddt import data, ddt from pydantic import ValidationError from qiskit.providers import BackendV1 -from qiskit.providers.fake_provider import FakeManila, FakeNairobiV2 + from qiskit.transpiler import CouplingMap from qiskit_aer.noise import NoiseModel @@ -25,6 +25,7 @@ from qiskit_ibm_runtime.options.utils import merge_options from qiskit_ibm_runtime.options import EstimatorOptions, SamplerOptions from qiskit_ibm_runtime.utils.qctrl import _warn_and_clean_options +from qiskit_ibm_runtime.fake_provider import FakeManila, FakeNairobiV2 from ..ibm_test_case import IBMTestCase from ..utils import dict_keys_equal, dict_paritally_equal, flat_dict_partially_equal, combine diff --git a/test/unit/test_runtime_ws.py b/test/unit/test_runtime_ws.py index 27c3230c4e..3dc232a1bb 100644 --- a/test/unit/test_runtime_ws.py +++ b/test/unit/test_runtime_ws.py @@ -15,7 +15,6 @@ import time from unittest.mock import MagicMock -from qiskit.providers.fake_provider import FakeQasmSimulator from qiskit.quantum_info import SparsePauliOp from qiskit_ibm_runtime import ( RuntimeJob, @@ -235,7 +234,6 @@ def _get_job(self, callback=None, job_id=JOB_ID_PROGRESS_DONE, backend=None): params = ClientParameters( channel="ibm_quantum", token="my_token", url=MockWsServer.VALID_WS_URL ) - backend = backend or FakeQasmSimulator() job = RuntimeJob( backend=backend, api_client=BaseFakeRuntimeClient(), diff --git a/test/unit/test_session.py b/test/unit/test_session.py index a8bd57c3d2..db7816ad91 100644 --- a/test/unit/test_session.py +++ b/test/unit/test_session.py @@ -15,10 +15,9 @@ import sys import time from concurrent.futures import ThreadPoolExecutor, wait - from unittest.mock import MagicMock, Mock, patch -from qiskit_ibm_runtime.fake_provider import FakeManila +from qiskit_ibm_runtime.fake_provider import FakeManila from qiskit_ibm_runtime import Session from qiskit_ibm_runtime.ibm_backend import IBMBackend from qiskit_ibm_runtime.utils.default_session import _DEFAULT_SESSION diff --git a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py index 1454204785..d548665a1a 100644 --- a/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py +++ b/test/unit/transpiler/passes/scheduling/test_dynamical_decoupling.py @@ -35,13 +35,13 @@ DynamicCircuitInstructionDurations, ) -from .control_flow_test_case import ControlFlowTestCase +from .....ibm_test_case import IBMTestCase # pylint: disable=invalid-name,not-context-manager @ddt -class TestPadDynamicalDecoupling(ControlFlowTestCase): +class TestPadDynamicalDecoupling(IBMTestCase): """Tests PadDynamicalDecoupling pass.""" def setUp(self): @@ -1038,18 +1038,32 @@ def test_disjoint_coupling_map(self): self.assertEqual(delay_dict[0], delay_dict[2]) def test_no_unused_qubits(self): - """Test DD with if_test circuit that unused qubits are untouched and not scheduled. - - This ensures that programs don't have unnecessary information for unused qubits. - Which might hurt performance in later executon stages. + """Test DD with if_test circuit that unused qubits are untouched and + not scheduled. Unused qubits may also have missing durations when + not operational. + This ensures that programs don't have unnecessary information for + unused qubits. + Which might hurt performance in later execution stages. """ + # Here "x" on qubit 3 is not defined + durations = DynamicCircuitInstructionDurations( + [ + ("h", 0, 50), + ("x", 0, 50), + ("x", 1, 50), + ("x", 2, 50), + ("measure", 0, 840), + ("reset", 0, 1340), + ] + ) + dd_sequence = [XGate(), XGate()] pm = PassManager( [ ASAPScheduleAnalysis(self.durations), PadDynamicalDecoupling( - self.durations, + durations, dd_sequence, pulse_alignment=1, sequence_min_length_ratios=[0.0], @@ -1057,16 +1071,13 @@ def test_no_unused_qubits(self): ] ) - qc = QuantumCircuit(3, 1) + qc = QuantumCircuit(4, 1) qc.measure(0, 0) qc.x(1) - with qc.if_test((0, True)): - qc.x(1) - qc.measure(0, 0) with qc.if_test((0, True)): qc.x(0) qc.x(1) qc_dd = pm.run(qc) - dont_use = qc_dd.qubits[-1] + dont_use = qc_dd.qubits[-2:] for op in qc_dd.data: self.assertNotIn(dont_use, op.qubits) diff --git a/test/unit/transpiler/passes/scheduling/test_scheduler.py b/test/unit/transpiler/passes/scheduling/test_scheduler.py index e9ed82e1f1..5903fec8e8 100644 --- a/test/unit/transpiler/passes/scheduling/test_scheduler.py +++ b/test/unit/transpiler/passes/scheduling/test_scheduler.py @@ -15,12 +15,12 @@ from unittest.mock import patch from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, transpile -from qiskit.providers.fake_provider import FakeJakarta from qiskit.pulse import Schedule, Play, Constant, DriveChannel from qiskit.transpiler.passes import ConvertConditionsToIfOps from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError +from qiskit_ibm_runtime.fake_provider import FakeJakarta from qiskit_ibm_runtime.transpiler.passes.scheduling.pad_delay import PadDelay from qiskit_ibm_runtime.transpiler.passes.scheduling.scheduler import ( ALAPScheduleAnalysis, @@ -30,12 +30,12 @@ DynamicCircuitInstructionDurations, ) -from .control_flow_test_case import ControlFlowTestCase +from .....ibm_test_case import IBMTestCase # pylint: disable=invalid-name,not-context-manager -class TestASAPSchedulingAndPaddingPass(ControlFlowTestCase): +class TestASAPSchedulingAndPaddingPass(IBMTestCase): """Tests the ASAP Scheduling passes""" def test_if_test_gate_after_measure(self): @@ -808,7 +808,7 @@ def test_c_if_plugin_conversion_with_transpile(self): self.assertEqual(expected, scheduled) -class TestALAPSchedulingAndPaddingPass(ControlFlowTestCase): +class TestALAPSchedulingAndPaddingPass(IBMTestCase): """Tests the ALAP Scheduling passes""" def test_alap(self): @@ -1774,23 +1774,16 @@ def test_transpile_both_paths(self): qr = QuantumRegister(7, name="q") expected = QuantumCircuit(qr, cr) - expected.delay(24080, qr[1]) - expected.delay(24080, qr[2]) - expected.delay(24080, qr[3]) - expected.delay(24080, qr[4]) - expected.delay(24080, qr[5]) - expected.delay(24080, qr[6]) + for q_ind in range(1, 7): + expected.delay(24240, qr[q_ind]) expected.measure(qr[0], cr[0]) with expected.if_test((cr[0], 1)): expected.x(qr[0]) with expected.if_test((cr[0], 1)): - expected.delay(160, qr[0]) expected.x(qr[1]) - expected.delay(160, qr[2]) - expected.delay(160, qr[3]) - expected.delay(160, qr[4]) - expected.delay(160, qr[5]) - expected.delay(160, qr[6]) + for q_ind in range(7): + if q_ind != 1: + expected.delay(160, qr[q_ind]) self.assertEqual(expected, scheduled) def test_c_if_plugin_conversion_with_transpile(self): @@ -1837,7 +1830,7 @@ def test_no_unused_qubits(self): """Test DD with if_test circuit that unused qubits are untouched and not scheduled. This ensures that programs don't have unnecessary information for unused qubits. - Which might hurt performance in later executon stages. + Which might hurt performance in later execution stages. """ durations = DynamicCircuitInstructionDurations([("x", None, 200), ("measure", None, 840)]) diff --git a/test/unit/transpiler/passes/scheduling/test_utils.py b/test/unit/transpiler/passes/scheduling/test_utils.py index 50cd79ff7e..e53cf59e65 100644 --- a/test/unit/transpiler/passes/scheduling/test_utils.py +++ b/test/unit/transpiler/passes/scheduling/test_utils.py @@ -15,6 +15,7 @@ from qiskit_ibm_runtime.transpiler.passes.scheduling.utils import ( DynamicCircuitInstructionDurations, ) +from qiskit_ibm_runtime.fake_provider import FakeKolkata, FakeKolkataV2 from .....ibm_test_case import IBMTestCase @@ -51,6 +52,33 @@ def test_patch_measure(self): self.assertEqual(short_odd_durations.get("measure", (0,)), 1224) self.assertEqual(short_odd_durations.get("reset", (0,)), 1224) + def test_durations_from_backend_v1(self): + """Test loading and patching durations from a V1 Backend""" + + durations = DynamicCircuitInstructionDurations.from_backend(FakeKolkata()) + + self.assertEqual(durations.get("x", (0,)), 160) + self.assertEqual(durations.get("measure", (0,)), 3200) + self.assertEqual(durations.get("reset", (0,)), 3200) + + def test_durations_from_backend_v2(self): + """Test loading and patching durations from a V2 Backend""" + + durations = DynamicCircuitInstructionDurations.from_backend(FakeKolkataV2()) + + self.assertEqual(durations.get("x", (0,)), 160) + self.assertEqual(durations.get("measure", (0,)), 3200) + self.assertEqual(durations.get("reset", (0,)), 3200) + + def test_durations_from_target(self): + """Test loading and patching durations from a target""" + + durations = DynamicCircuitInstructionDurations.from_target(FakeKolkataV2().target) + + self.assertEqual(durations.get("x", (0,)), 160) + self.assertEqual(durations.get("measure", (0,)), 3200) + self.assertEqual(durations.get("reset", (0,)), 3200) + def test_patch_disable(self): """Test if schedules circuits with c_if after measure with a common clbit. See: https://github.com/Qiskit/qiskit-terra/issues/7654""" diff --git a/test/utils.py b/test/utils.py index 5473ed5afe..ea4ca2023e 100644 --- a/test/utils.py +++ b/test/utils.py @@ -15,16 +15,15 @@ import os import logging import time +import itertools import unittest from unittest import mock from typing import Dict, Optional, Any from datetime import datetime - from ddt import data, unpack -from qiskit.compiler import transpile -from qiskit.test.utils import generate_cases from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister +from qiskit.compiler import transpile from qiskit.providers.jobstatus import JOB_FINAL_STATES, JobStatus from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.models import BackendStatus, BackendProperties @@ -305,6 +304,27 @@ def submit_and_cancel(backend: IBMBackend, logger: logging.Logger) -> RuntimeJob return job +class Case(dict): + """""" + + +def generate_cases(docstring, dsc=None, name=None, **kwargs): + """Combines kwargs in Cartesian product and creates Case with them""" + ret = [] + keys = kwargs.keys() + vals = kwargs.values() + for values in itertools.product(*vals): + case = Case(zip(keys, values)) + if docstring is not None: + setattr(case, "__doc__", docstring.format(**case)) + if dsc is not None: + setattr(case, "__doc__", dsc.format(**case)) + if name is not None: + setattr(case, "__name__", name.format(**case)) + ret.append(case) + return ret + + def combine(**kwargs): """Decorator to create combinations and tests @combine(level=[0, 1, 2, 3], diff --git a/tox.ini b/tox.ini index f2662be4bb..773bc59183 100644 --- a/tox.ini +++ b/tox.ini @@ -37,7 +37,7 @@ envdir = .tox/docs deps = -r requirements-dev.txt commands = - sphinx-build -j auto -W -b html {posargs} {toxinidir}/docs {toxinidir}/docs/_build/html + sphinx-build -j auto -b html {posargs} {toxinidir}/docs {toxinidir}/docs/_build/html [testenv:docs-clean] skip_install = true