diff --git a/.pylintrc b/.pylintrc index c68dde34a6..a90abc2627 100644 --- a/.pylintrc +++ b/.pylintrc @@ -74,7 +74,8 @@ disable=no-self-use, # disabled as it is too verbose import-outside-toplevel, docstring-first-line-empty, no-name-in-module, # remove when pylint behaves - import-error # remove when pylint behaves + import-error, # remove when pylint behaves + bad-continuation, bad-whitespace # differences of opinion with black [REPORTS] @@ -212,7 +213,7 @@ max-nested-blocks=5 [FORMAT] # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=105 # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ diff --git a/Makefile b/Makefile index 05a31372f7..443fd028b2 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ mypy: mypy --module qiskit_ibm_runtime style: - pycodestyle qiskit_ibm_runtime test + black --check qiskit_ibm_runtime test test: python -m unittest -v @@ -36,4 +36,7 @@ test3: python -m unittest -v test/ibm/test_ibm_job_attributes.py test/ibm/test_ibm_job.py test/ibm/websocket/test_websocket.py test/ibm/websocket/test_websocket_integration.py runtime_integration: - python -m unittest -v test/ibm/runtime/test_runtime_integration.py \ No newline at end of file + python -m unittest -v test/ibm/runtime/test_runtime_integration.py + +black: + black qiskit_ibm_runtime test \ No newline at end of file diff --git a/qiskit_ibm_runtime/__init__.py b/qiskit_ibm_runtime/__init__.py index 97bbd71fda..e21ae5fb84 100644 --- a/qiskit_ibm_runtime/__init__.py +++ b/qiskit_ibm_runtime/__init__.py @@ -296,17 +296,17 @@ def interim_result_callback(job_id, interim_result): setup_logger(logger) # Constants used by the IBM Quantum logger. -QISKIT_IBM_RUNTIME_LOGGER_NAME = 'qiskit_ibm_runtime' +QISKIT_IBM_RUNTIME_LOGGER_NAME = "qiskit_ibm_runtime" """The name of the IBM Quantum logger.""" -QISKIT_IBM_RUNTIME_LOG_LEVEL = 'QISKIT_IBM_RUNTIME_LOG_LEVEL' +QISKIT_IBM_RUNTIME_LOG_LEVEL = "QISKIT_IBM_RUNTIME_LOG_LEVEL" """The environment variable name that is used to set the level for the IBM Quantum logger.""" -QISKIT_IBM_RUNTIME_LOG_FILE = 'QISKIT_IBM_RUNTIME_LOG_FILE' +QISKIT_IBM_RUNTIME_LOG_FILE = "QISKIT_IBM_RUNTIME_LOG_FILE" """The environment variable name that is used to set the file for the IBM Quantum logger.""" def least_busy( - backends: List[Union[Backend, BaseBackend]], - reservation_lookahead: Optional[int] = 60 + backends: List[Union[Backend, BaseBackend]], + reservation_lookahead: Optional[int] = 60, ) -> Union[Backend, BaseBackend]: """Return the least busy backend from a list. @@ -330,14 +330,15 @@ def least_busy( does not have the ``pending_jobs`` attribute in its status. """ if not backends: - raise IBMError('Unable to find the least_busy ' - 'backend from an empty list.') from None + raise IBMError( + "Unable to find the least_busy backend from an empty list." + ) from None try: candidates = [] now = datetime.now() for back in backends: backend_status = back.status() - if not backend_status.operational or backend_status.status_msg != 'active': + if not backend_status.operational or backend_status.status_msg != "active": continue if reservation_lookahead and isinstance(back, IBMBackend): end_time = now + timedelta(minutes=reservation_lookahead) @@ -345,12 +346,17 @@ def least_busy( if back.reservations(now, end_time): continue except Exception as err: # pylint: disable=broad-except - logger.warning("Unable to find backend reservation information. " - "It will not be taken into consideration. %s", str(err)) + logger.warning( + "Unable to find backend reservation information. " + "It will not be taken into consideration. %s", + str(err), + ) candidates.append(back) if not candidates: - raise IBMError('No backend matches the criteria.') + raise IBMError("No backend matches the criteria.") return min(candidates, key=lambda b: b.status().pending_jobs) except AttributeError as ex: - raise IBMError('A backend in the list does not have the `pending_jobs` ' - 'attribute in its status.') from ex + raise IBMError( + "A backend in the list does not have the `pending_jobs` " + "attribute in its status." + ) from ex diff --git a/qiskit_ibm_runtime/api/clients/account.py b/qiskit_ibm_runtime/api/clients/account.py index 7708c1047c..d0d73c3509 100644 --- a/qiskit_ibm_runtime/api/clients/account.py +++ b/qiskit_ibm_runtime/api/clients/account.py @@ -30,11 +30,7 @@ class AccountClient(BaseClient): """Client for accessing an individual IBM Quantum account.""" - def __init__( - self, - credentials: Credentials, - **request_kwargs: Any - ) -> None: + def __init__(self, credentials: Credentials, **request_kwargs: Any) -> None: """AccountClient constructor. Args: @@ -42,12 +38,17 @@ def __init__( **request_kwargs: Arguments for the request ``Session``. """ self._session = RetrySession( - credentials.base_url, credentials.access_token, **request_kwargs) + credentials.base_url, credentials.access_token, **request_kwargs + ) # base_api is used to handle endpoints that don't include h/g/p. # account_api is for h/g/p. self.base_api = Api(self._session) - self.account_api = Account(session=self._session, hub=credentials.hub, - group=credentials.group, project=credentials.project) + self.account_api = Account( + session=self._session, + hub=credentials.hub, + group=credentials.group, + project=credentials.project, + ) self._credentials = credentials # Backend-related public functions. @@ -75,9 +76,7 @@ def backend_status(self, backend_name: str) -> Dict[str, Any]: return self.account_api.backend(backend_name).status() def backend_properties( - self, - backend_name: str, - datetime: Optional[datetime] = None + self, backend_name: str, datetime: Optional[datetime] = None ) -> Dict[str, Any]: """Return the properties of the backend. @@ -114,10 +113,10 @@ def backend_job_limit(self, backend_name: str) -> Dict[str, Any]: return self.account_api.backend(backend_name).job_limit() def backend_reservations( - self, - backend_name: str, - start_datetime: Optional[datetime] = None, - end_datetime: Optional[datetime] = None + self, + backend_name: str, + start_datetime: Optional[datetime] = None, + end_datetime: Optional[datetime] = None, ) -> List: """Return backend reservation information. @@ -129,7 +128,7 @@ def backend_reservations( Returns: Backend reservation information. """ - backend_api = Backend(self._session, backend_name, '/Network') + backend_api = Backend(self._session, backend_name, "/Network") return backend_api.reservations(start_datetime, end_datetime) def my_reservations(self) -> List: diff --git a/qiskit_ibm_runtime/api/clients/auth.py b/qiskit_ibm_runtime/api/clients/auth.py index 18e36deeed..b59121423c 100644 --- a/qiskit_ibm_runtime/api/clients/auth.py +++ b/qiskit_ibm_runtime/api/clients/auth.py @@ -56,8 +56,9 @@ def _init_service_clients(self, **request_kwargs: Any) -> Api: self._service_urls = self.user_urls() # Create the api server client, using the access token. - base_api = Api(RetrySession(self._service_urls['http'], access_token, - **request_kwargs)) + base_api = Api( + RetrySession(self._service_urls["http"], access_token, **request_kwargs) + ) return base_api @@ -73,19 +74,22 @@ def _request_access_token(self) -> str: """ try: response = self.auth_api.login(self.api_token) - return response['id'] + 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 = original_exception.response # pylint: disable=no-member + 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'] + 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. @@ -106,7 +110,7 @@ def user_urls(self) -> Dict[str, str]: * ``services`: The API URL for additional services. """ response = self.auth_api.user_info() - return response['urls'] + return response["urls"] def user_hubs(self) -> List[Dict[str, str]]: """Retrieve the hub/group/project sets available to the user. @@ -122,15 +126,17 @@ def user_hubs(self) -> List[Dict[str, str]]: 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} + 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'): + if project.get("isDefault"): hubs.insert(0, entry) else: hubs.append(entry) diff --git a/qiskit_ibm_runtime/api/clients/base.py b/qiskit_ibm_runtime/api/clients/base.py index ff9c128fb1..05b96d085d 100644 --- a/qiskit_ibm_runtime/api/clients/base.py +++ b/qiskit_ibm_runtime/api/clients/base.py @@ -33,6 +33,7 @@ class BaseClient: """Abstract class for clients.""" + pass @@ -52,11 +53,11 @@ class BaseWebsocketClient(BaseClient, ABC): """Maximum time to wait between retries.""" def __init__( - self, - websocket_url: str, - credentials: Credentials, - job_id: str, - message_queue: Optional[Queue] = None + self, + websocket_url: str, + credentials: Credentials, + job_id: str, + message_queue: Optional[Queue] = None, ) -> None: """BaseWebsocketClient constructor. @@ -66,8 +67,10 @@ def __init__( job_id: Job ID. message_queue: Queue used to hold received messages. """ - self._websocket_url = websocket_url.rstrip('/') - self._proxy_params = ws_proxy_params(credentials=credentials, ws_url=self._websocket_url) + self._websocket_url = websocket_url.rstrip("/") + self._proxy_params = ws_proxy_params( + credentials=credentials, ws_url=self._websocket_url + ) self._access_token = credentials.access_token self._job_id = job_id self._message_queue = message_queue @@ -128,8 +131,12 @@ def on_close(self, wsa: WebSocketApp, status_code: int, msg: str) -> None: # 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) + 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. @@ -141,10 +148,10 @@ def on_error(self, wsa: WebSocketApp, error: Exception) -> None: self._error = self._format_exception(error) def stream( - self, - url: str, - retries: int = 5, - backoff_factor: float = 0.5, + self, + url: str, + retries: int = 5, + backoff_factor: float = 0.5, ) -> Any: """Stream from the websocket. @@ -165,17 +172,24 @@ def stream( 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) + 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) + 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._ws.run_forever( + ping_interval=60, ping_timeout=10, **self._proxy_params + ) self.connected = False logger.debug("Websocket run_forever finished.") @@ -183,28 +197,35 @@ def stream( # Handle path-specific errors self._handle_stream_iteration() - if self._client_close_code in (WebsocketClientCloseCode.NORMAL, - WebsocketClientCloseCode.CANCEL): + 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 + "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}." + 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 " \ - f"websocket connection." + error_message = ( + "Max retries exceeded: Failed to establish a " + f"websocket connection." + ) if self._error: error_message += f" Error: {self._error}" @@ -214,13 +235,19 @@ def stream( # 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) + 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.' + exception_message = ( + "Max retries exceeded: Failed to establish a websocket " + "connection due to a network error." + ) logger.info(exception_message) raise WebsocketError(exception_message) @@ -247,8 +274,10 @@ def _backoff_time(self, backoff_factor: float, current_retry_attempt: int) -> fl return min(self.BACKOFF_MAX, backoff_time) def disconnect( - self, - close_code: Optional[WebsocketClientCloseCode] = WebsocketClientCloseCode.NORMAL + self, + close_code: Optional[ + WebsocketClientCloseCode + ] = WebsocketClientCloseCode.NORMAL, ) -> None: """Close the websocket connection. @@ -256,7 +285,9 @@ def disconnect( close_code: Disconnect status code. """ if self._ws is not None: - logger.debug("Client closing websocket connection with code %s.", close_code) + 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: @@ -271,8 +302,11 @@ def _format_exception(self, error: Exception) -> str: Returns: Formatted exception. """ - return "".join(traceback.format_exception( - type(error), error, getattr(error, '__traceback__', ""))) + return "".join( + traceback.format_exception( + type(error), error, getattr(error, "__traceback__", "") + ) + ) def _reset_state(self) -> None: """Reset state for a new connection.""" diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index ed664dcfe3..067e0f7788 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -27,16 +27,19 @@ class RuntimeClient: """Client for accessing runtime service.""" def __init__( - self, - credentials: Credentials, + self, + credentials: Credentials, ) -> None: """RandomClient constructor. Args: credentials: Account credentials. """ - self._session = RetrySession(credentials.runtime_url, credentials.access_token, - **credentials.connection_parameters()) + self._session = RetrySession( + credentials.runtime_url, + credentials.access_token, + **credentials.connection_parameters() + ) self.api = Runtime(self._session) def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: @@ -52,13 +55,13 @@ def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: return self.api.list_programs(limit, skip) def program_create( - self, - program_data: str, - name: str, - description: str, - max_execution_time: int, - is_public: Optional[bool] = False, - spec: Optional[Dict] = None + self, + program_data: str, + name: str, + description: str, + max_execution_time: int, + is_public: Optional[bool] = False, + spec: Optional[Dict] = None, ) -> Dict: """Create a new program. @@ -76,8 +79,10 @@ def program_create( return self.api.create_program( program_data=program_data, name=name, - description=description, max_execution_time=max_execution_time, - is_public=is_public, spec=spec + description=description, + max_execution_time=max_execution_time, + is_public=is_public, + spec=spec, ) def program_get(self, program_id: str) -> Dict: @@ -106,12 +111,12 @@ def set_program_visibility(self, program_id: str, public: bool) -> None: self.api.program(program_id).make_private() def program_run( - self, - program_id: str, - credentials: Credentials, - backend_name: str, - params: Dict, - image: str + self, + program_id: str, + credentials: Credentials, + backend_name: str, + params: Dict, + image: str, ) -> Dict: """Run the specified program. @@ -125,10 +130,15 @@ def program_run( Returns: JSON response. """ - return self.api.program_run(program_id=program_id, hub=credentials.hub, - group=credentials.group, project=credentials.project, - backend_name=backend_name, params=params, - image=image) + return self.api.program_run( + program_id=program_id, + hub=credentials.hub, + group=credentials.group, + project=credentials.project, + backend_name=backend_name, + params=params, + image=image, + ) def program_delete(self, program_id: str) -> None: """Delete the specified program. @@ -139,13 +149,13 @@ def program_delete(self, program_id: str) -> None: self.api.program(program_id).delete() def program_update( - self, - program_id: str, - program_data: str = None, - name: str = None, - description: str = None, - max_execution_time: int = None, - spec: Optional[Dict] = None + self, + program_id: str, + program_data: str = None, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None, ) -> None: """Update a program. @@ -162,8 +172,11 @@ def program_update( if any([name, description, max_execution_time, spec]): self.api.program(program_id).update_metadata( - name=name, description=description, - max_execution_time=max_execution_time, spec=spec) + name=name, + description=description, + max_execution_time=max_execution_time, + spec=spec, + ) def job_get(self, job_id: str) -> Dict: """Get job data. @@ -179,14 +192,14 @@ def job_get(self, job_id: str) -> Dict: return response def jobs_get( - self, - limit: int = None, - skip: int = None, - pending: bool = None, - program_id: str = None, - hub: str = None, - group: str = None, - project: str = None + self, + limit: int = None, + skip: int = None, + pending: bool = None, + program_id: str = None, + hub: str = None, + group: str = None, + project: str = None, ) -> Dict: """Get job data for all jobs. @@ -203,8 +216,15 @@ def jobs_get( Returns: JSON response. """ - return self.api.jobs_get(limit=limit, skip=skip, pending=pending, - program_id=program_id, hub=hub, group=group, project=project) + return self.api.jobs_get( + limit=limit, + skip=skip, + pending=pending, + program_id=program_id, + hub=hub, + group=group, + project=project, + ) def job_results(self, job_id: str) -> str: """Get the results of a program job. diff --git a/qiskit_ibm_runtime/api/clients/runtime_ws.py b/qiskit_ibm_runtime/api/clients/runtime_ws.py index f1b9c9cc1c..5883d27221 100644 --- a/qiskit_ibm_runtime/api/clients/runtime_ws.py +++ b/qiskit_ibm_runtime/api/clients/runtime_ws.py @@ -26,11 +26,11 @@ class RuntimeWebsocketClient(BaseWebsocketClient): """Client for websocket communication with the IBM Quantum runtime service.""" def __init__( - self, - websocket_url: str, - credentials: Credentials, - job_id: str, - message_queue: Optional[Queue] = None + self, + websocket_url: str, + credentials: Credentials, + job_id: str, + message_queue: Optional[Queue] = None, ) -> None: """WebsocketClient constructor. @@ -55,11 +55,7 @@ def _handle_message(self, message: str) -> None: self._message_queue.put_nowait(message) self._current_retry = 0 - def job_results( - self, - max_retries: int = 5, - backoff_factor: float = 0.5 - ) -> None: + def job_results(self, max_retries: int = 5, backoff_factor: float = 0.5) -> None: """Return the interim result of a runtime job. Args: @@ -70,7 +66,7 @@ def job_results( Raises: WebsocketError: If a websocket error occurred. """ - url = '{}/stream/jobs/{}'.format(self._websocket_url, self._job_id) + 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: diff --git a/qiskit_ibm_runtime/api/clients/utils.py b/qiskit_ibm_runtime/api/clients/utils.py index 1ed41ff68d..2152f5f103 100644 --- a/qiskit_ibm_runtime/api/clients/utils.py +++ b/qiskit_ibm_runtime/api/clients/utils.py @@ -32,29 +32,37 @@ def ws_proxy_params(credentials: Credentials, ws_url: str) -> Dict: out = {} if "proxies" in conn_data: - proxies = conn_data['proxies'] + proxies = conn_data["proxies"] url_parts = urlparse(ws_url) proxy_keys = [ ws_url, - 'wss', - 'https://' + url_parts.hostname, - 'https', - 'all://' + url_parts.hostname, - 'all' + "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 + 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) + out["http_proxy_auth"] = ( + proxy_parts.username, + proxy_parts.password, + ) break if "auth" in conn_data: - out['http_proxy_auth'] = (credentials.proxies['username_ntlm'], - credentials.proxies['password_ntlm']) + out["http_proxy_auth"] = ( + credentials.proxies["username_ntlm"], + credentials.proxies["password_ntlm"], + ) return out diff --git a/qiskit_ibm_runtime/api/clients/version.py b/qiskit_ibm_runtime/api/clients/version.py index 99da27cfcc..68b2c13c2c 100644 --- a/qiskit_ibm_runtime/api/clients/version.py +++ b/qiskit_ibm_runtime/api/clients/version.py @@ -30,8 +30,7 @@ def __init__(self, url: str, **request_kwargs: Any) -> None: url: URL of the service. **request_kwargs: Arguments for the request ``Session``. """ - self.client_version_finder = Api( - RetrySession(url, **request_kwargs)) + self.client_version_finder = Api(RetrySession(url, **request_kwargs)) def version(self) -> Dict[str, Union[bool, str]]: """Return the version information. diff --git a/qiskit_ibm_runtime/api/exceptions.py b/qiskit_ibm_runtime/api/exceptions.py index 84b5e4b057..e29c14b72c 100644 --- a/qiskit_ibm_runtime/api/exceptions.py +++ b/qiskit_ibm_runtime/api/exceptions.py @@ -17,6 +17,7 @@ class ApiError(IBMError): """Generic IBM Quantum API error.""" + pass @@ -36,39 +37,47 @@ def __init__(self, message: str, status_code: int = -1): class WebsocketError(ApiError): """Exceptions related to websockets.""" + pass class WebsocketIBMProtocolError(WebsocketError): """Exceptions related to IBM Quantum protocol error.""" + pass class WebsocketAuthenticationError(WebsocketError): """Exception caused during websocket authentication.""" + pass class WebsocketTimeoutError(WebsocketError): """Timeout during websocket communication.""" + pass class WebsocketRetryableError(WebsocketError): """A websocket error that can be retried.""" + pass class AuthenticationLicenseError(ApiError): """Exception due to user not having accepted the license agreement.""" + pass class ApiIBMProtocolError(ApiError): """Exception related to IBM Quantum API protocol error.""" + pass class UserTimeoutExceededError(ApiError): """Exceptions related to exceeding user defined timeout.""" + pass diff --git a/qiskit_ibm_runtime/api/rest/account.py b/qiskit_ibm_runtime/api/rest/account.py index 104e7f7dc3..7479c7dcf3 100644 --- a/qiskit_ibm_runtime/api/rest/account.py +++ b/qiskit_ibm_runtime/api/rest/account.py @@ -25,15 +25,15 @@ class Account(RestAdapterBase): """Rest adapter for hub/group/project related endpoints.""" - URL_MAP = { - 'backends': '/devices/v/1' - } + URL_MAP = {"backends": "/devices/v/1"} - TEMPLATE_IBM_HUBS = '/Network/{hub}/Groups/{group}/Projects/{project}' + TEMPLATE_IBM_HUBS = "/Network/{hub}/Groups/{group}/Projects/{project}" """str: Template for creating an IBM Quantum URL with hub/group/project information.""" - def __init__(self, session: RetrySession, hub: str, group: str, project: str) -> None: + def __init__( + self, session: RetrySession, hub: str, group: str, project: str + ) -> None: """Account constructor. Args: @@ -42,7 +42,9 @@ def __init__(self, session: RetrySession, hub: str, group: str, project: str) -> group: The group to use. project: The project to use. """ - self.url_prefix = self.TEMPLATE_IBM_HUBS.format(hub=hub, group=group, project=project) + self.url_prefix = self.TEMPLATE_IBM_HUBS.format( + hub=hub, group=group, project=project + ) super().__init__(session, self.url_prefix) # Function-specific rest adapters. @@ -69,5 +71,5 @@ def backends(self, timeout: Optional[float] = None) -> List[Dict[str, Any]]: Returns: JSON response. """ - url = self.get_url('backends') + url = self.get_url("backends") return self.session.get(url, timeout=timeout).json() diff --git a/qiskit_ibm_runtime/api/rest/backend.py b/qiskit_ibm_runtime/api/rest/backend.py index 7835ff0f19..70946dafec 100644 --- a/qiskit_ibm_runtime/api/rest/backend.py +++ b/qiskit_ibm_runtime/api/rest/backend.py @@ -25,14 +25,16 @@ class Backend(RestAdapterBase): """Rest adapter for backend related endpoints.""" URL_MAP = { - 'properties': '/properties', - 'pulse_defaults': '/defaults', - 'status': '/queue/status', - 'jobs_limit': '/jobsLimit', - 'bookings': '/bookings/v2' + "properties": "/properties", + "pulse_defaults": "/defaults", + "status": "/queue/status", + "jobs_limit": "/jobsLimit", + "bookings": "/bookings/v2", } - def __init__(self, session: RetrySession, backend_name: str, url_prefix: str = '') -> None: + def __init__( + self, session: RetrySession, backend_name: str, url_prefix: str = "" + ) -> None: """Backend constructor. Args: @@ -41,7 +43,7 @@ def __init__(self, session: RetrySession, backend_name: str, url_prefix: str = ' url_prefix: Base URL. """ self.backend_name = backend_name - super().__init__(session, '{}/devices/{}'.format(url_prefix, backend_name)) + super().__init__(session, "{}/devices/{}".format(url_prefix, backend_name)) def properties(self, datetime: Optional[datetime] = None) -> Dict[str, Any]: """Return backend properties. @@ -53,23 +55,21 @@ def properties(self, datetime: Optional[datetime] = None) -> Dict[str, Any]: JSON response of backend properties. """ # pylint: disable=redefined-outer-name - url = self.get_url('properties') + url = self.get_url("properties") - params = { - 'version': 1 - } + params = {"version": 1} query = {} if datetime: - extra_filter = {'last_update_date': {'lt': datetime.isoformat()}} - query['where'] = extra_filter - params['filter'] = json.dumps(query) # type: ignore[assignment] + extra_filter = {"last_update_date": {"lt": datetime.isoformat()}} + query["where"] = extra_filter + params["filter"] = json.dumps(query) # type: ignore[assignment] response = self.session.get(url, params=params).json() # Adjust name of the backend. if response: - response['backend_name'] = self.backend_name + response["backend_name"] = self.backend_name return response @@ -79,7 +79,7 @@ def pulse_defaults(self) -> Dict[str, Any]: Returns: JSON response of pulse defaults. """ - url = self.get_url('pulse_defaults') + url = self.get_url("pulse_defaults") return self.session.get(url).json() def status(self) -> Dict[str, Any]: @@ -88,26 +88,26 @@ def status(self) -> Dict[str, Any]: Returns: JSON response of backend status. """ - url = self.get_url('status') + url = self.get_url("status") response = self.session.get(url).json() # Adjust fields according to the specs (BackendStatus). ret = { - 'backend_name': self.backend_name, - 'backend_version': response.get('backend_version', '0.0.0'), - 'status_msg': response.get('status', ''), - 'operational': bool(response.get('state', False)) + "backend_name": self.backend_name, + "backend_version": response.get("backend_version", "0.0.0"), + "status_msg": response.get("status", ""), + "operational": bool(response.get("state", False)), } # 'pending_jobs' is required, and should be >= 0. - if 'lengthQueue' in response: - ret['pending_jobs'] = max(response['lengthQueue'], 0) + if "lengthQueue" in response: + ret["pending_jobs"] = max(response["lengthQueue"], 0) else: - ret['pending_jobs'] = 0 + ret["pending_jobs"] = 0 # Not part of the schema. - if 'busy' in response: - ret['dedicated'] = response['busy'] + if "busy" in response: + ret["dedicated"] = response["busy"] return ret @@ -117,13 +117,13 @@ def job_limit(self) -> Dict[str, Any]: Returns: JSON response of job limit. """ - url = self.get_url('jobs_limit') + url = self.get_url("jobs_limit") return map_jobs_limit_response(self.session.get(url).json()) def reservations( - self, - start_datetime: Optional[datetime] = None, - end_datetime: Optional[datetime] = None + self, + start_datetime: Optional[datetime] = None, + end_datetime: Optional[datetime] = None, ) -> List: """Return backend reservation information. @@ -136,8 +136,8 @@ def reservations( """ params = {} if start_datetime: - params['initialDate'] = start_datetime.isoformat() + params["initialDate"] = start_datetime.isoformat() if end_datetime: - params['endDate'] = end_datetime.isoformat() - url = self.get_url('bookings') + params["endDate"] = end_datetime.isoformat() + url = self.get_url("bookings") return self.session.get(url, params=params).json() diff --git a/qiskit_ibm_runtime/api/rest/base.py b/qiskit_ibm_runtime/api/rest/base.py index fde3527789..809dc606fb 100644 --- a/qiskit_ibm_runtime/api/rest/base.py +++ b/qiskit_ibm_runtime/api/rest/base.py @@ -21,9 +21,9 @@ class RestAdapterBase: URL_MAP = {} # type: ignore[var-annotated] """Mapping between the internal name of an endpoint and the actual URL.""" - _HEADER_JSON_CONTENT = {'Content-Type': 'application/json'} + _HEADER_JSON_CONTENT = {"Content-Type": "application/json"} - def __init__(self, session: RetrySession, prefix_url: str = '') -> None: + def __init__(self, session: RetrySession, prefix_url: str = "") -> None: """RestAdapterBase constructor. Args: @@ -42,4 +42,4 @@ def get_url(self, identifier: str) -> str: Returns: The resolved URL of the endpoint (relative to the session base URL). """ - return '{}{}'.format(self.prefix_url, self.URL_MAP[identifier]) + return "{}{}".format(self.prefix_url, self.URL_MAP[identifier]) diff --git a/qiskit_ibm_runtime/api/rest/root.py b/qiskit_ibm_runtime/api/rest/root.py index 946dcfc5e8..d736d7ae18 100644 --- a/qiskit_ibm_runtime/api/rest/root.py +++ b/qiskit_ibm_runtime/api/rest/root.py @@ -25,14 +25,14 @@ 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' + "login": "/users/loginWithToken", + "user_info": "/users/me", + "hubs": "/Network", + "version": "/version", + "bookings": "/Network/bookings/v2", } -# Client functions. + # Client functions. def hubs(self) -> List[Dict[str, Any]]: """Return the list of hub/group/project sets available to the user. @@ -40,7 +40,7 @@ def hubs(self) -> List[Dict[str, Any]]: Returns: JSON response. """ - url = self.get_url('hubs') + url = self.get_url("hubs") return self.session.get(url).json() def version(self) -> Dict[str, Union[str, bool]]: @@ -56,17 +56,14 @@ def version(self) -> Dict[str, Union[str, bool]]: * ``api-*`` (str): The versions of each individual API component """ - url = self.get_url('version') + url = self.get_url("version") response = self.session.get(url) try: version_info = response.json() - version_info['new_api'] = True + version_info["new_api"] = True except json.JSONDecodeError: - return { - 'new_api': False, - 'api': response.text - } + return {"new_api": False, "api": response.text} return version_info @@ -79,8 +76,8 @@ def login(self, api_token: str) -> Dict[str, Any]: Returns: JSON response. """ - url = self.get_url('login') - return self.session.post(url, json={'apiToken': api_token}).json() + 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. @@ -88,7 +85,7 @@ def user_info(self) -> Dict[str, Any]: Returns: JSON response of user information. """ - url = self.get_url('user_info') + url = self.get_url("user_info") response = self.session.get(url).json() return response @@ -99,5 +96,5 @@ def reservations(self) -> List: Returns: JSON response. """ - url = self.get_url('bookings') + url = self.get_url("bookings") return self.session.get(url).json() diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index ceaaf09140..9e1ddc3073 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -27,13 +27,9 @@ class Runtime(RestAdapterBase): """Rest adapter for Runtime base endpoints.""" - URL_MAP = { - 'programs': '/programs', - 'jobs': '/jobs', - 'logout': '/logout' - } + URL_MAP = {"programs": "/programs", "jobs": "/jobs", "logout": "/logout"} - def program(self, program_id: str) -> 'Program': + def program(self, program_id: str) -> "Program": """Return an adapter for the program. Args: @@ -44,7 +40,7 @@ def program(self, program_id: str) -> 'Program': """ return Program(self.session, program_id) - def program_job(self, job_id: str) -> 'ProgramJob': + def program_job(self, job_id: str) -> "ProgramJob": """Return an adapter for the job. Args: @@ -65,22 +61,22 @@ def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: Returns: A list of runtime programs. """ - url = self.get_url('programs') + url = self.get_url("programs") payload: Dict[str, int] = {} if limit: - payload['limit'] = limit + payload["limit"] = limit if skip: - payload['offset'] = skip + payload["offset"] = skip return self.session.get(url, params=payload).json() def create_program( - self, - program_data: str, - name: str, - description: str, - max_execution_time: int, - is_public: Optional[bool] = False, - spec: Optional[Dict] = None + self, + program_data: str, + name: str, + description: str, + max_execution_time: int, + is_public: Optional[bool] = False, + spec: Optional[Dict] = None, ) -> Dict: """Upload a new program. @@ -95,26 +91,28 @@ def create_program( Returns: JSON response. """ - url = self.get_url('programs') - payload = {'name': name, - 'data': program_data, - 'cost': max_execution_time, - 'description': description, - 'is_public': is_public} + url = self.get_url("programs") + payload = { + "name": name, + "data": program_data, + "cost": max_execution_time, + "description": description, + "is_public": is_public, + } if spec is not None: - payload['spec'] = spec + payload["spec"] = spec data = json.dumps(payload) return self.session.post(url, data=data).json() def program_run( - self, - program_id: str, - hub: str, - group: str, - project: str, - backend_name: str, - params: Dict, - image: str + self, + program_id: str, + hub: str, + group: str, + project: str, + backend_name: str, + params: Dict, + image: str, ) -> Dict: """Execute the program. @@ -130,28 +128,28 @@ def program_run( Returns: JSON response. """ - url = self.get_url('jobs') + url = self.get_url("jobs") payload = { - 'program_id': program_id, - 'hub': hub, - 'group': group, - 'project': project, - 'backend': backend_name, - 'params': params, - 'runtime': image + "program_id": program_id, + "hub": hub, + "group": group, + "project": project, + "backend": backend_name, + "params": params, + "runtime": image, } data = json.dumps(payload, cls=RuntimeEncoder) return self.session.post(url, data=data).json() def jobs_get( - self, - limit: int = None, - skip: int = None, - pending: bool = None, - program_id: str = None, - hub: str = None, - group: str = None, - project: str = None + self, + limit: int = None, + skip: int = None, + pending: bool = None, + program_id: str = None, + hub: str = None, + group: str = None, + project: str = None, ) -> Dict: """Get a list of job data. @@ -168,23 +166,23 @@ def jobs_get( Returns: JSON response. """ - url = self.get_url('jobs') + url = self.get_url("jobs") payload: Dict[str, Union[int, str]] = {} if limit: - payload['limit'] = limit + payload["limit"] = limit if skip: - payload['offset'] = skip + payload["offset"] = skip if pending is not None: - payload['pending'] = 'true' if pending else 'false' + payload["pending"] = "true" if pending else "false" if program_id: - payload['program'] = program_id + payload["program"] = program_id if all([hub, group, project]): - payload['provider'] = f"{hub}/{group}/{project}" + payload["provider"] = f"{hub}/{group}/{project}" return self.session.get(url, params=payload).json() def logout(self) -> None: """Clear authorization cache.""" - url = self.get_url('logout') + url = self.get_url("logout") self.session.post(url) @@ -192,16 +190,18 @@ class Program(RestAdapterBase): """Rest adapter for program related endpoints.""" URL_MAP = { - 'self': '', - 'data': '/data', - 'run': '/jobs', - 'private': '/private', - 'public': '/public' + "self": "", + "data": "/data", + "run": "/jobs", + "private": "/private", + "public": "/public", } _executor = futures.ThreadPoolExecutor() - def __init__(self, session: RetrySession, program_id: str, url_prefix: str = '') -> None: + def __init__( + self, session: RetrySession, program_id: str, url_prefix: str = "" + ) -> None: """Job constructor. Args: @@ -209,7 +209,7 @@ def __init__(self, session: RetrySession, program_id: str, url_prefix: str = '') program_id: ID of the runtime program. url_prefix: Prefix to use in the URL. """ - super().__init__(session, '{}/programs/{}'.format(url_prefix, program_id)) + super().__init__(session, "{}/programs/{}".format(url_prefix, program_id)) def get(self) -> Dict[str, Any]: """Return program information. @@ -217,17 +217,17 @@ def get(self) -> Dict[str, Any]: Returns: JSON response. """ - url = self.get_url('self') + url = self.get_url("self") return self.session.get(url).json() def make_public(self) -> None: """Sets a runtime program's visibility to public.""" - url = self.get_url('public') + url = self.get_url("public") self.session.put(url) def make_private(self) -> None: """Sets a runtime program's visibility to private.""" - url = self.get_url('private') + url = self.get_url("private") self.session.put(url) def delete(self) -> None: @@ -236,7 +236,7 @@ def delete(self) -> None: Returns: JSON response. """ - url = self.get_url('self') + url = self.get_url("self") self.session.delete(url) def update_data(self, program_data: str) -> None: @@ -246,15 +246,16 @@ def update_data(self, program_data: str) -> None: program_data: Program data (base64 encoded). """ url = self.get_url("data") - self.session.put(url, data=program_data, - headers={'Content-Type': 'application/octet-stream'}) + self.session.put( + url, data=program_data, headers={"Content-Type": "application/octet-stream"} + ) def update_metadata( - self, - name: str = None, - description: str = None, - max_execution_time: int = None, - spec: Optional[Dict] = None + self, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None, ) -> None: """Update program metadata. @@ -281,18 +282,10 @@ def update_metadata( class ProgramJob(RestAdapterBase): """Rest adapter for program job related endpoints.""" - URL_MAP = { - 'self': '', - 'results': '/results', - 'cancel': '/cancel', - 'logs': '/logs' - } + URL_MAP = {"self": "", "results": "/results", "cancel": "/cancel", "logs": "/logs"} def __init__( - self, - session: RetrySession, - job_id: str, - url_prefix: str = '' + self, session: RetrySession, job_id: str, url_prefix: str = "" ) -> None: """ProgramJob constructor. @@ -301,8 +294,7 @@ def __init__( job_id: ID of the program job. url_prefix: Prefix to use in the URL. """ - super().__init__(session, '{}/jobs/{}'.format( - url_prefix, job_id)) + super().__init__(session, "{}/jobs/{}".format(url_prefix, job_id)) def get(self) -> Dict: """Return program job information. @@ -310,11 +302,11 @@ def get(self) -> Dict: Returns: JSON response. """ - return self.session.get(self.get_url('self')).json() + return self.session.get(self.get_url("self")).json() def delete(self) -> None: """Delete program job.""" - self.session.delete(self.get_url('self')) + self.session.delete(self.get_url("self")) def results(self) -> str: """Return program job results. @@ -322,12 +314,12 @@ def results(self) -> str: Returns: Job results. """ - response = self.session.get(self.get_url('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')) + self.session.post(self.get_url("cancel")) def logs(self) -> str: """Retrieve job logs. @@ -335,4 +327,4 @@ def logs(self) -> str: Returns: Job logs. """ - return self.session.get(self.get_url('logs')).text + return self.session.get(self.get_url("logs")).text diff --git a/qiskit_ibm_runtime/api/rest/utils/data_mapper.py b/qiskit_ibm_runtime/api/rest/utils/data_mapper.py index 2dec97fc4b..342f08b6fa 100644 --- a/qiskit_ibm_runtime/api/rest/utils/data_mapper.py +++ b/qiskit_ibm_runtime/api/rest/utils/data_mapper.py @@ -26,10 +26,7 @@ def map_jobs_limit_response(data: Dict[str, Any]) -> Dict[str, Any]: Returns: Mapped data. """ - field_map = { - 'maximumJobs': 'maximum_jobs', - 'runningJobs': 'running_jobs' - } + field_map = {"maximumJobs": "maximum_jobs", "runningJobs": "running_jobs"} dict_to_identifier(data, field_map) return data @@ -67,12 +64,14 @@ def to_python_identifier(name: str) -> str: # and underscores and cannot start with a digit. pattern = re.compile(r"\W|^(?=\d)", re.ASCII) if not name.isidentifier(): - name = re.sub(pattern, '_', name) + name = re.sub(pattern, "_", name) # Convert to snake case - name = re.sub('((?<=[a-z0-9])[A-Z]|(?!^)(?)() -RE_DEVICES_ENDPOINT = re.compile(r'^(.*/devices/)([^/}]{2,})(.*)$', re.IGNORECASE) +RE_DEVICES_ENDPOINT = re.compile(r"^(.*/devices/)([^/}]{2,})(.*)$", re.IGNORECASE) def _get_client_header() -> str: """Return the client version.""" try: - client_header = 'qiskit/' + pkg_resources.get_distribution('qiskit').version + client_header = "qiskit/" + pkg_resources.get_distribution("qiskit").version return client_header except Exception: # pylint: disable=broad-except pass - qiskit_pkgs = ['qiskit-terra', 'qiskit-aer', 'qiskit-ignis', 'qiskit-aqua'] + qiskit_pkgs = ["qiskit-terra", "qiskit-aer", "qiskit-ignis", "qiskit-aqua"] pkg_versions = {"qiskit-ibm": ibm_provider_version} for pkg_name in qiskit_pkgs: try: pkg_versions[pkg_name] = pkg_resources.get_distribution(pkg_name).version except Exception: # pylint: disable=broad-except pass - return ','.join(pkg_versions.keys()) + '/' + ','.join(pkg_versions.values()) + return ",".join(pkg_versions.keys()) + "/" + ",".join(pkg_versions.values()) CLIENT_APPLICATION = _get_client_header() @@ -77,13 +77,13 @@ class PostForcelistRetry(Retry): """ def increment( # type: ignore[no-untyped-def] - self, - method=None, - url=None, - response=None, - error=None, - _pool=None, - _stacktrace=None, + self, + method=None, + url=None, + response=None, + error=None, + _pool=None, + _stacktrace=None, ): """Overwrites parent class increment method for logging.""" if logger.getEffectiveLevel() is logging.DEBUG: @@ -92,16 +92,26 @@ def increment( # type: ignore[no-untyped-def] status = response.status data = response.data headers = response.headers - logger.debug("Retrying method=%s, url=%s, status=%s, error=%s, data=%s, headers=%s", - method, url, status, error, data, headers) - return super().increment(method=method, url=url, response=response, - error=error, _pool=_pool, _stacktrace=_stacktrace) + logger.debug( + "Retrying method=%s, url=%s, status=%s, error=%s, data=%s, headers=%s", + method, + url, + status, + error, + data, + headers, + ) + return super().increment( + method=method, + url=url, + response=response, + error=error, + _pool=_pool, + _stacktrace=_stacktrace, + ) def is_retry( - self, - method: str, - status_code: int, - has_retry_after: bool = False + self, method: str, status_code: int, has_retry_after: bool = False ) -> bool: """Indicate whether the request should be retried. @@ -113,7 +123,7 @@ def is_retry( Returns: ``True`` if the request should be retried, ``False`` otherwise. """ - if method.upper() == 'POST' and status_code in self.status_forcelist: + if method.upper() == "POST" and status_code in self.status_forcelist: return True return super().is_retry(method, status_code, has_retry_after) @@ -127,16 +137,16 @@ class RetrySession(Session): """ def __init__( - self, - base_url: str, - access_token: Optional[str] = None, - retries_total: int = 5, - retries_connect: int = 3, - backoff_factor: float = 0.5, - verify: bool = True, - proxies: Optional[Dict[str, str]] = None, - auth: Optional[AuthBase] = None, - timeout: Tuple[float, Union[float, None]] = (5.0, None) + self, + base_url: str, + access_token: Optional[str] = None, + retries_total: int = 5, + retries_connect: int = 3, + backoff_factor: float = 0.5, + verify: bool = True, + proxies: Optional[Dict[str, str]] = None, + auth: Optional[AuthBase] = None, + timeout: Tuple[float, Union[float, None]] = (5.0, None), ) -> None: """RetrySession constructor. @@ -176,15 +186,12 @@ def access_token(self, value: Optional[str]) -> None: """Set the session access token.""" self._access_token = value if value: - self.headers.update({'X-Access-Token': value}) # type: ignore[attr-defined] + self.headers.update({"X-Access-Token": value}) # type: ignore[attr-defined] else: - self.headers.pop('X-Access-Token', None) # type: ignore[attr-defined] + self.headers.pop("X-Access-Token", None) # type: ignore[attr-defined] def _initialize_retry( - self, - retries_total: int, - retries_connect: int, - backoff_factor: float + self, retries_total: int, retries_connect: int, backoff_factor: float ) -> None: """Set the session retry policy. @@ -201,14 +208,11 @@ def _initialize_retry( ) retry_adapter = HTTPAdapter(max_retries=retry) - self.mount('http://', retry_adapter) - self.mount('https://', retry_adapter) + self.mount("http://", retry_adapter) + self.mount("https://", retry_adapter) def _initialize_session_parameters( - self, - verify: bool, - proxies: Dict[str, str], - auth: Optional[AuthBase] = None + self, verify: bool, proxies: Dict[str, str], auth: Optional[AuthBase] = None ) -> None: """Set the session parameters and attributes. @@ -224,18 +228,14 @@ def _initialize_session_parameters( if custom_header: client_app_header += "/" + custom_header - self.headers.update({'X-Qx-Client-Application': client_app_header}) + self.headers.update({"X-Qx-Client-Application": client_app_header}) self.auth = auth self.proxies = proxies or {} self.verify = verify def request( # type: ignore[override] - self, - method: str, - url: str, - bare: bool = False, - **kwargs: Any + self, method: str, url: str, bare: bool = False, **kwargs: Any ) -> Response: """Construct, prepare, and send a ``Request``. @@ -259,18 +259,18 @@ def request( # type: ignore[override] if bare: final_url = url # Explicitly pass `None` as the `access_token` param, disabling it. - params = kwargs.get('params', {}) - params.update({'access_token': None}) - kwargs.update({'params': params}) + params = kwargs.get("params", {}) + params.update({"access_token": None}) + kwargs.update({"params": params}) else: final_url = self.base_url + url # Add a timeout to the connection for non-proxy connections. - if not self.proxies and 'timeout' not in kwargs: - kwargs.update({'timeout': self._timeout}) + if not self.proxies and "timeout" not in kwargs: + kwargs.update({"timeout": self._timeout}) headers = self.headers.copy() - headers.update(kwargs.pop('headers', {})) + headers.update(kwargs.pop("headers", {})) try: self._log_request_info(url, method, kwargs) @@ -284,16 +284,20 @@ def request( # type: ignore[override] if ex.response is not None: status_code = ex.response.status_code try: - error_json = ex.response.json()['error'] + error_json = ex.response.json()["error"] message += ". {}, Error code: {}.".format( - error_json['message'], error_json['code']) - logger.debug("Response uber-trace-id: %s", ex.response.headers['uber-trace-id']) + error_json["message"], error_json["code"] + ) + logger.debug( + "Response uber-trace-id: %s", + ex.response.headers["uber-trace-id"], + ) except Exception: # pylint: disable=broad-except # the response did not contain the expected json. message += f". {ex.response.text}" if self.access_token: - message = message.replace(self.access_token, '...') + message = message.replace(self.access_token, "...") # Modify the original message on the chained exceptions. self._modify_chained_exception_messages(ex) @@ -317,15 +321,12 @@ def _modify_chained_exception_messages(self, exc: BaseException) -> None: for arg in exc.args: exc_message = arg if isinstance(exc_message, str): - exc_message = exc_message.replace(self.access_token, '...') + exc_message = exc_message.replace(self.access_token, "...") modified_args.append(exc_message) exc.args = tuple(modified_args) def _log_request_info( - self, - url: str, - method: str, - request_data: Dict[str, Any] + self, url: str, method: str, request_data: Dict[str, Any] ) -> None: """Log the request data, filtering out specific information. @@ -348,20 +349,28 @@ def _log_request_info( Exception: If there was an error logging the request information. """ # Replace the device name in the URL with `...` if it matches, otherwise leave it as is. - filtered_url = re.sub(RE_DEVICES_ENDPOINT, '\\1...\\3', url) + filtered_url = re.sub(RE_DEVICES_ENDPOINT, "\\1...\\3", url) if self._is_worth_logging(filtered_url): try: if logger.getEffectiveLevel() is logging.DEBUG: request_data_to_log = "" - if filtered_url in ('/devices/.../properties', '/Jobs'): + if filtered_url in ("/devices/.../properties", "/Jobs"): # Log filtered request data for these endpoints. - request_data_to_log = 'Request Data: {}.'.format(filter_data(request_data)) - logger.debug('Endpoint: %s. Method: %s. %s', - filtered_url, method.upper(), request_data_to_log) + request_data_to_log = "Request Data: {}.".format( + filter_data(request_data) + ) + logger.debug( + "Endpoint: %s. Method: %s. %s", + filtered_url, + method.upper(), + request_data_to_log, + ) except Exception as ex: # pylint: disable=broad-except # Catch general exception so as not to disturb the program if filtering fails. - logger.info('Filtering failed when logging request information: %s', str(ex)) + logger.info( + "Filtering failed when logging request information: %s", str(ex) + ) def _is_worth_logging(self, endpoint_url: str) -> bool: """Returns whether the endpoint URL should be logged. @@ -375,16 +384,23 @@ def _is_worth_logging(self, endpoint_url: str) -> bool: Returns: Whether the endpoint URL should be logged. """ - if endpoint_url.endswith(('/queue/status', '/devices/v/1', '/Jobs/status', - '/.../properties', '/.../defaults')): + if endpoint_url.endswith( + ( + "/queue/status", + "/devices/v/1", + "/Jobs/status", + "/.../properties", + "/.../defaults", + ) + ): return False - if endpoint_url.startswith(('/users', '/version')): + if endpoint_url.startswith(("/users", "/version")): return False - if endpoint_url == '/Network': + if endpoint_url == "/Network": return False - if 'objectstorage' in endpoint_url: + if "objectstorage" in endpoint_url: return False - if 'bookings' in endpoint_url: + if "bookings" in endpoint_url: return False return True diff --git a/qiskit_ibm_runtime/apiconstants.py b/qiskit_ibm_runtime/apiconstants.py index 4e4e76c009..2ba3e8ee25 100644 --- a/qiskit_ibm_runtime/apiconstants.py +++ b/qiskit_ibm_runtime/apiconstants.py @@ -14,7 +14,7 @@ import enum -QISKIT_IBM_RUNTIME_API_URL = 'https://auth.quantum-computing.ibm.com/api' +QISKIT_IBM_RUNTIME_API_URL = "https://auth.quantum-computing.ibm.com/api" class ApiJobStatus(enum.Enum): @@ -27,21 +27,21 @@ class ApiJobStatus(enum.Enum): `CREATING -> CREATED -> VALIDATING -> VALIDATED -> RUNNING -> COMPLETED` """ - CREATING = 'CREATING' - CREATED = 'CREATED' - TRANSPILING = 'TRANSPILING' - TRANSPILED = 'TRANSPILED' - VALIDATING = 'VALIDATING' - VALIDATED = 'VALIDATED' - RUNNING = 'RUNNING' - COMPLETED = 'COMPLETED' - PENDING_IN_QUEUE = 'PENDING_IN_QUEUE' - QUEUED = 'QUEUED' - CANCELLED = 'CANCELLED' - ERROR_CREATING_JOB = 'ERROR_CREATING_JOB' - ERROR_VALIDATING_JOB = 'ERROR_VALIDATING_JOB' - ERROR_RUNNING_JOB = 'ERROR_RUNNING_JOB' - ERROR_TRANSPILING_JOB = 'ERROR_TRANSPILING_JOB' + CREATING = "CREATING" + CREATED = "CREATED" + TRANSPILING = "TRANSPILING" + TRANSPILED = "TRANSPILED" + VALIDATING = "VALIDATING" + VALIDATED = "VALIDATED" + RUNNING = "RUNNING" + COMPLETED = "COMPLETED" + PENDING_IN_QUEUE = "PENDING_IN_QUEUE" + QUEUED = "QUEUED" + CANCELLED = "CANCELLED" + ERROR_CREATING_JOB = "ERROR_CREATING_JOB" + ERROR_VALIDATING_JOB = "ERROR_VALIDATING_JOB" + ERROR_RUNNING_JOB = "ERROR_RUNNING_JOB" + ERROR_TRANSPILING_JOB = "ERROR_TRANSPILING_JOB" API_JOB_FINAL_STATES = ( @@ -50,12 +50,13 @@ class ApiJobStatus(enum.Enum): ApiJobStatus.ERROR_CREATING_JOB, ApiJobStatus.ERROR_VALIDATING_JOB, ApiJobStatus.ERROR_RUNNING_JOB, - ApiJobStatus.ERROR_TRANSPILING_JOB + ApiJobStatus.ERROR_TRANSPILING_JOB, ) class ApiJobKind(enum.Enum): """Possible values used by the API for a job kind.""" - QOBJECT = 'q-object' - QOBJECT_STORAGE = 'q-object-external-storage' - CIRCUIT = 'q-circuit' + + QOBJECT = "q-object" + QOBJECT_STORAGE = "q-object-external-storage" + CIRCUIT = "q-circuit" diff --git a/qiskit_ibm_runtime/backendjoblimit.py b/qiskit_ibm_runtime/backendjoblimit.py index d3872cdcf6..2d2851b964 100644 --- a/qiskit_ibm_runtime/backendjoblimit.py +++ b/qiskit_ibm_runtime/backendjoblimit.py @@ -49,4 +49,4 @@ def __getattr__(self, name: str) -> Any: try: return self._data[name] except KeyError: - raise AttributeError('Attribute {} is not defined.'.format(name)) from None + raise AttributeError("Attribute {} is not defined.".format(name)) from None diff --git a/qiskit_ibm_runtime/backendreservation.py b/qiskit_ibm_runtime/backendreservation.py index b802f927b1..f877c20ec4 100644 --- a/qiskit_ibm_runtime/backendreservation.py +++ b/qiskit_ibm_runtime/backendreservation.py @@ -37,14 +37,14 @@ class BackendReservation: """ def __init__( - self, - backend_name: str, - start_datetime: datetime, - end_datetime: datetime, - creation_datetime: Optional[datetime] = None, - mode: Optional[str] = None, - reservation_id: Optional[str] = None, - hub_info: Optional[dict] = None + self, + backend_name: str, + start_datetime: datetime, + end_datetime: datetime, + creation_datetime: Optional[datetime] = None, + mode: Optional[str] = None, + reservation_id: Optional[str] = None, + hub_info: Optional[dict] = None, ) -> None: """BackendReservation constructor. @@ -65,31 +65,46 @@ def __init__( self.reservation_id = reservation_id self.creation_datetime = creation_datetime if hub_info: - self.hub = hub_info['hub']['name'] - self.group = hub_info['group']['name'] - self.project = hub_info['project']['name'] + self.hub = hub_info["hub"]["name"] + self.group = hub_info["group"]["name"] + self.project = hub_info["project"]["name"] else: self.hub = self.group = self.project = None def __repr__(self) -> str: out_str = "<{}(backend_name={}, start_datetime={}, end_datetime={}".format( - self.__class__.__name__, self.backend_name, self.start_datetime.isoformat(), - self.end_datetime.isoformat()) - for attr in ['mode', 'duration', 'reservation_id', 'creation_datetime', - 'hub', 'group', 'project']: + self.__class__.__name__, + self.backend_name, + self.start_datetime.isoformat(), + self.end_datetime.isoformat(), + ) + for attr in [ + "mode", + "duration", + "reservation_id", + "creation_datetime", + "hub", + "group", + "project", + ]: val = getattr(self, attr) if isinstance(val, datetime): val = val.isoformat() if val is not None: out_str += ", {}={}".format(attr, val) - out_str += ')>' + out_str += ")>" return out_str def __eq__(self, other: Any) -> bool: - if isinstance(other, BackendReservation) and self.backend_name == other.backend_name: + if ( + isinstance(other, BackendReservation) + and self.backend_name == other.backend_name + ): if self.reservation_id and self.reservation_id == other.reservation_id: return True - if self.start_datetime == other.start_datetime and \ - self.end_datetime == other.end_datetime: + if ( + self.start_datetime == other.start_datetime + and self.end_datetime == other.end_datetime + ): return True return False diff --git a/qiskit_ibm_runtime/constants.py b/qiskit_ibm_runtime/constants.py index 7f6ecf8c43..34d89185ab 100644 --- a/qiskit_ibm_runtime/constants.py +++ b/qiskit_ibm_runtime/constants.py @@ -16,16 +16,16 @@ API_TO_JOB_STATUS = { - 'QUEUED': JobStatus.QUEUED, - 'RUNNING': JobStatus.RUNNING, - 'COMPLETED': JobStatus.DONE, - 'FAILED': JobStatus.ERROR, - 'CANCELLED': JobStatus.CANCELLED, - 'CANCELLED - RAN TOO LONG': JobStatus.ERROR + "QUEUED": JobStatus.QUEUED, + "RUNNING": JobStatus.RUNNING, + "COMPLETED": JobStatus.DONE, + "FAILED": JobStatus.ERROR, + "CANCELLED": JobStatus.CANCELLED, + "CANCELLED - RAN TOO LONG": JobStatus.ERROR, } API_TO_JOB_ERROR_MESSAGE = { - 'FAILED': 'Job {} has failed:\n{}', - 'CANCELLED - RAN TOO LONG': 'Job {} ran longer than maximum execution time. ' - 'Job was cancelled:\n{}' + "FAILED": "Job {} has failed:\n{}", + "CANCELLED - RAN TOO LONG": "Job {} ran longer than maximum execution time. " + "Job was cancelled:\n{}", } diff --git a/qiskit_ibm_runtime/credentials/__init__.py b/qiskit_ibm_runtime/credentials/__init__.py index e0c5c3e1ea..bf742b7460 100644 --- a/qiskit_ibm_runtime/credentials/__init__.py +++ b/qiskit_ibm_runtime/credentials/__init__.py @@ -43,16 +43,24 @@ from .credentials import Credentials from .hub_group_project_id import HubGroupProjectID -from .exceptions import (CredentialsError, InvalidCredentialsFormatError, - CredentialsNotFoundError, HubGroupProjectIDInvalidStateError) -from .configrc import read_credentials_from_qiskitrc, store_credentials, store_preferences +from .exceptions import ( + CredentialsError, + InvalidCredentialsFormatError, + CredentialsNotFoundError, + HubGroupProjectIDInvalidStateError, +) +from .configrc import ( + read_credentials_from_qiskitrc, + store_credentials, + store_preferences, +) from .environ import read_credentials_from_environ logger = logging.getLogger(__name__) def discover_credentials( - qiskitrc_filename: Optional[str] = None + qiskitrc_filename: Optional[str] = None, ) -> Tuple[Dict[HubGroupProjectID, Credentials], Dict]: """Automatically discover credentials for IBM Quantum. @@ -81,27 +89,33 @@ def discover_credentials( # dict[str:function] that defines the different locations for looking for # credentials, and their precedence order. - readers = OrderedDict([ - ('environment variables', (read_credentials_from_environ, {})), - ('qiskitrc', (read_credentials_from_qiskitrc, - {'filename': qiskitrc_filename})) - ]) # type: OrderedDict[str, Any] + readers = OrderedDict( + [ + ("environment variables", (read_credentials_from_environ, {})), + ( + "qiskitrc", + (read_credentials_from_qiskitrc, {"filename": qiskitrc_filename}), + ), + ] + ) # type: OrderedDict[str, Any] # Attempt to read the credentials from the different sources. for display_name, (reader_function, kwargs) in readers.items(): try: stored_account_info = reader_function(**kwargs) # type: ignore[arg-type] - if display_name == 'qiskitrc': + if display_name == "qiskitrc": # Read from `qiskitrc`, which may have stored preferences. credentials_, preferences = stored_account_info else: credentials_ = stored_account_info if credentials_: - logger.info('Using credentials from %s', display_name) + logger.info("Using credentials from %s", display_name) break except CredentialsError as ex: logger.warning( - 'Automatic discovery of %s credentials failed: %s', - display_name, str(ex)) + "Automatic discovery of %s credentials failed: %s", + display_name, + str(ex), + ) return credentials_, preferences diff --git a/qiskit_ibm_runtime/credentials/configrc.py b/qiskit_ibm_runtime/credentials/configrc.py index 167ac54269..fd10ec309c 100644 --- a/qiskit_ibm_runtime/credentials/configrc.py +++ b/qiskit_ibm_runtime/credentials/configrc.py @@ -25,18 +25,15 @@ logger = logging.getLogger(__name__) -DEFAULT_QISKITRC_FILE = os.path.join(os.path.expanduser("~"), - '.qiskit', 'qiskitrc') +DEFAULT_QISKITRC_FILE = os.path.join(os.path.expanduser("~"), ".qiskit", "qiskitrc") """Default location of the configuration file.""" -_ACTIVE_PREFERENCES = { - 'experiment': {'auto_save': lambda val: val.lower() == 'true'} -} -_PREFERENCES_SECTION_NAME = 'qiskit_ibm_runtime.preferences' +_ACTIVE_PREFERENCES = {"experiment": {"auto_save": lambda val: val.lower() == "true"}} +_PREFERENCES_SECTION_NAME = "qiskit_ibm_runtime.preferences" def read_credentials_from_qiskitrc( - filename: Optional[str] = None + filename: Optional[str] = None, ) -> Tuple[Dict[HubGroupProjectID, Credentials], Dict]: """Read a configuration file and return a dictionary with its contents. @@ -65,14 +62,15 @@ def read_credentials_from_qiskitrc( config_parser.read(filename) except ParsingError as ex: raise InvalidCredentialsFormatError( - 'Error parsing file {}: {}'.format(filename, str(ex))) from ex + "Error parsing file {}: {}".format(filename, str(ex)) + ) from ex # Build the credentials dictionary. credentials_dict: Dict[HubGroupProjectID, Credentials] = {} preferences: Dict[HubGroupProjectID, Dict] = {} for name in config_parser.sections(): - if not name.startswith('qiskit_ibm_runtime'): + if not name.startswith("qiskit_ibm_runtime"): continue single_section = dict(config_parser.items(name)) @@ -86,15 +84,15 @@ def read_credentials_from_qiskitrc( # TODO: consider generalizing, moving to json configuration or a more # robust alternative. for key, val in single_section.items(): - if key == 'proxies': + if key == "proxies": configs[key] = literal_eval(val) - elif key == 'verify': - configs[key] = config_parser[name].getboolean('verify') - elif key == 'default_provider': + elif key == "verify": + configs[key] = config_parser[name].getboolean("verify") + elif key == "default_provider": configs[key] = HubGroupProjectID.from_stored_format(val) - elif key == 'url': + elif key == "url": configs[key] = val - configs['auth_url'] = val + configs["auth_url"] = val else: configs[key] = val @@ -116,7 +114,7 @@ def _parse_preferences(pref_section: Dict) -> Dict[HubGroupProjectID, Dict]: preferences: Dict[HubGroupProjectID, Dict] = defaultdict(dict) for key, val in pref_section.items(): # Preferences section format is hgp,category,item=value - elems = key.split(',') + elems = key.split(",") if len(elems) != 3: continue hgp_id, pref_cat, pref_key = elems @@ -133,9 +131,9 @@ def _parse_preferences(pref_section: Dict) -> Dict[HubGroupProjectID, Dict]: def write_qiskit_rc( - credentials: Dict[HubGroupProjectID, Credentials], - preferences: Optional[Dict] = None, - filename: Optional[str] = None + credentials: Dict[HubGroupProjectID, Credentials], + preferences: Optional[Dict] = None, + filename: Optional[str] = None, ) -> None: """Write credentials to the configuration file. @@ -146,27 +144,32 @@ def write_qiskit_rc( filename: Full path to the configuration file. If ``None``, the default location is used (``$HOME/.qiskit/qiskitrc``). """ + def _credentials_object_to_dict( - credentials_obj: Credentials, + credentials_obj: Credentials, ) -> Dict[str, Any]: """Convert a ``Credential`` object to a dictionary.""" - credentials_dict = {key: getattr(credentials_obj, key) for key in - ['token', 'url', 'proxies', 'verify'] - if getattr(credentials_obj, key)} + credentials_dict = { + key: getattr(credentials_obj, key) + for key in ["token", "url", "proxies", "verify"] + if getattr(credentials_obj, key) + } # Save the default provider to disk, if specified. if credentials_obj.default_provider: - credentials_dict['default_provider'] = \ - credentials_obj.default_provider.to_stored_format() + credentials_dict[ + "default_provider" + ] = credentials_obj.default_provider.to_stored_format() return credentials_dict def _section_name(credentials_: Credentials) -> str: """Return a string suitable for use as a unique section name.""" - base_name = 'qiskit_ibm_runtime' + base_name = "qiskit_ibm_runtime" if credentials_.is_ibm_quantum(): - base_name = '{}_{}_{}_{}'.format(base_name, - *credentials_.unique_id().to_tuple()) + base_name = "{}_{}_{}_{}".format( + base_name, *credentials_.unique_id().to_tuple() + ) return base_name filename = filename or DEFAULT_QISKITRC_FILE @@ -174,8 +177,9 @@ def _section_name(credentials_: Credentials) -> str: os.makedirs(os.path.dirname(filename), exist_ok=True) unrolled_credentials = { - _section_name(credentials_object): - _credentials_object_to_dict(credentials_object) + _section_name(credentials_object): _credentials_object_to_dict( + credentials_object + ) for _, credentials_object in credentials.items() } @@ -191,7 +195,7 @@ def _section_name(credentials_: Credentials) -> str: unrolled_credentials[_PREFERENCES_SECTION_NAME] = unrolled_pref # Write the configuration file. - with open(filename, 'w') as config_file: + with open(filename, "w") as config_file: config_parser = ConfigParser() config_parser.optionxform = str # type: ignore config_parser.read_dict(unrolled_credentials) @@ -199,9 +203,7 @@ def _section_name(credentials_: Credentials) -> str: def store_credentials( - credentials: Credentials, - overwrite: bool = False, - filename: Optional[str] = None + credentials: Credentials, overwrite: bool = False, filename: Optional[str] = None ) -> None: """Store the credentials for a single account in the configuration file. @@ -218,19 +220,22 @@ def store_credentials( # Check if duplicated credentials are already stored. By convention, # we assume (hub, group, project) is always unique. if credentials.unique_id() in stored_credentials and not overwrite: - logger.warning('Credentials already present. ' - 'Set overwrite=True to overwrite.') + logger.warning( + "Credentials already present. " "Set overwrite=True to overwrite." + ) return # Append and write the credentials to file. stored_credentials[credentials.unique_id()] = credentials - write_qiskit_rc(credentials=stored_credentials, preferences=stored_preferences, - filename=filename) + write_qiskit_rc( + credentials=stored_credentials, + preferences=stored_preferences, + filename=filename, + ) def remove_credentials( - credentials: Credentials, - filename: Optional[str] = None + credentials: Credentials, filename: Optional[str] = None ) -> None: """Remove credentials from the configuration file. @@ -249,15 +254,20 @@ def remove_credentials( try: del stored_credentials[credentials.unique_id()] except KeyError: - raise CredentialsNotFoundError('The account {} does not exist in the configuration file.' - .format(credentials.unique_id())) from None - write_qiskit_rc(credentials=stored_credentials, preferences=stored_preferences, - filename=filename) + raise CredentialsNotFoundError( + "The account {} does not exist in the configuration file.".format( + credentials.unique_id() + ) + ) from None + write_qiskit_rc( + credentials=stored_credentials, + preferences=stored_preferences, + filename=filename, + ) def store_preferences( - preferences: Dict[HubGroupProjectID, Dict], - filename: Optional[str] = None + preferences: Dict[HubGroupProjectID, Dict], filename: Optional[str] = None ) -> None: """Store the preferences in the configuration file. @@ -276,5 +286,8 @@ def store_preferences( merged_hgp.update(hgp_val) stored_preferences[hgp] = merged_hgp - write_qiskit_rc(credentials=stored_credentials, preferences=stored_preferences, - filename=filename) + write_qiskit_rc( + credentials=stored_credentials, + preferences=stored_preferences, + filename=filename, + ) diff --git a/qiskit_ibm_runtime/credentials/credentials.py b/qiskit_ibm_runtime/credentials/credentials.py index f6f6faae4e..6368257628 100644 --- a/qiskit_ibm_runtime/credentials/credentials.py +++ b/qiskit_ibm_runtime/credentials/credentials.py @@ -21,12 +21,12 @@ REGEX_IBM_HUBS = ( - '(?Phttp[s]://.+/api)' - '/Hubs/(?P[^/]+)/Groups/(?P[^/]+)/Projects/(?P[^/]+)' + "(?Phttp[s]://.+/api)" + "/Hubs/(?P[^/]+)/Groups/(?P[^/]+)/Projects/(?P[^/]+)" ) """str: Regex that matches an IBM Quantum URL with hub information.""" -TEMPLATE_IBM_HUBS = '{prefix}/Network/{hub}/Groups/{group}/Projects/{project}' +TEMPLATE_IBM_HUBS = "{prefix}/Network/{hub}/Groups/{group}/Projects/{project}" """str: Template for creating an IBM Quantum URL with hub/group/project information.""" @@ -39,20 +39,20 @@ class Credentials: """ def __init__( - self, - token: str, - url: str, - auth_url: Optional[str] = None, - websockets_url: Optional[str] = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - proxies: Optional[Dict] = None, - verify: bool = True, - services: Optional[Dict] = None, - access_token: Optional[str] = None, - preferences: Optional[Dict] = None, - default_provider: Optional[HubGroupProjectID] = None, + self, + token: str, + url: str, + auth_url: Optional[str] = None, + websockets_url: Optional[str] = None, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, + proxies: Optional[Dict] = None, + verify: bool = True, + services: Optional[Dict] = None, + access_token: Optional[str] = None, + preferences: Optional[Dict] = None, + default_provider: Optional[HubGroupProjectID] = None, ) -> None: """Credentials constructor. @@ -74,9 +74,13 @@ def __init__( """ self.token = token self.access_token = access_token - (self.url, self.base_url, - self.hub, self.group, self.project) = _unify_ibm_quantum_url( - url, hub, group, project) + ( + self.url, + self.base_url, + self.hub, + self.group, + self.project, + ) = _unify_ibm_quantum_url(url, hub, group, project) self.auth_url = auth_url or url self.websockets_url = websockets_url self.proxies = proxies or {} @@ -86,9 +90,9 @@ def __init__( # Initialize additional service URLs. services = services or {} - self.extractor_url = services.get('extractorsService', None) - self.experiment_url = services.get('resultsDB', None) - self.runtime_url = services.get('runtime', None) + self.extractor_url = services.get("extractorsService", None) + self.experiment_url = services.get("resultsDB", None) + self.runtime_url = services.get("runtime", None) def is_ibm_quantum(self) -> bool: """Return whether the credentials represent an IBM Quantum account.""" @@ -118,28 +122,25 @@ def connection_parameters(self) -> Dict[str, Any]: expected by ``requests``. The following keys can be present: ``proxies``, ``verify``, and ``auth``. """ - request_kwargs = { - 'verify': self.verify - } + request_kwargs = {"verify": self.verify} if self.proxies: - if 'urls' in self.proxies: - request_kwargs['proxies'] = self.proxies['urls'] + if "urls" in self.proxies: + request_kwargs["proxies"] = self.proxies["urls"] - if 'username_ntlm' in self.proxies and 'password_ntlm' in self.proxies: - request_kwargs['auth'] = HttpNtlmAuth( - self.proxies['username_ntlm'], - self.proxies['password_ntlm'] + if "username_ntlm" in self.proxies and "password_ntlm" in self.proxies: + request_kwargs["auth"] = HttpNtlmAuth( + self.proxies["username_ntlm"], self.proxies["password_ntlm"] ) return request_kwargs def _unify_ibm_quantum_url( - url: str, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None + url: str, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, ) -> Tuple[str, str, Optional[str], Optional[str], Optional[str]]: """Return a new-style set of credential values (url and hub parameters). @@ -169,8 +170,9 @@ def _unify_ibm_quantum_url( else: if hub and group and project: # Assume it is an IBM Quantum URL, and update the url. - url = TEMPLATE_IBM_HUBS.format(prefix=url, hub=hub, group=group, - project=project) + url = TEMPLATE_IBM_HUBS.format( + prefix=url, hub=hub, group=group, project=project + ) else: # Cleanup the hub, group and project, without modifying the url. hub = group = project = None diff --git a/qiskit_ibm_runtime/credentials/environ.py b/qiskit_ibm_runtime/credentials/environ.py index f73b4051f3..e59292665c 100644 --- a/qiskit_ibm_runtime/credentials/environ.py +++ b/qiskit_ibm_runtime/credentials/environ.py @@ -19,11 +19,11 @@ from .hub_group_project_id import HubGroupProjectID VARIABLES_MAP = { - 'QISKIT_IBM_RUNTIME_API_TOKEN': 'token', - 'QISKIT_IBM_RUNTIME_API_URL': 'url', - 'QISKIT_IBM_RUNTIME_HUB': 'hub', - 'QISKIT_IBM_RUNTIME_GROUP': 'group', - 'QISKIT_IBM_RUNTIME_PROJECT': 'project' + "QISKIT_IBM_RUNTIME_API_TOKEN": "token", + "QISKIT_IBM_RUNTIME_API_URL": "url", + "QISKIT_IBM_RUNTIME_HUB": "hub", + "QISKIT_IBM_RUNTIME_GROUP": "group", + "QISKIT_IBM_RUNTIME_PROJECT": "project", } """Dictionary that maps `ENV_VARIABLE_NAME` to credential parameter.""" @@ -36,7 +36,10 @@ def read_credentials_from_environ() -> Dict[HubGroupProjectID, Credentials]: ``{credentials_unique_id: Credentials}`` format. """ # The token is the only required parameter. - if not (os.getenv('QISKIT_IBM_RUNTIME_API_TOKEN') and os.getenv('QISKIT_IBM_RUNTIME_API_URL')): + if not ( + os.getenv("QISKIT_IBM_RUNTIME_API_TOKEN") + and os.getenv("QISKIT_IBM_RUNTIME_API_URL") + ): return {} # Build the credentials based on environment variables. @@ -48,17 +51,17 @@ def read_credentials_from_environ() -> Dict[HubGroupProjectID, Credentials]: for envar_name, credential_key in VARIABLES_MAP.items(): if os.getenv(envar_name): credentials_dict[credential_key] = os.getenv(envar_name) - if envar_name == 'QISKIT_IBM_RUNTIME_API_URL': - credentials_dict['auth_url'] = os.getenv(envar_name) - elif envar_name == 'QISKIT_IBM_RUNTIME_HUB': + if envar_name == "QISKIT_IBM_RUNTIME_API_URL": + credentials_dict["auth_url"] = os.getenv(envar_name) + elif envar_name == "QISKIT_IBM_RUNTIME_HUB": hub = os.getenv(envar_name) - elif envar_name == 'QISKIT_IBM_RUNTIME_GROUP': + elif envar_name == "QISKIT_IBM_RUNTIME_GROUP": group = os.getenv(envar_name) - elif envar_name == 'QISKIT_IBM_RUNTIME_PROJECT': + elif envar_name == "QISKIT_IBM_RUNTIME_PROJECT": project = os.getenv(envar_name) if all([hub, group, project]): - credentials_dict['default_provider'] = HubGroupProjectID(hub, group, project) + credentials_dict["default_provider"] = HubGroupProjectID(hub, group, project) credentials = Credentials(**credentials_dict) # type: ignore[arg-type] return {credentials.unique_id(): credentials} diff --git a/qiskit_ibm_runtime/credentials/exceptions.py b/qiskit_ibm_runtime/credentials/exceptions.py index d7bab40104..48241050c4 100644 --- a/qiskit_ibm_runtime/credentials/exceptions.py +++ b/qiskit_ibm_runtime/credentials/exceptions.py @@ -17,24 +17,29 @@ class CredentialsError(IBMError): """Base class for errors raised during credential management.""" + pass class InvalidCredentialsFormatError(CredentialsError): """Errors raised when the credentials are in an invalid format.""" + pass class CredentialsNotFoundError(CredentialsError): """Errors raised when the credentials are not found.""" + pass class HubGroupProjectIDError(IBMError): """Base class for errors raised by the hub_group_project_id module.""" + pass class HubGroupProjectIDInvalidStateError(HubGroupProjectIDError): """Errors raised when a HubGroupProjectID is in an invalid state for an operation.""" + pass diff --git a/qiskit_ibm_runtime/credentials/hub_group_project_id.py b/qiskit_ibm_runtime/credentials/hub_group_project_id.py index b5485718c9..545b2bea5b 100644 --- a/qiskit_ibm_runtime/credentials/hub_group_project_id.py +++ b/qiskit_ibm_runtime/credentials/hub_group_project_id.py @@ -20,19 +20,14 @@ class HubGroupProjectID: """Class for representing a hub/group/project id.""" - def __init__( - self, - hub: str = None, - group: str = None, - project: str = None - ) -> None: + def __init__(self, hub: str = None, group: str = None, project: str = None) -> None: """HubGroupProjectID constructor.""" self.hub = hub self.group = group self.project = project @classmethod - def from_stored_format(cls, hgp_id: str) -> 'HubGroupProjectID': + def from_stored_format(cls, hgp_id: str) -> "HubGroupProjectID": """Instantiates a ``HubGroupProjectID`` instance from a string. Note: @@ -47,33 +42,37 @@ def from_stored_format(cls, hgp_id: str) -> 'HubGroupProjectID': A ``HubGroupProjectID`` instance. """ try: - hub, group, project = hgp_id.split('/') + hub, group, project = hgp_id.split("/") if (not hub) or (not group) or (not project): raise HubGroupProjectIDInvalidStateError( 'The hub/group/project id "{}" is in an invalid format. ' - 'Every field must be specified: hub = "{}", group = "{}", project = "{}".' - .format(hgp_id, hub, group, project)) + 'Every field must be specified: hub = "{}", group = "{}", project = "{}".'.format( + hgp_id, hub, group, project + ) + ) except ValueError: # Not enough, or too many, values were provided. raise HubGroupProjectIDInvalidStateError( 'The hub/group/project id "{}" is in an invalid format. ' - 'Use the "//" format.' - .format(hgp_id)) + 'Use the "//" format.'.format( + hgp_id + ) + ) return cls(hub, group, project) @classmethod def from_credentials( - cls, - credentials_obj: 'Credentials' # type: ignore - ) -> 'HubGroupProjectID': + cls, credentials_obj: "Credentials" # type: ignore + ) -> "HubGroupProjectID": """Instantiates a ``HubGroupProjectID`` instance from ``Credentials``. Returns: A ``HubGroupProjectID`` instance. """ - hub, group, project = [getattr(credentials_obj, key) - for key in ['hub', 'group', 'project']] + hub, group, project = [ + getattr(credentials_obj, key) for key in ["hub", "group", "project"] + ] return cls(hub, group, project) def to_stored_format(self) -> str: @@ -93,18 +92,24 @@ def to_stored_format(self) -> str: """ if (not self.hub) or (not self.group) or (not self.project): raise HubGroupProjectIDInvalidStateError( - 'The hub/group/project id cannot be represented in the stored format. ' - 'Every field must be specified: hub = "{}", group = "{}", project = "{}".' - .format(self.hub, self.group, self.project)) - return '/'.join([self.hub, self.group, self.project]) + "The hub/group/project id cannot be represented in the stored format. " + 'Every field must be specified: hub = "{}", group = "{}", project = "{}".'.format( + self.hub, self.group, self.project + ) + ) + return "/".join([self.hub, self.group, self.project]) def to_tuple(self) -> Tuple[Optional[str], Optional[str], Optional[str]]: """Returns ``HubGroupProjectID`` as a tuple.""" return self.hub, self.group, self.project - def __eq__(self, other: 'HubGroupProjectID') -> bool: # type: ignore + def __eq__(self, other: "HubGroupProjectID") -> bool: # type: ignore """Two instances are equal if they define the same hub, group, project.""" - return (self.hub, self.group, self.project) == (other.hub, other.group, other.project) + return (self.hub, self.group, self.project) == ( + other.hub, + other.group, + other.project, + ) def __hash__(self) -> int: """Returns a hash for an instance.""" diff --git a/qiskit_ibm_runtime/exceptions.py b/qiskit_ibm_runtime/exceptions.py index 02eb2516b1..0d2ab06ecf 100644 --- a/qiskit_ibm_runtime/exceptions.py +++ b/qiskit_ibm_runtime/exceptions.py @@ -17,109 +17,131 @@ class IBMError(QiskitError): """Base class for errors raised by the provider modules.""" + pass class IBMProviderError(IBMError): """Base class for errors raise by IBMRuntimeService.""" + pass class IBMProviderValueError(IBMProviderError): """Value errors raised by IBMRuntimeService.""" + pass class IBMProviderCredentialsNotFound(IBMProviderError): """Errors raised when credentials are not found.""" + pass class IBMProviderMultipleCredentialsFound(IBMProviderError): """Errors raised when multiple credentials are found.""" + pass class IBMProviderCredentialsInvalidFormat(IBMProviderError): """Errors raised when the credentials format is invalid.""" + pass class IBMProviderCredentialsInvalidToken(IBMProviderError): """Errors raised when an IBM Quantum token is invalid.""" + pass class IBMProviderCredentialsInvalidUrl(IBMProviderError): """Errors raised when an IBM Quantum URL is invalid.""" + pass class IBMBackendError(IBMError): """Base class for errors raised by the backend modules.""" + pass class IBMBackendApiError(IBMBackendError): """Errors that occur unexpectedly when querying the server.""" + pass class IBMBackendApiProtocolError(IBMBackendApiError): """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 IBMBackendJobLimitError(IBMBackendError): """Errors raised when job limit is reached.""" + pass class IBMInputValueError(IBMError): """Error raised due to invalid input value.""" + pass class IBMNotAuthorizedError(IBMError): """Error raised when a service is invoked from an unauthorized account.""" + pass class IBMApiError(IBMError): """Error raised when a server error encountered.""" + pass class QiskitRuntimeError(IBMError): """Base class for errors raised by the runtime service modules.""" + pass class RuntimeDuplicateProgramError(QiskitRuntimeError): """Error raised when a program being uploaded already exists.""" + pass class RuntimeProgramNotFound(QiskitRuntimeError): """Error raised when a program is not found.""" + pass class RuntimeJobFailureError(QiskitRuntimeError): """Error raised when a runtime job failed.""" + pass class RuntimeJobNotFound(QiskitRuntimeError): """Error raised when a job is not found.""" + pass class RuntimeInvalidStateError(QiskitRuntimeError): """Errors raised when the state is not valid for the operation.""" + pass diff --git a/qiskit_ibm_runtime/hub_group_project.py b/qiskit_ibm_runtime/hub_group_project.py index eca3fbfc2b..10365ec04a 100644 --- a/qiskit_ibm_runtime/hub_group_project.py +++ b/qiskit_ibm_runtime/hub_group_project.py @@ -19,7 +19,10 @@ from qiskit.providers.backend import BackendV1 as Backend from qiskit.providers.models import PulseBackendConfiguration, QasmBackendConfiguration -from qiskit_ibm_runtime import ibm_runtime_service, ibm_backend # pylint: disable=unused-import +from qiskit_ibm_runtime import ( # pylint: disable=unused-import + ibm_runtime_service, + ibm_backend, +) from .utils.json_decoder import decode_backend_configuration from .api.clients import AccountClient @@ -33,10 +36,10 @@ class HubGroupProject: """Represents a hub/group/project with IBM Quantum backends and services associated with it.""" def __init__( - self, - credentials: Credentials, - service: 'ibm_runtime_service.IBMRuntimeService', - is_open: bool + self, + credentials: Credentials, + service: "ibm_runtime_service.IBMRuntimeService", + is_open: bool, ) -> None: """HubGroupProject constructor @@ -48,19 +51,20 @@ def __init__( self.credentials = credentials self._service = service self.is_open = is_open - self._api_client = AccountClient(self.credentials, - **self.credentials.connection_parameters()) + self._api_client = AccountClient( + self.credentials, **self.credentials.connection_parameters() + ) # Initialize the internal list of backends. - self._backends: Dict[str, 'ibm_backend.IBMBackend'] = {} + self._backends: Dict[str, "ibm_backend.IBMBackend"] = {} self._service_urls = { - 'backend': self.credentials.url, - 'experiment': self.credentials.experiment_url, - 'random': self.credentials.extractor_url, - 'runtime': self.credentials.runtime_url + "backend": self.credentials.url, + "experiment": self.credentials.experiment_url, + "random": self.credentials.extractor_url, + "runtime": self.credentials.runtime_url, } @property - def backends(self) -> Dict[str, 'ibm_backend.IBMBackend']: + def backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: """Gets the backends for the hub/group/project, if not loaded. Returns: @@ -71,7 +75,7 @@ def backends(self) -> Dict[str, 'ibm_backend.IBMBackend']: return self._backends @backends.setter - def backends(self, value: Dict[str, 'ibm_backend.IBMBackend']) -> None: + def backends(self, value: Dict[str, "ibm_backend.IBMBackend"]) -> None: """Sets the value for the hub/group/project's backends. Args: @@ -80,9 +84,8 @@ def backends(self, value: Dict[str, 'ibm_backend.IBMBackend']) -> None: self._backends = value def _discover_remote_backends( - self, - timeout: Optional[float] = None - ) -> Dict[str, 'ibm_backend.IBMBackend']: + self, timeout: Optional[float] = None + ) -> Dict[str, "ibm_backend.IBMBackend"]: """Return the remote backends available for this hub/group/project. Args: @@ -97,8 +100,10 @@ def _discover_remote_backends( for raw_config in configs_list: # Make sure the raw_config is of proper type if not isinstance(raw_config, dict): - logger.warning("An error occurred when retrieving backend " - "information. Some backends might not be available.") + logger.warning( + "An error occurred when retrieving backend " + "information. Some backends might not be available." + ) continue try: decode_backend_configuration(raw_config) @@ -106,19 +111,25 @@ def _discover_remote_backends( config = PulseBackendConfiguration.from_dict(raw_config) except (KeyError, TypeError): config = QasmBackendConfiguration.from_dict(raw_config) - backend_cls = ibm_backend.IBMSimulator if config.simulator \ + backend_cls = ( + ibm_backend.IBMSimulator + if config.simulator else ibm_backend.IBMBackend + ) ret[config.backend_name] = backend_cls( configuration=config, service=self._service, credentials=self.credentials, - api_client=self._api_client) + api_client=self._api_client, + ) except Exception: # pylint: disable=broad-except logger.warning( 'Remote backend "%s" for provider %s could not be instantiated due to an ' - 'invalid config: %s', - raw_config.get('backend_name', raw_config.get('name', 'unknown')), - repr(self), traceback.format_exc()) + "invalid config: %s", + raw_config.get("backend_name", raw_config.get("name", "unknown")), + repr(self), + traceback.format_exc(), + ) return ret def get_backend(self, name: str) -> Optional[Backend]: @@ -133,13 +144,11 @@ def has_service(self, name: str) -> bool: def __repr__(self) -> str: credentials_info = "hub='{}', group='{}', project='{}'".format( - self.credentials.hub, self.credentials.group, self.credentials.project) + self.credentials.hub, self.credentials.group, self.credentials.project + ) return "<{}({})>".format(self.__class__.__name__, credentials_info) - def __eq__( - self, - other: Any - ) -> bool: + def __eq__(self, other: Any) -> bool: if not isinstance(other, HubGroupProject): return False return self.credentials == other.credentials diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py index f028ddfac8..134db45599 100644 --- a/qiskit_ibm_runtime/ibm_backend.py +++ b/qiskit_ibm_runtime/ibm_backend.py @@ -24,10 +24,13 @@ from qiskit.qobj.utils import MeasLevel, MeasReturnType from qiskit.providers.backend import BackendV1 as Backend from qiskit.providers.options import Options -from qiskit.providers.models import (BackendStatus, BackendProperties, - PulseDefaults, GateConfig) -from qiskit.providers.models import (QasmBackendConfiguration, - PulseBackendConfiguration) +from qiskit.providers.models import ( + BackendStatus, + BackendProperties, + PulseDefaults, + GateConfig, +) +from qiskit.providers.models import QasmBackendConfiguration, PulseBackendConfiguration # pylint: disable=unused-import, cyclic-import from qiskit_ibm_runtime import ibm_runtime_service @@ -73,11 +76,11 @@ class IBMBackend(Backend): id_warning_issued = False def __init__( - self, - configuration: Union[QasmBackendConfiguration, PulseBackendConfiguration], - service: 'ibm_runtime_service.IBMRuntimeService', - credentials: Credentials, - api_client: AccountClient + self, + configuration: Union[QasmBackendConfiguration, PulseBackendConfiguration], + service: "ibm_runtime_service.IBMRuntimeService", + credentials: Credentials, + api_client: AccountClient, ) -> None: """IBMBackend constructor. @@ -102,19 +105,24 @@ def __init__( @classmethod def _default_options(cls) -> Options: """Default runtime options.""" - return Options(shots=4000, memory=False, - qubit_lo_freq=None, meas_lo_freq=None, - schedule_los=None, - meas_level=MeasLevel.CLASSIFIED, - meas_return=MeasReturnType.AVERAGE, - memory_slots=None, memory_slot_size=100, - rep_time=None, rep_delay=None, - init_qubits=True, use_measure_esp=None) + return Options( + shots=4000, + memory=False, + qubit_lo_freq=None, + meas_lo_freq=None, + schedule_los=None, + meas_level=MeasLevel.CLASSIFIED, + meas_return=MeasReturnType.AVERAGE, + memory_slots=None, + memory_slot_size=100, + rep_time=None, + rep_delay=None, + init_qubits=True, + use_measure_esp=None, + ) def properties( - self, - refresh: bool = False, - datetime: Optional[python_datetime] = None + self, refresh: bool = False, datetime: Optional[python_datetime] = None ) -> Optional[BackendProperties]: """Return the backend properties, subject to optional filtering. @@ -142,8 +150,10 @@ def properties( """ # pylint: disable=arguments-differ if not isinstance(refresh, bool): - raise TypeError("The 'refresh' argument needs to be a boolean. " - "{} is of type {}".format(refresh, type(refresh))) + raise TypeError( + "The 'refresh' argument needs to be a boolean. " + "{} is of type {}".format(refresh, type(refresh)) + ) if datetime and not isinstance(datetime, python_datetime): raise TypeError("'{}' is not of type 'datetime'.") @@ -151,13 +161,15 @@ def properties( datetime = local_to_utc(datetime) if datetime or refresh or self._properties is None: - api_properties = self._api_client.backend_properties(self.name(), datetime=datetime) + api_properties = self._api_client.backend_properties( + self.name(), datetime=datetime + ) if not api_properties: return None decode_backend_properties(api_properties) api_properties = utc_to_local_all(api_properties) backend_properties = BackendProperties.from_dict(api_properties) - if datetime: # Don't cache result. + if datetime: # Don't cache result. return backend_properties self._properties = backend_properties return self._properties @@ -182,8 +194,9 @@ def status(self) -> BackendStatus: return BackendStatus.from_dict(api_status) except TypeError as ex: raise IBMBackendApiProtocolError( - 'Unexpected return value received from the server when ' - 'getting backend status: {}'.format(str(ex))) from ex + "Unexpected return value received from the server when " + "getting backend status: {}".format(str(ex)) + ) from ex def defaults(self, refresh: bool = False) -> Optional[PulseDefaults]: """Return the pulse defaults for the backend. @@ -250,8 +263,9 @@ def job_limit(self) -> BackendJobLimit: return job_limit except TypeError as ex: raise IBMBackendApiProtocolError( - 'Unexpected return value received from the server when ' - 'querying job limit data for the backend: {}.'.format(ex)) from ex + "Unexpected return value received from the server when " + "querying job limit data for the backend: {}.".format(ex) + ) from ex def remaining_jobs_count(self) -> Optional[int]: """Return the number of remaining jobs that could be submitted to the backend. @@ -281,9 +295,9 @@ def remaining_jobs_count(self) -> Optional[int]: return job_limit.maximum_jobs - job_limit.active_jobs def reservations( - self, - start_datetime: Optional[python_datetime] = None, - end_datetime: Optional[python_datetime] = None + self, + start_datetime: Optional[python_datetime] = None, + end_datetime: Optional[python_datetime] = None, ) -> List[BackendReservation]: """Return backend reservations. @@ -303,10 +317,13 @@ def reservations( start_datetime = local_to_utc(start_datetime) if start_datetime else None end_datetime = local_to_utc(end_datetime) if end_datetime else None raw_response = self._api_client.backend_reservations( - self.name(), start_datetime, end_datetime) + self.name(), start_datetime, end_datetime + ) return convert_reservation_data(raw_response, self.name()) - def configuration(self) -> Union[QasmBackendConfiguration, PulseBackendConfiguration]: + def configuration( + self, + ) -> Union[QasmBackendConfiguration, PulseBackendConfiguration]: """Return the backend configuration. Backend configuration contains fixed information about the backend, such @@ -325,9 +342,10 @@ def __repr__(self) -> str: return "<{}('{}')>".format(self.__class__.__name__, self.name()) def _deprecate_id_instruction( - self, - circuits: Union[QuantumCircuit, Schedule, - List[Union[QuantumCircuit, Schedule]]] + self, + circuits: Union[ + QuantumCircuit, Schedule, List[Union[QuantumCircuit, Schedule]] + ], ) -> None: """Raise a DeprecationWarning if any circuit contains an 'id' instruction. @@ -343,8 +361,10 @@ def _deprecate_id_instruction( None """ - id_support = 'id' in getattr(self.configuration(), 'basis_gates', []) - delay_support = 'delay' in getattr(self.configuration(), 'supported_instructions', []) + id_support = "id" in getattr(self.configuration(), "basis_gates", []) + delay_support = "delay" in getattr( + self.configuration(), "supported_instructions", [] + ) if not delay_support: return @@ -352,27 +372,35 @@ def _deprecate_id_instruction( if not isinstance(circuits, List): circuits = [circuits] - circuit_has_id = any(instr.name == 'id' - for circuit in circuits - if isinstance(circuit, QuantumCircuit) - for instr, qargs, cargs in circuit.data) + circuit_has_id = any( + instr.name == "id" + for circuit in circuits + if isinstance(circuit, QuantumCircuit) + for instr, qargs, cargs in circuit.data + ) if not circuit_has_id: return if not self.id_warning_issued: if id_support and delay_support: - warnings.warn("Support for the 'id' instruction has been deprecated " - "from IBM hardware backends. Any 'id' instructions " - "will be replaced with their equivalent 'delay' instruction. " - "Please use the 'delay' instruction instead.", DeprecationWarning, - stacklevel=4) + warnings.warn( + "Support for the 'id' instruction has been deprecated " + "from IBM hardware backends. Any 'id' instructions " + "will be replaced with their equivalent 'delay' instruction. " + "Please use the 'delay' instruction instead.", + DeprecationWarning, + stacklevel=4, + ) else: - warnings.warn("Support for the 'id' instruction has been removed " - "from IBM hardware backends. Any 'id' instructions " - "will be replaced with their equivalent 'delay' instruction. " - "Please use the 'delay' instruction instead.", DeprecationWarning, - stacklevel=4) + warnings.warn( + "Support for the 'id' instruction has been removed " + "from IBM hardware backends. Any 'id' instructions " + "will be replaced with their equivalent 'delay' instruction. " + "Please use the 'delay' instruction instead.", + DeprecationWarning, + stacklevel=4, + ) self.id_warning_issued = True @@ -383,9 +411,9 @@ def _deprecate_id_instruction( continue for idx, (instr, qargs, cargs) in enumerate(circuit.data): - if instr.name == 'id': + if instr.name == "id": - sx_duration = self.properties().gate_length('sx', qargs[0].index) + sx_duration = self.properties().gate_length("sx", qargs[0].index) sx_duration_in_dt = duration_in_dt(sx_duration, dt_in_s) delay_instr = Delay(sx_duration_in_dt) @@ -409,9 +437,7 @@ def _default_options(cls) -> Options: return options def properties( - self, - refresh: bool = False, - datetime: Optional[python_datetime] = None + self, refresh: bool = False, datetime: Optional[python_datetime] = None ) -> None: """Return ``None``, simulators do not have backend properties.""" return None @@ -426,11 +452,11 @@ class IBMRetiredBackend(IBMBackend): """Backend class interfacing with an IBM Quantum device no longer available.""" def __init__( - self, - configuration: Union[QasmBackendConfiguration, PulseBackendConfiguration], - service: 'ibm_runtime_service.IBMRuntimeService', - credentials: Credentials, - api_client: AccountClient + self, + configuration: Union[QasmBackendConfiguration, PulseBackendConfiguration], + service: "ibm_runtime_service.IBMRuntimeService", + credentials: Credentials, + api_client: AccountClient, ) -> None: """IBMRetiredBackend constructor. @@ -446,7 +472,8 @@ def __init__( backend_version=self.configuration().backend_version, operational=False, pending_jobs=0, - status_msg='This backend is no longer available.') + status_msg="This backend is no longer available.", + ) @classmethod def _default_options(cls) -> Options: @@ -454,9 +481,7 @@ def _default_options(cls) -> Options: return Options() def properties( - self, - refresh: bool = False, - datetime: Optional[python_datetime] = None + self, refresh: bool = False, datetime: Optional[python_datetime] = None ) -> None: """Return the backend properties.""" return None @@ -478,33 +503,31 @@ def remaining_jobs_count(self) -> None: return None def reservations( - self, - start_datetime: Optional[python_datetime] = None, - end_datetime: Optional[python_datetime] = None + self, + start_datetime: Optional[python_datetime] = None, + end_datetime: Optional[python_datetime] = None, ) -> List[BackendReservation]: return [] - def run( # type: ignore[override] - self, - *args: Any, - **kwargs: Any - ) -> None: + def run(self, *args: Any, **kwargs: Any) -> None: # type: ignore[override] """Run a Circuit.""" # pylint: disable=arguments-differ - raise IBMBackendError('This backend ({}) is no longer available.'.format(self.name())) + raise IBMBackendError( + "This backend ({}) is no longer available.".format(self.name()) + ) @classmethod def from_name( - cls, - backend_name: str, - service: 'ibm_runtime_service.IBMRuntimeService', - credentials: Credentials, - api: AccountClient - ) -> 'IBMRetiredBackend': + cls, + backend_name: str, + service: "ibm_runtime_service.IBMRuntimeService", + credentials: Credentials, + api: AccountClient, + ) -> "IBMRetiredBackend": """Return a retired backend from its name.""" configuration = QasmBackendConfiguration( backend_name=backend_name, - backend_version='0.0.0', + backend_version="0.0.0", n_qubits=1, basis_gates=[], simulator=False, @@ -513,7 +536,7 @@ def from_name( open_pulse=False, memory=False, max_shots=1, - gates=[GateConfig(name='TODO', parameters=[], qasm_def='TODO')], + gates=[GateConfig(name="TODO", parameters=[], qasm_def="TODO")], coupling_map=[[0, 1]], ) return cls(configuration, service, credentials, api) diff --git a/qiskit_ibm_runtime/ibm_runtime_service.py b/qiskit_ibm_runtime/ibm_runtime_service.py index 01dae8135d..c0eebd4cab 100644 --- a/qiskit_ibm_runtime/ibm_runtime_service.py +++ b/qiskit_ibm_runtime/ibm_runtime_service.py @@ -34,11 +34,18 @@ from .runtime_program import RuntimeProgram, ParameterNamespace from .utils import RuntimeDecoder, to_base64_string, to_python_identifier from .utils.backend import convert_reservation_data -from .exceptions import (QiskitRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, - RuntimeJobNotFound, IBMProviderCredentialsInvalidToken, - IBMProviderCredentialsInvalidFormat, IBMProviderCredentialsNotFound, - IBMProviderMultipleCredentialsFound, IBMProviderCredentialsInvalidUrl, - IBMProviderValueError) +from .exceptions import ( + QiskitRuntimeError, + RuntimeDuplicateProgramError, + RuntimeProgramNotFound, + RuntimeJobNotFound, + IBMProviderCredentialsInvalidToken, + IBMProviderCredentialsInvalidFormat, + IBMProviderCredentialsNotFound, + IBMProviderMultipleCredentialsFound, + IBMProviderCredentialsInvalidUrl, + IBMProviderValueError, +) from .program.result_decoder import ResultDecoder from .api.clients import AuthClient, VersionClient from .api.clients.runtime import RuntimeClient @@ -48,14 +55,17 @@ from .hub_group_project import HubGroupProject # pylint: disable=cyclic-import from .exceptions import IBMNotAuthorizedError, IBMInputValueError, IBMProviderError from .credentials import Credentials, HubGroupProjectID, discover_credentials -from .credentials.configrc import (remove_credentials, read_credentials_from_qiskitrc, - store_credentials) +from .credentials.configrc import ( + remove_credentials, + read_credentials_from_qiskitrc, + store_credentials, +) from .credentials.exceptions import HubGroupProjectIDInvalidStateError from .runner_result import RunnerResult logger = logging.getLogger(__name__) -SERVICE_NAME = 'runtime' +SERVICE_NAME = "runtime" class IBMRuntimeService: @@ -117,10 +127,7 @@ class IBMRuntimeService: """ def __init__( - self, - token: Optional[str] = None, - url: Optional[str] = None, - **kwargs: Any + self, token: Optional[str] = None, url: Optional[str] = None, **kwargs: Any ) -> None: """IBMRuntimeService constructor @@ -146,31 +153,30 @@ def __init__( # pylint: disable=unused-argument,unsubscriptable-object super().__init__() account_credentials, account_preferences = self._resolve_credentials( - token=token, - url=url, - **kwargs + token=token, url=url, **kwargs ) - self._initialize_hgps(credentials=account_credentials, preferences=account_preferences) - self._backends: Dict[str, 'ibm_backend.IBMBackend'] = {} + self._initialize_hgps( + credentials=account_credentials, preferences=account_preferences + ) + self._backends: Dict[str, "ibm_backend.IBMBackend"] = {} self._api_client = None hgps = self._get_hgps() for hgp in hgps: for name, backend in hgp.backends.items(): if name not in self._backends: self._backends[name] = backend - if not self._api_client and hgp.has_service('runtime'): + if not self._api_client and hgp.has_service("runtime"): self._default_hgp = hgp self._api_client = RuntimeClient(self._default_hgp.credentials) self._access_token = self._default_hgp.credentials.access_token - self._ws_url = self._default_hgp.credentials.runtime_url.replace('https', 'wss') + self._ws_url = self._default_hgp.credentials.runtime_url.replace( + "https", "wss" + ) self._programs: Dict[str, RuntimeProgram] = {} self._discover_backends() def _resolve_credentials( - self, - token: Optional[str] = None, - url: Optional[str] = None, - **kwargs: Any + self, token: Optional[str] = None, url: Optional[str] = None, **kwargs: Any ) -> Tuple[Credentials, Dict]: """Resolve credentials after looking up env variables and credentials saved on disk @@ -195,10 +201,17 @@ def _resolve_credentials( if token: if not isinstance(token, str): raise IBMProviderCredentialsInvalidToken( - 'Invalid IBM Quantum token ' - 'found: "{}" of type {}.'.format(token, type(token))) - url = url or os.getenv('QISKIT_IBM_RUNTIME_API_URL') or QISKIT_IBM_RUNTIME_API_URL - account_credentials = Credentials(token=token, url=url, auth_url=url, **kwargs) + "Invalid IBM Quantum token " + 'found: "{}" of type {}.'.format(token, type(token)) + ) + url = ( + url + or os.getenv("QISKIT_IBM_RUNTIME_API_URL") + or QISKIT_IBM_RUNTIME_API_URL + ) + account_credentials = Credentials( + token=token, url=url, auth_url=url, **kwargs + ) preferences: Optional[Dict] = {} else: # Check for valid credentials in env variables or qiskitrc file. @@ -206,22 +219,22 @@ def _resolve_credentials( saved_credentials, preferences = discover_credentials() except HubGroupProjectIDInvalidStateError as ex: raise IBMProviderCredentialsInvalidFormat( - 'Invalid hub/group/project data found {}' - .format(str(ex))) from ex + "Invalid hub/group/project data found {}".format(str(ex)) + ) from ex credentials_list = list(saved_credentials.values()) if not credentials_list: raise IBMProviderCredentialsNotFound( - 'No IBM Quantum credentials found.') + "No IBM Quantum credentials found." + ) if len(credentials_list) > 1: raise IBMProviderMultipleCredentialsFound( - 'Multiple IBM Quantum Experience credentials found.') + "Multiple IBM Quantum Experience credentials found." + ) account_credentials = credentials_list[0] return account_credentials, preferences def _initialize_hgps( - self, - credentials: Credentials, - preferences: Optional[Dict] = None + self, credentials: Credentials, preferences: Optional[Dict] = None ) -> None: """Authenticate against IBM Quantum and populate the hub/group/projects. @@ -237,14 +250,18 @@ def _initialize_hgps( self._hgps: Dict[HubGroupProjectID, HubGroupProject] = OrderedDict() version_info = self._check_api_version(credentials) # Check the URL is a valid authentication URL. - if not version_info['new_api'] or 'api-auth' not in version_info: + if not version_info["new_api"] or "api-auth" not in version_info: raise IBMProviderCredentialsInvalidUrl( - 'The URL specified ({}) is not an IBM Quantum authentication URL. ' - 'Valid authentication URL: {}.' - .format(credentials.url, QISKIT_IBM_RUNTIME_API_URL)) - auth_client = AuthClient(credentials.token, - credentials.base_url, - **credentials.connection_parameters()) + "The URL specified ({}) is not an IBM Quantum authentication URL. " + "Valid authentication URL: {}.".format( + credentials.url, QISKIT_IBM_RUNTIME_API_URL + ) + ) + auth_client = AuthClient( + credentials.token, + credentials.base_url, + **credentials.connection_parameters(), + ) service_urls = auth_client.current_service_urls() user_hubs = auth_client.user_hubs() preferences = preferences or {} @@ -254,28 +271,36 @@ def _initialize_hgps( hgp_credentials = Credentials( credentials.token, access_token=auth_client.current_access_token(), - url=service_urls['http'], + url=service_urls["http"], auth_url=credentials.auth_url, - websockets_url=service_urls['ws'], + websockets_url=service_urls["ws"], proxies=credentials.proxies, verify=credentials.verify, - services=service_urls.get('services', {}), + services=service_urls.get("services", {}), default_provider=credentials.default_provider, - **hub_info + **hub_info, + ) + hgp_credentials.preferences = preferences.get( + hgp_credentials.unique_id(), {} ) - hgp_credentials.preferences = \ - preferences.get(hgp_credentials.unique_id(), {}) # Build the hgp. try: - hgp = HubGroupProject(credentials=hgp_credentials, service=self, is_open=is_open) + hgp = HubGroupProject( + credentials=hgp_credentials, service=self, is_open=is_open + ) self._hgps[hgp.credentials.unique_id()] = hgp is_open = False # hgps after first are premium and not open access except Exception: # pylint: disable=broad-except # Catch-all for errors instantiating the hgp. - logger.warning('Unable to instantiate hub/group/project for %s: %s', - hub_info, traceback.format_exc()) + logger.warning( + "Unable to instantiate hub/group/project for %s: %s", + hub_info, + traceback.format_exc(), + ) if not self._hgps: - raise IBMProviderError('No hub/group/project could be found for this account.') + raise IBMProviderError( + "No hub/group/project could be found for this account." + ) # Move open hgp to end of the list if len(self._hgps) > 1: open_hgp = self._get_hgp() @@ -296,17 +321,18 @@ def _check_api_version(credentials: Credentials) -> Dict[str, Union[bool, str]]: Returns: A dictionary with version information. """ - version_finder = VersionClient(credentials.base_url, - **credentials.connection_parameters()) + version_finder = VersionClient( + credentials.base_url, **credentials.connection_parameters() + ) return version_finder.version() def _get_hgp( - self, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - backend_name: Optional[str] = None, - service_name: Optional[str] = None + self, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, + backend_name: Optional[str] = None, + service_name: Optional[str] = None, ) -> HubGroupProject: """Return an instance of `HubGroupProject` for a single hub/group/project combination. @@ -331,36 +357,42 @@ def _get_hgp( """ # If any `hub`, `group`, or `project` is specified, make sure all parameters are set. if any([hub, group, project]) and not all([hub, group, project]): - raise IBMProviderError('The hub, group, and project parameters must all be ' - 'specified. ' - 'hub = "{}", group = "{}", project = "{}"' - .format(hub, group, project)) + raise IBMProviderError( + "The hub, group, and project parameters must all be " + "specified. " + 'hub = "{}", group = "{}", project = "{}"'.format(hub, group, project) + ) hgps = self._get_hgps(hub=hub, group=group, project=project) if any([hub, group, project]): if not hgps: - raise IBMProviderError('No hub/group/project matches the specified criteria: ' - 'hub = {}, group = {}, project = {}' - .format(hub, group, project)) + raise IBMProviderError( + "No hub/group/project matches the specified criteria: " + "hub = {}, group = {}, project = {}".format(hub, group, project) + ) if len(hgps) > 1: - raise IBMProviderError('More than one hub/group/project matches the ' - 'specified criteria. hub = {}, group = {}, project = {}' - .format(hub, group, project)) + raise IBMProviderError( + "More than one hub/group/project matches the " + "specified criteria. hub = {}, group = {}, project = {}".format( + hub, group, project + ) + ) elif not hgps: # Prevent edge case where no hub/group/project is available. - raise IBMProviderError('No hub/group/project could be found for this account.') + raise IBMProviderError( + "No hub/group/project could be found for this account." + ) elif backend_name and service_name: for hgp in hgps: - if hgp.has_service(service_name) and \ - hgp.get_backend(backend_name): + if hgp.has_service(service_name) and hgp.get_backend(backend_name): return hgp raise IBMProviderError("No backend matches the criteria.") return hgps[0] def _get_hgps( - self, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, + self, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, ) -> List[HubGroupProject]: """Return a list of `HubGroupProject` instances, subject to optional filtering. @@ -379,8 +411,7 @@ def _get_hgps( filters.append(lambda hgp: hgp.group == group) if project: filters.append(lambda hgp: hgp.project == project) - hgps = [hgp for key, hgp in self._hgps.items() - if all(f(key) for f in filters)] + hgps = [hgp for key, hgp in self._hgps.items() if all(f(key) for f in filters)] return hgps def _discover_backends(self) -> None: @@ -389,20 +420,20 @@ def _discover_backends(self) -> None: backend_name = to_python_identifier(backend.name()) # Append _ if duplicate while backend_name in self.__dict__: - backend_name += '_' + backend_name += "_" setattr(self, backend_name, backend) def backends( - self, - name: Optional[str] = None, - filters: Optional[Callable[[List['ibm_backend.IBMBackend']], bool]] = None, - min_num_qubits: Optional[int] = None, - input_allowed: Optional[Union[str, List[str]]] = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - **kwargs: Any - ) -> List['ibm_backend.IBMBackend']: + self, + name: Optional[str] = None, + filters: Optional[Callable[[List["ibm_backend.IBMBackend"]], bool]] = None, + min_num_qubits: Optional[int] = None, + input_allowed: Optional[Union[str, List[str]]] = None, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, + **kwargs: Any, + ) -> List["ibm_backend.IBMBackend"]: """Return all backends accessible via this account, subject to optional filtering. Args: @@ -434,7 +465,7 @@ def backends( IBMBackendValueError: If only one or two parameters from `hub`, `group`, `project` are specified. """ - backends: List['ibm_backend.IBMBackend'] = list() + backends: List["ibm_backend.IBMBackend"] = list() if all([hub, group, project]): hgp = self._get_hgp(hub, group, project) backends = list(hgp.backends.values()) @@ -445,15 +476,21 @@ def backends( aliases = self._aliased_backend_names() aliases.update(self._deprecated_backend_names()) name = aliases.get(name, name) - kwargs['backend_name'] = name + kwargs["backend_name"] = name if min_num_qubits: - backends = list(filter( - lambda b: b.configuration().n_qubits >= min_num_qubits, backends)) + backends = list( + filter(lambda b: b.configuration().n_qubits >= min_num_qubits, backends) + ) if input_allowed: if not isinstance(input_allowed, list): input_allowed = [input_allowed] - backends = list(filter( - lambda b: set(input_allowed) <= set(b.configuration().input_allowed), backends)) + backends = list( + filter( + lambda b: set(input_allowed) + <= set(b.configuration().input_allowed), + backends, + ) + ) return filter_backends(backends, filters=filters, **kwargs) def my_reservations(self) -> List[BackendReservation]: @@ -469,20 +506,20 @@ def my_reservations(self) -> List[BackendReservation]: def _deprecated_backend_names() -> Dict[str, str]: """Returns deprecated backend names.""" return { - 'ibmqx_qasm_simulator': 'ibmq_qasm_simulator', - 'ibmqx_hpc_qasm_simulator': 'ibmq_qasm_simulator', - 'real': 'ibmqx1' - } + "ibmqx_qasm_simulator": "ibmq_qasm_simulator", + "ibmqx_hpc_qasm_simulator": "ibmq_qasm_simulator", + "real": "ibmqx1", + } @staticmethod def _aliased_backend_names() -> Dict[str, str]: """Returns aliased backend names.""" return { - 'ibmq_5_yorktown': 'ibmqx2', - 'ibmq_5_tenerife': 'ibmqx4', - 'ibmq_16_rueschlikon': 'ibmqx5', - 'ibmq_20_austin': 'QS1_1' - } + "ibmq_5_yorktown": "ibmqx2", + "ibmq_5_tenerife": "ibmqx4", + "ibmq_16_rueschlikon": "ibmqx5", + "ibmq_20_austin": "QS1_1", + } def active_account(self) -> Optional[Dict[str, str]]: """Return the IBM Quantum account currently in use for the session. @@ -495,8 +532,8 @@ def active_account(self) -> Optional[Dict[str, str]]: return None first_hgp = self._get_hgp() return { - 'token': first_hgp.credentials.token, - 'url': first_hgp.credentials.auth_url + "token": first_hgp.credentials.token, + "url": first_hgp.credentials.auth_url, } @staticmethod @@ -512,22 +549,24 @@ def delete_account() -> None: stored_credentials, _ = read_credentials_from_qiskitrc() if not stored_credentials: raise IBMProviderCredentialsNotFound( - 'No IBM Quantum credentials found on disk.') + "No IBM Quantum credentials found on disk." + ) credentials = list(stored_credentials.values())[0] if credentials.url != QISKIT_IBM_RUNTIME_API_URL: raise IBMProviderCredentialsInvalidUrl( - 'Invalid IBM Quantum credentials found on disk. ') + "Invalid IBM Quantum credentials found on disk. " + ) remove_credentials(credentials) @staticmethod def save_account( - token: str, - url: str = QISKIT_IBM_RUNTIME_API_URL, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - overwrite: bool = False, - **kwargs: Any + token: str, + url: str = QISKIT_IBM_RUNTIME_API_URL, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, + overwrite: bool = False, + **kwargs: Any, ) -> None: """Save the account to disk for future use. @@ -556,21 +595,31 @@ def save_account( """ if url != QISKIT_IBM_RUNTIME_API_URL: raise IBMProviderCredentialsInvalidUrl( - 'Invalid IBM Quantum credentials found.') + "Invalid IBM Quantum credentials found." + ) if not token or not isinstance(token, str): raise IBMProviderCredentialsInvalidToken( - 'Invalid IBM Quantum token ' - 'found: "{}" of type {}.'.format(token, type(token))) + "Invalid IBM Quantum token " + 'found: "{}" of type {}.'.format(token, type(token)) + ) # If any `hub`, `group`, or `project` is specified, make sure all parameters are set. if any([hub, group, project]) and not all([hub, group, project]): - raise IBMProviderValueError('The hub, group, and project parameters must all be ' - 'specified when storing a default hub/group/project to ' - 'disk: hub = "{}", group = "{}", project = "{}"' - .format(hub, group, project)) + raise IBMProviderValueError( + "The hub, group, and project parameters must all be " + "specified when storing a default hub/group/project to " + 'disk: hub = "{}", group = "{}", project = "{}"'.format( + hub, group, project + ) + ) # If specified, get the hub/group/project to store. - default_hgp_id = HubGroupProjectID(hub, group, project) \ - if all([hub, group, project]) else None - credentials = Credentials(token=token, url=url, default_provider=default_hgp_id, **kwargs) + default_hgp_id = ( + HubGroupProjectID(hub, group, project) + if all([hub, group, project]) + else None + ) + credentials = Credentials( + token=token, url=url, default_provider=default_hgp_id, **kwargs + ) store_credentials(credentials, overwrite=overwrite) @staticmethod @@ -590,19 +639,17 @@ def saved_account() -> Dict[str, str]: credentials = list(stored_credentials.values())[0] if credentials.url != QISKIT_IBM_RUNTIME_API_URL: raise IBMProviderCredentialsInvalidUrl( - 'Invalid IBM Quantum credentials found on disk.') - return { - 'token': credentials.token, - 'url': credentials.url - } + "Invalid IBM Quantum credentials found on disk." + ) + return {"token": credentials.token, "url": credentials.url} def get_backend( - self, - name: str = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - **kwargs: Any + self, + name: str = None, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, + **kwargs: Any, ) -> Backend: """Return a single backend matching the specified filtering. @@ -625,32 +672,34 @@ def get_backend( # pylint: disable=arguments-differ backends = self.backends(name, hub=hub, group=group, project=project, **kwargs) if len(backends) > 1: - raise QiskitBackendNotFoundError("More than one backend matches the criteria") + raise QiskitBackendNotFoundError( + "More than one backend matches the criteria" + ) if not backends: raise QiskitBackendNotFoundError("No backend matches the criteria") return backends[0] def run_circuits( - self, - circuits: Union[QuantumCircuit, List[QuantumCircuit]], - backend_name: str, - shots: Optional[int] = None, - initial_layout: Optional[Union[Layout, Dict, List]] = None, - layout_method: Optional[str] = None, - routing_method: Optional[str] = None, - translation_method: Optional[str] = None, - seed_transpiler: Optional[int] = None, - optimization_level: int = 1, - init_qubits: bool = True, - rep_delay: Optional[float] = None, - transpiler_options: Optional[dict] = None, - measurement_error_mitigation: bool = False, - use_measure_esp: Optional[bool] = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - **run_config: Dict - ) -> 'runtime_job.RuntimeJob': + self, + circuits: Union[QuantumCircuit, List[QuantumCircuit]], + backend_name: str, + shots: Optional[int] = None, + initial_layout: Optional[Union[Layout, Dict, List]] = None, + layout_method: Optional[str] = None, + routing_method: Optional[str] = None, + translation_method: Optional[str] = None, + seed_transpiler: Optional[int] = None, + optimization_level: int = 1, + init_qubits: bool = True, + rep_delay: Optional[float] = None, + transpiler_options: Optional[dict] = None, + measurement_error_mitigation: bool = False, + use_measure_esp: Optional[bool] = None, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, + **run_config: Dict, + ) -> "runtime_job.RuntimeJob": """Execute the input circuit(s) on a backend using the runtime service. Note: @@ -714,35 +763,46 @@ def run_circuits( Runtime job. """ inputs = copy.deepcopy(run_config) # type: Dict[str, Any] - inputs['circuits'] = circuits - inputs['optimization_level'] = optimization_level - inputs['init_qubits'] = init_qubits - inputs['measurement_error_mitigation'] = measurement_error_mitigation + inputs["circuits"] = circuits + inputs["optimization_level"] = optimization_level + inputs["init_qubits"] = init_qubits + inputs["measurement_error_mitigation"] = measurement_error_mitigation if shots: - inputs['shots'] = shots + inputs["shots"] = shots if initial_layout: - inputs['initial_layout'] = initial_layout + inputs["initial_layout"] = initial_layout if layout_method: - inputs['layout_method'] = layout_method + inputs["layout_method"] = layout_method if routing_method: - inputs['routing_method'] = routing_method + inputs["routing_method"] = routing_method if translation_method: - inputs['translation_method'] = translation_method + inputs["translation_method"] = translation_method if seed_transpiler: - inputs['seed_transpiler'] = seed_transpiler + inputs["seed_transpiler"] = seed_transpiler if rep_delay: - inputs['rep_delay'] = rep_delay + inputs["rep_delay"] = rep_delay if transpiler_options: - inputs['transpiler_options'] = transpiler_options + inputs["transpiler_options"] = transpiler_options if use_measure_esp is not None: - inputs['use_measure_esp'] = use_measure_esp - options = {'backend_name': backend_name} - return self.run('circuit-runner', options=options, inputs=inputs, - result_decoder=RunnerResult, - hub=hub, group=group, project=project) - - def pprint_programs(self, refresh: bool = False, detailed: bool = False, - limit: int = 20, skip: int = 0) -> None: + inputs["use_measure_esp"] = use_measure_esp + options = {"backend_name": backend_name} + return self.run( + "circuit-runner", + options=options, + inputs=inputs, + result_decoder=RunnerResult, + hub=hub, + group=group, + project=project, + ) + + def pprint_programs( + self, + refresh: bool = False, + detailed: bool = False, + limit: int = 20, + skip: int = 0, + ) -> None: """Pretty print information about available runtime programs. Args: @@ -755,16 +815,19 @@ def pprint_programs(self, refresh: bool = False, detailed: bool = False, """ programs = self.programs(refresh, limit, skip) for prog in programs: - print("="*50) + print("=" * 50) if detailed: print(str(prog)) else: - print(f"{prog.program_id}:",) + print( + f"{prog.program_id}:", + ) print(f" Name: {prog.name}") print(f" Description: {prog.description}") - def programs(self, refresh: bool = False, - limit: int = 20, skip: int = 0) -> List[RuntimeProgram]: + def programs( + self, refresh: bool = False, limit: int = 20, skip: int = 0 + ) -> List[RuntimeProgram]: """Return available runtime programs. Currently only program metadata is returned. @@ -785,7 +848,9 @@ def programs(self, refresh: bool = False, current_page_limit = 20 offset = 0 while True: - response = self._api_client.list_programs(limit=current_page_limit, skip=offset) + response = self._api_client.list_programs( + limit=current_page_limit, skip=offset + ) program_page = response.get("programs", []) # count is the total number of programs that would be returned if # there was no limit or skip @@ -799,7 +864,7 @@ def programs(self, refresh: bool = False, offset += len(program_page) if limit is None: limit = len(self._programs) - return list(self._programs.values())[skip:limit+skip] + return list(self._programs.values())[skip : limit + skip] def program(self, program_id: str, refresh: bool = False) -> RuntimeProgram: """Retrieve a runtime program. @@ -823,7 +888,9 @@ def program(self, program_id: str, refresh: bool = False) -> RuntimeProgram: response = self._api_client.program_get(program_id) except RequestsApiError as ex: if ex.status_code == 404: - raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None + raise RuntimeProgramNotFound( + f"Program not found: {ex.message}" + ) from None raise QiskitRuntimeError(f"Failed to get program: {ex}") from None self._programs[program_id] = self._to_program(response) @@ -844,36 +911,38 @@ def _to_program(self, response: Dict) -> RuntimeProgram: return_values = {} interim_results = {} if "spec" in response: - backend_requirements = response["spec"].get('backend_requirements', {}) - parameters = response["spec"].get('parameters', {}) - return_values = response["spec"].get('return_values', {}) - interim_results = response["spec"].get('interim_results', {}) - - return RuntimeProgram(program_name=response['name'], - program_id=response['id'], - description=response.get('description', ""), - parameters=parameters, - return_values=return_values, - interim_results=interim_results, - max_execution_time=response.get('cost', 0), - creation_date=response.get('creation_date', ""), - update_date=response.get('update_date', ""), - backend_requirements=backend_requirements, - is_public=response.get('is_public', False), - data=response.get('data', ""), - api_client=self._api_client) + backend_requirements = response["spec"].get("backend_requirements", {}) + parameters = response["spec"].get("parameters", {}) + return_values = response["spec"].get("return_values", {}) + interim_results = response["spec"].get("interim_results", {}) + + return RuntimeProgram( + program_name=response["name"], + program_id=response["id"], + description=response.get("description", ""), + parameters=parameters, + return_values=return_values, + interim_results=interim_results, + max_execution_time=response.get("cost", 0), + creation_date=response.get("creation_date", ""), + update_date=response.get("update_date", ""), + backend_requirements=backend_requirements, + is_public=response.get("is_public", False), + data=response.get("data", ""), + api_client=self._api_client, + ) def run( - self, - program_id: str, - options: Dict, - inputs: Union[Dict, ParameterNamespace], - callback: Optional[Callable] = None, - result_decoder: Optional[Type[ResultDecoder]] = None, - image: Optional[str] = "", - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None + self, + program_id: str, + options: Dict, + inputs: Union[Dict, ParameterNamespace], + callback: Optional[Callable] = None, + result_decoder: Optional[Type[ResultDecoder]] = None, + image: Optional[str] = "", + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, ) -> RuntimeJob: """Execute the runtime program. @@ -903,46 +972,60 @@ def run( Raises: IBMInputValueError: If input is invalid. """ - if 'backend_name' not in options: + if "backend_name" not in options: raise IBMInputValueError('"backend_name" is required field in "options"') # If using params object, extract as dictionary if isinstance(inputs, ParameterNamespace): inputs.validate() inputs = vars(inputs) - if image and not \ - re.match("[a-zA-Z0-9]+([/.\\-_][a-zA-Z0-9]+)*:[a-zA-Z0-9]+([.\\-_][a-zA-Z0-9]+)*$", - image): + if image and not re.match( + "[a-zA-Z0-9]+([/.\\-_][a-zA-Z0-9]+)*:[a-zA-Z0-9]+([.\\-_][a-zA-Z0-9]+)*$", + image, + ): raise IBMInputValueError('"image" needs to be in form of image_name:tag') - backend_name = options['backend_name'] - if not all([hub, group, project]) and self._default_hgp.get_backend(backend_name): + backend_name = options["backend_name"] + if not all([hub, group, project]) and self._default_hgp.get_backend( + backend_name + ): hgp = self._default_hgp else: - hgp = self._get_hgp(hub=hub, group=group, project=project, - backend_name=backend_name, service_name=SERVICE_NAME) + hgp = self._get_hgp( + hub=hub, + group=group, + project=project, + backend_name=backend_name, + service_name=SERVICE_NAME, + ) credentials = hgp.credentials - api_client = self._api_client if hgp == self._default_hgp else RuntimeClient(credentials) + api_client = ( + self._api_client if hgp == self._default_hgp else RuntimeClient(credentials) + ) result_decoder = result_decoder or ResultDecoder - response = api_client.program_run(program_id=program_id, - credentials=credentials, - backend_name=backend_name, - params=inputs, - image=image) + response = api_client.program_run( + program_id=program_id, + credentials=credentials, + backend_name=backend_name, + params=inputs, + image=image, + ) backend = self.get_backend(backend_name) - job = RuntimeJob(backend=backend, - api_client=api_client, - credentials=credentials, - job_id=response['id'], program_id=program_id, params=inputs, - user_callback=callback, - result_decoder=result_decoder, - image=image) + job = RuntimeJob( + backend=backend, + api_client=api_client, + credentials=credentials, + job_id=response["id"], + program_id=program_id, + params=inputs, + user_callback=callback, + result_decoder=result_decoder, + image=image, + ) return job def upload_program( - self, - data: str, - metadata: Optional[Union[Dict, str]] = None + self, data: str, metadata: Optional[Union[Dict, str]] = None ) -> str: """Upload a runtime program. @@ -994,7 +1077,7 @@ def upload_program( """ program_metadata = self._read_metadata(metadata=metadata) - for req in ['name', 'description', 'max_execution_time']: + for req in ["name", "description", "max_execution_time"]: if req not in program_metadata or not program_metadata[req]: raise IBMInputValueError(f"{req} is a required metadata field.") @@ -1005,22 +1088,22 @@ def upload_program( try: program_data = to_base64_string(data) - response = self._api_client.program_create(program_data=program_data, - **program_metadata) + response = self._api_client.program_create( + program_data=program_data, **program_metadata + ) except RequestsApiError as ex: if ex.status_code == 409: raise RuntimeDuplicateProgramError( - "Program with the same name already exists.") from None + "Program with the same name already exists." + ) from None if ex.status_code == 403: raise IBMNotAuthorizedError( - "You are not authorized to upload programs.") from None + "You are not authorized to upload programs." + ) from None raise QiskitRuntimeError(f"Failed to create program: {ex}") from None - return response['id'] + return response["id"] - def _read_metadata( - self, - metadata: Optional[Union[Dict, str]] = None - ) -> Dict: + def _read_metadata(self, metadata: Optional[Union[Dict, str]] = None) -> Dict: """Read metadata. Args: @@ -1032,24 +1115,29 @@ def _read_metadata( upd_metadata: dict = {} if metadata is not None: if isinstance(metadata, str): - with open(metadata, 'r') as file: + with open(metadata, "r") as file: upd_metadata = json.load(file) else: upd_metadata = metadata # TODO validate metadata format - metadata_keys = ['name', 'max_execution_time', 'description', - 'spec', 'is_public'] + metadata_keys = [ + "name", + "max_execution_time", + "description", + "spec", + "is_public", + ] return {key: val for key, val in upd_metadata.items() if key in metadata_keys} def update_program( - self, - program_id: str, - data: str = None, - metadata: Optional[Union[Dict, str]] = None, - name: str = None, - description: str = None, - max_execution_time: int = None, - spec: Optional[Dict] = None + self, + program_id: str, + data: str = None, + metadata: Optional[Union[Dict, str]] = None, + name: str = None, + description: str = None, + max_execution_time: int = None, + spec: Optional[Dict] = None, ) -> None: """Update a runtime program. @@ -1073,9 +1161,11 @@ def update_program( QiskitRuntimeError: If the request failed. """ if not any([data, metadata, name, description, max_execution_time, spec]): - warnings.warn("None of the 'data', 'metadata', 'name', 'description', " - "'max_execution_time', or 'spec' parameters is specified. " - "No update is made.") + warnings.warn( + "None of the 'data', 'metadata', 'name', 'description', " + "'max_execution_time', or 'spec' parameters is specified. " + "No update is made." + ) return if data: @@ -1088,26 +1178,29 @@ def update_program( if metadata: metadata = self._read_metadata(metadata=metadata) combined_metadata = self._merge_metadata( - metadata=metadata, name=name, description=description, - max_execution_time=max_execution_time, spec=spec) + metadata=metadata, + name=name, + description=description, + max_execution_time=max_execution_time, + spec=spec, + ) try: self._api_client.program_update( - program_id, program_data=data, **combined_metadata) + program_id, program_data=data, **combined_metadata + ) except RequestsApiError as ex: if ex.status_code == 404: - raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None + raise RuntimeProgramNotFound( + f"Program not found: {ex.message}" + ) from None raise QiskitRuntimeError(f"Failed to update program: {ex}") from None if program_id in self._programs: program = self._programs[program_id] program._refresh() - def _merge_metadata( - self, - metadata: Optional[Dict] = None, - **kwargs: Any - ) -> Dict: + def _merge_metadata(self, metadata: Optional[Dict] = None, **kwargs: Any) -> Dict: """Merge multiple copies of metadata. Args: metadata: Program metadata. @@ -1117,7 +1210,7 @@ def _merge_metadata( """ merged = {} metadata = metadata or {} - metadata_keys = ['name', 'max_execution_time', 'description', 'spec'] + metadata_keys = ["name", "max_execution_time", "description", "spec"] for key in metadata_keys: if kwargs.get(key, None) is not None: merged[key] = kwargs[key] @@ -1139,7 +1232,9 @@ def delete_program(self, program_id: str) -> None: self._api_client.program_delete(program_id=program_id) except RequestsApiError as ex: if ex.status_code == 404: - raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None + raise RuntimeProgramNotFound( + f"Program not found: {ex.message}" + ) from None raise QiskitRuntimeError(f"Failed to delete program: {ex}") from None if program_id in self._programs: @@ -1162,7 +1257,9 @@ def set_program_visibility(self, program_id: str, public: bool) -> None: except RequestsApiError as ex: if ex.status_code == 404: raise RuntimeJobNotFound(f"Program not found: {ex.message}") from None - raise QiskitRuntimeError(f"Failed to set program visibility: {ex}") from None + raise QiskitRuntimeError( + f"Failed to set program visibility: {ex}" + ) from None if program_id in self._programs: program = self._programs[program_id] @@ -1190,14 +1287,14 @@ def job(self, job_id: str) -> RuntimeJob: return self._decode_job(response) def jobs( - self, - limit: Optional[int] = 10, - skip: int = 0, - pending: bool = None, - program_id: str = None, - hub: str = None, - group: str = None, - project: str = None + self, + limit: Optional[int] = 10, + skip: int = 0, + pending: bool = None, + program_id: str = None, + hub: str = None, + group: str = None, + project: str = None, ) -> List[RuntimeJob]: """Retrieve all runtime jobs, subject to optional filtering. @@ -1220,10 +1317,11 @@ def jobs( and ``project`` are given. """ if any([hub, group, project]) and not all([hub, group, project]): - raise IBMInputValueError('Hub, group and project ' - 'parameters must all be specified. ' - 'hub = "{}", group = "{}", project = "{}"' - .format(hub, group, project)) + raise IBMInputValueError( + "Hub, group and project " + "parameters must all be specified. " + 'hub = "{}", group = "{}", project = "{}"'.format(hub, group, project) + ) job_responses = [] # type: List[Dict[str, Any]] current_page_limit = limit or 20 offset = skip @@ -1236,7 +1334,8 @@ def jobs( program_id=program_id, hub=hub, group=group, - project=project) + project=project, + ) job_page = jobs_response["jobs"] # count is the total number of jobs that would be returned if # there was no limit or skip @@ -1288,23 +1387,25 @@ def _decode_job(self, raw_data: Dict) -> RuntimeJob: Returns: Decoded job data. """ - hub = raw_data['hub'] - group = raw_data['group'] - project = raw_data['project'] + hub = raw_data["hub"] + group = raw_data["group"] + project = raw_data["project"] # Try to find the right backend try: - backend = self.get_backend(raw_data['backend'], - hub=hub, group=group, project=project) + backend = self.get_backend( + raw_data["backend"], hub=hub, group=group, project=project + ) except (IBMProviderError, QiskitBackendNotFoundError): backend = ibm_backend.IBMRetiredBackend.from_name( - backend_name=raw_data['backend'], + backend_name=raw_data["backend"], service=self, - credentials=Credentials(token="", url="", - hub=hub, group=group, project=project), - api=None + credentials=Credentials( + token="", url="", hub=hub, group=group, project=project + ), + api=None, ) - params = raw_data.get('params', {}) + params = raw_data.get("params", {}) if isinstance(params, list): if len(params) > 0: params = params[0] @@ -1314,13 +1415,15 @@ def _decode_job(self, raw_data: Dict) -> RuntimeJob: params = json.dumps(params) decoded = json.loads(params, cls=RuntimeDecoder) - return RuntimeJob(backend=backend, - api_client=self._api_client, - credentials=self._default_hgp.credentials, - job_id=raw_data['id'], - program_id=raw_data.get('program', {}).get('id', ""), - params=decoded, - creation_date=raw_data.get('created', None)) + return RuntimeJob( + backend=backend, + api_client=self._api_client, + credentials=self._default_hgp.credentials, + job_id=raw_data["id"], + program_id=raw_data.get("program", {}).get("id", ""), + params=decoded, + creation_date=raw_data.get("created", None), + ) def logout(self) -> None: """Clears authorization cache on the server. diff --git a/qiskit_ibm_runtime/jupyter/__init__.py b/qiskit_ibm_runtime/jupyter/__init__.py index 657e4b16c9..491f856983 100644 --- a/qiskit_ibm_runtime/jupyter/__init__.py +++ b/qiskit_ibm_runtime/jupyter/__init__.py @@ -68,9 +68,9 @@ """ import sys -if ('ipykernel' in sys.modules) and ('spyder' not in sys.modules): +if ("ipykernel" in sys.modules) and ("spyder" not in sys.modules): - from IPython import get_ipython # pylint: disable=import-error + from IPython import get_ipython # pylint: disable=import-error from .dashboard.dashboard import IBMDashboardMagic from qiskit.test.mock import FakeBackend from ..ibm_backend import IBMBackend @@ -79,7 +79,7 @@ _IP = get_ipython() if _IP is not None: _IP.register_magics(IBMDashboardMagic) - HTML_FORMATTER = _IP.display_formatter.formatters['text/html'] + HTML_FORMATTER = _IP.display_formatter.formatters["text/html"] # Make backend_widget the html repr for IBM Quantum backends HTML_FORMATTER.for_type(IBMBackend, backend_widget) HTML_FORMATTER.for_type(FakeBackend, backend_widget) diff --git a/qiskit_ibm_runtime/jupyter/backend_info.py b/qiskit_ibm_runtime/jupyter/backend_info.py index ffa63099e6..dbabd2564b 100644 --- a/qiskit_ibm_runtime/jupyter/backend_info.py +++ b/qiskit_ibm_runtime/jupyter/backend_info.py @@ -32,28 +32,43 @@ def backend_widget(backend: Union[IBMBackend, FakeBackend]) -> None: backend: Display information about this backend. """ cred = backend._credentials - card = vue.Card(height=600, outlined=True, - children=[ - vue.Toolbar(flat=True, color="#002d9c", - children=[ - vue.ToolbarTitle(children=['{} @ ({}/{}/{})'.format( - backend.name(), - cred.hub, - cred.group, - cred.project)], - style_="color:white")]), - vue.Tabs(vertical=True, - children=[ - vue.Tab(children=['Configuration']), - vue.Tab(children=['Qubits']), - vue.Tab(children=['Non-local Gates']), - vue.Tab(children=['Error map']), - vue.TabItem(children=[config_tab(backend)]), - vue.TabItem(children=[qubits_tab(backend)]), - vue.TabItem(children=[gates_tab(backend)]), - vue.TabItem(children=[iplot_error_map(backend, - figsize=(None, None), - as_widget=True)]) - ]) - ]) + card = vue.Card( + height=600, + outlined=True, + children=[ + vue.Toolbar( + flat=True, + color="#002d9c", + children=[ + vue.ToolbarTitle( + children=[ + "{} @ ({}/{}/{})".format( + backend.name(), cred.hub, cred.group, cred.project + ) + ], + style_="color:white", + ) + ], + ), + vue.Tabs( + vertical=True, + children=[ + vue.Tab(children=["Configuration"]), + vue.Tab(children=["Qubits"]), + vue.Tab(children=["Non-local Gates"]), + vue.Tab(children=["Error map"]), + vue.TabItem(children=[config_tab(backend)]), + vue.TabItem(children=[qubits_tab(backend)]), + vue.TabItem(children=[gates_tab(backend)]), + vue.TabItem( + children=[ + iplot_error_map( + backend, figsize=(None, None), as_widget=True + ) + ] + ), + ], + ), + ], + ) display(card) diff --git a/qiskit_ibm_runtime/jupyter/config_widget.py b/qiskit_ibm_runtime/jupyter/config_widget.py index 3126b17c18..6ddcab177b 100644 --- a/qiskit_ibm_runtime/jupyter/config_widget.py +++ b/qiskit_ibm_runtime/jupyter/config_widget.py @@ -37,32 +37,41 @@ def config_tab(backend: Union[IBMBackend, FakeBackend]) -> wid.GridBox: config = backend.configuration().to_dict() next_resrv = get_next_reservation(backend) if next_resrv: - reservation_str = "in {} ({}m)".format(duration_difference(next_resrv.start_datetime), - next_resrv.duration) + reservation_str = "in {} ({}m)".format( + duration_difference(next_resrv.start_datetime), next_resrv.duration + ) else: - reservation_str = '-' + reservation_str = "-" config_dict = {**status, **config} - config_dict['reservation'] = reservation_str - - upper_list = ['n_qubits'] - - if 'quantum_volume' in config.keys(): - if config['quantum_volume']: - upper_list.append('quantum_volume') - - upper_list.extend(['operational', - 'status_msg', 'pending_jobs', 'reservation', - 'backend_version', 'basis_gates', - 'max_shots', 'max_experiments']) + config_dict["reservation"] = reservation_str + + upper_list = ["n_qubits"] + + if "quantum_volume" in config.keys(): + if config["quantum_volume"]: + upper_list.append("quantum_volume") + + upper_list.extend( + [ + "operational", + "status_msg", + "pending_jobs", + "reservation", + "backend_version", + "basis_gates", + "max_shots", + "max_experiments", + ] + ) lower_list = list(set(config_dict.keys()).difference(upper_list)) # Remove gates because they are in a different tab - lower_list.remove('gates') + lower_list.remove("gates") # Look for hamiltonian - if 'hamiltonian' in lower_list: - htex = config_dict['hamiltonian']['h_latex'] - config_dict['hamiltonian'] = "$$%s$$" % htex + if "hamiltonian" in lower_list: + htex = config_dict["hamiltonian"]["h_latex"] + config_dict["hamiltonian"] = "$$%s$$" % htex upper_str = "" upper_str += """""" lower_str += "" for key in lower_list: - if key != 'name': - lower_str += "" % ( - key, config_dict[key]) + if key != "name": + lower_str += "" % (key, config_dict[key]) lower_str += footer - lower_table = wid.HTMLMath(value=lower_str, - layout=wid.Layout(width='auto', - grid_area='bottom')) - - grid = wid.GridBox(children=[upper_table, image_widget, lower_table], - layout=wid.Layout(max_height='500px', - margin='10px', - overflow='hidden scroll', - grid_template_rows='auto auto', - grid_template_columns='33% 21% 21% 21%', - grid_template_areas=''' + lower_table = wid.HTMLMath( + value=lower_str, layout=wid.Layout(width="auto", grid_area="bottom") + ) + + grid = wid.GridBox( + children=[upper_table, image_widget, lower_table], + layout=wid.Layout( + max_height="500px", + margin="10px", + overflow="hidden scroll", + grid_template_rows="auto auto", + grid_template_columns="33% 21% 21% 21%", + grid_template_areas=""" "left right right right" "bottom bottom bottom bottom" - ''', - grid_gap='0px 0px')) + """, + grid_gap="0px 0px", + ), + ) return grid diff --git a/qiskit_ibm_runtime/jupyter/dashboard/backend_update.py b/qiskit_ibm_runtime/jupyter/dashboard/backend_update.py index ad69c877c8..988be8e59c 100644 --- a/qiskit_ibm_runtime/jupyter/dashboard/backend_update.py +++ b/qiskit_ibm_runtime/jupyter/dashboard/backend_update.py @@ -19,11 +19,15 @@ from qiskit_ibm_runtime.utils.converters import duration_difference from ..utils import get_next_reservation -from .constants import RESERVATION_STR, RESERVATION_NONE, STAT_FONT_VALUE, STAT_FONT_VALUE_COLOR +from .constants import ( + RESERVATION_STR, + RESERVATION_NONE, + STAT_FONT_VALUE, + STAT_FONT_VALUE_COLOR, +) -def update_backend_info(device_list: wid.VBox, - interval: int = 30) -> None: +def update_backend_info(device_list: wid.VBox, interval: int = 30) -> None: """Updates the device list from another thread. Args: @@ -33,7 +37,7 @@ def update_backend_info(device_list: wid.VBox, my_thread = threading.currentThread() current_interval = 0 started = False - reservation_interval = 10*60 + reservation_interval = 10 * 60 cur_rsvr_interval = 0 while getattr(my_thread, "do_run", False): if current_interval == interval or started is False: @@ -48,22 +52,25 @@ def update_backend_info(device_list: wid.VBox, stat_msg = status.status_msg pending = str(status.pending_jobs) - color = '#000000' - if stat_msg == 'active': - color = '#34bc6e' - if stat_msg in ['maintenance', 'internal', 'dedicated']: - color = '#FFB000' + color = "#000000" + if stat_msg == "active": + color = "#34bc6e" + if stat_msg in ["maintenance", "internal", "dedicated"]: + color = "#FFB000" if cur_rsvr_interval >= reservation_interval: cur_rsvr_interval = 0 next_resrv = get_next_reservation(backend_pane._backend) reservation_wid = backend_pane._reservation_val_wid if next_resrv: - start_dt_str = duration_difference(next_resrv.start_datetime) + start_dt_str = duration_difference( + next_resrv.start_datetime + ) new_resrv_val = RESERVATION_STR.format( - start_dt=start_dt_str, duration=next_resrv.duration) - if stat_msg == 'active': - stat_msg += ' [R]' + start_dt=start_dt_str, duration=next_resrv.duration + ) + if stat_msg == "active": + stat_msg += " [R]" else: new_resrv_val = RESERVATION_NONE @@ -71,12 +78,14 @@ def update_backend_info(device_list: wid.VBox, reservation_wid.value = new_resrv_val status_wid = backend_pane._status_val_wid - if status_wid.value.split('>')[1].split("<")[0] != stat_msg: + if status_wid.value.split(">")[1].split("<")[0] != stat_msg: # If the status message has changed. - status_wid.value = STAT_FONT_VALUE_COLOR.format(color=color, msg=stat_msg) + status_wid.value = STAT_FONT_VALUE_COLOR.format( + color=color, msg=stat_msg + ) pend_wid = backend_pane._queue_val_wid - if pend_wid.value.split('>')[1].split("<")[0] != pending: + if pend_wid.value.split(">")[1].split("<")[0] != pending: # If the number of pending jobs has changed. pend_wid.value = STAT_FONT_VALUE.format(pending) diff --git a/qiskit_ibm_runtime/jupyter/dashboard/backend_widget.py b/qiskit_ibm_runtime/jupyter/dashboard/backend_widget.py index e99ccef560..35a63fd4b7 100644 --- a/qiskit_ibm_runtime/jupyter/dashboard/backend_widget.py +++ b/qiskit_ibm_runtime/jupyter/dashboard/backend_widget.py @@ -19,13 +19,18 @@ from ...visualization.interactive import iplot_gate_map from .provider_buttons import provider_buttons from .utils import BackendWithProviders -from .constants import (RESERVATION_STR, RESERVATION_NONE, STAT_FONT_TITLE, - STAT_FONT_VALUE, STAT_FONT_VALUE_COLOR) +from .constants import ( + RESERVATION_STR, + RESERVATION_NONE, + STAT_FONT_TITLE, + STAT_FONT_VALUE, + STAT_FONT_VALUE_COLOR, +) from ..utils import get_next_reservation def make_backend_widget(backend_item: BackendWithProviders) -> wid.HBox: - """ Construct a backend widget for a given device. + """Construct a backend widget for a given device. Args: backend_item: A ``BackendWithProviders`` instance containing the @@ -50,39 +55,48 @@ def make_backend_widget(backend_item: BackendWithProviders) -> wid.HBox: status_wid = wid.HTML(value=STAT_FONT_TITLE.format("Status:")) - color = '#000000' + color = "#000000" status_msg = status.status_msg - if status_msg == 'active': - color = '#34BC6E' + if status_msg == "active": + color = "#34BC6E" if next_resrv: - status_msg += ' [R]' - if status_msg in ['maintenance', 'internal']: - color = '#FFB000' + status_msg += " [R]" + if status_msg in ["maintenance", "internal"]: + color = "#FFB000" - status_val_wid = wid.HTML(value=STAT_FONT_VALUE_COLOR.format(color=color, msg=status_msg)) + status_val_wid = wid.HTML( + value=STAT_FONT_VALUE_COLOR.format(color=color, msg=status_msg) + ) left_labels = wid.VBox(children=[qubits_wid, status_wid]) - left_values = wid.VBox(children=[qubits_val_wid, status_val_wid], - layout=wid.Layout(margin="1px 0px 0px 0px")) + left_values = wid.VBox( + children=[qubits_val_wid, status_val_wid], + layout=wid.Layout(margin="1px 0px 0px 0px"), + ) - left_stats = wid.HBox(children=[left_labels, left_values], - layout=wid.Layout(width='175px') - ) + left_stats = wid.HBox( + children=[left_labels, left_values], layout=wid.Layout(width="175px") + ) version_wid = wid.HTML(value=STAT_FONT_TITLE.format("Version:")) - version_val_wid = wid.HTML(value=STAT_FONT_VALUE.format(config.backend_version), - layout=wid.Layout(margin="3px 0px 0px 0px")) + version_val_wid = wid.HTML( + value=STAT_FONT_VALUE.format(config.backend_version), + layout=wid.Layout(margin="3px 0px 0px 0px"), + ) queue_wid = wid.HTML(value=STAT_FONT_TITLE.format("Queue:")) - queue_val_wid = wid.HTML(value=STAT_FONT_VALUE.format(status.pending_jobs), - layout=wid.Layout(margin="5px 0px 0px 0px")) + queue_val_wid = wid.HTML( + value=STAT_FONT_VALUE.format(status.pending_jobs), + layout=wid.Layout(margin="5px 0px 0px 0px"), + ) right_labels = wid.VBox(children=[version_wid, queue_wid]) right_values = wid.VBox(children=[version_val_wid, queue_val_wid]) - right_stats = wid.HBox(children=[right_labels, right_values], - layout=wid.Layout(width='auto', - margin="0px 0px 0px 20px")) + right_stats = wid.HBox( + children=[right_labels, right_values], + layout=wid.Layout(width="auto", margin="0px 0px 0px 20px"), + ) stats = wid.HBox(children=[left_stats, right_stats]) @@ -91,7 +105,8 @@ def make_backend_widget(backend_item: BackendWithProviders) -> wid.HBox: if next_resrv: start_dt_str = duration_difference(next_resrv.start_datetime) reservation_val = RESERVATION_STR.format( - start_dt=start_dt_str, duration=next_resrv.duration) + start_dt=start_dt_str, duration=next_resrv.duration + ) else: reservation_val = RESERVATION_NONE reservation_val_wid = wid.HTML(value=reservation_val) @@ -101,10 +116,16 @@ def make_backend_widget(backend_item: BackendWithProviders) -> wid.HBox: providers_list = provider_buttons(backend_providers) - device_stats = wid.VBox(children=[backend_name, stats, reservation_wid, - providers_label, providers_list], - layout=wid.Layout(width='auto', - margin="0px 20px 0px 0px")) + device_stats = wid.VBox( + children=[ + backend_name, + stats, + reservation_wid, + providers_label, + providers_list, + ], + layout=wid.Layout(width="auto", margin="0px 20px 0px 0px"), + ) n_qubits = config.n_qubits @@ -120,50 +141,60 @@ def make_backend_widget(backend_item: BackendWithProviders) -> wid.HBox: qubit_size = 12 line_width = 3 - _gmap = iplot_gate_map(backend, figsize=(150, 150), - qubit_size=qubit_size, - line_width=line_width, - qubit_color='#031981', - line_color='#031981', - label_qubits=False, - as_widget=True) - - gmap = wid.Box(children=[_gmap], - layout=wid.Layout(width='auto', - justify_content='center', - align_content='center', - )) + _gmap = iplot_gate_map( + backend, + figsize=(150, 150), + qubit_size=qubit_size, + line_width=line_width, + qubit_color="#031981", + line_color="#031981", + label_qubits=False, + as_widget=True, + ) + + gmap = wid.Box( + children=[_gmap], + layout=wid.Layout( + width="auto", + justify_content="center", + align_content="center", + ), + ) # Get basic device stats - t1_units = props['qubits'][0][0]['unit'] - avg_t1 = round(sum([q[0]['value'] for q in props['qubits']])/n_qubits, 1) - avg_t2 = round(sum([q[1]['value'] for q in props['qubits']])/n_qubits, 1) + t1_units = props["qubits"][0][0]["unit"] + avg_t1 = round(sum([q[0]["value"] for q in props["qubits"]]) / n_qubits, 1) + avg_t2 = round(sum([q[1]["value"] for q in props["qubits"]]) / n_qubits, 1) if n_qubits != 1: sum_cx_err = 0 num_cx = 0 - for gate in props['gates']: - if gate['gate'] == 'cx': - for param in gate['parameters']: - if param['name'] == 'gate_error': + for gate in props["gates"]: + if gate["gate"] == "cx": + for param in gate["parameters"]: + if param["name"] == "gate_error": # Value == 1.0 means gate effectively off - if param['value'] != 1.0: - sum_cx_err += param['value'] + if param["value"] != 1.0: + sum_cx_err += param["value"] num_cx += 1 if num_cx: - avg_cx_err = round(sum_cx_err/(num_cx)*100, 2) + avg_cx_err = round(sum_cx_err / (num_cx) * 100, 2) else: avg_cx_err = 100.0 avg_meas_err = 0 - for qub in props['qubits']: + for qub in props["qubits"]: for item in qub: - if item['name'] == 'readout_error': - avg_meas_err += item['value'] - avg_meas_err = round(avg_meas_err/n_qubits*100, 2) - - t12_label = wid.HTML(value="Avg. T1 / T2:") - t12_str = "{t1}/{t2} {units}" + if item["name"] == "readout_error": + avg_meas_err += item["value"] + avg_meas_err = round(avg_meas_err / n_qubits * 100, 2) + + t12_label = wid.HTML( + value="Avg. T1 / T2:" + ) + t12_str = ( + "{t1}/{t2} {units}" + ) t12_wid = wid.HTML(value=t12_str.format(t1=avg_t1, t2=avg_t2, units=t1_units)) meas_label = wid.HTML(value="Avg. Readout Err.:") @@ -175,15 +206,16 @@ def make_backend_widget(backend_item: BackendWithProviders) -> wid.HBox: cx_label = wid.HTML(value="Avg. CX Err.:") cx_wid = wid.HTML(value=cx_str.format(cx_err=avg_cx_err)) - quant_vol = 'None' + quant_vol = "None" try: quant_vol = config.quantum_volume except AttributeError: pass qv_label = wid.HTML(value="Quantum Volume:") qv_str = "{qv}" - qv_wid = wid.HTML(value=qv_str.format(qv=quant_vol), - layout=wid.Layout(margin="-1px 0px 0px 0px")) + qv_wid = wid.HTML( + value=qv_str.format(qv=quant_vol), layout=wid.Layout(margin="-1px 0px 0px 0px") + ) if n_qubits != 1: left_wids = [t12_label, cx_label, meas_label, qv_label] @@ -193,26 +225,34 @@ def make_backend_widget(backend_item: BackendWithProviders) -> wid.HBox: left_wids = [t12_label, meas_label, qv_label] right_wids = [t12_wid, meas_wid, qv_wid] - left_wid = wid.VBox(children=left_wids, - layout=wid.Layout(margin="2px 0px 0px 0px")) - right_wid = wid.VBox(children=right_wids, - layout=wid.Layout(margin="0px 0px 0px 3px")) + left_wid = wid.VBox(children=left_wids, layout=wid.Layout(margin="2px 0px 0px 0px")) + right_wid = wid.VBox( + children=right_wids, layout=wid.Layout(margin="0px 0px 0px 3px") + ) stats = wid.HBox(children=[left_wid, right_wid]) - gate_map = wid.VBox(children=[gmap, stats], - layout=wid.Layout(width='230px', - justify_content='center', - align_content='center', - margin="-25px 0px 0px 0px")) - - out = wid.HBox(children=[device_stats, gate_map], - layout=wid.Layout(width='auto', - height='auto', - padding="5px 5px 5px 5px", - justify_content='space-between', - max_width='700px', - border='1px solid #212121')) + gate_map = wid.VBox( + children=[gmap, stats], + layout=wid.Layout( + width="230px", + justify_content="center", + align_content="center", + margin="-25px 0px 0px 0px", + ), + ) + + out = wid.HBox( + children=[device_stats, gate_map], + layout=wid.Layout( + width="auto", + height="auto", + padding="5px 5px 5px 5px", + justify_content="space-between", + max_width="700px", + border="1px solid #212121", + ), + ) # Attach information to the backend panel for later updates. out._backend = backend # pylint: disable=protected-access diff --git a/qiskit_ibm_runtime/jupyter/dashboard/constants.py b/qiskit_ibm_runtime/jupyter/dashboard/constants.py index 5368a9e9b6..f9999cacaf 100644 --- a/qiskit_ibm_runtime/jupyter/dashboard/constants.py +++ b/qiskit_ibm_runtime/jupyter/dashboard/constants.py @@ -14,7 +14,9 @@ STAT_FONT_VALUE = "{}" """Font used for backend stat values.""" -STAT_FONT_VALUE_COLOR = "{msg}" +STAT_FONT_VALUE_COLOR = ( + "{msg}" +) """Font used for backend stat values with color.""" STAT_FONT_TITLE = "{}" """Font used for backend stat titles.""" @@ -22,5 +24,5 @@ RESERVATION_STR = STAT_FONT_VALUE.format("in {start_dt} ({duration}m)") """String used to format reservation information. ``start_dt`` is time until reservation starts. ``duration`` is reservation duration.""" -RESERVATION_NONE = STAT_FONT_VALUE.format('-') +RESERVATION_NONE = STAT_FONT_VALUE.format("-") """String used when there is no reservation.""" diff --git a/qiskit_ibm_runtime/jupyter/dashboard/dashboard.py b/qiskit_ibm_runtime/jupyter/dashboard/dashboard.py index 632204d433..8751c35ad2 100644 --- a/qiskit_ibm_runtime/jupyter/dashboard/dashboard.py +++ b/qiskit_ibm_runtime/jupyter/dashboard/dashboard.py @@ -30,9 +30,7 @@ class AccordionWithThread(wid.Accordion): """An ``Accordion`` that will close an attached thread.""" - def __init__(self, - children: Optional[List] = None, - **kwargs: Any): + def __init__(self, children: Optional[List] = None, **kwargs: Any): """AccordionWithThread constructor. Args: @@ -47,7 +45,7 @@ def __init__(self, def __del__(self): """Object disposal.""" - if hasattr(self, '_thread'): + if hasattr(self, "_thread"): try: self._thread.do_run = False self._thread.join() @@ -56,8 +54,7 @@ def __del__(self): self.close() -def _add_device_to_list(backend: BackendWithProviders, - device_list: wid.VBox) -> None: +def _add_device_to_list(backend: BackendWithProviders, device_list: wid.VBox) -> None: """Add the backend to the device list widget. Args: @@ -90,14 +87,17 @@ def _get_backends(self) -> None: ibm_backends = {} for pro in self.service._get_hgps(): - pro_name = "{hub}/{group}/{project}".format(hub=pro.credentials.hub, - group=pro.credentials.group, - project=pro.credentials.project) + pro_name = "{hub}/{group}/{project}".format( + hub=pro.credentials.hub, + group=pro.credentials.group, + project=pro.credentials.project, + ) for back in pro.backends(): if not back.configuration().simulator: if back.name() not in ibm_backends.keys(): - ibm_backends[back.name()] = \ - BackendWithProviders(backend=back, providers=[pro_name]) + ibm_backends[back.name()] = BackendWithProviders( + backend=back, providers=[pro_name] + ) else: ibm_backends[back.name()].providers.append(pro_name) @@ -109,8 +109,9 @@ def refresh_device_list(self) -> None: _wid.close() self.dashboard._device_list.children = [] for back in self.backend_dict.values(): - _thread = threading.Thread(target=_add_device_to_list, - args=(back, self.dashboard._device_list)) + _thread = threading.Thread( + target=_add_device_to_list, args=(back, self.dashboard._device_list) + ) _thread.start() def start_dashboard(self, service: IBMRuntimeService) -> None: @@ -119,8 +120,9 @@ def start_dashboard(self, service: IBMRuntimeService) -> None: self.dashboard = build_dashboard_widget() self._get_backends() self.refresh_device_list() - self.dashboard._thread = threading.Thread(target=update_backend_info, - args=(self.dashboard._device_list,)) + self.dashboard._thread = threading.Thread( + target=update_backend_info, args=(self.dashboard._device_list,) + ) self.dashboard._thread.do_run = True self.dashboard._thread.start() @@ -139,34 +141,32 @@ def build_dashboard_widget() -> AccordionWithThread: Returns: Dashboard widget. """ - tabs = wid.Tab(layout=wid.Layout(width='760px', - max_height='650px') - ) + tabs = wid.Tab(layout=wid.Layout(width="760px", max_height="650px")) - devices = wid.VBox(children=[], - layout=wid.Layout(width='740px', - height='100%') - ) + devices = wid.VBox(children=[], layout=wid.Layout(width="740px", height="100%")) - device_list = wid.Box(children=[devices], layout=wid.Layout(width='auto', - max_height='600px' - )) + device_list = wid.Box( + children=[devices], layout=wid.Layout(width="auto", max_height="600px") + ) tabs.children = [device_list] - tabs.set_title(0, 'Devices') + tabs.set_title(0, "Devices") - acc = AccordionWithThread(children=[tabs], - layout=wid.Layout(width='auto', - max_height='700px', - )) + acc = AccordionWithThread( + children=[tabs], + layout=wid.Layout( + width="auto", + max_height="700px", + ), + ) acc._device_list = acc.children[0].children[0].children[0] - acc.set_title(0, 'IBM Quantum Dashboard') + acc.set_title(0, "IBM Quantum Dashboard") acc.selected_index = None - acc.layout.visibility = 'hidden' + acc.layout.visibility = "hidden" display(acc) - acc.layout.visibility = 'visible' + acc.layout.visibility = "visible" return acc @@ -175,19 +175,18 @@ class IBMDashboardMagic(Magics): """A class for enabling/disabling the IBM Quantum dashboard.""" @line_magic - def ibm_quantum_dashboard(self, line='', cell=None) -> None: + def ibm_quantum_dashboard(self, line="", cell=None) -> None: """A Jupyter magic function to enable the dashboard.""" # pylint: disable=unused-argument try: service = IBMRuntimeService() except Exception: - raise QiskitError( - "Could not load IBM Quantum account from the local file.") + raise QiskitError("Could not load IBM Quantum account from the local file.") _IBM_DASHBOARD.stop_dashboard() _IBM_DASHBOARD.start_dashboard(service) @line_magic - def disable_ibm_quantum_dashboard(self, line='', cell=None) -> None: + def disable_ibm_quantum_dashboard(self, line="", cell=None) -> None: """A Jupyter magic function to disable the dashboard.""" # pylint: disable=unused-argument _IBM_DASHBOARD.stop_dashboard() diff --git a/qiskit_ibm_runtime/jupyter/dashboard/provider_buttons.py b/qiskit_ibm_runtime/jupyter/dashboard/provider_buttons.py index 74dcec5582..87160e172b 100644 --- a/qiskit_ibm_runtime/jupyter/dashboard/provider_buttons.py +++ b/qiskit_ibm_runtime/jupyter/dashboard/provider_buttons.py @@ -28,10 +28,13 @@ def _copy_text_thread(button: vue.Btn) -> None: button: Button whose text is to be copied. """ old_text = button.children[0] - hub, group, project = old_text.split('/') - pyperclip.copy(f"IBMRuntimeService(hub='{hub}', group='{group}', project='{project}')" - .format(hub=hub, group=group, project=project)) - button.children = ['Copied to clipboard.'] + hub, group, project = old_text.split("/") + pyperclip.copy( + f"IBMRuntimeService(hub='{hub}', group='{group}', project='{project}')".format( + hub=hub, group=group, project=project + ) + ) + button.children = ["Copied to clipboard."] time.sleep(1) button.children = [old_text] @@ -56,16 +59,21 @@ def provider_buttons(providers: List[str]) -> wid.VBox: """ vbox_buttons = [] for pro in providers: - button = wid.Box(children=[vue.Btn(color='#f5f5f5', small=True, - children=[pro], - style_="font-family: Arial," - "sans-serif; font-size:10px;")], - layout=wid.Layout(margin="0px 0px 2px 0px", - width='350px')) + button = wid.Box( + children=[ + vue.Btn( + color="#f5f5f5", + small=True, + children=[pro], + style_="font-family: Arial," "sans-serif; font-size:10px;", + ) + ], + layout=wid.Layout(margin="0px 0px 2px 0px", width="350px"), + ) - button.children[0].on_event('click', _copy_text) + button.children[0].on_event("click", _copy_text) vbox_buttons.append(button) - return wid.VBox(children=vbox_buttons, - layout=wid.Layout(width='350px', - max_width='350px')) + return wid.VBox( + children=vbox_buttons, layout=wid.Layout(width="350px", max_width="350px") + ) diff --git a/qiskit_ibm_runtime/jupyter/dashboard/utils.py b/qiskit_ibm_runtime/jupyter/dashboard/utils.py index 3cd5975f71..3e65eddad5 100644 --- a/qiskit_ibm_runtime/jupyter/dashboard/utils.py +++ b/qiskit_ibm_runtime/jupyter/dashboard/utils.py @@ -14,5 +14,5 @@ from collections import namedtuple -BackendWithProviders = namedtuple('BackendWithProviders', ['backend', 'providers']) +BackendWithProviders = namedtuple("BackendWithProviders", ["backend", "providers"]) """Named tuple used to pass a backend and its providers.""" diff --git a/qiskit_ibm_runtime/jupyter/gates_widget.py b/qiskit_ibm_runtime/jupyter/gates_widget.py index 9f1b1f0cea..af0defb612 100644 --- a/qiskit_ibm_runtime/jupyter/gates_widget.py +++ b/qiskit_ibm_runtime/jupyter/gates_widget.py @@ -31,17 +31,15 @@ def gates_tab(backend: Union[IBMBackend, FakeBackend]) -> wid.GridBox: A widget with gate information. """ props = backend.properties().to_dict() - update_date = props['last_update_date'] + update_date = props["last_update_date"] date_str = update_date.strftime("%a %d %B %Y at %H:%M %Z") - multi_qubit_gates = [g for g in props['gates'] if len(g['qubits']) > 1] + multi_qubit_gates = [g for g in props["gates"] if len(g["qubits"]) > 1] header_html = "
{key}: {value}
" - header_html = header_html.format(key='last_update_date', - value=date_str) + header_html = header_html.format(key="last_update_date", value=date_str) - update_date_widget = wid.HTML(value=header_html, - layout=wid.Layout(grid_area='top')) + update_date_widget = wid.HTML(value=header_html, layout=wid.Layout(grid_area="top")) gate_html = "
%s%s
%s%s
" gate_html += """