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 handling of controlled gates in converters #391

Merged
merged 34 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c32e2ee
simple converter function
CalMacCQ Sep 12, 2024
77c5d97
start adding control state handling
CalMacCQ Sep 12, 2024
0eb0610
implement control_state handling in qiskit_to_tk
CalMacCQ Sep 12, 2024
903f2e9
remove unecessary if-else
CalMacCQ Sep 12, 2024
fdc0c4e
use a list comprehension
CalMacCQ Sep 12, 2024
9fe2128
annotate helper function call
CalMacCQ Sep 12, 2024
5dab332
refactor qcontrolbox building out of _add_qiskit_data
CalMacCQ Sep 12, 2024
0feb9fd
update controlbox utility function
CalMacCQ Sep 13, 2024
6f24ed8
fix ordering in Unitary boxes
CalMacCQ Sep 16, 2024
5038ea5
get rid of Union typing
CalMacCQ Sep 16, 2024
73c9ab8
use helper function to handle UnitaryGate instances
CalMacCQ Sep 17, 2024
c39b82b
fix handling for all UnitaryGate instances
CalMacCQ Sep 17, 2024
7e8b770
remove original UnitaryGate helper function
CalMacCQ Sep 17, 2024
f3761d1
helper function takes a unitary as an argument not a UnitaryGate
CalMacCQ Sep 17, 2024
0aefdfc
add assert and a comment
CalMacCQ Sep 17, 2024
b2b48b0
Support QControlBox in tk_to_qiskit converter
CalMacCQ Sep 17, 2024
cd69dff
add test for QControlBox conversion
CalMacCQ Sep 17, 2024
0ab2d48
shorten line to make pylint happy
CalMacCQ Sep 17, 2024
57d55fd
check unitary equivalence in test
CalMacCQ Sep 17, 2024
a97d407
add a type ignore for mypy for now
CalMacCQ Sep 17, 2024
cd9cec9
remove a type: ignore comment
CalMacCQ Sep 17, 2024
62ac814
unitarybox helper takes num_qubits as an arg
CalMacCQ Sep 17, 2024
8cc30ac
add another comment
CalMacCQ Sep 17, 2024
700dd55
add changelog entry
CalMacCQ Sep 17, 2024
7e3dfe6
add another changelog entry
CalMacCQ Sep 17, 2024
cb81a07
remove duplicate assert
CalMacCQ Sep 17, 2024
16453b1
update docs build in build and test workflow
CalMacCQ Sep 17, 2024
5b4aeec
Revert "update docs build in build and test workflow"
CalMacCQ Sep 17, 2024
daa3090
import _get_pytket_ctrl_state following review
CalMacCQ Sep 18, 2024
b42946e
fix handling of parameterised gate
CalMacCQ Sep 23, 2024
c1b315c
add a couple of tests for parameterised gates
CalMacCQ Sep 23, 2024
4473235
fix import
CalMacCQ Sep 23, 2024
9dee730
Merge branch 'main' into feat/improved_controlled_gates
CalMacCQ Sep 23, 2024
05dd936
update unitary testing
CalMacCQ Sep 24, 2024
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
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ Changelog

.. currentmodule:: pytket.extensions.qiskit

Unreleased
----------
* 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.
* 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.

0.56.0 (September 2024)
-----------------------
Expand Down
198 changes: 113 additions & 85 deletions pytket/extensions/qiskit/qiskit_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,11 @@
from pytket.unit_id import _TEMP_BIT_NAME
from pytket.pauli import Pauli, QubitPauliString
from pytket.architecture import Architecture, FullyConnected
from pytket.utils import QubitPauliOperator, gen_term_sequence_circuit
from pytket.utils import (
QubitPauliOperator,
gen_term_sequence_circuit,
permute_rows_cols_in_unitary,
)
from pytket.passes import AutoRebase

