diff --git a/cirq-ionq/cirq_ionq/ionq_client.py b/cirq-ionq/cirq_ionq/ionq_client.py index 787eba89e50..f21d3245b72 100644 --- a/cirq-ionq/cirq_ionq/ionq_client.py +++ b/cirq-ionq/cirq_ionq/ionq_client.py @@ -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, ): @@ -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. @@ -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}.' @@ -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. @@ -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 @@ -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) @@ -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]]: @@ -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: diff --git a/cirq-ionq/cirq_ionq/ionq_client_test.py b/cirq-ionq/cirq_ionq/ionq_client_test.py index 2d73d351c9b..c5175cffa36 100644 --- a/cirq-ionq/cirq_ionq/ionq_client_test.py +++ b/cirq-ionq/cirq_ionq/ionq_client_test.py @@ -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', @@ -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' ) @@ -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 = { @@ -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 ) @@ -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') @@ -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') @@ -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 @@ -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={} ) @@ -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'}, @@ -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={} ) @@ -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={}), @@ -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={}), @@ -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 ) @@ -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') @@ -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 ) @@ -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={}, @@ -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}, @@ -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={}, @@ -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={}), @@ -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={}), diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index 6b549e8089a..cccbf89d971 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -15,7 +15,7 @@ import time import warnings -from typing import Dict, Sequence, Union, TYPE_CHECKING +from typing import Dict, Sequence, Union, Optional, TYPE_CHECKING from cirq_ionq import ionq_exceptions, results from cirq._doc import document @@ -171,13 +171,20 @@ def measurement_dict(self) -> Dict[str, Sequence[int]]: return measurement_dict def results( - self, timeout_seconds: int = 7200, polling_seconds: int = 1 + self, + timeout_seconds: int = 7200, + polling_seconds: int = 1, + sharpen: Optional[bool] = None, + extra_query_params: Optional[dict] = None, ) -> Union[results.QPUResult, results.SimulatorResult]: """Polls the IonQ api for results. Args: timeout_seconds: The total number of seconds to poll for. polling_seconds: The interval with which to poll. + 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: Either a `cirq_ionq.QPUResults` or `cirq_ionq.SimulatorResults` depending on whether @@ -210,12 +217,16 @@ def results( raise RuntimeError( f'Job was not completed successfully. Instead had status: {self.status()}' ) + + histogram = self._client.get_results( + job_id=self.job_id(), sharpen=sharpen, extra_query_params=extra_query_params + ) # IonQ returns results in little endian, Cirq prefers to use big endian, so we convert. if self.target().startswith('qpu'): repetitions = self.repetitions() counts = { _little_endian_to_big(int(k), self.num_qubits()): round(repetitions * float(v)) - for k, v in self._job['data']['histogram'].items() + for k, v in histogram.items() } return results.QPUResult( counts=counts, @@ -225,7 +236,7 @@ def results( else: probabilities = { _little_endian_to_big(int(k), self.num_qubits()): float(v) - for k, v in self._job['data']['histogram'].items() + for k, v in histogram.items() } return results.SimulatorResult( probabilities=probabilities, diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index ce9931aee7c..481669e276f 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -58,16 +58,17 @@ def test_job_str(): def test_job_results_qpu(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = {'0': '0.6', '2': '0.4'} job_dict = { 'id': 'my_id', 'status': 'completed', 'qubits': '2', 'target': 'qpu', 'metadata': {'shots': 1000, 'measurement0': f'a{chr(31)}0,1'}, - 'data': {'histogram': {'0': '0.6', '2': '0.4'}}, 'warning': {'messages': ['foo', 'bar']}, } - job = ionq.Job(None, job_dict) + job = ionq.Job(mock_client, job_dict) with warnings.catch_warnings(record=True) as w: results = job.results() assert len(w) == 2 @@ -78,16 +79,17 @@ def test_job_results_qpu(): def test_job_results_rounding_qpu(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = {'0': '0.0006', '2': '0.9994'} job_dict = { 'id': 'my_id', 'status': 'completed', 'qubits': '2', 'target': 'qpu', 'metadata': {'shots': 5000, 'measurement0': f'a{chr(31)}0,1'}, - 'data': {'histogram': {'0': '0.0006', '2': '0.9994'}}, } # 5000*0.0006 ~ 2.9999 but should be interpreted as 3 - job = ionq.Job(None, job_dict) + job = ionq.Job(mock_client, job_dict) expected = ionq.QPUResult({0: 3, 1: 4997}, 2, {'a': [0, 1]}) results = job.results() assert results == expected @@ -110,20 +112,23 @@ def test_job_results_failed_no_error_message(): def test_job_results_qpu_endianness(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} job_dict = { 'id': 'my_id', 'status': 'completed', 'qubits': '2', 'target': 'qpu', 'metadata': {'shots': 1000}, - 'data': {'histogram': {'0': '0.6', '1': '0.4'}}, } - job = ionq.Job(None, job_dict) + job = ionq.Job(mock_client, job_dict) results = job.results() assert results == ionq.QPUResult({0: 600, 2: 400}, 2, measurement_dict={}) def test_job_results_qpu_target_endianness(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} job_dict = { 'id': 'my_id', 'status': 'completed', @@ -132,7 +137,7 @@ def test_job_results_qpu_target_endianness(): 'metadata': {'shots': 1000}, 'data': {'histogram': {'0': '0.6', '1': '0.4'}}, } - job = ionq.Job(None, job_dict) + job = ionq.Job(mock_client, job_dict) results = job.results() assert results == ionq.QPUResult({0: 600, 2: 400}, 2, measurement_dict={}) @@ -146,10 +151,10 @@ def test_job_results_poll(mock_sleep): 'qubits': '1', 'target': 'qpu', 'metadata': {'shots': 1000}, - 'data': {'histogram': {'0': '0.6', '1': '0.4'}}, } mock_client = mock.MagicMock() mock_client.get_job.side_effect = [ready_job, completed_job] + mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} job = ionq.Job(mock_client, ready_job) results = job.results(polling_seconds=0) assert results == ionq.QPUResult({0: 600, 1: 400}, 1, measurement_dict={}) @@ -179,33 +184,50 @@ def test_job_results_poll_timeout_with_error_message(mock_sleep): def test_job_results_simulator(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} job_dict = { 'id': 'my_id', 'status': 'completed', 'qubits': '1', 'target': 'simulator', - 'data': {'histogram': {'0': '0.6', '1': '0.4'}}, 'metadata': {'shots': '100'}, } - job = ionq.Job(None, job_dict) + job = ionq.Job(mock_client, job_dict) results = job.results() assert results == ionq.SimulatorResult({0: 0.6, 1: 0.4}, 1, {}, 100) def test_job_results_simulator_endianness(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} job_dict = { 'id': 'my_id', 'status': 'completed', 'qubits': '2', 'target': 'simulator', - 'data': {'histogram': {'0': '0.6', '1': '0.4'}}, 'metadata': {'shots': '100'}, } - job = ionq.Job(None, job_dict) + job = ionq.Job(mock_client, job_dict) results = job.results() assert results == ionq.SimulatorResult({0: 0.6, 2: 0.4}, 2, {}, 100) +def test_job_sharpen_results(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = {'0': '60', '1': '40'} + job_dict = { + 'id': 'my_id', + 'status': 'completed', + 'qubits': '1', + 'target': 'simulator', + 'metadata': {'shots': '100'}, + } + job = ionq.Job(mock_client, job_dict) + results = job.results(sharpen=False) + assert results == ionq.SimulatorResult({0: 60, 1: 40}, 1, {}, 100) + + def test_job_cancel(): ready_job = {'id': 'my_id', 'status': 'ready'} canceled_job = {'id': 'my_id', 'status': 'canceled'} diff --git a/cirq-ionq/cirq_ionq/sampler.py b/cirq-ionq/cirq_ionq/sampler.py index 02c07d17a6e..7f7104f5435 100644 --- a/cirq-ionq/cirq_ionq/sampler.py +++ b/cirq-ionq/cirq_ionq/sampler.py @@ -86,10 +86,10 @@ def run_sweep( ) for resolver in resolvers ] - kwargs = {} if self._timeout_seconds is not None: - kwargs = {"timeout_seconds": self._timeout_seconds} - job_results = [job.results(**kwargs) for job in jobs] + job_results = [job.results(timeout_seconds=self._timeout_seconds) for job in jobs] + else: + job_results = [job.results() for job in jobs] cirq_results = [] for result, params in zip(job_results, resolvers): if isinstance(result, results.QPUResult): diff --git a/cirq-ionq/cirq_ionq/sampler_test.py b/cirq-ionq/cirq_ionq/sampler_test.py index 037f9d98bc2..2bc0c69f23f 100644 --- a/cirq-ionq/cirq_ionq/sampler_test.py +++ b/cirq-ionq/cirq_ionq/sampler_test.py @@ -29,11 +29,11 @@ def test_sampler_qpu(): 'qubits': '1', 'target': 'qpu', 'metadata': {'shots': 4, 'measurement0': f'a{chr(31)}0'}, - 'data': {'histogram': {'0': '0.25', '1': '0.75'}}, } job = ionq.Job(client=mock_service, job_dict=job_dict) mock_service.create_job.return_value = job + mock_service.get_results.return_value = {'0': '0.25', '1': '0.75'} sampler = ionq.Sampler(service=mock_service, target='qpu') q0 = cirq.LineQubit(0) @@ -53,11 +53,11 @@ def test_sampler_simulator(): 'qubits': '1', 'target': 'simulator', 'metadata': {'shots': 4, 'measurement0': f'a{chr(31)}0'}, - 'data': {'histogram': {'0': '0.25', '1': '0.75'}}, } job = ionq.Job(client=mock_service, job_dict=job_dict) mock_service.create_job.return_value = job + mock_service.get_results.return_value = {'0': '0.25', '1': '0.75'} sampler = ionq.Sampler(service=mock_service, target='simulator', seed=10) q0 = cirq.LineQubit(0) @@ -79,7 +79,6 @@ def test_sampler_multiple_jobs(): 'qubits': '1', 'target': 'qpu', 'metadata': {'shots': 4, 'measurement0': f'a{chr(31)}0'}, - 'data': {'histogram': {'0': '0.25', '1': '0.75'}}, } job_dict1 = { 'id': '1', @@ -87,12 +86,12 @@ def test_sampler_multiple_jobs(): 'qubits': '1', 'target': 'qpu', 'metadata': {'shots': 4, 'measurement0': f'a{chr(31)}0'}, - 'data': {'histogram': {'0': '0.5', '1': '0.5'}}, } job0 = ionq.Job(client=mock_service, job_dict=job_dict0) job1 = ionq.Job(client=mock_service, job_dict=job_dict1) mock_service.create_job.side_effect = [job0, job1] + mock_service.get_results.side_effect = [{'0': '0.25', '1': '0.75'}, {'0': '0.5', '1': '0.5'}] sampler = ionq.Sampler(service=mock_service, timeout_seconds=10, target='qpu') q0 = cirq.LineQubit(0) @@ -120,3 +119,36 @@ def test_sampler_multiple_jobs(): ] ) assert mock_service.create_job.call_count == 2 + + +def test_sampler_run_sweep(): + mock_service = mock.MagicMock() + job_dict = { + 'id': '1', + 'status': 'completed', + 'qubits': '1', + 'target': 'qpu', + 'metadata': {'shots': 4, 'measurement0': f'a{chr(31)}0'}, + } + + job = ionq.Job(client=mock_service, job_dict=job_dict) + mock_service.create_job.return_value = job + mock_service.get_results.return_value = {'0': '0.25', '1': '0.75'} + + sampler = ionq.Sampler(service=mock_service, target='qpu') + q0 = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(q0) ** sp.Symbol('theta'), cirq.measure(q0, key='a')) + + sweep = cirq.Linspace(key='theta', start=0.0, stop=2.0, length=5) + + results = sampler.run_sweep(program=circuit, params=sweep, repetitions=4) + assert len(results) == 5 # Confirm that 5 result objects were created + assert all([result.repetitions == 4 for result in results]) # Confirm all repetitions were 4 + + # Assert that create_job was called 5 times + assert mock_service.create_job.call_count == 5 + # For each call, assert that it was called with the correct repetitions and target + for call in mock_service.create_job.call_args_list: + _, kwargs = call + assert kwargs["repetitions"] == 4 + assert kwargs["target"] == 'qpu' diff --git a/cirq-ionq/cirq_ionq/serializer.py b/cirq-ionq/cirq_ionq/serializer.py index 5864ffe0827..1ef330ee5a4 100644 --- a/cirq-ionq/cirq_ionq/serializer.py +++ b/cirq-ionq/cirq_ionq/serializer.py @@ -40,6 +40,7 @@ class SerializedProgram: body: dict metadata: dict + error_mitigation: Optional[dict] = None class Serializer: @@ -75,7 +76,9 @@ def __init__(self, atol: float = 1e-8): MSGate: self._serialize_ms_gate, } - def serialize(self, circuit: cirq.AbstractCircuit) -> SerializedProgram: + def serialize( + self, circuit: cirq.AbstractCircuit, error_mitigation: Optional[dict] = None + ) -> SerializedProgram: """Serialize the given circuit. Raises: @@ -97,7 +100,7 @@ def serialize(self, circuit: cirq.AbstractCircuit) -> SerializedProgram: } metadata = self._serialize_measurements(op for op in serialized_ops if op['gate'] == 'meas') - return SerializedProgram(body=body, metadata=metadata) + return SerializedProgram(body=body, metadata=metadata, error_mitigation=error_mitigation) def _validate_circuit(self, circuit: cirq.AbstractCircuit): if len(circuit) == 0: diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index 1b2384e2177..d30bdd8b65e 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -35,7 +35,7 @@ def __init__( remote_host: Optional[str] = None, api_key: Optional[str] = None, default_target: Optional[str] = None, - api_version='v0.1', + api_version='v0.3', max_retry_seconds: int = 3600, verbose=False, ): @@ -53,7 +53,7 @@ def __init__( and target must always be specified in calls. If set, then this default is used, unless a target is specified for a given call. Supports either 'qpu' or 'simulator'. - api_version: Version of the api. Defaults to 'v0.1'. + api_version: Version of the api. Defaults to 'v0.3'. max_retry_seconds: The number of seconds to retry calls for. Defaults to one hour. verbose: Whether to print to stdio and stderr on retriable errors. @@ -91,6 +91,9 @@ def run( target: Optional[str] = None, param_resolver: cirq.ParamResolverOrSimilarType = cirq.ParamResolver({}), seed: cirq.RANDOM_STATE_OR_SEED_LIKE = None, + error_mitigation: Optional[dict] = None, + sharpen: Optional[bool] = None, + extra_query_params: Optional[dict] = None, ) -> cirq.Result: """Run the given circuit on the IonQ API. @@ -103,12 +106,26 @@ def run( seed: If the target is `simulation` the seed for generating results. If None, this will be `np.random`, if an int, will be `np.random.RandomState(int)`, otherwise must be a modulate similar to `np.random`. + error_mitigation: A dictionary of error mitigation settings. Valid keys include: + - 'debias': A boolean indicating whether to use the debiasing technique for + aggregating results. This technique is used to reduce the bias in the results + caused by measurement error and can improve the accuracy of the output. + 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: A `cirq.Result` for running the circuit. """ resolved_circuit = cirq.resolve_parameters(circuit, param_resolver) - result = self.create_job(resolved_circuit, repetitions, name, target).results() + result = self.create_job( + circuit=resolved_circuit, + repetitions=repetitions, + name=name, + target=target, + error_mitigation=error_mitigation, + extra_query_params=extra_query_params, + ).results(sharpen=sharpen) if isinstance(result, results.QPUResult): return result.to_cirq_result(params=cirq.ParamResolver(param_resolver)) # pylint: disable=unexpected-keyword-arg @@ -136,6 +153,8 @@ def create_job( repetitions: int = 100, name: Optional[str] = None, target: Optional[str] = None, + error_mitigation: Optional[dict] = None, + extra_query_params: Optional[dict] = None, ) -> job.Job: """Create a new job to run the given circuit. @@ -144,6 +163,11 @@ def create_job( repetitions: The number of times to repeat the circuit. Defaults to 100. name: An optional name for the created job. Different from the `job_id`. target: Where to run the job. Can be 'qpu' or 'simulator'. + error_mitigation: A dictionary of error mitigation settings. Valid keys include: + - 'debias': A boolean indicating whether to use the debiasing technique for + aggregating results. This technique is used to reduce the bias in the results + caused by measurement error and can improve the accuracy of the output. + extra_query_params: Specify any parameters to include in the request. Returns: A `cirq_ionq.IonQJob` which can be queried for status or results. @@ -151,9 +175,13 @@ def create_job( Raises: IonQException: If there was an error accessing the API. """ - serialized_program = serializer.Serializer().serialize(circuit) + serialized_program = serializer.Serializer().serialize(circuit, error_mitigation) result = self._client.create_job( - serialized_program=serialized_program, repetitions=repetitions, target=target, name=name + serialized_program=serialized_program, + repetitions=repetitions, + target=target, + name=name, + extra_query_params=extra_query_params, ) # The returned job does not have fully populated fields, so make # a second call and return the results of the fully filled out job. diff --git a/cirq-ionq/cirq_ionq/service_test.py b/cirq-ionq/cirq_ionq/service_test.py index c673d562cab..d3e4fae7077 100644 --- a/cirq-ionq/cirq_ionq/service_test.py +++ b/cirq-ionq/cirq_ionq/service_test.py @@ -39,9 +39,9 @@ def test_service_run(target, expected_results): 'target': target, 'metadata': {'shots': '4', 'measurement0': f'a{chr(31)}0'}, 'qubits': '1', - 'data': {'histogram': {'0': '0.25', '1': '0.75'}}, 'status': 'completed', } + mock_client.get_results.return_value = {'0': '0.25', '1': '0.75'} service._client = mock_client a = sympy.Symbol('a') @@ -72,10 +72,10 @@ def test_sampler(): 'qubits': '1', 'target': 'qpu', 'metadata': {'shots': 4, 'measurement0': f'a{chr(31)}0'}, - 'data': {'histogram': {'0': '0.25', '1': '0.75'}}, } mock_client.create_job.return_value = job_dict mock_client.get_job.return_value = job_dict + mock_client.get_results.return_value = {'0': '0.25', '1': '0.75'} sampler = service.sampler(target='qpu', seed=10) @@ -226,8 +226,8 @@ def test_service_remote_host_from_env_var_ionq(): @mock.patch.dict(os.environ, {}, clear=True) def test_service_remote_host_default(): - service = ionq.Service(api_key='tomyheart', api_version='v0.1') - assert service.remote_host == 'https://api.ionq.co/v0.1' + service = ionq.Service(api_key='tomyheart', api_version='v0.3') + assert service.remote_host == 'https://api.ionq.co/v0.3' @mock.patch.dict( diff --git a/docs/hardware/ionq/access.md b/docs/hardware/ionq/access.md index 10216c19aa0..a97572aa3d4 100644 --- a/docs/hardware/ionq/access.md +++ b/docs/hardware/ionq/access.md @@ -1,33 +1,42 @@ # Access and Authentication -IonQ's API gives access to IonQ's trapped ion quantum computers as well as a cloud simulator. -IonQ direct access is currently restricted to those with access to an IonQ API key. -As of January 2021, this access is currently restricted to partners. More information -about partnerships can be found at [ionq.com/get-started](https://ionq.com/get-started). +Access to IonQ's API requires an API key. This can be obtained on from the IonQ +Console at [cloud.ionq.com/settings/keys](https://cloud.ionq.com/settings/keys) + +For those accounts _without_ console access, contact +[support@ionq.com](mailto:support@ionq.com) to obtain a key. ## Authentication -To use Cirq with the IonQ API, one needs an API key. This is a random looking string. +An API key is required to access IonQ via Cirq. You will pass this key to an +instance of a `cirq_ionq.Service`, which can then be used to interact +with IonQ computers. + +Here is an example of this pattern: -Given that you have the API key, there are two ways to use these to -get an object in python to access the API. The object that you construct to access -the API are instances of the `cirq_ionq.Service` class. You can directly use the API key in constructing this instances of this class. Here is an example of this pattern: ```python import cirq_ionq as ionq service = ionq.Service(api_key='tomyheart') ``` -Alternatively, you can use environment variables to set this value. These environment variable for the api key is `IONQ_API_KEY`. Details of how to set environment variables vary by operating system. For example in bash, you would do +Alternatively, you can use environment variables to set this value. This +environment variable for the API key is `IONQ_API_KEY`. Details of how to set +environment variables vary by operating system. For example, in `bash`: + ```bash export IONQ_API_KEY=tomyheart ``` -In the case that you have set these environment variables, you can just perform + +Once this variable is set, the `ionq.Service()` will look for it automatically +in the environment: + ```python import cirq_ionq as ionq service = ionq.Service() ``` + The advantage of doing things this way is that you do not have to store the API key in source code, which might accidentally get uploaded to a version control system, and hence leak the API key. diff --git a/docs/hardware/ionq/service.md b/docs/hardware/ionq/service.md index f02f5352ebb..f87c2295c4e 100644 --- a/docs/hardware/ionq/service.md +++ b/docs/hardware/ionq/service.md @@ -1,14 +1,15 @@ # IonQ API Service -IonQ's API provides a way to execute quantum circuits on IonQ's trapped ion quantum computers -or on cloud based simulators. As of April 2021, this access is restricted to partners. -See [Access and Authentication](access.md) for details of access. +IonQ's API provides a way to execute quantum circuits on [IonQ's](https://ionq.com) +trapped ion quantum computers and cloud based simulators. An IonQ account is +required to use this service. To learn more, or sign up, see +[ionq.com/get-started](https://ionq.com/get-started). ## Service class The main entrance for accessing IonQ's API are instances of the `cirq_ionq.Service` class. -These objects need to be initialized with an api key, see -[Access and Authentication](access.md) for details. +These objects need to be initialized with an API key, see +[Access and Authentication](access.md) for details on obtaining one. The basic steps for running a quantum circuit in a blocking manner are: @@ -27,12 +28,12 @@ import cirq_ionq as ionq # A circuit that applies a square root of NOT and then a measurement. qubit = cirq.LineQubit(0) circuit = cirq.Circuit( - cirq.X(qubit)**0.5, # Square root of NOT. - cirq.measure(qubit, key='x') # Measurement store in key 'x' + cirq.X(qubit)**0.5, # Square root of NOT. + cirq.measure(qubit, key='x') # Measurement store in key 'x' ) # Create a ionq.Service object. -# Replace API_KEY with your api key. +# Replace API_KEY with your API key. # Alternatively, if you have the IONQ_API_KEY environment # variable set, you can omit specifying this api_key parameters. service = ionq.Service(api_key=API_KEY) @@ -49,39 +50,70 @@ print(f'Histogram: {histogram}') # You can also get the data as a pandas frame. print(f'Data:\n{result.data}') ``` -This produces output (will vary due to quantum randomness!) + +This produces the following output: (will vary due to quantum randomness!) ``` Histogram: Counter({0: 53, 1: 47}) Data: - x -0 0 -1 0 -2 0 -3 0 -4 0 + x +0 0 +1 0 +2 0 +3 0 +4 0 .. .. -95 1 -96 1 -97 1 -98 1 -99 1 +95 1 +96 1 +97 1 +98 1 +99 1 [100 rows x 1 columns] ``` -## Service options +## Service parameters -In addition to the `api_key`, there are some other options which are -useful for configuring the service. These are passed as arguments +In addition to the `api_key`, there are some other parameters which are +useful for configuring the service. These are passed as arguments when creating a `cirq_ionq.Service` object. -* `remote_host`: The location of the api in the form of an url. If this is None, +* `remote_host`: The location of the API in the form of a URL. If this is None, then this instance will use the environment variable `IONQ_REMOTE_HOST`. If that variable is not set, then this uses `https://api.ionq.co/{api_version}`. -* `default_target`: this is a string of either `simulator` or `qpu`. By setting this you do not have to specify a target every time you run a job using `run`, `create_job` or via the `sampler` interface. A helpful pattern is to create two services with defaults for the simulator and for the QPU separately. -* `api_version`: Version of the api. Defaults to 'v0.1'. -* `max_retry_seconds`: The API will pull with exponential backoff for completed jobs. By specifying this you can change the number of seconds before this retry gives up. It is common to set this to a very small number when, for example, wanting to fail fast, or to be set very long for long running jobs. +* `default_target`: this is a string of either `simulator` or `qpu`. By setting this, you do not have to specify a target every time you run a job using `run`, `create_job` or via the `sampler` interface. A helpful pattern is to create two services with defaults for the simulator and for the QPU separately. +* `api_version`: Version of the API to be used. Defaults to 'v0.3'. +* `max_retry_seconds`: The API will poll with exponential backoff for completed jobs. By specifying this, you can change the number of seconds before this retry gives up. It is common to set this to a very small number when, for example, wanting to fail fast, or to be set very high for long running jobs. + +## Run parameters + +When running a job, there are several parameters that can be provided: + +* `circuit`: The `cirq.Circuit` to run. +* `repetitions`: The number of times to run the circuit. +* `name`: An name for the created job (optional.) +* `target`: Where to run the job. Can be 'qpu' or 'simulator'. +* `param_resolver`: A `cirq.ParamResolver` to resolve parameters in `circuit`. +* `seed`: For `simulation` jobs, specify the seed for simulating results. If None, this will be `np.random`, if an int, will be `np.random.RandomState(int)`, otherwise must be a modulate similar to `np.random`. +* `error_mitigation`: A dictionary of error mitigation settings. Valid keys include: + - 'debias': Set to true to turn on [debiasing](https://ionq.com/resources/debiasing-and-sharpening), which can improve circuit performance by removing qubit-specific noise. _Debiasing is on by default for Aria-class systems._ +* `sharpen`: Get sharpened results from debiasing. Off by default. Will generally return more accurate results if your expected output distribution has peaks. +* `extra_query_params`: A dictionary that specifies additional parameters to be provided in the request. (Currently unused) + +Here is an example of using error mitigation and sharpening options: + +```python +# Run a program against the service with error mitigation and sharpening +result = service.run( + circuit=circuit, + repetitions=100, + target='qpu', + error_mitigation={'debias': True}, + sharpen=True +) +``` + +The run method will return a `cirq.Result` object from which you can get a histogram of results. Refer to the first example in this doc for how to process the results. ## Next steps