diff --git a/docs/changelog.md b/docs/changelog.md index 347022a4..a29c105f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,8 +4,9 @@ # Changelog -## Unreleased +## 0.63.0 (January 2025) - UNRELEASED +- Support conversion of qiskit circuits containing `IfElseOp` in the {py:func}`qiskit_to_tk` converter. Note: Only conditions on a single bit are supported currently. Handling of more general conditions will be added in future. - Reject circuits containing nested conditionals when converting to qiskit. - Update pytket version requirement to 1.38.0. - Add support for fractional gates on backends that support them. diff --git a/pytket/extensions/qiskit/qiskit_convert.py b/pytket/extensions/qiskit/qiskit_convert.py index 112a279c..c4e79a9a 100644 --- a/pytket/extensions/qiskit/qiskit_convert.py +++ b/pytket/extensions/qiskit/qiskit_convert.py @@ -77,6 +77,7 @@ Clbit, ControlledGate, Gate, + IfElseOp, Instruction, InstructionSet, Measure, @@ -313,7 +314,7 @@ def _all_bits_set(integer: int, n_bits: int) -> bool: def _get_controlled_tket_optype(c_gate: ControlledGate) -> OpType: - """Get a pytket contolled OpType from a qiskit ControlledGate.""" + """Get a pytket controlled OpType from a qiskit ControlledGate.""" # If the control state is not "all |1>", use QControlBox if not _all_bits_set(c_gate.ctrl_state, c_gate.num_ctrl_qubits): @@ -480,6 +481,82 @@ def _build_circbox(instr: Instruction, circuit: QuantumCircuit) -> CircBox: return CircBox(subc) +# Used for handling of IfElseOp +# docs -> https://docs.quantum.ibm.com/api/qiskit/qiskit.circuit.IfElseOp +# Examples -> https://docs.quantum.ibm.com/guides/classical-feedforward-and-control-flow +# pytket-qiskit issue -> https://github.com/CQCL/pytket-qiskit/issues/415 +def _pytket_boxes_from_ifelseop( + if_else_op: IfElseOp, qregs: list[QuantumRegister], cregs: list[ClassicalRegister] +) -> tuple[CircBox, Optional[CircBox]]: + # Extract the QuantumCircuit implementing true_body + if_qc: QuantumCircuit = if_else_op.blocks[0] + if_builder = CircuitBuilder(qregs, cregs) + if_builder.add_qiskit_data(if_qc) + if_circuit = if_builder.circuit() + if_circuit.name = "If" + # Remove blank wires to ensure CircBox is the correct size. + if_circuit.remove_blank_wires() + + # The false_body arg is optional + if len(if_else_op.blocks) == 2: + else_qc: QuantumCircuit = if_else_op.blocks[1] + else_builder = CircuitBuilder(qregs, cregs) + else_builder.add_qiskit_data(else_qc) + else_circuit = else_builder.circuit() + else_circuit.name = "Else" + else_circuit.remove_blank_wires() + return CircBox(if_circuit), CircBox(else_circuit) + + # If no false_body is specified IfElseOp.blocks is of length 1. + # In this case we return a CircBox implementing true_body and None. + return CircBox(if_circuit), None + + +def _build_if_else_circuit( + if_else_op: IfElseOp, + qregs: list[QuantumRegister], + cregs: list[ClassicalRegister], + 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" + ) + + 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]}" + ) + circ.add_circbox( + circbox=else_box, + args=qubits, + condition_bits=bits, + condition_value=1 ^ if_else_op.condition[1], + ) + return circ + + class CircuitBuilder: def __init__( self, @@ -523,7 +600,7 @@ def add_qiskit_data( bits: list[Bit] = [self.cbmap[bit] for bit in cargs] condition_kwargs = {} - if instr.condition is not None: + if instr.condition is not None and type(instr) is not IfElseOp: condition_kwargs = _get_pytket_condition_kwargs( instruction=instr, cregmap=self.cregmap, @@ -531,8 +608,8 @@ def add_qiskit_data( ) optype = None - if type(instr) not in (PauliEvolutionGate, UnitaryGate): - # Handling of PauliEvolutionGate and UnitaryGate below + if type(instr) not in (PauliEvolutionGate, UnitaryGate, IfElseOp): + # Handling of PauliEvolutionGate, UnitaryGate and IfElseOp below optype = _optype_from_qiskit_instruction(instruction=instr) if optype == OpType.QControlBox: @@ -544,6 +621,16 @@ def add_qiskit_data( # Append OpType found by stateprep helpers _add_state_preparation(self.tkc, qubits, instr) + elif type(instr) is IfElseOp: + if_else_circ = _build_if_else_circuit( + if_else_op=instr, + qregs=self.qregs, + cregs=self.cregs, + qubits=qubits, + bits=bits, + ) + self.tkc.append(if_else_circ) + elif type(instr) is PauliEvolutionGate: qpo = _qpo_from_peg(instr, qubits) empty_circ = Circuit(len(qargs)) diff --git a/tests/qiskit_convert_test.py b/tests/qiskit_convert_test.py index d0063fa2..4886b530 100644 --- a/tests/qiskit_convert_test.py +++ b/tests/qiskit_convert_test.py @@ -1188,6 +1188,113 @@ def test_nonregister_bits() -> None: tk_to_qiskit(c) +# https://github.com/CQCL/pytket-qiskit/issues/415 +def test_ifelseop_two_branches() -> None: + qreg = QuantumRegister(1, "r") + creg = ClassicalRegister(1, "s") + circuit = QuantumCircuit(qreg, creg) + + circuit.h(qreg[0]) + circuit.measure(qreg[0], creg[0]) + + with circuit.if_test((creg[0], 1)) as else_: + circuit.h(qreg[0]) + with else_: + circuit.x(qreg[0]) + circuit.measure(qreg[0], creg[0]) + + tkc = qiskit_to_tk(circuit) + tkc.name = "test_circ" + + # Manually build the expected pytket Circuit. + # Validate against tkc. + expected_circ = Circuit(name="test_circ") + r_reg = expected_circ.add_q_register("r", 1) + s_reg = expected_circ.add_c_register("s", 1) + expected_circ.H(r_reg[0]) + expected_circ.Measure(r_reg[0], s_reg[0]) + + h_circ = Circuit() + h_reg = h_circ.add_q_register("r", 1) + h_circ.name = "If" + h_circ.H(h_reg[0]) + + x_circ = Circuit() + x_reg = x_circ.add_q_register("r", 1) + x_circ.name = "Else" + x_circ.X(x_reg[0]) + + expected_circ.add_circbox( + CircBox(h_circ), [r_reg[0]], condition_bits=[s_reg[0]], condition_value=1 + ) + expected_circ.add_circbox( + CircBox(x_circ), [r_reg[0]], condition_bits=[s_reg[0]], condition_value=0 + ) + + expected_circ.Measure(r_reg[0], s_reg[0]) + + assert expected_circ == tkc + + +# https://github.com/CQCL/pytket-qiskit/issues/415 +def test_ifelseop_one_branch() -> None: + qubits = QuantumRegister(1, "q1") + clbits = ClassicalRegister(1, "c1") + circuit = QuantumCircuit(qubits, clbits) + (q0,) = qubits + (c0,) = clbits + + circuit.h(q0) + circuit.measure(q0, c0) + with circuit.if_test((c0, 1)): + circuit.x(q0) + circuit.measure(q0, c0) + + tket_circ_if_else = qiskit_to_tk(circuit) + tket_circ_if_else.name = "test_circ" + + # Manually build the expected pytket Circuit. + # Validate against tket_circ_if_else. + expected_circ = Circuit() + expected_circ.name = "test_circ" + q1_tk = expected_circ.add_q_register("q1", 1) + c1_tk = expected_circ.add_c_register("c1", 1) + expected_circ.H(q1_tk[0]) + expected_circ.Measure(q1_tk[0], c1_tk[0]) + x_circ = Circuit() + x_circ.name = "If" + xq1 = x_circ.add_q_register("q1", 1) + x_circ.X(xq1[0]) + expected_circ.add_circbox( + CircBox(x_circ), [q1_tk[0]], condition_bits=[c1_tk[0]], condition_value=1 + ) + + expected_circ.Measure(q1_tk[0], c1_tk[0]) + + assert tket_circ_if_else == expected_circ + + +def test_ifelseop_multi_bit_cond() -> None: + qreg = QuantumRegister(2, "q") + creg = ClassicalRegister(2, "c") + circuit = QuantumCircuit(creg, qreg) + (q0, q1) = qreg + (c0, c1) = creg + + circuit.h(q0) + circuit.h(q1) + circuit.measure(q0, c0) + circuit.measure(q1, c1) + with circuit.if_test((creg, 2)): + circuit.x(q0) + circuit.x(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) + + def test_range_preds_with_conditionals() -> None: # https://github.com/CQCL/pytket-qiskit/issues/375 c = Circuit(1, 1)