if TYPE_CHECKING:
Expand Down Expand Up @@ -290,9 +294,27 @@ def _string_to_circuit(
return circ


def _get_pytket_ctrl_state(bitstring: str, n_bits: int) -> tuple[bool, ...]:
"Converts a little endian string '001'=1 (LE) to (1, 0, 0)."
cqc-alec marked this conversation as resolved.
Show resolved Hide resolved
assert set(bitstring).issubset({"0", "1"})
pytket_ctrl_state = [bool(int(b)) for b in bitstring[::-1]]
padding_zeros: list[bool] = [False] * (n_bits - len(bitstring))
pytket_ctrl_state.extend(padding_zeros)
return tuple(pytket_ctrl_state)


def _all_bits_set(integer: int, n_bits: int) -> bool:
return integer.bit_count() == n_bits


def _get_controlled_tket_optype(c_gate: ControlledGate) -> OpType:
"""Get a pytket contolled OpType from a qiskit ControlledGate."""
if c_gate.base_class in _known_qiskit_gate:

# If the control state is not "all |1>", use QControlBox
if not _all_bits_set(c_gate.ctrl_state, c_gate.num_ctrl_qubits):
return OpType.QControlBox

elif c_gate.base_class in _known_qiskit_gate:
# First we check if the gate is in _known_qiskit_gate
# this avoids CZ being converted to CnZ
return _known_qiskit_gate[c_gate.base_class]
Expand Down Expand Up @@ -334,6 +356,49 @@ def _optype_from_qiskit_instruction(instruction: Instruction) -> OpType:
)


UnitaryBox = Unitary1qBox | Unitary2qBox | Unitary3qBox


def _get_unitary_box(unitary: NDArray[np.complex128], num_qubits: int) -> UnitaryBox:
match num_qubits:
case 1:
assert unitary.shape == (2, 2)
return Unitary1qBox(unitary)
case 2:
assert unitary.shape == (4, 4)
return Unitary2qBox(unitary)
case 3:
assert unitary.shape == (8, 8)
return Unitary3qBox(unitary)
case _:
raise NotImplementedError(
f"Conversion of {num_qubits}-qubit unitary gates not supported."
)


def _get_qcontrol_box(c_gate: ControlledGate, params: list[float]) -> QControlBox:
qiskit_ctrl_state: str = bin(c_gate.ctrl_state)[2:]
pytket_ctrl_state: tuple[bool, ...] = _get_pytket_ctrl_state(
bitstring=qiskit_ctrl_state, n_bits=c_gate.num_ctrl_qubits
)
if isinstance(c_gate.base_gate, UnitaryGate):
unitary = c_gate.base_gate.params[0]
# Here we reverse the order of the columns to correct for endianness.
new_unitary: NDArray[np.complex128] = permute_rows_cols_in_unitary(
matrix=unitary,
permutation=tuple(reversed(range(c_gate.base_gate.num_qubits))),
)
base_op: Op = _get_unitary_box(new_unitary, c_gate.base_gate.num_qubits)
else:
base_tket_gate: OpType = _known_qiskit_gate[c_gate.base_gate.base_class]

base_op: Op = Op.create(base_tket_gate, params) # type: ignore
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe I can get around this type: ignore. Mypy doesn't seem happy with the fact that base_op is defined on two branches.

The _get_unitary_box function has return type Unitary1qBox | Unitary2qBox | Unitary3qBox. Not sure if mypy is able to find out that these unitary boxes inherit from Op.


return QControlBox(
base_op, n_controls=c_gate.num_ctrl_qubits, control_state=pytket_ctrl_state
)


