diff --git a/docs/changelog.rst b/docs/changelog.rst index 675e35c9..350f4bac 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Unreleased ---------- +* Handle more multi-controlled gates in ``tk_to_qiskit`` and ``qiskit_to_tk`` converters (including CnY and CnZ). * Drop support for Python 3.8; add support for 3.11. 0.33.0 (December 2022) diff --git a/pytket/extensions/qiskit/qiskit_convert.py b/pytket/extensions/qiskit/qiskit_convert.py index d56d68c5..b5d5889d 100644 --- a/pytket/extensions/qiskit/qiskit_convert.py +++ b/pytket/extensions/qiskit/qiskit_convert.py @@ -53,7 +53,7 @@ ParameterExpression, Reset, ) -from qiskit.circuit.library import CRYGate, RYGate, MCMT, PauliEvolutionGate # type: ignore +from qiskit.circuit.library import CRYGate, RYGate, PauliEvolutionGate # type: ignore from qiskit.extensions.unitary import UnitaryGate # type: ignore from pytket.circuit import ( # type: ignore @@ -67,6 +67,7 @@ CustomGateDef, Bit, Qubit, + QControlBox, ) from pytket._tket.circuit import _TEMP_BIT_NAME # type: ignore from pytket.pauli import Pauli, QubitPauliString # type: ignore @@ -271,42 +272,59 @@ def circuit(self) -> Circuit: return self.tkc def add_qiskit_data(self, data: "QuantumCircuitData") -> None: - for i, qargs, cargs in data: + for instr, qargs, cargs in data: condition_kwargs = {} - if i.condition is not None: - cond_reg = self.cregmap[i.condition[0]] + if instr.condition is not None: + cond_reg = self.cregmap[instr.condition[0]] condition_kwargs = { "condition_bits": [cond_reg[k] for k in range(len(cond_reg))], - "condition_value": i.condition[1], + "condition_value": instr.condition[1], } optype = None - if type(i) == ControlledGate: - if type(i.base_gate) == qiskit_gates.RYGate: + if type(instr) == ControlledGate: + if type(instr.base_gate) == qiskit_gates.RYGate: optype = OpType.CnRy + elif type(instr.base_gate) == qiskit_gates.YGate: + optype = OpType.CnY + elif type(instr.base_gate) == qiskit_gates.ZGate: + optype = OpType.CnZ else: - # Maybe handle multicontrolled gates in a more general way, - # but for now just do CnRy - raise NotImplementedError( - "qiskit ControlledGate with " - + "base gate {} not implemented".format(i.base_gate) - ) - elif type(i) == PauliEvolutionGate: + if type(instr.base_gate) in _known_qiskit_gate: + optype = OpType.QControlBox # QControlBox case handled below + else: + raise NotImplementedError( + f"qiskit ControlledGate with base gate {instr.base_gate}" + + "not implemented" + ) + elif type(instr) == PauliEvolutionGate: pass # Special handling below else: - optype = _known_qiskit_gate[type(i)] + optype = _known_qiskit_gate[type(instr)] qubits = [self.qbmap[qbit] for qbit in qargs] bits = [self.cbmap[bit] for bit in cargs] if optype == OpType.Unitary2qBox: - u = i.to_matrix() + u = instr.to_matrix() ubox = Unitary2qBox(u) # Note reversal of qubits, to account for endianness (pytket unitaries # are ILO-BE == DLO-LE; qiskit unitaries are ILO-LE == DLO-BE). self.tkc.add_unitary2qbox( ubox, qubits[1], qubits[0], **condition_kwargs ) - elif type(i) == PauliEvolutionGate: - qpo = _qpo_from_peg(i, qubits) + elif optype == OpType.QControlBox: + base_tket_gate = _known_qiskit_gate[type(instr.base_gate)] + params = [param_to_tk(p) for p in instr.base_gate.params] + n_base_qubits = instr.base_gate.num_qubits + sub_circ = Circuit(n_base_qubits) + # use base gate name for the CircBox (shows in renderer) + sub_circ.name = instr.base_gate.name.capitalize() + sub_circ.add_gate(base_tket_gate, params, list(range(n_base_qubits))) + c_box = CircBox(sub_circ) + q_ctrl_box = QControlBox(c_box, instr.num_ctrl_qubits) + self.tkc.add_qcontrolbox(q_ctrl_box, qubits) + + elif type(instr) == PauliEvolutionGate: + qpo = _qpo_from_peg(instr, qubits) empty_circ = Circuit(len(qargs)) circ = gen_term_sequence_circuit(qpo, empty_circ) ccbox = CircBox(circ) @@ -314,12 +332,18 @@ def add_qiskit_data(self, data: "QuantumCircuitData") -> None: elif optype == OpType.Barrier: self.tkc.add_barrier(qubits) elif optype in (OpType.CircBox, OpType.CustomGate): - qregs = [QuantumRegister(i.num_qubits, "q")] if i.num_qubits > 0 else [] + qregs = ( + [QuantumRegister(instr.num_qubits, "q")] + if instr.num_qubits > 0 + else [] + ) cregs = ( - [ClassicalRegister(i.num_clbits, "c")] if i.num_clbits > 0 else [] + [ClassicalRegister(instr.num_clbits, "c")] + if instr.num_clbits > 0 + else [] ) builder = CircuitBuilder(qregs, cregs) - builder.add_qiskit_data(i.definition) + builder.add_qiskit_data(instr.definition) subc = builder.circuit() if optype == OpType.CircBox: cbox = CircBox(subc) @@ -327,23 +351,23 @@ def add_qiskit_data(self, data: "QuantumCircuitData") -> None: else: # warning, this will catch all `Gate` instances # that were not picked up as a subclass in _known_qiskit_gate - params = [param_to_tk(p) for p in i.params] + params = [param_to_tk(p) for p in instr.params] gate_def = CustomGateDef.define( - i.name, subc, list(subc.free_symbols()) + instr.name, subc, list(subc.free_symbols()) ) self.tkc.add_custom_gate(gate_def, params, qubits + bits) - elif optype == OpType.CU3 and type(i) == qiskit_gates.CUGate: - if i.params[-1] == 0: + elif optype == OpType.CU3 and type(instr) == qiskit_gates.CUGate: + if instr.params[-1] == 0: self.tkc.add_gate( optype, - [param_to_tk(p) for p in i.params[:-1]], + [param_to_tk(p) for p in instr.params[:-1]], qubits, **condition_kwargs, ) else: raise NotImplementedError("CUGate with nonzero phase") else: - params = [param_to_tk(p) for p in i.params] + params = [param_to_tk(p) for p in instr.params] self.tkc.add_gate(optype, params, qubits + bits, **condition_kwargs) @@ -500,8 +524,10 @@ def append_tk_command_to_qiskit( qargs = [qregmap[q.reg_name][q.index[0]] for q in args] if optype == OpType.CnX: return qcirc.mcx(qargs[:-1], qargs[-1]) - - # special case + if optype == OpType.CnY: + return qcirc.append(qiskit_gates.YGate().control(len(qargs) - 1), qargs) + if optype == OpType.CnZ: + return qcirc.append(qiskit_gates.ZGate().control(len(qargs) - 1), qargs) if optype == OpType.CnRy: # might as well do a bit more checking assert len(op.params) == 1 @@ -511,10 +537,7 @@ def append_tk_command_to_qiskit( # presumably more efficient; single control only new_gate = CRYGate(alpha) else: - new_ry_gate = RYGate(alpha) - new_gate = MCMT( - gate=new_ry_gate, num_ctrl_qubits=len(qargs) - 1, num_target_qubits=1 - ) + new_gate = RYGate(alpha).control(len(qargs) - 1) qcirc.append(new_gate, qargs) return qcirc @@ -556,6 +579,12 @@ def append_tk_command_to_qiskit( # The set of tket gates that can be converted directly to qiskit gates _supported_tket_gates = set(_known_gate_rev_phase.keys()) +_additional_multi_controlled_gates = {OpType.CnY, OpType.CnZ, OpType.CnRy} + +# tket gates which are protected from being decomposed in the rebase +_protected_tket_gates = _supported_tket_gates | _additional_multi_controlled_gates + + Param = Union[float, "sympy.Expr"] # Type for TK1 and U3 parameters # Use the U3 gate for tk1_replacement as this is a member of _supported_tket_gates @@ -566,7 +595,7 @@ def _tk1_to_u3(a: Param, b: Param, c: Param) -> Circuit: # This is a rebase to the set of tket gates which have an exact substitution in qiskit -supported_gate_rebase = RebaseCustom(_supported_tket_gates, _cx_replacement, _tk1_to_u3) +supported_gate_rebase = RebaseCustom(_protected_tket_gates, _cx_replacement, _tk1_to_u3) def tk_to_qiskit( diff --git a/tests/qiskit_convert_test.py b/tests/qiskit_convert_test.py index 1aa6e58c..9c080a9b 100644 --- a/tests/qiskit_convert_test.py +++ b/tests/qiskit_convert_test.py @@ -31,6 +31,7 @@ from qiskit.quantum_info import Pauli # type: ignore from qiskit.transpiler import PassManager # type: ignore from qiskit.circuit.library import RYGate, MCMT # type: ignore +import qiskit.circuit.library.standard_gates as qiskit_gates # type: ignore from qiskit.circuit import Parameter # type: ignore from pytket.circuit import ( # type: ignore Circuit, @@ -729,3 +730,44 @@ def test_parametrized_evolution() -> None: qc: QuantumCircuit = evolved_circ_op.primitive tk_qc: Circuit = qiskit_to_tk(qc) assert len(tk_qc.free_symbols()) == 1 + + +def test_multicontrolled_gate_conversion() -> None: + my_qc = QuantumCircuit(4) + my_qc.append(qiskit_gates.YGate().control(3), [0, 1, 2, 3]) + my_qc.append(qiskit_gates.RYGate(0.25).control(3), [0, 1, 2, 3]) + my_qc.append(qiskit_gates.ZGate().control(3), [0, 1, 2, 3]) + my_tkc = qiskit_to_tk(my_qc) + my_tkc.add_gate(OpType.CnRy, [0.95], [0, 1, 2, 3]) + my_tkc.add_gate(OpType.CnZ, [1, 2, 3, 0]) + my_tkc.add_gate(OpType.CnY, [0, 1, 3, 2]) + unitary_before = my_tkc.get_unitary() + assert my_tkc.n_gates_of_type(OpType.CnY) == 2 + assert my_tkc.n_gates_of_type(OpType.CnZ) == 2 + assert my_tkc.n_gates_of_type(OpType.CnRy) == 2 + my_new_qc = tk_to_qiskit(my_tkc) + qiskit_ops = my_new_qc.count_ops() + assert qiskit_ops["c3y"] and qiskit_ops["c3z"] and qiskit_ops["c3ry"] == 2 + tcirc = qiskit_to_tk(my_new_qc) + unitary_after = tcirc.get_unitary() + assert compare_unitaries(unitary_before, unitary_after) + + +def test_qcontrolbox_conversion() -> None: + qr = QuantumRegister(3) + qc = QuantumCircuit(qr) + c2h_gate = qiskit_gates.HGate().control(2) + qc.append(c2h_gate, qr) + c = qiskit_to_tk(qc) + assert c.n_gates == 1 + assert c.n_gates_of_type(OpType.QControlBox) == 1 + c3rx_gate = qiskit_gates.RXGate(0.7).control(3) + c3rz_gate = qiskit_gates.RZGate(pi / 4).control(3) + c2rzz_gate = qiskit_gates.RZZGate(pi / 3).control(2) + qc2 = QuantumCircuit(4) + qc2.append(c3rz_gate, [0, 1, 3, 2]) + qc2.append(c3rx_gate, [0, 1, 2, 3]) + qc2.append(c2rzz_gate, [0, 1, 2, 3]) + tkc2 = qiskit_to_tk(qc2) + assert tkc2.n_gates == 3 + assert tkc2.n_gates_of_type(OpType.QControlBox) == 3