diff --git a/.travis.yml b/.travis.yml index 6ae695563..643652dfa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,15 +33,8 @@ sudo: false stage_generic: &stage_generic install: # Install step for jobs that require compilation and qa. - # TODO: until terra 0.8 is released, install a terra branch and this - # package dependencies manually. - # - pip install -U -r requirements.txt - - pip install "requests>=2.19" - - pip install "requests-ntlm>=1.1.0" - - pip install "websockets>=7,<8" - - git clone https://github.com/Qiskit/qiskit-terra.git - pip install cython - - pip install -e qiskit-terra + - pip install -U -r requirements.txt - pip install -U -r requirements-dev.txt script: # Run the tests. diff --git a/qiskit/providers/ibmq/api/ibmqconnector.py b/qiskit/providers/ibmq/api/ibmqconnector.py index 75ebaf32b..3136c0ff9 100644 --- a/qiskit/providers/ibmq/api/ibmqconnector.py +++ b/qiskit/providers/ibmq/api/ibmqconnector.py @@ -385,6 +385,33 @@ def available_backends(self): return response + def circuit_run(self, name, **kwargs): + """Execute a Circuit. + + Args: + name (str): name of the Circuit. + **kwargs (dict): arguments for the Circuit. + + Returns: + dict: json response. + + Raises: + CredentialsError: if the user was not authenticated. + """ + if not self.check_credentials(): + raise CredentialsError('credentials invalid') + + url = '/QCircuitApiModels' + + payload = { + 'name': name, + 'params': kwargs + } + + response = self.req.post(url, data=json.dumps(payload)) + + return response + def websocket_client(self): """Return a websocket client for interacting with IBMQ. diff --git a/qiskit/providers/ibmq/api_v2/exceptions.py b/qiskit/providers/ibmq/api_v2/exceptions.py index 22d2ca40a..46adc7bde 100644 --- a/qiskit/providers/ibmq/api_v2/exceptions.py +++ b/qiskit/providers/ibmq/api_v2/exceptions.py @@ -18,6 +18,11 @@ class ApiError(ApiErrorV1): """Generic IBM Q API error.""" - def __init__(self, *args, original_exception=None, **kwargs): + pass + + +class RequestsApiError(ApiError): + """Exception re-raising a RequestException.""" + def __init__(self, original_exception, *args, **kwargs): self.original_exception = original_exception super().__init__(*args, **kwargs) diff --git a/qiskit/providers/ibmq/api_v2/ibmqclient.py b/qiskit/providers/ibmq/api_v2/ibmqclient.py index 5296d193a..a83c5a4b5 100644 --- a/qiskit/providers/ibmq/api_v2/ibmqclient.py +++ b/qiskit/providers/ibmq/api_v2/ibmqclient.py @@ -203,6 +203,18 @@ def api_version(self): """ return self.api_client.version() + def circuit_run(self, name, **kwargs): + """Execute a Circuit. + + Args: + name (str): name of the Circuit. + **kwargs (dict): arguments for the Circuit. + + Returns: + dict: json response. + """ + return self.api_client.circuit(name, **kwargs) + # Endpoints for compatibility with classic IBMQConnector. These functions # are meant to facilitate the transition, and should be removed moving # forward. diff --git a/qiskit/providers/ibmq/api_v2/rest/auth.py b/qiskit/providers/ibmq/api_v2/rest/auth.py index d07e3a564..8e0bb2a93 100644 --- a/qiskit/providers/ibmq/api_v2/rest/auth.py +++ b/qiskit/providers/ibmq/api_v2/rest/auth.py @@ -45,9 +45,8 @@ def user_info(self): # Revise the URL. try: api_url = response['urls']['http'] - if api_url.endswith('.com?private=true'): - response['urls']['http'] = '{}/api'.format( - api_url.split('?')[0]) + if not api_url.endswith('/api'): + response['urls']['http'] = '{}/api'.format(api_url) except KeyError: pass diff --git a/qiskit/providers/ibmq/api_v2/rest/root.py b/qiskit/providers/ibmq/api_v2/rest/root.py index 1ecf0c272..4a9b9cf06 100644 --- a/qiskit/providers/ibmq/api_v2/rest/root.py +++ b/qiskit/providers/ibmq/api_v2/rest/root.py @@ -29,6 +29,7 @@ class Api(RestAdapterBase): 'hubs': '/Network', 'jobs': '/Jobs', 'jobs_status': '/Jobs/status', + 'circuit': '/QCircuitApiModels', 'version': '/version' } @@ -100,9 +101,30 @@ def run_job(self, backend_name, qobj_dict): """ url = self.get_url('jobs') - payload = {'qObject': qobj_dict, - 'backend': {'name': backend_name}, - 'shots': qobj_dict.get('config', {}).get('shots', 1)} + payload = { + 'qObject': qobj_dict, + 'backend': {'name': backend_name}, + 'shots': qobj_dict.get('config', {}).get('shots', 1) + } + + return self.session.post(url, json=payload).json() + + def circuit(self, name, **kwargs): + """Execute a Circuit. + + Args: + name (str): name of the Circuit. + **kwargs (dict): arguments for the Circuit. + + Returns: + dict: json response. + """ + url = self.get_url('circuit') + + payload = { + 'name': name, + 'params': kwargs + } return self.session.post(url, json=payload).json() diff --git a/qiskit/providers/ibmq/api_v2/session.py b/qiskit/providers/ibmq/api_v2/session.py index 4c57d0ce8..daf63f2a4 100644 --- a/qiskit/providers/ibmq/api_v2/session.py +++ b/qiskit/providers/ibmq/api_v2/session.py @@ -18,8 +18,7 @@ from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry -from .exceptions import ApiError - +from .exceptions import RequestsApiError STATUS_FORCELIST = ( 500, # Internal Server Error @@ -116,7 +115,6 @@ def request(self, method, url, **kwargs): if self.access_token: message = message.replace(self.access_token, '[redacted]') - raise ApiError(message, - original_exception=ex) + raise RequestsApiError(ex, message) from None return response diff --git a/qiskit/providers/ibmq/circuits/__init__.py b/qiskit/providers/ibmq/circuits/__init__.py new file mode 100644 index 000000000..96dae257d --- /dev/null +++ b/qiskit/providers/ibmq/circuits/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Module for interacting with Circuits.""" + +from .manager import CircuitsManager diff --git a/qiskit/providers/ibmq/circuits/exceptions.py b/qiskit/providers/ibmq/circuits/exceptions.py new file mode 100644 index 000000000..5c0de6605 --- /dev/null +++ b/qiskit/providers/ibmq/circuits/exceptions.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2019. +# +# 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 Circuits.""" + +from qiskit.providers.ibmq.exceptions import IBMQError + + +CIRCUIT_NOT_ALLOWED = 'Circuit support is not available yet in this account' +CIRCUIT_SUBMIT_ERROR = 'Circuit could not be submitted: {}' +CIRCUIT_RESULT_ERROR = 'Circuit result could not be returned: {}' + + +class CircuitError(IBMQError): + """Generic Circuit exception.""" + pass + + +class CircuitAvailabilityError(CircuitError): + """Error while accessing a Circuit.""" + + def __init__(self, message=''): + super().__init__(message or CIRCUIT_NOT_ALLOWED) + + +class CircuitSubmitError(CircuitError): + """Error while submitting a Circuit.""" + + def __init__(self, message): + super().__init__(CIRCUIT_SUBMIT_ERROR.format(message)) + + +class CircuitResultError(CircuitError): + """Error during the results of a Circuit.""" + + def __init__(self, message): + super().__init__(CIRCUIT_RESULT_ERROR.format(message)) diff --git a/qiskit/providers/ibmq/circuits/manager.py b/qiskit/providers/ibmq/circuits/manager.py new file mode 100644 index 000000000..42eec1045 --- /dev/null +++ b/qiskit/providers/ibmq/circuits/manager.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2019. +# +# 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. + +"""Manager for interacting with Circuits.""" + +from functools import wraps + +from qiskit.providers import JobStatus +from qiskit.providers.ibmq.ibmqjob import IBMQJob +from qiskit.providers.ibmq.api_v2.exceptions import RequestsApiError + +from .exceptions import (CircuitError, + CircuitAvailabilityError, CircuitResultError, + CircuitSubmitError) + + +GRAPH_STATE = 'graph_state' +HARDWARE_EFFICIENT = 'hardware_efficient' +RANDOM_UNIFORM = 'random_uniform' + + +def requires_api_connection(func): + """Decorator that ensures that a CircuitsManager has a valid API.""" + @wraps(func) + def wrapper(self, *args, **kwargs): + if not self.client: + raise CircuitAvailabilityError( + 'An account must be loaded in order to use Circuits') + + return func(self, *args, **kwargs) + + return wrapper + + +class CircuitsManager: + """Class that provides access to the different Circuits.""" + def __init__(self): + self.client = None + + def _call_circuit(self, name, **kwargs): + """Execute a Circuit. + + Args: + name (str): name of the Circuit. + **kwargs: parameters passed to the Circuit. + + Returns: + Result: the result of executing the circuit. + + Raises: + CircuitAvailabilityError: if Circuits are not available. + CircuitSubmitError: if there was an error submitting the Circuit. + CircuitResultError: if the result of the Circuit could not be + returned. + """ + try: + response = self.client.circuit_run(name=name, **kwargs) + except RequestsApiError as ex: + # Revise the original requests exception to intercept. + response = ex.original_exception.response + + # Check for errors related to the submission. + try: + body = response.json() + except ValueError: + body = {} + + # Generic authorization or unavailable endpoint error. + if response.status_code in (401, 404): + raise CircuitAvailabilityError() from None + + if response.status_code == 400: + # Hub permission error. + if body.get('error', {}).get('code') == 'HUB_NOT_FOUND': + raise CircuitAvailabilityError() from None + + # Generic error. + if body.get('error', {}).get('code') == 'GENERIC_ERROR': + raise CircuitAvailabilityError() from None + + # Handle the rest of the exceptions as unexpected. + raise CircuitSubmitError(str(ex)) + except Exception as ex: + # Handle non-requests exception as unexpected. + raise CircuitSubmitError(str(ex)) + + # Extra check for IBMQConnector code path. + if 'error' in response: + if response['error'].get('code') == 'HUB_NOT_FOUND': + raise CircuitAvailabilityError() from None + raise CircuitSubmitError(str(response)) + + # Create a Job for the circuit. + try: + job = IBMQJob(backend=None, + job_id=response['id'], + api=self.client, + creation_date=response['creationDate'], + api_status=response['status']) + except Exception as ex: + raise CircuitResultError(str(ex)) + + # Wait for the job to complete, explicitly checking for errors. + job._wait_for_completion() + if job.status() is JobStatus.ERROR: + raise CircuitResultError( + 'Job {} finished with an error'.format(job.job_id())) + + return job.result() + + @requires_api_connection + def graph_state(self, number_of_qubits, adjacency_matrix, angles): + """Execute the graph state Circuit. + + This circuit implements graph state circuits that are measured in a + product basis. Measurement angles can be chosen to measure graph state + stabilizers (for validation/characterization) or to measure in a basis + such that the circuit family may be hard to classically simulate. + + Args: + number_of_qubits (int): number of qubits to use, in the 2-20 range. + adjacency_matrix (list[list]): square matrix of elements whose + values are 0 or 1. The matrix size is `number_of_qubits` by + `number_of_qubits` and is expected to be symmetric and have + zeros on the diagonal. + angles (list[float]): list of phase angles, each in the interval + `[0, 2*pi)` radians. There should be 3 * number_of_qubits + elements in the array. The first three elements are the + theta, phi, and lambda angles, respectively, of a u3 gate + acting on the first qubit. Each of the number_of_qubits triples + is interpreted accordingly as the parameters of a u3 gate + acting on subsequent qubits. + + Returns: + Result: the result of executing the circuit. + + Raises: + CircuitError: if the parameters are not valid. + """ + if 2 <= number_of_qubits <= 20: + raise CircuitError('Invalid number_of_qubits') + if len(angles) != number_of_qubits*3: + raise CircuitError('Invalid angles length') + + return self._call_circuit(name=GRAPH_STATE, + number_of_qubits=number_of_qubits, + adjacency_matrix=adjacency_matrix, + angles=angles) + + @requires_api_connection + def hardware_efficient(self, number_of_qubits, angles): + """Execute the hardware efficient Circuit. + + This circuit implements the random lattice circuit across a user + specified number of qubits and phase angles. + + Args: + number_of_qubits (int): number of qubits to use, in the 4-20 range. + angles (list): array of three phase angles (x/y/z) each from + 0 to 2*Pi, one set for each qubit of each layer of the lattice. + There should be 3 * number_of_qubits * desired lattice depth + entries in the array. + + Returns: + Result: the result of executing the circuit. + + Raises: + CircuitError: if the parameters are not valid. + """ + if 4 <= number_of_qubits <= 20: + raise CircuitError('Invalid number_of_qubits') + if len(angles) % 3*number_of_qubits != 0: + raise CircuitError('Invalid angles length') + + return self._call_circuit(name=HARDWARE_EFFICIENT, + number_of_qubits=number_of_qubits, + angles=angles) + + @requires_api_connection + def random_uniform(self, number_of_qubits=None): + """Execute the random uniform Circuit. + + This circuit implements hadamard gates across all available qubits on + the device. + + Args: + number_of_qubits (int) : optional argument for number of qubits to + use. If not specified will use all qubits on device. + + Returns: + Result: the result of executing the circuit. + """ + kwargs = {} + if number_of_qubits is not None: + kwargs['number_of_qubits'] = number_of_qubits + + return self._call_circuit(name=RANDOM_UNIFORM, **kwargs) diff --git a/qiskit/providers/ibmq/ibmqprovider.py b/qiskit/providers/ibmq/ibmqprovider.py index 4c6d65e76..ae50c9c52 100644 --- a/qiskit/providers/ibmq/ibmqprovider.py +++ b/qiskit/providers/ibmq/ibmqprovider.py @@ -24,6 +24,7 @@ read_credentials_from_qiskitrc, store_credentials, discover_credentials) from .exceptions import IBMQAccountError from .ibmqsingleprovider import IBMQSingleProvider +from .circuits import CircuitsManager QE_URL = 'https://quantumexperience.ng.bluemix.net/api' @@ -42,6 +43,12 @@ def __init__(self): # keys are tuples (hub, group, project), as the convention is that # that tuple uniquely identifies a set of credentials. self._accounts = OrderedDict() + self._circuits_manager = CircuitsManager() + + @property + def circuits(self): + """Entry point for Circuit invocation.""" + return self._circuits_manager def backends(self, name=None, filters=None, **kwargs): """Return all backends accessible via IBMQ provider, subject to optional filtering. @@ -211,6 +218,11 @@ def disable_accounts(self, **kwargs): credentials = Credentials(current_creds[creds].credentials.token, current_creds[creds].credentials.url) if self._credentials_match_filter(credentials, kwargs): + # Remove api from circuits manager if in use. + if (self._accounts[credentials.unique_id()]._api == + self._circuits_manager.client): + self._circuits_manager.client = None + del self._accounts[credentials.unique_id()] disabled = True @@ -249,14 +261,27 @@ def _append_account(self, credentials): Returns: IBMQSingleProvider: new single-account provider. """ + update_circuits_manager = False + # Use the first account as the account for circuits. + if not self._accounts: + update_circuits_manager = True + # Check if duplicated credentials are already in use. By convention, # we assume (hub, group, project) is always unique. if credentials.unique_id() in self._accounts.keys(): warnings.warn('Credentials are already in use.') + # Remove api from circuits manager if in use. + if (self._accounts[credentials.unique_id()]._api == + self._circuits_manager.client): + update_circuits_manager = True + single_provider = IBMQSingleProvider(credentials, self) self._accounts[credentials.unique_id()] = single_provider + if update_circuits_manager: + self._circuits_manager.client = single_provider._api + return single_provider def _credentials_match_filter(self, credentials, filter_dict): diff --git a/requirements.txt b/requirements.txt index f199e5d3a..6b05fc6c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +qiskit-terra>=0.8 requests>=2.19 requests-ntlm>=1.1.0 websockets>=7,<8