def _add_state_preparation(
tkc: Circuit, qubits: list[Qubit], prep: Initialize | StatePreparation
) -> None:
Expand Down Expand Up @@ -432,21 +497,6 @@ def __init__(
def circuit(self) -> Circuit:
return self.tkc

def add_xs(
self,
num_ctrl_qubits: Optional[int],
ctrl_state: Optional[str | int],
qargs: list["Qubit"],
) -> None:
if ctrl_state is not None:
assert isinstance(num_ctrl_qubits, int)
assert num_ctrl_qubits >= 0
c = int(ctrl_state, 2) if isinstance(ctrl_state, str) else int(ctrl_state)
assert c >= 0 and (c >> num_ctrl_qubits) == 0
for i in range(num_ctrl_qubits):
if ((c >> i) & 1) == 0:
self.tkc.X(self.qbmap[qargs[i]])

def add_qiskit_data(
self, circuit: QuantumCircuit, data: Optional["QuantumCircuitData"] = None
) -> None:
Expand All @@ -465,42 +515,14 @@ def add_qiskit_data(
circuit=circuit,
)

# Controlled operations may be controlled on values other than all-1. Handle
# this by prepending and appending X gates on the control qubits.
ctrl_state, num_ctrl_qubits = None, None
try:
ctrl_state = instr.ctrl_state
num_ctrl_qubits = instr.num_ctrl_qubits
except AttributeError:
pass
self.add_xs(num_ctrl_qubits, ctrl_state, qargs)

optype = None
if type(instr) not in (PauliEvolutionGate, UnitaryGate):
# Handling of PauliEvolutionGate and UnitaryGate below
optype = _optype_from_qiskit_instruction(instruction=instr)

if optype == OpType.QControlBox:
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()

if type(instr.base_gate) is UnitaryGate:
assert len(cargs) == 0
add_qiskit_unitary_to_tkc(
sub_circ, instr.base_gate, sub_circ.qubits, condition_kwargs
)
else:
base_tket_gate: OpType = _known_qiskit_gate[
instr.base_gate.base_class
]
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)
q_ctrl_box = _get_qcontrol_box(c_gate=instr, params=params)
self.tkc.add_qcontrolbox(q_ctrl_box, qubits)

