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

Feature/convert more controlled gates #50

Merged
merged 11 commits into from
Jan 10, 2023
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
97 changes: 63 additions & 34 deletions pytket/extensions/qiskit/qiskit_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -271,79 +272,102 @@ 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)))
Copy link
Contributor Author

@CalMacCQ CalMacCQ Jan 3, 2023

Choose a reason for hiding this comment

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

Do you think that adding the base_tket_gate to the list given by list(range(n_base_qubits) is correct here? I guess I have implicitly assumed that the base gate acts on the base qubits symmetrically.

i.e. sub_circ.add_gate(base_tket_gate, params, [0, 1])) is equivalent to sub_circ.add_gate(base_tket_gate, params, [1, 0 ]))

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes I think it is correct. The order of qubits is handled below.

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)
self.tkc.add_circbox(ccbox, qubits)
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)
self.tkc.add_circbox(cbox, qubits + bits, **condition_kwargs)
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)


Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
42 changes: 42 additions & 0 deletions tests/qiskit_convert_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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