Skip to content

Commit

Permalink
v0.3 API + debiasing (#6077)
Browse files Browse the repository at this point in the history
* Upgrade to API v0.3

This new version of the API separates job status and metadata from actual results into two different endpoints.

v0.3 also includes support for error_mitigation settings like symmetrization as described in https://arxiv.org/pdf/2301.07233.pdf

Because of that:
- added a new error_mitigation parameter on job submission so users can configure it
- added a new aggregation parameter on results that would allow getting results aggregated under the two different methods described in the paper. Averaging and Plurality voting

* fix formatting and linting

* pass cirq linting

* update job_retry_409 to v0.3

* add _IonQClient.get_results test

* rename symmetrization to debiasing

* rename aggregation to sharpen

* update ionq cirq docs

* add extra_request_payload param

* fix tests

* fix typing

* test get_results with extra payload

* rename to extra_query_params

* Updates access and service docs files for IonQ service.

* Changes access.md and service.md docs for IonQ service

* Updates to service.md for IonQ service docs

* improve sharpen docstring

* fix api_versioning failure

* test run_sweep

* fix run_sweep test

---------

Co-authored-by: Mauricio Muñoz <gmauricio.munoz@gmail.com>
Co-authored-by: Patrick Deuley <deuley@ionq.co>
  • Loading branch information
3 people committed Aug 30, 2023
1 parent b70b2fc commit 0e80fa5
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 94 deletions.
52 changes: 47 additions & 5 deletions cirq-ionq/cirq_ionq/ionq_client.py
Expand Up @@ -54,14 +54,14 @@ class _IonQClient:
"""

SUPPORTED_TARGETS = {'qpu', 'simulator'}
SUPPORTED_VERSIONS = {'v0.1'}
SUPPORTED_VERSIONS = {'v0.3'}

def __init__(
self,
remote_host: str,
api_key: str,
default_target: Optional[str] = None,
api_version: str = 'v0.1',
api_version: str = 'v0.3',
max_retry_seconds: int = 3600, # 1 hour
verbose: bool = False,
):
Expand All @@ -79,7 +79,7 @@ def __init__(
api_key: The key used for authenticating against the IonQ API.
default_target: The default target to run against. Supports one of 'qpu' and
'simulator'. Can be overridden by calls with target in their signature.
api_version: Which version fo the api to use. As of Dec, 2020, accepts 'v0.1' only,
api_version: Which version fo the api to use. As of Feb, 2023, accepts 'v0.3' only,
which is the default.
max_retry_seconds: The time to continue retriable responses. Defaults to 3600.
verbose: Whether to print to stderr and stdio any retriable errors that are encountered.
Expand All @@ -91,7 +91,7 @@ def __init__(
)
assert (
api_version in self.SUPPORTED_VERSIONS
), f'Only api v0.1 is accepted but was {api_version}'
), f'Only api v0.3 is accepted but was {api_version}'
assert (
default_target is None or default_target in self.SUPPORTED_TARGETS
), f'Target can only be one of {self.SUPPORTED_TARGETS} but was {default_target}.'
Expand All @@ -109,6 +109,7 @@ def create_job(
repetitions: Optional[int] = None,
target: Optional[str] = None,
name: Optional[str] = None,
extra_query_params: Optional[dict] = None,
) -> dict:
"""Create a job.
Expand All @@ -121,6 +122,7 @@ def create_job(
target: If supplied the target to run on. Supports one of `qpu` or `simulator`. If not
set, uses `default_target`.
name: An optional name of the job. Different than the `job_id` of the job.
extra_query_params: Specify any parameters to include in the request.
Returns:
The json body of the response as a dict. This does not contain populated information
Expand All @@ -146,6 +148,12 @@ def create_job(
# API does not return number of shots so pass this through as metadata.
json['metadata']['shots'] = str(repetitions)

if serialized_program.error_mitigation:
json['error_mitigation'] = serialized_program.error_mitigation

if extra_query_params is not None:
json.update(extra_query_params)

def request():
return requests.post(f'{self.url}/jobs', json=json, headers=self.headers)

Expand All @@ -170,6 +178,40 @@ def request():

return self._make_request(request, {}).json()

def get_results(
self, job_id: str, sharpen: Optional[bool] = None, extra_query_params: Optional[dict] = None
):
"""Get job results from IonQ API.
Args:
job_id: The UUID of the job (returned when the job was created).
sharpen: A boolean that determines how to aggregate error mitigated.
If True, apply majority vote mitigation; if False, apply average mitigation.
extra_query_params: Specify any parameters to include in the request.
Returns:
extra_query_paramsresponse as a dict.
Raises:
IonQNotFoundException: If job or results don't exist.
IonQException: For other API call failures.
"""

params = {}

if sharpen is not None:
params["sharpen"] = sharpen

if extra_query_params is not None:
params.update(extra_query_params)

def request():
return requests.get(
f'{self.url}/jobs/{job_id}/results', params=params, headers=self.headers
)

return self._make_request(request, {}).json()

def list_jobs(
self, status: Optional[str] = None, limit: int = 100, batch_size: int = 1000
) -> List[Dict[str, Any]]:
Expand Down Expand Up @@ -197,7 +239,7 @@ def cancel_job(self, job_id: str) -> dict:
Args:
job_id: The UUID of the job (returned when the job was created).
Note that the IonQ API v0.1 can cancel a completed job, which updates its status to
Note that the IonQ API v0.3 can cancel a completed job, which updates its status to
canceled.
Returns:
Expand Down
114 changes: 96 additions & 18 deletions cirq-ionq/cirq_ionq/ionq_client_test.py
Expand Up @@ -78,7 +78,7 @@ def test_ionq_client_attributes():
max_retry_seconds=10,
verbose=True,
)
assert client.url == 'http://example.com/v0.1'
assert client.url == 'http://example.com/v0.3'
assert client.headers == {
'Authorization': 'apiKey to_my_heart',
'Content-Type': 'application/json',
Expand All @@ -96,7 +96,9 @@ def test_ionq_client_create_job(mock_post):
mock_post.return_value.json.return_value = {'foo': 'bar'}

client = ionq.ionq_client._IonQClient(remote_host='http://example.com', api_key='to_my_heart')
program = ionq.SerializedProgram(body={'job': 'mine'}, metadata={'a': '0,1'})
program = ionq.SerializedProgram(
body={'job': 'mine'}, metadata={'a': '0,1'}, error_mitigation={'debias': True}
)
response = client.create_job(
serialized_program=program, repetitions=200, target='qpu', name='bacon'
)
Expand All @@ -108,6 +110,7 @@ def test_ionq_client_create_job(mock_post):
'body': {'job': 'mine'},
'name': 'bacon',
'shots': '200',
'error_mitigation': {'debias': True},
'metadata': {'shots': '200', 'a': '0,1'},
}
expected_headers = {
Expand All @@ -116,7 +119,42 @@ def test_ionq_client_create_job(mock_post):
'User-Agent': client._user_agent(),
}
mock_post.assert_called_with(
'http://example.com/v0.1/jobs', json=expected_json, headers=expected_headers
'http://example.com/v0.3/jobs', json=expected_json, headers=expected_headers
)


@mock.patch('requests.post')
def test_ionq_client_create_job_extra_params(mock_post):
mock_post.return_value.status_code.return_value = requests.codes.ok
mock_post.return_value.json.return_value = {'foo': 'bar'}

client = ionq.ionq_client._IonQClient(remote_host='http://example.com', api_key='to_my_heart')
program = ionq.SerializedProgram(body={'job': 'mine'}, metadata={'a': '0,1'})
response = client.create_job(
serialized_program=program,
repetitions=200,
target='qpu',
name='bacon',
extra_query_params={'error_mitigation': {'debias': True}},
)
assert response == {'foo': 'bar'}

expected_json = {
'target': 'qpu',
'lang': 'json',
'body': {'job': 'mine'},
'name': 'bacon',
'shots': '200',
'error_mitigation': {'debias': True},
'metadata': {'shots': '200', 'a': '0,1'},
}
expected_headers = {
'Authorization': 'apiKey to_my_heart',
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
mock_post.assert_called_with(
'http://example.com/v0.3/jobs', json=expected_json, headers=expected_headers
)


Expand Down Expand Up @@ -272,7 +310,7 @@ def test_ionq_client_get_job_retry_409(mock_get):
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with('http://example.com/v0.1/jobs/job_id', headers=expected_headers)
mock_get.assert_called_with('http://example.com/v0.3/jobs/job_id', headers=expected_headers)


@mock.patch('requests.get')
Expand All @@ -288,7 +326,7 @@ def test_ionq_client_get_job(mock_get):
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with('http://example.com/v0.1/jobs/job_id', headers=expected_headers)
mock_get.assert_called_with('http://example.com/v0.3/jobs/job_id', headers=expected_headers)


@mock.patch('requests.get')
Expand Down Expand Up @@ -342,6 +380,46 @@ def test_ionq_client_get_job_retry(mock_get):
assert mock_get.call_count == 2


@mock.patch('requests.get')
def test_ionq_client_get_results(mock_get):
mock_get.return_value.ok = True
mock_get.return_value.json.return_value = {'foo': 'bar'}
client = ionq.ionq_client._IonQClient(remote_host='http://example.com', api_key='to_my_heart')
response = client.get_results(job_id='job_id', sharpen=False)
assert response == {'foo': 'bar'}

expected_headers = {
'Authorization': 'apiKey to_my_heart',
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with(
'http://example.com/v0.3/jobs/job_id/results',
headers=expected_headers,
params={'sharpen': False},
)


@mock.patch('requests.get')
def test_ionq_client_get_results_extra_params(mock_get):
mock_get.return_value.ok = True
mock_get.return_value.json.return_value = {'foo': 'bar'}
client = ionq.ionq_client._IonQClient(remote_host='http://example.com', api_key='to_my_heart')
response = client.get_results(job_id='job_id', extra_query_params={'sharpen': False})
assert response == {'foo': 'bar'}

expected_headers = {
'Authorization': 'apiKey to_my_heart',
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with(
'http://example.com/v0.3/jobs/job_id/results',
headers=expected_headers,
params={'sharpen': False},
)


@mock.patch('requests.get')
def test_ionq_client_list_jobs(mock_get):
mock_get.return_value.ok = True
Expand All @@ -356,7 +434,7 @@ def test_ionq_client_list_jobs(mock_get):
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with(
'http://example.com/v0.1/jobs', headers=expected_headers, json={'limit': 1000}, params={}
'http://example.com/v0.3/jobs', headers=expected_headers, json={'limit': 1000}, params={}
)


Expand All @@ -374,7 +452,7 @@ def test_ionq_client_list_jobs_status(mock_get):
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with(
'http://example.com/v0.1/jobs',
'http://example.com/v0.3/jobs',
headers=expected_headers,
json={'limit': 1000},
params={'status': 'canceled'},
Expand All @@ -395,7 +473,7 @@ def test_ionq_client_list_jobs_limit(mock_get):
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with(
'http://example.com/v0.1/jobs', headers=expected_headers, json={'limit': 1000}, params={}
'http://example.com/v0.3/jobs', headers=expected_headers, json={'limit': 1000}, params={}
)


Expand All @@ -416,7 +494,7 @@ def test_ionq_client_list_jobs_batches(mock_get):
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
url = 'http://example.com/v0.1/jobs'
url = 'http://example.com/v0.3/jobs'
mock_get.assert_has_calls(
[
mock.call(url, headers=expected_headers, json={'limit': 1}, params={}),
Expand Down Expand Up @@ -445,7 +523,7 @@ def test_ionq_client_list_jobs_batches_does_not_divide_total(mock_get):
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
url = 'http://example.com/v0.1/jobs'
url = 'http://example.com/v0.3/jobs'
mock_get.assert_has_calls(
[
mock.call(url, headers=expected_headers, json={'limit': 2}, params={}),
Expand Down Expand Up @@ -503,7 +581,7 @@ def test_ionq_client_cancel_job(mock_put):
'User-Agent': client._user_agent(),
}
mock_put.assert_called_with(
'http://example.com/v0.1/jobs/job_id/status/cancel', headers=expected_headers
'http://example.com/v0.3/jobs/job_id/status/cancel', headers=expected_headers
)


Expand Down Expand Up @@ -571,7 +649,7 @@ def test_ionq_client_delete_job(mock_delete):
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
mock_delete.assert_called_with('http://example.com/v0.1/jobs/job_id', headers=expected_headers)
mock_delete.assert_called_with('http://example.com/v0.3/jobs/job_id', headers=expected_headers)


@mock.patch('requests.delete')
Expand Down Expand Up @@ -639,7 +717,7 @@ def test_ionq_client_get_current_calibrations(mock_get):
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with(
'http://example.com/v0.1/calibrations/current', headers=expected_headers
'http://example.com/v0.3/calibrations/current', headers=expected_headers
)


Expand Down Expand Up @@ -700,7 +778,7 @@ def test_ionq_client_list_calibrations(mock_get):
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with(
'http://example.com/v0.1/calibrations',
'http://example.com/v0.3/calibrations',
headers=expected_headers,
json={'limit': 1000},
params={},
Expand All @@ -724,7 +802,7 @@ def test_ionq_client_list_calibrations_dates(mock_get):
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with(
'http://example.com/v0.1/calibrations',
'http://example.com/v0.3/calibrations',
headers=expected_headers,
json={'limit': 1000},
params={'start': 1284286794000, 'end': 1284286795000},
Expand All @@ -747,7 +825,7 @@ def test_ionq_client_list_calibrations_limit(mock_get):
'User-Agent': client._user_agent(),
}
mock_get.assert_called_with(
'http://example.com/v0.1/calibrations',
'http://example.com/v0.3/calibrations',
headers=expected_headers,
json={'limit': 1000},
params={},
Expand All @@ -771,7 +849,7 @@ def test_ionq_client_list_calibrations_batches(mock_get):
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
url = 'http://example.com/v0.1/calibrations'
url = 'http://example.com/v0.3/calibrations'
mock_get.assert_has_calls(
[
mock.call(url, headers=expected_headers, json={'limit': 1}, params={}),
Expand Down Expand Up @@ -800,7 +878,7 @@ def test_ionq_client_list_calibrations_batches_does_not_divide_total(mock_get):
'Content-Type': 'application/json',
'User-Agent': client._user_agent(),
}
url = 'http://example.com/v0.1/calibrations'
url = 'http://example.com/v0.3/calibrations'
mock_get.assert_has_calls(
[
mock.call(url, headers=expected_headers, json={'limit': 2}, params={}),
Expand Down

0 comments on commit 0e80fa5

Please sign in to comment.