elif isinstance(instr, (Initialize, StatePreparation)):
Expand All @@ -515,8 +537,20 @@ def add_qiskit_data(
self.tkc.add_circbox(ccbox, qubits)

elif type(instr) is UnitaryGate:
assert len(cargs) == 0
add_qiskit_unitary_to_tkc(self.tkc, instr, qubits, condition_kwargs)
unitary = cast(NDArray[np.complex128], instr.params[0])
if len(qubits) == 0:
# If the UnitaryGate acts on no qubits, we add a phase.
self.tkc.add_phase(np.angle(unitary[0][0]) / np.pi)
else:
unitary_box = _get_unitary_box(
unitary=unitary, num_qubits=instr.num_qubits
)
self.tkc.add_gate(
unitary_box,
list(reversed(qubits)),
**condition_kwargs,
)

elif optype == OpType.Barrier:
self.tkc.add_barrier(qubits)
elif optype == OpType.CircBox:
Expand Down Expand Up @@ -550,42 +584,6 @@ def add_qiskit_data(
params = [param_to_tk(p) for p in instr.params]
self.tkc.add_gate(optype, params, qubits + bits, **condition_kwargs) # type: ignore

self.add_xs(num_ctrl_qubits, ctrl_state, qargs)


def add_qiskit_unitary_to_tkc(
tkc: Circuit,
u_gate: UnitaryGate,
qubits: list[Qubit],
condition_kwargs: dict[str, Any],
) -> None:
# Note reversal of qubits, to account for endianness (pytket unitaries
# are ILO-BE == DLO-LE; qiskit unitaries are ILO-LE == DLO-BE).
params = u_gate.params
assert len(params) == 1
u = cast(np.ndarray, params[0])

n = len(qubits)
if n == 0:
assert u.shape == (1, 1)
tkc.add_phase(np.angle(u[0][0]) / np.pi)
elif n == 1:
assert u.shape == (2, 2)
u1box = Unitary1qBox(u)
tkc.add_unitary1qbox(u1box, qubits[0], **condition_kwargs)
elif n == 2:
assert u.shape == (4, 4)
u2box = Unitary2qBox(u)
tkc.add_unitary2qbox(u2box, qubits[1], qubits[0], **condition_kwargs)
elif n == 3:
assert u.shape == (8, 8)
u3box = Unitary3qBox(u)
tkc.add_unitary3qbox(u3box, qubits[2], qubits[1], qubits[0], **condition_kwargs)
else:
raise NotImplementedError(
f"Conversion of {n}-qubit unitary gates not supported."
)


def qiskit_to_tk(qcirc: QuantumCircuit, preserve_param_uuid: bool = False) -> Circuit:
"""
Expand Down Expand Up @@ -616,6 +614,10 @@ def qiskit_to_tk(qcirc: QuantumCircuit, preserve_param_uuid: bool = False) -> Ci
return builder.circuit()


def _get_qiskit_control_state(bool_list: list[bool]) -> str:
return "".join(str(int(b)) for b in bool_list)[::-1]


def param_to_tk(p: float | ParameterExpression) -> sympy.Expr:
if isinstance(p, ParameterExpression):
symexpr = p._symbol_expr
Expand Down Expand Up @@ -698,6 +700,27 @@ def append_tk_command_to_qiskit(
qiskit_state_prep_box = StatePreparation(statevector_array)
return qcirc.append(qiskit_state_prep_box, qargs=list(reversed(qargs)))

if optype == OpType.QControlBox:
assert isinstance(op, QControlBox)
qargs = [qregmap[q.reg_name][q.index[0]] for q in args]
pytket_control_state: list[bool] = op.get_control_state_bits()
qiskit_control_state: str = _get_qiskit_control_state(pytket_control_state)
try:
base_qiskit_gate, phase = _known_gate_rev_phase[op.get_op().type]
except KeyError:
raise NotImplementedError(
"Conversion of QControlBox with base gate"
+ f"{op.get_op()} not supported by tk_to_qiskit."
)

qiskit_controlled_gate: ControlledGate = base_qiskit_gate().control(
num_ctrl_qubits=op.get_n_controls(), ctrl_state=qiskit_control_state
)
return qcirc.append(
qiskit_controlled_gate,
qargs=qargs,
)

if optype == OpType.Barrier:
if any(q.type == UnitType.bit for q in args):
raise NotImplementedError(
Expand Down Expand Up @@ -818,7 +841,12 @@ def append_tk_command_to_qiskit(
_protected_tket_gates = (
_supported_tket_gates
| _additional_multi_controlled_gates
| {OpType.Unitary1qBox, OpType.Unitary2qBox, OpType.Unitary3qBox}
| {
OpType.Unitary1qBox,
OpType.Unitary2qBox,
OpType.Unitary3qBox,
OpType.QControlBox,
}
| {OpType.CustomGate}
)

Expand Down
17 changes: 17 additions & 0 deletions tests/qiskit_convert_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@
Unitary2qBox,
Unitary3qBox,
OpType,
Op,
Qubit,
Bit,
CustomGateDef,
reg_eq,
StatePreparationBox,
QControlBox,
)
from pytket.extensions.qiskit import tk_to_qiskit, qiskit_to_tk, IBMQBackend
from pytket.extensions.qiskit.backends import qiskit_aer_backend
Expand Down Expand Up @@ -865,6 +867,21 @@ def test_controlled_unitary_conversion() -> None:
assert np.allclose(u_qc, u_tkc)


def test_qcontrol_box_conversion_to_qiskit() -> None:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I will add tests for a wider range of controlled operations

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes please.

Copy link
Contributor Author

@CalMacCQ CalMacCQ Sep 18, 2024

Choose a reason for hiding this comment

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

Conversion doesn't work properly for various parameterized gates. Will update the handling and add tests.

Copy link
Contributor Author

@CalMacCQ CalMacCQ Sep 23, 2024

Choose a reason for hiding this comment

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

I've fixed the conversion of parametrised gates in b42946e and added some controlled parametrised gates to the test in c1b315c

h_op = Op.create(OpType.H)
multi_controlled_h = QControlBox(
h_op, n_controls=3, control_state=(False, False, True)
)
circ1 = Circuit(4, name="CCH test")
CalMacCQ marked this conversation as resolved.
Show resolved Hide resolved
circ1.add_gate(multi_controlled_h, [0, 1, 2, 3])
qc = tk_to_qiskit(circ1)
circ2 = qiskit_to_tk(qc)
DecomposeBoxes().apply(circ1)
DecomposeBoxes().apply(circ2)
assert circ1 == circ2
assert compare_unitaries(circ1.get_unitary(), circ2.get_unitary())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this check of unitary equivalence may be redundant given the previous line

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, can remove this. However, I think we should add a test that compares the unitary of the tket circuit with the unitary of the qiskit circuit (adjusted for endianness).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added in 05dd936



# Ensures that the tk_to_qiskit converter does not cancel redundant gates
def test_tk_to_qiskit_redundancies() -> None:
h_circ = Circuit(1).H(0).H(0)
Expand Down