diff --git a/docs/apidocs/ibm-provider.rst b/docs/apidocs/ibm-provider.rst index 0964685c6..51c647b18 100644 --- a/docs/apidocs/ibm-provider.rst +++ b/docs/apidocs/ibm-provider.rst @@ -12,4 +12,3 @@ Qiskit IBM Quantum Provider API Reference ibm_jupyter ibm_utils ibm_random - ibm_runtime diff --git a/docs/apidocs/ibm_runtime.rst b/docs/apidocs/ibm_runtime.rst deleted file mode 100644 index 1a6de0b9c..000000000 --- a/docs/apidocs/ibm_runtime.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit_ibm-runtime: - -.. automodule:: qiskit_ibm.runtime - :no-members: - :no-inherited-members: - :no-special-members: diff --git a/qiskit_ibm/__init__.py b/qiskit_ibm/__init__.py index bbaa64b75..46c1fc62c 100644 --- a/qiskit_ibm/__init__.py +++ b/qiskit_ibm/__init__.py @@ -57,7 +57,6 @@ BackendJobLimit IBMBackend IBMBackendService - RunnerResult Exceptions ========== @@ -91,7 +90,6 @@ from .exceptions import * from .ibm_backend_service import IBMBackendService from .utils.utils import setup_logger -from .runner_result import RunnerResult from .version import __version__ # Setup the logger for the IBM Quantum Provider package. diff --git a/qiskit_ibm/api/clients/__init__.py b/qiskit_ibm/api/clients/__init__.py index d66469ec5..2c3185562 100644 --- a/qiskit_ibm/api/clients/__init__.py +++ b/qiskit_ibm/api/clients/__init__.py @@ -17,5 +17,3 @@ from .auth import AuthClient from .version import VersionClient from .websocket import WebsocketClient -from .runtime import RuntimeClient -from .runtime_ws import RuntimeWebsocketClient diff --git a/qiskit_ibm/api/clients/runtime.py b/qiskit_ibm/api/clients/runtime.py deleted file mode 100644 index 5cc4df6b5..000000000 --- a/qiskit_ibm/api/clients/runtime.py +++ /dev/null @@ -1,249 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Client for accessing IBM Quantum runtime service.""" - -import logging -from typing import Any, List, Dict, Union, Optional - -from qiskit_ibm.credentials import Credentials -from qiskit_ibm.api.session import RetrySession - -from ..rest.runtime import Runtime - -logger = logging.getLogger(__name__) - - -class RuntimeClient: - """Client for accessing runtime service.""" - - def __init__( - self, - credentials: Credentials, - ) -> None: - """RandomClient constructor. - - Args: - credentials: Account credentials. - """ - 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]: - """Return a list of runtime programs. - - Args: - limit: The number of programs to return. - skip: The number of programs to skip. - - Returns: - A list of runtime programs. - """ - 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 - ) -> Dict: - """Create a new program. - - Args: - name: Name of the program. - program_data: Program data (base64 encoded). - description: Program description. - max_execution_time: Maximum execution time. - is_public: Whether the program should be public. - spec: Backend requirements, parameters, interim results, return values, etc. - - Returns: - Server response. - """ - 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 - ) - - def program_get(self, program_id: str) -> Dict: - """Return a specific program. - - Args: - program_id: Program ID. - - Returns: - Program information. - """ - return self.api.program(program_id).get() - - def set_program_visibility(self, program_id: str, public: bool) -> None: - """Sets a program's visibility. - - Args: - program_id: Program ID. - public: If ``True``, make the program visible to all. - If ``False``, make the program visible to just your account. - - """ - if public: - self.api.program(program_id).make_public() - else: - self.api.program(program_id).make_private() - - def program_run( - self, - program_id: str, - credentials: Credentials, - backend_name: str, - params: Dict, - image: str - ) -> Dict: - """Run the specified program. - - Args: - program_id: Program ID. - credentials: Credentials used to run the program. - backend_name: Name of the backend to run the program. - params: Parameters to use. - image: The runtime image to use. - - 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) - - def program_delete(self, program_id: str) -> None: - """Delete the specified program. - - Args: - program_id: Program ID. - """ - 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 - ) -> None: - """Update a program. - - Args: - program_id: Program ID. - program_data: Program data (base64 encoded). - name: Name of the program. - description: Program description. - max_execution_time: Maximum execution time. - spec: Backend requirements, parameters, interim results, return values, etc. - """ - if program_data: - self.api.program(program_id).update_data(program_data) - - 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) - - def job_get(self, job_id: str) -> Dict: - """Get job data. - - Args: - job_id: Job ID. - - Returns: - JSON response. - """ - response = self.api.program_job(job_id).get() - logger.debug("Runtime job get response: %s", response) - 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 - ) -> Dict: - """Get job data for all jobs. - - Args: - limit: Number of results to return. - skip: Number of results to skip. - pending: Returns 'QUEUED' and 'RUNNING' jobs if True, - returns 'DONE', 'CANCELLED' and 'ERROR' jobs if False. - program_id: Filter by Program ID. - hub: Filter by hub - hub, group, and project must all be specified. - group: Filter by group - hub, group, and project must all be specified. - project: Filter by project - hub, group, and project must all be specified. - - Returns: - JSON response. - """ - 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. - - Args: - job_id: Program job ID. - - Returns: - Job result. - """ - return self.api.program_job(job_id).results() - - def job_cancel(self, job_id: str) -> None: - """Cancel a job. - - Args: - job_id: Runtime job ID. - """ - self.api.program_job(job_id).cancel() - - def job_delete(self, job_id: str) -> None: - """Delete a job. - - Args: - job_id: Runtime job ID. - """ - self.api.program_job(job_id).delete() - - def job_logs(self, job_id: str) -> str: - """Get the job logs. - - Args: - job_id: Program job ID. - - Returns: - Job logs. - """ - return self.api.program_job(job_id).logs() - - def logout(self) -> None: - """Clear authorization cache.""" - self.api.logout() diff --git a/qiskit_ibm/api/clients/runtime_ws.py b/qiskit_ibm/api/clients/runtime_ws.py deleted file mode 100644 index f1b9c9cc1..000000000 --- a/qiskit_ibm/api/clients/runtime_ws.py +++ /dev/null @@ -1,78 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Client for accessing IBM Quantum runtime service.""" - -import logging -from typing import Optional -from queue import Queue - -from ...credentials import Credentials -from .base import BaseWebsocketClient - -logger = logging.getLogger(__name__) - - -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 - ) -> None: - """WebsocketClient constructor. - - Args: - websocket_url: URL for websocket communication with IBM Quantum. - credentials: Account credentials. - job_id: Job ID. - message_queue: Queue used to hold received messages. - """ - super().__init__(websocket_url, credentials, job_id, message_queue) - self._header = {"X-Access-Token": credentials.access_token} - - def _handle_message(self, message: str) -> None: - """Handle received message. - - Args: - message: Message received. - """ - if not self._authenticated: - self._authenticated = True # First message is an ACK - else: - self._message_queue.put_nowait(message) - self._current_retry = 0 - - def job_results( - self, - max_retries: int = 5, - backoff_factor: float = 0.5 - ) -> None: - """Return the interim result of a runtime job. - - Args: - max_retries: Max number of retries. - backoff_factor: Backoff factor used to calculate the - time to wait between retries. - - Raises: - WebsocketError: If a websocket error occurred. - """ - url = '{}/stream/jobs/{}'.format(self._websocket_url, self._job_id) - self.stream(url=url, retries=max_retries, backoff_factor=backoff_factor) - - def _handle_stream_iteration(self) -> None: - """Handle a streaming iteration.""" - pass diff --git a/qiskit_ibm/api/rest/runtime.py b/qiskit_ibm/api/rest/runtime.py deleted file mode 100644 index e44265c96..000000000 --- a/qiskit_ibm/api/rest/runtime.py +++ /dev/null @@ -1,338 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Runtime REST adapter.""" - -import logging -from typing import Dict, List, Any, Union, Optional -import json -from concurrent import futures - -from .base import RestAdapterBase -from ..session import RetrySession -from ...runtime.utils import RuntimeEncoder - -logger = logging.getLogger(__name__) - - -class Runtime(RestAdapterBase): - """Rest adapter for Runtime base endpoints.""" - - URL_MAP = { - 'programs': '/programs', - 'jobs': '/jobs', - 'logout': '/logout' - } - - def program(self, program_id: str) -> 'Program': - """Return an adapter for the program. - - Args: - program_id: ID of the program. - - Returns: - The program adapter. - """ - return Program(self.session, program_id) - - def program_job(self, job_id: str) -> 'ProgramJob': - """Return an adapter for the job. - - Args: - job_id: Job ID. - - Returns: - The program job adapter. - """ - return ProgramJob(self.session, job_id) - - def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: - """Return a list of runtime programs. - - Args: - limit: The number of programs to return. - skip: The number of programs to skip. - - Returns: - A list of runtime programs. - """ - url = self.get_url('programs') - payload: Dict[str, int] = {} - if limit: - payload['limit'] = limit - if 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 - ) -> Dict: - """Upload a new program. - - Args: - program_data: Program data (base64 encoded). - name: Name of the program. - description: Program description. - max_execution_time: Maximum execution time. - is_public: Whether the program should be public. - spec: Backend requirements, parameters, interim results, return values, etc. - - Returns: - JSON response. - """ - 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 - 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 - ) -> Dict: - """Execute the program. - - Args: - program_id: Program ID. - hub: Hub to be used. - group: Group to be used. - project: Project to be used. - backend_name: Name of the backend. - params: Program parameters. - image: Runtime image. - - Returns: - JSON response. - """ - url = self.get_url('jobs') - payload = { - '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 - ) -> Dict: - """Get a list of job data. - - Args: - limit: Number of results to return. - skip: Number of results to skip. - pending: Returns 'QUEUED' and 'RUNNING' jobs if True, - returns 'DONE', 'CANCELLED' and 'ERROR' jobs if False. - program_id: Filter by Program ID. - hub: Filter by hub - hub, group, and project must all be specified. - group: Filter by group - hub, group, and project must all be specified. - project: Filter by project - hub, group, and project must all be specified. - - Returns: - JSON response. - """ - url = self.get_url('jobs') - payload: Dict[str, Union[int, str]] = {} - if limit: - payload['limit'] = limit - if skip: - payload['offset'] = skip - if pending is not None: - payload['pending'] = 'true' if pending else 'false' - if program_id: - payload['program'] = program_id - if all([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') - self.session.post(url) - - -class Program(RestAdapterBase): - """Rest adapter for program related endpoints.""" - - URL_MAP = { - 'self': '', - 'data': '/data', - 'run': '/jobs', - 'private': '/private', - 'public': '/public' - } - - _executor = futures.ThreadPoolExecutor() - - def __init__(self, session: RetrySession, program_id: str, url_prefix: str = '') -> None: - """Job constructor. - - Args: - session: Session to be used in the adapter. - program_id: ID of the runtime program. - url_prefix: Prefix to use in the URL. - """ - super().__init__(session, '{}/programs/{}'.format(url_prefix, program_id)) - - def get(self) -> Dict[str, Any]: - """Return program information. - - Returns: - JSON response. - """ - 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') - self.session.put(url) - - def make_private(self) -> None: - """Sets a runtime program's visibility to private.""" - url = self.get_url('private') - self.session.put(url) - - def delete(self) -> None: - """Delete this program. - - Returns: - JSON response. - """ - url = self.get_url('self') - self.session.delete(url) - - def update_data(self, program_data: str) -> None: - """Update program data. - - Args: - program_data: Program data (base64 encoded). - """ - url = self.get_url("data") - 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 - ) -> None: - """Update program metadata. - - Args: - name: Name of the program. - description: Program description. - max_execution_time: Maximum execution time. - spec: Backend requirements, parameters, interim results, return values, etc. - """ - url = self.get_url("self") - payload: Dict = {} - if name: - payload["name"] = name - if description: - payload["description"] = description - if max_execution_time: - payload["cost"] = max_execution_time - if spec: - payload["spec"] = spec - - self.session.patch(url, json=payload) - - -class ProgramJob(RestAdapterBase): - """Rest adapter for program job related endpoints.""" - - URL_MAP = { - 'self': '', - 'results': '/results', - 'cancel': '/cancel', - 'logs': '/logs' - } - - def __init__( - self, - session: RetrySession, - job_id: str, - url_prefix: str = '' - ) -> None: - """ProgramJob constructor. - - Args: - session: Session to be used in the adapter. - job_id: ID of the program job. - url_prefix: Prefix to use in the URL. - """ - super().__init__(session, '{}/jobs/{}'.format( - url_prefix, job_id)) - - def get(self) -> Dict: - """Return program job information. - - Returns: - JSON response. - """ - return self.session.get(self.get_url('self')).json() - - def delete(self) -> None: - """Delete program job.""" - self.session.delete(self.get_url('self')) - - def results(self) -> str: - """Return program job results. - - Returns: - Job results. - """ - response = self.session.get(self.get_url('results')) - return response.text - - def cancel(self) -> None: - """Cancel the job.""" - self.session.post(self.get_url('cancel')) - - def logs(self) -> str: - """Retrieve job logs. - - Returns: - Job logs. - """ - return self.session.get(self.get_url('logs')).text diff --git a/qiskit_ibm/credentials/credentials.py b/qiskit_ibm/credentials/credentials.py index f6f6faae4..ec1823e22 100644 --- a/qiskit_ibm/credentials/credentials.py +++ b/qiskit_ibm/credentials/credentials.py @@ -88,7 +88,6 @@ def __init__( services = services or {} 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.""" diff --git a/qiskit_ibm/hub_group_project.py b/qiskit_ibm/hub_group_project.py index dc5b6fab1..10486f879 100644 --- a/qiskit_ibm/hub_group_project.py +++ b/qiskit_ibm/hub_group_project.py @@ -56,8 +56,7 @@ def __init__( self._service_urls = { 'backend': self.credentials.url, 'experiment': self.credentials.experiment_url, - 'random': self.credentials.extractor_url, - 'runtime': self.credentials.runtime_url + 'random': self.credentials.extractor_url } @property diff --git a/qiskit_ibm/ibm_provider.py b/qiskit_ibm/ibm_provider.py index 1afc3e68c..e2e6856cd 100644 --- a/qiskit_ibm/ibm_provider.py +++ b/qiskit_ibm/ibm_provider.py @@ -13,36 +13,29 @@ """Provider for a single IBM Quantum account.""" import logging -from typing import Dict, List, Optional, Any, Callable, Tuple, Union -from collections import OrderedDict -import traceback -import copy import os +import traceback +from collections import OrderedDict +from typing import Dict, List, Optional, Any, Callable, Tuple, Union from qiskit.providers import ProviderV1 as Provider # type: ignore[attr-defined] -from qiskit.circuit import QuantumCircuit from qiskit.providers.backend import BackendV1 as Backend from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit.transpiler import Layout - -from qiskit_ibm.runtime import runtime_job # pylint: disable=unused-import from .api.clients import AuthClient, VersionClient from .apiconstants import QISKIT_IBM_API_URL -from .ibm_backend import IBMBackend # pylint: disable=cyclic-import from .credentials import Credentials, HubGroupProjectID, discover_credentials from .credentials.configrc import (remove_credentials, read_credentials_from_qiskitrc, store_credentials) from .credentials.exceptions import HubGroupProjectIDInvalidStateError -from .hub_group_project import HubGroupProject # pylint: disable=cyclic-import -from .ibm_backend_service import IBMBackendService # pylint: disable=cyclic-import -from .random.ibm_random_service import IBMRandomService # pylint: disable=cyclic-import -from .runtime.ibm_runtime_service import IBMRuntimeService # pylint: disable=cyclic-import from .exceptions import (IBMNotAuthorizedError, IBMInputValueError, IBMProviderCredentialsNotFound, IBMProviderCredentialsInvalidFormat, IBMProviderCredentialsInvalidToken, IBMProviderCredentialsInvalidUrl, IBMProviderError, IBMProviderValueError, IBMProviderMultipleCredentialsFound) -from .runner_result import RunnerResult # pylint: disable=cyclic-import +from .hub_group_project import HubGroupProject # pylint: disable=cyclic-import +from .ibm_backend import IBMBackend # pylint: disable=cyclic-import +from .ibm_backend_service import IBMBackendService # pylint: disable=cyclic-import +from .random.ibm_random_service import IBMRandomService # pylint: disable=cyclic-import logger = logging.getLogger(__name__) @@ -85,7 +78,7 @@ class IBMProvider(Provider): in decreasing order of priority. * The hub/group/project you explicity specify when calling a service. - Ex: `provider.get_backend()`, `provider.runtime.run()`, etc. + Ex: `provider.get_backend()`, etc. * The hub/group/project required for the service. * The default hub/group/project you set using `save_account()`. * A premium hub/group/project in your account. @@ -146,8 +139,7 @@ def __init__( * verify (bool): verify the server's TLS certificate. Returns: - An instance of IBMProvider with services like :class:`~qiskit_ibm.IBMBackendService`, - :class:`~qiskit_ibm.runtime.IBMRuntimeService`, + An instance of IBMProvider with services like :class:`~qiskit_ibm.IBMBackendService` and :class:`~qiskit_ibm.random.IBMRandomService` as available to the account. @@ -390,7 +382,6 @@ def _initialize_services(self) -> None: """Initialize all services.""" self._backend = None self._random = None - self._runtime = None hgps = self._get_hgps() for hgp in hgps: # Initialize backend service @@ -400,14 +391,10 @@ def _initialize_services(self) -> None: if not self._random: self._random = IBMRandomService(self, hgp) \ if hgp.has_service('random') else None - if not self._runtime: - self._runtime = IBMRuntimeService(self, hgp) \ - if hgp.has_service('runtime') else None - if all([self._backend, self._random, self._runtime]): + if all([self._backend, self._random]): break self._services = {'backend': self._backend, - 'random': self._random, - 'runtime': self._runtime} + 'random': self._random} @property def backend(self) -> IBMBackendService: @@ -434,21 +421,6 @@ def random(self) -> IBMRandomService: else: raise IBMNotAuthorizedError("You are not authorized to use the service.") - @property - def runtime(self) -> IBMRuntimeService: - """Return the runtime service. - - Returns: - The runtime service instance. - - Raises: - IBMNotAuthorizedError: If the account is not authorized to use the service. - """ - if self._runtime: - return self._runtime - else: - raise IBMNotAuthorizedError("You are not authorized to use the runtime service.") - def active_account(self) -> Optional[Dict[str, str]]: """Return the IBM Quantum account currently in use for the session. @@ -655,117 +627,6 @@ def has_service(self, name: str) -> bool: return False return True - 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': - """Execute the input circuit(s) on a backend using the runtime service. - - Note: - This method uses the IBM Quantum runtime service which is not - available to all accounts. - - Args: - circuits: Circuit(s) to execute. - - backend_name: Name of the backend to execute circuits on. - Transpiler options are automatically grabbed from backend configuration - and properties unless otherwise specified. - - shots: Number of repetitions of each circuit, for sampling. If not specified, - the backend default is used. - - initial_layout: Initial position of virtual qubits on physical qubits. - - layout_method: Name of layout selection pass ('trivial', 'dense', - 'noise_adaptive', 'sabre'). - Sometimes a perfect layout can be available in which case the layout_method - may not run. - - routing_method: Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre') - - translation_method: Name of translation pass ('unroller', 'translator', 'synthesis') - - seed_transpiler: Sets random seed for the stochastic parts of the transpiler. - - optimization_level: How much optimization to perform on the circuits. - Higher levels generate more optimized circuits, at the expense of longer - transpilation time. - If None, level 1 will be chosen as default. - - init_qubits: Whether to reset the qubits to the ground state for each shot. - - rep_delay: Delay between programs in seconds. Only supported on certain - backends (``backend.configuration().dynamic_reprate_enabled`` ). If supported, - ``rep_delay`` will be used instead of ``rep_time`` and must be from the - range supplied by the backend (``backend.configuration().rep_delay_range``). - Default is given by ``backend.configuration().default_rep_delay``. - - transpiler_options: Additional transpiler options. - - measurement_error_mitigation: Whether to apply measurement error mitigation. - - use_measure_esp: Whether to use excited state promoted (ESP) readout for measurements - which are the final instruction on a qubit. ESP readout can offer higher fidelity - than standard measurement sequences. See - `here `_. - - hub: Name of the hub. - - group: Name of the group. - - project: Name of the project. - - **run_config: Extra arguments used to configure the circuit execution. - - Returns: - 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 - if shots: - inputs['shots'] = shots - if initial_layout: - inputs['initial_layout'] = initial_layout - if layout_method: - inputs['layout_method'] = layout_method - if routing_method: - inputs['routing_method'] = routing_method - if translation_method: - inputs['translation_method'] = translation_method - if seed_transpiler: - inputs['seed_transpiler'] = seed_transpiler - if rep_delay: - inputs['rep_delay'] = rep_delay - if 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.runtime.run('circuit-runner', options=options, inputs=inputs, - result_decoder=RunnerResult, - hub=hub, group=group, project=project) - def service(self, name: str) -> Any: """Return the specified service. diff --git a/qiskit_ibm/runner_result.py b/qiskit_ibm/runner_result.py deleted file mode 100644 index 84155a253..000000000 --- a/qiskit_ibm/runner_result.py +++ /dev/null @@ -1,73 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Circuit-runner result class""" - -from typing import List, Union -import json - -from qiskit.result import Result, QuasiDistribution -from qiskit.result.postprocess import _hex_to_bin -from qiskit.exceptions import QiskitError - -from .runtime import ResultDecoder - - -class RunnerResult(Result, ResultDecoder): - """Result class for Qiskit Runtime program circuit-runner.""" - - @classmethod - def decode(cls, data: str) -> 'RunnerResult': - """Decoding for results from Qiskit runtime jobs.""" - return cls.from_dict(json.loads(data)) - - def get_quasiprobabilities( - self, - experiment: Union[int, List] = None - ) -> Union[QuasiDistribution, List[QuasiDistribution]]: - """Get quasiprobabilites associated with one or more experiments. - - Parameters: - experiment: Indices of experiments to grab quasiprobabilities from. - - Returns: - A single distribution or a list of distributions. - - Raises: - QiskitError: If experiment result doesn't contain quasiprobabilities. - """ - if experiment is None: - exp_keys = range(len(self.results)) - else: - exp_keys = [experiment] # type: ignore[assignment] - - dict_list = [] - for key in exp_keys: - if 'quasiprobabilities' in self.data(key).keys(): - shots = self.results[key].shots - hex_quasi = self.results[key].data.quasiprobabilities - bit_lenth = len(self.results[key].header.final_measurement_mapping) - quasi = {} - for hkey, val in hex_quasi.items(): - quasi[_hex_to_bin(hkey).zfill(bit_lenth)] = val - - out = QuasiDistribution(quasi, shots) - out.shots = shots - dict_list.append(out) - else: - raise QiskitError('No quasiprobabilities for experiment "{}"'.format(repr(key))) - - # Return first item of dict_list if size is 1 - if len(dict_list) == 1: - return dict_list[0] - else: - return dict_list diff --git a/qiskit_ibm/runtime/__init__.py b/qiskit_ibm/runtime/__init__.py deleted file mode 100644 index 7b8fc84aa..000000000 --- a/qiskit_ibm/runtime/__init__.py +++ /dev/null @@ -1,229 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -""" -============================================== -Runtime (:mod:`qiskit_ibm.runtime`) -============================================== - -.. currentmodule:: qiskit_ibm.runtime - -Modules related to Qiskit Runtime Service. - -.. note:: - - The Qiskit Runtime service is not available to all providers. To check if your provider - has access:: - - from qiskit_ibm import IBMProvider - - provider = IBMProvider() - can_use_runtime = provider.has_service('runtime') - -.. note:: - - Not all backends support Qiskit Runtime. Refer to documentation in - `Qiskit-Partners/qiskit-runtime - `_ for more information. - -.. caution:: - - This package is currently provided in beta form and heavy modifications to - both functionality and API are likely to occur. Backward compatibility is not - always guaranteed. - -Qiskit Runtime is a new architecture offered by IBM Quantum that -streamlines computations requiring many iterations. These experiments will -execute significantly faster within its improved hybrid quantum/classical process. - -The Qiskit Runtime Service allows authorized users to upload their Qiskit quantum programs. -A Qiskit quantum program, also called a runtime program, is a piece of Python -code and its metadata that takes certain inputs, performs -quantum and maybe classical processing, and returns the results. The same or other -authorized users can invoke these quantum programs by simply passing in parameters. - -`Qiskit-Partners/qiskit-runtime `_ -contains detailed tutorials on how to use Qiskit Runtime. - - -Listing runtime programs ------------------------- - -To list all available runtime programs:: - - from qiskit_ibm import IBMProvider - - provider = IBMProvider() - - # List all available programs. - provider.runtime.pprint_programs() - - # Get a single program. - program = provider.runtime.program('circuit-runner') - - # Print program metadata. - print(program) - -In the example above, ``provider.runtime`` points to the runtime service class -:class:`IBMRuntimeService`, which is the main entry -point for using this service. The example prints the program metadata of all -available runtime programs and of just the ``circuit-runner`` program. A program -metadata consists of the program's ID, name, description, input parameters, -return values, interim results, and other information that helps you to know -more about the program. - -Invoking a runtime program --------------------------- - -You can use the :meth:`IBMRuntimeService.run` method to invoke a runtime program. -For example:: - - from qiskit import QuantumCircuit - from qiskit_ibm import IBMProvider, RunnerResult - - provider = IBMProvider() - backend = provider.backend.ibmq_qasm_simulator - - # Create a circuit. - qc = QuantumCircuit(2, 2) - qc.h(0) - qc.cx(0, 1) - qc.measure_all() - - # Set the "circuit-runner" program parameters - params = provider.runtime.program(program_id="circuit-runner").parameters() - params.circuits = qc - params.measurement_error_mitigation = True - - # Configure backend options - options = {'backend_name': backend.name()} - - # Execute the circuit using the "circuit-runner" program. - job = provider.runtime.run(program_id="circuit-runner", - options=options, - inputs=params) - - # Get runtime job result. - result = job.result(decoder=RunnerResult) - -The example above invokes the ``circuit-runner`` program, -which compiles, executes, and optionally applies measurement error mitigation to -the circuit result. - -Runtime Jobs ------------- - -When you use the :meth:`IBMRuntimeService.run` method to invoke a runtime -program, a -:class:`RuntimeJob` instance is returned. This class has all the basic job -methods, such as :meth:`RuntimeJob.status`, :meth:`RuntimeJob.result`, and -:meth:`RuntimeJob.cancel`. Note that it does not have the same methods as regular -circuit jobs, which are instances of :class:`~qiskit_ibm.job.IBMJob`. - -Interim results ---------------- - -Some runtime programs provide interim results that inform you about program -progress. You can choose to stream the interim results when you run the -program by passing in the ``callback`` parameter, or at a later time using -the :meth:`RuntimeJob.stream_results` method. For example:: - - from qiskit import QuantumCircuit - from qiskit_ibm import IBMProvider - - provider = IBMProvider() - backend = provider.backend.ibmq_qasm_simulator - - def interim_result_callback(job_id, interim_result): - print(interim_result) - - # Stream interim results as soon as the job starts running. - job = provider.runtime.run(program_id="circuit-runner", - options=options, - inputs=program_inputs, - callback=interim_result_callback) - -Uploading a program -------------------- - -.. note:: - - Only authorized accounts can upload programs. Having access to the - runtime service doesn't imply access to upload programs. - -Each runtime program has both ``data`` and ``metadata``. Program data is -the Python code to be executed. Program metadata provides usage information, -such as program description, its inputs and outputs, and backend requirements. -A detailed program metadata helps the consumers of the program to know what is -needed to run the program. - -Each program data needs to have a ``main(backend, user_messenger, **kwargs)`` -method, which serves as the entry point to the program. The ``backend`` parameter -is a :class:`ProgramBackend` instance whose :meth:`ProgramBackend.run` method -can be used to submit circuits. The ``user_messenger`` is a :class:`UserMessenger` -instance whose :meth:`UserMessenger.publish` method can be used to publish interim and -final results. -See `qiskit_ibm/runtime/program/program_template.py` for a program data -template file. - -Each program metadata must include at least the program name, description, and -maximum execution time. You can find description of each metadata field in -the :meth:`IBMRuntimeService.upload_program` method. Instead of passing in -the metadata fields individually, you can pass in a JSON file or a dictionary -to :meth:`IBMRuntimeService.upload_program` via the ``metadata`` parameter. -`qiskit_ibm/runtime/program/program_metadata_sample.json` -is a sample file of program metadata. - -You can use the :meth:`IBMRuntimeService.upload_program` to upload a program. -For example:: - - from qiskit_ibm import IBMProvider - - provider = IBMProvider() - program_id = provider.runtime.upload_program( - data="my_vqe.py", - metadata="my_vqe_metadata.json" - ) - -In the example above, the file ``my_vqe.py`` contains the program data, and -``my_vqe_metadata.json`` contains the program metadata. - -Method :meth:`IBMRuntimeService.delete_program` allows you to delete a -program. - -Files related to writing a runtime program are in the -``qiskit_ibm/runtime/program`` directory. - - -Classes -========================== -.. autosummary:: - :toctree: ../stubs/ - - IBMRuntimeService - RuntimeJob - RuntimeProgram - UserMessenger - ProgramBackend - ResultDecoder - RuntimeEncoder - RuntimeDecoder - ParameterNamespace -""" - -from .ibm_runtime_service import IBMRuntimeService -from .runtime_job import RuntimeJob -from .runtime_program import RuntimeProgram, ParameterNamespace -from .program.user_messenger import UserMessenger -from .program.program_backend import ProgramBackend -from .program.result_decoder import ResultDecoder -from .utils import RuntimeEncoder, RuntimeDecoder diff --git a/qiskit_ibm/runtime/constants.py b/qiskit_ibm/runtime/constants.py deleted file mode 100644 index 7f6ecf8c4..000000000 --- a/qiskit_ibm/runtime/constants.py +++ /dev/null @@ -1,31 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Constant values.""" - -from qiskit.providers.jobstatus import JobStatus - - -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 -} - -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{}' -} diff --git a/qiskit_ibm/runtime/exceptions.py b/qiskit_ibm/runtime/exceptions.py deleted file mode 100644 index abd5e3d11..000000000 --- a/qiskit_ibm/runtime/exceptions.py +++ /dev/null @@ -1,46 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Exceptions related to IBM Quantum runtime service.""" - - -from ..exceptions import IBMError - - -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/ibm_runtime_service.py b/qiskit_ibm/runtime/ibm_runtime_service.py deleted file mode 100644 index fda4b449e..000000000 --- a/qiskit_ibm/runtime/ibm_runtime_service.py +++ /dev/null @@ -1,708 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Qiskit runtime service.""" - -import logging -from typing import Dict, Callable, Optional, Union, List, Any, Type -import json -import re -import warnings - -from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit_ibm import ibm_provider # pylint: disable=unused-import - -from .runtime_job import RuntimeJob -from .runtime_program import RuntimeProgram, ParameterNamespace -from .utils import RuntimeDecoder, to_base64_string -from .exceptions import (QiskitRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, - RuntimeJobNotFound) -from .program.result_decoder import ResultDecoder -from ..api.clients.runtime import RuntimeClient - -from ..api.exceptions import RequestsApiError -from ..hub_group_project import HubGroupProject -from ..exceptions import IBMNotAuthorizedError, IBMInputValueError, IBMProviderError -from ..ibm_backend import IBMRetiredBackend -from ..credentials import Credentials - -logger = logging.getLogger(__name__) - -SERVICE_NAME = 'runtime' - - -class IBMRuntimeService: - """Class for interacting with the Qiskit Runtime service. - - Qiskit Runtime is a new architecture offered by IBM Quantum that - streamlines computations requiring many iterations. These experiments will - execute significantly faster within its improved hybrid quantum/classical - process. - - The Qiskit Runtime Service allows authorized users to upload their Qiskit - quantum programs. A Qiskit quantum program, also called a runtime program, - is a piece of Python code and its metadata that takes certain inputs, performs - quantum and maybe classical processing, and returns the results. The same or other - authorized users can invoke these quantum programs by simply passing in parameters. - - A sample workflow of using the runtime service:: - - from qiskit import QuantumCircuit - from qiskit_ibm import IBMProvider, RunnerResult - - provider = IBMProvider() - backend = provider.backend.ibmq_qasm_simulator - - # List all available programs. - provider.runtime.pprint_programs() - - # Create a circuit. - qc = QuantumCircuit(2, 2) - qc.h(0) - qc.cx(0, 1) - qc.measure_all() - - # Set the "circuit-runner" program parameters - params = provider.runtime.program(program_id="circuit-runner").parameters() - params.circuits = qc - params.measurement_error_mitigation = True - - # Configure backend options - options = {'backend_name': backend.name()} - - # Execute the circuit using the "circuit-runner" program. - job = provider.runtime.run(program_id="circuit-runner", - options=options, - inputs=params) - - # Get runtime job result. - result = job.result(decoder=RunnerResult) - - If the program has any interim results, you can use the ``callback`` - parameter of the :meth:`run` method to stream the interim results. - Alternatively, you can use the :meth:`RuntimeJob.stream_results` method to stream - the results at a later time, but before the job finishes. - - The :meth:`run` method returns a - :class:`~qiskit_ibm.runtime.RuntimeJob` object. You can use its - methods to perform tasks like checking job status, getting job result, and - canceling job. - """ - - def __init__(self, provider: 'ibm_provider.IBMProvider', hgp: HubGroupProject) -> None: - """IBMRuntimeService constructor. - - Args: - provider: IBM Quantum account provider. - hgp: default hub/group/project to use for the service. - """ - self._provider = provider - 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._programs = {} # type: Dict - - 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: - refresh: If ``True``, re-query the server for the programs. Otherwise - return the cached value. - detailed: If ``True`` print all details about available runtime programs. - limit: The number of programs returned at a time. Default and maximum - value of 20. - skip: The number of programs to skip. - """ - programs = self.programs(refresh, limit, skip) - for prog in programs: - print("="*50) - if detailed: - print(str(prog)) - else: - 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]: - """Return available runtime programs. - - Currently only program metadata is returned. - - Args: - refresh: If ``True``, re-query the server for the programs. Otherwise - return the cached value. - limit: The number of programs returned at a time. ``None`` means no limit. - skip: The number of programs to skip. - - Returns: - A list of runtime programs. - """ - if skip is None: - skip = 0 - if not self._programs or refresh: - self._programs = {} - current_page_limit = 20 - offset = 0 - while True: - 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 - count = response.get("count", 0) - for prog_dict in program_page: - program = self._to_program(prog_dict) - self._programs[program.program_id] = program - if len(self._programs) == count: - # Stop if there are no more programs returned by the server. - break - offset += len(program_page) - if limit is None: - limit = len(self._programs) - return list(self._programs.values())[skip:limit+skip] - - def program(self, program_id: str, refresh: bool = False) -> RuntimeProgram: - """Retrieve a runtime program. - - Currently only program metadata is returned. - - Args: - program_id: Program ID. - refresh: If ``True``, re-query the server for the program. Otherwise - return the cached value. - - Returns: - Runtime program. - - Raises: - RuntimeProgramNotFound: If the program does not exist. - QiskitRuntimeError: If the request failed. - """ - if program_id not in self._programs or refresh: - try: - 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 QiskitRuntimeError(f"Failed to get program: {ex}") from None - - self._programs[program_id] = self._to_program(response) - - return self._programs[program_id] - - def _to_program(self, response: Dict) -> RuntimeProgram: - """Convert server response to ``RuntimeProgram`` instances. - - Args: - response: Server response. - - Returns: - A ``RuntimeProgram`` instance. - """ - backend_requirements = {} - parameters = {} - 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) - - 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 - ) -> RuntimeJob: - """Execute the runtime program. - - Args: - program_id: Program ID. - options: Runtime options that control the execution environment. - Currently the only available option is ``backend_name``, which is required. - inputs: Program input parameters. These input values are passed - to the runtime program. - callback: Callback function to be invoked for any interim results. - The callback function will receive 2 positional parameters: - - 1. Job ID - 2. Job interim result. - - result_decoder: A :class:`ResultDecoder` subclass used to decode job results. - ``ResultDecoder`` is used if not specified. - image: The runtime image used to execute the program, specified in the form - of image_name:tag. Not all accounts are authorized to select a different image. - hub: Name of the hub. - group: Name of the group. - project: Name of the project. - - Returns: - A ``RuntimeJob`` instance representing the execution. - - Raises: - IBMInputValueError: If input is invalid. - """ - 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): - 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): - hgp = self._default_hgp - else: - hgp = self._provider._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) - 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) - - backend = self._provider.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) - return job - - def upload_program( - self, - data: str, - metadata: Optional[Union[Dict, str]] = None - ) -> str: - """Upload a runtime program. - - In addition to program data, the following program metadata is also - required: - - - name - - max_execution_time - - description - - Program metadata can be specified using the `metadata` parameter or - individual parameter (for example, `name` and `description`). If the - same metadata field is specified in both places, the individual parameter - takes precedence. For example, if you specify:: - - upload_program(metadata={"name": "name1"}, name="name2") - - ``name2`` will be used as the program name. - - Args: - data: Program data or path of the file containing program data to upload. - metadata: Name of the program metadata file or metadata dictionary. - A metadata file needs to be in the JSON format. The ``parameters``, - ``return_values``, and ``interim_results`` should be defined as JSON Schema. - See :file:`program/program_metadata_sample.json` for an example. The - fields in metadata are explained below. - - * name: Name of the program. Required. - * max_execution_time: Maximum execution time in seconds. Required. - * description: Program description. Required. - * is_public: Whether the runtime program should be visible to the public. - The default is ``False``. - * spec: Specifications for backend characteristics and input parameters - required to run the program, interim results and final result. - - * backend_requirements: Backend requirements. - * parameters: Program input parameters in JSON schema format. - * return_values: Program return values in JSON schema format. - * interim_results: Program interim results in JSON schema format. - - Returns: - Program ID. - - Raises: - IBMInputValueError: If required metadata is missing. - RuntimeDuplicateProgramError: If a program with the same name already exists. - IBMNotAuthorizedError: If you are not authorized to upload programs. - QiskitRuntimeError: If the upload failed. - """ - program_metadata = self._read_metadata(metadata=metadata) - - 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.") - - if "def main(" not in data: - # This is the program file - with open(data, "r") as file: - data = file.read() - - try: - program_data = to_base64_string(data) - 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 - if ex.status_code == 403: - raise IBMNotAuthorizedError( - "You are not authorized to upload programs.") from None - raise QiskitRuntimeError(f"Failed to create program: {ex}") from None - return response['id'] - - def _read_metadata( - self, - metadata: Optional[Union[Dict, str]] = None - ) -> Dict: - """Read metadata. - - Args: - metadata: Name of the program metadata file or metadata dictionary. - - Returns: - Return metadata. - """ - upd_metadata: dict = {} - if metadata is not None: - if isinstance(metadata, str): - 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'] - 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 - ) -> None: - """Update a runtime program. - - Program metadata can be specified using the `metadata` parameter or - individual parameters, such as `name` and `description`. If the - same metadata field is specified in both places, the individual parameter - takes precedence. - - Args: - program_id: Program ID. - data: Program data or path of the file containing program data to upload. - metadata: Name of the program metadata file or metadata dictionary. - name: New program name. - description: New program description. - max_execution_time: New maximum execution time. - spec: New specifications for backend characteristics, input parameters, - interim results and final result. - - Raises: - RuntimeProgramNotFound: If the program doesn't exist. - 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.") - return - - if data: - if "def main(" not in data: - # This is the program file - with open(data, "r") as file: - data = file.read() - data = to_base64_string(data) - - 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) - - try: - self._api_client.program_update( - 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 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: - """Merge multiple copies of metadata. - Args: - metadata: Program metadata. - **kwargs: Additional metadata fields to overwrite. - Returns: - Merged metadata. - """ - merged = {} - metadata = metadata or {} - 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] - elif key in metadata.keys(): - merged[key] = metadata[key] - return merged - - def delete_program(self, program_id: str) -> None: - """Delete a runtime program. - - Args: - program_id: Program ID. - - Raises: - RuntimeProgramNotFound: If the program doesn't exist. - QiskitRuntimeError: If the request failed. - """ - try: - 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 QiskitRuntimeError(f"Failed to delete program: {ex}") from None - - if program_id in self._programs: - del self._programs[program_id] - - def set_program_visibility(self, program_id: str, public: bool) -> None: - """Sets a program's visibility. - - Args: - program_id: Program ID. - public: If ``True``, make the program visible to all. - If ``False``, make the program visible to just your account. - - Raises: - RuntimeJobNotFound: if program not found (404) - QiskitRuntimeError: if update failed (401, 403) - """ - try: - self._api_client.set_program_visibility(program_id, public) - 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 - - if program_id in self._programs: - program = self._programs[program_id] - program._is_public = public - - def job(self, job_id: str) -> RuntimeJob: - """Retrieve a runtime job. - - Args: - job_id: Job ID. - - Returns: - Runtime job retrieved. - - Raises: - RuntimeJobNotFound: If the job doesn't exist. - QiskitRuntimeError: If the request failed. - """ - try: - response = self._api_client.job_get(job_id) - except RequestsApiError as ex: - if ex.status_code == 404: - raise RuntimeJobNotFound(f"Job not found: {ex.message}") from None - raise QiskitRuntimeError(f"Failed to delete job: {ex}") from None - 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 - ) -> List[RuntimeJob]: - """Retrieve all runtime jobs, subject to optional filtering. - - Args: - limit: Number of jobs to retrieve. ``None`` means no limit. - skip: Starting index for the job retrieval. - pending: Filter by job pending state. If ``True``, 'QUEUED' and 'RUNNING' - jobs are included. If ``False``, 'DONE', 'CANCELLED' and 'ERROR' jobs - are included. - program_id: Filter by Program ID. - hub: Filter by hub - hub, group, and project must all be specified. - group: Filter by group - hub, group, and project must all be specified. - project: Filter by project - hub, group, and project must all be specified. - - Returns: - A list of runtime jobs. - - Raises: - IBMInputValueError: If any but not all of the parameters ``hub``, ``group`` - 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)) - job_responses = [] # type: List[Dict[str, Any]] - current_page_limit = limit or 20 - offset = skip - - while True: - jobs_response = self._api_client.jobs_get( - limit=current_page_limit, - skip=offset, - pending=pending, - program_id=program_id, - hub=hub, - group=group, - 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 - count = jobs_response["count"] - - job_responses += job_page - - if len(job_responses) == count - skip: - # Stop if there are no more jobs returned by the server. - break - - if limit: - if len(job_responses) >= limit: - # Stop if we have reached the limit. - break - current_page_limit = limit - len(job_responses) - else: - current_page_limit = 20 - - offset += len(job_page) - - return [self._decode_job(job) for job in job_responses] - - def delete_job(self, job_id: str) -> None: - """Delete a runtime job. - - Note that this operation cannot be reversed. - - Args: - job_id: ID of the job to delete. - - Raises: - RuntimeJobNotFound: If the job doesn't exist. - QiskitRuntimeError: If the request failed. - """ - try: - self._api_client.job_delete(job_id) - except RequestsApiError as ex: - if ex.status_code == 404: - raise RuntimeJobNotFound(f"Job not found: {ex.message}") from None - raise QiskitRuntimeError(f"Failed to delete job: {ex}") from None - - def _decode_job(self, raw_data: Dict) -> RuntimeJob: - """Decode job data received from the server. - - Args: - raw_data: Raw job data received from the server. - - Returns: - Decoded job data. - """ - hub = raw_data['hub'] - group = raw_data['group'] - project = raw_data['project'] - # Try to find the right backend - try: - backend = self._provider.get_backend(raw_data['backend'], - hub=hub, group=group, project=project) - except (IBMProviderError, QiskitBackendNotFoundError): - backend = IBMRetiredBackend.from_name( - backend_name=raw_data['backend'], - provider=self._provider, - credentials=Credentials(token="", url="", - hub=hub, group=group, project=project), - api=None - ) - - params = raw_data.get('params', {}) - if isinstance(params, list): - if len(params) > 0: - params = params[0] - else: - params = {} - if not isinstance(params, str): - 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)) - - def logout(self) -> None: - """Clears authorization cache on the server. - - For better performance, the runtime server caches each user's - authorization information. This method is used to force the server - to clear its cache. - - Note: - Invoke this method ONLY when your access level to the runtime - service has changed - for example, the first time your account is - given the authority to upload a program. - """ - self._api_client.logout() diff --git a/qiskit_ibm/runtime/program/__init__.py b/qiskit_ibm/runtime/program/__init__.py deleted file mode 100644 index 16919434b..000000000 --- a/qiskit_ibm/runtime/program/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Qiskit Runtime program package. - -This package contains files to help you write Qiskit Runtime programs. -""" diff --git a/qiskit_ibm/runtime/program/program_backend.py b/qiskit_ibm/runtime/program/program_backend.py deleted file mode 100644 index b4392f1bd..000000000 --- a/qiskit_ibm/runtime/program/program_backend.py +++ /dev/null @@ -1,67 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Base class for program backend.""" - -import logging -from typing import Union, List, Dict, Optional -from abc import abstractmethod, ABC - -from qiskit.pulse import Schedule -from qiskit.providers.backend import BackendV1 as Backend -from qiskit.providers.job import JobV1 as Job -from qiskit.circuit import QuantumCircuit - -logger = logging.getLogger(__name__) - - -class ProgramBackend(Backend, ABC): - """Base class for a program backend. - - This is a :class:`~qiskit.providers.Backend` class for runtime programs to - submit circuits. - """ - - @abstractmethod - def run( - self, - circuits: Union[QuantumCircuit, Schedule, - List[Union[QuantumCircuit, Schedule]]], - timeout: Optional[int] = None, - **run_config: Dict - ) -> Job: - """Run on the backend. - - Runtime circuit execution is synchronous, and control will not go - back until the execution finishes. You can use the `timeout` parameter - to set a timeout value to wait for the execution to finish. Note that if - the execution times out, circuit execution results will not be available. - - Args: - circuits: An individual or a - list of :class:`~qiskit.circuits.QuantumCircuit` or - :class:`~qiskit.pulse.Schedule` objects to run on the backend. - timeout: Seconds to wait for circuit execution to finish. - **run_config: Extra arguments used to configure the run. - - Returns: - The job to be executed. - - Raises: - IBMBackendApiError: If an unexpected error occurred while submitting - the job. - IBMBackendApiProtocolError: If an unexpected value received from - the server. - IBMBackendValueError: If an input parameter value is not valid. - """ - # pylint: disable=arguments-differ - pass diff --git a/qiskit_ibm/runtime/program/program_metadata_sample.json b/qiskit_ibm/runtime/program/program_metadata_sample.json deleted file mode 100644 index 6e9ae5000..000000000 --- a/qiskit_ibm/runtime/program/program_metadata_sample.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "runtime-simple", - "description": "Simple runtime program used for testing.", - "max_execution_time": 300, - "spec": { - "backend_requirements": { - "min_num_qubits": 5 - }, - "parameters": { - "type": "object", - "properties": { - "iterations": { - "description": "Number of iterations to run. Each iteration generates and runs a random circuit.", - "type": "integer" - } - }, - "required": ["iterations"] - }, - "return_values": { - "type": "string", - "description": "A string that says 'All done!'." - }, - "interim_results": { - "type": "object", - "properties": { - "iteration": { - "description": "Iteration number.", - "type": "integer" - }, - "counts": { - "description": "Histogram data of the circuit result.", - "type": "object" - } - } - } - } -} diff --git a/qiskit_ibm/runtime/program/program_template.py b/qiskit_ibm/runtime/program/program_template.py deleted file mode 100644 index c35be4d23..000000000 --- a/qiskit_ibm/runtime/program/program_template.py +++ /dev/null @@ -1,51 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=unused-argument -# pylint: disable=invalid-name - -"""Runtime program template. - -The ``main()`` method is the entry point of a runtime program. It takes a -:class:`ProgramBackend` and a :class:`UserMessenger` that can be used to -send circuits to the backend and messages to the user, respectively. -""" - -from typing import Any - -from qiskit_ibm.runtime import UserMessenger, ProgramBackend - - -def program(backend: ProgramBackend, user_messenger: UserMessenger, **kwargs): - """Function that does classical-quantum calculation.""" - # UserMessenger can be used to publish interim results. - user_messenger.publish("This is an interim result.") - return "final result" - - -def main(backend: ProgramBackend, user_messenger: UserMessenger, **kwargs) -> Any: - """This is the main entry point of a runtime program. - - The name of this method must not change. It also must have ``backend`` - and ``user_messenger`` as the first two positional arguments. - - Args: - backend: Backend for the circuits to run on. - user_messenger: Used to communicate with the program user. - kwargs: User inputs. - - Returns: - The final result of the runtime program. - """ - # Massage the input if necessary. - result = program(backend, user_messenger, **kwargs) - # Final results can be directly returned - return result diff --git a/qiskit_ibm/runtime/program/result_decoder.py b/qiskit_ibm/runtime/program/result_decoder.py deleted file mode 100644 index 5b04f6dd0..000000000 --- a/qiskit_ibm/runtime/program/result_decoder.py +++ /dev/null @@ -1,53 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Qiskit runtime job result decoder.""" - -import json -from typing import Any - -from qiskit_ibm.runtime.utils import RuntimeDecoder - - -class ResultDecoder: - """Runtime job result decoder. - - You can subclass this class and overwrite the :meth:`decode` method - to create a custom result decoder for the - results of your runtime program. For example:: - - class MyResultDecoder(ResultDecoder): - - @classmethod - def decode(cls, data): - decoded = super().decode(data) - custom_processing(decoded) # perform custom processing - - Users of your program will need to pass in the subclass when invoking - :meth:`qiskit_ibm.runtime.RuntimeJob.result` or - :meth:`qiskit_ibm.runtime.IBMRuntimeService.run`. - """ - - @classmethod - def decode(cls, data: str) -> Any: - """Decode the result data. - - Args: - data: Result data to be decoded. - - Returns: - Decoded result data. - """ - try: - return json.loads(data, cls=RuntimeDecoder) - except json.JSONDecodeError: - return data diff --git a/qiskit_ibm/runtime/program/user_messenger.py b/qiskit_ibm/runtime/program/user_messenger.py deleted file mode 100644 index 36240c9fa..000000000 --- a/qiskit_ibm/runtime/program/user_messenger.py +++ /dev/null @@ -1,50 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Base class for handling communication with program users.""" - -import json -from typing import Any, Type - -from ..utils import RuntimeEncoder - - -class UserMessenger: - """Base class for handling communication with program users. - - This class can be used when writing a new Qiskit Runtime program. - """ - - def publish( - self, - message: Any, - encoder: Type[json.JSONEncoder] = RuntimeEncoder, - final: bool = False - ) -> None: - """Publish message. - - You can use this method to publish messages, such as interim and final results, - to the program user. The messages will be made immediately available to the user, - but they may choose not to receive the messages. - - The `final` parameter is used to indicate whether the message is - the final result of the program. Final results may be processed differently - from interim results. - - Args: - message: Message to be published. Can be any type. - encoder: An optional JSON encoder for serializing - final: Whether the message being published is the final result. - """ - # pylint: disable=unused-argument - # Default implementation for testing. - print(json.dumps(message, cls=encoder)) diff --git a/qiskit_ibm/runtime/runtime_job.py b/qiskit_ibm/runtime/runtime_job.py deleted file mode 100644 index 945264f97..000000000 --- a/qiskit_ibm/runtime/runtime_job.py +++ /dev/null @@ -1,436 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Qiskit runtime job.""" - -from typing import Any, Optional, Callable, Dict, Type -import time -import logging -from concurrent import futures -import traceback -import queue -from datetime import datetime - -from qiskit.providers.exceptions import JobTimeoutError -from qiskit.providers.backend import Backend -from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES - -from .constants import API_TO_JOB_ERROR_MESSAGE, API_TO_JOB_STATUS -from .exceptions import RuntimeJobFailureError, RuntimeInvalidStateError, QiskitRuntimeError -from .program.result_decoder import ResultDecoder -from ..api.clients import RuntimeClient, RuntimeWebsocketClient, WebsocketClientCloseCode -from ..exceptions import IBMError -from ..api.exceptions import RequestsApiError -from ..utils.converters import utc_to_local -from ..credentials import Credentials - -logger = logging.getLogger(__name__) - - -class RuntimeJob: - """Representation of a runtime program execution. - - A new ``RuntimeJob`` instance is returned when you call - :meth:`IBMRuntimeService.run` - to execute a runtime program, or - :meth:`IBMRuntimeService.job` - to retrieve a previously executed job. - - If the program execution is successful, you can inspect the job's status by - calling :meth:`status()`. Job status can be one of the - :class:`~qiskit.providers.JobStatus` members. - - Some of the methods in this class are blocking, which means control may - not be returned immediately. :meth:`result()` is an example - of a blocking method:: - - job = provider.runtime.run(...) - - try: - job_result = job.result() # It will block until the job finishes. - print("The job finished with result {}".format(job_result)) - except RuntimeJobFailureError as ex: - print("Job failed!: {}".format(ex)) - - If the program has any interim results, you can use the ``callback`` - parameter of the - :meth:`~qiskit_ibm.runtime.IBMRuntimeService.run` - method to stream the interim results. - Alternatively, you can use the :meth:`stream_results` method to stream - the results at a later time, but before the job finishes. - """ - - _POISON_PILL = "_poison_pill" - """Used to inform streaming to stop.""" - - _executor = futures.ThreadPoolExecutor(thread_name_prefix="runtime_job") - - def __init__( - self, - backend: Backend, - api_client: RuntimeClient, - credentials: Credentials, - job_id: str, - program_id: str, - params: Optional[Dict] = None, - creation_date: Optional[str] = None, - user_callback: Optional[Callable] = None, - result_decoder: Type[ResultDecoder] = ResultDecoder, - image: Optional[str] = "" - ) -> None: - """RuntimeJob constructor. - - Args: - backend: The backend instance used to run this job. - api_client: Object for connecting to the server. - credentials: Account credentials. - job_id: Job ID. - program_id: ID of the program this job is for. - params: Job parameters. - creation_date: Job creation date, in UTC. - user_callback: User callback function. - result_decoder: A :class:`ResultDecoder` subclass used to decode job results. - image: Runtime image used for this job: image_name:tag. - """ - self._job_id = job_id - self._backend = backend - self._api_client = api_client - self._results: Optional[Any] = None - self._params = params or {} - self._creation_date = creation_date - self._program_id = program_id - self._status = JobStatus.INITIALIZING - self._error_message = None # type: Optional[str] - self._result_decoder = result_decoder - self._image = image - - # Used for streaming - self._ws_client_future = None # type: Optional[futures.Future] - self._result_queue = queue.Queue() # type: queue.Queue - self._ws_client = RuntimeWebsocketClient( - websocket_url=credentials.runtime_url.replace('https', 'wss'), - credentials=credentials, - job_id=job_id, - message_queue=self._result_queue) - - if user_callback is not None: - self.stream_results(user_callback) - - def result( - self, - timeout: Optional[float] = None, - wait: float = 5, - decoder: Optional[Type[ResultDecoder]] = None - ) -> Any: - """Return the results of the job. - - Args: - timeout: Number of seconds to wait for job. - wait: Seconds between queries. - decoder: A :class:`ResultDecoder` subclass used to decode job results. - - Returns: - Runtime job result. - - Raises: - RuntimeJobFailureError: If the job failed. - """ - _decoder = decoder or self._result_decoder - if self._results is None or (_decoder != self._result_decoder): - self.wait_for_final_state(timeout=timeout, wait=wait) - if self._status == JobStatus.ERROR: - raise RuntimeJobFailureError(f"Unable to retrieve job result. " - f"{self.error_message()}") - result_raw = self._api_client.job_results(job_id=self.job_id) - self._results = _decoder.decode(result_raw) - return self._results - - def cancel(self) -> None: - """Cancel the job. - - Raises: - RuntimeInvalidStateError: If the job is in a state that cannot be cancelled. - QiskitRuntimeError: If unable to cancel job. - """ - try: - self._api_client.job_cancel(self.job_id) - except RequestsApiError as ex: - if ex.status_code == 409: - raise RuntimeInvalidStateError(f"Job cannot be cancelled: {ex}") from None - raise QiskitRuntimeError(f"Failed to cancel job: {ex}") from None - self.cancel_result_streaming() - self._status = JobStatus.CANCELLED - - def status(self) -> JobStatus: - """Return the status of the job. - - Returns: - Status of this job. - """ - self._set_status_and_error_message() - return self._status - - def error_message(self) -> Optional[str]: - """Returns the reason if the job failed. - - Returns: - Error message string or ``None``. - """ - self._set_status_and_error_message() - return self._error_message - - def wait_for_final_state( - self, - timeout: Optional[float] = None, - wait: float = 5 - ) -> None: - """Poll the job status until it progresses to a final state such as ``DONE`` or ``ERROR``. - - Args: - timeout: Seconds to wait for the job. If ``None``, wait indefinitely. - wait: Seconds between queries. - - Raises: - JobTimeoutError: If the job does not reach a final state before the - specified timeout. - """ - start_time = time.time() - status = self.status() - while status not in JOB_FINAL_STATES: - elapsed_time = time.time() - start_time - if timeout is not None and elapsed_time >= timeout: - raise JobTimeoutError( - 'Timeout while waiting for job {}.'.format(self.job_id)) - time.sleep(wait) - status = self.status() - - def stream_results( - self, - callback: Callable, - decoder: Optional[Type[ResultDecoder]] = None - ) -> None: - """Start streaming job results. - - Args: - callback: Callback function to be invoked for any interim results. - The callback function will receive 2 positional parameters: - - 1. Job ID - 2. Job interim result. - - decoder: A :class:`ResultDecoder` subclass used to decode job results. - - Raises: - RuntimeInvalidStateError: If a callback function is already streaming results or - if the job already finished. - """ - if self._is_streaming(): - raise RuntimeInvalidStateError("A callback function is already streaming results.") - - if self._status in JOB_FINAL_STATES: - raise RuntimeInvalidStateError("Job already finished.") - - self._ws_client_future = self._executor.submit(self._start_websocket_client) - self._executor.submit(self._stream_results, - result_queue=self._result_queue, user_callback=callback, - decoder=decoder) - - def cancel_result_streaming(self) -> None: - """Cancel result streaming.""" - if not self._is_streaming(): - return - self._ws_client.disconnect(WebsocketClientCloseCode.CANCEL) - - def logs(self) -> str: - """Return job logs. - - Note: - Job logs are only available after the job finishes. - - Returns: - Job logs, including standard output and error. - - Raises: - QiskitRuntimeError: If a network error occurred. - """ - if self.status() not in JOB_FINAL_STATES: - logger.warning("Job logs are only available after the job finishes.") - try: - return self._api_client.job_logs(self.job_id) - except RequestsApiError as err: - if err.status_code == 404: - return "" - raise QiskitRuntimeError(f"Failed to get job logs: {err}") from None - - def _set_status_and_error_message(self) -> None: - """Fetch and set status and error message.""" - if self._status not in JOB_FINAL_STATES: - response = self._api_client.job_get(job_id=self.job_id) - self._set_status(response) - self._set_error_message(response) - - def _set_status(self, job_response: Dict) -> None: - """Set status. - - Args: - job_response: Job response from runtime API. - - Raises: - IBMError: If an unknown status is returned from the server. - """ - try: - self._status = API_TO_JOB_STATUS[job_response['status'].upper()] - except KeyError: - raise IBMError(f"Unknown status: {job_response['status']}") - - def _set_error_message(self, job_response: Dict) -> None: - """Set error message if the job failed. - - Args: - job_response: Job response from runtime API. - """ - if self._status == JobStatus.ERROR: - job_result_raw = self._api_client.job_results(job_id=self.job_id) - self._error_message = API_TO_JOB_ERROR_MESSAGE[job_response['status'].upper()].format( - self.job_id, job_result_raw) - else: - self._error_message = None - - def _is_streaming(self) -> bool: - """Return whether job results are being streamed. - - Returns: - Whether job results are being streamed. - """ - if self._ws_client_future is None: - return False - - if self._ws_client_future.done(): - return False - - return True - - def _start_websocket_client( - self - ) -> None: - """Start websocket client to stream results.""" - try: - logger.debug("Start websocket client for job %s", self.job_id) - self._ws_client.job_results() - except Exception: # pylint: disable=broad-except - logger.warning( - "An error occurred while streaming results " - "from the server for job %s:\n%s", self.job_id, traceback.format_exc()) - finally: - self._result_queue.put_nowait(self._POISON_PILL) - - def _stream_results( - self, - result_queue: queue.Queue, - user_callback: Callable, - decoder: Optional[Type[ResultDecoder]] = None - ) -> None: - """Stream interim results. - - Args: - result_queue: Queue used to pass websocket messages. - user_callback: User callback function. - decoder: A :class:`ResultDecoder` (sub)class used to decode job results. - """ - logger.debug("Start result streaming for job %s", self.job_id) - _decoder = decoder or self._result_decoder - while True: - try: - response = result_queue.get() - if response == self._POISON_PILL: - self._empty_result_queue(result_queue) - return - user_callback(self.job_id, _decoder.decode(response)) - except Exception: # pylint: disable=broad-except - logger.warning( - "An error occurred while streaming results " - "for job %s:\n%s", self.job_id, traceback.format_exc()) - - def _empty_result_queue(self, result_queue: queue.Queue) -> None: - """Empty the result queue. - - Args: - result_queue: Result queue to empty. - """ - try: - while True: - result_queue.get_nowait() - except queue.Empty: - pass - - @property - def job_id(self) -> str: - """Return a unique ID identifying the job. - - Returns: - Job ID. - """ - return self._job_id - - @property - def backend(self) -> Backend: - """Return the backend where this job was executed. - - Returns: - Backend used for the job. - """ - return self._backend - - @property - def image(self) -> str: - """Return the runtime image used for the job. - - Returns: - Runtime image: image_name:tag or "" if the default - image is used. - """ - return self._image - - @property - def inputs(self) -> Dict: - """Job input parameters. - - Returns: - Input parameters used in this job. - """ - return self._params - - @property - def program_id(self) -> str: - """Program ID. - - Returns: - ID of the program this job is for. - """ - return self._program_id - - @property - def creation_date(self) -> Optional[datetime]: - """Job creation date in local time. - - Returns: - The job creation date as a datetime object, in local time, or - ``None`` if creation date is not available. - """ - if not self._creation_date: - response = self._api_client.job_get(job_id=self.job_id) - self._creation_date = response.get('created', None) - - if not self._creation_date: - return None - creation_date_local_dt = utc_to_local(self._creation_date) - return creation_date_local_dt diff --git a/qiskit_ibm/runtime/runtime_program.py b/qiskit_ibm/runtime/runtime_program.py deleted file mode 100644 index 7cd1b6a2e..000000000 --- a/qiskit_ibm/runtime/runtime_program.py +++ /dev/null @@ -1,388 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Qiskit runtime program.""" - -import logging -import re -from typing import Optional, List, Dict -from types import SimpleNamespace -from qiskit_ibm.exceptions import IBMInputValueError, IBMNotAuthorizedError -from .exceptions import QiskitRuntimeError, RuntimeProgramNotFound -from ..api.clients.runtime import RuntimeClient -from ..api.exceptions import RequestsApiError - -logger = logging.getLogger(__name__) - - -class RuntimeProgram: - """Class representing program metadata. - - This class contains the metadata describing a program, such as its - name, ID, description, etc. - - You can use the :class:`~qiskit_ibm.runtime.IBMRuntimeService` - to retrieve the metadata of a specific program or all programs. For example:: - - from qiskit_ibm import IBMProvider - - provider = IBMProvider() - - # To retrieve metadata of all programs. - programs = provider.runtime.programs() - - # To retrieve metadata of a single program. - program = provider.runtime.program(program_id='circuit-runner') - print(f"Program {program.name} takes parameters {program.parameters().metadata}") - """ - - def __init__( - self, - program_name: str, - program_id: str, - description: str, - parameters: Optional[Dict] = None, - return_values: Optional[Dict] = None, - interim_results: Optional[Dict] = None, - max_execution_time: int = 0, - backend_requirements: Optional[Dict] = None, - creation_date: str = "", - update_date: str = "", - is_public: Optional[bool] = False, - data: str = "", - api_client: Optional[RuntimeClient] = None - ) -> None: - """RuntimeProgram constructor. - - Args: - program_name: Program name. - program_id: Program ID. - description: Program description. - parameters: Documentation on program parameters. - return_values: Documentation on program return values. - interim_results: Documentation on program interim results. - max_execution_time: Maximum execution time. - backend_requirements: Backend requirements. - creation_date: Program creation date. - update_date: Program last updated date. - is_public: ``True`` if program is visible to all. ``False`` if it's only visible to you. - data: Program data. - api_client: Runtime api client. - """ - self._name = program_name - self._id = program_id - self._description = description - self._max_execution_time = max_execution_time - self._backend_requirements = backend_requirements or {} - self._parameters = parameters or {} - self._return_values = return_values or {} - self._interim_results = interim_results or {} - self._creation_date = creation_date - self._update_date = update_date - self._is_public = is_public - self._data = data - self._api_client = api_client - - def __str__(self) -> str: - def _format_common(schema: Dict) -> None: - """Add title, description and property details to `formatted`.""" - if "description" in schema: - formatted.append(" "*4 + "Description: {}".format(schema["description"])) - if "type" in schema: - formatted.append(" "*4 + "Type: {}".format(str(schema["type"]))) - if "properties" in schema: - formatted.append(" "*4 + "Properties:") - for property_name, property_value in schema["properties"].items(): - formatted.append(" "*8 + "- " + property_name + ":") - for key, value in property_value.items(): - formatted.append(" "*12 + "{}: {}".format(sentence_case(key), str(value))) - formatted.append(" "*12 + "Required: " + - str(property_name in schema.get("required", []))) - - def sentence_case(camel_case_text: str) -> str: - """Converts camelCase to Sentence case""" - if camel_case_text == '': - return camel_case_text - sentence_case_text = re.sub('([A-Z])', r' \1', camel_case_text) - return sentence_case_text[:1].upper() + sentence_case_text[1:].lower() - - formatted = [f'{self.program_id}:', - f" Name: {self.name}", - f" Description: {self.description}", - f" Creation date: {self.creation_date}", - f" Update date: {self.update_date}", - f" Max execution time: {self.max_execution_time}"] - - formatted.append(" Input parameters:") - if self._parameters: - _format_common(self._parameters) - else: - formatted.append(" "*4 + "none") - - formatted.append(" Interim results:") - if self._interim_results: - _format_common(self._interim_results) - else: - formatted.append(" "*4 + "none") - - formatted.append(" Returns:") - if self._return_values: - _format_common(self._return_values) - else: - formatted.append(" "*4 + "none") - return '\n'.join(formatted) - - def to_dict(self) -> Dict: - """Convert program metadata to dictionary format. - - Returns: - Program metadata in dictionary format. - """ - return { - "program_id": self.program_id, - "name": self.name, - "description": self.description, - "max_execution_time": self.max_execution_time, - "backend_requirements": self.backend_requirements, - "parameters": self.parameters(), - "return_values": self.return_values, - "interim_results": self.interim_results, - "is_public": self._is_public - } - - def parameters(self) -> 'ParameterNamespace': - """Program parameter namespace. - - You can use the returned namespace to assign parameter values and pass - the namespace to :meth:`qiskit_ibm.runtime.IBMRuntimeService.run`. - The namespace allows you to use auto-completion to find program parameters. - - Note that each call to this method returns a new namespace instance and - does not include any modification to the previous instance. - - Returns: - Program parameter namespace. - """ - return ParameterNamespace(self._parameters) - - @property - def program_id(self) -> str: - """Program ID. - - Returns: - Program ID. - """ - return self._id - - @property - def name(self) -> str: - """Program name. - - Returns: - Program name. - """ - return self._name - - @property - def description(self) -> str: - """Program description. - - Returns: - Program description. - """ - return self._description - - @property - def return_values(self) -> Dict: - """Program return value definitions. - - Returns: - Return value definitions for this program. - """ - return self._return_values - - @property - def interim_results(self) -> Dict: - """Program interim result definitions. - - Returns: - Interim result definitions for this program. - """ - return self._interim_results - - @property - def max_execution_time(self) -> int: - """Maximum execution time in seconds. - - A program execution exceeding this time will be forcibly terminated. - - Returns: - Maximum execution time. - """ - return self._max_execution_time - - @property - def backend_requirements(self) -> Dict: - """Backend requirements. - - Returns: - Backend requirements for this program. - """ - return self._backend_requirements - - @property - def creation_date(self) -> str: - """Program creation date. - - Returns: - Program creation date. - """ - return self._creation_date - - @property - def update_date(self) -> str: - """Program last updated date. - - Returns: - Program last updated date. - """ - return self._update_date - - @property - def is_public(self) -> bool: - """Whether the program is visible to all. - - Returns: - Whether the program is public. - """ - return self._is_public - - @property - def data(self) -> str: - """Program data. - - Returns: - Program data. - - Raises: - IBMNotAuthorizedError: if user is not the program author. - """ - if not self._data: - self._refresh() - if not self._data: - raise IBMNotAuthorizedError( - 'Only program authors are authorized to retrieve program data') - return self._data - - def _refresh(self) -> None: - """Refresh program data and metadata - - Raises: - RuntimeProgramNotFound: If the program does not exist. - QiskitRuntimeError: If the request failed. - """ - try: - response = self._api_client.program_get(self._id) - except RequestsApiError as ex: - if ex.status_code == 404: - raise RuntimeProgramNotFound(f"Program not found: {ex.message}") from None - raise QiskitRuntimeError(f"Failed to get program: {ex}") from None - self._backend_requirements = {} - self._parameters = {} - self._return_values = {} - self._interim_results = {} - if "spec" in response: - self._backend_requirements = response["spec"].get('backend_requirements', {}) - self._parameters = response["spec"].get('parameters', {}) - self._return_values = response["spec"].get('return_values', {}) - self._interim_results = response["spec"].get('interim_results', {}) - self._name = response['name'] - self._id = response['id'] - self._description = response.get('description', "") - self._max_execution_time = response.get('cost', 0) - self._creation_date = response.get('creation_date', "") - self._update_date = response.get('update_date', "") - self._is_public = response.get('is_public', False) - self._data = response.get('data', "") - - -class ParameterNamespace(SimpleNamespace): - """ A namespace for program parameters with validation. - - This class provides a namespace for program parameters with auto-completion - and validation support. - """ - - def __init__(self, parameters: Dict): - """ParameterNamespace constructor. - - Args: - parameters: The program's input parameters. - """ - super().__init__() - # Allow access to the raw program parameters dict - self.__metadata = parameters - # For localized logic, create store of parameters in dictionary - self.__program_params: dict = {} - - for parameter_name, parameter_value in parameters.get("properties", {}).items(): - # (1) Add parameters to a dict by name - setattr(self, parameter_name, None) - # (2) Store the program params for validation - self.__program_params[parameter_name] = parameter_value - - @property - def metadata(self) -> Dict: - """Returns the parameter metadata""" - return self.__metadata - - def validate(self) -> None: - """Validate program input values. - - Note: - This method only verifies that required parameters have values. It - does not fail the validation if the namespace has extraneous parameters. - - Raises: - IBMInputValueError: if validation fails - """ - - # Iterate through the user's stored inputs - for parameter_name, parameter_value in self.__program_params.items(): - # Set invariants: User-specified parameter value (value) and if it's required (req) - value = getattr(self, parameter_name, None) - # Check there exists a program parameter of that name. - if value is None and parameter_name in self.metadata.get("required", []): - raise IBMInputValueError('Param (%s) missing required value!' % parameter_name) - - def __str__(self) -> str: - """Creates string representation of object""" - # Header - header = '| {:10.10} | {:12.12} | {:12.12} ' \ - '| {:8.8} | {:>15} |'.format( - 'Name', - 'Value', - 'Type', - 'Required', - 'Description' - ) - params_str = '\n'.join([ - '| {:10.10} | {:12.12} | {:12.12}| {:8.8} | {:>15} |'.format( - parameter_name, - str(getattr(self, parameter_name, "None")), - str(parameter_value.get("type", "None")), - str(parameter_name in self.metadata.get("required", [])), - str(parameter_value.get("description", "None")) - ) for parameter_name, parameter_value in self.__program_params.items()]) - - return "ParameterNamespace (Values):\n%s\n%s\n%s" \ - % (header, '-' * len(header), params_str) diff --git a/qiskit_ibm/runtime/utils.py b/qiskit_ibm/runtime/utils.py deleted file mode 100644 index b0e4c2a57..000000000 --- a/qiskit_ibm/runtime/utils.py +++ /dev/null @@ -1,260 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. -# pylint: disable=method-hidden -# pylint: disable=too-many-return-statements - -"""Utility functions for the runtime service.""" - -import base64 -import copy -import importlib -import inspect -import io -import json -import warnings -import zlib -from datetime import date -from typing import Any, Callable, Dict, List, Union - -import dateutil.parser -import numpy as np - -try: - import scipy.sparse - HAS_SCIPY = True -except ImportError: - HAS_SCIPY = False - -from qiskit.circuit import (Instruction, ParameterExpression, QuantumCircuit, - qpy_serialization) -from qiskit.circuit.library import BlueprintCircuit -from qiskit.result import Result - - -def to_base64_string(data: str) -> str: - """Convert string to base64 string. - - Args: - data: string to convert - - Returns: - data as base64 string - """ - return base64.b64encode(data.encode('utf-8')).decode('utf-8') - - -def _serialize_and_encode( - data: Any, - serializer: Callable, - compress: bool = True, - **kwargs: Any -) -> str: - """Serialize the input data and return the encoded string. - - Args: - data: Data to be serialized. - serializer: Function used to serialize data. - compress: Whether to compress the serialized data. - kwargs: Keyword arguments to pass to the serializer. - - Returns: - String representation. - """ - buff = io.BytesIO() - serializer(buff, data, **kwargs) - buff.seek(0) - serialized_data = buff.read() - buff.close() - if compress: - serialized_data = zlib.compress(serialized_data) - return base64.standard_b64encode(serialized_data).decode("utf-8") - - -def _decode_and_deserialize(data: str, deserializer: Callable, decompress: bool = True) -> Any: - """Decode and deserialize input data. - - Args: - data: Data to be deserialized. - deserializer: Function used to deserialize data. - decompress: Whether to decompress. - - Returns: - Deserialized data. - """ - buff = io.BytesIO() - decoded = base64.standard_b64decode(data) - if decompress: - decoded = zlib.decompress(decoded) - buff.write(decoded) - buff.seek(0) - orig = deserializer(buff) - buff.close() - return orig - - -def deserialize_from_settings(mod_name: str, class_name: str, settings: Dict) -> Any: - """Deserialize an object from its settings. - - Args: - mod_name: Name of the module. - class_name: Name of the class. - settings: Object settings. - - Returns: - Deserialized object. - - Raises: - ValueError: If unable to find the class. - """ - mod = importlib.import_module(mod_name) - for name, clz in inspect.getmembers(mod, inspect.isclass): - if name == class_name: - return clz(**settings) - raise ValueError(f"Unable to find class {class_name} in module {mod_name}") - - -def _set_int_keys_flag(obj: Dict) -> Union[Dict, List]: - """Recursively sets '__int_keys__' flag if dictionary uses integer keys - - Args: - obj: dictionary - - Returns: - obj with the '__int_keys__' flag set if dictionary uses integer key - """ - if isinstance(obj, dict): - for k, v in list(obj.items()): - if isinstance(k, int): - obj['__int_keys__'] = True - _set_int_keys_flag(v) - return obj - - -def _cast_strings_keys_to_int(obj: Dict) -> Dict: - """Casts string to int keys in dictionary when '__int_keys__' flag is set - - Args: - obj: dictionary - - Returns: - obj with string keys cast to int keys and '__int_keys__' flags removed - """ - if isinstance(obj, dict): - int_keys: List[int] = [] - for k, v in list(obj.items()): - if '__int_keys__' in obj: - try: - int_keys.append(int(k)) - except ValueError: - pass - _cast_strings_keys_to_int(v) - while len(int_keys) > 0: - key = int_keys.pop() - obj[key] = obj[str(key)] - obj.pop(str(key)) - if '__int_keys__' in obj: - del obj['__int_keys__'] - return obj - - -class RuntimeEncoder(json.JSONEncoder): - """JSON Encoder used by runtime service.""" - - def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ - if isinstance(obj, date): - return {'__type__': 'datetime', '__value__': obj.isoformat()} - if isinstance(obj, complex): - return {'__type__': 'complex', '__value__': [obj.real, obj.imag]} - if isinstance(obj, np.ndarray): - value = _serialize_and_encode(obj, np.save, allow_pickle=False) - return {'__type__': 'ndarray', '__value__': value} - if isinstance(obj, set): - return {'__type__': 'set', '__value__': list(obj)} - if isinstance(obj, Result): - return {'__type__': 'Result', '__value__': obj.to_dict()} - if hasattr(obj, 'to_json'): - return {'__type__': 'to_json', '__value__': obj.to_json()} - if isinstance(obj, QuantumCircuit): - # TODO Remove the decompose when terra 6713 is released. - if isinstance(obj, BlueprintCircuit): - obj = obj.decompose() - value = _serialize_and_encode( - data=obj, - serializer=lambda buff, data: qpy_serialization.dump(data, buff) - ) - return {'__type__': 'QuantumCircuit', '__value__': value} - if isinstance(obj, ParameterExpression): - value = _serialize_and_encode( - data=obj, - serializer=qpy_serialization._write_parameter_expression, - compress=False, - ) - return {'__type__': 'ParameterExpression', '__value__': value} - if isinstance(obj, Instruction): - value = _serialize_and_encode( - data=obj, serializer=qpy_serialization._write_instruction, compress=False) - return {'__type__': 'Instruction', '__value__': value} - if hasattr(obj, "settings"): - return {'__type__': 'settings', - '__module__': obj.__class__.__module__, - '__class__': obj.__class__.__name__, - '__value__': _set_int_keys_flag(copy.deepcopy(obj.settings))} - if callable(obj): - warnings.warn(f"Callable {obj} is not JSON serializable and will be set to None.") - return None - if HAS_SCIPY and isinstance(obj, scipy.sparse.spmatrix): - value = _serialize_and_encode(obj, scipy.sparse.save_npz, compress=False) - return {'__type__': 'spmatrix', '__value__': value} - return super().default(obj) - - -class RuntimeDecoder(json.JSONDecoder): - """JSON Decoder used by runtime service.""" - - def __init__(self, *args: Any, **kwargs: Any): - super().__init__(object_hook=self.object_hook, *args, **kwargs) - - def object_hook(self, obj: Any) -> Any: - """Called to decode object.""" - if '__type__' in obj: - obj_type = obj['__type__'] - obj_val = obj['__value__'] - - if obj_type == 'datetime': - return dateutil.parser.parse(obj_val) - if obj_type == 'complex': - return obj_val[0] + 1j * obj_val[1] - if obj_type == 'ndarray': - return _decode_and_deserialize(obj_val, np.load) - if obj_type == 'set': - return set(obj_val) - if obj_type == 'QuantumCircuit': - return _decode_and_deserialize(obj_val, qpy_serialization.load)[0] - if obj_type == 'ParameterExpression': - return _decode_and_deserialize( - obj_val, qpy_serialization._read_parameter_expression, False) - if obj_type == 'Instruction': - return _decode_and_deserialize( - obj_val, qpy_serialization._read_instruction, False) - if obj_type == 'settings': - return deserialize_from_settings( - mod_name=obj['__module__'], - class_name=obj['__class__'], - settings=_cast_strings_keys_to_int(obj_val) - ) - if obj_type == 'Result': - return Result.from_dict(obj_val) - if obj_type == 'spmatrix': - return _decode_and_deserialize(obj_val, scipy.sparse.load_npz, False) - if obj_type == 'to_json': - return obj_val - return obj diff --git a/test/ibm/runtime/__init__.py b/test/ibm/runtime/__init__.py deleted file mode 100644 index 99de8ba78..000000000 --- a/test/ibm/runtime/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Runtime related tests.""" diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/ibm/runtime/fake_runtime_client.py deleted file mode 100644 index a8b6643fd..000000000 --- a/test/ibm/runtime/fake_runtime_client.py +++ /dev/null @@ -1,382 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Fake RuntimeClient.""" - -import time -import uuid -import json -import base64 -from typing import Optional, Dict -from concurrent.futures import ThreadPoolExecutor - -from qiskit_ibm.credentials import Credentials -from qiskit_ibm.api.exceptions import RequestsApiError -from qiskit_ibm.runtime.utils import RuntimeEncoder - - -class BaseFakeProgram: - """Base class for faking a program.""" - - def __init__(self, program_id, name, data, cost, description, - backend_requirements=None, parameters=None, return_values=None, - interim_results=None, is_public=False): - """Initialize a fake program.""" - self._id = program_id - self._name = name - self._data = data - self._cost = cost - self._description = description - self._backend_requirements = backend_requirements - self._parameters = parameters - self._return_values = return_values - self._interim_results = interim_results - self._is_public = is_public - - def to_dict(self, include_data=False): - """Convert this program to a dictionary format.""" - out = {'id': self._id, - 'name': self._name, - 'cost': self._cost, - 'description': self._description, - 'is_public': self._is_public, - 'creation_date': '2021-09-13T17:27:42Z', - 'update_date': '2021-09-14T19:25:32Z'} - if include_data: - out['data'] = base64.standard_b64decode(self._data).decode() - out['spec'] = {} - if self._backend_requirements: - out['spec']['backend_requirements'] = self._backend_requirements - if self._parameters: - out['spec']['parameters'] = self._parameters - if self._return_values: - out['spec']['return_values'] = self._return_values - if self._interim_results: - out['spec']['interim_results'] = self._interim_results - - return out - - -class BaseFakeRuntimeJob: - """Base class for faking a runtime job.""" - - _job_progress = [ - "QUEUED", - "RUNNING", - "COMPLETED" - ] - - _executor = ThreadPoolExecutor() # pylint: disable=bad-option-value,consider-using-with - - def __init__(self, job_id, program_id, hub, group, project, backend_name, final_status, - params, image): - """Initialize a fake job.""" - self._job_id = job_id - self._status = final_status or "QUEUED" - self._program_id = program_id - self._hub = hub - self._group = group - self._project = project - self._backend_name = backend_name - self._params = params - self._image = image - if final_status is None: - self._future = self._executor.submit(self._auto_progress) - self._result = None - elif final_status == "COMPLETED": - self._result = json.dumps("foo") - self._final_status = final_status - - def _auto_progress(self): - """Automatically update job status.""" - for status in self._job_progress: - time.sleep(0.5) - self._status = status - - if self._status == "COMPLETED": - self._result = json.dumps("foo") - - def to_dict(self): - """Convert to dictionary format.""" - return {'id': self._job_id, - 'hub': self._hub, - 'group': self._group, - 'project': self._project, - 'backend': self._backend_name, - 'status': self._status, - 'params': [self._params], - 'program': {'id': self._program_id}, - 'image': self._image} - - def result(self): - """Return job result.""" - return self._result - - -class FailedRuntimeJob(BaseFakeRuntimeJob): - """Class for faking a failed runtime job.""" - - _job_progress = [ - "QUEUED", - "RUNNING", - "FAILED" - ] - - def _auto_progress(self): - """Automatically update job status.""" - super()._auto_progress() - - if self._status == "FAILED": - self._result = "Kaboom!" - - -class FailedRanTooLongRuntimeJob(BaseFakeRuntimeJob): - """Class for faking a failed runtime job.""" - - _job_progress = [ - "QUEUED", - "RUNNING", - "CANCELLED - RAN TOO LONG" - ] - - def _auto_progress(self): - """Automatically update job status.""" - super()._auto_progress() - - if self._status == "CANCELLED - RAN TOO LONG": - self._result = "Kaboom!" - - -class CancelableRuntimeJob(BaseFakeRuntimeJob): - """Class for faking a cancelable runtime job.""" - - _job_progress = [ - "QUEUED", - "RUNNING" - ] - - def __init__(self, *args, **kwargs): - """Initialize a cancellable job.""" - super().__init__(*args, **kwargs) - self._cancelled = False - - def cancel(self): - """Cancel the job.""" - self._future.cancel() - self._cancelled = True - - def to_dict(self): - """Convert to dictionary format.""" - data = super().to_dict() - if self._cancelled: - data['status'] = "CANCELLED" - return data - - -class CustomResultRuntimeJob(BaseFakeRuntimeJob): - """Class for using custom job result.""" - - custom_result = "bar" - - def _auto_progress(self): - """Automatically update job status.""" - super()._auto_progress() - - if self._status == "COMPLETED": - self._result = json.dumps(self.custom_result, cls=RuntimeEncoder) - - -class TimedRuntimeJob(BaseFakeRuntimeJob): - """Class for a job that runs for the input seconds.""" - - def __init__(self, **kwargs): - self._runtime = kwargs.pop('run_time') - super().__init__(**kwargs) - - def _auto_progress(self): - self._status = "RUNNING" - time.sleep(self._runtime) - self._status = "COMPLETED" - - if self._status == "COMPLETED": - self._result = json.dumps("foo") - - -class BaseFakeRuntimeClient: - """Base class for faking the runtime client.""" - - def __init__(self, job_classes=None, final_status=None, job_kwargs=None, - hub=None, group=None, project=None): - """Initialize a fake runtime client.""" - self._programs = {} - self._jobs = {} - self._job_classes = job_classes or [] - self._final_status = final_status - self._job_kwargs = job_kwargs or {} - self._hub = hub - self._group = group - self._project = project - - def set_hgp(self, hub, group, project): - """Set hub, group and project""" - self._hub = hub - self._group = group - self._project = project - - def set_job_classes(self, classes): - """Set job classes to use.""" - if not isinstance(classes, list): - classes = [classes] - self._job_classes = classes - - def set_final_status(self, final_status): - """Set job status to passed in final status instantly.""" - self._final_status = final_status - - def list_programs(self, limit, skip): - """List all programs.""" - programs = [] - for prog in self._programs.values(): - programs.append(prog.to_dict()) - return {"programs": programs[skip:limit+skip], "count": len(self._programs)} - - def program_create(self, program_data, name, description, max_execution_time, - spec=None, is_public=False): - """Create a program.""" - program_id = name - if program_id in self._programs: - raise RequestsApiError("Program already exists.", status_code=409) - backend_requirements = spec.get('backend_requirements', None) - parameters = spec.get('parameters', None) - return_values = spec.get('return_values', None) - interim_results = spec.get('interim_results', None) - self._programs[program_id] = BaseFakeProgram( - program_id=program_id, name=name, data=program_data, cost=max_execution_time, - description=description, backend_requirements=backend_requirements, - parameters=parameters, return_values=return_values, interim_results=interim_results, - is_public=is_public) - return {'id': program_id} - - 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 - ) -> None: - """Update a program.""" - if program_id not in self._programs: - raise RequestsApiError("Program not found", status_code=404) - program = self._programs[program_id] - program._data = program_data or program._data - program._name = name or program._name - program._description = description or program._description - program._cost = max_execution_time or program._cost - if spec: - program._backend_requirements = \ - spec.get("backend_requirements") or program._backend_requirements - program._parameters = spec.get("parameters") or program._parameters - program._return_values = spec.get("return_values") or program._return_values - program._interim_results = spec.get("interim_results") or program._interim_results - - def program_get(self, program_id: str): - """Return a specific program.""" - if program_id not in self._programs: - raise RequestsApiError("Program not found", status_code=404) - return self._programs[program_id].to_dict(include_data=True) - - def program_run( - self, - program_id: str, - credentials: Credentials, - backend_name: str, - params: str, - image: Optional[str] = "" - ): - """Run the specified program.""" - job_id = uuid.uuid4().hex - job_cls = self._job_classes.pop(0) if len(self._job_classes) > 0 else BaseFakeRuntimeJob - hub = self._hub or credentials.hub - group = self._group or credentials.group - project = self._project or credentials.project - job = job_cls(job_id=job_id, program_id=program_id, - hub=hub, group=group, - project=project, backend_name=backend_name, - params=params, final_status=self._final_status, image=image, - **self._job_kwargs) - self._jobs[job_id] = job - return {'id': job_id} - - def program_delete(self, program_id: str) -> None: - """Delete the specified program.""" - if program_id not in self._programs: - raise RequestsApiError("Program not found", status_code=404) - del self._programs[program_id] - - def job_get(self, job_id): - """Get the specific job.""" - return self._get_job(job_id).to_dict() - - def jobs_get(self, limit=None, skip=None, pending=None, program_id=None, - hub=None, group=None, project=None): - """Get all jobs.""" - pending_statuses = ['QUEUED', 'RUNNING'] - returned_statuses = ['COMPLETED', 'FAILED', 'CANCELLED'] - limit = limit or len(self._jobs) - skip = skip or 0 - jobs = list(self._jobs.values()) - count = len(self._jobs) - if pending is not None: - job_status_list = pending_statuses if pending else returned_statuses - jobs = [job for job in jobs if job._status in job_status_list] - count = len(jobs) - if program_id: - jobs = [job for job in jobs if job._program_id == program_id] - count = len(jobs) - if all([hub, group, project]): - jobs = [job for job in jobs if - job._hub == hub and job._group == group and job._project == project] - count = len(jobs) - jobs = jobs[skip:limit+skip] - return {"jobs": [job.to_dict() for job in jobs], - "count": count} - - def set_program_visibility(self, program_id: str, public: bool) -> None: - """Sets a program's visibility. - - Args: - program_id: Program ID. - public: If ``True``, make the program visible to all. - If ``False``, make the program visible to just your account. - """ - self._programs[program_id]._is_public = public - - def job_results(self, job_id): - """Get the results of a program job.""" - return self._get_job(job_id).result() - - def job_cancel(self, job_id): - """Cancel the job.""" - self._get_job(job_id).cancel() - - def job_delete(self, job_id): - """Delete the job.""" - self._get_job(job_id) - del self._jobs[job_id] - - def _get_job(self, job_id): - """Get job.""" - if job_id not in self._jobs: - raise RequestsApiError("Job not found", status_code=404) - return self._jobs[job_id] diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py deleted file mode 100644 index 4083cd7be..000000000 --- a/test/ibm/runtime/test_runtime.py +++ /dev/null @@ -1,813 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for runtime service.""" - -import copy -import json -import os -from io import StringIO -from unittest.mock import patch -from unittest import mock, skipIf -import uuid -import time -import random -import subprocess -import tempfile -import warnings -from datetime import datetime -import numpy as np -import scipy.sparse - -from qiskit.algorithms.optimizers import ( - ADAM, - GSLS, - IMFIL, - SPSA, - QNSPSA, - SNOBFIT, - L_BFGS_B, - NELDER_MEAD, -) -from qiskit.result import Result -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.test.reference_circuits import ReferenceCircuits -from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import (PauliSumOp, MatrixOp, PauliOp, CircuitOp, EvolvedOp, - TaperedPauliSumOp, Z2Symmetries, I, X, Y, Z, - StateFn, CircuitStateFn, DictStateFn, VectorStateFn, OperatorStateFn, - SparseVectorStateFn, CVaRMeasurement, ComposedOp, SummedOp, TensoredOp) -from qiskit.quantum_info import SparsePauliOp, Pauli, PauliTable, Statevector -from qiskit.providers.jobstatus import JobStatus - -from qiskit_ibm.exceptions import IBMInputValueError -from qiskit_ibm.ibm_provider import IBMProvider -from qiskit_ibm.credentials import Credentials -from qiskit_ibm.hub_group_project import HubGroupProject -from qiskit_ibm.runtime.utils import RuntimeEncoder, RuntimeDecoder -from qiskit_ibm.runtime import IBMRuntimeService, RuntimeJob -from qiskit_ibm.runtime.constants import API_TO_JOB_ERROR_MESSAGE -from qiskit_ibm.runtime.exceptions import RuntimeProgramNotFound, RuntimeJobFailureError -from qiskit_ibm.runtime.runtime_program import ParameterNamespace - -from ...ibm_test_case import IBMTestCase -from .fake_runtime_client import (BaseFakeRuntimeClient, FailedRanTooLongRuntimeJob, - FailedRuntimeJob, CancelableRuntimeJob, CustomResultRuntimeJob) -from .utils import SerializableClass, SerializableClassDecoder, get_complex_types - - -class TestRuntime(IBMTestCase): - """Class for testing runtime modules.""" - - DEFAULT_DATA = "def main() {}" - DEFAULT_METADATA = { - "name": "qiskit-test", - "description": "Test program.", - "max_execution_time": 300, - "spec": { - "backend_requirements": { - "min_num_qubits": 5 - }, - "parameters": { - "properties": { - "param1": { - "description": "Desc 1", - "type": "string", - "enum": [ - "a", - "b", - "c" - ] - }, - "param2": { - "description": "Desc 2", - "type": "integer", - "min": 0 - } - }, - "required": [ - "param1" - ] - }, - "return_values": { - "type": "object", - "description": "Return values", - "properties": { - "ret_val": { - "description": "Some return value.", - "type": "string" - } - } - }, - "interim_results": { - "properties": { - "int_res": { - "description": "Some interim result", - "type": "string" - } - } - } - } - } - - def setUp(self): - """Initial test setup.""" - super().setUp() - provider = mock.MagicMock(spec=IBMProvider) - hgp = mock.MagicMock(spec=HubGroupProject) - hgp.credentials = Credentials( - token="", url="", services={"runtime": "https://quantum-computing.ibm.com"}) - self.runtime = IBMRuntimeService(provider, hgp) - self.runtime._api_client = BaseFakeRuntimeClient() - - def test_coder(self): - """Test runtime encoder and decoder.""" - result = Result(backend_name='ibmqx2', - backend_version='1.1', - qobj_id='12345', - job_id='67890', - success=False, - results=[]) - - data = {"string": "foo", - "float": 1.5, - "complex": 2+3j, - "array": np.array([[1, 2, 3], [4, 5, 6]]), - "result": result, - "sclass": SerializableClass("foo"), - } - encoded = json.dumps(data, cls=RuntimeEncoder) - decoded = json.loads(encoded, cls=RuntimeDecoder) - decoded["sclass"] = SerializableClass.from_json(decoded['sclass']) - - decoded_result = decoded.pop('result') - data.pop('result') - - decoded_array = decoded.pop('array') - orig_array = data.pop('array') - - self.assertEqual(decoded, data) - self.assertIsInstance(decoded_result, Result) - self.assertTrue((decoded_array == orig_array).all()) - - def test_coder_qc(self): - """Test runtime encoder and decoder for circuits.""" - bell = ReferenceCircuits.bell() - unbound = EfficientSU2(num_qubits=4, reps=1, entanglement='linear') - subtests = ( - bell, - unbound, - [bell, unbound] - ) - for circ in subtests: - with self.subTest(circ=circ): - encoded = json.dumps(circ, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - if not isinstance(circ, list): - decoded = [decoded] - self.assertTrue(all(isinstance(item, QuantumCircuit) for item in decoded)) - - def test_coder_operators(self): - """Test runtime encoder and decoder for operators.""" - x = Parameter("x") - y = x + 1 - qc = QuantumCircuit(1) - qc.h(0) - coeffs = np.array([1, 2, 3, 4, 5, 6]) - table = PauliTable.from_labels(["III", "IXI", "IYY", "YIZ", "XYZ", "III"]) - op = (2.0 * I ^ I) - z2_symmetries = Z2Symmetries( - [Pauli("IIZI"), Pauli("ZIII")], [Pauli("IIXI"), Pauli("XIII")], [1, 3], [-1, 1] - ) - isqrt2 = 1 / np.sqrt(2) - sparse = scipy.sparse.csr_matrix([[0, isqrt2, 0, isqrt2]]) - - subtests = ( - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1]), coeff=y), - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1 + 2j]), coeff=3 - 2j), - PauliSumOp.from_list([("II", -1.052373245772859), ("IZ", 0.39793742484318045)]), - PauliSumOp(SparsePauliOp(table, coeffs), coeff=10), - MatrixOp(primitive=np.array([[0, -1j], [1j, 0]]), coeff=x), - PauliOp(primitive=Pauli("Y"), coeff=x), - CircuitOp(qc, coeff=x), - EvolvedOp(op, coeff=x), - TaperedPauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), z2_symmetries), - StateFn(qc, coeff=x), - CircuitStateFn(qc, is_measurement=True), - DictStateFn("1" * 3, is_measurement=True), - VectorStateFn(np.ones(2 ** 3, dtype=complex)), - OperatorStateFn(CircuitOp(QuantumCircuit(1))), - SparseVectorStateFn(sparse), - Statevector([1, 0]), - CVaRMeasurement(Z, 0.2), - ComposedOp([(X ^ Y ^ Z), (Z ^ X ^ Y ^ Z).to_matrix_op()]), - SummedOp([X ^ X * 2, Y ^ Y], 2), - TensoredOp([(X ^ Y), (Z ^ I)]), - (Z ^ Z) ^ (I ^ 2), - ) - for op in subtests: - with self.subTest(op=op): - encoded = json.dumps(op, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertEqual(op, decoded) - - @skipIf(os.name == 'nt', 'Test not supported on Windows') - def test_coder_optimizers(self): - """Test runtime encoder and decoder for optimizers.""" - subtests = ( - (ADAM, {"maxiter": 100, "amsgrad": True}), - (GSLS, {"maxiter": 50, "min_step_size": 0.01}), - (IMFIL, {"maxiter": 20}), - (SPSA, {"maxiter": 10, "learning_rate": 0.01, "perturbation": 0.1}), - (SNOBFIT, {"maxiter": 200, "maxfail": 20}), - (QNSPSA, {"fidelity": 123, "maxiter": 25, "resamplings": {1: 100, 2: 50}}), - # some SciPy optimizers only work with default arguments due to Qiskit/qiskit-terra#6682 - (L_BFGS_B, {}), - (NELDER_MEAD, {}), - ) - for opt_cls, settings in subtests: - with self.subTest(opt_cls=opt_cls): - optimizer = opt_cls(**settings) - encoded = json.dumps(optimizer, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertTrue(isinstance(decoded, opt_cls)) - for key, value in settings.items(): - self.assertEqual(decoded.settings[key], value) - - def test_encoder_datetime(self): - """Test encoding a datetime.""" - subtests = ( - {"datetime": datetime.now()}, - {"datetime": datetime(2021, 8, 4)}, - {"datetime": datetime.fromtimestamp(1326244364)} - ) - for obj in subtests: - encoded = json.dumps(obj, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertEqual(decoded, obj) - - def test_encoder_callable(self): - """Test encoding a callable.""" - with warnings.catch_warnings(record=True) as warn_cm: - encoded = json.dumps({"fidelity": lambda x: x}, cls=RuntimeEncoder) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertIsNone(decoded["fidelity"]) - self.assertEqual(len(warn_cm), 1) - - def test_decoder_import(self): - """Test runtime decoder importing modules.""" - script = """ -import sys -import json -from qiskit_ibm.runtime import RuntimeDecoder -if __name__ == '__main__': - obj = json.loads(sys.argv[1], cls=RuntimeDecoder) - print(obj.__class__.__name__) -""" - temp_fp = tempfile.NamedTemporaryFile(mode='w', delete=False) - self.addCleanup(os.remove, temp_fp.name) - temp_fp.write(script) - temp_fp.close() - - subtests = ( - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), - DictStateFn("1" * 3, is_measurement=True), - Statevector([1, 0]), - ) - for op in subtests: - with self.subTest(op=op): - encoded = json.dumps(op, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - cmd = ["python", temp_fp.name, encoded] - proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - universal_newlines=True, check=True) - self.assertIn(op.__class__.__name__, proc.stdout) - - def test_list_programs(self): - """Test listing programs.""" - program_id = self._upload_program() - programs = self.runtime.programs() - all_ids = [prog.program_id for prog in programs] - self.assertIn(program_id, all_ids) - - def test_list_programs_with_limit_skip(self): - """Test listing programs with limit and skip.""" - program_1 = self._upload_program() - program_2 = self._upload_program() - program_3 = self._upload_program() - programs = self.runtime.programs(limit=2, skip=1) - all_ids = [prog.program_id for prog in programs] - self.assertNotIn(program_1, all_ids) - self.assertIn(program_2, all_ids) - self.assertIn(program_3, all_ids) - programs = self.runtime.programs(limit=3) - all_ids = [prog.program_id for prog in programs] - self.assertIn(program_1, all_ids) - - def test_list_program(self): - """Test listing a single program.""" - program_id = self._upload_program() - program = self.runtime.program(program_id) - self.assertEqual(program_id, program.program_id) - - def test_print_programs(self): - """Test printing programs.""" - ids = [] - for idx in range(3): - ids.append(self._upload_program(name=f"name_{idx}")) - - programs = self.runtime.programs() - with patch('sys.stdout', new=StringIO()) as mock_stdout: - self.runtime.pprint_programs() - stdout = mock_stdout.getvalue() - for prog in programs: - self.assertIn(prog.program_id, stdout) - self.assertIn(prog.name, stdout) - self.assertNotIn(str(prog.max_execution_time), stdout) - self.runtime.pprint_programs(detailed=True) - stdout_detailed = mock_stdout.getvalue() - for prog in programs: - self.assertIn(prog.program_id, stdout_detailed) - self.assertIn(prog.name, stdout_detailed) - self.assertIn(str(prog.max_execution_time), stdout_detailed) - - def test_upload_program(self): - """Test uploading a program.""" - max_execution_time = 3000 - is_public = True - program_id = self._upload_program(max_execution_time=max_execution_time, - is_public=is_public) - self.assertTrue(program_id) - program = self.runtime.program(program_id) - self.assertTrue(program) - self.assertEqual(max_execution_time, program.max_execution_time) - self.assertEqual(program.is_public, is_public) - - def test_update_program(self): - """Test updating program.""" - new_data = "def main() {foo=bar}" - new_metadata = copy.deepcopy(self.DEFAULT_METADATA) - new_metadata["name"] = "test_update_program" - new_name = "name2" - new_description = "some other description" - new_cost = self.DEFAULT_METADATA["max_execution_time"] + 100 - new_spec = copy.deepcopy(self.DEFAULT_METADATA["spec"]) - new_spec["backend_requirements"] = {"input_allowed": "runtime"} - - sub_tests = [ - {"data": new_data}, - {"metadata": new_metadata}, - {"data": new_data, "metadata": new_metadata}, - {"metadata": new_metadata, "name": new_name}, - {"data": new_data, "metadata": new_metadata, "description": new_description}, - {"max_execution_time": new_cost, "spec": new_spec} - ] - - for new_vals in sub_tests: - with self.subTest(new_vals=new_vals.keys()): - program_id = self._upload_program() - self.runtime.update_program(program_id=program_id, **new_vals) - updated = self.runtime.program(program_id, refresh=True) - if "data" in new_vals: - raw_program = self.runtime._api_client.program_get(program_id) - self.assertEqual(new_data, raw_program["data"]) - if "metadata" in new_vals and "name" not in new_vals: - self.assertEqual(new_metadata["name"], updated.name) - if "name" in new_vals: - self.assertEqual(new_name, updated.name) - if "description" in new_vals: - self.assertEqual(new_description, updated.description) - if "max_execution_time" in new_vals: - self.assertEqual(new_cost, updated.max_execution_time) - if "spec" in new_vals: - raw_program = self.runtime._api_client.program_get(program_id) - self.assertEqual(new_spec, raw_program["spec"]) - - def test_update_program_no_new_fields(self): - """Test updating a program without any new data.""" - program_id = self._upload_program() - with warnings.catch_warnings(record=True) as warn_cm: - self.runtime.update_program(program_id=program_id) - self.assertEqual(len(warn_cm), 1) - - def test_delete_program(self): - """Test deleting program.""" - program_id = self._upload_program() - self.runtime.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.runtime.program(program_id, refresh=True) - - def test_double_delete_program(self): - """Test deleting a deleted program.""" - program_id = self._upload_program() - self.runtime.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.runtime.delete_program(program_id) - - def test_run_program(self): - """Test running program.""" - params = {'param1': 'foo'} - job = self._run_program(inputs=params) - self.assertTrue(job.job_id) - self.assertIsInstance(job, RuntimeJob) - self.assertIsInstance(job.status(), JobStatus) - self.assertEqual(job.inputs, params) - job.wait_for_final_state() - self.assertEqual(job.status(), JobStatus.DONE) - self.assertTrue(job.result()) - - def test_run_program_with_custom_runtime_image(self): - """Test running program.""" - params = {'param1': 'foo'} - image = "name:tag" - job = self._run_program(inputs=params, image=image) - self.assertTrue(job.job_id) - self.assertIsInstance(job, RuntimeJob) - self.assertIsInstance(job.status(), JobStatus) - self.assertEqual(job.inputs, params) - job.wait_for_final_state() - self.assertEqual(job.status(), JobStatus.DONE) - self.assertTrue(job.result()) - self.assertEqual(job.image, image) - - def test_retrieve_program_data(self): - """Test retrieving program data""" - program_id = self._upload_program(name="qiskit-test") - self.runtime.programs() - program = self.runtime.program(program_id) - self.assertEqual(program.data, self.DEFAULT_DATA) - self._validate_program(program) - - def test_program_params_validation(self): - """Test program parameters validation process""" - program_id = self.runtime.upload_program( - data=self.DEFAULT_DATA, metadata=self.DEFAULT_METADATA) - program = self.runtime.program(program_id) - params: ParameterNamespace = program.parameters() - params.param1 = 'Hello, World' - # Check OK params - params.validate() - # Check OK params - contains unnecessary param - params.param3 = 'Hello, World' - params.validate() - # Check bad params - missing required param - params.param1 = None - with self.assertRaises(IBMInputValueError): - params.validate() - params.param1 = 'foo' - - def test_program_params_namespace(self): - """Test running a program using parameter namespace.""" - program_id = self.runtime.upload_program( - data=self.DEFAULT_DATA, metadata=self.DEFAULT_METADATA) - params = self.runtime.program(program_id).parameters() - params.param1 = "Hello World" - self._run_program(program_id, inputs=params) - - def test_run_program_failed(self): - """Test a failed program execution.""" - job = self._run_program(job_classes=FailedRuntimeJob) - job.wait_for_final_state() - job_result_raw = self.runtime._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertEqual(API_TO_JOB_ERROR_MESSAGE['FAILED'].format( - job.job_id, job_result_raw), job.error_message()) - with self.assertRaises(RuntimeJobFailureError): - job.result() - - def test_run_program_failed_ran_too_long(self): - """Test a program that failed since it ran longer than maxiumum execution time.""" - job = self._run_program(job_classes=FailedRanTooLongRuntimeJob) - job.wait_for_final_state() - job_result_raw = self.runtime._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertEqual(API_TO_JOB_ERROR_MESSAGE['CANCELLED - RAN TOO LONG'].format( - job.job_id, job_result_raw), job.error_message()) - with self.assertRaises(RuntimeJobFailureError): - job.result() - - def test_retrieve_job(self): - """Test retrieving a job.""" - program_id = self._upload_program() - params = {'param1': 'foo'} - job = self._run_program(program_id, inputs=params) - rjob = self.runtime.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(program_id, rjob.program_id) - - def test_jobs_no_limit(self): - """Test retrieving jobs without limit.""" - jobs = [] - program_id = self._upload_program() - for _ in range(25): - jobs.append(self._run_program(program_id)) - rjobs = self.runtime.jobs(limit=None) - self.assertEqual(25, len(rjobs)) - - def test_jobs_limit(self): - """Test retrieving jobs with limit.""" - jobs = [] - job_count = 25 - program_id = self._upload_program() - for _ in range(job_count): - jobs.append(self._run_program(program_id)) - - limits = [21, 30] - for limit in limits: - with self.subTest(limit=limit): - rjobs = self.runtime.jobs(limit=limit) - self.assertEqual(min(limit, job_count), len(rjobs)) - - def test_jobs_skip(self): - """Test retrieving jobs with skip.""" - jobs = [] - program_id = self._upload_program() - for _ in range(5): - jobs.append(self._run_program(program_id)) - rjobs = self.runtime.jobs(skip=4) - self.assertEqual(1, len(rjobs)) - - def test_jobs_skip_limit(self): - """Test retrieving jobs with skip and limit.""" - jobs = [] - program_id = self._upload_program() - for _ in range(10): - jobs.append(self._run_program(program_id)) - rjobs = self.runtime.jobs(skip=4, limit=2) - self.assertEqual(2, len(rjobs)) - - def test_jobs_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING).""" - jobs = [] - program_id = self._upload_program() - (jobs, pending_jobs_count, _) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id) - rjobs = self.runtime.jobs(pending=True) - self.assertEqual(pending_jobs_count, len(rjobs)) - - def test_jobs_limit_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING) with limit.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses(jobs=jobs, program_id=program_id) - limit = 4 - rjobs = self.runtime.jobs(limit=limit, pending=True) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_skip_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING) with skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, pending_jobs_count, _) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id) - skip = 4 - rjobs = self.runtime.jobs(skip=skip, pending=True) - self.assertEqual(pending_jobs_count - skip, len(rjobs)) - - def test_jobs_limit_skip_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING) with limit and skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses(jobs=jobs, program_id=program_id) - limit = 2 - skip = 3 - rjobs = self.runtime.jobs(limit=limit, skip=skip, pending=True) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED).""" - jobs = [] - program_id = self._upload_program() - (jobs, _, returned_jobs_count) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id) - rjobs = self.runtime.jobs(pending=False) - self.assertEqual(returned_jobs_count, len(rjobs)) - - def test_jobs_limit_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with limit.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses(jobs=jobs, program_id=program_id) - limit = 6 - rjobs = self.runtime.jobs(limit=limit, pending=False) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_skip_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, _, returned_jobs_count) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id) - skip = 4 - rjobs = self.runtime.jobs(skip=skip, pending=False) - self.assertEqual(returned_jobs_count - skip, len(rjobs)) - - def test_jobs_limit_skip_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with limit and skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses(jobs=jobs, program_id=program_id) - limit = 6 - skip = 2 - rjobs = self.runtime.jobs(limit=limit, skip=skip, pending=False) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_filter_by_program_id(self): - """Test retrieving jobs by Program ID.""" - program_id = self._upload_program() - program_id_1 = self._upload_program() - job = self._run_program(program_id=program_id) - job_1 = self._run_program(program_id=program_id_1) - job.wait_for_final_state() - job_1.wait_for_final_state() - rjobs = self.runtime.jobs(program_id=program_id) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - - def test_jobs_filter_by_provider(self): - """Test retrieving jobs by provider.""" - program_id = self._upload_program() - job = self._run_program(program_id=program_id, - hub="defaultHub", group="defaultGroup", project="defaultProject") - job.wait_for_final_state() - rjobs = self.runtime.jobs(program_id=program_id, - hub="defaultHub", group="defaultGroup", project="defaultProject") - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - rjobs = self.runtime.jobs(program_id=program_id, - hub="test", group="test", project="test") - self.assertFalse(rjobs) - with self.assertRaises(IBMInputValueError): - self.runtime.jobs(hub="defaultHub") - - def test_cancel_job(self): - """Test canceling a job.""" - job = self._run_program(job_classes=CancelableRuntimeJob) - time.sleep(1) - job.cancel() - self.assertEqual(job.status(), JobStatus.CANCELLED) - rjob = self.runtime.job(job.job_id) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) - - def test_final_result(self): - """Test getting final result.""" - job = self._run_program() - result = job.result() - self.assertTrue(result) - - def test_job_status(self): - """Test job status.""" - job = self._run_program() - time.sleep(random.randint(1, 5)) - self.assertTrue(job.status()) - - def test_job_inputs(self): - """Test job inputs.""" - inputs = {"param1": "foo", "param2": "bar"} - job = self._run_program(inputs=inputs) - self.assertEqual(inputs, job.inputs) - - def test_job_program_id(self): - """Test job program ID.""" - program_id = self._upload_program() - job = self._run_program(program_id=program_id) - self.assertEqual(program_id, job.program_id) - - def test_wait_for_final_state(self): - """Test wait for final state.""" - job = self._run_program() - job.wait_for_final_state() - self.assertEqual(JobStatus.DONE, job.status()) - - def test_result_decoder(self): - """Test result decoder.""" - custom_result = get_complex_types() - job_cls = CustomResultRuntimeJob - job_cls.custom_result = custom_result - - sub_tests = [(SerializableClassDecoder, None), (None, SerializableClassDecoder)] - for result_decoder, decoder in sub_tests: - with self.subTest(decoder=decoder): - job = self._run_program(job_classes=job_cls, decoder=result_decoder) - result = job.result(decoder=decoder) - self.assertIsInstance(result['serializable_class'], SerializableClass) - - def test_get_result_twice(self): - """Test getting results multiple times.""" - custom_result = get_complex_types() - job_cls = CustomResultRuntimeJob - job_cls.custom_result = custom_result - - job = self._run_program(job_classes=job_cls) - _ = job.result() - _ = job.result() - - def test_program_metadata(self): - """Test program metadata.""" - file_name = "test_metadata.json" - with open(file_name, 'w') as file: - json.dump(self.DEFAULT_METADATA, file) - self.addCleanup(os.remove, file_name) - - sub_tests = [file_name, self.DEFAULT_METADATA] - - for metadata in sub_tests: - with self.subTest(metadata_type=type(metadata)): - program_id = self.runtime.upload_program(data=self.DEFAULT_DATA, metadata=metadata) - program = self.runtime.program(program_id) - self.runtime.delete_program(program_id) - self._validate_program(program) - - def test_different_providers(self): - """Test retrieving job submitted with different provider.""" - program_id = self._upload_program() - job = self._run_program(program_id) - cred = Credentials(token="", url="", hub="hub2", group="group2", project="project2", - services={"runtime": "https://quantum-computing.ibm.com"}) - self.runtime._default_hgp.credentials = cred - rjob = self.runtime.job(job.job_id) - self.assertIsNotNone(rjob.backend) - - def _upload_program(self, name=None, max_execution_time=300, - is_public: bool = False): - """Upload a new program.""" - name = name or uuid.uuid4().hex - data = self.DEFAULT_DATA - metadata = copy.deepcopy(self.DEFAULT_METADATA) - metadata.update(name=name) - metadata.update(is_public=is_public) - metadata.update(max_execution_time=max_execution_time) - program_id = self.runtime.upload_program( - data=data, - metadata=metadata) - return program_id - - def _run_program(self, program_id=None, inputs=None, job_classes=None, final_status=None, - decoder=None, image="", hub=None, group=None, project=None): - """Run a program.""" - options = {'backend_name': "some_backend"} - if final_status is not None: - self.runtime._api_client.set_final_status(final_status) - elif job_classes: - self.runtime._api_client.set_job_classes(job_classes) - elif all([hub, group, project]): - self.runtime._api_client.set_hgp(hub, group, project) - if program_id is None: - program_id = self._upload_program() - with patch('qiskit_ibm.runtime.ibm_runtime_service.RuntimeClient', - return_value=self.runtime._api_client): - job = self.runtime.run(program_id=program_id, options=options, - inputs=inputs, result_decoder=decoder, - image=image) - return job - - def _populate_jobs_with_all_statuses(self, jobs, program_id): - pending_jobs_count = 0 - returned_jobs_count = 0 - for _ in range(3): - jobs.append(self._run_program(program_id, final_status='RUNNING')) - pending_jobs_count += 1 - for _ in range(4): - jobs.append(self._run_program(program_id, final_status='COMPLETED')) - returned_jobs_count += 1 - for _ in range(2): - jobs.append(self._run_program(program_id, final_status='QUEUED')) - pending_jobs_count += 1 - for _ in range(3): - jobs.append(self._run_program(program_id, final_status='FAILED')) - returned_jobs_count += 1 - for _ in range(2): - jobs.append(self._run_program(program_id, final_status='CANCELLED')) - returned_jobs_count += 1 - return (jobs, pending_jobs_count, returned_jobs_count) - - def _validate_program(self, program): - """Validate a program.""" - self.assertEqual(self.DEFAULT_METADATA['name'], program.name) - self.assertEqual(self.DEFAULT_METADATA['description'], program.description) - self.assertEqual(self.DEFAULT_METADATA['max_execution_time'], - program.max_execution_time) - self.assertTrue(program.creation_date) - self.assertTrue(program.update_date) - self.assertEqual(self.DEFAULT_METADATA['spec']['backend_requirements'], - program.backend_requirements) - self.assertEqual(self.DEFAULT_METADATA['spec']['parameters'], - program.parameters().metadata) - self.assertEqual(self.DEFAULT_METADATA['spec']['return_values'], - program.return_values) - self.assertEqual(self.DEFAULT_METADATA['spec']['interim_results'], - program.interim_results) diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py deleted file mode 100644 index 49ea27c94..000000000 --- a/test/ibm/runtime/test_runtime_integration.py +++ /dev/null @@ -1,749 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for runtime service.""" - -import copy -import unittest -import os -import uuid -import time -import random -from contextlib import suppress -import tempfile - -from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES -from qiskit.test.reference_circuits import ReferenceCircuits - -from qiskit_ibm.runtime.constants import API_TO_JOB_ERROR_MESSAGE -from qiskit_ibm.exceptions import IBMNotAuthorizedError -from qiskit_ibm.runtime.runtime_program import RuntimeProgram -from qiskit_ibm.runtime.exceptions import (RuntimeDuplicateProgramError, - RuntimeProgramNotFound, - RuntimeJobFailureError, - RuntimeInvalidStateError, - RuntimeJobNotFound) - -from ...ibm_test_case import IBMTestCase -from ...decorators import requires_runtime_device -from ...proxy_server import MockProxyServer, use_proxies -from .utils import SerializableClass, SerializableClassDecoder, get_complex_types - - -class TestRuntimeIntegration(IBMTestCase): - """Integration tests for runtime modules.""" - - RUNTIME_PROGRAM = """ -import random -import time -import warnings - -from qiskit import transpile -from qiskit.circuit.random import random_circuit - -def prepare_circuits(backend): - circuit = random_circuit(num_qubits=5, depth=4, measure=True, - seed=random.randint(0, 1000)) - return transpile(circuit, backend) - -def main(backend, user_messenger, **kwargs): - iterations = kwargs['iterations'] - sleep_per_iteration = kwargs.pop('sleep_per_iteration', 0) - interim_results = kwargs.pop('interim_results', {}) - final_result = kwargs.pop("final_result", {}) - for it in range(iterations): - time.sleep(sleep_per_iteration) - qc = prepare_circuits(backend) - user_messenger.publish({"iteration": it, "interim_results": interim_results}) - backend.run(qc).result() - - user_messenger.publish(final_result, final=True) - print("this is a stdout message") - warnings.warn("this is a stderr message") - """ - - RUNTIME_PROGRAM_METADATA = { - "max_execution_time": 600, - "description": "Qiskit test program" - } - PROGRAM_PREFIX = 'qiskit-test' - - @classmethod - @requires_runtime_device - def setUpClass(cls, backend): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.backend = backend - cls.poll_time = 1 if backend.configuration().simulator else 5 - cls.provider = backend.provider() - metadata = copy.deepcopy(cls.RUNTIME_PROGRAM_METADATA) - metadata['name'] = cls._get_program_name() - try: - cls.program_id = cls.provider.runtime.upload_program( - data=cls.RUNTIME_PROGRAM, - metadata=metadata) - except RuntimeDuplicateProgramError: - pass - except IBMNotAuthorizedError: - raise unittest.SkipTest("No upload access.") - - @classmethod - def tearDownClass(cls) -> None: - """Class level teardown.""" - super().tearDownClass() - with suppress(Exception): - cls.provider.runtime.delete_program(cls.program_id) - - def setUp(self) -> None: - """Test level setup.""" - super().setUp() - self.to_delete = [] - self.to_cancel = [] - self.proxy_process = None - - def tearDown(self) -> None: - """Test level teardown.""" - super().tearDown() - # Delete programs - for prog in self.to_delete: - with suppress(Exception): - self.provider.runtime.delete_program(prog) - - # Cancel and delete jobs. - for job in self.to_cancel: - with suppress(Exception): - job.cancel() - with suppress(Exception): - self.provider.runtime.delete_job(job.job_id) - - def test_runtime_service(self): - """Test getting runtime service.""" - self.assertTrue(self.provider.service('runtime')) - - def test_list_programs(self): - """Test listing programs.""" - programs = self.provider.runtime.programs() - self.assertTrue(programs) - found = False - for prog in programs: - self._validate_program(prog) - if prog.program_id == self.program_id: - found = True - self.assertTrue(found, f"Program {self.program_id} not found!") - - def test_list_programs_with_limit_skip(self): - """Test listing programs with limit and skip.""" - self._upload_program() - self._upload_program() - self._upload_program() - programs = self.provider.runtime.programs(limit=3, refresh=True) - all_ids = [prog.program_id for prog in programs] - self.assertEqual(len(all_ids), 3) - programs = self.provider.runtime.programs(limit=2, skip=1) - some_ids = [prog.program_id for prog in programs] - self.assertEqual(len(some_ids), 2) - self.assertNotIn(all_ids[0], some_ids) - self.assertIn(all_ids[1], some_ids) - self.assertIn(all_ids[2], some_ids) - - def test_list_program(self): - """Test listing a single program.""" - program = self.provider.runtime.program(self.program_id) - self.assertEqual(self.program_id, program.program_id) - self._validate_program(program) - - def test_retrieve_program_data(self): - """Test retrieving program data""" - program = self.provider.runtime.program(self.program_id) - self.assertEqual(self.RUNTIME_PROGRAM, program.data) - self._validate_program(program) - - def test_retrieve_unauthorized_program_data(self): - """Test retrieving program data when user is not the program author""" - program = self.provider.runtime.program('sample-program') - self._validate_program(program) - with self.assertRaises(IBMNotAuthorizedError): - return program.data - - def test_upload_program(self): - """Test uploading a program.""" - max_execution_time = 3000 - program_id = self._upload_program(max_execution_time=max_execution_time) - self.assertTrue(program_id) - program = self.provider.runtime.program(program_id) - self.assertTrue(program) - self.assertEqual(max_execution_time, program.max_execution_time) - - def test_upload_program_file(self): - """Test uploading a program using a file.""" - temp_fp = tempfile.NamedTemporaryFile(mode='w', delete=False) - self.addCleanup(os.remove, temp_fp.name) - temp_fp.write(self.RUNTIME_PROGRAM) - temp_fp.close() - - program_id = self._upload_program(data=temp_fp.name) - self.assertTrue(program_id) - program = self.provider.runtime.program(program_id) - self.assertTrue(program) - - @unittest.skipIf( - not os.environ.get('QISKIT_IBM_USE_STAGING_CREDENTIALS', ''), - "Only runs on staging" - ) - def test_upload_public_program(self): - """Test uploading a public program.""" - max_execution_time = 3000 - is_public = True - program_id = self._upload_program(max_execution_time=max_execution_time, - is_public=is_public) - self.assertTrue(program_id) - program = self.provider.runtime.program(program_id) - self.assertTrue(program) - self.assertEqual(max_execution_time, program.max_execution_time) - self.assertEqual(program.is_public, is_public) - - @unittest.skipIf( - not os.environ.get('QISKIT_IBM_USE_STAGING_CREDENTIALS', ''), - "Only runs on staging" - ) - def test_set_visibility(self): - """Test setting the visibility of a program.""" - program_id = self._upload_program() - # Get the initial visibility - prog: RuntimeProgram = self.provider.runtime.program(program_id) - start_vis = prog.is_public - # Flip the original value - self.provider.runtime.set_program_visibility(program_id, not start_vis) - # Get the new visibility - prog: RuntimeProgram = self.provider.runtime.program(program_id, refresh=True) - end_vis = prog.is_public - # Verify changed - self.assertNotEqual(start_vis, end_vis) - - def test_delete_program(self): - """Test deleting program.""" - program_id = self._upload_program() - self.provider.runtime.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.provider.runtime.program(program_id, refresh=True) - - def test_double_delete_program(self): - """Test deleting a deleted program.""" - program_id = self._upload_program() - self.provider.runtime.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.provider.runtime.delete_program(program_id) - - def test_update_program_data(self): - """Test updating program data.""" - program_v1 = """ -def main(backend, user_messenger, **kwargs): - return "version 1" - """ - program_v2 = """ -def main(backend, user_messenger, **kwargs): - return "version 2" - """ - program_id = self._upload_program(data=program_v1) - self.assertEqual(program_v1, self.provider.runtime.program(program_id).data) - self.provider.runtime.update_program(program_id=program_id, data=program_v2) - self.assertEqual(program_v2, self.provider.runtime.program(program_id).data) - - def test_update_program_metadata(self): - """Test updating program metadata.""" - program_id = self._upload_program() - original = self.provider.runtime.program(program_id) - new_metadata = { - "name": self._get_program_name(), - "description": "test_update_program_metadata", - "max_execution_time": original.max_execution_time + 100, - "spec": { - "return_values": { - "type": "object", - "description": "Some return value" - } - } - } - self.provider.runtime.update_program(program_id=program_id, metadata=new_metadata) - updated = self.provider.runtime.program(program_id, refresh=True) - self.assertEqual(new_metadata["name"], updated.name) - self.assertEqual(new_metadata["description"], updated.description) - self.assertEqual(new_metadata["max_execution_time"], updated.max_execution_time) - self.assertEqual(new_metadata["spec"]["return_values"], updated.return_values) - - def test_run_program(self): - """Test running a program.""" - job = self._run_program(final_result="foo") - result = job.result() - self.assertEqual(JobStatus.DONE, job.status()) - self.assertEqual("foo", result) - - def test_run_program_failed(self): - """Test a failed program execution.""" - options = {'backend_name': self.backend.name()} - job = self.provider.runtime.run(program_id=self.program_id, inputs={}, options=options) - self.log.info("Runtime job %s submitted.", job.job_id) - - job.wait_for_final_state() - job_result_raw = self.provider.runtime._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertIn(API_TO_JOB_ERROR_MESSAGE['FAILED'].format( - job.job_id, job_result_raw), job.error_message()) - with self.assertRaises(RuntimeJobFailureError) as err_cm: - job.result() - self.assertIn('KeyError', str(err_cm.exception)) - - def test_run_program_failed_ran_too_long(self): - """Test a program that failed since it ran longer than maxiumum execution time.""" - max_execution_time = 60 - inputs = { - 'iterations': 1, - 'sleep_per_iteration': 60 - } - program_id = self._upload_program(max_execution_time=max_execution_time) - options = {'backend_name': self.backend.name()} - job = self.provider.runtime.run(program_id=program_id, inputs=inputs, options=options) - self.log.info("Runtime job %s submitted.", job.job_id) - - job.wait_for_final_state() - job_result_raw = self.provider.runtime._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertIn(API_TO_JOB_ERROR_MESSAGE['CANCELLED - RAN TOO LONG'].format( - job.job_id, job_result_raw), job.error_message()) - with self.assertRaises(RuntimeJobFailureError): - job.result() - - def test_retrieve_job_queued(self): - """Test retrieving a queued job.""" - _ = self._run_program(iterations=10) - job = self._run_program(iterations=2) - self._wait_for_status(job, JobStatus.QUEUED) - rjob = self.provider.runtime.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(self.program_id, rjob.program_id) - - def test_retrieve_job_running(self): - """Test retrieving a running job.""" - job = self._run_program(iterations=10) - self._wait_for_status(job, JobStatus.RUNNING) - rjob = self.provider.runtime.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(self.program_id, job.program_id) - - def test_retrieve_job_done(self): - """Test retrieving a finished job.""" - job = self._run_program() - job.wait_for_final_state() - rjob = self.provider.runtime.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(self.program_id, job.program_id) - - def test_retrieve_all_jobs(self): - """Test retrieving all jobs.""" - job = self._run_program() - rjobs = self.provider.runtime.jobs() - found = False - for rjob in rjobs: - if rjob.job_id == job.job_id: - self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.inputs, rjob.inputs) - found = True - break - self.assertTrue(found, f"Job {job.job_id} not returned.") - - def test_retrieve_jobs_limit(self): - """Test retrieving jobs with limit.""" - jobs = [] - for _ in range(3): - jobs.append(self._run_program()) - - rjobs = self.provider.runtime.jobs(limit=2) - self.assertEqual(len(rjobs), 2) - job_ids = {job.job_id for job in jobs} - rjob_ids = {rjob.job_id for rjob in rjobs} - self.assertTrue(rjob_ids.issubset(job_ids)) - - def test_retrieve_pending_jobs(self): - """Test retrieving pending jobs (QUEUED, RUNNING).""" - job = self._run_program(iterations=10) - self._wait_for_status(job, JobStatus.RUNNING) - rjobs = self.provider.runtime.jobs(pending=True) - found = False - for rjob in rjobs: - if rjob.job_id == job.job_id: - self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.inputs, rjob.inputs) - found = True - break - self.assertTrue(found, f"Pending job {job.job_id} not retrieved.") - - def test_retrieve_returned_jobs(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED).""" - job = self._run_program() - job.wait_for_final_state() - rjobs = self.provider.runtime.jobs(pending=False) - found = False - for rjob in rjobs: - if rjob.job_id == job.job_id: - self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.inputs, rjob.inputs) - found = True - break - self.assertTrue(found, f"Returned job {job.job_id} not retrieved.") - - def test_retrieve_jobs_by_program_id(self): - """Test retrieving jobs by Program ID.""" - program_id = self._upload_program() - job = self._run_program(program_id=program_id) - job.wait_for_final_state() - rjobs = self.provider.runtime.jobs(program_id=program_id) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - - def test_jobs_filter_by_provider(self): - """Test retrieving jobs by provider.""" - hub = self.provider.credentials.hub - group = self.provider.credentials.group - project = self.provider.credentials.project - program_id = self._upload_program() - job = self._run_program(program_id=program_id) - job.wait_for_final_state() - rjobs = self.provider.runtime.jobs(program_id=program_id, - hub=hub, group=group, project=project) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - rjobs = self.provider.runtime.jobs(program_id=program_id, - hub="test", group="test", project="test") - self.assertFalse(rjobs) - - def test_cancel_job_queued(self): - """Test canceling a queued job.""" - _ = self._run_program(iterations=10) - job = self._run_program(iterations=2) - self._wait_for_status(job, JobStatus.QUEUED) - job.cancel() - self.assertEqual(job.status(), JobStatus.CANCELLED) - time.sleep(10) # Wait a bit for DB to update. - rjob = self.provider.runtime.job(job.job_id) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) - - def test_cancel_job_running(self): - """Test canceling a running job.""" - job = self._run_program(iterations=3) - self._wait_for_status(job, JobStatus.RUNNING) - job.cancel() - self.assertEqual(job.status(), JobStatus.CANCELLED) - time.sleep(10) # Wait a bit for DB to update. - rjob = self.provider.runtime.job(job.job_id) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) - - def test_cancel_job_done(self): - """Test canceling a finished job.""" - job = self._run_program() - job.wait_for_final_state() - with self.assertRaises(RuntimeInvalidStateError): - job.cancel() - - def test_delete_job(self): - """Test deleting a job.""" - sub_tests = [JobStatus.QUEUED, JobStatus.RUNNING, JobStatus.DONE] - for status in sub_tests: - with self.subTest(status=status): - if status == JobStatus.QUEUED: - _ = self._run_program(iterations=10) - - job = self._run_program(iterations=2) - self._wait_for_status(job, status) - self.provider.runtime.delete_job(job.job_id) - with self.assertRaises(RuntimeJobNotFound): - self.provider.runtime.job(job.job_id) - - def test_interim_result_callback(self): - """Test interim result callback.""" - def result_callback(job_id, interim_result): - nonlocal final_it - final_it = interim_result['iteration'] - nonlocal callback_err - if job_id != job.job_id: - callback_err.append(f"Unexpected job ID: {job_id}") - if interim_result['interim_results'] != int_res: - callback_err.append(f"Unexpected interim result: {interim_result}") - - int_res = "foo" - final_it = 0 - callback_err = [] - iterations = 3 - job = self._run_program(iterations=iterations, interim_results=int_res, - callback=result_callback) - job.wait_for_final_state() - self.assertEqual(iterations-1, final_it) - self.assertFalse(callback_err) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_stream_results(self): - """Test stream_results method.""" - def result_callback(job_id, interim_result): - nonlocal final_it - final_it = interim_result['iteration'] - nonlocal callback_err - if job_id != job.job_id: - callback_err.append(f"Unexpected job ID: {job_id}") - if interim_result['interim_results'] != int_res: - callback_err.append(f"Unexpected interim result: {interim_result}") - - int_res = "bar" - final_it = 0 - callback_err = [] - iterations = 3 - job = self._run_program(iterations=iterations, interim_results=int_res) - job.stream_results(result_callback) - job.wait_for_final_state() - self.assertEqual(iterations-1, final_it) - self.assertFalse(callback_err) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_stream_results_done(self): - """Test streaming interim results after job is done.""" - def result_callback(job_id, interim_result): - # pylint: disable=unused-argument - nonlocal called_back - called_back = True - - called_back = False - job = self._run_program(interim_results="foobar") - job.wait_for_final_state() - job._status = JobStatus.RUNNING # Allow stream_results() - job.stream_results(result_callback) - time.sleep(2) - self.assertFalse(called_back) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_callback_error(self): - """Test error in callback method.""" - def result_callback(job_id, interim_result): - # pylint: disable=unused-argument - if interim_result['iteration'] == 0: - raise ValueError("Kaboom!") - nonlocal final_it - final_it = interim_result['iteration'] - - final_it = 0 - iterations = 3 - with self.assertLogs('qiskit_ibm.runtime', level='WARNING') as err_cm: - job = self._run_program(iterations=iterations, interim_results="foo", - callback=result_callback) - job.wait_for_final_state() - - self.assertIn("Kaboom", ', '.join(err_cm.output)) - self.assertEqual(iterations-1, final_it) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_callback_cancel_job(self): - """Test canceling a running job while streaming results.""" - def result_callback(job_id, interim_result): - # pylint: disable=unused-argument - nonlocal final_it - final_it = interim_result['iteration'] - - final_it = 0 - iterations = 3 - sub_tests = [JobStatus.QUEUED, JobStatus.RUNNING] - - for status in sub_tests: - with self.subTest(status=status): - if status == JobStatus.QUEUED: - _ = self._run_program(iterations=10) - - job = self._run_program(iterations=iterations, interim_results="foo", - callback=result_callback) - self._wait_for_status(job, status) - job.cancel() - time.sleep(3) # Wait for cleanup - self.assertIsNotNone(job._ws_client._server_close_code) - self.assertLess(final_it, iterations) - - def test_final_result(self): - """Test getting final result.""" - final_result = get_complex_types() - job = self._run_program(final_result=final_result) - result = job.result(decoder=SerializableClassDecoder) - self.assertEqual(final_result, result) - - rresults = self.provider.runtime.job(job.job_id).result(decoder=SerializableClassDecoder) - self.assertEqual(final_result, rresults) - - def test_job_status(self): - """Test job status.""" - job = self._run_program(iterations=1) - time.sleep(random.randint(1, 5)) - self.assertTrue(job.status()) - - def test_job_inputs(self): - """Test job inputs.""" - interim_results = get_complex_types() - inputs = {'iterations': 1, - 'interim_results': interim_results} - options = {'backend_name': self.backend.name()} - job = self.provider.runtime.run(program_id=self.program_id, inputs=inputs, - options=options) - self.log.info("Runtime job %s submitted.", job.job_id) - self.to_cancel.append(job) - self.assertEqual(inputs, job.inputs) - rjob = self.provider.runtime.job(job.job_id) - rinterim_results = rjob.inputs['interim_results'] - self._assert_complex_types_equal(interim_results, rinterim_results) - - def test_job_backend(self): - """Test job backend.""" - job = self._run_program() - self.assertEqual(self.backend, job.backend) - - def test_job_program_id(self): - """Test job program ID.""" - job = self._run_program() - self.assertEqual(self.program_id, job.program_id) - - def test_wait_for_final_state(self): - """Test wait for final state.""" - job = self._run_program() - job.wait_for_final_state() - self.assertEqual(JobStatus.DONE, job.status()) - - def test_logout(self): - """Test logout.""" - self.provider.runtime.logout() - # Make sure we can still do things. - self._upload_program() - _ = self._run_program() - - def test_run_circuit(self): - """Test run_circuits""" - job = self.provider.run_circuits( - ReferenceCircuits.bell(), backend_name=self.backend.name(), shots=100) - counts = job.result().get_counts() - self.assertEqual(100, sum(counts.values())) - - def test_job_creation_date(self): - """Test job creation date.""" - job = self._run_program(iterations=1) - self.assertTrue(job.creation_date) - rjob = self.provider.runtime.job(job.job_id) - self.assertTrue(rjob.creation_date) - rjobs = self.provider.runtime.jobs(limit=2) - for rjob in rjobs: - self.assertTrue(rjob.creation_date) - - def test_websocket_proxy(self): - """Test connecting to websocket via proxy.""" - def result_callback(job_id, interim_result): # pylint: disable=unused-argument - nonlocal callback_called - callback_called = True - - MockProxyServer(self, self.log).start() - callback_called = False - - with use_proxies(self.provider.runtime._default_hgp, MockProxyServer.VALID_PROXIES): - job = self._run_program(iterations=1, callback=result_callback) - job.wait_for_final_state() - - self.assertTrue(callback_called) - - def test_websocket_proxy_invalid_port(self): - """Test connecting to websocket via invalid proxy port.""" - def result_callback(job_id, interim_result): # pylint: disable=unused-argument - nonlocal callback_called - callback_called = True - - callback_called = False - invalid_proxy = {'https': 'http://{}:{}'.format(MockProxyServer.PROXY_IP_ADDRESS, - MockProxyServer.INVALID_PROXY_PORT)} - with use_proxies(self.provider.runtime._default_hgp, invalid_proxy): - with self.assertLogs('qiskit_ibm', 'WARNING') as log_cm: - job = self._run_program(iterations=1, callback=result_callback) - job.wait_for_final_state() - self.assertIn("WebsocketError", ','.join(log_cm.output)) - self.assertFalse(callback_called) - - def test_job_logs(self): - """Test job logs.""" - job = self._run_program(final_result="foo") - with self.assertLogs('qiskit_ibm', 'WARN'): - job.logs() - job.wait_for_final_state() - job_logs = job.logs() - self.assertIn("this is a stdout message", job_logs) - self.assertIn("this is a stderr message", job_logs) - - def _validate_program(self, program): - """Validate a program.""" - self.assertTrue(program) - self.assertTrue(program.name) - self.assertTrue(program.program_id) - self.assertTrue(program.description) - self.assertTrue(program.max_execution_time) - self.assertTrue(program.creation_date) - self.assertTrue(program.update_date) - - def _upload_program( - self, - name=None, - max_execution_time=300, - data=None, - is_public: bool = False): - """Upload a new program.""" - name = name or self._get_program_name() - data = data or self.RUNTIME_PROGRAM - metadata = copy.deepcopy(self.RUNTIME_PROGRAM_METADATA) - metadata['name'] = name - metadata['max_execution_time'] = max_execution_time - metadata['is_public'] = is_public - program_id = self.provider.runtime.upload_program( - data=data, - metadata=metadata) - self.to_delete.append(program_id) - return program_id - - @classmethod - def _get_program_name(cls): - """Return a unique program name.""" - return cls.PROGRAM_PREFIX + "_" + uuid.uuid4().hex - - def _assert_complex_types_equal(self, expected, received): - """Verify the received data in complex types is expected.""" - if 'serializable_class' in received: - received['serializable_class'] = \ - SerializableClass.from_json(received['serializable_class']) - self.assertEqual(expected, received) - - def _run_program(self, program_id=None, iterations=1, - interim_results=None, final_result=None, - callback=None): - """Run a program.""" - inputs = {'iterations': iterations, - 'interim_results': interim_results or {}, - 'final_result': final_result or {}} - pid = program_id or self.program_id - options = {'backend_name': self.backend.name()} - job = self.provider.runtime.run(program_id=pid, inputs=inputs, - options=options, callback=callback) - self.log.info("Runtime job %s submitted.", job.job_id) - self.to_cancel.append(job) - return job - - def _wait_for_status(self, job, status): - """Wait for job to reach a certain status.""" - wait_time = 1 if status == JobStatus.QUEUED else self.poll_time - while job.status() not in JOB_FINAL_STATES + (status,): - time.sleep(wait_time) - if job.status() != status: - self.skipTest(f"Job {job.job_id} unable to reach status {status}.") diff --git a/test/ibm/runtime/test_runtime_ws.py b/test/ibm/runtime/test_runtime_ws.py deleted file mode 100644 index 79e64f178..000000000 --- a/test/ibm/runtime/test_runtime_ws.py +++ /dev/null @@ -1,184 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test for the Websocket client.""" - -import time - -from qiskit.test.mock.fake_qasm_simulator import FakeQasmSimulator - -from qiskit_ibm.credentials import Credentials -from qiskit_ibm.runtime import RuntimeJob -from qiskit_ibm.runtime.exceptions import RuntimeInvalidStateError - -from ...ibm_test_case import IBMTestCase -from ...ws_server import MockWsServer -from .ws_handler import (websocket_handler, JOB_ID_PROGRESS_DONE, JOB_ID_ALREADY_DONE, - JOB_ID_RETRY_SUCCESS, JOB_ID_RETRY_FAILURE, - JOB_PROGRESS_RESULT_COUNT) -from .fake_runtime_client import BaseFakeRuntimeClient - - -class TestRuntimeWebsocketClient(IBMTestCase): - """Tests for the the websocket client against a mock server.""" - - @classmethod - def setUpClass(cls): - """Initial class level setup.""" - super().setUpClass() - # Launch the mock server. - cls.server = MockWsServer(websocket_handler, cls.log) - cls.server.start() - - @classmethod - def tearDownClass(cls): - """Class level cleanup.""" - super().tearDownClass() - - # Close the mock server. - cls.server.stop() - - def test_interim_result_callback(self): - """Test interim result callback.""" - def result_callback(job_id, interim_result): - nonlocal results - results.append(interim_result) - self.assertEqual(JOB_ID_PROGRESS_DONE, job_id) - - results = [] - job = self._get_job(callback=result_callback) - time.sleep(JOB_PROGRESS_RESULT_COUNT+2) - self.assertEqual(JOB_PROGRESS_RESULT_COUNT, len(results)) - self.assertFalse(job._ws_client.connected) - - def test_stream_results(self): - """Test streaming results.""" - def result_callback(job_id, interim_result): - nonlocal results - results.append(interim_result) - self.assertEqual(JOB_ID_PROGRESS_DONE, job_id) - - results = [] - job = self._get_job() - job.stream_results(callback=result_callback) - time.sleep(JOB_PROGRESS_RESULT_COUNT+2) - self.assertEqual(JOB_PROGRESS_RESULT_COUNT, len(results)) - self.assertFalse(job._ws_client.connected) - - def test_duplicate_streaming(self): - """Testing duplicate streaming.""" - def result_callback(job_id, interim_result): - nonlocal results - results.append(interim_result) - self.assertEqual(JOB_ID_PROGRESS_DONE, job_id) - - results = [] - job = self._get_job(callback=result_callback) - time.sleep(1) - with self.assertRaises(RuntimeInvalidStateError): - job.stream_results(callback=result_callback) - - def test_cancel_streaming(self): - """Test canceling streaming.""" - def result_callback(job_id, interim_result): - nonlocal results - results.append(interim_result) - self.assertEqual(JOB_ID_PROGRESS_DONE, job_id) - - results = [] - job = self._get_job(callback=result_callback) - time.sleep(1) - job.cancel_result_streaming() - for _ in range(10): - self.log.debug("Waiting for client to finish disconnect.") - if not job._ws_client.connected: - break - time.sleep(1) - self.assertFalse(job._ws_client.connected) - - def test_cancel_closed_streaming(self): - """Test canceling streaming that's already closed.""" - def result_callback(job_id, interim_result): - nonlocal results - results.append(interim_result) - self.assertEqual(JOB_ID_ALREADY_DONE, job_id) - - results = [] - job = self._get_job(callback=result_callback, job_id=JOB_ID_ALREADY_DONE) - time.sleep(2) - job.cancel_result_streaming() - self.assertFalse(job._ws_client.connected) - - def test_completed_job(self): - """Test callback from completed job.""" - def result_callback(job_id, interim_result): - nonlocal results - results.append(interim_result) - self.assertEqual(JOB_ID_ALREADY_DONE, job_id) - - results = [] - job = self._get_job(callback=result_callback, job_id=JOB_ID_ALREADY_DONE) - time.sleep(2) - self.assertEqual(0, len(results)) - self.assertFalse(job._ws_client.connected) - - def test_completed_job_stream(self): - """Test streaming from completed job.""" - def result_callback(job_id, interim_result): - nonlocal results - results.append(interim_result) - self.assertEqual(JOB_ID_ALREADY_DONE, job_id) - - results = [] - job = self._get_job(job_id=JOB_ID_ALREADY_DONE) - job.stream_results(callback=result_callback) - time.sleep(2) - self.assertEqual(0, len(results)) - self.assertFalse(job._ws_client.connected) - - def test_websocket_retry_success(self): - """Test successful retry.""" - def result_callback(job_id, interim_result): - nonlocal results - results.append(interim_result) - self.assertEqual(JOB_ID_RETRY_SUCCESS, job_id) - - results = [] - job = self._get_job(job_id=JOB_ID_RETRY_SUCCESS, callback=result_callback) - time.sleep(JOB_PROGRESS_RESULT_COUNT+2) - self.assertEqual(JOB_PROGRESS_RESULT_COUNT, len(results)) - self.assertFalse(job._ws_client.connected) - - def test_websocket_retry_failure(self): - """Test failed retry.""" - def result_callback(job_id, interim_result): - nonlocal results - results.append(interim_result) - self.assertEqual(JOB_ID_RETRY_FAILURE, job_id) - - results = [] - job = self._get_job(job_id=JOB_ID_RETRY_FAILURE, callback=result_callback) - time.sleep(20) # Need to wait for all retries. - self.assertEqual(0, len(results)) - self.assertFalse(job._ws_client.connected) - - def _get_job(self, callback=None, job_id=JOB_ID_PROGRESS_DONE): - """Get a runtime job.""" - cred = Credentials(token="my_token", url="", - services={"runtime": MockWsServer.VALID_WS_URL}) - job = RuntimeJob(backend=FakeQasmSimulator(), - api_client=BaseFakeRuntimeClient(), - credentials=cred, - job_id=job_id, - program_id="my-program", - user_callback=callback) - return job diff --git a/test/ibm/runtime/utils.py b/test/ibm/runtime/utils.py deleted file mode 100644 index b929d2ce1..000000000 --- a/test/ibm/runtime/utils.py +++ /dev/null @@ -1,67 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Utility functions for runtime testing.""" - -import json - -from qiskit_ibm.runtime import ResultDecoder - - -def get_complex_types(): - """Return a dictionary with values of more complicated types.""" - return {"string": "foo", - "float": 1.5, - "complex": 2+3j, - "serializable_class": SerializableClass("foo")} - - -class SerializableClass: - """Custom class with serialization methods.""" - - def __init__(self, value): - self.value = value - - def to_json(self): - """To JSON serializable.""" - return json.dumps({"value": self.value}) - - @classmethod - def from_json(cls, json_str): - """From JSON serializable.""" - return cls(**json.loads(json_str)) - - def __eq__(self, other): - return self.value == other.value - - -class SerializableClassDecoder(ResultDecoder): - """Decoder used for decode SerializableClass in job result.""" - - @classmethod - def decode(cls, data): - """Decode input data.""" - decoded = super().decode(data) - if 'serializable_class' in decoded: - decoded['serializable_class'] = \ - SerializableClass.from_json(decoded['serializable_class']) - return decoded - - -class UnserializableClass: - """Custom class without serialization methods.""" - - def __init__(self, value): - self.value = value - - def __eq__(self, other): - return self.value == other.value diff --git a/test/ibm/runtime/ws_handler.py b/test/ibm/runtime/ws_handler.py deleted file mode 100644 index 2cc6bb68b..000000000 --- a/test/ibm/runtime/ws_handler.py +++ /dev/null @@ -1,65 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Websocket server for testing purposes.""" - -import asyncio - -JOB_ID_PROGRESS_DONE = 'JOB_ID_PROGRESS_DONE' -JOB_ID_ALREADY_DONE = 'JOB_ID_ALREADY_DONE' -JOB_ID_RETRY_SUCCESS = 'JOB_ID_RETRY_SUCCESS' -JOB_ID_RETRY_FAILURE = 'JOB_ID_RETRY_FAILURE' -JOB_PROGRESS_RESULT_COUNT = 5 - - -async def websocket_handler(websocket, path): - """Entry point for the websocket mock server.""" - request = path.split('/')[-1] - await websocket.send("ACK") - - if request == JOB_ID_PROGRESS_DONE: - await handle_job_progress_done(websocket) - elif request == JOB_ID_ALREADY_DONE: - await handle_job_already_done(websocket) - elif request == JOB_ID_RETRY_SUCCESS: - await handle_token_retry_success(websocket) - elif request == JOB_ID_RETRY_FAILURE: - await handle_token_retry_failure(websocket) - else: - raise ValueError(f"Unknown request {request}") - - -async def handle_job_progress_done(websocket): - """Send a few results then close with 1000.""" - for idx in range(JOB_PROGRESS_RESULT_COUNT): - await websocket.send(f"foo{idx}") - await asyncio.sleep(1) - await websocket.close(code=1000) - - -async def handle_job_already_done(websocket): - """Close immediately with 1000.""" - await websocket.close(code=1000) - - -async def handle_token_retry_success(websocket): - """Close the socket once and force a retry.""" - if not hasattr(handle_token_retry_success, 'retry_attempt'): - setattr(handle_token_retry_success, 'retry_attempt', True) - await handle_token_retry_failure(websocket) - else: - await handle_job_progress_done(websocket) - - -async def handle_token_retry_failure(websocket): - """Continually close the socket, until both the first attempt and retry fail.""" - await websocket.close(code=1011)