diff --git a/cirq-ionq/cirq_ionq/ionq_client.py b/cirq-ionq/cirq_ionq/ionq_client.py index 859742dcfe2..fc500557845 100644 --- a/cirq-ionq/cirq_ionq/ionq_client.py +++ b/cirq-ionq/cirq_ionq/ionq_client.py @@ -101,7 +101,8 @@ def __init__( ), f'Target can only be one of {self.SUPPORTED_TARGETS} but was {default_target}.' assert max_retry_seconds >= 0, 'Negative retry not possible without time machine.' - self.url = f'{url.scheme}://{url.netloc}/{api_version}' + self.url_base = f'{url.scheme}://{url.netloc}' + self.url = f'{self.url_base}/{api_version}' self.headers = self.api_headers(api_key) self.default_target = default_target self.max_retry_seconds = max_retry_seconds @@ -220,7 +221,7 @@ def get_results( extra_query_params: Specify any parameters to include in the request. Returns: - extra_query_paramsresponse as a dict. + response as a dict. Raises: IonQNotFoundException: If job or results don't exist. @@ -251,6 +252,24 @@ def request(): return self._make_request(request, {}).json() + def get_shots(self, shots_url): + """Get job shotwise output from IonQ API. + + Args: + shots_url: The shots URL as returned by the IonQ API. + + Returns: + response as a dict. + + Raises: + IonQException: For other API call failures. + """ + + def request(): + return requests.get(f"{self.url_base}/{shots_url}", headers=self.headers) + + return self._make_request(request, {}).json() + def list_jobs( self, status: str | None = None, limit: int = 100, batch_size: int = 1000 ) -> list[dict[str, Any]]: diff --git a/cirq-ionq/cirq_ionq/ionq_client_test.py b/cirq-ionq/cirq_ionq/ionq_client_test.py index dad5b425e86..cde81591169 100644 --- a/cirq-ionq/cirq_ionq/ionq_client_test.py +++ b/cirq-ionq/cirq_ionq/ionq_client_test.py @@ -595,6 +595,23 @@ def test_ionq_client_get_job_retry(mock_get): assert mock_get.call_count == 2 +@mock.patch('requests.get') +def test_ionq_client_get_shots(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') + client.batch_mode = False + response = client.get_shots(shots_url="v0.4/results/shots/") + 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.4/results/shots/', headers=expected_headers) + + @mock.patch('requests.get') def test_ionq_client_get_results(mock_get): mock_get.return_value.ok = True diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index cad0cc9987e..bb3785ad458 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -240,6 +240,18 @@ def results( f'Job was not completed successfully. Instead had status: {self.status()}' ) + shotwise_results = None + retrieve_shotwise_result = self.target().startswith('qpu') or ( + "noise" in self._job + and "model" in self._job["noise"] + and self._job["noise"]["model"] != "ideal" + ) + if retrieve_shotwise_result: + try: + shotwise_results = self._client.get_shots(self._job["results"]["shots"]["url"]) + except: + pass + backend_results = self._client.get_results( job_id=self.job_id(), sharpen=sharpen, extra_query_params=extra_query_params ) @@ -267,6 +279,7 @@ def results( counts=counts, num_qubits=self.num_qubits(circuit_index), measurement_dict=self.measurement_dict(circuit_index=circuit_index), + shotwise_results=shotwise_results, ) ) return big_endian_results_qpu @@ -283,6 +296,7 @@ def results( num_qubits=self.num_qubits(circuit_index), measurement_dict=self.measurement_dict(circuit_index=circuit_index), repetitions=self.repetitions(), + shotwise_results=shotwise_results, ) ) return big_endian_results_sim diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index 8572769c3a0..b4e07bd337f 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -18,6 +18,7 @@ import warnings from unittest import mock +from numpy import array import pytest import cirq_ionq as ionq @@ -456,3 +457,68 @@ def test_job_fields_update_status(): assert job.name() == 'bacon' assert job.num_qubits() == 5 assert job.repetitions() == 1000 + + +def test_shotwise_job_results_ideal_simulator(): + mock_client = mock.MagicMock() + mock_client.get_shots.return_value = [1, 1, 1, 1, 1] + mock_client.get_results.return_value = {'0': '1'} + job_dict = { + 'id': 'my_id', + 'status': 'completed', + 'stats': {'qubits': '2'}, + 'backend': 'simulator', + 'metadata': { + 'shots': '5', + 'measurements': json.dumps([{'measurement0': f'results{chr(31)}0,1'}]), + }, + 'results': {'shots': {'url': 'http://fake.url/shots'}}, + "noise": {"model": "ideal"}, + } + job = ionq.Job(mock_client, job_dict) + results = job.results() + cirq_result = results[0].to_cirq_result() + assert cirq_result.measurements["results"].tolist() == [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] + + +def test_shotwise_job_results_noisy_simulator(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} + mock_client.get_shots.return_value = [2, 1, 3, 1, 0] + job_dict = { + 'id': 'my_id', + 'status': 'completed', + 'stats': {'qubits': '2'}, + 'backend': 'simulator', + 'metadata': { + 'shots': '5', + 'measurements': json.dumps([{'measurement0': f'results{chr(31)}0,1'}]), + }, + 'results': {'shots': {'url': 'http://fake.url/shots'}}, + "noise": {"model": "aria-1"}, + } + job = ionq.Job(mock_client, job_dict) + results = job.results() + cirq_result = results[0].to_cirq_result() + assert cirq_result.measurements["results"].tolist() == [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] + + +def test_job_results_qpu(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = {'0': '0.6', '3': '0.4'} + mock_client.get_shots.return_value = [2, 1, 3, 1, 0] + job_dict = { + 'id': 'my_id', + 'status': 'completed', + 'stats': {'qubits': '2'}, + 'backend': 'qpu', + 'metadata': { + 'shots': 5, + 'measurements': json.dumps([{'measurement0': f'results{chr(31)}0,1'}]), + }, + 'results': {'shots': {'url': 'http://fake.url/shots'}}, + } + job = ionq.Job(mock_client, job_dict) + results = job.results() + cirq_result = results[0].to_cirq_result() + assert cirq_result.measurements["results"].tolist() == [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index d561986c1c6..acb6258da85 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -27,7 +27,11 @@ class QPUResult: """The results of running on an IonQ QPU.""" def __init__( - self, counts: dict[int, int], num_qubits: int, measurement_dict: dict[str, Sequence[int]] + self, + counts: dict[int, int], + num_qubits: int, + measurement_dict: dict[str, Sequence[int]], + shotwise_results: list[int] | None = None, ): # We require a consistent ordering, and here we use bitvector as such. # OrderedDict can be removed in python 3.7, where it is part of the contract. @@ -35,6 +39,7 @@ def __init__( self._num_qubits = num_qubits self._measurement_dict = measurement_dict self._repetitions = sum(self._counts.values()) + self._shotwise_results = shotwise_results def num_qubits(self) -> int: """Returns the number of qubits the circuit was run on.""" @@ -44,6 +49,10 @@ def repetitions(self) -> int: """Returns the number of times the circuit was run.""" return self._repetitions + def shotwise_results(self) -> list[int] | None: + """Returns the shotwise results if available, otherwise None.""" + return self._shotwise_results + def ordered_results(self, key: str | None = None) -> list[int]: """Returns a list of arbitrarily but consistently ordered results as big endian ints. @@ -134,12 +143,24 @@ def to_cirq_result(self, params: cirq.ParamResolver | None = None) -> cirq.Resul 'Can convert to cirq results only if the circuit had measurement gates ' 'with measurement keys.' ) + measurements = {} - for key, targets in self.measurement_dict().items(): - qpu_results = self.ordered_results(key) - measurements[key] = np.array( - list(cirq.big_endian_int_to_bits(x, bit_count=len(targets)) for x in qpu_results) - ) + if self.shotwise_results() is not None: + for key, targets in self.measurement_dict().items(): + bits = [ + list(cirq.big_endian_int_to_bits(int(x), bit_count=len(targets)))[::-1] + for x in self.shotwise_results() + ] + measurements[key] = np.array(bits) + else: + for key, targets in self.measurement_dict().items(): + qpu_results = self.ordered_results(key) + measurements[key] = np.array( + list( + cirq.big_endian_int_to_bits(x, bit_count=len(targets)) for x in qpu_results + ) + ) + return cirq.ResultDict(params=params or cirq.ParamResolver({}), measurements=measurements) def __eq__(self, other): @@ -169,11 +190,13 @@ def __init__( num_qubits: int, measurement_dict: dict[str, Sequence[int]], repetitions: int, + shotwise_results: list[int] | None = None, ): self._probabilities = probabilities self._num_qubits = num_qubits self._measurement_dict = measurement_dict self._repetitions = repetitions + self._shotwise_results = shotwise_results def num_qubits(self) -> int: """Returns the number of qubits the circuit was run on.""" @@ -187,6 +210,10 @@ def repetitions(self) -> int: """ return self._repetitions + def shotwise_results(self) -> list[int] | None: + """Returns the shotwise results if available, otherwise None.""" + return self._shotwise_results + def probabilities(self, key: str | None = None) -> dict[int, float]: """Returns the probabilities of the measurement results. @@ -264,26 +291,36 @@ def to_cirq_result( 'Can convert to cirq results only if the circuit had measurement gates ' 'with measurement keys.' ) - rand = cirq.value.parse_random_state(seed) - measurements = {} - values, weights = zip(*list(self.probabilities().items())) - # normalize weights to sum to 1 if within tolerance because - # IonQ's pauliexp gates results are not extremely precise - total = sum(weights) - if np.isclose(total, 1.0, rtol=0, atol=1e-5): - weights = tuple((w / total for w in weights)) + measurements = {} - indices = rand.choice( - range(len(values)), p=weights, size=override_repetitions or self.repetitions() - ) - rand_values = np.array(values)[indices] - for key, targets in self.measurement_dict().items(): - bits = [ - [(value >> (self.num_qubits() - target - 1)) & 1 for target in targets] - for value in rand_values - ] - measurements[key] = np.array(bits) + if self.shotwise_results() is not None: + for key, targets in self.measurement_dict().items(): + # why do we need to reverse here? In QpuResult we don't do that .. + bits = [ + list(cirq.big_endian_int_to_bits(int(x), bit_count=len(targets)))[::-1] + for x in self.shotwise_results() + ] + measurements[key] = np.array(bits) + else: + rand = cirq.value.parse_random_state(seed) + values, weights = zip(*list(self.probabilities().items())) + # normalize weights to sum to 1 if within tolerance because + # IonQ's pauliexp gates results are not extremely precise + total = sum(weights) + if np.isclose(total, 1.0, rtol=0, atol=1e-5): + weights = tuple((w / total for w in weights)) + + indices = rand.choice( + range(len(values)), p=weights, size=override_repetitions or self.repetitions() + ) + rand_values = np.array(values)[indices] + for key, targets in self.measurement_dict().items(): + bits = [ + [(value >> (self.num_qubits() - target - 1)) & 1 for target in targets] + for value in rand_values + ] + measurements[key] = np.array(bits) return cirq.ResultDict(params=params or cirq.ParamResolver({}), measurements=measurements) def __eq__(self, other): diff --git a/cirq-ionq/cirq_ionq/results_test.py b/cirq-ionq/cirq_ionq/results_test.py index 176764ae21c..7925e96c5f4 100644 --- a/cirq-ionq/cirq_ionq/results_test.py +++ b/cirq-ionq/cirq_ionq/results_test.py @@ -22,11 +22,14 @@ def test_qpu_result_fields(): - result = ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [0]}) + result = ionq.QPUResult( + {0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [0]}, shotwise_results=[1, 2, 3] + ) assert result.counts() == {0: 10, 1: 10} assert result.repetitions() == 20 assert result.num_qubits() == 1 assert result.measurement_dict() == {'a': [0]} + assert result.shotwise_results() == [1, 2, 3] def test_qpu_result_str(): @@ -160,12 +163,17 @@ def test_ordered_results_invalid_key(): def test_simulator_result_fields(): result = ionq.SimulatorResult( - {0: 0.4, 1: 0.6}, num_qubits=1, measurement_dict={'a': [0]}, repetitions=100 + {0: 0.4, 1: 0.6}, + num_qubits=1, + measurement_dict={'a': [0]}, + repetitions=100, + shotwise_results=[1, 2, 3], ) assert result.probabilities() == {0: 0.4, 1: 0.6} assert result.num_qubits() == 1 assert result.measurement_dict() == {'a': [0]} assert result.repetitions() == 100 + assert result.shotwise_results() == [1, 2, 3] def test_simulator_result_str():