diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3538da2..0c0d1ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: debug-statements - repo: https://github.com/crate-ci/typos - rev: v1.18.2 + rev: v1.19.0 hooks: - id: typos diff --git a/pyproject.toml b/pyproject.toml index 43940c9..35e5a5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "phir>=0.3.0", + "phir>=0.3.1", "pytket>=1.21.0", "wasmtime>=15.0.0", ] diff --git a/pytket/phir/phirgen.py b/pytket/phir/phirgen.py index b7b65c7..7328a4e 100644 --- a/pytket/phir/phirgen.py +++ b/pytket/phir/phirgen.py @@ -169,58 +169,82 @@ def classical_op(exp: LogicExp, *, bitwise: bool = False) -> JsonDict: } +def convert_gate(op: tk.Op, cmd: tk.Command) -> JsonDict | None: + """Return PHIR dict for a tket gate op.""" + try: + gate = tket_gate_to_phir[op.type] + except KeyError: + if op.type == tk.OpType.Phase: + # ignore global phase + return {"mop": "Skip"} + logging.exception("Gate %s unsupported by PHIR", op.get_name()) + raise + + angles = (op.params, "pi") if op.params else None + qop: JsonDict + match gate: + case "Measure": + qop = { + "qop": gate, + "returns": [arg_to_bit(cmd.bits[0])], + "args": [arg_to_bit(cmd.args[0])], + } + case ("CX" + | "CY" + | "CZ" + | "RXX" + | "RYY" + | "RZZ" + | "R2XXYYZZ" + | "SXX" + | "SXXdg" + | "SYY" + | "SYYdg" + | "SZZ" + | "SZZdg" + | "SWAP" + ): # two-qubit gates # fmt: skip + qop = { + "qop": gate, + "angles": angles, + "args": [[arg_to_bit(cmd.qubits[0]), arg_to_bit(cmd.qubits[1])]], + } + case _: # single-qubit gates + qop = { + "qop": gate, + "angles": angles, + "args": [arg_to_bit(cmd.qubits[0])], + } + return qop + + def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | None: - """Return PHIR dict give op and its arguments.""" + """Return PHIR dict given a tket op and its arguments.""" if op.is_gate(): - try: - gate = tket_gate_to_phir[op.type] - except KeyError: - if op.type == tk.OpType.Phase: - # ignore global phase - return {"mop": "Skip"} - logging.exception("Gate %s unsupported by PHIR", op.get_name()) - raise - angles = (op.params, "pi") if op.params else None - qop: JsonDict - match gate: - case "Measure": - qop = { - "qop": gate, - "returns": [arg_to_bit(cmd.bits[0])], - "args": [arg_to_bit(cmd.args[0])], - } - case ("CX" - | "CY" - | "CZ" - | "RXX" - | "RYY" - | "RZZ" - | "R2XXYYZZ" - | "SXX" - | "SXXdg" - | "SYY" - | "SYYdg" - | "SZZ" - | "SZZdg" - | "SWAP" - ): # two-qubit gates # fmt: skip - qop = { - "qop": gate, - "angles": angles, - "args": [[arg_to_bit(cmd.qubits[0]), arg_to_bit(cmd.qubits[1])]], - } - case _: # single-qubit gates - qop = { - "qop": gate, - "angles": angles, - "args": [arg_to_bit(cmd.qubits[0])], - } - return qop + return convert_gate(op, cmd) out: JsonDict | None = None match op: # non-quantum op case tk.BarrierOp(): - out = {"meta": "barrier", "args": [arg_to_bit(qbit) for qbit in cmd.qubits]} + if op.data: + # See https://github.com/CQCL/tket/blob/0ec603986821d994caa3a0fb9c4640e5bc6c0a24/pytket/pytket/qasm/qasm.py#L419-L459 + match op.data[0:5]: + case "sleep": + dur = op.data.removeprefix("sleep(").removesuffix(")") + out = { + "mop": "Idle", + "args": [arg_to_bit(qbit) for qbit in cmd.qubits], + "duration": (dur, "s"), + } + case "order" | "group": + raise NotImplementedError(op.data) + case _: + raise TypeError(op.data) + else: + out = { + "meta": "barrier", + "args": [arg_to_bit(qbit) for qbit in cmd.qubits], + } case tk.Conditional(): # where the condition is equality check out = { @@ -345,19 +369,31 @@ def dedupe_bits_to_registers(bits: "Sequence[UnitID]") -> list[str]: return list(dict.fromkeys([bit.reg_name for bit in bits])) -def make_comment_text(command: tk.Command, op: tk.Op) -> str: +def make_comment_text(cmd: tk.Command, op: tk.Op) -> str: """Converts a command + op to the PHIR comment spec.""" + comment = str(cmd) match op: case tk.Conditional(): - conditional_text = str(command) + conditional_text = str(cmd) cleaned = conditional_text[: conditional_text.find("THEN") + 4] - return f"{cleaned} {make_comment_text(command, op.op)}" + comment = f"{cleaned} {make_comment_text(cmd, op.op)}" case tk.WASMOp(): - args, returns = extract_wasm_args_and_returns(command, op) - return f"WASM function={op.func_name} args={args} returns={returns}" + args, returns = extract_wasm_args_and_returns(cmd, op) + comment = f"WASM function={op.func_name} args={args} returns={returns}" + + case tk.BarrierOp(): + comment = op.data + " " + str(cmd.args[0]) + ";" if op.data else str(cmd) + + case tk.ClassicalExpBox(): + exp = op.get_exp() + match exp: + case BitLogicExp(): + comment = str(cmd.bits[0]) + " = " + str(op.get_exp()) + case RegLogicExp(): + comment = str(cmd.bits[0].reg_name) + " = " + str(op.get_exp()) - return str(command) + return comment def get_decls(qbits: set["Qubit"], cbits: set[tkBit]) -> list[dict[str, str | int]]: diff --git a/requirements.txt b/requirements.txt index 2843cca..bd42795 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -build==1.0.3 +build==1.1.1 mypy==1.8.0 networkx==2.8.8 -phir==0.3.0 +phir==0.3.1 pre-commit==3.6.2 pydata_sphinx_theme==0.15.2 pytest==8.0.2 @@ -10,5 +10,5 @@ pytket==1.25.0 ruff==0.2.2 setuptools_scm==8.0.4 sphinx==7.2.6 -wasmtime==18.0.0 +wasmtime==18.0.2 wheel==0.42.0 diff --git a/tests/data/qasm/sleep.qasm b/tests/data/qasm/sleep.qasm new file mode 100644 index 0000000..e066cbd --- /dev/null +++ b/tests/data/qasm/sleep.qasm @@ -0,0 +1,17 @@ +OPENQASM 2.0; +include "hqslib1_dev.inc"; + +qreg q[1]; +creg c[1]; + +rx(pi/2) q[0]; + +barrier q[0]; +sleep(1) q[0]; +barrier q[0]; + +rx(-pi/2) q[0]; + +barrier q[0]; + +measure q -> c; diff --git a/tests/test_parallelization.py b/tests/test_parallelization.py index f31fdaf..3fe1143 100644 --- a/tests/test_parallelization.py +++ b/tests/test_parallelization.py @@ -10,6 +10,8 @@ import logging +import pytest + from .test_utils import QasmFile, get_phir_json logger = logging.getLogger(__name__) @@ -156,6 +158,7 @@ def test_two_qubit_exec_order_preserved() -> None: } +@pytest.mark.order("first") def test_group_ordering() -> None: """Test that groups are in the right order when the group number can decrement.""" phir = get_phir_json(QasmFile.group_ordering, rebase=True) diff --git a/tests/test_phirgen.py b/tests/test_phirgen.py index 05cf7d5..995137f 100644 --- a/tests/test_phirgen.py +++ b/tests/test_phirgen.py @@ -13,6 +13,7 @@ from pytket.circuit import Circuit from pytket.phir.api import pytket_to_phir +from pytket.qasm.qasm import circuit_from_qasm_str from .test_utils import QasmFile, get_qasm_as_circuit @@ -125,3 +126,27 @@ def test_global_phase() -> None: phir = json.loads(pytket_to_phir(circ)) assert phir["ops"][-7]["true_branch"] == [{"mop": "Skip"}] + + +def test_sleep_idle() -> None: + """Ensure sleep from qasm gets converted to PHIR Idle Mop.""" + circ = get_qasm_as_circuit(QasmFile.sleep) + phir = json.loads(pytket_to_phir(circ)) + assert phir["ops"][7] == {"mop": "Idle", "args": [["q", 0]], "duration": ["1", "s"]} + + +def test_multiple_sleep() -> None: + """Ensure multiple sleep ops get converted correctly.""" + qasm = """ + OPENQASM 2.0; + include "hqslib1_dev.inc"; + + qreg q[2]; + + sleep(1) q[0]; + sleep(2) q[1]; + """ + circ = circuit_from_qasm_str(qasm) + phir = json.loads(pytket_to_phir(circ)) + assert phir["ops"][2] == {"mop": "Idle", "args": [["q", 0]], "duration": ["1", "s"]} + assert phir["ops"][4] == {"mop": "Idle", "args": [["q", 1]], "duration": ["2", "s"]} diff --git a/tests/test_utils.py b/tests/test_utils.py index bcc2609..0671737 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -51,6 +51,7 @@ class QasmFile(Enum): cond_barrier = auto() arbitrary_qreg_names = auto() group_ordering = auto() + sleep = auto() class WatFile(Enum):