Skip to content

Commit 0e80fa5

Browse files
splchgmauriciomocha
authored
v0.3 API + debiasing (#6077)
* 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 <[email protected]> Co-authored-by: Patrick Deuley <[email protected]>
1 parent b70b2fc commit 0e80fa5

11 files changed

+351
-94
lines changed

cirq-ionq/cirq_ionq/ionq_client.py

+47-5
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ class _IonQClient:
5454
"""
5555

5656
SUPPORTED_TARGETS = {'qpu', 'simulator'}
57-
SUPPORTED_VERSIONS = {'v0.1'}
57+
SUPPORTED_VERSIONS = {'v0.3'}
5858

5959
def __init__(
6060
self,
6161
remote_host: str,
6262
api_key: str,
6363
default_target: Optional[str] = None,
64-
api_version: str = 'v0.1',
64+
api_version: str = 'v0.3',
6565
max_retry_seconds: int = 3600, # 1 hour
6666
verbose: bool = False,
6767
):
@@ -79,7 +79,7 @@ def __init__(
7979
api_key: The key used for authenticating against the IonQ API.
8080
default_target: The default target to run against. Supports one of 'qpu' and
8181
'simulator'. Can be overridden by calls with target in their signature.
82-
api_version: Which version fo the api to use. As of Dec, 2020, accepts 'v0.1' only,
82+
api_version: Which version fo the api to use. As of Feb, 2023, accepts 'v0.3' only,
8383
which is the default.
8484
max_retry_seconds: The time to continue retriable responses. Defaults to 3600.
8585
verbose: Whether to print to stderr and stdio any retriable errors that are encountered.
@@ -91,7 +91,7 @@ def __init__(
9191
)
9292
assert (
9393
api_version in self.SUPPORTED_VERSIONS
94-
), f'Only api v0.1 is accepted but was {api_version}'
94+
), f'Only api v0.3 is accepted but was {api_version}'
9595
assert (
9696
default_target is None or default_target in self.SUPPORTED_TARGETS
9797
), f'Target can only be one of {self.SUPPORTED_TARGETS} but was {default_target}.'
@@ -109,6 +109,7 @@ def create_job(
109109
repetitions: Optional[int] = None,
110110
target: Optional[str] = None,
111111
name: Optional[str] = None,
112+
extra_query_params: Optional[dict] = None,
112113
) -> dict:
113114
"""Create a job.
114115
@@ -121,6 +122,7 @@ def create_job(
121122
target: If supplied the target to run on. Supports one of `qpu` or `simulator`. If not
122123
set, uses `default_target`.
123124
name: An optional name of the job. Different than the `job_id` of the job.
125+
extra_query_params: Specify any parameters to include in the request.
124126
125127
Returns:
126128
The json body of the response as a dict. This does not contain populated information
@@ -146,6 +148,12 @@ def create_job(
146148
# API does not return number of shots so pass this through as metadata.
147149
json['metadata']['shots'] = str(repetitions)
148150

151+
if serialized_program.error_mitigation:
152+
json['error_mitigation'] = serialized_program.error_mitigation
153+
154+
if extra_query_params is not None:
155+
json.update(extra_query_params)
156+
149157
def request():
150158
return requests.post(f'{self.url}/jobs', json=json, headers=self.headers)
151159

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

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

181+
def get_results(
182+
self, job_id: str, sharpen: Optional[bool] = None, extra_query_params: Optional[dict] = None
183+
):
184+
"""Get job results from IonQ API.
185+
186+
Args:
187+
job_id: The UUID of the job (returned when the job was created).
188+
sharpen: A boolean that determines how to aggregate error mitigated.
189+
If True, apply majority vote mitigation; if False, apply average mitigation.
190+
extra_query_params: Specify any parameters to include in the request.
191+
192+
Returns:
193+
extra_query_paramsresponse as a dict.
194+
195+
Raises:
196+
IonQNotFoundException: If job or results don't exist.
197+
IonQException: For other API call failures.
198+
"""
199+
200+
params = {}
201+
202+
if sharpen is not None:
203+
params["sharpen"] = sharpen
204+
205+
if extra_query_params is not None:
206+
params.update(extra_query_params)
207+
208+
def request():
209+
return requests.get(
210+
f'{self.url}/jobs/{job_id}/results', params=params, headers=self.headers
211+
)
212+
213+
return self._make_request(request, {}).json()
214+
173215
def list_jobs(
174216
self, status: Optional[str] = None, limit: int = 100, batch_size: int = 1000
175217
) -> List[Dict[str, Any]]:
@@ -197,7 +239,7 @@ def cancel_job(self, job_id: str) -> dict:
197239
Args:
198240
job_id: The UUID of the job (returned when the job was created).
199241
200-
Note that the IonQ API v0.1 can cancel a completed job, which updates its status to
242+
Note that the IonQ API v0.3 can cancel a completed job, which updates its status to
201243
canceled.
202244
203245
Returns:

cirq-ionq/cirq_ionq/ionq_client_test.py

+96-18
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def test_ionq_client_attributes():
7878
max_retry_seconds=10,
7979
verbose=True,
8080
)
81-
assert client.url == 'http://example.com/v0.1'
81+
assert client.url == 'http://example.com/v0.3'
8282
assert client.headers == {
8383
'Authorization': 'apiKey to_my_heart',
8484
'Content-Type': 'application/json',
@@ -96,7 +96,9 @@ def test_ionq_client_create_job(mock_post):
9696
mock_post.return_value.json.return_value = {'foo': 'bar'}
9797

9898
client = ionq.ionq_client._IonQClient(remote_host='http://example.com', api_key='to_my_heart')
99-
program = ionq.SerializedProgram(body={'job': 'mine'}, metadata={'a': '0,1'})
99+
program = ionq.SerializedProgram(
100+
body={'job': 'mine'}, metadata={'a': '0,1'}, error_mitigation={'debias': True}
101+
)
100102
response = client.create_job(
101103
serialized_program=program, repetitions=200, target='qpu', name='bacon'
102104
)
@@ -108,6 +110,7 @@ def test_ionq_client_create_job(mock_post):
108110
'body': {'job': 'mine'},
109111
'name': 'bacon',
110112
'shots': '200',
113+
'error_mitigation': {'debias': True},
111114
'metadata': {'shots': '200', 'a': '0,1'},
112115
}
113116
expected_headers = {
@@ -116,7 +119,42 @@ def test_ionq_client_create_job(mock_post):
116119
'User-Agent': client._user_agent(),
117120
}
118121
mock_post.assert_called_with(
119-
'http://example.com/v0.1/jobs', json=expected_json, headers=expected_headers
122+
'http://example.com/v0.3/jobs', json=expected_json, headers=expected_headers
123+
)
124+
125+
126+
@mock.patch('requests.post')
127+
def test_ionq_client_create_job_extra_params(mock_post):
128+
mock_post.return_value.status_code.return_value = requests.codes.ok
129+
mock_post.return_value.json.return_value = {'foo': 'bar'}
130+
131+
client = ionq.ionq_client._IonQClient(remote_host='http://example.com', api_key='to_my_heart')
132+
program = ionq.SerializedProgram(body={'job': 'mine'}, metadata={'a': '0,1'})
133+
response = client.create_job(
134+
serialized_program=program,
135+
repetitions=200,
136+
target='qpu',
137+
name='bacon',
138+
extra_query_params={'error_mitigation': {'debias': True}},
139+
)
140+
assert response == {'foo': 'bar'}
141+
142+
expected_json = {
143+
'target': 'qpu',
144+
'lang': 'json',
145+
'body': {'job': 'mine'},
146+
'name': 'bacon',
147+
'shots': '200',
148+
'error_mitigation': {'debias': True},
149+
'metadata': {'shots': '200', 'a': '0,1'},
150+
}
151+
expected_headers = {
152+
'Authorization': 'apiKey to_my_heart',
153+
'Content-Type': 'application/json',
154+
'User-Agent': client._user_agent(),
155+
}
156+
mock_post.assert_called_with(
157+
'http://example.com/v0.3/jobs', json=expected_json, headers=expected_headers
120158
)
121159

122160

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

277315

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

293331

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

344382

383+
@mock.patch('requests.get')
384+
def test_ionq_client_get_results(mock_get):
385+
mock_get.return_value.ok = True
386+
mock_get.return_value.json.return_value = {'foo': 'bar'}
387+
client = ionq.ionq_client._IonQClient(remote_host='http://example.com', api_key='to_my_heart')
388+
response = client.get_results(job_id='job_id', sharpen=False)
389+
assert response == {'foo': 'bar'}
390+
391+
expected_headers = {
392+
'Authorization': 'apiKey to_my_heart',
393+
'Content-Type': 'application/json',
394+
'User-Agent': client._user_agent(),
395+
}
396+
mock_get.assert_called_with(
397+
'http://example.com/v0.3/jobs/job_id/results',
398+
headers=expected_headers,
399+
params={'sharpen': False},
400+
)
401+
402+
403+
@mock.patch('requests.get')
404+
def test_ionq_client_get_results_extra_params(mock_get):
405+
mock_get.return_value.ok = True
406+
mock_get.return_value.json.return_value = {'foo': 'bar'}
407+
client = ionq.ionq_client._IonQClient(remote_host='http://example.com', api_key='to_my_heart')
408+
response = client.get_results(job_id='job_id', extra_query_params={'sharpen': False})
409+
assert response == {'foo': 'bar'}
410+
411+
expected_headers = {
412+
'Authorization': 'apiKey to_my_heart',
413+
'Content-Type': 'application/json',
414+
'User-Agent': client._user_agent(),
415+
}
416+
mock_get.assert_called_with(
417+
'http://example.com/v0.3/jobs/job_id/results',
418+
headers=expected_headers,
419+
params={'sharpen': False},
420+
)
421+
422+
345423
@mock.patch('requests.get')
346424
def test_ionq_client_list_jobs(mock_get):
347425
mock_get.return_value.ok = True
@@ -356,7 +434,7 @@ def test_ionq_client_list_jobs(mock_get):
356434
'User-Agent': client._user_agent(),
357435
}
358436
mock_get.assert_called_with(
359-
'http://example.com/v0.1/jobs', headers=expected_headers, json={'limit': 1000}, params={}
437+
'http://example.com/v0.3/jobs', headers=expected_headers, json={'limit': 1000}, params={}
360438
)
361439

362440

@@ -374,7 +452,7 @@ def test_ionq_client_list_jobs_status(mock_get):
374452
'User-Agent': client._user_agent(),
375453
}
376454
mock_get.assert_called_with(
377-
'http://example.com/v0.1/jobs',
455+
'http://example.com/v0.3/jobs',
378456
headers=expected_headers,
379457
json={'limit': 1000},
380458
params={'status': 'canceled'},
@@ -395,7 +473,7 @@ def test_ionq_client_list_jobs_limit(mock_get):
395473
'User-Agent': client._user_agent(),
396474
}
397475
mock_get.assert_called_with(
398-
'http://example.com/v0.1/jobs', headers=expected_headers, json={'limit': 1000}, params={}
476+
'http://example.com/v0.3/jobs', headers=expected_headers, json={'limit': 1000}, params={}
399477
)
400478

