From 155bc72ef5b3c5128ff9fedcf3e562c3a43df1a8 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Wed, 5 Nov 2025 19:26:29 +0200 Subject: [PATCH 01/30] Start working on shotwise output. --- cirq-ionq/cirq_ionq/ionq_client.py | 23 ++++++++++- cirq-ionq/cirq_ionq/job.py | 14 +++++++ cirq-ionq/cirq_ionq/results.py | 64 ++++++++++++++++++++---------- 3 files changed, 79 insertions(+), 22 deletions(-) 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/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/results.py b/cirq-ionq/cirq_ionq/results.py index d561986c1c6..1941b86caa3 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.""" @@ -169,11 +174,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.""" @@ -264,26 +271,43 @@ 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)) - 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) + measurements = {} + print("SHOTWISE RESULTS:", self._shotwise_results) + + 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())) + print("--------------------------------- Values:", values) + print("--------------------------------- Weights:", weights) + # 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() + ) + print("INDICES:", indices) + rand_values = np.array(values)[indices] + print("RANDOM VALUES:", rand_values) + for key, targets in self.measurement_dict().items(): + print(" **************** Key:", key, " targets:", targets) + bits = [ + [(value >> (self.num_qubits() - target - 1)) & 1 for target in targets] + for value in rand_values + ] + measurements[key] = np.array(bits) + print("Here are the measurement results: ", measurements) return cirq.ResultDict(params=params or cirq.ParamResolver({}), measurements=measurements) def __eq__(self, other): From ac65b95c6e22393c73f8d104187b42ccf80d1857 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Thu, 13 Nov 2025 17:23:05 +0200 Subject: [PATCH 02/30] Update tests. Update production code for qpus. --- cirq-ionq/cirq_ionq/ionq_client_test.py | 17 +++++++ cirq-ionq/cirq_ionq/job_test.py | 66 +++++++++++++++++++++++++ cirq-ionq/cirq_ionq/results.py | 41 +++++++++------ cirq-ionq/cirq_ionq/results_test.py | 12 ++++- 4 files changed, 120 insertions(+), 16 deletions(-) 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_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 1941b86caa3..acb6258da85 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -49,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. @@ -139,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): @@ -194,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. @@ -273,21 +293,18 @@ def to_cirq_result( ) measurements = {} - print("SHOTWISE RESULTS:", self._shotwise_results) - if self._shotwise_results is not None: + 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 + 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())) - print("--------------------------------- Values:", values) - print("--------------------------------- Weights:", weights) # normalize weights to sum to 1 if within tolerance because # IonQ's pauliexp gates results are not extremely precise total = sum(weights) @@ -297,17 +314,13 @@ def to_cirq_result( indices = rand.choice( range(len(values)), p=weights, size=override_repetitions or self.repetitions() ) - print("INDICES:", indices) rand_values = np.array(values)[indices] - print("RANDOM VALUES:", rand_values) for key, targets in self.measurement_dict().items(): - print(" **************** Key:", key, " targets:", targets) bits = [ [(value >> (self.num_qubits() - target - 1)) & 1 for target in targets] for value in rand_values ] measurements[key] = np.array(bits) - print("Here are the measurement results: ", measurements) 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(): From 9ec87a957c1134ff1b335d6705c7c17a53c79e6d Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Wed, 26 Nov 2025 17:29:17 +0200 Subject: [PATCH 03/30] Remove incorrectly merged code. --- cirq-ionq/cirq_ionq/results.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index e31f75436e4..0ddcc4207c6 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -294,12 +294,6 @@ def to_cirq_result( ) measurements = {} - # 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) - 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 .. From b6ac4f2c1a41e45293b2b86eb20c0885650f9d7e Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Wed, 26 Nov 2025 18:12:26 +0200 Subject: [PATCH 04/30] Fixing linting, formatting errors. --- cirq-ionq/cirq_ionq/job_test.py | 6 ++---- cirq-ionq/cirq_ionq/results.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index b4e07bd337f..7e6043ec2ac 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -15,12 +15,10 @@ from __future__ import annotations import json +import pytest import warnings from unittest import mock -from numpy import array -import pytest - import cirq_ionq as ionq @@ -503,7 +501,7 @@ def test_shotwise_job_results_noisy_simulator(): assert cirq_result.measurements["results"].tolist() == [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] -def test_job_results_qpu(): +def test_shotwise_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] diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index 0ddcc4207c6..d0d18be0cd1 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -309,7 +309,7 @@ def to_cirq_result( # 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)) + weights = tuple(w / total for w in weights) indices = rand.choice( range(len(values)), p=weights, size=override_repetitions or self.repetitions() From 0c65398daca0baf7526a6f86bd6a9c14a50fc5dd Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Wed, 26 Nov 2025 18:15:43 +0200 Subject: [PATCH 05/30] Fix format error. --- cirq-ionq/cirq_ionq/job_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index 7e6043ec2ac..bdd9cf7cb5d 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -15,10 +15,11 @@ from __future__ import annotations import json -import pytest import warnings from unittest import mock +import pytest + import cirq_ionq as ionq From c17922d19360c44306cb6ce92bd69d6ad2d9f50d Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Thu, 27 Nov 2025 11:49:47 +0200 Subject: [PATCH 06/30] Fix mypy error. --- cirq-ionq/cirq_ionq/results.py | 16 ++++------------ cirq-ionq/cirq_ionq/results_test.py | 4 ++-- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index d0d18be0cd1..edb8bccd846 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -50,10 +50,6 @@ 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. @@ -146,11 +142,11 @@ def to_cirq_result(self, params: cirq.ParamResolver | None = None) -> cirq.Resul ) measurements = {} - if self.shotwise_results() is not None: + 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() + for x in self._shotwise_results ] measurements[key] = np.array(bits) else: @@ -211,10 +207,6 @@ 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. @@ -294,12 +286,12 @@ def to_cirq_result( ) measurements = {} - if self.shotwise_results() is not None: + 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() + for x in self._shotwise_results ] measurements[key] = np.array(bits) else: diff --git a/cirq-ionq/cirq_ionq/results_test.py b/cirq-ionq/cirq_ionq/results_test.py index 7925e96c5f4..1e787f646f8 100644 --- a/cirq-ionq/cirq_ionq/results_test.py +++ b/cirq-ionq/cirq_ionq/results_test.py @@ -29,7 +29,7 @@ def test_qpu_result_fields(): assert result.repetitions() == 20 assert result.num_qubits() == 1 assert result.measurement_dict() == {'a': [0]} - assert result.shotwise_results() == [1, 2, 3] + assert result._shotwise_results == [1, 2, 3] def test_qpu_result_str(): @@ -173,7 +173,7 @@ def test_simulator_result_fields(): assert result.num_qubits() == 1 assert result.measurement_dict() == {'a': [0]} assert result.repetitions() == 100 - assert result.shotwise_results() == [1, 2, 3] + assert result._shotwise_results == [1, 2, 3] def test_simulator_result_str(): From 8fc36e2126cd99ba5bdbc2649a10f2c38ffa737f Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Thu, 27 Nov 2025 12:59:03 +0200 Subject: [PATCH 07/30] Remove comment. --- cirq-ionq/cirq_ionq/results.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index edb8bccd846..0e1333f3283 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -288,7 +288,6 @@ def to_cirq_result( measurements = {} 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 From 41bf9cc3b01f7b8c2bc360ab79c831656d2b5db0 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Mon, 9 Mar 2026 14:13:32 +0200 Subject: [PATCH 08/30] Fixing shotwise tests. --- cirq-ionq/cirq_ionq/job_test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index 552a1a48d3e..bf4b49a7da9 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -475,8 +475,8 @@ def test_shotwise_job_results_ideal_simulator(): "noise": {"model": "ideal"}, } job = ionq.Job(mock_client, job_dict) - results = job.results() - cirq_result = results[0].to_cirq_result() + result = job.results() + cirq_result = result.to_cirq_result() assert cirq_result.measurements["results"].tolist() == [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] @@ -497,8 +497,8 @@ def test_shotwise_job_results_noisy_simulator(): "noise": {"model": "aria-1"}, } job = ionq.Job(mock_client, job_dict) - results = job.results() - cirq_result = results[0].to_cirq_result() + result = job.results() + cirq_result = result.to_cirq_result() assert cirq_result.measurements["results"].tolist() == [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] @@ -518,6 +518,6 @@ def test_shotwise_job_results_qpu(): 'results': {'shots': {'url': 'http://fake.url/shots'}}, } job = ionq.Job(mock_client, job_dict) - results = job.results() - cirq_result = results[0].to_cirq_result() + result = job.results() + cirq_result = result.to_cirq_result() assert cirq_result.measurements["results"].tolist() == [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] From fdc2b497007c699312992e31311eac77fc748ca7 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Wed, 27 May 2026 17:40:52 +0300 Subject: [PATCH 09/30] Introduce memory argument to the service to controll shots retrieval. Warn on 4xx/5xx errors whne retroeving shots. --- cirq-ionq/cirq_ionq/job.py | 83 +++++++++++++++++++++++------ cirq-ionq/cirq_ionq/job_test.py | 38 +++++++++---- cirq-ionq/cirq_ionq/results.py | 4 +- cirq-ionq/cirq_ionq/service.py | 26 +++++++-- cirq-ionq/cirq_ionq/service_test.py | 75 +++++++++++++++++++++++--- 5 files changed, 188 insertions(+), 38 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index 1c2dd3fec91..b19c9be2c7b 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -68,7 +68,9 @@ class Job: 'data associated with it beyond an id and a status.', ) - def __init__(self, client: cirq_ionq.ionq_client._IonQClient, job_dict: dict): + def __init__( + self, client: cirq_ionq.ionq_client._IonQClient, job_dict: dict, memory: bool = False + ): """Construct an IonQJob. Users should not call this themselves. If you only know the `job_id`, use `get_job` @@ -77,9 +79,11 @@ def __init__(self, client: cirq_ionq.ionq_client._IonQClient, job_dict: dict): Args: client: The client used for calling the API. job_dict: A dict representing the response from a call to get_job on the client. + memory: Whether to attempt to retrieve shotwise results for the job. """ self._client = client self._job = job_dict + self._memory = memory def _refresh_job(self): """If the last fetched job is not terminal, gets the job from the API.""" @@ -257,18 +261,6 @@ 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 ) @@ -278,6 +270,39 @@ def results( is_batch = isinstance(some_inner_value, dict) histograms = list(backend_results.values()) if is_batch else [backend_results] + shotwise_results = [None for _ in histograms] + retrieve_shotwise_result = self._memory and ( + 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: + if len(histograms) > 1: + child_job_ids = self._job.get("child_job_ids") + if child_job_ids is None or len(child_job_ids) != len(histograms): + self._warn_shotwise_fallback( + "However, the job does not have the correct child job " + "ids to retrieve shotwise results for a batch job." + ) + else: + shotwise_results = self._retrieve_child_job_shots(child_job_ids) + else: + shotwise_results = [ + self._client.get_shots(self._job["results"]["shots"]["url"]) + ] + except ( + ionq_exceptions.IonQException, + ionq_exceptions.IonQNotFoundException, + TimeoutError, + ) as ex: + self._warn_shotwise_fallback( + f"However, retrieving shots failed with this error: {ex}." + ) + # IonQ returns results in little endian, but # Cirq prefers to use big endian, so we convert. if self.target().startswith('qpu'): @@ -295,7 +320,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, + shotwise_results=shotwise_results[circuit_index], ) ) return ( @@ -316,7 +341,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, + shotwise_results=shotwise_results[circuit_index], ) ) return ( @@ -325,6 +350,34 @@ def results( else big_endian_results_sim[0] ) + def _retrieve_child_job_shots(self, child_job_ids): + shotwise_results = [] + for child_job_id in child_job_ids: + try: + shotwise_result = self._client.get_shots( + self._client.get_job(child_job_id)["results"]["shots"]["url"] + ) + shotwise_results.append(shotwise_result) + except ( + ionq_exceptions.IonQException, + ionq_exceptions.IonQNotFoundException, + TimeoutError, + ) as ex: + self._warn_shotwise_fallback( + "However, retrieving shots for child job " + f"{child_job_id} failed with this error: {ex}." + ) + shotwise_results.append(None) + return shotwise_results + + def _warn_shotwise_fallback(self, detail): + """Warn that shotwise results will fall back to sampled probabilities.""" + warnings.warn( + "You set the memory argument to `True`. " + f"{detail} Shotwise results will be generated by randomly sampling " + "the probability distribution returned by the IonQ server." + ) + def cancel(self): """Cancel the given job. diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index bf4b49a7da9..0bdffc56281 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -458,9 +458,10 @@ def test_job_fields_update_status(): assert job.repetitions() == 1000 -def test_shotwise_job_results_ideal_simulator(): +@pytest.mark.parametrize('memory', [True, False]) +def test_shotwise_job_results_ideal_simulator(memory): mock_client = mock.MagicMock() - mock_client.get_shots.return_value = [1, 1, 1, 1, 1] + mock_client.get_shots.return_value = [2, 1, 3, 1, 0] mock_client.get_results.return_value = {'0': '1'} job_dict = { 'id': 'my_id', @@ -474,15 +475,17 @@ def test_shotwise_job_results_ideal_simulator(): 'results': {'shots': {'url': 'http://fake.url/shots'}}, "noise": {"model": "ideal"}, } - job = ionq.Job(mock_client, job_dict) + job = ionq.Job(mock_client, job_dict, memory=memory) result = job.results() cirq_result = result.to_cirq_result() assert cirq_result.measurements["results"].tolist() == [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] + mock_client.get_shots.assert_not_called() -def test_shotwise_job_results_noisy_simulator(): +@pytest.mark.parametrize('memory', [True, False]) +def test_shotwise_job_results_noisy_simulator(memory): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} + mock_client.get_results.return_value = {'0': '1'} mock_client.get_shots.return_value = [2, 1, 3, 1, 0] job_dict = { 'id': 'my_id', @@ -496,13 +499,19 @@ def test_shotwise_job_results_noisy_simulator(): 'results': {'shots': {'url': 'http://fake.url/shots'}}, "noise": {"model": "aria-1"}, } - job = ionq.Job(mock_client, job_dict) + job = ionq.Job(mock_client, job_dict, memory=memory) result = job.results() cirq_result = result.to_cirq_result() - assert cirq_result.measurements["results"].tolist() == [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] + expected = ( + [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] + if memory + else [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] + ) + assert cirq_result.measurements["results"].tolist() == expected -def test_shotwise_job_results_qpu(): +@pytest.mark.parametrize('memory', [True, False]) +def test_shotwise_job_results_qpu(memory): 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] @@ -517,7 +526,16 @@ def test_shotwise_job_results_qpu(): }, 'results': {'shots': {'url': 'http://fake.url/shots'}}, } - job = ionq.Job(mock_client, job_dict) + job = ionq.Job(mock_client, job_dict, memory=memory) result = job.results() cirq_result = result.to_cirq_result() - assert cirq_result.measurements["results"].tolist() == [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] + expected = ( + [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] + if memory + else [[0, 0], [0, 0], [0, 0], [1, 1], [1, 1]] + ) + assert cirq_result.measurements["results"].tolist() == expected + if memory: + mock_client.get_shots.assert_called_once_with('http://fake.url/shots') + else: + mock_client.get_shots.assert_not_called() diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index 3f72817699e..6f1c926d129 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -153,9 +153,7 @@ def to_cirq_result(self, params: cirq.ParamResolver | None = None) -> cirq.Resul 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 - ) + [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) diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index 712a77ccec7..7da1a452d68 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -105,6 +105,7 @@ def run( metadata: dict | None = None, dry_run: bool = False, sharpen: bool | None = None, + memory: bool = False, extra_query_params: dict | None = None, ) -> cirq.Result: """Run the given circuit on the IonQ API. @@ -134,6 +135,9 @@ def run( If True, apply majority vote mitigation; if False, apply average mitigation. See: `Debiasing and Sharpening ` + memory: A boolean that determines whether to retrieve shotwise results for the job + from IonQ servers, default is False. When False, shots will be generated locally + by sampling the retrieved probability distribution. noise (dict): {"model": str (required), "seed": int (optional)}. Defaults to None. Available noise models: ideal, aria-1, aria-2, forte-1, forte-enterprise-1 dry_run: If True, the job will be submitted by the API client but not processed @@ -155,6 +159,7 @@ def run( noise=noise, metadata=metadata, dry_run=dry_run, + memory=memory, extra_query_params=extra_query_params, ).results(sharpen=sharpen) result = job_results[0] if isinstance(job_results, list) else job_results @@ -178,6 +183,7 @@ def run_batch( metadata: dict | None = None, dry_run: bool = False, sharpen: bool | None = None, + memory: bool = False, extra_query_params: dict | None = None, ) -> list[cirq.Result]: """Run the given circuits on the IonQ API. @@ -211,6 +217,9 @@ def run_batch( dry_run: If True, the job will be submitted by the API client but not processed remotely. Useful for obtaining cost estimates. Defaults to False. metadata (dict): optional metadata to attach to the job. Defaults to None. + memory: A boolean that determines whether to retrieve shotwise results for the job + from IonQ servers, default is False. When False, shots will be generated locally + by sampling the retrieved probability distribution. extra_query_params: Specify any parameters to include in the request. Returns: @@ -230,6 +239,7 @@ def run_batch( noise=noise, metadata=metadata, dry_run=dry_run, + memory=memory, extra_query_params=extra_query_params, ).results(sharpen=sharpen) @@ -274,6 +284,7 @@ def create_job( noise: dict | None = None, metadata: dict | None = None, dry_run: bool = False, + memory: bool = False, extra_query_params: dict | None = None, ) -> job.Job: """Create a new job to run the given circuit. @@ -303,6 +314,9 @@ def create_job( dry_run: If True, the job will be submitted by the API client but not processed remotely. Useful for obtaining cost estimates. Defaults to False. metadata (dict): optional metadata to attach to the job. Defaults to None. + memory: A boolean that determines whether to retrieve shotwise results for the job + from IonQ servers, default is False. When False, shots will be generated locally + by sampling the retrieved probability distribution. extra_query_params: Specify any parameters to include in the request. Returns: @@ -329,7 +343,7 @@ def create_job( ) # The returned job does not have fully populated fields, so make # a second call and return the results of the fully filled out job. - return self.get_job(result['id']) + return self.get_job(result['id'], memory=memory) def create_batch_job( self, @@ -342,6 +356,7 @@ def create_batch_job( noise: dict | None = None, metadata: dict | None = None, dry_run: bool = False, + memory: bool = False, extra_query_params: dict | None = None, ) -> job.Job: """Create a new job to run the given circuit. @@ -371,6 +386,9 @@ def create_batch_job( dry_run: If True, the job will be submitted by the API client but not processed remotely. Useful for obtaining cost estimates. Defaults to False. metadata (dict): optional metadata to attach to the job. Defaults to None. + memory: A boolean that determines whether to retrieve shotwise results for the job + from IonQ servers, default is False. When False, shots will be generated locally + by sampling the retrieved probability distribution. extra_query_params: Specify any parameters to include in the request. Returns: @@ -398,9 +416,9 @@ def create_batch_job( ) # The returned job does not have fully populated fields, so make # a second call and return the results of the fully filled out job. - return self.get_job(result['id']) + return self.get_job(result['id'], memory=memory) - def get_job(self, job_id: str) -> job.Job: + def get_job(self, job_id: str, memory: bool) -> job.Job: """Gets a job that has been created on the IonQ API. Args: @@ -415,7 +433,7 @@ def get_job(self, job_id: str) -> job.Job: IonQException: If there was an error accessing the API. """ job_dict = self._client.get_job(job_id=job_id) - return job.Job(client=self._client, job_dict=job_dict) + return job.Job(client=self._client, job_dict=job_dict, memory=memory) def list_jobs( self, status: str | None = None, limit: int = 100, batch_size: int = 1000 diff --git a/cirq-ionq/cirq_ionq/service_test.py b/cirq-ionq/cirq_ionq/service_test.py index e9c90767d21..49e1b1c4ad3 100644 --- a/cirq-ionq/cirq_ionq/service_test.py +++ b/cirq-ionq/cirq_ionq/service_test.py @@ -63,6 +63,21 @@ def test_service_run(target, expected_results): assert create_job_kwargs['name'] == 'bacon' +def test_service_run_passes_memory_to_create_job(): + service = ionq.Service(remote_host='http://example.com', api_key='key') + q = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.measure(q, key='m')) + qpu_result = ionq.QPUResult(counts={1: 1}, num_qubits=1, measurement_dict={'m': [0]}) + mock_job = mock.MagicMock() + mock_job.results.return_value = qpu_result + + with mock.patch.object(service, 'create_job', return_value=mock_job) as create_job: + result = service.run(circuit=circuit, repetitions=1, target='qpu', memory=True) + + assert result == qpu_result.to_cirq_result(params=cirq.ParamResolver({})) + assert create_job.call_args.kwargs['memory'] is True + + @pytest.mark.parametrize( 'target,expected_results1,expected_results2', [ @@ -125,6 +140,21 @@ def test_service_run_batch(target, expected_results1, expected_results2): assert create_job_kwargs['name'] == 'bacon' +def test_service_run_batch_passes_memory_to_create_batch_job(): + service = ionq.Service(remote_host='http://example.com', api_key='key') + q = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.measure(q, key='m')) + qpu_result = ionq.QPUResult(counts={1: 1}, num_qubits=1, measurement_dict={'m': [0]}) + mock_job = mock.MagicMock() + mock_job.results.return_value = qpu_result + + with mock.patch.object(service, 'create_batch_job', return_value=mock_job) as create_batch_job: + results = service.run_batch(circuits=[circuit], repetitions=1, target='qpu', memory=True) + + assert results == [qpu_result.to_cirq_result(params=cirq.ParamResolver({}))] + assert create_batch_job.call_args.kwargs['memory'] is True + + def test_sampler(): service = ionq.Service(remote_host='http://example.com', api_key='key') mock_client = mock.MagicMock() @@ -151,34 +181,67 @@ def test_sampler(): mock_client.create_job.assert_called_once() -def test_service_get_job(): +@pytest.mark.parametrize('memory', [True, False]) +def test_service_get_job(memory): service = ionq.Service(remote_host='http://example.com', api_key='key') mock_client = mock.MagicMock() job_dict = {'id': 'job_id', 'status': 'ready'} mock_client.get_job.return_value = job_dict service._client = mock_client - job = service.get_job('job_id') - assert job.job_id() == 'job_id' + with mock.patch('cirq_ionq.service.job.Job', autospec=True) as job_cls: + returned_job = service.get_job('job_id', memory=memory) + + assert returned_job is job_cls.return_value mock_client.get_job.assert_called_with(job_id='job_id') + job_cls.assert_called_once_with(client=mock_client, job_dict=job_dict, memory=memory) def test_service_create_job(): service = ionq.Service(remote_host='http://example.com', api_key='key') mock_client = mock.MagicMock() mock_client.create_job.return_value = {'id': 'job_id', 'status': 'ready'} - mock_client.get_job.return_value = {'id': 'job_id', 'status': 'completed'} service._client = mock_client circuit = cirq.Circuit(cirq.X(cirq.LineQubit(0))) - job = service.create_job(circuit=circuit, repetitions=100, target='qpu', name='bacon') - assert job.status() == 'completed' + mock_job = mock.MagicMock() + with mock.patch.object(service, 'get_job', return_value=mock_job) as get_job: + job = service.create_job( + circuit=circuit, repetitions=100, target='qpu', name='bacon', memory=True + ) + + assert job is mock_job + create_job_kwargs = mock_client.create_job.call_args[1] + # Serialization induces a float, so we don't validate full circuit. + assert create_job_kwargs['serialized_program'].input['qubits'] == 1 + assert create_job_kwargs['repetitions'] == 100 + assert create_job_kwargs['target'] == 'qpu' + assert create_job_kwargs['name'] == 'bacon' + get_job.assert_called_once_with('job_id', memory=True) + + +def test_service_create_batch_job(): + service = ionq.Service(remote_host='http://example.com', api_key='key') + mock_client = mock.MagicMock() + mock_client.create_job.return_value = {'id': 'job_id', 'status': 'ready'} + service._client = mock_client + + circuit = cirq.Circuit(cirq.X(cirq.LineQubit(0))) + mock_job = mock.MagicMock() + with mock.patch.object(service, 'get_job', return_value=mock_job) as get_job: + job = service.create_batch_job( + circuits=[circuit], repetitions=100, target='qpu', name='bacon', memory=True + ) + + assert job is mock_job create_job_kwargs = mock_client.create_job.call_args[1] # Serialization induces a float, so we don't validate full circuit. assert create_job_kwargs['serialized_program'].input['qubits'] == 1 assert create_job_kwargs['repetitions'] == 100 assert create_job_kwargs['target'] == 'qpu' assert create_job_kwargs['name'] == 'bacon' + assert create_job_kwargs['batch_mode'] is True + get_job.assert_called_once_with('job_id', memory=True) def test_service_list_jobs(): From b72b43de09870a0fe289f4a19b4341663a0dcf22 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Wed, 27 May 2026 18:18:56 +0300 Subject: [PATCH 10/30] Fix linting issues, extend tests, add default arg. --- cirq-ionq/cirq_ionq/job_test.py | 25 ++++++++++++++++++------- cirq-ionq/cirq_ionq/service.py | 4 +++- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index 0bdffc56281..210a707d0cf 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -485,7 +485,7 @@ def test_shotwise_job_results_ideal_simulator(memory): @pytest.mark.parametrize('memory', [True, False]) def test_shotwise_job_results_noisy_simulator(memory): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '1'} + 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', @@ -501,12 +501,23 @@ def test_shotwise_job_results_noisy_simulator(memory): } job = ionq.Job(mock_client, job_dict, memory=memory) result = job.results() - cirq_result = result.to_cirq_result() - expected = ( - [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] - if memory - else [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0]] - ) + if memory: + cirq_result = result.to_cirq_result() + expected = [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] + mock_client.get_shots.assert_called_once_with('http://fake.url/shots') + else: + fake_random_state = mock.Mock() + fake_random_state.choice.return_value = [1, 0, 1, 1, 0] + with mock.patch( + 'cirq_ionq.results.cirq.value.parse_random_state', return_value=fake_random_state + ) as parse_random_state: + cirq_result = result.to_cirq_result() + + expected = [[1, 0], [0, 0], [1, 0], [1, 0], [0, 0]] + parse_random_state.assert_called_once_with(None) + fake_random_state.choice.assert_called_once_with(range(2), p=(0.6, 0.4), size=5) + mock_client.get_shots.assert_not_called() + assert cirq_result.measurements["results"].tolist() == expected diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index 7da1a452d68..316b3192b51 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -418,7 +418,7 @@ def create_batch_job( # a second call and return the results of the fully filled out job. return self.get_job(result['id'], memory=memory) - def get_job(self, job_id: str, memory: bool) -> job.Job: + def get_job(self, job_id: str, memory: bool = False) -> job.Job: """Gets a job that has been created on the IonQ API. Args: @@ -431,6 +431,8 @@ def get_job(self, job_id: str, memory: bool) -> job.Job: Raises: IonQNotFoundException: If there was no job with the given `job_id`. IonQException: If there was an error accessing the API. + memory: A boolean that determines whether to retrieve shotwise results for the job + from IonQ servers, default is False. """ job_dict = self._client.get_job(job_id=job_id) return job.Job(client=self._client, job_dict=job_dict, memory=memory) From 9ae894c0774e6b796ea3ec5c93bd50ba8770f682 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 2 Jun 2026 10:52:31 +0300 Subject: [PATCH 11/30] Rename shotwise -> memory throughut the code. --- cirq-ionq/cirq_ionq/ionq_client.py | 2 +- cirq-ionq/cirq_ionq/job.py | 40 ++++++++++++++--------------- cirq-ionq/cirq_ionq/job_test.py | 6 ++--- cirq-ionq/cirq_ionq/results.py | 16 ++++++------ cirq-ionq/cirq_ionq/results_test.py | 8 +++--- cirq-ionq/cirq_ionq/service.py | 12 ++++----- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/cirq-ionq/cirq_ionq/ionq_client.py b/cirq-ionq/cirq_ionq/ionq_client.py index 9414fbb0e66..c6bc7c9cfca 100644 --- a/cirq-ionq/cirq_ionq/ionq_client.py +++ b/cirq-ionq/cirq_ionq/ionq_client.py @@ -254,7 +254,7 @@ def request(): return self._make_request(request, {}).json() def get_shots(self, shots_url): - """Get job shotwise output from IonQ API. + """Get job per shot output from IonQ API. Args: shots_url: The shots URL as returned by the IonQ API. diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index b19c9be2c7b..2e152557a74 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -79,7 +79,7 @@ def __init__( Args: client: The client used for calling the API. job_dict: A dict representing the response from a call to get_job on the client. - memory: Whether to attempt to retrieve shotwise results for the job. + memory: Whether to attempt to retrieve memory (per shot) results for the job. """ self._client = client self._job = job_dict @@ -270,8 +270,8 @@ def results( is_batch = isinstance(some_inner_value, dict) histograms = list(backend_results.values()) if is_batch else [backend_results] - shotwise_results = [None for _ in histograms] - retrieve_shotwise_result = self._memory and ( + memory_results = [None for _ in histograms] + retrieve_memory_result = self._memory and ( self.target().startswith('qpu') or ( "noise" in self._job @@ -279,19 +279,19 @@ def results( and self._job["noise"]["model"] != "ideal" ) ) - if retrieve_shotwise_result: + if retrieve_memory_result: try: if len(histograms) > 1: child_job_ids = self._job.get("child_job_ids") if child_job_ids is None or len(child_job_ids) != len(histograms): - self._warn_shotwise_fallback( + self._warn_memory_fallback( "However, the job does not have the correct child job " - "ids to retrieve shotwise results for a batch job." + "ids to retrieve per shot results for a batch job." ) else: - shotwise_results = self._retrieve_child_job_shots(child_job_ids) + memory_results = self._retrieve_child_job_shots(child_job_ids) else: - shotwise_results = [ + memory_results = [ self._client.get_shots(self._job["results"]["shots"]["url"]) ] except ( @@ -299,7 +299,7 @@ def results( ionq_exceptions.IonQNotFoundException, TimeoutError, ) as ex: - self._warn_shotwise_fallback( + self._warn_memory_fallback( f"However, retrieving shots failed with this error: {ex}." ) @@ -320,7 +320,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[circuit_index], + memory_results=memory_results[circuit_index], ) ) return ( @@ -341,7 +341,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[circuit_index], + memory_results=memory_results[circuit_index], ) ) return ( @@ -351,30 +351,30 @@ def results( ) def _retrieve_child_job_shots(self, child_job_ids): - shotwise_results = [] + memory_results = [] for child_job_id in child_job_ids: try: - shotwise_result = self._client.get_shots( + memory_result = self._client.get_shots( self._client.get_job(child_job_id)["results"]["shots"]["url"] ) - shotwise_results.append(shotwise_result) + memory_results.append(memory_result) except ( ionq_exceptions.IonQException, ionq_exceptions.IonQNotFoundException, TimeoutError, ) as ex: - self._warn_shotwise_fallback( + self._warn_memory_fallback( "However, retrieving shots for child job " f"{child_job_id} failed with this error: {ex}." ) - shotwise_results.append(None) - return shotwise_results + memory_results.append(None) + return memory_results - def _warn_shotwise_fallback(self, detail): - """Warn that shotwise results will fall back to sampled probabilities.""" + def _warn_memory_fallback(self, detail): + """Warn that memory results will fall back to sampled probabilities.""" warnings.warn( "You set the memory argument to `True`. " - f"{detail} Shotwise results will be generated by randomly sampling " + f"{detail} Per shot results will be generated by randomly sampling " "the probability distribution returned by the IonQ server." ) diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index 210a707d0cf..51a2633e041 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -459,7 +459,7 @@ def test_job_fields_update_status(): @pytest.mark.parametrize('memory', [True, False]) -def test_shotwise_job_results_ideal_simulator(memory): +def test_memory_job_results_ideal_simulator(memory): mock_client = mock.MagicMock() mock_client.get_shots.return_value = [2, 1, 3, 1, 0] mock_client.get_results.return_value = {'0': '1'} @@ -483,7 +483,7 @@ def test_shotwise_job_results_ideal_simulator(memory): @pytest.mark.parametrize('memory', [True, False]) -def test_shotwise_job_results_noisy_simulator(memory): +def test_memory_job_results_noisy_simulator(memory): 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] @@ -522,7 +522,7 @@ def test_shotwise_job_results_noisy_simulator(memory): @pytest.mark.parametrize('memory', [True, False]) -def test_shotwise_job_results_qpu(memory): +def test_memory_job_results_qpu(memory): 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] diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index 6f1c926d129..00cae5a914e 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -32,7 +32,7 @@ def __init__( counts: dict[int, int], num_qubits: int, measurement_dict: dict[str, Sequence[int]], - shotwise_results: list[int] | None = None, + memory_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. @@ -40,7 +40,7 @@ def __init__( self._num_qubits = num_qubits self._measurement_dict = measurement_dict self._repetitions = sum(self._counts.values()) - self._shotwise_results = shotwise_results + self._memory_results = memory_results def num_qubits(self) -> int: """Returns the number of qubits the circuit was run on.""" @@ -142,11 +142,11 @@ def to_cirq_result(self, params: cirq.ParamResolver | None = None) -> cirq.Resul ) measurements = {} - if self._shotwise_results is not None: + if self._memory_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 + for x in self._memory_results ] measurements[key] = np.array(bits) else: @@ -185,13 +185,13 @@ def __init__( num_qubits: int, measurement_dict: dict[str, Sequence[int]], repetitions: int, - shotwise_results: list[int] | None = None, + memory_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 + self._memory_results = memory_results def num_qubits(self) -> int: """Returns the number of qubits the circuit was run on.""" @@ -284,11 +284,11 @@ def to_cirq_result( ) measurements = {} - if self._shotwise_results is not None: + if self._memory_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 + for x in self._memory_results ] measurements[key] = np.array(bits) else: diff --git a/cirq-ionq/cirq_ionq/results_test.py b/cirq-ionq/cirq_ionq/results_test.py index 1e787f646f8..20ce4804045 100644 --- a/cirq-ionq/cirq_ionq/results_test.py +++ b/cirq-ionq/cirq_ionq/results_test.py @@ -23,13 +23,13 @@ def test_qpu_result_fields(): result = ionq.QPUResult( - {0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [0]}, shotwise_results=[1, 2, 3] + {0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [0]}, memory_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] + assert result._memory_results == [1, 2, 3] def test_qpu_result_str(): @@ -167,13 +167,13 @@ def test_simulator_result_fields(): num_qubits=1, measurement_dict={'a': [0]}, repetitions=100, - shotwise_results=[1, 2, 3], + memory_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] + assert result._memory_results == [1, 2, 3] def test_simulator_result_str(): diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index 316b3192b51..22e312374fd 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -135,7 +135,7 @@ def run( If True, apply majority vote mitigation; if False, apply average mitigation. See: `Debiasing and Sharpening ` - memory: A boolean that determines whether to retrieve shotwise results for the job + memory: A boolean that determines whether to retrieve per shot results for the job from IonQ servers, default is False. When False, shots will be generated locally by sampling the retrieved probability distribution. noise (dict): {"model": str (required), "seed": int (optional)}. Defaults to None. @@ -217,7 +217,7 @@ def run_batch( dry_run: If True, the job will be submitted by the API client but not processed remotely. Useful for obtaining cost estimates. Defaults to False. metadata (dict): optional metadata to attach to the job. Defaults to None. - memory: A boolean that determines whether to retrieve shotwise results for the job + memory: A boolean that determines whether to retrieve per shot results for the job from IonQ servers, default is False. When False, shots will be generated locally by sampling the retrieved probability distribution. extra_query_params: Specify any parameters to include in the request. @@ -314,7 +314,7 @@ def create_job( dry_run: If True, the job will be submitted by the API client but not processed remotely. Useful for obtaining cost estimates. Defaults to False. metadata (dict): optional metadata to attach to the job. Defaults to None. - memory: A boolean that determines whether to retrieve shotwise results for the job + memory: A boolean that determines whether to retrieve per shot results for the job from IonQ servers, default is False. When False, shots will be generated locally by sampling the retrieved probability distribution. extra_query_params: Specify any parameters to include in the request. @@ -386,7 +386,7 @@ def create_batch_job( dry_run: If True, the job will be submitted by the API client but not processed remotely. Useful for obtaining cost estimates. Defaults to False. metadata (dict): optional metadata to attach to the job. Defaults to None. - memory: A boolean that determines whether to retrieve shotwise results for the job + memory: A boolean that determines whether to retrieve per shot results for the job from IonQ servers, default is False. When False, shots will be generated locally by sampling the retrieved probability distribution. extra_query_params: Specify any parameters to include in the request. @@ -431,8 +431,8 @@ def get_job(self, job_id: str, memory: bool = False) -> job.Job: Raises: IonQNotFoundException: If there was no job with the given `job_id`. IonQException: If there was an error accessing the API. - memory: A boolean that determines whether to retrieve shotwise results for the job - from IonQ servers, default is False. + memory: A boolean that determines whether to retrieve per shot results + for the job from IonQ servers, default is False. """ job_dict = self._client.get_job(job_id=job_id) return job.Job(client=self._client, job_dict=job_dict, memory=memory) From d71452522966d6c90ff99be71b062478fdfc9653 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 2 Jun 2026 14:21:17 +0300 Subject: [PATCH 12/30] Bugfix: take into account measurement keys. --- cirq-ionq/cirq_ionq/results.py | 13 +++++++++---- cirq-ionq/cirq_ionq/results_test.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index 00cae5a914e..a0af1884bce 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -24,6 +24,11 @@ import cirq +def _memory_bits_for_targets(value: int, num_qubits: int, targets: Sequence[int]) -> list[int]: + shot_bits = list(cirq.big_endian_int_to_bits(value, bit_count=num_qubits))[::-1] + return [shot_bits[target] for target in targets] + + class QPUResult: """The results of running on an IonQ QPU.""" @@ -145,8 +150,8 @@ def to_cirq_result(self, params: cirq.ParamResolver | None = None) -> cirq.Resul if self._memory_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._memory_results + _memory_bits_for_targets(int(value), self.num_qubits(), targets) + for value in self._memory_results ] measurements[key] = np.array(bits) else: @@ -287,8 +292,8 @@ def to_cirq_result( if self._memory_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._memory_results + _memory_bits_for_targets(int(value), self.num_qubits(), targets) + for value in self._memory_results ] measurements[key] = np.array(bits) else: diff --git a/cirq-ionq/cirq_ionq/results_test.py b/cirq-ionq/cirq_ionq/results_test.py index 20ce4804045..5d0202788ba 100644 --- a/cirq-ionq/cirq_ionq/results_test.py +++ b/cirq-ionq/cirq_ionq/results_test.py @@ -149,6 +149,15 @@ def test_qpu_result_to_cirq_result_multiple_keys(): ) +def test_qpu_result_to_cirq_result_shotwise_uses_target_indices(): + result = ionq.QPUResult( + {0b10: 1}, num_qubits=2, measurement_dict={'b': [1]}, memory_results=[0b10] + ) + assert result.to_cirq_result() == cirq.ResultDict( + params=cirq.ParamResolver({}), measurements={'b': np.array([[1]])} + ) + + def test_qpu_result_to_cirq_result_no_keys(): result = ionq.QPUResult({0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={}) with pytest.raises(ValueError, match='cirq results'): @@ -326,6 +335,15 @@ def test_simulator_result_to_cirq_result_multiple_keys(): ) +def test_simulator_result_to_cirq_result_shotwise_uses_target_indices(): + result = ionq.SimulatorResult( + {0b10: 1.0}, num_qubits=2, measurement_dict={'b': [1]}, repetitions=1, memory_results=[0b10] + ) + assert result.to_cirq_result() == cirq.ResultDict( + params=cirq.ParamResolver({}), measurements={'b': np.array([[1]])} + ) + + def test_simulator_result_to_cirq_result_no_keys(): result = ionq.SimulatorResult( {0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={}, repetitions=3 From 5c734e12c82c54330ca267720d64692a9e70782e Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 2 Jun 2026 15:39:23 +0300 Subject: [PATCH 13/30] Move memory docs in corrcet position. --- cirq-ionq/cirq_ionq/service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index 22e312374fd..fb5f4a39ea1 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -423,7 +423,9 @@ def get_job(self, job_id: str, memory: bool = False) -> job.Job: Args: job_id: The UUID of the job. Jobs are assigned these numbers by the - server during the creation of the job. + server during the creation of the job. + memory: A boolean that determines whether to retrieve per shot results + for the job from IonQ servers, default is False. Returns: A `cirq_ionq.IonQJob` which can be queried for status or results. @@ -431,8 +433,6 @@ def get_job(self, job_id: str, memory: bool = False) -> job.Job: Raises: IonQNotFoundException: If there was no job with the given `job_id`. IonQException: If there was an error accessing the API. - memory: A boolean that determines whether to retrieve per shot results - for the job from IonQ servers, default is False. """ job_dict = self._client.get_job(job_id=job_id) return job.Job(client=self._client, job_dict=job_dict, memory=memory) From 76453ecf47ac3163772ac9fbf41ff47ce3596a30 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 2 Jun 2026 15:40:57 +0300 Subject: [PATCH 14/30] Code format fix. --- cirq-ionq/cirq_ionq/job.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index 2e152557a74..1eda56f7226 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -291,9 +291,7 @@ def results( else: memory_results = self._retrieve_child_job_shots(child_job_ids) else: - memory_results = [ - self._client.get_shots(self._job["results"]["shots"]["url"]) - ] + memory_results = [self._client.get_shots(self._job["results"]["shots"]["url"])] except ( ionq_exceptions.IonQException, ionq_exceptions.IonQNotFoundException, From fc081628ae14b040a38a25077407edbc3037f9d6 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 2 Jun 2026 15:56:58 +0300 Subject: [PATCH 15/30] Shots url is relative. --- cirq-ionq/cirq_ionq/job_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index 51a2633e041..040d51e912e 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -472,7 +472,7 @@ def test_memory_job_results_ideal_simulator(memory): 'shots': '5', 'measurements': json.dumps([{'measurement0': f'results{chr(31)}0,1'}]), }, - 'results': {'shots': {'url': 'http://fake.url/shots'}}, + 'results': {'shots': {'url': '/shots'}}, "noise": {"model": "ideal"}, } job = ionq.Job(mock_client, job_dict, memory=memory) @@ -496,7 +496,7 @@ def test_memory_job_results_noisy_simulator(memory): 'shots': '5', 'measurements': json.dumps([{'measurement0': f'results{chr(31)}0,1'}]), }, - 'results': {'shots': {'url': 'http://fake.url/shots'}}, + 'results': {'shots': {'url': '/shots'}}, "noise": {"model": "aria-1"}, } job = ionq.Job(mock_client, job_dict, memory=memory) @@ -504,7 +504,7 @@ def test_memory_job_results_noisy_simulator(memory): if memory: cirq_result = result.to_cirq_result() expected = [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] - mock_client.get_shots.assert_called_once_with('http://fake.url/shots') + mock_client.get_shots.assert_called_once_with('/shots') else: fake_random_state = mock.Mock() fake_random_state.choice.return_value = [1, 0, 1, 1, 0] @@ -535,7 +535,7 @@ def test_memory_job_results_qpu(memory): 'shots': 5, 'measurements': json.dumps([{'measurement0': f'results{chr(31)}0,1'}]), }, - 'results': {'shots': {'url': 'http://fake.url/shots'}}, + 'results': {'shots': {'url': '/shots'}}, } job = ionq.Job(mock_client, job_dict, memory=memory) result = job.results() @@ -547,6 +547,6 @@ def test_memory_job_results_qpu(memory): ) assert cirq_result.measurements["results"].tolist() == expected if memory: - mock_client.get_shots.assert_called_once_with('http://fake.url/shots') + mock_client.get_shots.assert_called_once_with('/shots') else: mock_client.get_shots.assert_not_called() From bf0d950349c2b20eba168af7b60475315a9dfd4f Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 2 Jun 2026 16:31:23 +0300 Subject: [PATCH 16/30] Add docstrings for private method. --- cirq-ionq/cirq_ionq/job.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index 1eda56f7226..53b9118d6dc 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -349,6 +349,7 @@ def results( ) def _retrieve_child_job_shots(self, child_job_ids): + """Retrieve shots for child jobs. Warn that memory results will fall back to sampled probabilities if retrieval fails.""" memory_results = [] for child_job_id in child_job_ids: try: From 48873c1258f917910d5987d2dea2adf6dfbaf635 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 11:08:01 +0300 Subject: [PATCH 17/30] Add exception handling for cases where shots url is missing. --- cirq-ionq/cirq_ionq/job.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index 53b9118d6dc..65633b465ef 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -291,7 +291,15 @@ def results( else: memory_results = self._retrieve_child_job_shots(child_job_ids) else: - memory_results = [self._client.get_shots(self._job["results"]["shots"]["url"])] + try: + memory_results = [ + self._client.get_shots(self._job["results"]["shots"]["url"]) + ] + except KeyError as ex: + self._warn_memory_fallback( + f"However, retrieving shots for the job failed because the " + f"url for shots result was not found in the job response: {ex}." + ) except ( ionq_exceptions.IonQException, ionq_exceptions.IonQNotFoundException, @@ -349,7 +357,9 @@ def results( ) def _retrieve_child_job_shots(self, child_job_ids): - """Retrieve shots for child jobs. Warn that memory results will fall back to sampled probabilities if retrieval fails.""" + """Retrieve shots for child jobs. Warn that memory results will + fall back to sampled probabilities if retrieval fails. + """ memory_results = [] for child_job_id in child_job_ids: try: @@ -357,6 +367,12 @@ def _retrieve_child_job_shots(self, child_job_ids): self._client.get_job(child_job_id)["results"]["shots"]["url"] ) memory_results.append(memory_result) + except KeyError as ex: + self._warn_memory_fallback( + f"However, retrieving shots for child job {child_job_id} failed because " + f"the url for shots result was not found in the job response: {ex}." + ) + memory_results.append(None) except ( ionq_exceptions.IonQException, ionq_exceptions.IonQNotFoundException, From bc8321b1e05c5a825e05aa89998e47adc535e3f6 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 11:16:54 +0300 Subject: [PATCH 18/30] Fix format issues. --- cirq-ionq/cirq_ionq/job.py | 80 +++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index 65633b465ef..b0a82402479 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -45,27 +45,27 @@ class Job: valid. """ - TERMINAL_STATES = ('completed', 'canceled', 'failed', 'deleted') + TERMINAL_STATES = ("completed", "canceled", "failed", "deleted") document( TERMINAL_STATES, - 'States of the IonQ API job from which the job cannot transition. ' - 'Note that deleted can only exist in a return call from a delete ' - '(subsequent calls will return not found).', + "States of the IonQ API job from which the job cannot transition. " + "Note that deleted can only exist in a return call from a delete " + "(subsequent calls will return not found).", ) - NON_TERMINAL_STATES = ('ready', 'submitted', 'running') + NON_TERMINAL_STATES = ("ready", "submitted", "running") document( - NON_TERMINAL_STATES, 'States of the IonQ API job which can transition to other states.' + NON_TERMINAL_STATES, "States of the IonQ API job which can transition to other states." ) ALL_STATES = TERMINAL_STATES + NON_TERMINAL_STATES - document(ALL_STATES, 'All states that an IonQ API job can exist in.') + document(ALL_STATES, "All states that an IonQ API job can exist in.") - UNSUCCESSFUL_STATES = ('canceled', 'failed', 'deleted') + UNSUCCESSFUL_STATES = ("canceled", "failed", "deleted") document( UNSUCCESSFUL_STATES, - 'States of the IonQ API job when it was not successful and so does not have any ' - 'data associated with it beyond an id and a status.', + "States of the IonQ API job when it was not successful and so does not have any " + "data associated with it beyond an id and a status.", ) def __init__( @@ -87,7 +87,7 @@ def __init__( def _refresh_job(self): """If the last fetched job is not terminal, gets the job from the API.""" - if self._job['status'] not in self.TERMINAL_STATES: + if self._job["status"] not in self.TERMINAL_STATES: self._job = self._client.get_job(self.job_id()) def _check_if_unsuccessful(self): @@ -99,7 +99,7 @@ def job_id(self) -> str: This is the id used for identifying the job by the API. """ - return self._job['id'] + return self._job["id"] def status(self) -> str: """Gets the current status of the job. @@ -114,7 +114,7 @@ def status(self) -> str: The job status. """ self._refresh_job() - return self._job['status'] + return self._job["status"] def target(self) -> str: """Returns the target where the job is to be run, or was run. @@ -127,7 +127,7 @@ def target(self) -> str: IonQException: If unable to get the status of the job from the API. """ self._check_if_unsuccessful() - return self._job['backend'] + return self._job["backend"] def name(self) -> str: """Returns the name of the job which was supplied during job creation. @@ -139,7 +139,7 @@ def name(self) -> str: IonQException: If unable to get the status of the job from the API. """ self._check_if_unsuccessful() - return self._job['name'] + return self._job["name"] def num_qubits(self, circuit_index=None) -> int: """Returns the number of qubits for the job. @@ -149,14 +149,14 @@ def num_qubits(self, circuit_index=None) -> int: IonQException: If unable to get the status of the job from the API. """ self._check_if_unsuccessful() - if 'metadata' in self._job and circuit_index is not None: - if 'qubit_numbers' in self._job['metadata'].keys(): - qubit_numbers = json.loads(self._job['metadata']['qubit_numbers']) + if "metadata" in self._job and circuit_index is not None: + if "qubit_numbers" in self._job["metadata"].keys(): + qubit_numbers = json.loads(self._job["metadata"]["qubit_numbers"]) for index, qubit_number in enumerate(qubit_numbers): if index == circuit_index: return qubit_number - return int(self._job['stats']['qubits']) + return int(self._job["stats"]["qubits"]) def repetitions(self) -> int: """Returns the number of repetitions for the job. @@ -166,33 +166,33 @@ def repetitions(self) -> int: IonQException: If unable to get the status of the job from the API. """ self._check_if_unsuccessful() - return int(self._job['metadata']['shots']) + return int(self._job["metadata"]["shots"]) def measurement_dict(self, circuit_index=0) -> dict[str, Sequence[int]]: """Returns a dictionary of measurement keys to target qubit index.""" measurement_dict: dict[str, Sequence[int]] = {} - if 'metadata' in self._job: + if "metadata" in self._job: measurement_matadata = None - if 'measurements' in self._job['metadata'].keys(): - measurements = json.loads(self._job['metadata']['measurements']) + if "measurements" in self._job["metadata"].keys(): + measurements = json.loads(self._job["metadata"]["measurements"]) for index, measurement in enumerate(measurements): if index == circuit_index: measurement_matadata = measurement break else: - measurement_matadata = self._job['metadata'] + measurement_matadata = self._job["metadata"] if measurement_matadata is not None: - full_str = ''.join( + full_str = "".join( value for key, value in measurement_matadata.items() - if key.startswith('measurement') + if key.startswith("measurement") ) - if full_str == '': + if full_str == "": return measurement_dict for key_value in full_str.split(chr(30)): key, value = key_value.split(chr(31)) - measurement_dict[key] = [int(t) for t in value.split(',')] + measurement_dict[key] = [int(t) for t in value.split(",")] return measurement_dict @@ -247,18 +247,18 @@ def results( break time.sleep(polling_seconds) time_waited_seconds += polling_seconds - if 'warning' in self._job and 'messages' in self._job['warning']: - for warning in self._job['warning']['messages']: + if "warning" in self._job and "messages" in self._job["warning"]: + for warning in self._job["warning"]["messages"]: warnings.warn(warning) - if self.status() != 'completed': - if 'failure' in self._job and 'error' in self._job['failure']: - error = self._job['failure']['error'] - raise RuntimeError(f'Job failed. Error message: {error}') + if self.status() != "completed": + if "failure" in self._job and "error" in self._job["failure"]: + error = self._job["failure"]["error"] + raise RuntimeError(f"Job failed. Error message: {error}") if time_waited_seconds >= timeout_seconds: - raise TimeoutError(f'Job timed out after waiting {time_waited_seconds} seconds.') + raise TimeoutError(f"Job timed out after waiting {time_waited_seconds} seconds.") raise RuntimeError( - f'Job was not completed successfully. Instead had status: {self.status()}' + f"Job was not completed successfully. Instead had status: {self.status()}" ) backend_results = self._client.get_results( @@ -272,7 +272,7 @@ def results( memory_results = [None for _ in histograms] retrieve_memory_result = self._memory and ( - self.target().startswith('qpu') + self.target().startswith("qpu") or ( "noise" in self._job and "model" in self._job["noise"] @@ -311,7 +311,7 @@ def results( # IonQ returns results in little endian, but # Cirq prefers to use big endian, so we convert. - if self.target().startswith('qpu'): + if self.target().startswith("qpu"): big_endian_results_qpu: list[results.QPUResult] = [] for circuit_index, histogram in enumerate(histograms): repetitions = self.repetitions() @@ -358,7 +358,7 @@ def results( def _retrieve_child_job_shots(self, child_job_ids): """Retrieve shots for child jobs. Warn that memory results will - fall back to sampled probabilities if retrieval fails. + fall back to sampled probabilities if retrieval fails. """ memory_results = [] for child_job_id in child_job_ids: @@ -409,4 +409,4 @@ def delete(self): self._job = self._client.delete_job(job_id=self.job_id()) def __str__(self) -> str: - return f'cirq_ionq.Job(job_id={self.job_id()})' + return f"cirq_ionq.Job(job_id={self.job_id()})" From f564312bbdfdf94a96a584693b069e3522f17e4f Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 11:42:59 +0300 Subject: [PATCH 19/30] Reformat code. --- cirq-ionq/cirq_ionq/job.py | 57 ++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index b0a82402479..ead20e974d1 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -280,34 +280,17 @@ def results( ) ) if retrieve_memory_result: - try: - if len(histograms) > 1: - child_job_ids = self._job.get("child_job_ids") - if child_job_ids is None or len(child_job_ids) != len(histograms): - self._warn_memory_fallback( - "However, the job does not have the correct child job " - "ids to retrieve per shot results for a batch job." - ) - else: - memory_results = self._retrieve_child_job_shots(child_job_ids) + if len(histograms) > 1: + child_job_ids = self._job.get("child_job_ids") + if child_job_ids is None or len(child_job_ids) != len(histograms): + self._warn_memory_fallback( + "However, the job does not have the correct child job " + "ids to retrieve per shot results for a batch job." + ) else: - try: - memory_results = [ - self._client.get_shots(self._job["results"]["shots"]["url"]) - ] - except KeyError as ex: - self._warn_memory_fallback( - f"However, retrieving shots for the job failed because the " - f"url for shots result was not found in the job response: {ex}." - ) - except ( - ionq_exceptions.IonQException, - ionq_exceptions.IonQNotFoundException, - TimeoutError, - ) as ex: - self._warn_memory_fallback( - f"However, retrieving shots failed with this error: {ex}." - ) + memory_results = self._retrieve_child_job_shots(child_job_ids) + else: + memory_results = self._retrieve_job_shots() # IonQ returns results in little endian, but # Cirq prefers to use big endian, so we convert. @@ -385,6 +368,26 @@ def _retrieve_child_job_shots(self, child_job_ids): memory_results.append(None) return memory_results + def _retrieve_job_shots(self): + """Retrieve shots for the job. Warn that memory results will + fall back to sampled probabilities if retrieval fails. + """ + memory_results = [None] + try: + memory_results = [self._client.get_shots(self._job["results"]["shots"]["url"])] + except KeyError as ex: + self._warn_memory_fallback( + f"However, retrieving shots for the job failed because the " + f"url for shots result was not found in the job response: {ex}." + ) + except ( + ionq_exceptions.IonQException, + ionq_exceptions.IonQNotFoundException, + TimeoutError, + ) as ex: + self._warn_memory_fallback(f"However, retrieving shots failed with this error: {ex}.") + return memory_results + def _warn_memory_fallback(self, detail): """Warn that memory results will fall back to sampled probabilities.""" warnings.warn( From d571e2db8bd14abe2a8f6dee51d223ec18abfcc1 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 12:15:49 +0300 Subject: [PATCH 20/30] Test _retrieve_job_shots() and _retrieve_child_job_shots() Job test methods. --- cirq-ionq/cirq_ionq/job_test.py | 713 ++++++++++++++++++++++---------- 1 file changed, 489 insertions(+), 224 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index 040d51e912e..fd6ed425b52 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -25,72 +25,72 @@ def test_job_fields(): job_dict = { - 'id': 'my_id', - 'backend': 'qpu', - 'name': 'bacon', - 'stats': {'qubits': '5'}, - 'status': 'completed', - 'metadata': {'shots': 1000, 'measurement0': f'a{chr(31)}0,1'}, + "id": "my_id", + "backend": "qpu", + "name": "bacon", + "stats": {"qubits": "5"}, + "status": "completed", + "metadata": {"shots": 1000, "measurement0": f"a{chr(31)}0,1"}, } job = ionq.Job(None, job_dict) - assert job.job_id() == 'my_id' - assert job.target() == 'qpu' - assert job.name() == 'bacon' + assert job.job_id() == "my_id" + assert job.target() == "qpu" + assert job.name() == "bacon" assert job.num_qubits() == 5 assert job.repetitions() == 1000 - assert job.measurement_dict() == {'a': [0, 1]} + assert job.measurement_dict() == {"a": [0, 1]} def test_job_fields_multiple_circuits(): job_dict = { - 'id': 'my_id', - 'backend': 'qpu', - 'name': 'bacon', - 'stats': {'qubits': '5'}, - 'status': 'completed', - 'metadata': { - 'shots': 1000, - 'measurements': json.dumps([{'measurement0': f'a{chr(31)}0,1'}]), + "id": "my_id", + "backend": "qpu", + "name": "bacon", + "stats": {"qubits": "5"}, + "status": "completed", + "metadata": { + "shots": 1000, + "measurements": json.dumps([{"measurement0": f"a{chr(31)}0,1"}]), }, } job = ionq.Job(None, job_dict) - assert job.job_id() == 'my_id' - assert job.target() == 'qpu' - assert job.name() == 'bacon' + assert job.job_id() == "my_id" + assert job.target() == "qpu" + assert job.name() == "bacon" assert job.num_qubits() == 5 assert job.repetitions() == 1000 - assert job.measurement_dict() == {'a': [0, 1]} + assert job.measurement_dict() == {"a": [0, 1]} def test_job_status_refresh(): for status in ionq.Job.NON_TERMINAL_STATES: mock_client = mock.MagicMock() - mock_client.get_job.return_value = {'id': 'my_id', 'status': 'completed'} - job = ionq.Job(mock_client, {'id': 'my_id', 'status': status}) - assert job.status() == 'completed' - mock_client.get_job.assert_called_with('my_id') + mock_client.get_job.return_value = {"id": "my_id", "status": "completed"} + job = ionq.Job(mock_client, {"id": "my_id", "status": status}) + assert job.status() == "completed" + mock_client.get_job.assert_called_with("my_id") for status in ionq.Job.TERMINAL_STATES: mock_client = mock.MagicMock() - job = ionq.Job(mock_client, {'id': 'my_id', 'status': status}) + job = ionq.Job(mock_client, {"id": "my_id", "status": status}) assert job.status() == status mock_client.get_job.assert_not_called() def test_job_str(): - job = ionq.Job(None, {'id': 'my_id'}) - assert str(job) == 'cirq_ionq.Job(job_id=my_id)' + job = ionq.Job(None, {"id": "my_id"}) + assert str(job) == "cirq_ionq.Job(job_id=my_id)" def test_job_results_qpu(): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '0.6', '2': '0.4'} + mock_client.get_results.return_value = {"0": "0.6", "2": "0.4"} job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '2'}, - 'backend': 'qpu', - 'metadata': {'shots': 1000, 'measurement0': f'a{chr(31)}0,1'}, - 'warning': {'messages': ['foo', 'bar']}, + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": {"shots": 1000, "measurement0": f"a{chr(31)}0,1"}, + "warning": {"messages": ["foo", "bar"]}, } job = ionq.Job(mock_client, job_dict) with warnings.catch_warnings(record=True) as w: @@ -98,29 +98,29 @@ def test_job_results_qpu(): assert len(w) == 2 assert "foo" in str(w[0].message) assert "bar" in str(w[1].message) - expected = ionq.QPUResult({0: 600, 1: 400}, 2, {'a': [0, 1]}) + expected = ionq.QPUResult({0: 600, 1: 400}, 2, {"a": [0, 1]}) assert results == expected def test_batch_job_results_qpu(): mock_client = mock.MagicMock() mock_client.get_results.return_value = { - '0190070f-9691-7000-a1f6-306623179a83': {'0': '0.6', '2': '0.4'}, - '0190070f-991c-7000-8700-c4b56b30715d': {'1': 1.0}, + "0190070f-9691-7000-a1f6-306623179a83": {"0": "0.6", "2": "0.4"}, + "0190070f-991c-7000-8700-c4b56b30715d": {"1": 1.0}, } job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '2'}, - 'backend': 'qpu', - 'metadata': { - 'shots': 1000, - 'measurements': json.dumps( - [{'measurement0': f'a{chr(31)}0,1'}, {'measurement0': f'a{chr(31)}0'}] + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": { + "shots": 1000, + "measurements": json.dumps( + [{"measurement0": f"a{chr(31)}0,1"}, {"measurement0": f"a{chr(31)}0"}] ), - 'qubit_numbers': json.dumps([2, 1]), + "qubit_numbers": json.dumps([2, 1]), }, - 'warning': {'messages': ['foo', 'bar']}, + "warning": {"messages": ["foo", "bar"]}, } job = ionq.Job(mock_client, job_dict) with warnings.catch_warnings(record=True) as w: @@ -128,54 +128,54 @@ def test_batch_job_results_qpu(): assert len(w) == 2 assert "foo" in str(w[0].message) assert "bar" in str(w[1].message) - expected_0 = ionq.QPUResult({0: 600, 1: 400}, 2, {'a': [0, 1]}) - expected_1 = ionq.QPUResult({1: 1000}, 1, {'a': [0]}) + expected_0 = ionq.QPUResult({0: 600, 1: 400}, 2, {"a": [0, 1]}) + expected_1 = ionq.QPUResult({1: 1000}, 1, {"a": [0]}) assert results[0] == expected_0 assert results[1] == expected_1 def test_job_results_rounding_qpu(): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '0.0006', '2': '0.9994'} + mock_client.get_results.return_value = {"0": "0.0006", "2": "0.9994"} job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '2'}, - 'backend': 'qpu', - 'metadata': {'shots': 5000, 'measurement0': f'a{chr(31)}0,1'}, + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": {"shots": 5000, "measurement0": f"a{chr(31)}0,1"}, } # 5000*0.0006 ~ 2.9999 but should be interpreted as 3 job = ionq.Job(mock_client, job_dict) - expected = ionq.QPUResult({0: 3, 1: 4997}, 2, {'a': [0, 1]}) + expected = ionq.QPUResult({0: 3, 1: 4997}, 2, {"a": [0, 1]}) results = job.results() assert results == expected def test_job_results_failed(): - job_dict = {'id': 'my_id', 'status': 'failed', 'failure': {'error': 'too many qubits'}} + job_dict = {"id": "my_id", "status": "failed", "failure": {"error": "too many qubits"}} job = ionq.Job(None, job_dict) - with pytest.raises(RuntimeError, match='too many qubits'): + with pytest.raises(RuntimeError, match="too many qubits"): _ = job.results() - assert job.status() == 'failed' + assert job.status() == "failed" def test_job_results_failed_no_error_message(): - job_dict = {'id': 'my_id', 'status': 'failed', 'failure': {}} + job_dict = {"id": "my_id", "status": "failed", "failure": {}} job = ionq.Job(None, job_dict) - with pytest.raises(RuntimeError, match='failed'): + with pytest.raises(RuntimeError, match="failed"): _ = job.results() - assert job.status() == 'failed' + assert job.status() == "failed" def test_job_results_qpu_endianness(): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} + mock_client.get_results.return_value = {"0": "0.6", "1": "0.4"} job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '2'}, - 'backend': 'qpu', - 'metadata': {'shots': 1000}, + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": {"shots": 1000}, } job = ionq.Job(mock_client, job_dict) results = job.results() @@ -185,34 +185,34 @@ def test_job_results_qpu_endianness(): def test_batch_job_results_qpu_endianness(): mock_client = mock.MagicMock() mock_client.get_results.return_value = { - '0190070f-9691-7000-a1f6-306623179a83': {'0': '0.6', '1': '0.4'} + "0190070f-9691-7000-a1f6-306623179a83": {"0": "0.6", "1": "0.4"} } job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '2'}, - 'backend': 'qpu', - 'metadata': { - 'shots': 1000, - 'measurements': json.dumps([{'measurement0': f'a{chr(31)}0,1'}]), - 'qubit_numbers': json.dumps([2]), + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": { + "shots": 1000, + "measurements": json.dumps([{"measurement0": f"a{chr(31)}0,1"}]), + "qubit_numbers": json.dumps([2]), }, } job = ionq.Job(mock_client, job_dict) results = job.results() - assert results == ionq.QPUResult({0: 600, 2: 400}, 2, measurement_dict={'a': [0, 1]}) + assert results == ionq.QPUResult({0: 600, 2: 400}, 2, measurement_dict={"a": [0, 1]}) def test_job_results_qpu_target_endianness(): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} + mock_client.get_results.return_value = {"0": "0.6", "1": "0.4"} job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '2'}, - 'backend': 'qpu.target', - 'metadata': {'shots': 1000}, - 'data': {'histogram': {'0': '0.6', '1': '0.4'}}, + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu.target", + "metadata": {"shots": 1000}, + "data": {"histogram": {"0": "0.6", "1": "0.4"}}, } job = ionq.Job(mock_client, job_dict) results = job.results() @@ -222,75 +222,75 @@ def test_job_results_qpu_target_endianness(): def test_batch_job_results_qpu_target_endianness(): mock_client = mock.MagicMock() mock_client.get_results.return_value = { - '0190070f-9691-7000-a1f6-306623179a83': {'0': '0.6', '1': '0.4'} + "0190070f-9691-7000-a1f6-306623179a83": {"0": "0.6", "1": "0.4"} } job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '2'}, - 'backend': 'qpu.target', - 'metadata': { - 'shots': 1000, - 'measurements': json.dumps([{'measurement0': f'a{chr(31)}0,1'}]), - 'qubit_numbers': json.dumps([2]), + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu.target", + "metadata": { + "shots": 1000, + "measurements": json.dumps([{"measurement0": f"a{chr(31)}0,1"}]), + "qubit_numbers": json.dumps([2]), }, - 'data': {'histogram': {'0': '0.6', '1': '0.4'}}, + "data": {"histogram": {"0": "0.6", "1": "0.4"}}, } job = ionq.Job(mock_client, job_dict) results = job.results() - assert results == ionq.QPUResult({0: 600, 2: 400}, 2, measurement_dict={'a': [0, 1]}) + assert results == ionq.QPUResult({0: 600, 2: 400}, 2, measurement_dict={"a": [0, 1]}) -@mock.patch('time.sleep', return_value=None) +@mock.patch("time.sleep", return_value=None) def test_job_results_poll(mock_sleep): - ready_job = {'id': 'my_id', 'status': 'ready'} + ready_job = {"id": "my_id", "status": "ready"} completed_job = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '1'}, - 'backend': 'qpu', - 'metadata': {'shots': 1000}, + "id": "my_id", + "status": "completed", + "stats": {"qubits": "1"}, + "backend": "qpu", + "metadata": {"shots": 1000}, } 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'} + 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={}) mock_sleep.assert_called_once() -@mock.patch('time.sleep', return_value=None) +@mock.patch("time.sleep", return_value=None) def test_job_results_poll_timeout(mock_sleep): - ready_job = {'id': 'my_id', 'status': 'ready'} + ready_job = {"id": "my_id", "status": "ready"} mock_client = mock.MagicMock() mock_client.get_job.return_value = ready_job job = ionq.Job(mock_client, ready_job) - with pytest.raises(TimeoutError, match='seconds'): + with pytest.raises(TimeoutError, match="seconds"): _ = job.results(timeout_seconds=1, polling_seconds=0.1) assert mock_sleep.call_count == 11 -@mock.patch('time.sleep', return_value=None) +@mock.patch("time.sleep", return_value=None) def test_job_results_poll_timeout_with_error_message(mock_sleep): - ready_job = {'id': 'my_id', 'status': 'failure', 'failure': {'error': 'too many qubits'}} + ready_job = {"id": "my_id", "status": "failure", "failure": {"error": "too many qubits"}} mock_client = mock.MagicMock() mock_client.get_job.return_value = ready_job job = ionq.Job(mock_client, ready_job) - with pytest.raises(RuntimeError, match='too many qubits'): + with pytest.raises(RuntimeError, match="too many qubits"): _ = job.results(timeout_seconds=1, polling_seconds=0.1) assert mock_sleep.call_count == 11 def test_job_results_simulator(): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} + mock_client.get_results.return_value = {"0": "0.6", "1": "0.4"} job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '1'}, - 'backend': 'simulator', - 'metadata': {'shots': '100'}, + "id": "my_id", + "status": "completed", + "stats": {"qubits": "1"}, + "backend": "simulator", + "metadata": {"shots": "100"}, } job = ionq.Job(mock_client, job_dict) results = job.results() @@ -300,39 +300,39 @@ def test_job_results_simulator(): def test_batch_job_results_simulator(): mock_client = mock.MagicMock() mock_client.get_results.return_value = { - '0190070f-9691-7000-a1f6-306623179a83': {'0': '0.6', '2': '0.4'}, - '0190070f-991c-7000-8700-c4b56b30715d': {'1': 1.0}, + "0190070f-9691-7000-a1f6-306623179a83": {"0": "0.6", "2": "0.4"}, + "0190070f-991c-7000-8700-c4b56b30715d": {"1": 1.0}, } job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '2'}, - 'backend': 'simulator', - 'metadata': { - 'shots': 1000, - 'measurements': json.dumps( - [{'measurement0': f'a{chr(31)}0,1'}, {'measurement0': f'a{chr(31)}0'}] + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "simulator", + "metadata": { + "shots": 1000, + "measurements": json.dumps( + [{"measurement0": f"a{chr(31)}0,1"}, {"measurement0": f"a{chr(31)}0"}] ), - 'qubit_numbers': json.dumps([2, 1]), + "qubit_numbers": json.dumps([2, 1]), }, } job = ionq.Job(mock_client, job_dict) results = job.results() - expected_0 = ionq.SimulatorResult({0: 0.6, 1: 0.4}, 2, {'a': [0, 1]}, repetitions=1000) - expected_1 = ionq.SimulatorResult({1: 1}, 1, {'a': [0]}, repetitions=1000) + expected_0 = ionq.SimulatorResult({0: 0.6, 1: 0.4}, 2, {"a": [0, 1]}, repetitions=1000) + expected_1 = ionq.SimulatorResult({1: 1}, 1, {"a": [0]}, repetitions=1000) assert results[0] == expected_0 assert results[1] == expected_1 def test_job_results_simulator_endianness(): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} + mock_client.get_results.return_value = {"0": "0.6", "1": "0.4"} job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '2'}, - 'backend': 'simulator', - 'metadata': {'shots': '100'}, + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "simulator", + "metadata": {"shots": "100"}, } job = ionq.Job(mock_client, job_dict) results = job.results() @@ -342,33 +342,33 @@ def test_job_results_simulator_endianness(): def test_batch_job_results_simulator_endianness(): mock_client = mock.MagicMock() mock_client.get_results.return_value = { - '0190070f-9691-7000-a1f6-306623179a83': {'0': '0.6', '1': '0.4'} + "0190070f-9691-7000-a1f6-306623179a83": {"0": "0.6", "1": "0.4"} } job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '2'}, - 'backend': 'simulator', - 'metadata': { - 'shots': 1000, - 'measurements': json.dumps([{'measurement0': f'a{chr(31)}0,1'}]), - 'qubit_numbers': json.dumps([2]), + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "simulator", + "metadata": { + "shots": 1000, + "measurements": json.dumps([{"measurement0": f"a{chr(31)}0,1"}]), + "qubit_numbers": json.dumps([2]), }, } job = ionq.Job(mock_client, job_dict) results = job.results() - assert results == ionq.SimulatorResult({0: 0.6, 2: 0.4}, 2, {'a': [0, 1]}, 1000) + assert results == ionq.SimulatorResult({0: 0.6, 2: 0.4}, 2, {"a": [0, 1]}, 1000) def test_job_sharpen_results(): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '60', '1': '40'} + mock_client.get_results.return_value = {"0": "60", "1": "40"} job_dict = { - 'id': 'my_id', - 'status': 'completed', - 'stats': {'qubits': '1'}, - 'backend': 'simulator', - 'metadata': {'shots': '100'}, + "id": "my_id", + "status": "completed", + "stats": {"qubits": "1"}, + "backend": "simulator", + "metadata": {"shots": "100"}, } job = ionq.Job(mock_client, job_dict) results = job.results(sharpen=False) @@ -376,103 +376,103 @@ def test_job_sharpen_results(): def test_job_cancel(): - ready_job = {'id': 'my_id', 'status': 'ready'} - canceled_job = {'id': 'my_id', 'status': 'canceled'} + ready_job = {"id": "my_id", "status": "ready"} + canceled_job = {"id": "my_id", "status": "canceled"} mock_client = mock.MagicMock() mock_client.cancel_job.return_value = canceled_job job = ionq.Job(mock_client, ready_job) job.cancel() - mock_client.cancel_job.assert_called_with(job_id='my_id') - assert job.status() == 'canceled' + mock_client.cancel_job.assert_called_with(job_id="my_id") + assert job.status() == "canceled" def test_job_delete(): - ready_job = {'id': 'my_id', 'status': 'ready'} - deleted_job = {'id': 'my_id', 'status': 'deleted'} + ready_job = {"id": "my_id", "status": "ready"} + deleted_job = {"id": "my_id", "status": "deleted"} mock_client = mock.MagicMock() mock_client.delete_job.return_value = deleted_job job = ionq.Job(mock_client, ready_job) job.delete() - mock_client.delete_job.assert_called_with(job_id='my_id') - assert job.status() == 'deleted' + mock_client.delete_job.assert_called_with(job_id="my_id") + assert job.status() == "deleted" def test_job_fields_unsuccessful(): job_dict = { - 'id': 'my_id', - 'backend': 'qpu', - 'name': 'bacon', - 'stats': {'qubits': '5'}, - 'status': 'deleted', - 'metadata': {'shots': 1000}, + "id": "my_id", + "backend": "qpu", + "name": "bacon", + "stats": {"qubits": "5"}, + "status": "deleted", + "metadata": {"shots": 1000}, } job = ionq.Job(None, job_dict) - with pytest.raises(ionq.IonQUnsuccessfulJobException, match='deleted'): + with pytest.raises(ionq.IonQUnsuccessfulJobException, match="deleted"): _ = job.target() - with pytest.raises(ionq.IonQUnsuccessfulJobException, match='deleted'): + with pytest.raises(ionq.IonQUnsuccessfulJobException, match="deleted"): _ = job.name() - with pytest.raises(ionq.IonQUnsuccessfulJobException, match='deleted'): + with pytest.raises(ionq.IonQUnsuccessfulJobException, match="deleted"): _ = job.num_qubits() - with pytest.raises(ionq.IonQUnsuccessfulJobException, match='deleted'): + with pytest.raises(ionq.IonQUnsuccessfulJobException, match="deleted"): _ = job.repetitions() def test_job_fields_cannot_get_status(): job_dict = { - 'id': 'my_id', - 'backend': 'qpu', - 'name': 'bacon', - 'stats': {'qubits': '5'}, - 'status': 'running', - 'metadata': {'shots': 1000}, + "id": "my_id", + "backend": "qpu", + "name": "bacon", + "stats": {"qubits": "5"}, + "status": "running", + "metadata": {"shots": 1000}, } mock_client = mock.MagicMock() - mock_client.get_job.side_effect = ionq.IonQException('bad') + mock_client.get_job.side_effect = ionq.IonQException("bad") job = ionq.Job(mock_client, job_dict) - with pytest.raises(ionq.IonQException, match='bad'): + with pytest.raises(ionq.IonQException, match="bad"): _ = job.target() - with pytest.raises(ionq.IonQException, match='bad'): + with pytest.raises(ionq.IonQException, match="bad"): _ = job.name() - with pytest.raises(ionq.IonQException, match='bad'): + with pytest.raises(ionq.IonQException, match="bad"): _ = job.num_qubits() - with pytest.raises(ionq.IonQException, match='bad'): + with pytest.raises(ionq.IonQException, match="bad"): _ = job.repetitions() def test_job_fields_update_status(): job_dict = { - 'id': 'my_id', - 'backend': 'qpu', - 'name': 'bacon', - 'stats': {'qubits': '5'}, - 'status': 'running', - 'metadata': {'shots': 1000}, + "id": "my_id", + "backend": "qpu", + "name": "bacon", + "stats": {"qubits": "5"}, + "status": "running", + "metadata": {"shots": 1000}, } mock_client = mock.MagicMock() mock_client.get_job.return_value = job_dict job = ionq.Job(mock_client, job_dict) - assert job.job_id() == 'my_id' - assert job.target() == 'qpu' - assert job.name() == 'bacon' + assert job.job_id() == "my_id" + assert job.target() == "qpu" + assert job.name() == "bacon" assert job.num_qubits() == 5 assert job.repetitions() == 1000 -@pytest.mark.parametrize('memory', [True, False]) +@pytest.mark.parametrize("memory", [True, False]) def test_memory_job_results_ideal_simulator(memory): mock_client = mock.MagicMock() mock_client.get_shots.return_value = [2, 1, 3, 1, 0] - mock_client.get_results.return_value = {'0': '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'}]), + "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': '/shots'}}, + "results": {"shots": {"url": "/shots"}}, "noise": {"model": "ideal"}, } job = ionq.Job(mock_client, job_dict, memory=memory) @@ -482,21 +482,21 @@ def test_memory_job_results_ideal_simulator(memory): mock_client.get_shots.assert_not_called() -@pytest.mark.parametrize('memory', [True, False]) +@pytest.mark.parametrize("memory", [True, False]) def test_memory_job_results_noisy_simulator(memory): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '0.6', '1': '0.4'} + 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'}]), + "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': '/shots'}}, + "results": {"shots": {"url": "/shots"}}, "noise": {"model": "aria-1"}, } job = ionq.Job(mock_client, job_dict, memory=memory) @@ -504,12 +504,12 @@ def test_memory_job_results_noisy_simulator(memory): if memory: cirq_result = result.to_cirq_result() expected = [[0, 1], [1, 0], [1, 1], [1, 0], [0, 0]] - mock_client.get_shots.assert_called_once_with('/shots') + mock_client.get_shots.assert_called_once_with("/shots") else: fake_random_state = mock.Mock() fake_random_state.choice.return_value = [1, 0, 1, 1, 0] with mock.patch( - 'cirq_ionq.results.cirq.value.parse_random_state', return_value=fake_random_state + "cirq_ionq.results.cirq.value.parse_random_state", return_value=fake_random_state ) as parse_random_state: cirq_result = result.to_cirq_result() @@ -521,21 +521,21 @@ def test_memory_job_results_noisy_simulator(memory): assert cirq_result.measurements["results"].tolist() == expected -@pytest.mark.parametrize('memory', [True, False]) +@pytest.mark.parametrize("memory", [True, False]) def test_memory_job_results_qpu(memory): mock_client = mock.MagicMock() - mock_client.get_results.return_value = {'0': '0.6', '3': '0.4'} + 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'}]), + "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': '/shots'}}, + "results": {"shots": {"url": "/shots"}}, } job = ionq.Job(mock_client, job_dict, memory=memory) result = job.results() @@ -547,6 +547,271 @@ def test_memory_job_results_qpu(memory): ) assert cirq_result.measurements["results"].tolist() == expected if memory: - mock_client.get_shots.assert_called_once_with('/shots') + mock_client.get_shots.assert_called_once_with("/shots") else: mock_client.get_shots.assert_not_called() + + +def test_retrieve_job_shots_key_error(): + """Test _retrieve_job_shots handles KeyError when shots url is missing.""" + mock_client = mock.MagicMock() + job_dict = { + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": {"shots": 5}, + "results": {}, # Missing 'shots' key + } + job = ionq.Job(mock_client, job_dict, memory=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = job._retrieve_job_shots() + + # Verify warning was raised + assert len(w) == 1 + assert "memory argument" in str(w[0].message) + assert "url for shots result was not found" in str(w[0].message) + + # Verify result is [None] + assert result == [None] + # Verify get_shots was not called + mock_client.get_shots.assert_not_called() + + +@pytest.mark.parametrize( + "exception,error_msg", + [ + (ionq.IonQException("API error"), "API error"), + (ionq.IonQNotFoundException("Job not found"), "Job not found"), + (TimeoutError("Request timed out"), "Request timed out"), + ], +) +def test_retrieve_job_shots_api_errors(exception, error_msg): + """Test _retrieve_job_shots handles various API errors.""" + mock_client = mock.MagicMock() + mock_client.get_shots.side_effect = exception + + job_dict = { + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": {"shots": 5}, + "results": {"shots": {"url": "/shots"}}, + } + job = ionq.Job(mock_client, job_dict, memory=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = job._retrieve_job_shots() + + # Verify warning was raised + assert len(w) == 1 + assert "memory argument" in str(w[0].message) + assert error_msg in str(w[0].message) + + # Verify result is [None] + assert result == [None] + # Verify get_shots was called + mock_client.get_shots.assert_called_once_with("/shots") + + +def test_retrieve_job_shots_success(): + """Test _retrieve_job_shots successfully retrieves shots.""" + mock_client = mock.MagicMock() + mock_shots = [2, 1, 3, 1, 0] + mock_client.get_shots.return_value = mock_shots + + job_dict = { + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": {"shots": 5}, + "results": {"shots": {"url": "/shots"}}, + } + job = ionq.Job(mock_client, job_dict, memory=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = job._retrieve_job_shots() + + # Verify no warning was raised + assert len(w) == 0 + + # Verify result contains shots + assert result == [mock_shots] + # Verify get_shots was called + mock_client.get_shots.assert_called_once_with("/shots") + + +def test_retrieve_child_job_shots_key_error(): + """Test _retrieve_child_job_shots handles KeyError when shots url is missing.""" + mock_client = mock.MagicMock() + # First child job has missing shots URL + mock_client.get_job.return_value = { + "id": "child_1", + "status": "completed", + "results": {}, # Missing 'shots' key + } + + job_dict = { + "id": "batch_job", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": {"shots": 5}, + } + job = ionq.Job(mock_client, job_dict, memory=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = job._retrieve_child_job_shots(["child_1"]) + + # Verify warning was raised + assert len(w) == 1 + assert "memory argument" in str(w[0].message) + assert "url for shots result was not found" in str(w[0].message) + assert "child_1" in str(w[0].message) + + # Verify result is [None] for failed child + assert result == [None] + # Verify get_shots was not called + mock_client.get_shots.assert_not_called() + + +@pytest.mark.parametrize( + "exception,error_msg", + [ + (ionq.IonQException("API error"), "API error"), + (ionq.IonQNotFoundException("Job not found"), "Job not found"), + (TimeoutError("Request timed out"), "Request timed out"), + ], +) +def test_retrieve_child_job_shots_api_errors(exception, error_msg): + """Test _retrieve_child_job_shots handles various API errors for child jobs.""" + mock_client = mock.MagicMock() + mock_client.get_job.return_value = { + "id": "child_1", + "status": "completed", + "results": {"shots": {"url": "/shots"}}, + } + mock_client.get_shots.side_effect = exception + + job_dict = { + "id": "batch_job", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": {"shots": 5}, + } + job = ionq.Job(mock_client, job_dict, memory=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = job._retrieve_child_job_shots(["child_1"]) + + # Verify warning was raised + assert len(w) == 1 + assert "memory argument" in str(w[0].message) + assert error_msg in str(w[0].message) + assert "child_1" in str(w[0].message) + + # Verify result is [None] for failed child + assert result == [None] + # Verify get_shots was called + mock_client.get_shots.assert_called_once_with("/shots") + + +def test_retrieve_child_job_shots_success(): + """Test _retrieve_child_job_shots successfully retrieves shots for multiple child jobs.""" + mock_client = mock.MagicMock() + mock_shots_1 = [2, 1, 3, 1, 0] + mock_shots_2 = [0, 1, 0, 1, 1] + + def get_job_side_effect(job_id): + if job_id == "child_1": + return { + "id": "child_1", + "status": "completed", + "results": {"shots": {"url": "/shots/child_1"}}, + } + else: + return { + "id": "child_2", + "status": "completed", + "results": {"shots": {"url": "/shots/child_2"}}, + } + + mock_client.get_job.side_effect = get_job_side_effect + mock_client.get_shots.side_effect = [mock_shots_1, mock_shots_2] + + job_dict = { + "id": "batch_job", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": {"shots": 5}, + } + job = ionq.Job(mock_client, job_dict, memory=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = job._retrieve_child_job_shots(["child_1", "child_2"]) + + # Verify no warning was raised + assert len(w) == 0 + + # Verify results contain shots for both children + assert result == [mock_shots_1, mock_shots_2] + # Verify get_shots was called twice + assert mock_client.get_shots.call_count == 2 + + +def test_retrieve_child_job_shots_partial_failure(): + """Test _retrieve_child_job_shots handles partial failures across child jobs.""" + mock_client = mock.MagicMock() + mock_shots_1 = [2, 1, 3, 1, 0] + + def get_job_side_effect(job_id): + if job_id == "child_1": + return { + "id": "child_1", + "status": "completed", + "results": {"shots": {"url": "/shots/child_1"}}, + } + else: + # child_2 is missing shots URL + return { + "id": "child_2", + "status": "completed", + "results": {}, + } + + mock_client.get_job.side_effect = get_job_side_effect + mock_client.get_shots.return_value = mock_shots_1 + + job_dict = { + "id": "batch_job", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": "qpu", + "metadata": {"shots": 5}, + } + job = ionq.Job(mock_client, job_dict, memory=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = job._retrieve_child_job_shots(["child_1", "child_2"]) + + # Verify one warning was raised for child_2 + assert len(w) == 1 + assert "memory argument" in str(w[0].message) + assert "child_2" in str(w[0].message) + + # Verify results: child_1 succeeds, child_2 fails + assert result == [mock_shots_1, None] + # Verify get_shots was called once (for child_1 only) + mock_client.get_shots.assert_called_once_with("/shots/child_1") From 9c7f30630fb02307734a53b3f0b5e7042881a2d7 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 13:52:52 +0300 Subject: [PATCH 21/30] Extend test coverage. --- cirq-ionq/cirq_ionq/job_test.py | 123 ++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index fd6ed425b52..d599013885c 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -521,6 +521,87 @@ def test_memory_job_results_noisy_simulator(memory): assert cirq_result.measurements["results"].tolist() == expected +@pytest.mark.parametrize("backend", ["simulator", "qpu"]) +@pytest.mark.parametrize("child_job_ids", [None, ["child_1"]]) +def test_memory_batch_job_results_no_child_job_ids_falls_back(backend, child_job_ids): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = { + "0190070f-9691-7000-a1f6-306623179a83": {"0": "0.6", "1": "0.4"}, + "0190070f-991c-7000-8700-c4b56b30715d": {"0": "1.0"}, + } + job_dict = { + "id": "my_id", + "status": "completed", + "stats": {"qubits": "2"}, + "backend": backend, + "metadata": { + "shots": "5", + "measurements": json.dumps( + [{"measurement0": f"results{chr(31)}0,1"}, {"measurement0": f"results{chr(31)}0"}] + ), + "qubit_numbers": json.dumps([2, 1]), + }, + "noise": {"model": "aria-1"}, + } + if child_job_ids is not None: + job_dict["child_job_ids"] = child_job_ids + + job = ionq.Job(mock_client, job_dict, memory=True) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = job.results() + + assert len(w) == 1 + assert "correct child job ids to retrieve per shot results for a batch job" in str( + w[0].message + ) + assert len(result) == 2 + assert result[0]._memory_results is None + assert result[1]._memory_results is None + mock_client.get_job.assert_not_called() + mock_client.get_shots.assert_not_called() + + +def test_memory_batch_job_results_noisy_simulator(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = { + "0190070f-9691-7000-a1f6-306623179a83": {"0": "0.6", "1": "0.4"}, + "0190070f-991c-7000-8700-c4b56b30715d": {"0": "1.0"}, + } + mock_client.get_job.side_effect = [ + {"id": "child_1", "status": "completed", "results": {"shots": {"url": "/shots/1"}}}, + {"id": "child_2", "status": "completed", "results": {"shots": {"url": "/shots/2"}}}, + ] + mock_client.get_shots.side_effect = [[2, 1, 3, 1, 0], [0, 0, 1, 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"}, {"measurement0": f"results{chr(31)}0"}] + ), + "qubit_numbers": json.dumps([2, 1]), + }, + "noise": {"model": "aria-1"}, + "child_job_ids": ["child_1", "child_2"], + } + job = ionq.Job(mock_client, job_dict, memory=True) + result = job.results() + + assert len(result) == 2 + assert result[0]._memory_results == [2, 1, 3, 1, 0] + assert result[1]._memory_results == [0, 0, 1, 1, 0] + mock_client.get_job.assert_any_call("child_1") + mock_client.get_job.assert_any_call("child_2") + mock_client.get_shots.assert_any_call("/shots/1") + mock_client.get_shots.assert_any_call("/shots/2") + assert mock_client.get_shots.call_count == 2 + + @pytest.mark.parametrize("memory", [True, False]) def test_memory_job_results_qpu(memory): mock_client = mock.MagicMock() @@ -552,6 +633,48 @@ def test_memory_job_results_qpu(memory): mock_client.get_shots.assert_not_called() +def test_memory_batch_job_results_qpu(): + mock_client = mock.MagicMock() + mock_client.get_results.return_value = { + "0190070f-9691-7000-a1f6-306623179a83": {"0": "0.6", "3": "0.4"}, + "0190070f-991c-7000-8700-c4b56b30715d": {"1": "1.0"}, + } + mock_client.get_job.side_effect = [ + {"id": "child_1", "status": "completed", "results": {"shots": {"url": "/shots/1"}}}, + {"id": "child_2", "status": "completed", "results": {"shots": {"url": "/shots/2"}}}, + ] + mock_client.get_shots.side_effect = [[2, 1, 3, 1, 0], [0, 0, 1, 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"}, {"measurement0": f"results{chr(31)}0"}] + ), + "qubit_numbers": json.dumps([2, 1]), + }, + "results": {"shots": {"url": "/shots"}}, + "child_job_ids": ["child_1", "child_2"], + } + job = ionq.Job(mock_client, job_dict, memory=True) + + result = job.results() + + assert len(result) == 2 + assert result[0].counts() == {0: 3, 3: 2} + assert result[1].counts() == {1: 5} + assert result[0]._memory_results == [2, 1, 3, 1, 0] + assert result[1]._memory_results == [0, 0, 1, 1, 0] + mock_client.get_job.assert_any_call("child_1") + mock_client.get_job.assert_any_call("child_2") + mock_client.get_shots.assert_any_call("/shots/1") + mock_client.get_shots.assert_any_call("/shots/2") + assert mock_client.get_shots.call_count == 2 + + def test_retrieve_job_shots_key_error(): """Test _retrieve_job_shots handles KeyError when shots url is missing.""" mock_client = mock.MagicMock() From 8e7c2fc418ae217ef28a2dfb4c25cb66aee7c635 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 13:55:04 +0300 Subject: [PATCH 22/30] Fix code formatting. --- cirq-ionq/cirq_ionq/job_test.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job_test.py b/cirq-ionq/cirq_ionq/job_test.py index d599013885c..384aac91d54 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -553,9 +553,7 @@ def test_memory_batch_job_results_no_child_job_ids_falls_back(backend, child_job result = job.results() assert len(w) == 1 - assert "correct child job ids to retrieve per shot results for a batch job" in str( - w[0].message - ) + assert "correct child job ids to retrieve per shot results for a batch job" in str(w[0].message) assert len(result) == 2 assert result[0]._memory_results is None assert result[1]._memory_results is None @@ -907,11 +905,7 @@ def get_job_side_effect(job_id): } else: # child_2 is missing shots URL - return { - "id": "child_2", - "status": "completed", - "results": {}, - } + return {"id": "child_2", "status": "completed", "results": {}} mock_client.get_job.side_effect = get_job_side_effect mock_client.get_shots.return_value = mock_shots_1 From f1cb9391872ea0e7fa9d4b92c98920dabf238c44 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 16:47:49 +0300 Subject: [PATCH 23/30] Correctly apply override_repetitions to_cirq_result() argument with memory results on simulator. --- cirq-ionq/cirq_ionq/results.py | 3 ++- cirq-ionq/cirq_ionq/results_test.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index a0af1884bce..f1fea4517f4 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -290,10 +290,11 @@ def to_cirq_result( measurements = {} if self._memory_results is not None: + shots = self._memory_results[:override_repetitions] if override_repetitions else self._memory_results for key, targets in self.measurement_dict().items(): bits = [ _memory_bits_for_targets(int(value), self.num_qubits(), targets) - for value in self._memory_results + for value in shots ] measurements[key] = np.array(bits) else: diff --git a/cirq-ionq/cirq_ionq/results_test.py b/cirq-ionq/cirq_ionq/results_test.py index 5d0202788ba..2f4170cb829 100644 --- a/cirq-ionq/cirq_ionq/results_test.py +++ b/cirq-ionq/cirq_ionq/results_test.py @@ -321,6 +321,22 @@ def test_simulator_result_to_cirq_result(): # list instead of a numpy multidimensional array. Check this here. assert type(result.to_cirq_result().measurements['x']) == np.ndarray + # when memory_results is provided, actual shot data is used instead of sampling. + result = ionq.SimulatorResult( + {0b00: 0.5, 0b11: 0.5}, + num_qubits=2, + measurement_dict={'x': [0, 1]}, + repetitions=3, + memory_results=['2', '0', '2'], + ) + assert result.to_cirq_result() == cirq.ResultDict( + params=cirq.ParamResolver({}), measurements={'x': np.array([[0, 1], [0, 0], [0, 1]])} + ) + # override_repetitions slices the memory results when memory is present. + assert result.to_cirq_result(override_repetitions=2) == cirq.ResultDict( + params=cirq.ParamResolver({}), measurements={'x': np.array([[0, 1], [0, 0]])} + ) + def test_simulator_result_to_cirq_result_multiple_keys(): result = ionq.SimulatorResult( From 26920307eae84c1153a1272f474a5ec709c9c411 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 16:59:17 +0300 Subject: [PATCH 24/30] Comment: ignore memory_results in eq tests. --- cirq-ionq/cirq_ionq/results.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index f1fea4517f4..61e66109f80 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -167,6 +167,7 @@ def __eq__(self, other): if not isinstance(other, QPUResult): return NotImplemented return ( + # ignoring self._memory_results self._counts == other._counts and self._num_qubits == other._num_qubits and self._measurement_dict == other._measurement_dict @@ -322,6 +323,7 @@ def __eq__(self, other): if not isinstance(other, SimulatorResult): return NotImplemented return ( + # ignoring self._memory_results self._probabilities == other._probabilities and self._num_qubits == other._num_qubits and self._measurement_dict == other._measurement_dict From dba4d69019e8f692245ad8139c7a246c8a9b8001 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 17:03:01 +0300 Subject: [PATCH 25/30] Format check. --- cirq-ionq/cirq_ionq/results.py | 32 ++-- cirq-ionq/cirq_ionq/results_test.py | 218 ++++++++++++++-------------- 2 files changed, 127 insertions(+), 123 deletions(-) diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index 61e66109f80..048685ed86f 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -74,8 +74,8 @@ def ordered_results(self, key: str | None = None) -> list[int]: if key is not None and not key in self._measurement_dict: raise ValueError( - f'Measurement key {key} is not a key for a measurement gate in the' - 'circuit that produced these results.' + f"Measurement key {key} is not a key for a measurement gate in the" + "circuit that produced these results." ) targets = self._measurement_dict[key] if key is not None else range(self.num_qubits()) result: list[int] = [] @@ -109,8 +109,8 @@ def counts(self, key: str | None = None) -> Counter[int]: return collections.Counter(self._counts) if not key in self._measurement_dict: raise ValueError( - f'Measurement key {key} is not a key for a measurement gate in the' - 'circuit that produced these results.' + f"Measurement key {key} is not a key for a measurement gate in the" + "circuit that produced these results." ) result: Counter[int] = collections.Counter() result.update(self.ordered_results(key)) @@ -142,8 +142,8 @@ def to_cirq_result(self, params: cirq.ParamResolver | None = None) -> cirq.Resul """ if len(self.measurement_dict()) == 0: raise ValueError( - 'Can convert to cirq results only if the circuit had measurement gates ' - 'with measurement keys.' + "Can convert to cirq results only if the circuit had measurement gates " + "with measurement keys." ) measurements = {} @@ -235,8 +235,8 @@ def probabilities(self, key: str | None = None) -> dict[int, float]: return self._probabilities if not key in self._measurement_dict: raise ValueError( - f'Measurement key {key} is not a key for a measurement gate in the' - 'circuit that produced these results.' + f"Measurement key {key} is not a key for a measurement gate in the" + "circuit that produced these results." ) targets = self._measurement_dict[key] result: dict[int, float] = {} @@ -285,13 +285,17 @@ def to_cirq_result( """ if len(self.measurement_dict()) == 0: raise ValueError( - 'Can convert to cirq results only if the circuit had measurement gates ' - 'with measurement keys.' + "Can convert to cirq results only if the circuit had measurement gates " + "with measurement keys." ) measurements = {} if self._memory_results is not None: - shots = self._memory_results[:override_repetitions] if override_repetitions else self._memory_results + shots = ( + self._memory_results[:override_repetitions] + if override_repetitions + else self._memory_results + ) for key, targets in self.measurement_dict().items(): bits = [ _memory_bits_for_targets(int(value), self.num_qubits(), targets) @@ -338,6 +342,6 @@ def _pretty_str_dict(value: dict, bit_count: int) -> str: """Pretty prints a dict, converting int dict values to bit strings.""" strs = [] for k, v in value.items(): - bits = ''.join(str(b) for b in cirq.big_endian_int_to_bits(k, bit_count=bit_count)) - strs.append(f'{bits}: {v}') - return '\n'.join(strs) + bits = "".join(str(b) for b in cirq.big_endian_int_to_bits(k, bit_count=bit_count)) + strs.append(f"{bits}: {v}") + return "\n".join(strs) diff --git a/cirq-ionq/cirq_ionq/results_test.py b/cirq-ionq/cirq_ionq/results_test.py index 2f4170cb829..3b6de839466 100644 --- a/cirq-ionq/cirq_ionq/results_test.py +++ b/cirq-ionq/cirq_ionq/results_test.py @@ -23,318 +23,318 @@ def test_qpu_result_fields(): result = ionq.QPUResult( - {0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [0]}, memory_results=[1, 2, 3] + {0: 10, 1: 10}, num_qubits=1, measurement_dict={"a": [0]}, memory_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.measurement_dict() == {"a": [0]} assert result._memory_results == [1, 2, 3] def test_qpu_result_str(): result = ionq.QPUResult({0: 10, 1: 10}, num_qubits=2, measurement_dict={}) - assert str(result) == '00: 10\n01: 10' + assert str(result) == "00: 10\n01: 10" def test_qpu_result_eq(): equals_tester = cirq.testing.EqualsTester() equals_tester.add_equality_group( - ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [0]}), - ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [0]}), + ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={"a": [0]}), + ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={"a": [0]}), ) equals_tester.add_equality_group( - ionq.QPUResult({0: 10, 1: 20}, num_qubits=1, measurement_dict={'a': [0]}) + ionq.QPUResult({0: 10, 1: 20}, num_qubits=1, measurement_dict={"a": [0]}) ) equals_tester.add_equality_group( - ionq.QPUResult({0: 15, 1: 15}, num_qubits=1, measurement_dict={'a': [0]}) + ionq.QPUResult({0: 15, 1: 15}, num_qubits=1, measurement_dict={"a": [0]}) ) equals_tester.add_equality_group( - ionq.QPUResult({0: 10, 1: 10}, num_qubits=2, measurement_dict={'a': [0]}) + ionq.QPUResult({0: 10, 1: 10}, num_qubits=2, measurement_dict={"a": [0]}) ) equals_tester.add_equality_group( - ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={'b': [0]}) + ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={"b": [0]}) ) equals_tester.add_equality_group( - ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [1]}) + ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={"a": [1]}) ) equals_tester.add_equality_group( - ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={'a': [0, 1]}) + ionq.QPUResult({0: 10, 1: 10}, num_qubits=1, measurement_dict={"a": [0, 1]}) ) def test_qpu_result_measurement_key(): - result = ionq.QPUResult({0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={'a': [0]}) + result = ionq.QPUResult({0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={"a": [0]}) assert result.counts() == {0b00: 10, 0b01: 20} - result = ionq.QPUResult({0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={'a': [0]}) - assert result.counts('a') == {0b0: 30} - result = ionq.QPUResult({0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={'a': [1]}) - assert result.counts('a') == {0b0: 10, 0b1: 20} - result = ionq.QPUResult({0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={'a': [0, 1]}) - assert result.counts('a') == {0b00: 10, 0b01: 20} - result = ionq.QPUResult({0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={'a': [1, 0]}) - assert result.counts('a') == {0b00: 10, 0b10: 20} - result = ionq.QPUResult({0b000: 10, 0b111: 20}, num_qubits=3, measurement_dict={'a': [2]}) - assert result.counts('a') == {0b0: 10, 0b1: 20} - result = ionq.QPUResult({0b000: 10, 0b100: 20}, num_qubits=3, measurement_dict={'a': [1, 2]}) - assert result.counts('a') == {0b00: 30} + result = ionq.QPUResult({0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={"a": [0]}) + assert result.counts("a") == {0b0: 30} + result = ionq.QPUResult({0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={"a": [1]}) + assert result.counts("a") == {0b0: 10, 0b1: 20} + result = ionq.QPUResult({0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={"a": [0, 1]}) + assert result.counts("a") == {0b00: 10, 0b01: 20} + result = ionq.QPUResult({0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={"a": [1, 0]}) + assert result.counts("a") == {0b00: 10, 0b10: 20} + result = ionq.QPUResult({0b000: 10, 0b111: 20}, num_qubits=3, measurement_dict={"a": [2]}) + assert result.counts("a") == {0b0: 10, 0b1: 20} + result = ionq.QPUResult({0b000: 10, 0b100: 20}, num_qubits=3, measurement_dict={"a": [1, 2]}) + assert result.counts("a") == {0b00: 30} def test_qpu_result_measurement_multiple_key(): result = ionq.QPUResult( - {0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={'a': [0], 'b': [1]} + {0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={"a": [0], "b": [1]} ) - assert result.counts('a') == {0b0: 30} - assert result.counts('b') == {0b0: 10, 0b1: 20} + assert result.counts("a") == {0b0: 30} + assert result.counts("b") == {0b0: 10, 0b1: 20} result = ionq.QPUResult( - {0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={'a': [1], 'b': [0]} + {0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={"a": [1], "b": [0]} ) - assert result.counts('a') == {0b0: 10, 0b1: 20} - assert result.counts('b') == {0b0: 30} + assert result.counts("a") == {0b0: 10, 0b1: 20} + assert result.counts("b") == {0b0: 30} def test_qpu_result_bad_measurement_key(): result = ionq.QPUResult( - {0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={'a': [0], 'b': [1]} + {0b00: 10, 0b01: 20}, num_qubits=2, measurement_dict={"a": [0], "b": [1]} ) - with pytest.raises(ValueError, match='bad'): - result.counts('bad') + with pytest.raises(ValueError, match="bad"): + result.counts("bad") def test_qpu_result_to_cirq_result(): - result = ionq.QPUResult({0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={'x': [0, 1]}) + result = ionq.QPUResult({0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={"x": [0, 1]}) assert result.to_cirq_result() == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'x': np.array([[0, 0], [0, 1], [0, 1]])} + params=cirq.ParamResolver({}), measurements={"x": np.array([[0, 0], [0, 1], [0, 1]])} ) - params = cirq.ParamResolver({'a': 0.1}) + params = cirq.ParamResolver({"a": 0.1}) assert result.to_cirq_result(params) == cirq.ResultDict( - params=params, measurements={'x': np.array([[0, 0], [0, 1], [0, 1]])} + params=params, measurements={"x": np.array([[0, 0], [0, 1], [0, 1]])} ) - result = ionq.QPUResult({0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={'x': [0]}) + result = ionq.QPUResult({0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={"x": [0]}) assert result.to_cirq_result() == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'x': np.array([[0], [0], [0]])} + params=cirq.ParamResolver({}), measurements={"x": np.array([[0], [0], [0]])} ) - result = ionq.QPUResult({0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={'x': [1]}) + result = ionq.QPUResult({0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={"x": [1]}) assert result.to_cirq_result() == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'x': np.array([[0], [1], [1]])} + params=cirq.ParamResolver({}), measurements={"x": np.array([[0], [1], [1]])} ) # cirq.Result only compares pandas data frame, so possible to have supplied an list of # list instead of a numpy multidimensional array. Check this here. - assert type(result.to_cirq_result().measurements['x']) == np.ndarray + assert type(result.to_cirq_result().measurements["x"]) == np.ndarray # Results bitstreams need to be consistent betwween measurement keys # Ordering is by bitvector, so 0b01 0b01 0b10 should be the ordering for all measurement dicts. result = ionq.QPUResult( - {0b10: 1, 0b01: 2}, num_qubits=2, measurement_dict={'x': [0, 1], 'y': [0], 'z': [1]} + {0b10: 1, 0b01: 2}, num_qubits=2, measurement_dict={"x": [0, 1], "y": [0], "z": [1]} ) assert result.to_cirq_result() == cirq.ResultDict( params=cirq.ParamResolver({}), measurements={ - 'x': np.array([[0, 1], [0, 1], [1, 0]]), - 'y': np.array([[0], [0], [1]]), - 'z': np.array([[1], [1], [0]]), + "x": np.array([[0, 1], [0, 1], [1, 0]]), + "y": np.array([[0], [0], [1]]), + "z": np.array([[1], [1], [0]]), }, ) def test_qpu_result_to_cirq_result_multiple_keys(): result = ionq.QPUResult( - {0b000: 2, 0b101: 3}, num_qubits=3, measurement_dict={'x': [1], 'y': [2, 0]} + {0b000: 2, 0b101: 3}, num_qubits=3, measurement_dict={"x": [1], "y": [2, 0]} ) assert result.to_cirq_result() == cirq.ResultDict( params=cirq.ParamResolver({}), measurements={ - 'x': np.array([[0], [0], [0], [0], [0]]), - 'y': np.array([[0, 0], [0, 0], [1, 1], [1, 1], [1, 1]]), + "x": np.array([[0], [0], [0], [0], [0]]), + "y": np.array([[0, 0], [0, 0], [1, 1], [1, 1], [1, 1]]), }, ) def test_qpu_result_to_cirq_result_shotwise_uses_target_indices(): result = ionq.QPUResult( - {0b10: 1}, num_qubits=2, measurement_dict={'b': [1]}, memory_results=[0b10] + {0b10: 1}, num_qubits=2, measurement_dict={"b": [1]}, memory_results=[0b10] ) assert result.to_cirq_result() == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'b': np.array([[1]])} + params=cirq.ParamResolver({}), measurements={"b": np.array([[1]])} ) def test_qpu_result_to_cirq_result_no_keys(): result = ionq.QPUResult({0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={}) - with pytest.raises(ValueError, match='cirq results'): + with pytest.raises(ValueError, match="cirq results"): _ = result.to_cirq_result() def test_ordered_results_invalid_key(): - result = ionq.QPUResult({0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={'x': [1]}) - with pytest.raises(ValueError, match='is not a key for'): - _ = result.ordered_results('y') + result = ionq.QPUResult({0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={"x": [1]}) + with pytest.raises(ValueError, match="is not a key for"): + _ = result.ordered_results("y") def test_simulator_result_fields(): result = ionq.SimulatorResult( {0: 0.4, 1: 0.6}, num_qubits=1, - measurement_dict={'a': [0]}, + measurement_dict={"a": [0]}, repetitions=100, memory_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.measurement_dict() == {"a": [0]} assert result.repetitions() == 100 assert result._memory_results == [1, 2, 3] def test_simulator_result_str(): result = ionq.SimulatorResult( - {0: 0.4, 1: 0.6}, num_qubits=2, measurement_dict={'a': [0]}, repetitions=100 + {0: 0.4, 1: 0.6}, num_qubits=2, measurement_dict={"a": [0]}, repetitions=100 ) - assert str(result) == '00: 0.4\n01: 0.6' + assert str(result) == "00: 0.4\n01: 0.6" def test_simulator_result_eq(): equals_tester = cirq.testing.EqualsTester() equals_tester.add_equality_group( ionq.SimulatorResult( - {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={'a': [0]}, repetitions=100 + {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={"a": [0]}, repetitions=100 ), ionq.SimulatorResult( - {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={'a': [0]}, repetitions=100 + {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={"a": [0]}, repetitions=100 ), ) equals_tester.add_equality_group( 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 ) ) equals_tester.add_equality_group( ionq.SimulatorResult( - {0: 0.5, 1: 0.5}, num_qubits=2, measurement_dict={'a': [0]}, repetitions=100 + {0: 0.5, 1: 0.5}, num_qubits=2, measurement_dict={"a": [0]}, repetitions=100 ) ) equals_tester.add_equality_group( ionq.SimulatorResult( - {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={'b': [0]}, repetitions=100 + {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={"b": [0]}, repetitions=100 ) ) equals_tester.add_equality_group( ionq.SimulatorResult( - {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={'a': [1]}, repetitions=100 + {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={"a": [1]}, repetitions=100 ) ) equals_tester.add_equality_group( ionq.SimulatorResult( - {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={'a': [0, 1]}, repetitions=100 + {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={"a": [0, 1]}, repetitions=100 ) ) equals_tester.add_equality_group( ionq.SimulatorResult( - {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={'a': [0, 1]}, repetitions=10 + {0: 0.5, 1: 0.5}, num_qubits=1, measurement_dict={"a": [0, 1]}, repetitions=10 ) ) def test_simulator_result_measurement_key(): result = ionq.SimulatorResult( - {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={'a': [0]}, repetitions=100 + {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={"a": [0]}, repetitions=100 ) assert result.probabilities() == {0b00: 0.2, 0b01: 0.8} result = ionq.SimulatorResult( - {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={'a': [0]}, repetitions=100 + {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={"a": [0]}, repetitions=100 ) - assert result.probabilities('a') == {0b0: 1.0} + assert result.probabilities("a") == {0b0: 1.0} result = ionq.SimulatorResult( - {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={'a': [1]}, repetitions=100 + {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={"a": [1]}, repetitions=100 ) - assert result.probabilities('a') == {0b0: 0.2, 0b1: 0.8} + assert result.probabilities("a") == {0b0: 0.2, 0b1: 0.8} result = ionq.SimulatorResult( - {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={'a': [0, 1]}, repetitions=100 + {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={"a": [0, 1]}, repetitions=100 ) - assert result.probabilities('a') == {0b00: 0.2, 0b01: 0.8} + assert result.probabilities("a") == {0b00: 0.2, 0b01: 0.8} result = ionq.SimulatorResult( - {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={'a': [1, 0]}, repetitions=100 + {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={"a": [1, 0]}, repetitions=100 ) - assert result.probabilities('a') == {0b00: 0.2, 0b10: 0.8} + assert result.probabilities("a") == {0b00: 0.2, 0b10: 0.8} result = ionq.SimulatorResult( - {0b000: 0.2, 0b111: 0.8}, num_qubits=3, measurement_dict={'a': [2]}, repetitions=100 + {0b000: 0.2, 0b111: 0.8}, num_qubits=3, measurement_dict={"a": [2]}, repetitions=100 ) - assert result.probabilities('a') == {0b0: 0.2, 0b1: 0.8} + assert result.probabilities("a") == {0b0: 0.2, 0b1: 0.8} result = ionq.SimulatorResult( - {0b000: 0.2, 0b100: 0.8}, num_qubits=3, measurement_dict={'a': [1, 2]}, repetitions=100 + {0b000: 0.2, 0b100: 0.8}, num_qubits=3, measurement_dict={"a": [1, 2]}, repetitions=100 ) - assert result.probabilities('a') == {0b00: 1.0} + assert result.probabilities("a") == {0b00: 1.0} def test_simulator_result_measurement_multiple_key(): result = ionq.SimulatorResult( - {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={'a': [0], 'b': [1]}, repetitions=100 + {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={"a": [0], "b": [1]}, repetitions=100 ) - assert result.probabilities('a') == {0b0: 1.0} - assert result.probabilities('b') == {0b0: 0.2, 0b1: 0.8} + assert result.probabilities("a") == {0b0: 1.0} + assert result.probabilities("b") == {0b0: 0.2, 0b1: 0.8} result = ionq.SimulatorResult( - {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={'a': [1], 'b': [0]}, repetitions=100 + {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={"a": [1], "b": [0]}, repetitions=100 ) - assert result.probabilities('a') == {0b0: 0.2, 0b1: 0.8} - assert result.probabilities('b') == {0b0: 1.0} + assert result.probabilities("a") == {0b0: 0.2, 0b1: 0.8} + assert result.probabilities("b") == {0b0: 1.0} def test_simulator_result_bad_measurement_key(): result = ionq.SimulatorResult( - {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={'a': [0], 'b': [1]}, repetitions=100 + {0b00: 0.2, 0b01: 0.8}, num_qubits=2, measurement_dict={"a": [0], "b": [1]}, repetitions=100 ) - with pytest.raises(ValueError, match='bad'): - result.probabilities('bad') + with pytest.raises(ValueError, match="bad"): + result.probabilities("bad") def test_simulator_result_to_cirq_result(): result = ionq.SimulatorResult( - {0b00: 0.25, 0b01: 0.75}, num_qubits=2, measurement_dict={'x': [0, 1]}, repetitions=3 + {0b00: 0.25, 0b01: 0.75}, num_qubits=2, measurement_dict={"x": [0, 1]}, repetitions=3 ) assert result.to_cirq_result(seed=2) == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'x': np.array([[0, 1], [0, 0], [0, 1]])} + params=cirq.ParamResolver({}), measurements={"x": np.array([[0, 1], [0, 0], [0, 1]])} ) assert result.to_cirq_result(seed=3) == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'x': np.array([[0, 1], [0, 1], [0, 1]])} + params=cirq.ParamResolver({}), measurements={"x": np.array([[0, 1], [0, 1], [0, 1]])} ) - params = cirq.ParamResolver({'a': 0.1}) + params = cirq.ParamResolver({"a": 0.1}) assert result.to_cirq_result(seed=3, params=params) == cirq.ResultDict( - params=params, measurements={'x': np.array([[0, 1], [0, 1], [0, 1]])} + params=params, measurements={"x": np.array([[0, 1], [0, 1], [0, 1]])} ) assert result.to_cirq_result(seed=2, override_repetitions=2) == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'x': np.array([[0, 1], [0, 0]])} + params=cirq.ParamResolver({}), measurements={"x": np.array([[0, 1], [0, 0]])} ) result = ionq.SimulatorResult( - {0b00: 0.25, 0b01: 0.75}, num_qubits=2, measurement_dict={'x': [0]}, repetitions=3 + {0b00: 0.25, 0b01: 0.75}, num_qubits=2, measurement_dict={"x": [0]}, repetitions=3 ) assert result.to_cirq_result(seed=2) == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'x': np.array([[0], [0], [0]])} + params=cirq.ParamResolver({}), measurements={"x": np.array([[0], [0], [0]])} ) result = ionq.SimulatorResult( - {0b00: 0.25, 0b01: 0.75}, num_qubits=2, measurement_dict={'x': [1]}, repetitions=3 + {0b00: 0.25, 0b01: 0.75}, num_qubits=2, measurement_dict={"x": [1]}, repetitions=3 ) assert result.to_cirq_result(seed=2) == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'x': np.array([[1], [0], [1]])} + params=cirq.ParamResolver({}), measurements={"x": np.array([[1], [0], [1]])} ) # cirq.Result only compares pandas data frame, so possible to have supplied an list of # list instead of a numpy multidimensional array. Check this here. - assert type(result.to_cirq_result().measurements['x']) == np.ndarray + assert type(result.to_cirq_result().measurements["x"]) == np.ndarray # when memory_results is provided, actual shot data is used instead of sampling. result = ionq.SimulatorResult( {0b00: 0.5, 0b11: 0.5}, num_qubits=2, - measurement_dict={'x': [0, 1]}, + measurement_dict={"x": [0, 1]}, repetitions=3, - memory_results=['2', '0', '2'], + memory_results=["2", "0", "2"], ) assert result.to_cirq_result() == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'x': np.array([[0, 1], [0, 0], [0, 1]])} + params=cirq.ParamResolver({}), measurements={"x": np.array([[0, 1], [0, 0], [0, 1]])} ) # override_repetitions slices the memory results when memory is present. assert result.to_cirq_result(override_repetitions=2) == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'x': np.array([[0, 1], [0, 0]])} + params=cirq.ParamResolver({}), measurements={"x": np.array([[0, 1], [0, 0]])} ) @@ -342,21 +342,21 @@ def test_simulator_result_to_cirq_result_multiple_keys(): result = ionq.SimulatorResult( {0b000: 0.25, 0b011: 0.75}, num_qubits=3, - measurement_dict={'x': [1], 'y': [2, 0]}, + measurement_dict={"x": [1], "y": [2, 0]}, repetitions=3, ) assert result.to_cirq_result(seed=2) == cirq.ResultDict( params=cirq.ParamResolver({}), - measurements={'x': np.array([[1], [0], [1]]), 'y': np.array([[1, 0], [0, 0], [1, 0]])}, + measurements={"x": np.array([[1], [0], [1]]), "y": np.array([[1, 0], [0, 0], [1, 0]])}, ) def test_simulator_result_to_cirq_result_shotwise_uses_target_indices(): result = ionq.SimulatorResult( - {0b10: 1.0}, num_qubits=2, measurement_dict={'b': [1]}, repetitions=1, memory_results=[0b10] + {0b10: 1.0}, num_qubits=2, measurement_dict={"b": [1]}, repetitions=1, memory_results=[0b10] ) assert result.to_cirq_result() == cirq.ResultDict( - params=cirq.ParamResolver({}), measurements={'b': np.array([[1]])} + params=cirq.ParamResolver({}), measurements={"b": np.array([[1]])} ) @@ -364,5 +364,5 @@ def test_simulator_result_to_cirq_result_no_keys(): result = ionq.SimulatorResult( {0b00: 1, 0b01: 2}, num_qubits=2, measurement_dict={}, repetitions=3 ) - with pytest.raises(ValueError, match='cirq results'): + with pytest.raises(ValueError, match="cirq results"): _ = result.to_cirq_result() From 8f4eb0a2898354f5841d261a8f0b920977437fc3 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 17:06:23 +0300 Subject: [PATCH 26/30] Normalize url with urljoin. --- cirq-ionq/cirq_ionq/ionq_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-ionq/cirq_ionq/ionq_client.py b/cirq-ionq/cirq_ionq/ionq_client.py index c6bc7c9cfca..1a8ab8e8e99 100644 --- a/cirq-ionq/cirq_ionq/ionq_client.py +++ b/cirq-ionq/cirq_ionq/ionq_client.py @@ -267,7 +267,7 @@ def get_shots(self, shots_url): """ def request(): - return requests.get(f"{self.url_base}/{shots_url}", headers=self.headers) + return requests.get(urllib.parse.urljoin(self.url_base, shots_url), headers=self.headers) return self._make_request(request, {}).json() From 1829dfca0ed8c4aa688e3d1cb71b636cc4ec1ce6 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Tue, 23 Jun 2026 17:10:34 +0300 Subject: [PATCH 27/30] Fix code format. --- cirq-ionq/cirq_ionq/ionq_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cirq-ionq/cirq_ionq/ionq_client.py b/cirq-ionq/cirq_ionq/ionq_client.py index 1a8ab8e8e99..3d6f06928f8 100644 --- a/cirq-ionq/cirq_ionq/ionq_client.py +++ b/cirq-ionq/cirq_ionq/ionq_client.py @@ -267,7 +267,9 @@ def get_shots(self, shots_url): """ def request(): - return requests.get(urllib.parse.urljoin(self.url_base, shots_url), headers=self.headers) + return requests.get( + urllib.parse.urljoin(self.url_base, shots_url), headers=self.headers + ) return self._make_request(request, {}).json() From 7903dc8575dfceedc8648d532a2ee4ec04990d30 Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Wed, 24 Jun 2026 10:14:05 +0300 Subject: [PATCH 28/30] Add type annotation to new methods. --- cirq-ionq/cirq_ionq/job.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index ead20e974d1..3915133a092 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -339,7 +339,7 @@ def results( else big_endian_results_sim[0] ) - def _retrieve_child_job_shots(self, child_job_ids): + def _retrieve_child_job_shots(self, child_job_ids) -> list[list[str] | None]: """Retrieve shots for child jobs. Warn that memory results will fall back to sampled probabilities if retrieval fails. """ @@ -368,7 +368,7 @@ def _retrieve_child_job_shots(self, child_job_ids): memory_results.append(None) return memory_results - def _retrieve_job_shots(self): + def _retrieve_job_shots(self) -> list[list[str] | None]: """Retrieve shots for the job. Warn that memory results will fall back to sampled probabilities if retrieval fails. """ From 7cbfaaad651db7adb95e5550ea7508222add7b0c Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Wed, 24 Jun 2026 10:35:19 +0300 Subject: [PATCH 29/30] Correct type annotation. --- cirq-ionq/cirq_ionq/job.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index 3915133a092..25414839665 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -270,7 +270,7 @@ def results( is_batch = isinstance(some_inner_value, dict) histograms = list(backend_results.values()) if is_batch else [backend_results] - memory_results = [None for _ in histograms] + memory_results: list[list[str] | None] = [None for _ in histograms] retrieve_memory_result = self._memory and ( self.target().startswith("qpu") or ( @@ -372,7 +372,7 @@ def _retrieve_job_shots(self) -> list[list[str] | None]: """Retrieve shots for the job. Warn that memory results will fall back to sampled probabilities if retrieval fails. """ - memory_results = [None] + memory_results: list[list[str] | None] = [None] try: memory_results = [self._client.get_shots(self._job["results"]["shots"]["url"])] except KeyError as ex: From af3d56c158e210c385f1e56706cf0d05bcb26c2f Mon Sep 17 00:00:00 2001 From: Radu Marginean Date: Wed, 24 Jun 2026 11:21:18 +0300 Subject: [PATCH 30/30] Correcting type annotations. --- cirq-ionq/cirq_ionq/results.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-ionq/cirq_ionq/results.py b/cirq-ionq/cirq_ionq/results.py index 048685ed86f..799a99e1fc7 100644 --- a/cirq-ionq/cirq_ionq/results.py +++ b/cirq-ionq/cirq_ionq/results.py @@ -37,7 +37,7 @@ def __init__( counts: dict[int, int], num_qubits: int, measurement_dict: dict[str, Sequence[int]], - memory_results: list[int] | None = None, + memory_results: list[str] | 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. @@ -191,7 +191,7 @@ def __init__( num_qubits: int, measurement_dict: dict[str, Sequence[int]], repetitions: int, - memory_results: list[int] | None = None, + memory_results: list[str] | None = None, ): self._probabilities = probabilities self._num_qubits = num_qubits