Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use pytket-qir for qir generation #24

Merged
merged 12 commits into from
Dec 20, 2024
2 changes: 1 addition & 1 deletion _metadata.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__extension_version__ = "0.3.0"
__extension_version__ = "0.4.0rc0"
__extension_name__ = "pytket-azure"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need an rc for this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 changes: 6 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
Changelog
~~~~~~~~~

0.4.0rc0 (December 2024)
------------------------

* Update minimum pytket version to 1.37.0.
* Update minimum pytket-qir version to 0.19.0.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems incomplete, are there no other changes we should list?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a few more details, see remove qiskit, update changelog and version
3a89c6e


0.3.0 (October 2024)
--------------------

Expand Down
77 changes: 63 additions & 14 deletions pytket/extensions/azure/backends/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -78,6 +76,26 @@ def _get_workspace(
}


_ADDITIONAL_GATES = {
cqc-alec marked this conversation as resolved.
Show resolved Hide resolved
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."""

Expand Down Expand Up @@ -129,14 +147,16 @@ 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:
return 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)
Expand Down Expand Up @@ -177,18 +197,33 @@ 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 == "ionq.simulator":
module_bitcode = pytket_to_qir(
c,
qir_format=QIRFormat.BINARY,
int_type=64,
cut_pytket_register=False,
profile=QIRProfile.AZUREBASE,
)
raise ValueError("ionq devices currently not supported")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than drop support for ionq can we continue to use the old method (using qiskit-qir) in this case?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had removed this, because this had issues with a lot of gates and it is unclear what is supported / should be supported. I have added this now back in with the same support as we had before. This is probably fine, as this is all not a state to use in production.

else:
cqc-alec marked this conversation as resolved.
Show resolved Hide resolved
module_bitcode = pytket_to_qir(
c,
qir_format=QIRFormat.BINARY,
int_type=64,
cut_pytket_register=False,
profile=QIRProfile.AZUREADAPTIVE,
)

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}",
Expand All @@ -198,6 +233,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
Expand All @@ -210,15 +247,27 @@ 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)
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[len(long_res) - creg.size : len(long_res)])
all_bits = "".join(list_bits)

counts[OutcomeArray.from_readouts([[int(x) for x in list(all_bits)]])] = n
cqc-melf marked this conversation as resolved.
Show resolved Hide resolved
return BackendResult(counts=counts, c_bits=self._result_bits[handle])

def circuit_status(self, handle) -> CircuitStatus:
job = self._jobs[handle]
Expand All @@ -228,7 +277,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":
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@
include_package_data=True,
install_requires=[
"azure-quantum >= 2.2.0",
"pytket >= 1.34.0",
"pytket >= 1.37.0",
"pytket-qiskit >= 0.58.0",
"qiskit-qir >= 0.5.0",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're not using it we can we remove this dependency (but as commented elsewhere I think we should continue to use it for other devices rather than stop supporting them).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"pytket-qir >= 0.19.0",
],
classifiers=[
"Environment :: Console",
Expand Down
163 changes: 151 additions & 12 deletions tests/backend_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,26 @@

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
REASON = "PYTKET_RUN_REMOTE_TESTS not set (requires Azure credentials)"


@pytest.mark.skipif(skip_remote_tests, reason=REASON)
@pytest.mark.skip(reason="resulthandling currently not supported for ionq")
@pytest.mark.parametrize("azure_backend", ["ionq.simulator"], indirect=True)
def test_ionq_simulator(azure_backend: AzureBackend) -> None:
c = Circuit(2).H(0).CX(0, 1).measure_all()
Expand All @@ -37,7 +45,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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change this? I think it should be a warning not an error.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found it really tricky to debug this with only getting warnings. If you want to, I can move back to the warnings, but I think as long as we are in the testing stage, it would be better to keep this as an error.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

raise ValueError(
"quantinuum.sim.h1-1sc unavailable or queue time >= 60s: not submitting"
)


@pytest.mark.skipif(skip_remote_tests, reason=REASON)
Expand All @@ -52,19 +62,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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment (and likewise in other tests below).

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely but not guaranteed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this is a valid assumption for 1000 shots. If you want me to remove this, I will remove it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a comment to make this clear.

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"
)
Loading