diff --git a/_metadata.py b/_metadata.py index d5ce1a9..e9589dd 100644 --- a/_metadata.py +++ b/_metadata.py @@ -1,2 +1,2 @@ -__extension_version__ = "0.3.0" +__extension_version__ = "0.4.0" __extension_name__ = "pytket-azure" diff --git a/docs/changelog.rst b/docs/changelog.rst index 39fc890..97e3715 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,14 @@ Changelog ~~~~~~~~~ +0.4.0 (unreleased) +------------------ + +* Update minimum pytket version to 1.37.0. +* Update minimum pytket-qir version to 0.19.0. +* Use pytket-qir for the QIR generation +* remove qiskit-qir and pytket-qiskit as dependencies + 0.3.0 (October 2024) -------------------- @@ -11,7 +19,6 @@ Changelog * Update minimum pytket version to 1.34.0. * Update minimum pytket-qiskit version to 0.58.0. - 0.2.0 (October 2024) --------------------- diff --git a/pytket/extensions/azure/backends/azure.py b/pytket/extensions/azure/backends/azure.py index 559582d..5b7fc44 100644 --- a/pytket/extensions/azure/backends/azure.py +++ b/pytket/extensions/azure/backends/azure.py @@ -19,8 +19,6 @@ from functools import cache from typing import Any, Optional, Union, cast -from qiskit_qir import to_qir_module - from azure.quantum import Job, Workspace from pytket.backends import Backend, CircuitStatus, ResultHandle, StatusEnum from pytket.backends.backend import KwargTypes @@ -30,9 +28,9 @@ from pytket.backends.resulthandle import _ResultIdTuple from pytket.circuit import Circuit, OpType from pytket.extensions.azure._metadata import __extension_version__ -from pytket.extensions.qiskit import tk_to_qiskit from pytket.passes import AutoRebase, BasePass from pytket.predicates import GateSetPredicate, Predicate +from pytket.qir import QIRFormat, QIRProfile, pytket_to_qir from pytket.utils import OutcomeArray from .config import AzureConfig @@ -78,6 +76,26 @@ def _get_workspace( } +_ADDITIONAL_GATES = { + OpType.Reset, + OpType.Measure, + OpType.Barrier, + OpType.RangePredicate, + OpType.MultiBit, + OpType.ExplicitPredicate, + OpType.ExplicitModifier, + OpType.SetBits, + OpType.CopyBits, + OpType.ClassicalExpBox, + OpType.ClExpr, + OpType.WASM, +} + + +_ALL_GATES = _ADDITIONAL_GATES.copy() +_ALL_GATES.update(_GATE_SET) + + class AzureBackend(Backend): """Interface to Azure Quantum.""" @@ -129,6 +147,8 @@ def __init__( ) _persistent_handles = False self._jobs: dict[ResultHandle, Job] = {} + self._result_bits: dict[ResultHandle, list] = {} + self._result_c_regs: dict[ResultHandle, list] = {} @property def backend_info(self) -> BackendInfo: @@ -136,7 +156,7 @@ def backend_info(self) -> BackendInfo: @property def required_predicates(self) -> list[Predicate]: - return [GateSetPredicate(_GATE_SET)] + return [GateSetPredicate(_ALL_GATES)] def rebase_pass(self) -> BasePass: return AutoRebase(gateset=_GATE_SET) @@ -177,18 +197,38 @@ def process_circuits( handles = [] for i, (c, n_shots) in enumerate(zip(circuits, n_shots_list)): - qkc = tk_to_qiskit(c) - module, entry_points = to_qir_module(qkc) - assert len(entry_points) == 1 + input_params = { - "entryPoint": entry_points[0], + "entryPoint": "main", "arguments": [], "count": n_shots, } + + if ( + self._backendinfo.device_name + and self._backendinfo.device_name[:11] == "quantinuum." + ): + + module_bitcode = pytket_to_qir( + c, + qir_format=QIRFormat.BINARY, + int_type=64, + cut_pytket_register=False, + profile=QIRProfile.AZUREADAPTIVE, + ) + else: + module_bitcode = pytket_to_qir( + c, + qir_format=QIRFormat.BINARY, + int_type=64, + cut_pytket_register=False, + profile=QIRProfile.AZUREBASE, + ) + if option_params is not None: input_params.update(option_params) # type: ignore job = self._target.submit( - input_data=module.bitcode, + input_data=module_bitcode, input_data_format="qir.v1", output_data_format="microsoft.quantum-results.v1", name=f"job_{i}", @@ -198,6 +238,8 @@ def process_circuits( handle = ResultHandle(jobid) handles.append(handle) self._jobs[handle] = job + self._result_bits[handle] = c.bits + self._result_c_regs[handle] = c.c_registers for handle in handles: self._cache[handle] = dict() return handles @@ -210,15 +252,38 @@ def _update_cache_result( else: self._cache[handle] = result_dict - def _make_backend_result(self, results: Any, job: Job) -> BackendResult: + def _make_backend_result( + self, results: Any, job: Job, handle: ResultHandle + ) -> BackendResult: n_shots = job.details.input_params["count"] counts: Counter[OutcomeArray] = Counter() - for s, p in results.items(): - outcome = literal_eval(s) - n = int(n_shots * p + 0.5) - oa = OutcomeArray.from_readouts([outcome]) - counts[oa] = n - return BackendResult(counts=counts) + if ( + self._backendinfo.device_name + and self._backendinfo.device_name[:11] == "quantinuum." + ): + for s, p in results.items(): + outcome = literal_eval(s) + n = int(n_shots * p + 0.5) + assert len(outcome) == len(self._result_c_regs[handle]) + list_bits: list = [] + for res, creg in zip(outcome, self._result_c_regs[handle]): + long_res = bin(int(res)).replace( + "0b", + "0000000000000000000000000000000000000\ +00000000000000000000000000", # 0 * 63 + ) + list_bits.append(long_res[-creg.size :]) + all_bits = "".join(list_bits) + + counts[OutcomeArray.from_readouts([[int(x) for x in all_bits]])] = n + return BackendResult(counts=counts, c_bits=self._result_bits[handle]) + else: + for s, p in results.items(): + outcome = literal_eval(s) + n = int(n_shots * p + 0.5) + oa = OutcomeArray.from_readouts([outcome]) + counts[oa] = n + return BackendResult(counts=counts) def circuit_status(self, handle) -> CircuitStatus: job = self._jobs[handle] @@ -228,7 +293,7 @@ def circuit_status(self, handle) -> CircuitStatus: results = job.get_results() self._update_cache_result( handle, - {"result": self._make_backend_result(results, job)}, + {"result": self._make_backend_result(results, job, handle)}, ) return CircuitStatus(StatusEnum.COMPLETED) elif status == "Waiting": diff --git a/setup.py b/setup.py index ce0c58a..a174de7 100644 --- a/setup.py +++ b/setup.py @@ -56,9 +56,8 @@ include_package_data=True, install_requires=[ "azure-quantum >= 2.2.0", - "pytket >= 1.34.0", - "pytket-qiskit >= 0.58.0", - "qiskit-qir >= 0.5.0", + "pytket >= 1.37.0", + "pytket-qir >= 0.19.0", ], classifiers=[ "Environment :: Console", diff --git a/tests/backend_test.py b/tests/backend_test.py index b66c32c..a5bd741 100644 --- a/tests/backend_test.py +++ b/tests/backend_test.py @@ -14,11 +14,18 @@ import os from collections import Counter -from warnings import warn import pytest -from pytket.circuit import Circuit +from pytket.circuit import Circuit, Qubit, if_not_bit +from pytket.circuit.logic_exp import ( + reg_eq, + reg_geq, + reg_gt, + reg_leq, + reg_lt, + reg_neq, +) from pytket.extensions.azure import AzureBackend skip_remote_tests: bool = os.getenv("PYTKET_RUN_REMOTE_TESTS") is None @@ -37,7 +44,9 @@ def test_ionq_simulator(azure_backend: AzureBackend) -> None: counts = r.get_counts() assert counts == Counter({(0, 0): 5, (1, 1): 5}) else: - warn("ionq.simulator unavailable or queue time >= 60s: not submitting") + raise ValueError( + "ionq.simulator unavailable or queue time >= 60s: not submitting" + ) @pytest.mark.skipif(skip_remote_tests, reason=REASON) @@ -52,19 +61,148 @@ def test_quantinuum_sim_h11e(azure_backend: AzureBackend) -> None: counts = r.get_counts() assert sum(counts.values()) == 1000 else: - warn("quantinuum.sim.h1-1sc unavailable or queue time >= 60s: not submitting") + raise ValueError( + "quantinuum.sim.h1-1sc unavailable or queue time >= 60s: not submitting" + ) + + +@pytest.mark.skipif(skip_remote_tests, reason=REASON) +@pytest.mark.parametrize("azure_backend", ["quantinuum.sim.h1-1sc"], indirect=True) +def test_quantinuum_sim_h11e_two_regs(azure_backend: AzureBackend) -> None: + c = Circuit(2, name="test_classical") + a = c.add_c_register("a", 10) + b = c.add_c_register("b", 11) + + c.Measure(Qubit(0), a[0]) + c.Measure(Qubit(1), b[0]) + + a_b = azure_backend + c1 = a_b.get_compiled_circuit(c) + if a_b.is_available() and a_b.average_queue_time_s() < 60: + h = a_b.process_circuit(c1, n_shots=1000) + r = a_b.get_result(h, timeout=120) + counts = r.get_counts() + assert sum(counts.values()) == 1000 + else: + raise ValueError( + "quantinuum.sim.h1-1sc unavailable or queue time >= 60s: not submitting" + ) + + +@pytest.mark.skipif(skip_remote_tests, reason=REASON) +@pytest.mark.parametrize("azure_backend", ["quantinuum.sim.h1-1sc"], indirect=True) +def test_quantinuum_sim_h11e_complex(azure_backend: AzureBackend) -> None: + c = Circuit(1, name="test_classical") + a = c.add_c_register("a", 10) + b = c.add_c_register("b", 11) + d = c.add_c_register("d", 20) + + c.Measure(Qubit(0), a[0]) + + c.add_c_setbits([True, True] + [False] * 9, list(b)) + + c.add_classicalexpbox_register(a + b, d) # type: ignore + a_b = azure_backend + c1 = a_b.get_compiled_circuit(c) + if a_b.is_available() and a_b.average_queue_time_s() < 60: + h = a_b.process_circuit(c1, n_shots=1000) + r = a_b.get_result(h, timeout=120) + counts = r.get_counts() + assert sum(counts.values()) == 1000 + else: + raise ValueError( + "quantinuum.sim.h1-1sc unavailable or queue time >= 60s: not submitting" + ) + + +@pytest.mark.skipif(skip_remote_tests, reason=REASON) +@pytest.mark.parametrize("azure_backend", ["quantinuum.sim.h1-1sc"], indirect=True) +def test_quantinuum_sim_h11e_cond(azure_backend: AzureBackend) -> None: + c = Circuit(1, name="test_classical") + a = c.add_c_register("a", 32) + b = c.add_c_register("b", 32) + d = c.add_c_register("d", 32) + + c.Measure(Qubit(0), a[0]) + + c.add_c_setreg(23, b) + + c.add_classicalexpbox_register(a + b, d) # type: ignore + + c.X(0, condition=a[0]) + c.Measure(Qubit(0), b[4]) + + a_b = azure_backend + c1 = a_b.get_compiled_circuit(c) + if a_b.is_available() and a_b.average_queue_time_s() < 60: + h = a_b.process_circuit(c1, n_shots=1000) + r = a_b.get_result(h, timeout=120) + counts = r.get_counts() + assert sum(counts.values()) == 1000 + else: + raise ValueError( + "quantinuum.sim.h1-1sc unavailable or queue time >= 60s: not submitting" + ) + + +@pytest.mark.skipif(skip_remote_tests, reason=REASON) +@pytest.mark.parametrize("azure_backend", ["quantinuum.sim.h1-1sc"], indirect=True) +def test_quantinuum_sim_h11e_cond_2(azure_backend: AzureBackend) -> None: + c = Circuit(1, name="test_classical") + a = c.add_c_register("a", 32) + b = c.add_c_register("b", 32) + d = c.add_c_register("d", 32) + + c.Measure(Qubit(0), a[0]) + + c.add_c_setreg(23, b) + + c.add_classicalexpbox_register(a + b, d) # type: ignore + c.add_classicalexpbox_register(a - b, d) # type: ignore + c.add_classicalexpbox_register(a << 1, a) # type: ignore + c.add_classicalexpbox_register(a >> 1, b) # type: ignore + + c.X(0, condition=reg_eq(a ^ b, 1)) + c.X(0, condition=(a[0] ^ b[0])) + c.X(0, condition=reg_eq(a & b, 1)) + c.X(0, condition=reg_eq(a | b, 1)) + + c.X(0, condition=a[0]) + c.Measure(Qubit(0), b[4]) + + c.X(0, condition=reg_neq(a, 1)) + c.X(0, condition=if_not_bit(a[0])) + c.X(0, condition=reg_gt(a, 1)) + c.X(0, condition=reg_lt(a, 1)) + c.X(0, condition=reg_geq(a, 1)) + c.X(0, condition=reg_leq(a, 1)) + c.Measure(Qubit(0), b[4]) + a_b = azure_backend + c1 = a_b.get_compiled_circuit(c) + if a_b.is_available() and a_b.average_queue_time_s() < 60: + h = a_b.process_circuit(c1, n_shots=1000) + r = a_b.get_result(h, timeout=120) + counts = r.get_counts() + assert sum(counts.values()) == 1000 + else: + raise ValueError( + "quantinuum.sim.h1-1sc unavailable or queue time >= 60s: not submitting" + ) @pytest.mark.skipif(skip_remote_tests, reason=REASON) @pytest.mark.parametrize("azure_backend", ["quantinuum.sim.h1-1e"], indirect=True) def test_quantinuum_option_params(azure_backend: AzureBackend) -> None: - c = Circuit(2).H(0).CX(0, 1).measure_all() - b = azure_backend - c1 = b.get_compiled_circuit(c) - if b.is_available() and b.average_queue_time_s() < 600: - h = b.process_circuit(c1, n_shots=1000, option_params={"error_model": False}) # type: ignore - r = b.get_result(h, timeout=1200) + c = Circuit(2, 2).H(0).CX(0, 1).measure_all() + a_b = azure_backend + c1 = a_b.get_compiled_circuit(c) + if a_b.is_available() and a_b.average_queue_time_s() < 600: + h = a_b.process_circuit(c1, n_shots=1000, option_params={"error_model": False}) # type: ignore + r = a_b.get_result(h, timeout=1200) counts = r.get_counts() - assert all(x0 == x1 for x0, x1 in counts) + assert all(x[0] == x[1] for x in counts) + assert any(x[0] == 1 for x in counts) # might fail in very rare cases else: - warn("quantinuum.sim.h1-1e unavailable or queue time >= 600s: not submitting") + raise ValueError( + "quantinuum.sim.h1-1e unavailable or queue time >= 600s: not submitting" + )