From 3e9cb5c2c54dcaf283c4a97069d9fa9d8f1fb49f Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Fri, 18 Apr 2025 14:03:27 -0700 Subject: [PATCH 01/16] return a single results object instead of always a list --- cirq-ionq/cirq_ionq/job.py | 28 ++++++++++++++++++++-------- cirq-ionq/cirq_ionq/job_test.py | 22 +++++++++++----------- cirq-ionq/cirq_ionq/sampler.py | 11 ++++++++--- cirq-ionq/cirq_ionq/service.py | 19 +++++++++++-------- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index da6e3d918ff..b1b9f873a4b 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -16,7 +16,7 @@ import json import time import warnings -from typing import Dict, Optional, Sequence, TYPE_CHECKING, Union +from typing import Dict, Optional, Sequence, TYPE_CHECKING, Union, List import cirq from cirq._doc import document @@ -195,7 +195,12 @@ def results( polling_seconds: int = 1, sharpen: Optional[bool] = None, extra_query_params: Optional[dict] = None, - ) -> Union[list[results.QPUResult], list[results.SimulatorResult]]: + ) -> Union[ + results.QPUResult, + results.SimulatorResult, + List[results.QPUResult], + List[results.SimulatorResult], + ]: """Polls the IonQ api for results. Args: @@ -242,11 +247,10 @@ def results( job_id=self.job_id(), sharpen=sharpen, extra_query_params=extra_query_params ) + # is this a batch run (dict‑of‑dicts) or a single circuit? some_inner_value = next(iter(backend_results.values())) - if isinstance(some_inner_value, dict): - histograms = backend_results.values() - else: - histograms = [backend_results] + is_batch = isinstance(some_inner_value, dict) + histograms = list(backend_results.values()) if is_batch else [backend_results] # IonQ returns results in little endian, but # Cirq prefers to use big endian, so we convert. @@ -267,7 +271,11 @@ def results( measurement_dict=self.measurement_dict(circuit_index=circuit_index), ) ) - return big_endian_results_qpu + return ( + big_endian_results_qpu + if len(big_endian_results_qpu) > 1 + else big_endian_results_qpu[0] + ) else: big_endian_results_sim: list[results.SimulatorResult] = [] for circuit_index, histogram in enumerate(histograms): @@ -283,7 +291,11 @@ def results( repetitions=self.repetitions(), ) ) - return big_endian_results_sim + return ( + big_endian_results_sim + if len(big_endian_results_sim) > 1 + else big_endian_results_sim[0] + ) 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 72880485378..2a46ae0dee1 100644 --- a/cirq-ionq/cirq_ionq/job_test.py +++ b/cirq-ionq/cirq_ionq/job_test.py @@ -97,7 +97,7 @@ def test_job_results_qpu(): 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]}) - assert results[0] == expected + assert results == expected def test_batch_job_results_qpu(): @@ -146,7 +146,7 @@ def test_job_results_rounding_qpu(): job = ionq.Job(mock_client, job_dict) expected = ionq.QPUResult({0: 3, 1: 4997}, 2, {'a': [0, 1]}) results = job.results() - assert results[0] == expected + assert results == expected def test_job_results_failed(): @@ -177,7 +177,7 @@ def test_job_results_qpu_endianness(): } job = ionq.Job(mock_client, job_dict) results = job.results() - assert results[0] == ionq.QPUResult({0: 600, 2: 400}, 2, measurement_dict={}) + assert results == ionq.QPUResult({0: 600, 2: 400}, 2, measurement_dict={}) def test_batch_job_results_qpu_endianness(): @@ -198,7 +198,7 @@ def test_batch_job_results_qpu_endianness(): } job = ionq.Job(mock_client, job_dict) results = job.results() - assert results[0] == 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(): @@ -214,7 +214,7 @@ def test_job_results_qpu_target_endianness(): } job = ionq.Job(mock_client, job_dict) results = job.results() - assert results[0] == ionq.QPUResult({0: 600, 2: 400}, 2, measurement_dict={}) + assert results == ionq.QPUResult({0: 600, 2: 400}, 2, measurement_dict={}) def test_batch_job_results_qpu_target_endianness(): @@ -236,7 +236,7 @@ def test_batch_job_results_qpu_target_endianness(): } job = ionq.Job(mock_client, job_dict) results = job.results() - assert results[0] == 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) @@ -254,7 +254,7 @@ def test_job_results_poll(mock_sleep): 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[0] == ionq.QPUResult({0: 600, 1: 400}, 1, measurement_dict={}) + assert results == ionq.QPUResult({0: 600, 1: 400}, 1, measurement_dict={}) mock_sleep.assert_called_once() @@ -292,7 +292,7 @@ def test_job_results_simulator(): } job = ionq.Job(mock_client, job_dict) results = job.results() - assert results[0] == ionq.SimulatorResult({0: 0.6, 1: 0.4}, 1, {}, 100) + assert results == ionq.SimulatorResult({0: 0.6, 1: 0.4}, 1, {}, 100) def test_batch_job_results_simulator(): @@ -334,7 +334,7 @@ def test_job_results_simulator_endianness(): } job = ionq.Job(mock_client, job_dict) results = job.results() - assert results[0] == ionq.SimulatorResult({0: 0.6, 2: 0.4}, 2, {}, 100) + assert results == ionq.SimulatorResult({0: 0.6, 2: 0.4}, 2, {}, 100) def test_batch_job_results_simulator_endianness(): @@ -355,7 +355,7 @@ def test_batch_job_results_simulator_endianness(): } job = ionq.Job(mock_client, job_dict) results = job.results() - assert results[0] == 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(): @@ -370,7 +370,7 @@ def test_job_sharpen_results(): } job = ionq.Job(mock_client, job_dict) results = job.results(sharpen=False) - assert results[0] == ionq.SimulatorResult({0: 60, 1: 40}, 1, {}, 100) + assert results == ionq.SimulatorResult({0: 60, 1: 40}, 1, {}, 100) def test_job_cancel(): diff --git a/cirq-ionq/cirq_ionq/sampler.py b/cirq-ionq/cirq_ionq/sampler.py index 14c0dde320f..78aa6129289 100644 --- a/cirq-ionq/cirq_ionq/sampler.py +++ b/cirq-ionq/cirq_ionq/sampler.py @@ -100,11 +100,16 @@ def run_sweep( ) for resolver in resolvers ] + # ─── collect results ─────────────────────────────────────────── if self._timeout_seconds is not None: - job_results = [job.results(timeout_seconds=self._timeout_seconds) for job in jobs] + raw_results = [j.results(timeout_seconds=self._timeout_seconds) for j in jobs] else: - job_results = [job.results() for job in jobs] - flattened_job_results = list(itertools.chain.from_iterable(job_results)) + raw_results = [j.results() for j in jobs] + + # each element of `raw_results` might be a single result or a list + flattened_job_results = [] + for r in raw_results: + flattened_job_results.extend(r if isinstance(r, list) else [r]) cirq_results = [] for result, params in zip(flattened_job_results, resolvers): if isinstance(result, results.QPUResult): diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index cd99c04bf28..58c59286198 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -124,7 +124,7 @@ def run( A `cirq.Result` for running the circuit. """ resolved_circuit = cirq.resolve_parameters(circuit, param_resolver) - job_results = self.create_job( + job_out = self.create_job( circuit=resolved_circuit, repetitions=repetitions, name=name, @@ -132,13 +132,16 @@ def run( error_mitigation=error_mitigation, extra_query_params=extra_query_params, ).results(sharpen=sharpen) - if isinstance(job_results[0], results.QPUResult): - return job_results[0].to_cirq_result(params=cirq.ParamResolver(param_resolver)) - if isinstance(job_results[0], results.SimulatorResult): - return job_results[0].to_cirq_result( - params=cirq.ParamResolver(param_resolver), seed=seed - ) - raise NotImplementedError(f"Unrecognized job result type '{type(job_results[0])}'.") + + # normalise: single‑circuit jobs should deliver one result + if isinstance(job_out, list): + job_out = job_out[0] + + if isinstance(job_out, results.QPUResult): + return job_out.to_cirq_result(params=cirq.ParamResolver(param_resolver)) + if isinstance(job_out, results.SimulatorResult): + return job_out.to_cirq_result(params=cirq.ParamResolver(param_resolver), seed=seed) + raise NotImplementedError(f"Unrecognized job result type '{type(job_out)}'.") def run_batch( self, From 382be84107f46fc987578a28707e15d4c047449e Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Fri, 18 Apr 2025 14:13:58 -0700 Subject: [PATCH 02/16] Clarify comment regarding single-circuit job results --- cirq-ionq/cirq_ionq/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index 58c59286198..d2c0861ebd9 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -133,7 +133,7 @@ def run( extra_query_params=extra_query_params, ).results(sharpen=sharpen) - # normalise: single‑circuit jobs should deliver one result + # single‑circuit jobs should deliver one result if isinstance(job_out, list): job_out = job_out[0] From 80e830fea88a7adca6722fdf96e3174ec3700a86 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Fri, 18 Apr 2025 14:21:19 -0700 Subject: [PATCH 03/16] Clarify return type in Sampler.run_sweep documentation --- cirq-ionq/cirq_ionq/sampler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cirq-ionq/cirq_ionq/sampler.py b/cirq-ionq/cirq_ionq/sampler.py index 78aa6129289..4e693b4ee47 100644 --- a/cirq-ionq/cirq_ionq/sampler.py +++ b/cirq-ionq/cirq_ionq/sampler.py @@ -88,8 +88,8 @@ def run_sweep( repetitions: The number of times to sample. Returns: - Either a list of `cirq_ionq.QPUResult` or a list of `cirq_ionq.SimulatorResult` - depending on whether the job was running on an actual quantum processor or a simulator. + Either a single scalar or list of `cirq_ionq.QPUResult` or `cirq_ionq.SimulatorResult` + depending on whether the job or jobs ran on an actual quantum processor or a simulator. """ resolvers = [r for r in cirq.to_resolvers(params)] jobs = [ @@ -100,7 +100,7 @@ def run_sweep( ) for resolver in resolvers ] - # ─── collect results ─────────────────────────────────────────── + # collect results if self._timeout_seconds is not None: raw_results = [j.results(timeout_seconds=self._timeout_seconds) for j in jobs] else: From af666067bfb8587a7de3ef09f952b64b13cfdb86 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Fri, 18 Apr 2025 14:27:19 -0700 Subject: [PATCH 04/16] Clarify handling of single-circuit job results in Service.run method --- cirq-ionq/cirq_ionq/service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index d2c0861ebd9..bb2cd56ccae 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -133,7 +133,10 @@ def run( extra_query_params=extra_query_params, ).results(sharpen=sharpen) - # single‑circuit jobs should deliver one result + # `create_job()` always submits a single circuit, so the API either gives us: + # - a QPUResult / SimulatorResult, or + # - a list of length‑1 (the batch logic in Job.results still wraps it in a list). + # In the latter case we unwrap it here. if isinstance(job_out, list): job_out = job_out[0] From 0625bbcd69c7cd249ce1cef7f1810238a07ea757 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Fri, 18 Apr 2025 14:31:19 -0700 Subject: [PATCH 05/16] Refactor type annotations in job and sampler modules for consistency --- cirq-ionq/cirq_ionq/job.py | 2 +- cirq-ionq/cirq_ionq/sampler.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index b1b9f873a4b..55b207e9302 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -16,7 +16,7 @@ import json import time import warnings -from typing import Dict, Optional, Sequence, TYPE_CHECKING, Union, List +from typing import Dict, List, Optional, Sequence, TYPE_CHECKING, Union import cirq from cirq._doc import document diff --git a/cirq-ionq/cirq_ionq/sampler.py b/cirq-ionq/cirq_ionq/sampler.py index 4e693b4ee47..ef20a0b5131 100644 --- a/cirq-ionq/cirq_ionq/sampler.py +++ b/cirq-ionq/cirq_ionq/sampler.py @@ -13,8 +13,7 @@ # limitations under the License. """A `cirq.Sampler` implementation for the IonQ API.""" -import itertools -from typing import Optional, Sequence, TYPE_CHECKING +from typing import Optional, Sequence, TYPE_CHECKING, Union import cirq from cirq_ionq import results @@ -107,7 +106,7 @@ def run_sweep( raw_results = [j.results() for j in jobs] # each element of `raw_results` might be a single result or a list - flattened_job_results = [] + flattened_job_results: list[Union[results.QPUResult, results.SimulatorResult]] = [] for r in raw_results: flattened_job_results.extend(r if isinstance(r, list) else [r]) cirq_results = [] From 6364c3044fcb5a57b49ccd4e479620e68ca8bb34 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Fri, 18 Apr 2025 14:38:22 -0700 Subject: [PATCH 06/16] Add assertion to ensure job results are iterable in Service.run method --- cirq-ionq/cirq_ionq/service.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index bb2cd56ccae..5af85cba6c1 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -16,6 +16,7 @@ import datetime import os from typing import List, Optional, Sequence +from collections.abc import Iterable import cirq from cirq_ionq import calibration, ionq_client, job, results, sampler, serializer @@ -192,6 +193,10 @@ def run_batch( error_mitigation=error_mitigation, extra_query_params=extra_query_params, ).results(sharpen=sharpen) + assert isinstance(job_results, Iterable), ( + "Expected job results to be iterable, but got type " + f"{type(job_results)}. This is a bug in the IonQ API." + ) cirq_results = [] for job_result in job_results: From 84b10f1abc3412eb204e29afeb2d7d6e9b5e7b0d Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Fri, 18 Apr 2025 14:39:42 -0700 Subject: [PATCH 07/16] Refactor import statements for improved organization in service.py --- cirq-ionq/cirq_ionq/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index 5af85cba6c1..7dbc7f10675 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -15,8 +15,8 @@ import datetime import os -from typing import List, Optional, Sequence from collections.abc import Iterable +from typing import List, Optional, Sequence import cirq from cirq_ionq import calibration, ionq_client, job, results, sampler, serializer From 809e2b631510ea27d1918b2ac8969a7836015825 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Mon, 21 Apr 2025 09:02:03 -0700 Subject: [PATCH 08/16] Add test to verify Service.run unwraps single result list --- cirq-ionq/cirq_ionq/service_test.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cirq-ionq/cirq_ionq/service_test.py b/cirq-ionq/cirq_ionq/service_test.py index 0b06c7363f1..8fe5e59798e 100644 --- a/cirq-ionq/cirq_ionq/service_test.py +++ b/cirq-ionq/cirq_ionq/service_test.py @@ -296,3 +296,27 @@ def test_service_remote_host_default(): def test_service_remote_host_from_env_var_cirq_ionq_precedence(): service = ionq.Service(api_key='tomyheart') assert service.remote_host == 'http://example.com' + +def test_service_run_unwraps_single_result_list(): + """`Service.run` should unwrap `[result]` to `result`.""" + # set up a real Service object (we'll monkey‑patch its create_job) + service = ionq.Service(remote_host="http://example.com", api_key="key") + + # simple 1‑qubit circuit + q = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(q), cirq.measure(q, key="m")) + + # fabricate a QPUResult and wrap it in a list to mimic an erroneous behavior + 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] # <- list of length‑1 + + # monkey‑patch create_job so Service.run sees our mock_job + with mock.patch.object(service, "create_job", return_value=mock_job): + out = service.run(circuit=circuit, repetitions=1, target="qpu") + + # expected Cirq result after unwrapping and conversion + expected = qpu_result.to_cirq_result(params=cirq.ParamResolver({})) + + assert out == expected + mock_job.results.assert_called_once() From 899cb05b93610a1e23f5e7b2ea2f373486ef9755 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Mon, 21 Apr 2025 09:02:27 -0700 Subject: [PATCH 09/16] format service --- cirq-ionq/cirq_ionq/service_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cirq-ionq/cirq_ionq/service_test.py b/cirq-ionq/cirq_ionq/service_test.py index 8fe5e59798e..8f47ee07e95 100644 --- a/cirq-ionq/cirq_ionq/service_test.py +++ b/cirq-ionq/cirq_ionq/service_test.py @@ -297,6 +297,7 @@ def test_service_remote_host_from_env_var_cirq_ionq_precedence(): service = ionq.Service(api_key='tomyheart') assert service.remote_host == 'http://example.com' + def test_service_run_unwraps_single_result_list(): """`Service.run` should unwrap `[result]` to `result`.""" # set up a real Service object (we'll monkey‑patch its create_job) From 76247223bf20acbc9233744727de2c7895beebc9 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Thu, 24 Apr 2025 09:29:46 -0700 Subject: [PATCH 10/16] Add test for Service.run_batch to preserve input order of circuits --- cirq-ionq/cirq_ionq/service_test.py | 50 +++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/cirq-ionq/cirq_ionq/service_test.py b/cirq-ionq/cirq_ionq/service_test.py index 8f47ee07e95..43926309670 100644 --- a/cirq-ionq/cirq_ionq/service_test.py +++ b/cirq-ionq/cirq_ionq/service_test.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import datetime import os from unittest import mock @@ -321,3 +322,52 @@ def test_service_run_unwraps_single_result_list(): assert out == expected mock_job.results.assert_called_once() + + +@pytest.mark.parametrize("target", ["qpu", "simulator"]) +def test_run_batch_preserves_order(target): + """``Service.run_batch`` must return results in the same order as the + input ``circuits`` list, regardless of how the IonQ API happens to order + its per‑circuit results. + """ + + # Service with a fully mocked HTTP client. + service = ionq.Service(remote_host="http://example.com", api_key="key") + client = mock.MagicMock() + service._client = client + + # Three trivial 1‑qubit circuits, each measuring under a unique key. + keys = ["a", "b", "c"] + q = cirq.LineQubit(0) + circuits = [cirq.Circuit(cirq.measure(q, key=k)) for k in keys] + + client.create_job.return_value = {"id": "job_id", "status": "ready"} + + client.get_job.return_value = { + "id": "job_id", + "status": "completed", + "target": target, + "qubits": "1", + "metadata": { + "shots": "1", + "measurements": json.dumps([{"measurement0": f"{k}\u001f0"} for k in keys]), + "qubit_numbers": json.dumps([1, 1, 1]), + }, + } + + # Intentionally scramble the order returned by the API: b, a, c. + client.get_results.return_value = { + "res_b": {"0": "1"}, + "res_a": {"0": "1"}, + "res_c": {"0": "1"}, + } + + results = service.run_batch(circuits, repetitions=1, target=target) + + # The order of measurement keys in the results should match the input + # circuit order exactly (a, b, c). + assert [next(iter(r.measurements)) for r in results] == keys + + # Smoke‑test on the mocked client usage. + client.create_job.assert_called_once() + client.get_results.assert_called_once() From e76ec62ea67a861100461114ae52201eac8c0e36 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Thu, 24 Apr 2025 10:22:02 -0700 Subject: [PATCH 11/16] Reorder import statements in service_test.py to follow conventions --- cirq-ionq/cirq_ionq/service_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-ionq/cirq_ionq/service_test.py b/cirq-ionq/cirq_ionq/service_test.py index 43926309670..2d1ce2a2575 100644 --- a/cirq-ionq/cirq_ionq/service_test.py +++ b/cirq-ionq/cirq_ionq/service_test.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json import datetime +import json import os from unittest import mock From 33e18a1a9d108022e1ae0af2d9baa02e4e7a472c Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Thu, 9 Oct 2025 13:20:38 -0700 Subject: [PATCH 12/16] Refactor type hints in job, sampler, and service modules for consistency and clarity --- cirq-ionq/cirq_ionq/job.py | 18 +++++++++--------- cirq-ionq/cirq_ionq/sampler.py | 5 ++--- cirq-ionq/cirq_ionq/service.py | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index 385a4ae1cf4..da3b4edbdd8 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -18,7 +18,7 @@ import json import time import warnings -from typing import Dict, List, Optional, Sequence, TYPE_CHECKING, Union +from typing import Sequence, TYPE_CHECKING import cirq from cirq._doc import document @@ -195,14 +195,14 @@ def results( self, timeout_seconds: int = 7200, polling_seconds: int = 1, - sharpen: Optional[bool] = None, - extra_query_params: Optional[dict] = None, - ) -> Union[ - results.QPUResult, - results.SimulatorResult, - List[results.QPUResult], - List[results.SimulatorResult], - ]: + sharpen: bool | None = None, + extra_query_params: dict | None = None, + ) -> ( + results.QPUResult + | results.SimulatorResult + | list[results.QPUResult] + | list[results.SimulatorResult] + ): """Polls the IonQ api for results. Args: diff --git a/cirq-ionq/cirq_ionq/sampler.py b/cirq-ionq/cirq_ionq/sampler.py index 5022cdb89b4..a26f920f6c8 100644 --- a/cirq-ionq/cirq_ionq/sampler.py +++ b/cirq-ionq/cirq_ionq/sampler.py @@ -15,8 +15,7 @@ from __future__ import annotations -import itertools -from typing import Optional, Sequence, TYPE_CHECKING, Union +from typing import Sequence, TYPE_CHECKING, Union import cirq from cirq_ionq import results @@ -109,7 +108,7 @@ def run_sweep( raw_results = [j.results() for j in jobs] # each element of `raw_results` might be a single result or a list - flattened_job_results: list[Union[results.QPUResult, results.SimulatorResult]] = [] + flattened_job_results: list[results.QPUResult | results.SimulatorResult] = [] for r in raw_results: flattened_job_results.extend(r if isinstance(r, list) else [r]) cirq_results = [] diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index d505b8f72b5..04b43f4aa55 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -18,7 +18,7 @@ import datetime import os from collections.abc import Iterable -from typing import List, Optional, Sequence +from typing import Sequence import cirq from cirq_ionq import calibration, ionq_client, job, results, sampler, serializer From 392a4b59cc2bb4a7d251c7a14a2305c46a2c8390 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Thu, 9 Oct 2025 13:27:47 -0700 Subject: [PATCH 13/16] Fix formatting inconsistencies in comments across job, sampler, and service modules --- cirq-ionq/cirq_ionq/job.py | 2 +- cirq-ionq/cirq_ionq/sampler.py | 2 +- cirq-ionq/cirq_ionq/service.py | 2 +- cirq-ionq/cirq_ionq/service_test.py | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index da3b4edbdd8..e5bb0b30bfb 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -249,7 +249,7 @@ def results( job_id=self.job_id(), sharpen=sharpen, extra_query_params=extra_query_params ) - # is this a batch run (dict‑of‑dicts) or a single circuit? + # is this a batch run (dict-of-dicts) or a single circuit? some_inner_value = next(iter(backend_results.values())) is_batch = isinstance(some_inner_value, dict) histograms = list(backend_results.values()) if is_batch else [backend_results] diff --git a/cirq-ionq/cirq_ionq/sampler.py b/cirq-ionq/cirq_ionq/sampler.py index a26f920f6c8..cecaa063125 100644 --- a/cirq-ionq/cirq_ionq/sampler.py +++ b/cirq-ionq/cirq_ionq/sampler.py @@ -15,7 +15,7 @@ from __future__ import annotations -from typing import Sequence, TYPE_CHECKING, Union +from typing import Sequence, TYPE_CHECKING import cirq from cirq_ionq import results diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index 04b43f4aa55..c0305e6a04b 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -161,7 +161,7 @@ def run( # `create_job()` always submits a single circuit, so the API either gives us: # - a QPUResult / SimulatorResult, or - # - a list of length‑1 (the batch logic in Job.results still wraps it in a list). + # - a list of length-1 (the batch logic in Job.results still wraps it in a list). # In the latter case we unwrap it here. if isinstance(job_out, list): job_out = job_out[0] diff --git a/cirq-ionq/cirq_ionq/service_test.py b/cirq-ionq/cirq_ionq/service_test.py index 89e0b5157af..2dd89b8f767 100644 --- a/cirq-ionq/cirq_ionq/service_test.py +++ b/cirq-ionq/cirq_ionq/service_test.py @@ -301,19 +301,19 @@ def test_service_remote_host_from_env_var_cirq_ionq_precedence(): def test_service_run_unwraps_single_result_list(): """`Service.run` should unwrap `[result]` to `result`.""" - # set up a real Service object (we'll monkey‑patch its create_job) + # set up a real Service object (we'll monkey-patch its create_job) service = ionq.Service(remote_host="http://example.com", api_key="key") - # simple 1‑qubit circuit + # simple 1-qubit circuit q = cirq.LineQubit(0) circuit = cirq.Circuit(cirq.X(q), cirq.measure(q, key="m")) # fabricate a QPUResult and wrap it in a list to mimic an erroneous behavior 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] # <- list of length‑1 + mock_job.results.return_value = [qpu_result] # <- list of length-1 - # monkey‑patch create_job so Service.run sees our mock_job + # monkey-patch create_job so Service.run sees our mock_job with mock.patch.object(service, "create_job", return_value=mock_job): out = service.run(circuit=circuit, repetitions=1, target="qpu") @@ -328,7 +328,7 @@ def test_service_run_unwraps_single_result_list(): def test_run_batch_preserves_order(target): """``Service.run_batch`` must return results in the same order as the input ``circuits`` list, regardless of how the IonQ API happens to order - its per‑circuit results. + its per-circuit results. """ # Service with a fully mocked HTTP client. @@ -336,7 +336,7 @@ def test_run_batch_preserves_order(target): client = mock.MagicMock() service._client = client - # Three trivial 1‑qubit circuits, each measuring under a unique key. + # Three trivial 1-qubit circuits, each measuring under a unique key. keys = ["a", "b", "c"] q = cirq.LineQubit(0) circuits = [cirq.Circuit(cirq.measure(q, key=k)) for k in keys] @@ -346,7 +346,7 @@ def test_run_batch_preserves_order(target): client.get_job.return_value = { "id": "job_id", "status": "completed", - "target": target, + "backend": target, "qubits": "1", "metadata": { "shots": "1", @@ -368,6 +368,6 @@ def test_run_batch_preserves_order(target): # circuit order exactly (a, b, c). assert [next(iter(r.measurements)) for r in results] == keys - # Smoke‑test on the mocked client usage. + # Smoke-test on the mocked client usage. client.create_job.assert_called_once() client.get_results.assert_called_once() From 604876f57938d1540dd5bf93ba3f3e92cff8138b Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Thu, 9 Oct 2025 14:41:13 -0700 Subject: [PATCH 14/16] Enhance documentation for job results and sampler methods in IonQ API integration --- cirq-ionq/cirq_ionq/job.py | 17 ++++++++++-- cirq-ionq/cirq_ionq/sampler.py | 7 +++-- cirq-ionq/cirq_ionq/service.py | 12 ++++++-- docs/hardware/ionq/jobs.md | 51 ++++++++++++++++++++++------------ 4 files changed, 63 insertions(+), 24 deletions(-) diff --git a/cirq-ionq/cirq_ionq/job.py b/cirq-ionq/cirq_ionq/job.py index e5bb0b30bfb..38fabff5661 100644 --- a/cirq-ionq/cirq_ionq/job.py +++ b/cirq-ionq/cirq_ionq/job.py @@ -213,9 +213,10 @@ def results( extra_query_params: Specify any parameters to include in the request. Returns: - Either a list of `cirq_ionq.QPUResult` or a list of `cirq_ionq.SimulatorResult` - depending on whether the job was running on an actual quantum processor or a - simulator. + Either a single `cirq_ionq.QPUResult` / `cirq_ionq.SimulatorResult` + (for a single-circuit job) or a `list` of such results (for a + batch job). The list order for batch jobs corresponds to the + order of the input circuits. Raises: IonQUnsuccessfulJob: If the job has failed, been canceled, or deleted. @@ -223,6 +224,16 @@ def results( RuntimeError: If the job reported that it had failed on the server, or the job had an unknown status. TimeoutError: If the job timed out at the server. + + Notes: + * IonQ returns results in little endian; Cirq presents them in + big endian. + * If your code previously assumed a list, use: + r = job.results() + results_list = r if isinstance(r, list) else [r] + If your code previously assumed a single result, use: + r = job.results() + r0 = r[0] if isinstance(r, list) else r """ time_waited_seconds = 0 while time_waited_seconds < timeout_seconds: diff --git a/cirq-ionq/cirq_ionq/sampler.py b/cirq-ionq/cirq_ionq/sampler.py index cecaa063125..58527af59dc 100644 --- a/cirq-ionq/cirq_ionq/sampler.py +++ b/cirq-ionq/cirq_ionq/sampler.py @@ -89,8 +89,11 @@ def run_sweep( repetitions: The number of times to sample. Returns: - Either a single scalar or list of `cirq_ionq.QPUResult` or `cirq_ionq.SimulatorResult` - depending on whether the job or jobs ran on an actual quantum processor or a simulator. + A list of `cirq.Result` objects, one per parameter resolver in + `params`, converted from IonQ results. + + Notes: + This method blocks until all jobs in the sweep complete. """ resolvers = [r for r in cirq.to_resolvers(params)] jobs = [ diff --git a/cirq-ionq/cirq_ionq/service.py b/cirq-ionq/cirq_ionq/service.py index c0305e6a04b..a8ba0579726 100644 --- a/cirq-ionq/cirq_ionq/service.py +++ b/cirq-ionq/cirq_ionq/service.py @@ -143,7 +143,11 @@ def run( extra_query_params: Specify any parameters to include in the request. Returns: - A `cirq.Result` for running the circuit. + A `cirq.Result` for the circuit. + + Notes: + The IonQ backend may return a list of length 1 for single-circuit + jobs. Cirq unwraps that to a single result for `Service.run(...)`. """ resolved_circuit = cirq.resolve_parameters(circuit, param_resolver) job_out = self.create_job( @@ -222,7 +226,11 @@ def run_batch( extra_query_params: Specify any parameters to include in the request. Returns: - A a list of `cirq.Result` for running the circuit. + A list of `cirq.Result` objects, one per circuit. + + Notes: + The output list preserves the order of the input `circuits` + argument, regardless of how the IonQ API orders per-circuit results. """ resolved_circuits = [] for circuit in circuits: diff --git a/docs/hardware/ionq/jobs.md b/docs/hardware/ionq/jobs.md index 3e0e6a6b87d..b420d3fb46d 100644 --- a/docs/hardware/ionq/jobs.md +++ b/docs/hardware/ionq/jobs.md @@ -5,23 +5,25 @@ IonQ simulator. In this section we assume a `cirq_ionq.Service` object has been instantiated and is called `service` and `cirq` and `cirq_ionq` have been imported: + ```python import cirq import cirq_ionq as ionq service = ionq.Service() ``` + See [IonQ API Service](service.md) for how to set up the service. ## Running programs -The IonQ API is a service that allows you to send a quantum circuit as a *job* -to a scheduler server. This means that you can submit a job to the API, and +The IonQ API is a service that allows you to send a quantum circuit as a _job_ +to a scheduler server. This means that you can submit a job to the API, and then this job is held in a queue before being scheduled to run on the appropriate -hardware (QPU) or simulator. Once a job is created (but not necessarily yet run) +hardware (QPU) or simulator. Once a job is created (but not necessarily yet run) on the scheduler, the job is assigned an id and then you can query this job via the API. The job has a status on it, which describes what state the job is in -`running`, `completed`, `failed`, etc. From a users perspective, this is abstracted -mostly away in Cirq. A job can be run in either block modes, or non-blocking mode, +`running`, `completed`, `failed`, etc. From a users perspective, this is abstracted +mostly away in Cirq. A job can be run in either block modes, or non-blocking mode, as described below. Here we describe these different methods. @@ -40,31 +42,34 @@ circuit = cirq.Circuit( result = service.run(circuit=circuit, repetitions=100, target='qpu') print(result) ``` + Which results in + ``` x=0000000000000000000000000000000000000000000000000000111111111111111111111111111111111111111111111111 ``` + Looking at these results you should notice something strange. What are the odds -that the x measurements were all 0s followed by all 1s? The reason for this +that the x measurements were all 0s followed by all 1s? The reason for this sorting is that the IonQAPI only returns statistics about the results, i.e. what count of results were 0 and what count were 1 (or if you are measuring -multiple qubits the counts of the different outcome bit string outcomes). In +multiple qubits the counts of the different outcome bit string outcomes). In order to make this compatible with Cirq's notion of `cirq.Result`, these are then converted into raw results with the exactly correct number of results (in lexical order). In other words, the measurement results are not in an order corresponding to the temporal order of the measurements. When calling run, you will need to include the number of `repetitions` or shots -for the given circuit. In addition, if there is no `default_target` set on the -service, then a `target` needs to be specified. Currently the supported targets +for the given circuit. In addition, if there is no `default_target` set on the +service, then a `target` needs to be specified. Currently the supported targets are `qpu` and `simulator`. ### Via a sampler -Another method to get results from the IonQ API is to use a sampler. A sampler +Another method to get results from the IonQ API is to use a sampler. A sampler is specifically design to be a lightweight interface for obtaining results in a [pandas](https://pandas.pydata.org/) dataframe and is the interface -used by other classes in Cirq for objects that process data. Here is a +used by other classes in Cirq for objects that process data. Here is a simple example showing how to get a sampler and use it. ```python @@ -81,11 +86,11 @@ print(result) ### Via create job The above two methods, using run and the sampler, both block waiting for -results. This can be problematic when the queueing time for the service -is long. Instead, it is recommended that you use the job api directly. +results. This can be problematic when the queueing time for the service +is long. Instead, it is recommended that you use the job api directly. In this pattern, you can first create the job with the quantum circuit you wish to run, and the service immediately returns an object that has -the id of the job. This job id can be recorded, and at any time in +the id of the job. This job id can be recorded, and at any time in the future you can query for the results of this job. ```python @@ -97,19 +102,31 @@ circuit = cirq.Circuit( job = service.create_job(circuit=circuit, target='qpu', repetitions=100) print(job) ``` + which shows that the returned object is a `cirq_ionq.Job`: + ``` cirq_ionq.Job(job_id=93d111c1-0898-48b8-babe-80d182f8ad66) ``` One difference between this approach and the run and sampler methods is that the returned job object's results are more directly related to the -return data from the IonQ API. They are of types `ionq.QPUResult` or -`ionq.SimulatorResult`. If you wish to convert these into the +return data from the IonQ API. They are of types `ionq.QPUResult` or +`ionq.SimulatorResult`. If you wish to convert these into the `cirq.Result` format, you can use `to_cirq_result` on both of these. +> **Note (result shape):** > `job.results()` now returns **either** a single `ionq.QPUResult`/`ionq.SimulatorResult` (for a single-circuit job) **or** a **list** of such results (for a batch job). +> For batch jobs, the list order matches the order of the input circuits. +> To write code that works with both shapes: +> +> ```python +> r = job.results() +> results_list = r if isinstance(r, list) else [r] +> cirq_results = [res.to_cirq_result() for res in results_list] +> ``` + Another useful feature of working with jobs directly is that you can -directly cancel or delete jobs. In particular, the `ionq.Job` object +directly cancel or delete jobs. In particular, the `ionq.Job` object returned by `create_job` has `cancel` and `delete` methods. ## Next steps From 72027cdee81ef255862f4660121b61a99d5b6ec7 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Thu, 9 Oct 2025 14:43:10 -0700 Subject: [PATCH 15/16] Clarify result shape in job results documentation for IonQ API --- docs/hardware/ionq/jobs.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/hardware/ionq/jobs.md b/docs/hardware/ionq/jobs.md index b420d3fb46d..60d5ea8b999 100644 --- a/docs/hardware/ionq/jobs.md +++ b/docs/hardware/ionq/jobs.md @@ -115,15 +115,18 @@ return data from the IonQ API. They are of types `ionq.QPUResult` or `ionq.SimulatorResult`. If you wish to convert these into the `cirq.Result` format, you can use `to_cirq_result` on both of these. -> **Note (result shape):** > `job.results()` now returns **either** a single `ionq.QPUResult`/`ionq.SimulatorResult` (for a single-circuit job) **or** a **list** of such results (for a batch job). -> For batch jobs, the list order matches the order of the input circuits. -> To write code that works with both shapes: +> **Note - result shape of `Job.results()`:** For jobs created from a **single circuit**, +> `job.results()` returns a **single** `ionq.QPUResult` or `ionq.SimulatorResult`. +> For **batch** jobs, it returns a **list** of those results. To write code that +> works with either shape: > > ```python > r = job.results() > results_list = r if isinstance(r, list) else [r] -> cirq_results = [res.to_cirq_result() for res in results_list] > ``` +> +> Each entry can be converted to a `cirq.Result` via `.to_cirq_result(...)`. +> (`Service.run(...)` continues to return a single `cirq.Result`.) Another useful feature of working with jobs directly is that you can directly cancel or delete jobs. In particular, the `ionq.Job` object From d4f5442e068b19dd7c2dbaa1af688203b6b517b4 Mon Sep 17 00:00:00 2001 From: Spencer Churchill Date: Tue, 14 Oct 2025 08:56:33 -0700 Subject: [PATCH 16/16] remove block quoting the note and use normal markdown --- docs/hardware/ionq/jobs.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/hardware/ionq/jobs.md b/docs/hardware/ionq/jobs.md index 60d5ea8b999..98a80cffad2 100644 --- a/docs/hardware/ionq/jobs.md +++ b/docs/hardware/ionq/jobs.md @@ -115,18 +115,18 @@ return data from the IonQ API. They are of types `ionq.QPUResult` or `ionq.SimulatorResult`. If you wish to convert these into the `cirq.Result` format, you can use `to_cirq_result` on both of these. -> **Note - result shape of `Job.results()`:** For jobs created from a **single circuit**, -> `job.results()` returns a **single** `ionq.QPUResult` or `ionq.SimulatorResult`. -> For **batch** jobs, it returns a **list** of those results. To write code that -> works with either shape: -> -> ```python -> r = job.results() -> results_list = r if isinstance(r, list) else [r] -> ``` -> -> Each entry can be converted to a `cirq.Result` via `.to_cirq_result(...)`. -> (`Service.run(...)` continues to return a single `cirq.Result`.) +**Note - result shape of `Job.results()`:** For jobs created from a **single circuit**, +`job.results()` returns a **single** `ionq.QPUResult` or `ionq.SimulatorResult`. +For **batch** jobs, it returns a **list** of those results. To write code that +works with either shape: + +```python +r = job.results() +results_list = r if isinstance(r, list) else [r] +``` + +Each entry can be converted to a `cirq.Result` via `.to_cirq_result(...)`. +(`Service.run(...)` continues to return a single `cirq.Result`.) Another useful feature of working with jobs directly is that you can directly cancel or delete jobs. In particular, the `ionq.Job` object