Skip to content

Commit e148bd8

Browse files
authored
feat: Improved handling of controlled gates in converters (#391)
* simple converter function * start adding control state handling * implement control_state handling in qiskit_to_tk * remove unecessary if-else * use a list comprehension * annotate helper function call * refactor qcontrolbox building out of _add_qiskit_data * update controlbox utility function * fix ordering in Unitary boxes * get rid of Union typing * use helper function to handle UnitaryGate instances * fix handling for all UnitaryGate instances * remove original UnitaryGate helper function * helper function takes a unitary as an argument not a UnitaryGate * add assert and a comment * Support QControlBox in tk_to_qiskit converter * add test for QControlBox conversion * shorten line to make pylint happy * check unitary equivalence in test * add a type ignore for mypy for now * remove a type: ignore comment * unitarybox helper takes num_qubits as an arg * add another comment * add changelog entry * add another changelog entry * remove duplicate assert * update docs build in build and test workflow * Revert "update docs build in build and test workflow" This reverts commit 16453b1. * import _get_pytket_ctrl_state following review * fix handling of parameterised gate * add a couple of tests for parameterised gates * fix import * update unitary testing
1 parent c2ff1e7 commit e148bd8

File tree

3 files changed

+145
-85
lines changed

3 files changed

+145
-85
lines changed

docs/changelog.rst

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Changelog
33

44
.. currentmodule:: pytket.extensions.qiskit
55

6+
Unreleased
7+
----------
8+
* Added handling of generalised controlled gates to :py:func:`qiskit_to_tk` and :py:func:`tk_to_qiskit`. The `control_state` is handled directly instead of using additional `X` gates.
9+
* A controlled :py:class:`UnitaryGate` will now be converted to a pytket controlled unitary box by :py:func:`qiskit_to_tk` instead of a controlled :py:class:`~pytket.circuit.CircBox` with a unitary box inside.
610

711
0.56.0 (September 2024)
812
-----------------------

pytket/extensions/qiskit/qiskit_convert.py

+112-85
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@
8181
from pytket.unit_id import _TEMP_BIT_NAME
8282
from pytket.pauli import Pauli, QubitPauliString
8383
from pytket.architecture import Architecture, FullyConnected
84-
from pytket.utils import QubitPauliOperator, gen_term_sequence_circuit
84+
from pytket.utils import (
85+
QubitPauliOperator,
86+
gen_term_sequence_circuit,
87+
permute_rows_cols_in_unitary,
88+
)
8589
from pytket.passes import AutoRebase
8690

8791
if TYPE_CHECKING:
@@ -290,9 +294,26 @@ def _string_to_circuit(
290294
return circ
291295

292296

297+
def _get_pytket_ctrl_state(bitstring: str, n_bits: int) -> tuple[bool, ...]:
298+
"Converts a little endian string '001'=1 (LE) to (1, 0, 0)."
299+
assert set(bitstring).issubset({"0", "1"})
300+
padded_bitstring = bitstring.zfill(n_bits)
301+
pytket_ctrl_state = reversed([bool(int(b)) for b in padded_bitstring])
302+
return tuple(pytket_ctrl_state)
303+
304+
305+
def _all_bits_set(integer: int, n_bits: int) -> bool:
306+
return integer.bit_count() == n_bits
307+
308+
293309
def _get_controlled_tket_optype(c_gate: ControlledGate) -> OpType:
294310
"""Get a pytket contolled OpType from a qiskit ControlledGate."""
295-
if c_gate.base_class in _known_qiskit_gate:
311+
312+
# If the control state is not "all |1>", use QControlBox
313+
if not _all_bits_set(c_gate.ctrl_state, c_gate.num_ctrl_qubits):
314+
return OpType.QControlBox
315+
316+
elif c_gate.base_class in _known_qiskit_gate:
296317
# First we check if the gate is in _known_qiskit_gate
297318
# this avoids CZ being converted to CnZ
298319
return _known_qiskit_gate[c_gate.base_class]
@@ -334,6 +355,49 @@ def _optype_from_qiskit_instruction(instruction: Instruction) -> OpType:
334355
)
335356

336357

358+
UnitaryBox = Unitary1qBox | Unitary2qBox | Unitary3qBox
359+
360+
361+
def _get_unitary_box(unitary: NDArray[np.complex128], num_qubits: int) -> UnitaryBox:
362+
match num_qubits:
363+
case 1:
364+
assert unitary.shape == (2, 2)
365+
return Unitary1qBox(unitary)
366+
case 2:
367+
assert unitary.shape == (4, 4)
368+
return Unitary2qBox(unitary)
369+
case 3:
370+
assert unitary.shape == (8, 8)
371+
return Unitary3qBox(unitary)
372+
case _:
373+
raise NotImplementedError(
374+
f"Conversion of {num_qubits}-qubit unitary gates not supported."
375+
)
376+
377+
378+
def _get_qcontrol_box(c_gate: ControlledGate, params: list[float]) -> QControlBox:
379+
qiskit_ctrl_state: str = bin(c_gate.ctrl_state)[2:]
380+
pytket_ctrl_state: tuple[bool, ...] = _get_pytket_ctrl_state(
381+
bitstring=qiskit_ctrl_state, n_bits=c_gate.num_ctrl_qubits
382+
)
383+
if isinstance(c_gate.base_gate, UnitaryGate):
384+
unitary = c_gate.base_gate.params[0]
385+
# Here we reverse the order of the columns to correct for endianness.
386+
new_unitary: NDArray[np.complex128] = permute_rows_cols_in_unitary(
387+
matrix=unitary,
388+
permutation=tuple(reversed(range(c_gate.base_gate.num_qubits))),
389+
)
390+
base_op: Op = _get_unitary_box(new_unitary, c_gate.base_gate.num_qubits)
391+
else:
392+
base_tket_gate: OpType = _known_qiskit_gate[c_gate.base_gate.base_class]
393+
394+
base_op: Op = Op.create(base_tket_gate, params) # type: ignore
395+
396+
return QControlBox(
397+
base_op, n_controls=c_gate.num_ctrl_qubits, control_state=pytket_ctrl_state
398+
)
399+
400+
337401
def _add_state_preparation(
338402
tkc: Circuit, qubits: list[Qubit], prep: Initialize | StatePreparation
339403
) -> None:
@@ -432,21 +496,6 @@ def __init__(
432496
def circuit(self) -> Circuit:
433497
return self.tkc
434498

435-
def add_xs(
436-
self,
437-
num_ctrl_qubits: Optional[int],
438-
ctrl_state: Optional[str | int],
439-
qargs: list["Qubit"],
440-
) -> None:
441-
if ctrl_state is not None:
442-
assert isinstance(num_ctrl_qubits, int)
443-
assert num_ctrl_qubits >= 0
444-
c = int(ctrl_state, 2) if isinstance(ctrl_state, str) else int(ctrl_state)
445-
assert c >= 0 and (c >> num_ctrl_qubits) == 0
446-
for i in range(num_ctrl_qubits):
447-
if ((c >> i) & 1) == 0:
448-
self.tkc.X(self.qbmap[qargs[i]])
449-
450499
def add_qiskit_data(
451500
self, circuit: QuantumCircuit, data: Optional["QuantumCircuitData"] = None
452501
) -> None:
@@ -465,42 +514,14 @@ def add_qiskit_data(
465514
circuit=circuit,
466515
)
467516

468-
# Controlled operations may be controlled on values other than all-1. Handle
469-
# this by prepending and appending X gates on the control qubits.
470-
ctrl_state, num_ctrl_qubits = None, None
471-
try:
472-
ctrl_state = instr.ctrl_state
473-
num_ctrl_qubits = instr.num_ctrl_qubits
474-
except AttributeError:
475-
pass
476-
self.add_xs(num_ctrl_qubits, ctrl_state, qargs)
477-
478517
optype = None
479518
if type(instr) not in (PauliEvolutionGate, UnitaryGate):
480519
# Handling of PauliEvolutionGate and UnitaryGate below
481520
optype = _optype_from_qiskit_instruction(instruction=instr)
482521

483522
if optype == OpType.QControlBox:
484523
params = [param_to_tk(p) for p in instr.base_gate.params]
485-
n_base_qubits = instr.base_gate.num_qubits
486-
sub_circ = Circuit(n_base_qubits)
487-
# use base gate name for the CircBox (shows in renderer)
488-
sub_circ.name = instr.base_gate.name.capitalize()
489-
490-
if type(instr.base_gate) is UnitaryGate:
491-
assert len(cargs) == 0
492-
add_qiskit_unitary_to_tkc(
493-
sub_circ, instr.base_gate, sub_circ.qubits, condition_kwargs
494-
)
495-
else:
496-
base_tket_gate: OpType = _known_qiskit_gate[
497-
instr.base_gate.base_class
498-
]
499-
sub_circ.add_gate(
500-
base_tket_gate, params, list(range(n_base_qubits))
501-
)
502-
c_box = CircBox(sub_circ)
503-
q_ctrl_box = QControlBox(c_box, instr.num_ctrl_qubits)
524+
q_ctrl_box = _get_qcontrol_box(c_gate=instr, params=params)
504525
self.tkc.add_qcontrolbox(q_ctrl_box, qubits)
505526

506527
elif isinstance(instr, (Initialize, StatePreparation)):
@@ -515,8 +536,20 @@ def add_qiskit_data(
515536
self.tkc.add_circbox(ccbox, qubits)
516537

517538
elif type(instr) is UnitaryGate:
518-
assert len(cargs) == 0
519-
add_qiskit_unitary_to_tkc(self.tkc, instr, qubits, condition_kwargs)
539+
unitary = cast(NDArray[np.complex128], instr.params[0])
540+
if len(qubits) == 0:
541+
# If the UnitaryGate acts on no qubits, we add a phase.
542+
self.tkc.add_phase(np.angle(unitary[0][0]) / np.pi)
543+
else:
544+
unitary_box = _get_unitary_box(
545+
unitary=unitary, num_qubits=instr.num_qubits
546+
)
547+
self.tkc.add_gate(
548+
unitary_box,
549+
list(reversed(qubits)),
550+
**condition_kwargs,
551+
)
552+
520553
elif optype == OpType.Barrier:
521554
self.tkc.add_barrier(qubits)
522555
elif optype == OpType.CircBox:
@@ -550,42 +583,6 @@ def add_qiskit_data(
550583
params = [param_to_tk(p) for p in instr.params]
551584
self.tkc.add_gate(optype, params, qubits + bits, **condition_kwargs) # type: ignore
552585

553-
self.add_xs(num_ctrl_qubits, ctrl_state, qargs)
554-
555-
556-
def add_qiskit_unitary_to_tkc(
557-
tkc: Circuit,
558-
u_gate: UnitaryGate,
559-
qubits: list[Qubit],
560-
condition_kwargs: dict[str, Any],
561-
) -> None:
562-
# Note reversal of qubits, to account for endianness (pytket unitaries
563-
# are ILO-BE == DLO-LE; qiskit unitaries are ILO-LE == DLO-BE).
564-
params = u_gate.params
565-
assert len(params) == 1
566-
u = cast(np.ndarray, params[0])
567-
568-
n = len(qubits)
569-
if n == 0:
570-
assert u.shape == (1, 1)
571-
tkc.add_phase(np.angle(u[0][0]) / np.pi)
572-
elif n == 1:
573-
assert u.shape == (2, 2)
574-
u1box = Unitary1qBox(u)
575-
tkc.add_unitary1qbox(u1box, qubits[0], **condition_kwargs)
576-
elif n == 2:
577-
assert u.shape == (4, 4)
578-
u2box = Unitary2qBox(u)
579-
tkc.add_unitary2qbox(u2box, qubits[1], qubits[0], **condition_kwargs)
580-
elif n == 3:
581-
assert u.shape == (8, 8)
582-
u3box = Unitary3qBox(u)
583-
tkc.add_unitary3qbox(u3box, qubits[2], qubits[1], qubits[0], **condition_kwargs)
584-
else:
585-
raise NotImplementedError(
586-
f"Conversion of {n}-qubit unitary gates not supported."
587-
)
588-
589586

590587
def qiskit_to_tk(qcirc: QuantumCircuit, preserve_param_uuid: bool = False) -> Circuit:
591588
"""
@@ -616,6 +613,10 @@ def qiskit_to_tk(qcirc: QuantumCircuit, preserve_param_uuid: bool = False) -> Ci
616613
return builder.circuit()
617614

618615

616+
def _get_qiskit_control_state(bool_list: list[bool]) -> str:
617+
return "".join(str(int(b)) for b in bool_list)[::-1]
618+
619+
619620
def param_to_tk(p: float | ParameterExpression) -> sympy.Expr:
620621
if isinstance(p, ParameterExpression):
621622
symexpr = p._symbol_expr
@@ -698,6 +699,27 @@ def append_tk_command_to_qiskit(
698699
qiskit_state_prep_box = StatePreparation(statevector_array)
699700
return qcirc.append(qiskit_state_prep_box, qargs=list(reversed(qargs)))
700701

702+
if optype == OpType.QControlBox:
703+
assert isinstance(op, QControlBox)
704+
qargs = [qregmap[q.reg_name][q.index[0]] for q in args]
705+
pytket_control_state: list[bool] = op.get_control_state_bits()
706+
qiskit_control_state: str = _get_qiskit_control_state(pytket_control_state)
707+
try:
708+
gatetype, phase = _known_gate_rev_phase[op.get_op().type]
709+
except KeyError:
710+
raise NotImplementedError(
711+
"Conversion of QControlBox with base gate"
712+
+ f"{op.get_op()} not supported by tk_to_qiskit."
713+
)
714+
params = _get_params(op.get_op(), symb_map)
715+
operation = gatetype(*params)
716+
return qcirc.append(
717+
operation.control(
718+
num_ctrl_qubits=op.get_n_controls(), ctrl_state=qiskit_control_state
719+
),
720+
qargs=qargs,
721+
)
722+
701723
if optype == OpType.Barrier:
702724
if any(q.type == UnitType.bit for q in args):
703725
raise NotImplementedError(
@@ -818,7 +840,12 @@ def append_tk_command_to_qiskit(
818840
_protected_tket_gates = (
819841
_supported_tket_gates
820842
| _additional_multi_controlled_gates
821-
| {OpType.Unitary1qBox, OpType.Unitary2qBox, OpType.Unitary3qBox}
843+
| {
844+
OpType.Unitary1qBox,
845+
OpType.Unitary2qBox,
846+
OpType.Unitary3qBox,
847+
OpType.QControlBox,
848+
}
822849
| {OpType.CustomGate}
823850
)
824851

tests/qiskit_convert_test.py

+29
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@
4444
Unitary2qBox,
4545
Unitary3qBox,
4646
OpType,
47+
Op,
4748
Qubit,
4849
Bit,
4950
CustomGateDef,
5051
reg_eq,
5152
StatePreparationBox,
53+
QControlBox,
5254
)
5355
from pytket.extensions.qiskit import tk_to_qiskit, qiskit_to_tk, IBMQBackend
5456
from pytket.extensions.qiskit.backends import qiskit_aer_backend
@@ -865,6 +867,33 @@ def test_controlled_unitary_conversion() -> None:
865867
assert np.allclose(u_qc, u_tkc)
866868

867869

870+
def test_qcontrol_box_conversion_to_qiskit() -> None:
871+
ccch_001 = QControlBox(
872+
Op.create(OpType.H), n_controls=3, control_state=(False, False, True)
873+
)
874+
cccs_110 = QControlBox(
875+
Op.create(OpType.S), n_controls=3, control_state=(True, True, False)
876+
)
877+
cccRy_100 = QControlBox(
878+
Op.create(OpType.Ry, 0.73), n_controls=3, control_state=(True, False, False)
879+
)
880+
ccU3_10 = QControlBox(
881+
Op.create(OpType.U3, [0.1, 0.2, 0.3]), n_controls=2, control_state=(True, False)
882+
)
883+
circ1 = Circuit(4, name="test_circ")
884+
circ1.add_gate(ccch_001, [0, 1, 2, 3])
885+
circ1.add_gate(cccs_110, [0, 1, 2, 3])
886+
circ1.add_gate(cccRy_100, [3, 2, 1, 0])
887+
circ1.add_gate(ccU3_10, [1, 0, 2])
888+
qc = tk_to_qiskit(circ1)
889+
qiskit_unitary = permute_rows_cols_in_unitary(Operator(qc).data, (3, 2, 1, 0))
890+
assert compare_unitaries(qiskit_unitary, circ1.get_unitary())
891+
circ2 = qiskit_to_tk(qc)
892+
DecomposeBoxes().apply(circ1)
893+
DecomposeBoxes().apply(circ2)
894+
assert circ1 == circ2
895+
896+
868897
# Ensures that the tk_to_qiskit converter does not cancel redundant gates
869898
def test_tk_to_qiskit_redundancies() -> None:
870899
h_circ = Circuit(1).H(0).H(0)

0 commit comments

Comments
 (0)