diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 3cee41ad..49530648 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -26,16 +26,20 @@ import numpy as np import pytket as tk -from pytket.circuit import (Bit, Command, Op, OpType, Qubit) +from pytket.circuit import (Bit, Command, Op, OpType) from pytket.utils import probs_from_counts import sympy from typing_extensions import Self -from lambeq.backend import Functor, Symbol -from lambeq.backend.quantum import (bit, Box, Bra, CCX, CCZ, Controlled, CRx, - CRy, CRz, Daggered, Diagram, Discard, - GATES, Id, Ket, Measure, quantum, qubit, - Rx, Ry, Rz, Scalar, Swap, X, Y, Z) +from lambeq.backend import Symbol +from lambeq.backend.quantum import (bit, Box, Bra, CCX, CCZ, + Controlled, CRx, CRy, CRz, + Diagram, Discard, GATES, Id, + is_circuital, Ket, Measure, + qubit, readoff_circuital, + Rx, Ry, Rz, Scalar, Swap, + to_circuital, X, Y, Z + ) OPTYPE_MAP = {'H': OpType.H, 'X': OpType.X, @@ -52,7 +56,7 @@ 'CRy': OpType.CRy, 'CRz': OpType.CRz, 'CCX': OpType.CCX, - 'Swap': OpType.SWAP} + 'SWAP': OpType.SWAP} class Circuit(tk.Circuit): @@ -192,161 +196,6 @@ def get_counts(self, return counts -def to_tk(circuit: Diagram): - """ - Takes a :py:class:`lambeq.quantum.Diagram`, returns - a :py:class:`Circuit`. - """ - # bits and qubits are lists of register indices, at layer i we want - # len(bits) == circuit[:i].cod.count(bit) and same for qubits - tk_circ = Circuit() - bits: list[int] = [] - qubits: list[int] = [] - circuit = circuit.init_and_discard() - - def remove_ketbra1(_, box: Box) -> Diagram | Box: - ob_map: dict[Box, Diagram] - ob_map = {Ket(1): Ket(0) >> X, # type: ignore[dict-item] - Bra(1): X >> Bra(0)} # type: ignore[dict-item] - return ob_map.get(box, box) - - def prepare_qubits(qubits: list[int], - box: Box, - offset: int) -> list[int]: - renaming = dict() - start = (tk_circ.n_qubits if not qubits else 0 - if not offset else qubits[offset - 1] + 1) - for i in range(start, tk_circ.n_qubits): - old = Qubit('q', i) - new = Qubit('q', i + len(box.cod)) - renaming.update({old: new}) - tk_circ.rename_units(renaming) - tk_circ.add_blank_wires(len(box.cod)) - return (qubits[:offset] + list(range(start, start + len(box.cod))) - + [i + len(box.cod) for i in qubits[offset:]]) - - def measure_qubits(qubits: list[int], - bits: list[int], - box: Box, - bit_offset: int, - qubit_offset: int) -> tuple[list[int], list[int]]: - if isinstance(box, Bra): - tk_circ.post_select({len(tk_circ.bits): box.bit}) - for j, _ in enumerate(box.dom): - i_bit, i_qubit = len(tk_circ.bits), qubits[qubit_offset + j] - offset = len(bits) if isinstance(box, Measure) else None - tk_circ.add_bit(Bit(i_bit), offset=offset) - tk_circ.Measure(i_qubit, i_bit) - if isinstance(box, Measure): - bits = bits[:bit_offset + j] + [i_bit] + bits[bit_offset + j:] - # remove measured qubits - qubits = (qubits[:qubit_offset] - + qubits[qubit_offset + len(box.dom):]) - return bits, qubits - - def swap(i: int, j: int, unit_factory=Qubit) -> None: - old, tmp, new = ( - unit_factory(i), unit_factory('tmp', 0), unit_factory(j)) - tk_circ.rename_units({old: tmp}) - tk_circ.rename_units({new: old}) - tk_circ.rename_units({tmp: new}) - - def add_gate(qubits: list[int], box: Box, offset: int) -> None: - - is_dagger = False - if isinstance(box, Daggered): - box = box.dagger() - is_dagger = True - - i_qubits = [qubits[offset + j] for j in range(len(box.dom))] - - if isinstance(box, (Rx, Ry, Rz)): - phase = box.phase - if isinstance(box.phase, Symbol): - # Tket uses sympy, lambeq uses custom symbol - phase = box.phase.to_sympy() - op = Op.create(OPTYPE_MAP[box.name[:2]], 2 * phase) - elif isinstance(box, Controlled): - # The following works only for controls on single qubit gates - - # reverse the distance order - dists = [] - curr_box: Box | Controlled = box - while isinstance(curr_box, Controlled): - dists.append(curr_box.distance) - curr_box = curr_box.controlled - dists.reverse() - - # Index of the controlled qubit is the last entry in rel_idx - rel_idx = [0] - for dist in dists: - if dist > 0: - # Add control to the left, offset by distance - rel_idx = [0] + [i + dist for i in rel_idx] - else: - # Add control to the right, don't offset - right_most_idx = max(rel_idx) - rel_idx.insert(-1, right_most_idx - dist) - - i_qubits = [i_qubits[i] for i in rel_idx] - - name = box.name.split('(')[0] - if box.name in ('CX', 'CZ', 'CCX'): - op = Op.create(OPTYPE_MAP[name]) - elif name in ('CRx', 'CRz'): - phase = box.phase - if isinstance(box.phase, Symbol): - # Tket uses sympy, lambeq uses custom symbol - phase = box.phase.to_sympy() - - op = Op.create(OPTYPE_MAP[name], 2 * phase) - elif name in ('CCX'): - op = Op.create(OPTYPE_MAP[name]) - elif box.name in OPTYPE_MAP: - op = Op.create(OPTYPE_MAP[box.name]) - else: - raise NotImplementedError(box) - - if is_dagger: - op = op.dagger - - tk_circ.add_gate(op, i_qubits) - - circuit = Functor(target_category=quantum, # type: ignore [assignment] - ob=lambda _, x: x, - ar=remove_ketbra1)(circuit) # type: ignore [arg-type] - for left, box, _ in circuit: - if isinstance(box, Ket): - qubits = prepare_qubits(qubits, box, left.count(qubit)) - elif isinstance(box, (Measure, Bra)): - bits, qubits = measure_qubits( - qubits, bits, box, left.count(bit), left.count(qubit)) - elif isinstance(box, Discard): - qubits = (qubits[:left.count(qubit)] - + qubits[left.count(qubit) + box.dom.count(qubit):]) - elif isinstance(box, Swap): - if box == Swap(qubit, qubit): - off = left.count(qubit) - swap(qubits[off], qubits[off + 1]) - elif box == Swap(bit, bit): - off = left.count(bit) - if tk_circ.post_processing: - right = Id(tk_circ.post_processing.cod[off + 2:]) - tk_circ.post_process( - Id(bit ** off) @ Swap(bit, bit) @ right) - else: - swap(bits[off], bits[off + 1], unit_factory=Bit) - else: # pragma: no cover - continue # bits and qubits live in different registers. - elif isinstance(box, Scalar): - tk_circ.scale(abs(box.array) ** 2) - elif isinstance(box, Box): - add_gate(qubits, box, left.count(qubit)) - else: # pragma: no cover - raise NotImplementedError - return tk_circ - - def _tk_to_lmbq_param(theta): if not isinstance(theta, sympy.Expr): return theta @@ -362,6 +211,67 @@ def _tk_to_lmbq_param(theta): raise ValueError('Parameter must be a (possibly scaled) sympy Symbol') +def to_tk(diagram: Diagram) -> Circuit: + """Takes a :py:class:`lambeq.quantum.Diagram`, returns + a :class:`lambeq.backend.converters.tk.Circuit` + for t|ket>. + + + Parameters + ---------- + diagram : :py:class:`~lambeq.backend.quantum.Diagram` + The :py:class:`Circuits ` + to be converted to a tket circuit. + + Returns + ------- + tk_circuit : lambeq.backend.quantum + A :class:`lambeq.backend.converters.tk.Circuit`. + + Note + ---- + * Converts to circuital. + * Copies the diagram to avoid modifying the original. + """ + + if not is_circuital(diagram): + diagram = to_circuital(diagram) + + circuitInfo = readoff_circuital(diagram) + + circuit = Circuit(circuitInfo.totalQubits, + len(circuitInfo.bitmap), + post_selection=circuitInfo.postmap + ) + + for gate in circuitInfo.gates: + + if gate.gtype == 'Scalar': + if gate.phase is None: + raise ValueError(f'Scalar gate {gate} has phase type None') + else: + circuit.scale(abs(gate.phase)**2) # type: ignore [arg-type] + continue + elif gate.gtype not in OPTYPE_MAP: + raise NotImplementedError(f'Gate {gate.gtype} not supported') + + if gate.phase: + op = Op.create(OPTYPE_MAP[gate.gtype], 2 * gate.phase) + else: + op = Op.create(OPTYPE_MAP[gate.gtype]) + + if gate.dagger: + op = op.dagger + + qubits = gate.qubits + circuit.add_gate(op, qubits) + + for mq, bi in circuitInfo.bitmap.items(): + circuit.Measure(mq, bi) + + return circuit + + def from_tk(tk_circuit: tk.Circuit) -> Diagram: """Translates from tket to a lambeq Diagram.""" tk_circ: Circuit = Circuit.upgrade(tk_circuit) diff --git a/lambeq/backend/grammar.py b/lambeq/backend/grammar.py index 1d4e3183..7bd213a1 100644 --- a/lambeq/backend/grammar.py +++ b/lambeq/backend/grammar.py @@ -203,6 +203,62 @@ def __getitem__(self, index: int | slice) -> Self: else: return self._fromiter(objects[index]) + def replace(self, other: Self, index: int) -> Self: + """Replace a type at the specified index in the complex type list. + + Parameters + ---------- + other : Ty + The type to insert. Can be atomic or complex. + index : int + The position where the type should be inserted. + """ + if not (index <= len(self) and index >= 0): + raise IndexError(f'Index {index} out of bounds for ' + f'type {self} with length {len(self)}.') + + if self.is_empty: + return other + else: + objects = self.objects.copy() + + if len(objects) == 1: + return other + + if index == 0: + objects = [*other] + objects[1:] + elif index == len(self): + objects = objects[:-1] + [*other] + else: + objects = objects[:index] + [*other] + objects[index+1:] + + return self._fromiter(objects) + + def insert(self, other: Self, index: int) -> Self: + """Insert a type at the specified index in the complex type list. + + Parameters + ---------- + other : Ty + The type to insert. Can be atomic or complex. + index : int + The position where the type should be inserted. + """ + if not (index <= len(self)): + raise IndexError(f'Index {index} out of bounds for ' + f'type {self} with length {len(self)}.') + + if self.is_empty: + return other + else: + if index == 0: + return other @ self + elif index == len(self): + return self @ other + objects = self.objects.copy() + objects = objects[:index] + [*other] + objects[index:] + return self._fromiter(objects) + @classmethod def _fromiter(cls, objects: Iterable[Self]) -> Self: """Create a Ty from an iterable of atomic objects.""" @@ -1912,16 +1968,16 @@ class Functor: >>> n = Ty('n') >>> diag = Cap(n, n.l) @ Id(n) >> Id(n) @ Cup(n.l, n) >>> diag.draw( - ... figsize=(2, 2), path='./snake.png') + ... figsize=(2, 2), path='./docs/_static/images/snake.png') - .. image:: ./_static/images/snake.png + .. image:: ../_static/images/snake.png :align: center >>> F = Functor(grammar, lambda _, ty : ty @ ty) >>> F(diag).draw( ... figsize=(2, 2), path='./snake-2.png') - .. image:: ./_static/images/snake-2.png + .. image:: ../_static/images/snake-2.png :align: center """ diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 77cc9010..26cdc4ef 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -43,146 +43,107 @@ from itertools import product import sys -from typing import TYPE_CHECKING +from typing import List, Set, Tuple, TYPE_CHECKING, Union import pennylane as qml -from pytket import OpType import sympy import torch -from lambeq.backend.quantum import Measure, Scalar +from lambeq.backend import Symbol +from lambeq.backend.quantum import (Gate, is_circuital, Measure, + readoff_circuital, + to_circuital) if TYPE_CHECKING: from lambeq.backend.quantum import Diagram OP_MAP = { - OpType.X: qml.PauliX, - OpType.Y: qml.PauliY, - OpType.Z: qml.PauliZ, - OpType.S: qml.S, - OpType.Sdg: lambda wires: qml.S(wires=wires).inv(), - OpType.T: qml.T, - OpType.Tdg: lambda wires: qml.T(wires=wires).inv(), - OpType.H: qml.Hadamard, - OpType.Rx: qml.RX, - OpType.Ry: qml.RY, - OpType.Rz: qml.RZ, - OpType.CX: qml.CNOT, - OpType.CY: qml.CY, - OpType.CZ: qml.CZ, - OpType.CRx: qml.CRX, - OpType.CRy: qml.CRY, - OpType.CRz: qml.CRZ, - OpType.CU1: lambda a, wires: qml.ctrl(qml.U1(a, wires=wires[1]), - control=wires[0]), - OpType.SWAP: qml.SWAP, - OpType.noop: qml.Identity, + 'H': qml.Hadamard, + 'X': qml.PauliX, + 'Y': qml.PauliY, + 'Z': qml.PauliZ, + 'S': qml.S, + 'Sdg': lambda wires: qml.S(wires=wires).inv(), + 'T': qml.T, + 'Tdg': lambda wires: qml.T(wires=wires).inv(), + 'Rx': qml.RX, + 'Ry': qml.RY, + 'Rz': qml.RZ, + 'CX': qml.CNOT, + 'CY': qml.CY, + 'CZ': qml.CZ, + 'CRx': qml.CRX, + 'CRy': qml.CRY, + 'CRz': qml.CRZ, + 'CU1': lambda a, wires: qml.ctrl(qml.U1(a, + wires=wires[1]), + control=wires[0]), + 'SWAP': qml.SWAP, + 'noop': qml.Identity, } -def tk_op_to_pennylane(tk_op): +def extract_ops_from_circuital( + gates: List['Gate'] +) -> Tuple[ + List[qml.operation.Operation], + List[Union[torch.Tensor, Symbol]], + Set[Symbol], + List[List[int]] +]: """ Extract the operation, parameters and wires from - a pytket :class:`Op`, and return the corresponding PennyLane + a circuital diagram dictionary, and return the corresponding PennyLane operation. Parameters ---------- - tk_op : :class:`pytket.circuit.Op` - The pytket :class:`Op` to convert. + circuit_dict : :class:`Dict` + The circuital dictionary to convert. Returns ------- - :class:`qml.operation.Operation` + list of :class:`qml.operation.Operation` The PennyLane operation equivalent to the input pytket Op. list of (:class:`torch.FloatTensor` or :class:`lambeq.backend.symbol.Symbol`) The parameters of the operation. list of :class:`lambeq.backend.symbol.Symbol` The free symbols in the parameters of the operation. - list of int + list of lists of int The wires/qubits to apply the operation to. """ - wires = [x.index[0] for x in tk_op.qubits] - params = tk_op.op.params + ops = [OP_MAP[x.gtype] for x in gates] + qubits = [x.qubits for x in gates] + params: list[Union[sympy.Expr, float, int, list]] = [x.phase + if x.phase + else [] + for x in gates] + symbols = set() - remapped_params = [] + remapped_params: list[Union[sympy.Expr, torch.Tensor]] = [] for param in params: - # scale rotation from [0, 2) to [0, 1) (rescale to [0, 2pi) later) - param /= 2 - if not isinstance(param, sympy.Expr): + + # Check if the param contains a symbol + if isinstance(param, list) and len(param) == 0: + remapped_params.append([]) + continue + elif not isinstance(param, sympy.Expr): param = torch.tensor(param) else: symbols.update(param.free_symbols) - remapped_params.append(param) - - return OP_MAP[tk_op.op.type], remapped_params, symbols, wires - - -def extract_ops_from_tk(tk_circ): - """ - Extract the operations, and corresponding parameters and wires, - from a pytket Circuit. Return these as lists to use in - constructing PennyLane circuit. - - Parameters - ---------- - tk_circ : :class:`lambeq.backend.converters.tk.Circuit` - The pytket circuit to extract the operations from. - - Returns - ------- - list of :class:`qml.operation.Operation` - The PennyLane operations extracted from the pytket circuit. - list of list of (:class:`torch.FloatTensor` or - :class:`lambeq.backend.symbol.Symbol`) - The corresponding parameters of the operations. - list of list of int - The corresponding wires of the operations. - set of :class:`lambeq.backend.symbol.Symbol` - The free symbols in the parameters of the tket circuit. - - """ - op_list, params_list, wires_list = [], [], [] - symbols_set = set() - - for op in tk_circ.__iter__(): - if op.op.type != OpType.Measure: - op, params, symbols, wires = tk_op_to_pennylane(op) - op_list.append(op) - params_list.append(params) - wires_list.append(wires) - symbols_set.update(symbols) - - return op_list, params_list, wires_list, symbols_set - - -def get_post_selection_dict(tk_circ): - """ - Return post-selections based on qubit indices. - - Parameters - ---------- - tk_circ : :class:`lambeq.backend.converters.tk.Circuit` - The pytket circuit to extract the post-selections from. - - Returns - ------- - dict of int - A mapping from qubit indices to pytket classical indices. + remapped_params.append([param]) - """ - q_post_sels = {} - for q, c in tk_circ.qubit_to_bit_map.items(): - q_post_sels[q.index[0]] = tk_circ.post_selection[c.index[0]] - return q_post_sels + return ops, remapped_params, symbols, qubits -def to_pennylane(lambeq_circuit: Diagram, probabilities=False, - backend_config=None, diff_method='best'): +def to_pennylane(diagram: Diagram, + probabilities=False, + backend_config=None, + diff_method='best') -> PennyLaneCircuit: """ Return a PennyLaneCircuit equivalent to the input lambeq circuit. `probabilities` determines whether the PennyLaneCircuit @@ -191,7 +152,7 @@ def to_pennylane(lambeq_circuit: Diagram, probabilities=False, Parameters ---------- - lambeq_circuit : :class:`lambeq.backend.quantum.Diagram` + diagram : :class:`lambeq.backend.quantum.Diagram` The lambeq circuit to convert to PennyLane. probabilities : bool, default: False Determines whether the PennyLane @@ -217,28 +178,34 @@ def to_pennylane(lambeq_circuit: Diagram, probabilities=False, The PennyLane circuit equivalent to the input lambeq circuit. """ - - if any(isinstance(box, Measure) for box in lambeq_circuit.boxes): + if any(isinstance(box, Measure) for box in diagram.boxes): raise ValueError('Only pure circuits, or circuits with discards' ' are currently supported.') - if lambeq_circuit.is_mixed and lambeq_circuit.cod: + if diagram.is_mixed and diagram.cod: # Some qubits discarded, some left open print('Warning: Circuit includes both discards and open codomain' ' wires. All open wires will be discarded during conversion', file=sys.stderr) - tk_circ = lambeq_circuit.to_tk() - op_list, params_list, wires_list, symbols_set = ( - extract_ops_from_tk(tk_circ) - ) + is_mixed = diagram.is_mixed + + if not is_circuital(diagram): + diagram = to_circuital(diagram) + + circuitInfo = readoff_circuital(diagram) + + scalar = 1.0 + for gate in circuitInfo.gates: + if gate.gtype == 'Scalar' and gate.phase is not None: + scalar *= gate.phase + circuitInfo.gates.remove(gate) - post_selection = get_post_selection_dict(tk_circ) + ex_ops = extract_ops_from_circuital(circuitInfo.gates) + op_list, params_list, symbols_set, wires_list = ex_ops - scalar = 1 - for box in lambeq_circuit.boxes: - if isinstance(box, Scalar): - scalar *= box.array + # Get post selection bits + post_selection = circuitInfo.postmap return PennyLaneCircuit(op_list, list(symbols_set), @@ -246,9 +213,9 @@ def to_pennylane(lambeq_circuit: Diagram, probabilities=False, wires_list, probabilities, post_selection, - lambeq_circuit.is_mixed, + is_mixed, scalar, - tk_circ.n_qubits, + circuitInfo.totalQubits, backend_config, diff_method) diff --git a/lambeq/backend/pregroup_tree.py b/lambeq/backend/pregroup_tree.py index 65b5e4c9..4dd3d4ef 100644 --- a/lambeq/backend/pregroup_tree.py +++ b/lambeq/backend/pregroup_tree.py @@ -116,14 +116,14 @@ def __eq__(self, other: object) -> bool: == (other.parent.ind if other.parent else None)) and self.children == other.children) - def is_same_word(self, other: object) -> bool: + def is_same_word(self, other: object) -> bool: # type : [no-any-return] """Check if these words are the same words in the sentence. This is a relaxed version of the `__eq__` function which doesn't check equality of the children - essentially, this just checks if `other` is the same token.""" if not isinstance(other, PregroupTreeNode): - return NotImplemented + raise NotImplementedError return (self.word == other.word and self.ind == other.ind) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 47b1426f..81eecc23 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -32,13 +32,14 @@ from collections.abc import Callable from dataclasses import dataclass, field, replace from functools import partial -from typing import cast, Dict +import pickle +from typing import cast, Dict, Optional, Tuple, Union import numpy as np import tensornetwork as tn from typing_extensions import Any, Self -from lambeq.backend import grammar, tensor +from lambeq.backend import Functor, grammar, Symbol, tensor from lambeq.backend.numerical_backend import backend, get_backend from lambeq.backend.symbol import lambdify @@ -1162,3 +1163,536 @@ def CRz(phi, distance=1): return Controlled(Rz(phi), distance) # noqa: E731 'CRy': CRy, 'CRz': CRz, } + + +def is_circuital(diagram: Diagram) -> bool: + """ + Takes a :py:class:`lambeq.quantum.Diagram`, + checks if a diagram is a quantum 'circuital' diagram. + + Circuital means: + 1. All initial layers are qubits + 2. All post selections are at the end + + Allows for mixed_circuit measurements + + Returns + ------- + bool + Whether the diagram is a circuital diagram. + + """ + + if diagram.dom: + return False + + layers = diagram.layers + + num_qubits = sum([1 for layer in layers + if isinstance(layer.box, Ket)]) + + qubit_layers = layers[:num_qubits] + + if not all([isinstance(layer.box, Ket) for layer in qubit_layers]): + return False + + for qubit_layer in qubit_layers: + if len(qubit_layer.right): + return False + + # Check there are no gates in between post-selections. + measure_idx = [i for i, layer in enumerate(layers[num_qubits:]) + if isinstance(layer.box, (Discard, Bra))] + if not measure_idx: + return True + mmax = max(measure_idx) + mmin = min(measure_idx) + for i, gate in enumerate(layers[num_qubits:]): + if not isinstance(gate.box, (Discard, Bra, Measure)): + if i > mmin and i < mmax: + return False + + return True + + +def to_circuital(diagram: Diagram) -> Diagram: + """Takes a :py:class:`lambeq.quantum.Diagram`, returns + a modified :py:class:`lambeq.quantum.Diagram` which + is easier to convert to tket and other circuit simulators + + Parameters + ---------- + diagram : :py:class:`~lambeq.backend.quantum.Diagram` + The :py:class:`Circuits ` + to be converted to a tket circuit. + + The returned circuit diagram has all qubits at the top + with layer depth equal to qubit index, + followed by gates, and then post-selection + measurements at the bottom. + + Returns + ------- + :py:class:`lambeq.quantum.Diagram` + Circuital diagram compatible with circuital_to_dict. + """ + + # bits and qubits are lists of register indices, at layer i we want + # len(bits) == circuit[:i].cod.count(bit) and same for qubits + # Necessary to ensure editing boxes is localized. + serializedcircuit = pickle.dumps(diagram) + circuit = pickle.loads(serializedcircuit) + + qubits: list[Layer] = [] + gates: list[Layer] = [] + measures: list[Layer] = [] + postselect: list[Layer] = [] + circuit = circuit.init_and_discard() + + # Cleans up any '1' kets and converts them to X|0> -> |1> + def remove_ketbra1(_, box: Box) -> Diagram | Box: + ob_map: dict[Box, Diagram] + ob_map = {Ket(1): Ket(0) >> X, # type: ignore[dict-item] + Bra(1): X >> Bra(0)} # type: ignore[dict-item] + return ob_map.get(box, box) + + def add_qubit(qubits: list[Layer], + layer: Layer, + offset: int, + gates: list[Layer]) -> Tuple[list[Layer], list[Layer]]: + """ + Adds a qubit to the qubit list. + Shifts all the gates to accommodate new qubit. + Assumes we only add one qubit at a time. + """ + + for qubit_layer in qubits: + from_left = len(qubit_layer.left) + if from_left >= offset: + qubit_layer.left = qubit_layer.left.insert(layer.box.cod, + offset) + + layer.right = Ty() + if offset > 0: + layer.left = qubit ** offset + else: + layer.left = Ty() + qubits.insert(offset, layer) + + return qubits, pull_qubit_through(offset, gates, dom=layer.box.cod)[0] + + def construct_measurements(last_layer: Layer, + post_selects: list[Layer]) -> list[Layer]: + # Change to accommodate measurements before + total_qubits = (len(last_layer.left) + + len(last_layer.box.cod) + + len(last_layer.right)) + + bit_idx = list(range(total_qubits)) + q_idx = {} + for layer in post_selects: + # Find the qubit for each post selection + q_idx[bit_idx[len(layer.left)]] = layer + bit_idx.remove(bit_idx[len(layer.left)]) + + # Inserting to the left is always trivial + total_layer = ([*last_layer.left] + [*last_layer.box.cod] + + [*last_layer.right]) + + new_postselects = [] + for key in sorted(q_idx.keys()): + bits_left = sum([1 for i in bit_idx if i < key]) + q_idx[key].left = bit ** bits_left + q_idx[key].right = q_idx[key].right._fromiter(total_layer[key+1:]) + new_postselects.append(q_idx[key]) + + return new_postselects + + def pull_bit_through(q_idx: int, + gates: list[Layer], + layer: Layer) -> tuple[list[Layer], int]: + """ + Inserts a qubit type into every layer at the appropriate index + q_idx: idx - index of where to insert the gate. + """ + + for i, gate_layer in enumerate(gates): # noqa: B007 + + l_size = len(gate_layer.left) + c_size = len(gate_layer.box.cod) + d_size = len(gate_layer.box.dom) + + # Inserting to the left is always trivial + if q_idx == l_size: + break + elif q_idx < l_size: + gate_layer.left = gate_layer.left.replace(qubit, q_idx) + # Qubit on right of gate. Handles 1 qubit gates by l(dom) = 1 + elif q_idx > l_size + len(gate_layer.box.dom) - 1: + + # Index relative to the 1st qubit on right + r_rel = q_idx - (l_size + len(gate_layer.box.dom)) + + # Insert on right. Update relative index from the left + gate_layer.right = gate_layer.right.replace(qubit, r_rel) + + q_idx = r_rel + l_size + len(gate_layer.box.cod) + + elif c_size == d_size: + # Initial control qubit box + box = gate_layer.box + box.dom = box.dom.replace(qubit, q_idx - l_size) + box.cod = box.cod.replace(qubit, q_idx - l_size) + + else: + raise NotImplementedError('Cannot pull bit through ' + f'box {gate_layer}') + + # Insert layer back into list and remove from the original + layer = build_left_right(q_idx, layer, [gates[i-1]]) + gates.insert(i, layer) + + return gates, q_idx + + def pull_qubit_through(q_idx: int, + gates: list[Layer], + dom: Ty = qubit) -> tuple[list[Layer], int]: # noqa: E501 + """ + Inserts a qubit type into every layer at the appropriate index + q_idx: idx - index of where to insert the gate. + """ + new_gates = [] + for gate_layer in gates: + + l_size = len(gate_layer.left) + + # Inserting to the left is always trivial + if q_idx <= l_size: + gate_layer.left = gate_layer.left.insert(dom, q_idx) + new_gates.append(gate_layer) + # Qubit on right of gate. Handles 1 qubit gates by l(dom) = 1 + elif q_idx > l_size + len(gate_layer.box.dom) - 1: + + # Index relative to the 1st qubit on right + r_rel = q_idx - (l_size + len(gate_layer.box.dom)) + + # Insert on right. Update relative index from the left + gate_layer.right = gate_layer.right.insert(dom, r_rel) + + q_idx = r_rel + l_size + len(gate_layer.box.cod) + new_gates.append(gate_layer) + + else: + if isinstance(gate_layer.box, Controlled): + gate_qubits = [len(gate_layer.left) + j + for j in range(len(gate_layer.box.dom))] + + # Initial control qubit box + dists = [0] + curr_box: Box | Controlled = gate_layer.box + while isinstance(curr_box, Controlled): + # Compute relative index control qubits + dists.append(curr_box.distance + sum(dists)) + curr_box = curr_box.controlled + + prev_pos = -1 * min(dists) + gate_qubits[0] + curr_box = gate_layer.box + + while isinstance(curr_box, Controlled): + curr_pos = prev_pos + curr_box.distance + if prev_pos < q_idx and q_idx <= curr_pos: + curr_box.distance = curr_box.distance + 1 + + elif q_idx <= prev_pos and q_idx > curr_pos: + curr_box.distance = curr_box.distance - 1 + + prev_pos = curr_pos + curr_box = curr_box.controlled + + box = gate_layer.box + + box.dom = box.dom.insert(dom, q_idx - l_size) + box.cod = box.cod.insert(dom, q_idx - l_size) + new_gates.append(gate_layer) + + if isinstance(gate_layer.box, Swap): + + """ + Replace single swap with a series of swaps + Swaps are 2 wide, so if a qubit is pulled through we + have to use the pulled qubit as an temp ancillary. + """ + new_gates.append(Layer(gate_layer.left, + Swap(qubit, qubit), + dom >> gate_layer.right)) + new_gates.append(Layer(dom >> gate_layer.left, + Swap(qubit, qubit), + gate_layer.right)) + new_gates.append(Layer(gate_layer.left, + Swap(qubit, qubit), + dom >> gate_layer.right)) + + return new_gates, q_idx + + def build_left_right(q_idx: int, + layer: Layer, + layers: list[Layer]) -> Layer: + """ + We assume that the left and right are constructable + from the last gate + and the left position of the bra. + (We type check at the end.) + Rebuild left and right based on the last layer + """ + if len(layers) == 0: + return layer + + gate_layer = layers[-1] + + total_layer = ([*gate_layer.left] + [*gate_layer.box.cod] + + [*gate_layer.right]) + + # Assumes you're only inserting one qubit at a time + total_layer[q_idx] = layer.box.cod + + if q_idx == 0 or not total_layer[:q_idx]: + layer.left = Ty() + else: + layer.left = layer.left._fromiter(total_layer[:q_idx]) + + if q_idx == len(total_layer) - 1 or not total_layer[q_idx+1:]: + layer.right = Ty() + else: + layer.right = layer.right._fromiter(total_layer[q_idx+1:]) + + return layer + + circuit = Functor(target_category=quantum, + ob=lambda _, x: x, + ar=remove_ketbra1)(circuit) # type: ignore [arg-type] + + layers = circuit.layers + + for i, layer in enumerate(layers): + + if isinstance(layer.box, Ket): + qubits, gates = add_qubit(qubits, + layer, + len(layer.left), + gates) + + elif isinstance(layer.box, (Bra, Discard)): + + q_idx = len(layer.left) + layers[i+1:], q_idx = pull_qubit_through(q_idx, layers[i+1:]) + layer = build_left_right(q_idx, layer, layers[i+1 :]) + + postselect.insert(0, layer) + + else: + gates.append(layer) + + if gates: + postselect = construct_measurements(gates[-1], postselect) + + # Rebuild the diagram + diags = [Diagram(dom=layer.dom, cod=layer.cod, layers=[layer]) # type: ignore [arg-type] # noqa: E501 + for layer in qubits + gates + postselect + measures] + + layerD = diags[0] + for diagram in diags[1:]: + layerD = layerD >> diagram + + return layerD + + +@dataclass +class Gate: + """Gate information for backend circuit construction. + + Parameters + ---------- + name : str + Arbitrary name / id + gtype : str + Type for backend conversion, e.g., 'Rx', 'X', etc. + qubits : list[int] + List of qubits the gate acts on. + phase : Union[float, Symbol, None] = 0 + Phase parameter for gate. + dagger : bool = False + Whether to dagger the gate. + control : Optional[list[int]] = None + For control gates, list of all the control qubits. + gate_q : Optional[int] = None + For control gates, the gates being controlled. + """ + name: str + gtype: str + qubits: list[int] + phase: Union[float, Symbol, None] = 0 + dagger: bool = False + control: Optional[list[int]] = None + gate_q: Optional[int] = None + + +def gateFromBox(box: Box, offset: int) -> Gate: + """Constructs Gate for backend circuit construction + from a Box. + + Parameters + ---------- + box : Box + Box to convert to a Gate. + offset : int + Qubit index on the leftmost part of the Gate. + """ + name = box.name + gtype = box.name.split('(')[0] + qubits = [offset + j for j in range(len(box.dom))] + phase = None + dagger = False + control = None + gate_q = None + + if isinstance(box, Daggered): + box = box.dagger() + dagger = True + gtype = box.name.split('(')[0] + + if isinstance(box, (Rx, Ry, Rz)): + phase = box.phase + if isinstance(box.phase, Symbol): + # Tket uses sympy, lambeq uses custom symbol + phase = box.phase.to_sympy() + + elif isinstance(box, Controlled): + + # reverse the distance order + dists = [] + curr_box: Box | Controlled = box + while isinstance(curr_box, Controlled): + dists.append(curr_box.distance) + curr_box = curr_box.controlled + dists.reverse() + + # Index of the controlled qubit is the last entry in rel_idx + rel_idx = [0] + for dist in dists: + if dist > 0: + # Add control to the left, offset by distance + rel_idx = [0] + [i + dist for i in rel_idx] + else: + # Add control to the right, don't offset + right_most_idx = max(rel_idx) + rel_idx.insert(-1, right_most_idx - dist) + + i_qubits = [qubits[i] for i in rel_idx] + + qubits = i_qubits + control = sorted(qubits[:-1]) + gate_q = qubits[-1] + + if gtype in ('CRx', 'CRz'): + phase = box.phase + if isinstance(box.phase, Symbol): + # Tket uses sympy, lambeq uses custom symbol + phase = box.phase.to_sympy() + + elif isinstance(box, Scalar): + gtype = 'Scalar' + phase = box.array + + return Gate( + name, + gtype, + qubits, + phase, + dagger, + control, + gate_q + ) + + +@dataclass +class CircuitInfo: + """Info for constructing circuits with backends. + + Parameters + ---------- + totalQubits : int + Total number of qubits in the circuit. + gates : list[:py:class:`~lambeq.backend.quantum.Gate`] + List containing gates, in topological ordering. + bitmap: dict[int, int] + Dictionary mapping qubit index to bit index for + measurements, postselection, etc. + postmap: dict[int, int] + Dictionary mapping qubit index to post selection value. + discards: list[int] + List of discarded qubit indeces. + """ + + totalQubits: int + gates: list[Gate] + bitmap: dict[int, int] + postmap: dict[int, int] + discards: list[int] + + +def readoff_circuital(diagram: Diagram) -> CircuitInfo: + """Takes a circuital :py:class:`lambeq.quantum.Diagram`, returns + a :py:class:`~lambeq.backend.quantum.CircuitInfo` which + is used by quantum backends to construct circuits. + Will check if the diagram is circuital before converting. + + Parameters + ---------- + diagram : :py:class:`~lambeq.backend.quantum.Diagram` + The :py:class:`Circuits ` + to be converted to dictionary. + + Returns + ------- + :py:class:`~lambeq.backend.quantum.CircuitInfo` + """ + + assert is_circuital(diagram) + + layers = diagram.layers + + totalQubits = sum([1 for layer in layers if isinstance(layer.box, Ket)]) + available_qubits = list(range(totalQubits)) + + gates: list[Gate] = [] + bitmap: dict = {} + postmap: dict = {} + discards: list[int] = [] + + for layer in layers: + if isinstance(layer.box, Ket): + pass + elif isinstance(layer.box, Measure): + qi = available_qubits[layer.left.count(qubit)] + available_qubits.remove(qi) + bitmap[qi] = len(bitmap) + + elif isinstance(layer.box, Bra): + qi = available_qubits[layer.left.count(qubit)] + available_qubits.remove(qi) + bitmap[qi] = len(bitmap) + postmap[qi] = layer.box.bit + + elif isinstance(layer.box, Discard): + qi = available_qubits[layer.left.count(qubit)] + available_qubits.remove(qi) + discards.append(qi) + else: + qi = len(layer.left) + gates.append(gateFromBox(layer.box, qi)) + + return CircuitInfo(totalQubits, + gates, + bitmap, + postmap, + discards) diff --git a/tests/backend/converters/test_tket_conversion.py b/tests/backend/converters/test_tket_conversion.py index d0391fb8..b0c7e788 100644 --- a/tests/backend/converters/test_tket_conversion.py +++ b/tests/backend/converters/test_tket_conversion.py @@ -27,7 +27,7 @@ tket_circuits = [ Circuit(3).H(0).CX(0,1).CX(1,2), - Circuit(4, 4).CX(0, 3).CX(1, 2).Measure(2, 1).Measure(3, 3).H(0).H(1).Measure(1, 0).Measure(0, 2).post_select({0: 0, 1: 0, 2: 0, 3: 0}), + Circuit(4, 4).CX(0, 3).CX(1, 2).Measure(2, 2).Measure(3, 3).H(0).H(1).Measure(0, 0).Measure(1, 1).post_select({0: 0, 1: 0, 2: 0, 3: 0}), Circuit(3).CCX(0, 1, 2).CCX(0, 2, 1).CCX(1, 2, 0), Circuit(4, 4).S(0).X(1).Y(2).Z(3).Rx(0.6, 0).Ry(0.4, 1).Rz(0.2, 2).H(3).T(0).Tdg(1).H(2).H(3).Measure(0, 0).Measure(1, 1).Measure(2, 2).Measure(3, 3).post_select({0: 0, 1: 0, 2: 0, 3: 0}).scale(0.25) ] @@ -55,7 +55,7 @@ @pytest.mark.parametrize('diagram, tket_circuit', zip(diagrams, tket_circuits)) def test_tp_tk(diagram, tket_circuit): - tket_diag = to_tk(diagram) + tket_diag = to_tk(diagram) assert tket_diag == tket_circuit @pytest.mark.parametrize('tket_circuit, reverse_conversion', zip(tket_circuits, reverse_conversions)) @@ -75,13 +75,13 @@ def test_hybrid_circs(): '.H(1)'\ '.CX(1, 2)'\ '.CX(0, 1)'\ - '.Measure(1, 0)'\ + '.Measure(1, 1)'\ '.H(0)'\ - '.Measure(0, 1)'\ + '.Measure(0, 0)'\ '.post_select({0: 0, 1: 0})'\ '.scale(4)' - assert repr((CX >> Measure() @ Measure() >> Swap(bit, bit)).to_tk())\ - == "tk.Circuit(2, 2).CX(0, 1).Measure(1, 0).Measure(0, 1)" + assert repr(((CX >> Measure() @ Measure()) >> Swap(bit, bit)).to_tk())\ + == "tk.Circuit(2, 2).CX(0, 1).SWAP(0, 1).Measure(0, 0).Measure(1, 1)" def test_back_n_forth(): diff --git a/tests/backend/test_quantum.py b/tests/backend/test_quantum.py index a20b5a84..c2fce679 100644 --- a/tests/backend/test_quantum.py +++ b/tests/backend/test_quantum.py @@ -25,6 +25,45 @@ def test_Ty(): assert qubit ** 0 == Ty() assert qubit ** 3 == qubit @ qubit @ qubit +def test_Insert(): +# Define some atomic and complex types for testing + tensor = qubit @ qubit # A complex type with two 'qubit' + +# Insert an atomic type into a complex type + result = tensor.insert(bit, 1) + expected = Ty('qubit') @ Ty('bit') @ Ty('qubit') + assert result == expected + +# Insert a complex type into another complex type + complex_type = qubit @ bit # A new complex type + result = tensor.insert(complex_type, 1) + expected = Ty('qubit') @ Ty('qubit') @ Ty('bit') @ Ty('qubit') + assert result == expected + +# Insert at the start of the complex type + result = tensor.insert(bit, 0) + expected = Ty('bit') @ Ty('qubit') @ Ty('qubit') + assert result == expected + +# Insert at the end of the complex type + result = tensor.insert(bit, 2) + expected = Ty('qubit') @ Ty('qubit') @ Ty('bit') + assert result == expected + +# Insert into an empty type + empty_type = Ty() + result = empty_type.insert(bit, 0) + expected = bit + assert result == expected + +# Test inserting at an index out of bounds + with pytest.raises(IndexError): + tensor.insert(bit, 5) + +# Test inserting with a negative index + result = tensor.insert(bit, -1) + expected = Ty('qubit') @ Ty('bit') @ Ty('qubit') + assert result == expected def test_dagger(): @@ -170,3 +209,23 @@ def test_eval_w_aer_backend(): backend = AerBackend() assert ((Ket(0) >> Measure()) @ (Ket(1) >> Measure())).eval(backend=backend) == pytest.approx(np.array([[0, 1], [0, 0]])) + +def test_to_circuital(): + circ = to_circuital((Ket(0) >> H >> Measure())) + assert is_circuital(circ) + cdict = readoff_circuital(circ) + assert cdict.gates[0].gtype == 'H' + assert cdict.gates[0].qubits == [0] + assert cdict.gates[0].phase == None + assert cdict.gates[0].dagger == False + + +def test_is_circuital(): + circ = (Ket(0) >> H >> Measure()) + assert is_circuital(circ) + + circ = (Ket(0) >> H) @ (Ket(0) >> H ) + assert not is_circuital(circ) + + +