From aa86d1c05b6464e9941ac986bdd7459732af51bb Mon Sep 17 00:00:00 2001 From: Asa Kosto Date: Thu, 4 Apr 2024 17:40:46 -0600 Subject: [PATCH 1/9] explicit predicates for common logical ops and multi-bit ops supported + unit tests --- pytket/phir/phirgen.py | 54 +++++++++++++-- pytket/phir/sharding/sharder.py | 2 + tests/test_phirgen.py | 113 ++++++++++++++++++++++++++++++++ tests/test_scratch.py | 48 ++++++++++++++ 4 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 tests/test_scratch.py diff --git a/pytket/phir/phirgen.py b/pytket/phir/phirgen.py index 7dfa958..6a0b5dd 100644 --- a/pytket/phir/phirgen.py +++ b/pytket/phir/phirgen.py @@ -24,6 +24,7 @@ LogicExp, RegLogicExp, RegWiseOp, + create_logic_exp, ) from pytket.unit_id import Bit as tkBit from pytket.unit_id import BitRegister @@ -218,12 +219,12 @@ def convert_gate(op: tk.Op, cmd: tk.Command) -> JsonDict | None: return qop -def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | None: +def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | list[JsonDict] | None: # noqa: PLR0915, PLR0914 """Return PHIR dict given a tket op and its arguments.""" if op.is_gate(): return convert_gate(op, cmd) - out: JsonDict | None = None + out: JsonDict | list[JsonDict] | None = None match op: # non-quantum op case tk.BarrierOp(): if op.data: @@ -314,10 +315,46 @@ def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | None: case tk.WASMOp(): return create_wasm_op(cmd, op) + case tk.MultiBitOp(): + cop_name = cmd.op.basic_op.get_name() # type: ignore [attr-defined] + cop = BitWiseOp[cop_name] + # TODO(Asa Kosto): Add a PR to Pytket so this number is a property of cmd.op + # https://github.com/CQCL/tket/issues/1326 + multiplier = len(cmd.bits) + total_args = len(cmd.args) + args_per_op = total_args // multiplier + # create a list of 'sub-ops' that make up the MultiBitOp + # https://tket.quantinuum.com/api-docs/circuit.html#pytket.circuit.MultiBitOp + out = [] + for i in range(0, total_args, args_per_op): + args_for_sub_op = cmd.args[i : i + args_per_op] + target_bit = args_for_sub_op.pop() + log_exp = create_logic_exp(cop, args_for_sub_op) # type: ignore [arg-type] + rhs = [classical_op(log_exp, bitwise=True)] + out.append(assign_cop([arg_to_bit(target_bit)], rhs)) case _: - # TODO(kartik): NYI - # https://github.com/CQCL/pytket-phir/issues/25 - raise NotImplementedError + match op.type: + case tk.OpType.ExplicitPredicate: + cop_name = op.get_name() + cop = BitWiseOp[cop_name] + # with conditionals, cmd.args contains all bits in the expression + # the first element in cmd.args will be the conditional bit + # parse out the bits that are relevant for the true branch + match cmd.op: + case tk.Conditional(): + bits = cmd.args[1:] + case _: + bits = cmd.args.copy() + target_bit = bits.pop() + log_exp = create_logic_exp(cop, bits) # type: ignore [arg-type] + rhs = [classical_op(log_exp, bitwise=True)] + out = assign_cop([arg_to_bit(target_bit)], rhs) + + case tk.OpType.ExplicitModifier: + raise NotImplementedError + + case _: + raise NotImplementedError return out @@ -330,9 +367,12 @@ def append_cmd(cmd: tk.Command, ops: list[JsonDict]) -> None: ops: the list of ops to append to """ ops.append({"//": make_comment_text(cmd, cmd.op)}) - op: JsonDict | None = convert_subcmd(cmd.op, cmd) + op: JsonDict | list[JsonDict] | None = convert_subcmd(cmd.op, cmd) if op: - ops.append(op) + if type(op) is list: + ops.extend(op) + elif type(op) is dict: + ops.append(op) def create_wasm_op(cmd: tk.Command, wasm_op: tk.WASMOp) -> JsonDict: diff --git a/pytket/phir/sharding/sharder.py b/pytket/phir/sharding/sharder.py index 6e31004..5f872f6 100644 --- a/pytket/phir/sharding/sharder.py +++ b/pytket/phir/sharding/sharder.py @@ -23,6 +23,8 @@ OpType.ClassicalExpBox, # some classical operations are rolled up into a box OpType.RangePredicate, OpType.ExplicitPredicate, + OpType.ExplicitModifier, + OpType.MultiBit, OpType.CopyBits, OpType.WASM, ] diff --git a/tests/test_phirgen.py b/tests/test_phirgen.py index 904c134..0107748 100644 --- a/tests/test_phirgen.py +++ b/tests/test_phirgen.py @@ -10,9 +10,12 @@ import json +import pytest + from pytket.circuit import Circuit from pytket.phir.api import pytket_to_phir from pytket.qasm.qasm import circuit_from_qasm_str +from pytket.unit_id import BitRegister from .test_utils import QasmFile, get_qasm_as_circuit @@ -201,3 +204,113 @@ def test_conditional_measure() -> None: "condition": {"cop": "==", "args": [["c", 0], 1]}, "true_branch": [{"qop": "Measure", "returns": [["c", 1]], "args": [["q", 1]]}], } + + +@pytest.mark.order("first") +def test_conditional_classical_not() -> None: + """From https://github.com/CQCL/pytket-phir/issues/159 .""" + circuit = Circuit() + target_reg = circuit.add_c_register(BitRegister(name="target_reg", size=1)) + control_reg = circuit.add_c_register(BitRegister(name="control_reg", size=1)) + + circuit.add_c_not( + arg_in=target_reg[0], arg_out=target_reg[0], condition=control_reg[0] + ) + + phir = json.loads(pytket_to_phir(circuit)) + assert phir["ops"][-1] == { + "block": "if", + "condition": {"cop": "==", "args": [["control_reg", 0], 1]}, + "true_branch": [ + { + "cop": "=", + "returns": [["target_reg", 0]], + "args": [{"cop": "~", "args": [["target_reg", 0]]}], + } + ], + } + + +@pytest.mark.order("first") +def test_standard_classical_ops() -> None: + """Test classical ops added to the circuit via dedicated circuit.""" + c = Circuit(0, 4) + c.add_c_and(1, 2, 3) + c.add_c_not(1, 2) + c.add_c_or(2, 1, 3) + c.add_c_xor(1, 2, 3) + phir = json.loads(pytket_to_phir(c)) + assert phir["ops"][-1] == { + "cop": "=", + "returns": [["c", 3]], + "args": [{"cop": "^", "args": [["c", 1], ["c", 2]]}], + } + assert phir["ops"][-3] == { + "cop": "=", + "returns": [["c", 3]], + "args": [{"cop": "|", "args": [["c", 2], ["c", 1]]}], + } + assert phir["ops"][-5] == { + "cop": "=", + "returns": [["c", 2]], + "args": [{"cop": "~", "args": [["c", 1]]}], + } + assert phir["ops"][-7] == { + "cop": "=", + "returns": [["c", 3]], + "args": [{"cop": "&", "args": [["c", 1], ["c", 2]]}], + } + + +@pytest.mark.order("first") +def test_multi_bit_ops() -> None: + """Test classical ops added to the circuit via tket multi-bit ops.""" + c = Circuit(0, 4) + c0 = c.add_c_register("c0", 2) + c1 = c.add_c_register("c1", 2) + c2 = c.add_c_register("c2", 2) + c.add_c_and_to_registers(c0, c1, c2) + c.add_c_not_to_registers(c0, c1) + c.add_c_or_to_registers(c0, c1, c2) + c.add_c_xor_to_registers(c0, c1, c2) + phir = json.loads(pytket_to_phir(c)) + assert phir["ops"][-1] == { + "cop": "=", + "returns": [["c2", 1]], + "args": [{"cop": "^", "args": [["c0", 1], ["c1", 1]]}], + } + assert phir["ops"][-2] == { + "cop": "=", + "returns": [["c2", 0]], + "args": [{"cop": "^", "args": [["c0", 0], ["c1", 0]]}], + } + assert phir["ops"][-4] == { + "cop": "=", + "returns": [["c2", 1]], + "args": [{"cop": "|", "args": [["c0", 1], ["c1", 1]]}], + } + assert phir["ops"][-5] == { + "cop": "=", + "returns": [["c2", 0]], + "args": [{"cop": "|", "args": [["c0", 0], ["c1", 0]]}], + } + assert phir["ops"][-7] == { + "cop": "=", + "returns": [["c1", 1]], + "args": [{"cop": "~", "args": [["c0", 1]]}], + } + assert phir["ops"][-8] == { + "cop": "=", + "returns": [["c1", 0]], + "args": [{"cop": "~", "args": [["c0", 0]]}], + } + assert phir["ops"][-10] == { + "cop": "=", + "returns": [["c2", 1]], + "args": [{"cop": "&", "args": [["c0", 1], ["c1", 1]]}], + } + assert phir["ops"][-11] == { + "cop": "=", + "returns": [["c2", 0]], + "args": [{"cop": "&", "args": [["c0", 0], ["c1", 0]]}], + } diff --git a/tests/test_scratch.py b/tests/test_scratch.py new file mode 100644 index 0000000..30d1024 --- /dev/null +++ b/tests/test_scratch.py @@ -0,0 +1,48 @@ +# type: ignore +# ruff: noqa +import json + +from pytket.circuit import Bit, Circuit +from pytket.phir.api import pytket_to_phir + +# test case conditional NOT +# c1 = Circuit() +# target_reg = c1.add_c_register(BitRegister(name="target_reg", size=2)) +# control_reg = c1.add_c_register(BitRegister(name="control_reg", size=1)) +# c1.add_c_not(arg_in=target_reg[0], arg_out=target_reg[1], condition=control_reg[0]) +# phir = json.loads(pytket_to_phir(c1)) + +# #test case for classical explicit predicates +# c = Circuit(0, 4) +# c.add_c_and(1, 2, 3) +# c.add_c_not(1, 2) +# c.add_c_or(2, 1, 3) +# c.add_c_xor(1, 2, 3) +# print(c.get_commands()) +# phir = json.loads(pytket_to_phir(c)) + +# # Test case for MultiBitOps +# c = Circuit(0, 4) +# c0 = c.add_c_register("c0", 2) +# c1 = c.add_c_register("c1", 2) +# c2 = c.add_c_register("c2", 2) +# c.add_c_and_to_registers(c0, c1, c2) +# c.add_c_or_to_registers(c0, c1, c2) +# c.add_c_xor_to_registers(c0, c1, c2) +# c.add_c_not_to_registers(c0, c1) +# # print(c.get_commands()) +# phir = json.loads(pytket_to_phir(c)) + + +# # test case for explicit modifier +# c = Circuit(0, 4) +# # exp modifier +# and_values = [bool(i) for i in [0, 0, 0, 1]] # binary AND +# c.add_c_modifier(and_values, [1], 2) +# c.add_c_modifier(and_values, [Bit(2)], Bit(3)) +# # exp predicate +# eq_pred_values = [True, False, False, True] # test 2 bits for equality +# c.add_c_predicate(eq_pred_values, [0, 1], 2, "EQ") +# c.add_c_predicate(eq_pred_values, [Bit(1), Bit(2)], Bit(3), "EQ") +# print(c.get_commands()) +# phir = json.loads(pytket_to_phir(c)) From f3edd7c2ac676e84f1fb82a80d651f0df3dbd483 Mon Sep 17 00:00:00 2001 From: Asa Kosto Date: Fri, 5 Apr 2024 08:53:23 -0600 Subject: [PATCH 2/9] remove working test file --- tests/test_scratch.py | 48 ------------------------------------------- 1 file changed, 48 deletions(-) delete mode 100644 tests/test_scratch.py diff --git a/tests/test_scratch.py b/tests/test_scratch.py deleted file mode 100644 index 30d1024..0000000 --- a/tests/test_scratch.py +++ /dev/null @@ -1,48 +0,0 @@ -# type: ignore -# ruff: noqa -import json - -from pytket.circuit import Bit, Circuit -from pytket.phir.api import pytket_to_phir - -# test case conditional NOT -# c1 = Circuit() -# target_reg = c1.add_c_register(BitRegister(name="target_reg", size=2)) -# control_reg = c1.add_c_register(BitRegister(name="control_reg", size=1)) -# c1.add_c_not(arg_in=target_reg[0], arg_out=target_reg[1], condition=control_reg[0]) -# phir = json.loads(pytket_to_phir(c1)) - -# #test case for classical explicit predicates -# c = Circuit(0, 4) -# c.add_c_and(1, 2, 3) -# c.add_c_not(1, 2) -# c.add_c_or(2, 1, 3) -# c.add_c_xor(1, 2, 3) -# print(c.get_commands()) -# phir = json.loads(pytket_to_phir(c)) - -# # Test case for MultiBitOps -# c = Circuit(0, 4) -# c0 = c.add_c_register("c0", 2) -# c1 = c.add_c_register("c1", 2) -# c2 = c.add_c_register("c2", 2) -# c.add_c_and_to_registers(c0, c1, c2) -# c.add_c_or_to_registers(c0, c1, c2) -# c.add_c_xor_to_registers(c0, c1, c2) -# c.add_c_not_to_registers(c0, c1) -# # print(c.get_commands()) -# phir = json.loads(pytket_to_phir(c)) - - -# # test case for explicit modifier -# c = Circuit(0, 4) -# # exp modifier -# and_values = [bool(i) for i in [0, 0, 0, 1]] # binary AND -# c.add_c_modifier(and_values, [1], 2) -# c.add_c_modifier(and_values, [Bit(2)], Bit(3)) -# # exp predicate -# eq_pred_values = [True, False, False, True] # test 2 bits for equality -# c.add_c_predicate(eq_pred_values, [0, 1], 2, "EQ") -# c.add_c_predicate(eq_pred_values, [Bit(1), Bit(2)], Bit(3), "EQ") -# print(c.get_commands()) -# phir = json.loads(pytket_to_phir(c)) From 02a5064e254c70a1291fa9ab2aed369566a29e8b Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Fri, 5 Apr 2024 12:45:18 -0500 Subject: [PATCH 3/9] feat: add support for ExplicitPredicate/Modifier & MultiBitOp --- .pre-commit-config.yaml | 2 +- pytket/phir/phirgen.py | 211 ++++++++++++++++++++++------------------ tests/test_phirgen.py | 108 ++++++++++---------- 3 files changed, 174 insertions(+), 147 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b77bf90..5d1bcbd 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.20.3 + rev: v1.20.4 hooks: - id: typos diff --git a/pytket/phir/phirgen.py b/pytket/phir/phirgen.py index 6a0b5dd..565fb0a 100644 --- a/pytket/phir/phirgen.py +++ b/pytket/phir/phirgen.py @@ -24,7 +24,6 @@ LogicExp, RegLogicExp, RegWiseOp, - create_logic_exp, ) from pytket.unit_id import Bit as tkBit from pytket.unit_id import BitRegister @@ -219,49 +218,42 @@ def convert_gate(op: tk.Op, cmd: tk.Command) -> JsonDict | None: return qop -def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | list[JsonDict] | None: # noqa: PLR0915, PLR0914 - """Return PHIR dict given a tket op and its arguments.""" - if op.is_gate(): - return convert_gate(op, cmd) +def cop_from_op_name(op_name: str) -> str: + """Get PHIR classical op name from pytket op name.""" + match op_name: + case "AND": + cop = "&" + case "OR": + cop = "|" + case "XOR": + cop = "^" + case "NOT": + cop = "~" + case name: + raise NotImplementedError(name) + return cop - out: JsonDict | list[JsonDict] | None = None - match op: # non-quantum op - case tk.BarrierOp(): - 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": - duration = op.data.removeprefix("sleep(").removesuffix(")") - out = { - "mop": "Idle", - "args": [arg_to_bit(qbit) for qbit in cmd.qubits], - "duration": (float(duration), "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 = { - "block": "if", - "condition": { - "cop": "==", - "args": [ - arg_to_bit(cmd.args[0]) - if op.width == 1 - else cmd.args[0].reg_name, - op.value, - ], - }, - "true_branch": [convert_subcmd(op.op, cmd)], - } +def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict | None: + """Return PHIR dict for a pytket ClassicalEvalOp.""" + if type(op) is not type(cmd.op): + logger.warning("cmd.args might carry a conditional bit") + out: JsonDict | None = None + match op: + case tk.CopyBitsOp(): + if len(cmd.bits) != len(cmd.args) // 2: + logger.warning("LHS and RHS lengths mismatch for CopyBits") + out = assign_cop( + [arg_to_bit(bit) for bit in cmd.bits], + [arg_to_bit(cmd.args[i]) for i in range(len(cmd.args) // 2)], + ) + case tk.SetBitsOp(): + if len(cmd.bits) != len(op.values): + logger.error("LHS and RHS lengths mismatch for classical assignment") + raise ValueError + out = assign_cop( + [arg_to_bit(bit) for bit in cmd.bits], list(map(int, op.values)) + ) case tk.RangePredicateOp(): # where the condition is a range cond: JsonDict match op.lower, op.upper: @@ -285,6 +277,71 @@ def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | list[JsonDict] | No "condition": cond, "true_branch": [assign_cop([arg_to_bit(cmd.bits[0])], [1])], } + case tk.MultiBitOp(): + # determine number of register operands involved in the operation + operand_count = ( + len(cmd.args) // len(cmd.bits) - 1 + if op.basic_op.type == tk.OpType.ExplicitPredicate + else len(cmd.args) // len(cmd.bits) + ) + out = assign_cop( + # Converting to regwise operations that pecos can handle + [cmd.bits[0].reg_name], + [ + { + "cop": cop_from_op_name(op.basic_op.get_name()), + "args": [arg.reg_name for arg in cmd.args[:operand_count]], + } + ], + ) + case _: + raise NotImplementedError(op) + + return out + + +def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | None: + """Return PHIR dict given a tket op and its arguments.""" + if op.is_gate(): + return convert_gate(op, cmd) + + out: JsonDict | None = None + match op: # non-quantum op + case tk.Conditional(): # where the condition is equality check + out = { + "block": "if", + "condition": { + "cop": "==", + "args": [ + arg_to_bit(cmd.args[0]) + if op.width == 1 + else cmd.args[0].reg_name, + op.value, + ], + }, + "true_branch": [convert_subcmd(op.op, cmd)], + } + + case tk.BarrierOp(): + 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": + duration = op.data.removeprefix("sleep(").removesuffix(")") + out = { + "mop": "Idle", + "args": [arg_to_bit(qbit) for qbit in cmd.qubits], + "duration": (float(duration), "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.ClassicalExpBox(): exp = op.get_exp() @@ -296,65 +353,29 @@ def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | list[JsonDict] | No rhs = [classical_op(exp)] out = assign_cop([cmd.bits[0].reg_name], rhs) - case tk.SetBitsOp(): - if len(cmd.bits) != len(op.values): - logger.error("LHS and RHS lengths mismatch for classical assignment") - raise ValueError - out = assign_cop( - [arg_to_bit(bit) for bit in cmd.bits], list(map(int, op.values)) - ) - - case tk.CopyBitsOp(): - if len(cmd.bits) != len(cmd.args) // 2: - logger.warning("LHS and RHS lengths mismatch for CopyBits") - out = assign_cop( - [arg_to_bit(bit) for bit in cmd.bits], - [arg_to_bit(cmd.args[i]) for i in range(len(cmd.args) // 2)], - ) + case tk.ClassicalEvalOp(): + return convert_classicalevalop(op, cmd) case tk.WASMOp(): return create_wasm_op(cmd, op) - case tk.MultiBitOp(): - cop_name = cmd.op.basic_op.get_name() # type: ignore [attr-defined] - cop = BitWiseOp[cop_name] - # TODO(Asa Kosto): Add a PR to Pytket so this number is a property of cmd.op - # https://github.com/CQCL/tket/issues/1326 - multiplier = len(cmd.bits) - total_args = len(cmd.args) - args_per_op = total_args // multiplier - # create a list of 'sub-ops' that make up the MultiBitOp - # https://tket.quantinuum.com/api-docs/circuit.html#pytket.circuit.MultiBitOp - out = [] - for i in range(0, total_args, args_per_op): - args_for_sub_op = cmd.args[i : i + args_per_op] - target_bit = args_for_sub_op.pop() - log_exp = create_logic_exp(cop, args_for_sub_op) # type: ignore [arg-type] - rhs = [classical_op(log_exp, bitwise=True)] - out.append(assign_cop([arg_to_bit(target_bit)], rhs)) case _: + args = cmd.args[1:] if isinstance(cmd.op, tk.Conditional) else cmd.args match op.type: - case tk.OpType.ExplicitPredicate: - cop_name = op.get_name() - cop = BitWiseOp[cop_name] - # with conditionals, cmd.args contains all bits in the expression - # the first element in cmd.args will be the conditional bit - # parse out the bits that are relevant for the true branch - match cmd.op: - case tk.Conditional(): - bits = cmd.args[1:] - case _: - bits = cmd.args.copy() - target_bit = bits.pop() - log_exp = create_logic_exp(cop, bits) # type: ignore [arg-type] - rhs = [classical_op(log_exp, bitwise=True)] - out = assign_cop([arg_to_bit(target_bit)], rhs) - - case tk.OpType.ExplicitModifier: - raise NotImplementedError - + case tk.OpType.ExplicitPredicate | tk.OpType.ExplicitModifier: + # exclude outbit when not modifying in place + args = args[:-1] if op.type == tk.OpType.ExplicitPredicate else args + out = assign_cop( + [arg_to_bit(cmd.bits[0])], + [ + { + "cop": cop_from_op_name(op.get_name()), + "args": [arg_to_bit(arg) for arg in args], + } + ], + ) case _: - raise NotImplementedError + raise NotImplementedError(op.type) return out @@ -431,6 +452,8 @@ def make_comment_text(cmd: tk.Command, op: tk.Op) -> str: comment = f"WASM_function='{op.func_name}' args={args} returns={returns};" case tk.BarrierOp(): + if type(op) is not type(cmd.op): + logger.warning("cmd.args might carry a conditional bit") comment = op.data + " " + str(cmd.args[0]) + ";" if op.data else str(cmd) case tk.ClassicalExpBox(): diff --git a/tests/test_phirgen.py b/tests/test_phirgen.py index 0107748..e62f012 100644 --- a/tests/test_phirgen.py +++ b/tests/test_phirgen.py @@ -10,8 +10,6 @@ import json -import pytest - from pytket.circuit import Circuit from pytket.phir.api import pytket_to_phir from pytket.qasm.qasm import circuit_from_qasm_str @@ -206,7 +204,6 @@ def test_conditional_measure() -> None: } -@pytest.mark.order("first") def test_conditional_classical_not() -> None: """From https://github.com/CQCL/pytket-phir/issues/159 .""" circuit = Circuit() @@ -231,86 +228,93 @@ def test_conditional_classical_not() -> None: } -@pytest.mark.order("first") -def test_standard_classical_ops() -> None: - """Test classical ops added to the circuit via dedicated circuit.""" +def test_explicit_classical_ops() -> None: + """Test explicit predicates and modifiers.""" + # From https://github.com/CQCL/tket/blob/a2f6fab8a57da8787dfae94764b7c3a8e5779024/pytket/tests/classical_test.py#L97-L101 c = Circuit(0, 4) + # predicates c.add_c_and(1, 2, 3) - c.add_c_not(1, 2) - c.add_c_or(2, 1, 3) + c.add_c_not(0, 1) c.add_c_xor(1, 2, 3) + # modifiers + c.add_c_and(2, 3, 3) + c.add_c_or(0, 3, 0) phir = json.loads(pytket_to_phir(c)) - assert phir["ops"][-1] == { + assert phir["ops"][1] == {"//": "AND c[1], c[2], c[3];"} + assert phir["ops"][2] == { "cop": "=", "returns": [["c", 3]], - "args": [{"cop": "^", "args": [["c", 1], ["c", 2]]}], + "args": [{"cop": "&", "args": [["c", 1], ["c", 2]]}], } - assert phir["ops"][-3] == { + assert phir["ops"][3] == {"//": "NOT c[0], c[1];"} + assert phir["ops"][4] == { "cop": "=", - "returns": [["c", 3]], - "args": [{"cop": "|", "args": [["c", 2], ["c", 1]]}], + "returns": [["c", 1]], + "args": [{"cop": "~", "args": [["c", 0]]}], } - assert phir["ops"][-5] == { + assert phir["ops"][5] == {"//": "XOR c[1], c[2], c[3];"} + assert phir["ops"][6] == { "cop": "=", - "returns": [["c", 2]], - "args": [{"cop": "~", "args": [["c", 1]]}], + "returns": [["c", 3]], + "args": [{"cop": "^", "args": [["c", 1], ["c", 2]]}], } - assert phir["ops"][-7] == { + assert phir["ops"][7] == {"//": "AND c[2], c[3];"} + assert phir["ops"][8] == { "cop": "=", "returns": [["c", 3]], - "args": [{"cop": "&", "args": [["c", 1], ["c", 2]]}], + "args": [{"cop": "&", "args": [["c", 2], ["c", 3]]}], + } + assert phir["ops"][9] == {"//": "OR c[3], c[0];"} + assert phir["ops"][10] == { + "cop": "=", + "returns": [["c", 0]], + "args": [{"cop": "|", "args": [["c", 3], ["c", 0]]}], } -@pytest.mark.order("first") def test_multi_bit_ops() -> None: """Test classical ops added to the circuit via tket multi-bit ops.""" + # Test from https://github.com/CQCL/tket/blob/a2f6fab8a57da8787dfae94764b7c3a8e5779024/pytket/tests/classical_test.py#L107-L112 c = Circuit(0, 4) - c0 = c.add_c_register("c0", 2) - c1 = c.add_c_register("c1", 2) - c2 = c.add_c_register("c2", 2) + c0 = c.add_c_register("c0", 3) + c1 = c.add_c_register("c1", 4) + c2 = c.add_c_register("c2", 5) + # predicates c.add_c_and_to_registers(c0, c1, c2) - c.add_c_not_to_registers(c0, c1) + c.add_c_not_to_registers(c1, c2) c.add_c_or_to_registers(c0, c1, c2) - c.add_c_xor_to_registers(c0, c1, c2) + # modifier + c.add_c_xor_to_registers(c2, c1, c2) phir = json.loads(pytket_to_phir(c)) - assert phir["ops"][-1] == { - "cop": "=", - "returns": [["c2", 1]], - "args": [{"cop": "^", "args": [["c0", 1], ["c1", 1]]}], + assert phir["ops"][3] == { + "//": "AND (*3) c0[0], c1[0], c2[0], c0[1], c1[1], c2[1], c0[2], c1[2], c2[2];" } - assert phir["ops"][-2] == { + assert phir["ops"][4] == { "cop": "=", - "returns": [["c2", 0]], - "args": [{"cop": "^", "args": [["c0", 0], ["c1", 0]]}], + "returns": ["c2"], + "args": [{"cop": "&", "args": ["c0", "c1"]}], } - assert phir["ops"][-4] == { - "cop": "=", - "returns": [["c2", 1]], - "args": [{"cop": "|", "args": [["c0", 1], ["c1", 1]]}], + assert phir["ops"][5] == { + "//": "NOT (*4) c1[0], c2[0], c1[1], c2[1], c1[2], c2[2], c1[3], c2[3];" } - assert phir["ops"][-5] == { + assert phir["ops"][6] == { "cop": "=", - "returns": [["c2", 0]], - "args": [{"cop": "|", "args": [["c0", 0], ["c1", 0]]}], + "returns": ["c2"], + "args": [{"cop": "~", "args": ["c1"]}], } - assert phir["ops"][-7] == { - "cop": "=", - "returns": [["c1", 1]], - "args": [{"cop": "~", "args": [["c0", 1]]}], + assert phir["ops"][7] == { + "//": "OR (*3) c0[0], c1[0], c2[0], c0[1], c1[1], c2[1], c0[2], c1[2], c2[2];" } - assert phir["ops"][-8] == { + assert phir["ops"][8] == { "cop": "=", - "returns": [["c1", 0]], - "args": [{"cop": "~", "args": [["c0", 0]]}], + "returns": ["c2"], + "args": [{"cop": "|", "args": ["c0", "c1"]}], } - assert phir["ops"][-10] == { - "cop": "=", - "returns": [["c2", 1]], - "args": [{"cop": "&", "args": [["c0", 1], ["c1", 1]]}], + assert phir["ops"][9] == { + "//": "XOR (*4) c1[0], c2[0], c1[1], c2[1], c1[2], c2[2], c1[3], c2[3];" } - assert phir["ops"][-11] == { + assert phir["ops"][10] == { "cop": "=", - "returns": [["c2", 0]], - "args": [{"cop": "&", "args": [["c0", 0], ["c1", 0]]}], + "returns": ["c2"], + "args": [{"cop": "^", "args": ["c1", "c2"]}], } From 2642518992e64be21965d3650d26cdc109669531 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Fri, 5 Apr 2024 13:33:09 -0500 Subject: [PATCH 4/9] refactor: some cleanup --- pytket/phir/phirgen.py | 9 ++------- tests/test_api.py | 23 ----------------------- tests/test_phirgen.py | 25 ++++++++++++++++++++++++- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/pytket/phir/phirgen.py b/pytket/phir/phirgen.py index 565fb0a..71e9c8e 100644 --- a/pytket/phir/phirgen.py +++ b/pytket/phir/phirgen.py @@ -388,12 +388,9 @@ def append_cmd(cmd: tk.Command, ops: list[JsonDict]) -> None: ops: the list of ops to append to """ ops.append({"//": make_comment_text(cmd, cmd.op)}) - op: JsonDict | list[JsonDict] | None = convert_subcmd(cmd.op, cmd) + op: JsonDict | None = convert_subcmd(cmd.op, cmd) if op: - if type(op) is list: - ops.extend(op) - elif type(op) is dict: - ops.append(op) + ops.append(op) def create_wasm_op(cmd: tk.Command, wasm_op: tk.WASMOp) -> JsonDict: @@ -452,8 +449,6 @@ def make_comment_text(cmd: tk.Command, op: tk.Op) -> str: comment = f"WASM_function='{op.func_name}' args={args} returns={returns};" case tk.BarrierOp(): - if type(op) is not type(cmd.op): - logger.warning("cmd.args might carry a conditional bit") comment = op.data + " " + str(cmd.args[0]) + ";" if op.data else str(cmd) case tk.ClassicalExpBox(): diff --git a/tests/test_api.py b/tests/test_api.py index 84e65af..f0372ce 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -8,12 +8,10 @@ # mypy: disable-error-code="misc" -import json import logging import pytest -from pytket.circuit import Bit, Circuit from pytket.phir.api import pytket_to_phir, qasm_to_phir from pytket.phir.qtm_machine import QtmMachine @@ -50,27 +48,6 @@ def test_pytket_to_phir_h1_1_all(self, test_file: QasmFile) -> None: assert pytket_to_phir(circuit, QtmMachine.H1) - def test_pytket_classical_only(self) -> None: - c = Circuit(1) - a = c.add_c_register("a", 2) - b = c.add_c_register("b", 3) - - c.add_c_copyreg(a, b) - c.add_c_copybits([Bit("b", 2), Bit("a", 1)], [Bit("a", 0), Bit("b", 0)]) - - phir = json.loads(pytket_to_phir(c)) - - assert phir["ops"][3] == { - "cop": "=", - "returns": [["b", 0], ["b", 1]], - "args": [["a", 0], ["a", 1]], - } - assert phir["ops"][5] == { - "cop": "=", - "returns": [["a", 0], ["b", 0]], - "args": [["b", 2], ["a", 1]], - } - def test_qasm_to_phir(self) -> None: """Test the qasm string entrypoint works.""" qasm = """ diff --git a/tests/test_phirgen.py b/tests/test_phirgen.py index e62f012..b54d7a2 100644 --- a/tests/test_phirgen.py +++ b/tests/test_phirgen.py @@ -10,7 +10,7 @@ import json -from pytket.circuit import Circuit +from pytket.circuit import Bit, Circuit from pytket.phir.api import pytket_to_phir from pytket.qasm.qasm import circuit_from_qasm_str from pytket.unit_id import BitRegister @@ -18,6 +18,29 @@ from .test_utils import QasmFile, get_qasm_as_circuit +def test_pytket_classical_only() -> None: + """From https://github.com/CQCL/pytket-phir/issues/61 .""" + c = Circuit(1) + a = c.add_c_register("a", 2) + b = c.add_c_register("b", 3) + + c.add_c_copyreg(a, b) + c.add_c_copybits([Bit("b", 2), Bit("a", 1)], [Bit("a", 0), Bit("b", 0)]) + + phir = json.loads(pytket_to_phir(c)) + + assert phir["ops"][3] == { + "cop": "=", + "returns": [["b", 0], ["b", 1]], + "args": [["a", 0], ["a", 1]], + } + assert phir["ops"][5] == { + "cop": "=", + "returns": [["a", 0], ["b", 0]], + "args": [["b", 2], ["a", 1]], + } + + def test_classicalexpbox() -> None: """From https://github.com/CQCL/pytket-phir/issues/86 .""" circ = Circuit(1) From c15e16a205f6663c400a7f489d6c156e1ecf9ec9 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Fri, 5 Apr 2024 13:55:27 -0500 Subject: [PATCH 5/9] fix(phirgen): handle ops inside a conditional correctly --- pytket/phir/phirgen.py | 11 ++++++----- tests/test_phirgen.py | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/pytket/phir/phirgen.py b/pytket/phir/phirgen.py index 71e9c8e..672679b 100644 --- a/pytket/phir/phirgen.py +++ b/pytket/phir/phirgen.py @@ -236,8 +236,8 @@ def cop_from_op_name(op_name: str) -> str: def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict | None: """Return PHIR dict for a pytket ClassicalEvalOp.""" - if type(op) is not type(cmd.op): - logger.warning("cmd.args might carry a conditional bit") + # Exclude conditional bit from args + args = cmd.args[1:] if isinstance(cmd.op, tk.Conditional) else cmd.args out: JsonDict | None = None match op: case tk.CopyBitsOp(): @@ -245,7 +245,7 @@ def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict logger.warning("LHS and RHS lengths mismatch for CopyBits") out = assign_cop( [arg_to_bit(bit) for bit in cmd.bits], - [arg_to_bit(cmd.args[i]) for i in range(len(cmd.args) // 2)], + [arg_to_bit(args[i]) for i in range(len(cmd.args) // 2)], ) case tk.SetBitsOp(): if len(cmd.bits) != len(op.values): @@ -290,7 +290,7 @@ def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict [ { "cop": cop_from_op_name(op.basic_op.get_name()), - "args": [arg.reg_name for arg in cmd.args[:operand_count]], + "args": [arg.reg_name for arg in args[:operand_count]], } ], ) @@ -360,10 +360,11 @@ def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | None: return create_wasm_op(cmd, op) case _: + # Exclude conditional bit from args args = cmd.args[1:] if isinstance(cmd.op, tk.Conditional) else cmd.args match op.type: case tk.OpType.ExplicitPredicate | tk.OpType.ExplicitModifier: - # exclude outbit when not modifying in place + # exclude output bit when not modifying in place args = args[:-1] if op.type == tk.OpType.ExplicitPredicate else args out = assign_cop( [arg_to_bit(cmd.bits[0])], diff --git a/tests/test_phirgen.py b/tests/test_phirgen.py index b54d7a2..61c43b6 100644 --- a/tests/test_phirgen.py +++ b/tests/test_phirgen.py @@ -26,6 +26,9 @@ def test_pytket_classical_only() -> None: c.add_c_copyreg(a, b) c.add_c_copybits([Bit("b", 2), Bit("a", 1)], [Bit("a", 0), Bit("b", 0)]) + c.add_c_copybits( + [Bit("b", 2), Bit("a", 1)], [Bit("a", 0), Bit("b", 0)], condition=Bit("b", 1) + ) phir = json.loads(pytket_to_phir(c)) @@ -39,6 +42,13 @@ def test_pytket_classical_only() -> None: "returns": [["a", 0], ["b", 0]], "args": [["b", 2], ["a", 1]], } + assert phir["ops"][7] == { + "block": "if", + "condition": {"cop": "==", "args": [["b", 1], 1]}, + "true_branch": [ + {"cop": "=", "returns": [["a", 0], ["b", 0]], "args": [["b", 2], ["a", 1]]} + ], + } def test_classicalexpbox() -> None: @@ -308,6 +318,9 @@ def test_multi_bit_ops() -> None: c.add_c_or_to_registers(c0, c1, c2) # modifier c.add_c_xor_to_registers(c2, c1, c2) + # conditionals + c.add_c_not_to_registers(c1, c2, condition=Bit("c0", 0)) + c.add_c_not_to_registers(c1, c1, condition=Bit("c0", 0)) phir = json.loads(pytket_to_phir(c)) assert phir["ops"][3] == { "//": "AND (*3) c0[0], c1[0], c2[0], c0[1], c1[1], c2[1], c0[2], c1[2], c2[2];" @@ -341,3 +354,17 @@ def test_multi_bit_ops() -> None: "returns": ["c2"], "args": [{"cop": "^", "args": ["c1", "c2"]}], } + assert phir["ops"][12] == { + "block": "if", + "condition": {"cop": "==", "args": [["c0", 0], 1]}, + "true_branch": [ + {"cop": "=", "returns": ["c2"], "args": [{"cop": "~", "args": ["c1"]}]} + ], + } + assert phir["ops"][14] == { + "block": "if", + "condition": {"cop": "==", "args": [["c0", 0], 1]}, + "true_branch": [ + {"cop": "=", "returns": ["c1"], "args": [{"cop": "~", "args": ["c1"]}]} + ], + } From 53195fcde6f1270277800f7e924acd4cb745c4ce Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Mon, 8 Apr 2024 08:39:15 -0500 Subject: [PATCH 6/9] fix(phirgen): make conditionals more robust based on Alec's feedback --- .pre-commit-config.yaml | 2 +- pytket/phir/phirgen.py | 51 ++++++++++++++---------- tests/test_phirgen.py | 88 +++++++++++++++++++++++++++-------------- 3 files changed, 90 insertions(+), 51 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d1bcbd..60c8cd9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/pytket/phir/phirgen.py b/pytket/phir/phirgen.py index 672679b..d3edc98 100644 --- a/pytket/phir/phirgen.py +++ b/pytket/phir/phirgen.py @@ -237,15 +237,15 @@ def cop_from_op_name(op_name: str) -> str: def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict | None: """Return PHIR dict for a pytket ClassicalEvalOp.""" # Exclude conditional bit from args - args = cmd.args[1:] if isinstance(cmd.op, tk.Conditional) else cmd.args + args = cmd.args[cmd.op.width :] if isinstance(cmd.op, tk.Conditional) else cmd.args out: JsonDict | None = None match op: case tk.CopyBitsOp(): - if len(cmd.bits) != len(cmd.args) // 2: + if len(cmd.bits) != len(args) // 2: logger.warning("LHS and RHS lengths mismatch for CopyBits") out = assign_cop( [arg_to_bit(bit) for bit in cmd.bits], - [arg_to_bit(args[i]) for i in range(len(cmd.args) // 2)], + [arg_to_bit(args[i]) for i in range(len(args) // 2)], ) case tk.SetBitsOp(): if len(cmd.bits) != len(op.values): @@ -260,17 +260,17 @@ def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict case l, u if l == u: cond = { "cop": "==", - "args": [cmd.args[0].reg_name, u], + "args": [args[0].reg_name, u], } case l, u if u == UINTMAX: cond = { "cop": ">=", - "args": [cmd.args[0].reg_name, l], + "args": [args[0].reg_name, l], } case 0, u: cond = { "cop": "<=", - "args": [cmd.args[0].reg_name, u], + "args": [args[0].reg_name, u], } out = { "block": "if", @@ -280,9 +280,9 @@ def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict case tk.MultiBitOp(): # determine number of register operands involved in the operation operand_count = ( - len(cmd.args) // len(cmd.bits) - 1 + len(args) // len(cmd.bits) - 1 if op.basic_op.type == tk.OpType.ExplicitPredicate - else len(cmd.args) // len(cmd.bits) + else len(args) // len(cmd.bits) ) out = assign_cop( # Converting to regwise operations that pecos can handle @@ -300,6 +300,19 @@ def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict return out +def multi_bit_condition(args: "list[UnitID]", value: int) -> JsonDict: + """Construct bitwise condition.""" + return { + "cop": "&", + "args": [ + {"cop": "==", "args": [arg_to_bit(arg), bval]} + for (arg, bval) in zip( + args, map(int, f"{value:0{len(args)}b}"), strict=True + ) + ], + } + + def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | None: """Return PHIR dict given a tket op and its arguments.""" if op.is_gate(): @@ -307,18 +320,12 @@ def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | None: out: JsonDict | None = None match op: # non-quantum op - case tk.Conditional(): # where the condition is equality check + case tk.Conditional(): out = { "block": "if", - "condition": { - "cop": "==", - "args": [ - arg_to_bit(cmd.args[0]) - if op.width == 1 - else cmd.args[0].reg_name, - op.value, - ], - }, + "condition": {"cop": "==", "args": [arg_to_bit(cmd.args[0]), op.value]} + if op.width == 1 + else multi_bit_condition(cmd.args[: op.width], op.value), "true_branch": [convert_subcmd(op.op, cmd)], } @@ -360,8 +367,12 @@ def convert_subcmd(op: tk.Op, cmd: tk.Command) -> JsonDict | None: return create_wasm_op(cmd, op) case _: - # Exclude conditional bit from args - args = cmd.args[1:] if isinstance(cmd.op, tk.Conditional) else cmd.args + # Exclude conditional bits from args + args = ( + cmd.args[cmd.op.width :] + if isinstance(cmd.op, tk.Conditional) + else cmd.args + ) match op.type: case tk.OpType.ExplicitPredicate | tk.OpType.ExplicitModifier: # exclude output bit when not modifying in place diff --git a/tests/test_phirgen.py b/tests/test_phirgen.py index 61c43b6..bb56158 100644 --- a/tests/test_phirgen.py +++ b/tests/test_phirgen.py @@ -18,6 +18,35 @@ from .test_utils import QasmFile, get_qasm_as_circuit +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.0, "s"]} + assert phir["ops"][4] == {"mop": "Idle", "args": [["q", 1]], "duration": [2.0, "s"]} + + +def test_simple_cond_classical() -> None: + """Ensure conditional classical operation are correctly generated.""" + circ = get_qasm_as_circuit(QasmFile.simple_cond) + phir = json.loads(pytket_to_phir(circ)) + assert phir["ops"][-6] == {"//": "IF ([c[0]] == 1) THEN SetBits(1) z[0];"} + assert phir["ops"][-5] == { + "block": "if", + "condition": {"cop": "==", "args": [["c", 0], 1]}, + "true_branch": [{"cop": "=", "returns": [["z", 0]], "args": [1]}], + } + + def test_pytket_classical_only() -> None: """From https://github.com/CQCL/pytket-phir/issues/61 .""" c = Circuit(1) @@ -29,6 +58,12 @@ def test_pytket_classical_only() -> None: c.add_c_copybits( [Bit("b", 2), Bit("a", 1)], [Bit("a", 0), Bit("b", 0)], condition=Bit("b", 1) ) + c.add_c_copybits( + [Bit("a", 0), Bit("a", 1)], # type: ignore[list-item] # overloaded function + [Bit("b", 0), Bit("b", 1)], # type: ignore[list-item] # overloaded function + condition_bits=[Bit("b", 1), Bit("b", 2)], + condition_value=2, + ) phir = json.loads(pytket_to_phir(c)) @@ -49,6 +84,22 @@ def test_pytket_classical_only() -> None: {"cop": "=", "returns": [["a", 0], ["b", 0]], "args": [["b", 2], ["a", 1]]} ], } + assert phir["ops"][8] == { + "//": "IF ([b[1], b[2]] == 2) THEN CopyBits a[0], a[1], b[0], b[1];" + } + assert phir["ops"][9] == { + "block": "if", + "condition": { + "cop": "&", + "args": [ + {"cop": "==", "args": [["b", 1], 1]}, + {"cop": "==", "args": [["b", 2], 0]}, + ], + }, + "true_branch": [ + {"cop": "=", "returns": [["b", 0], ["b", 1]], "args": [["a", 0], ["a", 1]]} + ], + } def test_classicalexpbox() -> None: @@ -121,23 +172,17 @@ def test_conditional_barrier() -> None: assert phir["ops"][4] == {"//": "IF ([m[0], m[1]] == 0) THEN Barrier q[0], q[1];"} assert phir["ops"][5] == { "block": "if", - "condition": {"cop": "==", "args": ["m", 0]}, + "condition": { + "cop": "&", + "args": [ + {"cop": "==", "args": [["m", 0], 0]}, + {"cop": "==", "args": [["m", 1], 0]}, + ], + }, "true_branch": [{"meta": "barrier", "args": [["q", 0], ["q", 1]]}], } -def test_simple_cond_classical() -> None: - """Ensure conditional classical operation are correctly generated.""" - circ = get_qasm_as_circuit(QasmFile.simple_cond) - phir = json.loads(pytket_to_phir(circ)) - assert phir["ops"][-6] == {"//": "IF ([c[0]] == 1) THEN SetBits(1) z[0];"} - assert phir["ops"][-5] == { - "block": "if", - "condition": {"cop": "==", "args": [["c", 0], 1]}, - "true_branch": [{"cop": "=", "returns": [["z", 0]], "args": [1]}], - } - - def test_nested_bitwise_op() -> None: """From https://github.com/CQCL/pytket-phir/issues/133 .""" circ = Circuit(4) @@ -171,23 +216,6 @@ def test_sleep_idle() -> None: assert phir["ops"][7] == {"mop": "Idle", "args": [["q", 0]], "duration": [1.0, "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.0, "s"]} - assert phir["ops"][4] == {"mop": "Idle", "args": [["q", 1]], "duration": [2.0, "s"]} - - def test_reordering_classical_conditional() -> None: """From https://github.com/CQCL/pytket-phir/issues/150 .""" circuit = Circuit(1) From f7f2c2600a732385caf3874108c90c3f0e6fe7fc Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Tue, 9 Apr 2024 13:19:22 -0500 Subject: [PATCH 7/9] feat(phirgen): add support for irregular multibit ops --- .pre-commit-config.yaml | 2 +- pytket/phir/phirgen.py | 64 +++++++++++++++++++++++++++++++---------- tests/test_phirgen.py | 35 ++++++++++++++++++++++ 3 files changed, 85 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 60c8cd9..8a0cfbf 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.20.4 + rev: v1.20.5 hooks: - id: typos diff --git a/pytket/phir/phirgen.py b/pytket/phir/phirgen.py index d3edc98..a389883 100644 --- a/pytket/phir/phirgen.py +++ b/pytket/phir/phirgen.py @@ -10,6 +10,7 @@ import json import logging +from copy import deepcopy from importlib.metadata import version from typing import TYPE_CHECKING, Any, TypeAlias @@ -278,22 +279,55 @@ def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict "true_branch": [assign_cop([arg_to_bit(cmd.bits[0])], [1])], } case tk.MultiBitOp(): + cop = cop_from_op_name(op.basic_op.get_name()) + is_explicit = op.basic_op.type == tk.OpType.ExplicitPredicate + # determine number of register operands involved in the operation - operand_count = ( - len(args) // len(cmd.bits) - 1 - if op.basic_op.type == tk.OpType.ExplicitPredicate - else len(args) // len(cmd.bits) - ) - out = assign_cop( - # Converting to regwise operations that pecos can handle - [cmd.bits[0].reg_name], - [ - { - "cop": cop_from_op_name(op.basic_op.get_name()), - "args": [arg.reg_name for arg in args[:operand_count]], - } - ], - ) + operand_count = len(args) // len(cmd.bits) - (1 if is_explicit else 0) + + iters = [iter(args)] * (operand_count + (1 if is_explicit else 0)) + iter2 = deepcopy(iters) + + # Columns of expressions, e.g., + # AND (*2) a[0], b[0], c[0] + # , a[1], b[1], c[1] + # would be [(a[0], a[1]), (b[0], b[1]), (c[0], c[1])] + # and AND (*2) a[0], a[1], b[0] + # , b[1], c[0], c[1] + # would be [(a[0], b[1]), (a[1], c[0]), (b[0], c[1])] + cols = zip(*zip(*iters, strict=True), strict=True) + + if all( + all(col[0].reg_name == bit.reg_name for bit in col) for col in cols + ): # expression can be applied register-wise + out = assign_cop( + [cmd.bits[0].reg_name], + [ + { + "cop": cop, + "args": [arg.reg_name for arg in args[:operand_count]], + } + ], + ) + else: # apply a sequence of bit-wise ops + exps = zip(*iter2, strict=True) + out = { + "block": "sequence", + "ops": [ + assign_cop( + [arg_to_bit(bit)], + [ + { + "cop": cop, + "args": [ + arg_to_bit(arg) for arg in exp[:operand_count] + ], + } + ], + ) + for bit, exp in zip(cmd.bits, exps, strict=True) + ], + } case _: raise NotImplementedError(op) diff --git a/tests/test_phirgen.py b/tests/test_phirgen.py index bb56158..090c798 100644 --- a/tests/test_phirgen.py +++ b/tests/test_phirgen.py @@ -396,3 +396,38 @@ def test_multi_bit_ops() -> None: {"cop": "=", "returns": ["c1"], "args": [{"cop": "~", "args": ["c1"]}]} ], } + + +def test_irregular_multibit_ops() -> None: + """From https://github.com/CQCL/pytket-phir/pull/162#discussion_r1555807863 .""" + c = Circuit() + areg = c.add_c_register("a", 2) + breg = c.add_c_register("b", 2) + creg = c.add_c_register("c", 2) + c.add_c_and_to_registers(areg, breg, creg) + mbop = c.get_commands()[0].op + c.add_gate(mbop, [areg[0], areg[1], breg[0], breg[1], creg[0], creg[1]]) + + phir = json.loads(pytket_to_phir(c)) + assert phir["ops"][3] == {"//": "AND (*2) a[0], b[0], c[0], a[1], b[1], c[1];"} + assert phir["ops"][4] == { + "cop": "=", + "returns": ["c"], + "args": [{"cop": "&", "args": ["a", "b"]}], + } + assert phir["ops"][5] == {"//": "AND (*2) a[0], a[1], b[0], b[1], c[0], c[1];"} + assert phir["ops"][6] == { + "block": "sequence", + "ops": [ + { + "cop": "=", + "returns": [["b", 0]], + "args": [{"cop": "&", "args": [["a", 0], ["a", 1]]}], + }, + { + "cop": "=", + "returns": [["c", 1]], + "args": [{"cop": "&", "args": [["b", 1], ["c", 0]]}], + }, + ], + } From 367d3c10e6ea3d79554fd58597321045fef08cf2 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 10 Apr 2024 07:00:59 -0500 Subject: [PATCH 8/9] Update pytket/phir/phirgen.py Co-authored-by: Alec Edgington <54802828+cqc-alec@users.noreply.github.com> --- pytket/phir/phirgen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytket/phir/phirgen.py b/pytket/phir/phirgen.py index a389883..91e2000 100644 --- a/pytket/phir/phirgen.py +++ b/pytket/phir/phirgen.py @@ -237,7 +237,7 @@ def cop_from_op_name(op_name: str) -> str: def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict | None: """Return PHIR dict for a pytket ClassicalEvalOp.""" - # Exclude conditional bit from args + # Exclude conditional bits from args args = cmd.args[cmd.op.width :] if isinstance(cmd.op, tk.Conditional) else cmd.args out: JsonDict | None = None match op: From d0c968530beef3c05e51b8250e454be10870ace0 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Wed, 10 Apr 2024 09:26:22 -0500 Subject: [PATCH 9/9] fix(review): Changes suggested by Alec --- .pre-commit-config.yaml | 2 +- pytket/phir/phirgen.py | 13 +++++++++---- tests/test_phirgen.py | 6 +++--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a0cfbf..edbfe90 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.20.5 + rev: v1.20.7 hooks: - id: typos diff --git a/pytket/phir/phirgen.py b/pytket/phir/phirgen.py index 91e2000..0a9941a 100644 --- a/pytket/phir/phirgen.py +++ b/pytket/phir/phirgen.py @@ -243,7 +243,8 @@ def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict match op: case tk.CopyBitsOp(): if len(cmd.bits) != len(args) // 2: - logger.warning("LHS and RHS lengths mismatch for CopyBits") + msg = "LHS and RHS lengths mismatch for CopyBits" + raise TypeError(msg) out = assign_cop( [arg_to_bit(bit) for bit in cmd.bits], [arg_to_bit(args[i]) for i in range(len(args) // 2)], @@ -279,13 +280,17 @@ def convert_classicalevalop(op: tk.ClassicalEvalOp, cmd: tk.Command) -> JsonDict "true_branch": [assign_cop([arg_to_bit(cmd.bits[0])], [1])], } case tk.MultiBitOp(): + if len(args) % len(cmd.bits) != 0: + msg = "Input bit- and output bit lengths mismatch." + raise TypeError(msg) + cop = cop_from_op_name(op.basic_op.get_name()) is_explicit = op.basic_op.type == tk.OpType.ExplicitPredicate # determine number of register operands involved in the operation - operand_count = len(args) // len(cmd.bits) - (1 if is_explicit else 0) + operand_count = len(args) // len(cmd.bits) - is_explicit - iters = [iter(args)] * (operand_count + (1 if is_explicit else 0)) + iters = [iter(args)] * (operand_count + is_explicit) iter2 = deepcopy(iters) # Columns of expressions, e.g., @@ -341,7 +346,7 @@ def multi_bit_condition(args: "list[UnitID]", value: int) -> JsonDict: "args": [ {"cop": "==", "args": [arg_to_bit(arg), bval]} for (arg, bval) in zip( - args, map(int, f"{value:0{len(args)}b}"), strict=True + args[::-1], map(int, f"{value:0{len(args)}b}"), strict=True ) ], } diff --git a/tests/test_phirgen.py b/tests/test_phirgen.py index 090c798..9ec0669 100644 --- a/tests/test_phirgen.py +++ b/tests/test_phirgen.py @@ -92,8 +92,8 @@ def test_pytket_classical_only() -> None: "condition": { "cop": "&", "args": [ - {"cop": "==", "args": [["b", 1], 1]}, - {"cop": "==", "args": [["b", 2], 0]}, + {"cop": "==", "args": [["b", 2], 1]}, + {"cop": "==", "args": [["b", 1], 0]}, ], }, "true_branch": [ @@ -175,8 +175,8 @@ def test_conditional_barrier() -> None: "condition": { "cop": "&", "args": [ - {"cop": "==", "args": [["m", 0], 0]}, {"cop": "==", "args": [["m", 1], 0]}, + {"cop": "==", "args": [["m", 0], 0]}, ], }, "true_branch": [{"meta": "barrier", "args": [["q", 0], ["q", 1]]}],