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

feat: Improved support for IfElseOp in qiskit_to_tk #463

Merged
merged 17 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

# Changelog

## 0.65.0 (UNRELEASED)

## 0.64.0 (March 2025)
- Improve {py:func}`qiskit_to_tk` to handle `IfElseOp` (generated by `QuantumCircuit.if_test`)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be under a new "Unreleased" heading above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, I mistakenly thought v0.64 was not out yet. Will fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed 493b3a4

with register conditions. Only conditions on an entire register are supported.


## 0.64.0

- Update pytket version requirement to 2.0.1.

Expand Down
84 changes: 43 additions & 41 deletions pytket/extensions/qiskit/qiskit_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
Unitary3qBox,
UnitType,
)
from pytket.circuit.logic_exp import reg_eq, reg_neq
from pytket.passes import AutoRebase
from pytket.pauli import Pauli, QubitPauliString
from pytket.unit_id import _TEMP_BIT_NAME
Expand Down Expand Up @@ -98,6 +99,7 @@
from qiskit_ibm_runtime.models.backend_properties import Nduv

from pytket.circuit import UnitID
from pytket.unit_id import BitRegister
from qiskit.circuit.quantumcircuitdata import QuantumCircuitData # type: ignore

_qiskit_gates_1q = {
Expand Down Expand Up @@ -518,41 +520,49 @@ def _build_if_else_circuit(
qubits: list[Qubit],
bits: list[Bit],
) -> Circuit:
# Coniditions must be on a single bit (for now) TODO: support multiple bits.
if len(bits) == 1:
# Get two CircBox objects which implement the true_body and false_body.
if_box, else_box = _pytket_boxes_from_ifelseop(if_else_op, qregs, cregs)
# else_box can be None if no false_body is specified.
circ_builder = CircuitBuilder(qregs, cregs)
circ = circ_builder.circuit()
else:
raise NotImplementedError("Conditions over multiple bits not yet supported.")

# Coniditions must be on a single bit (for now)
if not isinstance(if_else_op.condition[0], Clbit):
raise NotImplementedError(
"Handling of register conditions is not yet supported"
# Get two CircBox objects which implement the true_body and false_body.
if_box, else_box = _pytket_boxes_from_ifelseop(if_else_op, qregs, cregs)
# else_box can be None if no false_body is specified.
circ_builder = CircuitBuilder(qregs, cregs)
circ = circ_builder.circuit()

if isinstance(if_else_op.condition[0], Clbit):
if len(bits) != 1:
raise NotImplementedError("Conditions on multiple bits 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.

Is there an issues / is this something we want to add in the future?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Possibly but it may be more annoying to handle and maintain. I'm not certain on the limits on what you can express with QuantumCircuit.if_test (I should check).

I think for now we should stick with single bit conditions and entire register command conditions. If theres a demand for different cases then we can decide to support that.

circ.add_circbox(
circbox=if_box,
args=qubits,
condition_bits=bits,
condition_value=if_else_op.condition[1],
)

circ.add_circbox(
circbox=if_box,
args=qubits,
condition_bits=bits,
condition_value=if_else_op.condition[1],
)
# If we have an else_box defined, add it to the circuit
if else_box is not None:
if if_else_op.condition[1] not in {0, 1}:
raise ValueError(
"A bit must have condition value 0 or 1"
+ f", got {if_else_op.condition[1]}"
# If we have an else_box defined, add it to the circuit
if else_box is not None:
circ.add_circbox(
circbox=else_box,
args=qubits,
condition_bits=bits,
condition_value=1 ^ if_else_op.condition[1],
)

elif isinstance(if_else_op.condition[0], ClassicalRegister):
pytket_bit_reg: BitRegister = circ.get_c_register(if_else_op.condition[0].name)
circ.add_circbox(
circbox=else_box,
circbox=if_box,
args=qubits,
condition_bits=bits,
condition_value=1 ^ if_else_op.condition[1],
condition=reg_eq(pytket_bit_reg, if_else_op.condition[1]),
)
if else_box is not None:
circ.add_circbox(
circbox=else_box,
args=qubits,
condition=reg_neq(pytket_bit_reg, if_else_op.condition[1]),
)
else:
raise TypeError(
"Unrecognized type used to construct IfElseOp. Expected "
+ f"ClBit or ClassicalRegister, got {type(if_else_op.condition[0])}"
)

return circ


Expand Down Expand Up @@ -620,6 +630,8 @@ def add_qiskit_data(
# Append OpType found by stateprep helpers
_add_state_preparation(self.tkc, qubits, instr)

# Note: These IfElseOp/if_test type conditions are only handled
# for single bit conditions and conditions on entire registers.
elif type(instr) is IfElseOp:
if_else_circ = _build_if_else_circuit(
if_else_op=instr,
Expand Down Expand Up @@ -954,12 +966,6 @@ def append_tk_command_to_qiskit(
supported_gate_rebase = AutoRebase(_protected_tket_gates)


def _has_implicit_permutation(circ: Circuit) -> bool:
"""Returns True if a Circuit has a non-trivial permutation
of qubits, false otherwise."""
return any(q0 != q1 for q0, q1 in circ.implicit_qubit_permutation().items())


def tk_to_qiskit(
tkcirc: Circuit,
replace_implicit_swaps: bool = False,
Expand Down Expand Up @@ -987,11 +993,7 @@ def tk_to_qiskit(
if replace_implicit_swaps:
tkc.replace_implicit_wire_swaps()

if (
_has_implicit_permutation(tkcirc)
and perm_warning
and not replace_implicit_swaps
):
if tkcirc.has_implicit_wireswaps and perm_warning and not replace_implicit_swaps:
warnings.warn(
"The pytket Circuit contains implicit qubit permutations"
+ " which aren't handled by default."
Expand Down
98 changes: 93 additions & 5 deletions tests/qiskit_convert_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
Unitary2qBox,
Unitary3qBox,
reg_eq,
reg_neq,
)
from pytket.extensions.qiskit import IBMQBackend, qiskit_to_tk, tk_to_qiskit
from pytket.extensions.qiskit.backends import (
Expand Down Expand Up @@ -1274,7 +1275,61 @@ def test_ifelseop_one_branch() -> None:
assert tket_circ_if_else == expected_circ


def test_ifelseop_multi_bit_cond() -> None:
# https://github.com/CQCL/pytket-qiskit/issues/452
def test_ifelseop_reg_cond_if() -> None:
qreg = QuantumRegister(3, "q")
creg = ClassicalRegister(3, "c")
circuit = QuantumCircuit(creg, qreg)
(q0, q1, q2) = qreg
(c0, c1, c2) = creg
circuit.h(q0)
circuit.h(q1)
circuit.h(q2)
circuit.measure(q0, c0)
circuit.measure(q1, c1)
circuit.measure(q2, c2)
# Condition is on a register not a bit
with circuit.if_test((creg, 2)):
circuit.x(q0)
circuit.y(q1)
circuit.z(q2)
circuit.measure(q0, c0)
circuit.measure(q1, c1)
circuit.measure(q2, c2)

tkc: Circuit = qiskit_to_tk(circuit)
tkc.name = "test_circ"

expected_circ = Circuit()
expected_circ.name = "test_circ"
qreg_tk = expected_circ.add_q_register("q", 3)
creg_tk = expected_circ.add_c_register("c", 3)
expected_circ.H(qreg_tk[0])
expected_circ.H(qreg_tk[1])
expected_circ.H(qreg_tk[2])
expected_circ.Measure(qreg_tk[0], creg_tk[0])
expected_circ.Measure(qreg_tk[1], creg_tk[1])
expected_circ.Measure(qreg_tk[2], creg_tk[2])

pauli_circ = Circuit()
pauli_circ.name = "If"
pauli_qreg = pauli_circ.add_q_register("q", 3)
pauli_circ.X(pauli_qreg[0]).Y(pauli_qreg[1]).Z(pauli_qreg[2])
expected_circ.add_circbox(
CircBox(pauli_circ),
[qreg_tk[0], qreg_tk[1], qreg_tk[2]],
condition=reg_eq(creg_tk, 2),
)

expected_circ.Measure(qreg_tk[0], creg_tk[0])
expected_circ.Measure(qreg_tk[1], creg_tk[1])
expected_circ.Measure(qreg_tk[2], creg_tk[2])

assert expected_circ == tkc


# https://github.com/CQCL/pytket-qiskit/issues/452
def test_ifelseop_reg_cond_if_else() -> None:
qreg = QuantumRegister(2, "q")
creg = ClassicalRegister(2, "c")
circuit = QuantumCircuit(creg, qreg)
Expand All @@ -1285,14 +1340,47 @@ def test_ifelseop_multi_bit_cond() -> None:
circuit.h(q1)
circuit.measure(q0, c0)
circuit.measure(q1, c1)
with circuit.if_test((creg, 2)):
# Condition is on a register not a bit
with circuit.if_test((creg, 2)) as else_:
circuit.x(q0)
circuit.x(q1)
with else_:
circuit.y(q0)
circuit.y(q1)
circuit.measure(q0, c0)
circuit.measure(q1, c1)
# This currently gives an error as register exp not supported.
with pytest.raises(NotImplementedError):
qiskit_to_tk(circuit)
tkc: Circuit = qiskit_to_tk(circuit)
tkc.name = "test_circ"

expected_circ = Circuit()
expected_circ.name = "test_circ"
qreg_tk = expected_circ.add_q_register("q", 2)
creg_tk = expected_circ.add_c_register("c", 2)
expected_circ.H(qreg_tk[0])
expected_circ.H(qreg_tk[1])
expected_circ.Measure(qreg_tk[0], creg_tk[0])
expected_circ.Measure(qreg_tk[1], creg_tk[1])

x_circ2 = Circuit()
x_circ2.name = "If"
x_qreg = x_circ2.add_q_register("q", 2)
x_circ2.X(x_qreg[0]).X(x_qreg[1])
expected_circ.add_circbox(
CircBox(x_circ2), [qreg_tk[0], qreg_tk[1]], condition=reg_eq(creg_tk, 2)
)

y_circ2 = Circuit()
y_circ2.name = "Else"
y_qreg = y_circ2.add_q_register("q", 2)
y_circ2.Y(y_qreg[0]).Y(y_qreg[1])
expected_circ.add_circbox(
CircBox(y_circ2), [qreg_tk[0], qreg_tk[1]], condition=reg_neq(creg_tk, 2)
)

expected_circ.Measure(qreg_tk[0], creg_tk[0])
expected_circ.Measure(qreg_tk[1], creg_tk[1])

assert expected_circ == tkc

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we add one more test where there is no else condition to make sure that works?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good idea. A test for a single branch using more qubits/bits and a different circuit.


def test_range_preds_with_conditionals() -> None:
Expand Down