diff --git a/azure-quantum/azure/quantum/cirq/targets/generic.py b/azure-quantum/azure/quantum/cirq/targets/generic.py index ec73b249..49a5e6fd 100644 --- a/azure-quantum/azure/quantum/cirq/targets/generic.py +++ b/azure-quantum/azure/quantum/cirq/targets/generic.py @@ -111,7 +111,7 @@ class AzureGenericQirCirqTarget(AzureTarget, CirqTarget): that do not have dedicated Cirq target classes. Translation pipeline: - - Cirq circuit -> OpenQASM (via `cirq.Circuit.to_qasm()`) + - Cirq circuit -> OpenQASM 3 (via `cirq.Circuit.to_qasm(version="3.0")`) - OpenQASM -> QIR (via `qsharp.openqasm.compile`) Dependencies: requires `qsharp` to be installed. @@ -168,7 +168,13 @@ def _translate_cirq_circuit(circuit: "cirq.Circuit") -> QirRepresentable: "Install with: pip install azure-quantum[cirq,qsharp]" ) from exc - qasm = circuit.to_qasm() + if cirq.is_parameterized(circuit): + raise ValueError( + "Cannot serialize a parameterized Cirq circuit to OpenQASM 3. " + "Resolve parameters first (e.g. via cirq.resolve_parameters)." + ) + + qasm = circuit.to_qasm(version="3.0") return compile(qasm) diff --git a/azure-quantum/azure/quantum/cirq/targets/ionq.py b/azure-quantum/azure/quantum/cirq/targets/ionq.py index dd4f473a..8c03bca0 100644 --- a/azure-quantum/azure/quantum/cirq/targets/ionq.py +++ b/azure-quantum/azure/quantum/cirq/targets/ionq.py @@ -2,7 +2,9 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. ## -from typing import TYPE_CHECKING, Any, Dict, Union, Optional +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union try: import cirq @@ -117,7 +119,13 @@ def _translate_cirq_circuit(circuit) -> Dict[str, Any]: """Translate Cirq circuit to IonQ JSON. If dependencies \ are not installed, throw error with installation instructions.""" from cirq_ionq import Serializer - return Serializer().serialize(circuit) + + serializer = Serializer() + if hasattr(serializer, "serialize_single_circuit"): + return serializer.serialize_single_circuit(circuit) + + # Backward-compat for older cirq_ionq. + return serializer.serialize(circuit) def _to_cirq_job(self, azure_job: "AzureJob") -> "CirqIonqJob": """Convert Azure job to Cirq job""" @@ -160,10 +168,18 @@ def submit( @staticmethod def _to_cirq_result( - result: Union[QPUResult, SimulatorResult], + result: "IonQResultLike", param_resolver: cirq.ParamResolverOrSimilarType = cirq.ParamResolver({}), seed: cirq.RANDOM_STATE_OR_SEED_LIKE = None, ) -> "cirq.Result": + # cirq_ionq>=1.6 returns a list of results even for a single circuit. + if isinstance(result, (list, tuple)): + if len(result) != 1: + raise ValueError( + f"Expected a single IonQ result for a single circuit, got {len(result)} results." + ) + result = result[0] + if isinstance(result, QPUResult): return result.to_cirq_result(params=cirq.ParamResolver(param_resolver)) elif isinstance(result, SimulatorResult): @@ -172,4 +188,8 @@ def _to_cirq_result( raise ValueError("Result {result} not supported. \ Expecting either a cirq_ionq.results.QPUResult \ or cirq_ionq.results.SimulatorResult.") + + +IonQSingleResult = Union[QPUResult, SimulatorResult] +IonQResultLike = Union[IonQSingleResult, Sequence[IonQSingleResult]] \ No newline at end of file diff --git a/azure-quantum/azure/quantum/cirq/targets/quantinuum.py b/azure-quantum/azure/quantum/cirq/targets/quantinuum.py index d7e38c2f..9b2346e1 100644 --- a/azure-quantum/azure/quantum/cirq/targets/quantinuum.py +++ b/azure-quantum/azure/quantum/cirq/targets/quantinuum.py @@ -44,8 +44,21 @@ def __init__( @staticmethod def _translate_cirq_circuit(circuit) -> str: - """Translate `cirq` circuit to OpenQASM 2.0.""" - return circuit.to_qasm() + """Translate `cirq` circuit to OpenQASM 2.0. + + Note: The Quantinuum targets in this SDK default to + `input_data_format="honeywell.openqasm.v1"`, which corresponds to + OpenQASM 2.0. + """ + import cirq + + if cirq.is_parameterized(circuit): + raise ValueError( + "Cannot serialize a parameterized Cirq circuit to OpenQASM 2.0. " + "Resolve parameters first (e.g. via cirq.resolve_parameters)." + ) + + return circuit.to_qasm(version="2.0") @staticmethod def _to_cirq_result(result: Dict[str, Any], param_resolver, **kwargs): diff --git a/azure-quantum/requirements-cirq.txt b/azure-quantum/requirements-cirq.txt index 68705637..6ca0fa66 100644 --- a/azure-quantum/requirements-cirq.txt +++ b/azure-quantum/requirements-cirq.txt @@ -1,2 +1,2 @@ -cirq-core>=1.3.0,<=1.4.1 -cirq-ionq>=1.3.0,<=1.4.1 \ No newline at end of file +cirq-core>=1.6.1,<1.7; python_version >= "3.10" +cirq-ionq>=1.6.1,<1.7; python_version >= "3.10" \ No newline at end of file diff --git a/azure-quantum/tests/test_cirq.py b/azure-quantum/tests/test_cirq.py index 4c9b002e..aa777691 100644 --- a/azure-quantum/tests/test_cirq.py +++ b/azure-quantum/tests/test_cirq.py @@ -411,3 +411,61 @@ def test_cirq_job_results_converts_generic_target_shots( result.measurements["m"], np.asarray([[0, 1], [1, 0], [0, 0]], dtype=np.int8), ) + + +def test_cirq_to_qasm_supports_openqasm3_program(): + cirq = pytest.importorskip("cirq") + + q0, q1 = cirq.LineQubit.range(2) + circuit = cirq.Circuit( + cirq.H(q0), + cirq.CNOT(q0, q1), + cirq.measure(q0, q1, key="m"), + ) + + qasm = circuit.to_qasm(version="3.0") + + # Cirq's exporter may include a leading comment header. + assert "OPENQASM 3.0;" in qasm + assert 'include "stdgates.inc";' in qasm + assert "qubit[2] q;" in qasm + assert "h q[0];" in qasm + assert "cx q[0],q[1];" in qasm + assert "measure q[0];" in qasm + assert "measure q[1];" in qasm + + +def test_cirq_ionq_serializer_api_compatibility(): + cirq = pytest.importorskip("cirq") + pytest.importorskip("cirq_ionq") + + from azure.quantum.cirq.targets.ionq import IonQTarget + + q0 = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(q0), cirq.measure(q0, key="m")) + + serialized = IonQTarget._translate_cirq_circuit(circuit) + + assert hasattr(serialized, "body") + assert isinstance(serialized.body, dict) + assert "qubits" in serialized.body + + +def test_cirq_ionq_to_cirq_result_accepts_singleton_list(): + cirq = pytest.importorskip("cirq") + pytest.importorskip("cirq_ionq") + + from cirq_ionq.results import SimulatorResult + from azure.quantum.cirq.targets.ionq import IonQTarget + + sim_result = SimulatorResult( + probabilities={0: 0.5, 1: 0.5}, + num_qubits=1, + measurement_dict={"m": [0]}, + repetitions=10, + ) + + cirq_result = IonQTarget._to_cirq_result( + [sim_result], param_resolver=cirq.ParamResolver({}) + ) + assert hasattr(cirq_result, "measurements") diff --git a/azure_quantum_manual_tests.ipynb b/azure_quantum_manual_tests.ipynb index 9997af65..383f2f25 100644 --- a/azure_quantum_manual_tests.ipynb +++ b/azure_quantum_manual_tests.ipynb @@ -567,6 +567,11 @@ " target_name = futures[future]\n", " try:\n", " result = future.result()\n", + " # cirq_ionq>=1.6 returns a list of results even for a single circuit.\n", + " if isinstance(result, (list, tuple)):\n", + " if len(result) != 1:\n", + " raise ValueError(f\"[{target_name}] Expected a single result, got {len(result)}\")\n", + " result = result[0]\n", " # The IonQ provider wrapper (cirq_ionq.Job) returns a SimulatorResult or\n", " # QPUResult rather than a cirq.Result. Normalize by calling to_cirq_result().\n", " if hasattr(result, \"to_cirq_result\"):\n",