401479

@@ -416,7 +494,7 @@ def test_ionq_client_list_jobs_batches(mock_get):
416494
'Content-Type': 'application/json',
417495
'User-Agent': client._user_agent(),
418496
}
419-
url = 'http://example.com/v0.1/jobs'
497+
url = 'http://example.com/v0.3/jobs'
420498
mock_get.assert_has_calls(
421499
[
422500
mock.call(url, headers=expected_headers, json={'limit': 1}, params={}),
@@ -445,7 +523,7 @@ def test_ionq_client_list_jobs_batches_does_not_divide_total(mock_get):
445523
'Content-Type': 'application/json',
446524
'User-Agent': client._user_agent(),
447525
}
448-
url = 'http://example.com/v0.1/jobs'
526+
url = 'http://example.com/v0.3/jobs'
449527
mock_get.assert_has_calls(
450528
[
451529
mock.call(url, headers=expected_headers, json={'limit': 2}, params={}),
@@ -503,7 +581,7 @@ def test_ionq_client_cancel_job(mock_put):
503581
'User-Agent': client._user_agent(),
504582
}
505583
mock_put.assert_called_with(
506-
'http://example.com/v0.1/jobs/job_id/status/cancel', headers=expected_headers
584+
'http://example.com/v0.3/jobs/job_id/status/cancel', headers=expected_headers
507585
)
508586

509587

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

576654

577655
@mock.patch('requests.delete')
@@ -639,7 +717,7 @@ def test_ionq_client_get_current_calibrations(mock_get):
639717
'User-Agent': client._user_agent(),
640718
}
641719
mock_get.assert_called_with(
642-
'http://example.com/v0.1/calibrations/current', headers=expected_headers
720+
'http://example.com/v0.3/calibrations/current', headers=expected_headers
643721
)
644722

645723

@@ -700,7 +778,7 @@ def test_ionq_client_list_calibrations(mock_get):
700778
'User-Agent': client._user_agent(),
701779
}
702780
mock_get.assert_called_with(
703-
'http://example.com/v0.1/calibrations',
781+
'http://example.com/v0.3/calibrations',
704782
headers=expected_headers,
705783
json={'limit': 1000},
706784
params={},
@@ -724,7 +802,7 @@ def test_ionq_client_list_calibrations_dates(mock_get):
724802
'User-Agent': client._user_agent(),
725803
}
726804
mock_get.assert_called_with(
727-
'http://example.com/v0.1/calibrations',
805+
'http://example.com/v0.3/calibrations',
728806
headers=expected_headers,
729807
json={'limit': 1000},
730808
params={'start': 1284286794000, 'end': 1284286795000},
@@ -747,7 +825,7 @@ def test_ionq_client_list_calibrations_limit(mock_get):
747825
'User-Agent': client._user_agent(),
748826
}
749827
mock_get.assert_called_with(
750-
'http://example.com/v0.1/calibrations',
828+
'http://example.com/v0.3/calibrations',
751829
headers=expected_headers,
752830
json={'limit': 1000},
753831
params={},
@@ -771,7 +849,7 @@ def test_ionq_client_list_calibrations_batches(mock_get):
771849
'Content-Type': 'application/json',
772850
'User-Agent': client._user_agent(),
773851
}
774-
url = 'http://example.com/v0.1/calibrations'
852+
url = 'http://example.com/v0.3/calibrations'
775853
mock_get.assert_has_calls(
776854
[
777855
mock.call(url, headers=expected_headers, json={'limit': 1}, params={}),
@@ -800,7 +878,7 @@ def test_ionq_client_list_calibrations_batches_does_not_divide_total(mock_get):
800878
'Content-Type': 'application/json',
801879
'User-Agent': client._user_agent(),
802880
}
803-
url = 'http://example.com/v0.1/calibrations'
881+
url = 'http://example.com/v0.3/calibrations'
804882
mock_get.assert_has_calls(
805883
[
806884
mock.call(url, headers=expected_headers, json={'limit': 2}, params={}),

0 commit comments

Comments
 (0)