From 08462a3495c292d3871f111dbc472ffda8d2543d Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 20 Sep 2024 14:10:45 +0100 Subject: [PATCH 01/40] Added make_circuital function. Makes compatible with tket, pennylane, and other backends. Type checks. --- lambeq/backend/quantum.py | 253 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 47b1426f..e9eac691 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1162,3 +1162,256 @@ def CRz(phi, distance=1): return Controlled(Rz(phi), distance) # noqa: E731 'CRy': CRy, 'CRz': CRz, } + +def is_circuital(diagram: Diagram) -> bool: + """Check if a diagram is a quantum circuit diagram. + + Adapted from :py:class:`...`. + + Returns + ------- + bool + Whether the diagram is a circuital diagram. + + """ + + + if diagram.dom: + # quantum circuit diagrams must have empty domain? + # Maybe they have a domain for qubits? + return False + + + in_qubit = True + in_gates = False + for layer in self.layers: + if in_qubit and isinstance(layer.box, Qubit): + if not layer.right.is_empty: + return False + else: + if not isinstance(layer.box, (Cup, Swap)): + return False + in_qubit = False + return True + + +from pytket.circuit import (Bit, Command, Op, OpType, Qubit) +from pytket.utils import probs_from_counts +from lambeq.backend import Functor, Symbol +from lambeq.backend.converters.tk import Circuit + +# String -> OpType mapping +OPTYPE_MAP = {'H': OpType.H, + 'X': OpType.X, + 'Y': OpType.Y, + 'Z': OpType.Z, + 'S': OpType.S, + 'T': OpType.T, + 'Rx': OpType.Rx, + 'Ry': OpType.Ry, + 'Rz': OpType.Rz, + 'CX': OpType.CX, + 'CZ': OpType.CZ, + 'CRx': OpType.CRx, + 'CRy': OpType.CRy, + 'CRz': OpType.CRz, + 'CCX': OpType.CCX, + 'Swap': OpType.SWAP} + + +def make_circuital(circuit: Diagram): + """ + Takes a :py:class:`lambeq.quantum.Diagram`, returns + a :py:class:`Circuit`. + The returned circuit diagram has all qubits at the top with layer depth equal to qubit index, + followed by gates, and then measurements at the bottom. + """ + + # bits and qubits are lists of register indices, at layer i we want + # len(bits) == circuit[:i].cod.count(bit) and same for qubits + + qubits = [] + bits = [] + gates = [] + + circuit = circuit.init_and_discard() # Keep. + + # Cleans up any '1' kets and converts them to X|0> -> |1> + # Keep this in make_circuital + def remove_ket1(_, box: Box) -> Diagram | Box: + ob_map: dict[Box, Diagram] + ob_map = {Ket(1): Ket(0) >> X} # type: ignore[dict-item] + return ob_map.get(box, box) + + def add_qubit(qubits: list[int], + layer: Layer, + offset: int, + gates) -> list[int]: + + # Adds a qubit to the qubit list + # Appends shifts all the gates + + # Will I ever have types other than single qubits? - BW + for qubit_layer in qubits: + if qubit_layer.left.count(qubit) >= offset: + qubit_layer.left = qubit >> qubit_layer.left + + layer.right = Ty() + qubits.insert(offset, layer) + + return qubits, pull_through(layer, offset, gates) + + def add_measure(bits: list[int], + layer: Layer, + r_offset: int, + gates) -> list[int]: + + # Insert measurements on the right + for bit_layer in bits: + if bit_layer.right.count(qubit) >= r_offset: + bit_layer.right = qubit >> bit_layer.right + + + offset = layer.left.count(qubit) + layer.left = Ty() + bits.insert(-r_offset if r_offset > 0 else len(bits), layer) + + return bits, pull_through(layer, offset, gates) + + + def pull_through(layer, offset:idx, gates): + + + # Modify gates to account for the new qubit being pulled to the top. + for gate_layer in gates: + box = gate_layer.box + + # Idx of the first qubit in the gate before adding the new qubit + qubit_start = gate_layer.left.count(qubit) + orig_qubits = [qubit_start + j for j in range(len(box.dom))] + num_qubits = len(orig_qubits) + qubit_last = orig_qubits[-1] + + # Checks if we have to bring the qubit up through the gate. + # Only if past the first qubit + gate_contains_qubit = qubit_start < offset and offset <= qubit_last + + if num_qubits == 1 or not gate_contains_qubit: + if qubit_start >= offset: + gate_layer.left = qubit >> gate_layer.left + else: + gate_layer.right = qubit >> gate_layer.right + + else: + if isinstance(box, Controlled): + + # Initial control qubit box + dists = [0] + curr_box: Box | Controlled = box + while isinstance(curr_box, Controlled): + # Append the relative index of the next qubit in the sequence + # The one furthest left relative to the initial control qubit + # tells us the distance from the left of the box + dists.append(curr_box.distance + sum(dists)) + curr_box = curr_box.controlled + + # Obtain old absolute index of the old controlled qubit + prev_pos = -1 * min(dists) + qubit_start + curr_box: Box | Controlled = box + + while isinstance(curr_box, Controlled): + curr_pos = prev_pos + curr_box.distance + if prev_pos < offset and offset <= curr_pos: + curr_box.distance = curr_box.distance + 1 + + elif offset <= prev_pos and offset > curr_pos: + curr_box.distance = curr_box.distance - 1 + + prev_pos = curr_pos + curr_box = curr_box.controlled + box.dom = qubit >> box.dom + box.cod = qubit >> box.cod + + if isinstance(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. + gates.append(Layer(Swap(qubit, qubit), layer.left, qubit >> layer.right)) + gates.append(Layer(Swap(qubit, qubit), qubit >> layer.left, layer.right)) + gates.append(Layer(Swap(qubit, qubit), layer.left, qubit >> layer.right)) + + + + return gates + + circuit = Functor(target_category=quantum, # type: ignore [assignment] + ob=lambda _, x: x, + ar=remove_ket1)(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, layer.left.count(qubit), gates) + elif isinstance(layer.box, (Measure, Bra, Discard)): + br_i = i + break + else: + gates.append(layer) + + # reverse and add kets + # Assumes that once you hit a bra there won't be any more kets. + post_gates = [] + for i, layer in reversed(list(enumerate(layers))): + + box = layer.box + if isinstance(box, (Measure, Bra, Discard)): + bits, post_gates = add_measure(bits, layers[i], layer.right.count(qubit), post_gates) + else: + post_gates.insert(0, layer) + + if br_i == i: + break + + #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 + + qubitDom = qubit ** len(qubits) + + def build_from_layers(layers: list[Layer]) -> Diagram: + # Type checking at the end + layerDiags = [Diagram(dom=layer.dom, cod = layer.cod, layers = [layer]) for layer in layers] + layerD = layerDiags[0] + for layer in layerDiags[1:]: + layerD = layerD >> layer + return layerD + + diag = build_from_layers(qubits + gates + post_gates + bits) + if diag.dom != circuit.dom or diag.cod != circuit.cod: + raise ValueError('Circuit conversion failed. The domain and codomain of the circuit do not match the domain and codomain of the diagram.') + + return diag From dee07d5340e0014539db079830f228bd2b73f1d3 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 20 Sep 2024 14:10:45 +0100 Subject: [PATCH 02/40] Added make_circuital function. Makes compatible with tket, pennylane, and other backends. Type checks. From b8e14c7ea9108c82ab9e3fd3211578c1646735e7 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 4 Oct 2024 17:39:33 +0100 Subject: [PATCH 03/40] Added is_circuital and circuital_to_dict helper functions. Will clean up in future commit. --- lambeq/backend/quantum.py | 438 +++++++++++++++++++++++++++++++++++--- 1 file changed, 409 insertions(+), 29 deletions(-) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index e9eac691..d18fd825 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1177,21 +1177,23 @@ def is_circuital(diagram: Diagram) -> bool: if diagram.dom: - # quantum circuit diagrams must have empty domain? - # Maybe they have a domain for qubits? return False + layers = diagram.layers + + # Check if the first and last layers are all qubits and measurements + num_qubits = sum([1 for l in layers if isinstance(l.box, Ket)]) + num_measures = sum([1 for l in layers if isinstance(l.box, (Bra, Measure, Discard))]) + + qubit_layers = layers[:num_qubits] + measure_layers = layers[-num_measures:] + + if not all([isinstance(layer.box, Ket) for layer in qubit_layers]): + return False + + if not all([isinstance(layer.box, (Bra, Measure, Discard)) for layer in measure_layers]): + return False - in_qubit = True - in_gates = False - for layer in self.layers: - if in_qubit and isinstance(layer.box, Qubit): - if not layer.right.is_empty: - return False - else: - if not isinstance(layer.box, (Cup, Swap)): - return False - in_qubit = False return True @@ -1200,23 +1202,6 @@ def is_circuital(diagram: Diagram) -> bool: from lambeq.backend import Functor, Symbol from lambeq.backend.converters.tk import Circuit -# String -> OpType mapping -OPTYPE_MAP = {'H': OpType.H, - 'X': OpType.X, - 'Y': OpType.Y, - 'Z': OpType.Z, - 'S': OpType.S, - 'T': OpType.T, - 'Rx': OpType.Rx, - 'Ry': OpType.Ry, - 'Rz': OpType.Rz, - 'CX': OpType.CX, - 'CZ': OpType.CZ, - 'CRx': OpType.CRx, - 'CRy': OpType.CRy, - 'CRz': OpType.CRz, - 'CCX': OpType.CCX, - 'Swap': OpType.SWAP} def make_circuital(circuit: Diagram): @@ -1415,3 +1400,398 @@ def build_from_layers(layers: list[Layer]) -> Diagram: raise ValueError('Circuit conversion failed. The domain and codomain of the circuit do not match the domain and codomain of the diagram.') return diag + +# String -> OpType mapping +OPTYPE_MAP = {'H': OpType.H, + 'X': OpType.X, + 'Y': OpType.Y, + 'Z': OpType.Z, + 'S': OpType.S, + 'T': OpType.T, + 'Rx': OpType.Rx, + 'Ry': OpType.Ry, + 'Rz': OpType.Rz, + 'CX': OpType.CX, + 'CZ': OpType.CZ, + 'CRx': OpType.CRx, + 'CRy': OpType.CRy, + 'CRz': OpType.CRz, + 'CCX': OpType.CCX, + 'Swap': OpType.SWAP} + +def circuital_to_dict(diagram): + + assert is_circuital(diagram) + + circuit_dict = {} + layers = diagram.layers + + num_qubits = sum([1 for l in layers if isinstance(l.box, Ket)]) + num_measures = sum([1 for l in layers if isinstance(l.box, (Bra, Measure, Discard))]) + + qubit_layers = layers[:num_qubits] + measure_layers = layers[-num_measures:] + gates = layers[num_qubits:-num_measures] + + circuit_dict['num_qubits'] = num_qubits + circuit_dict['qubits'] = len(qubit_layers) + circuit_dict['gates'] = [] + +# for i, layer in enumerate(qubit_layers): +# circuit_dict['qubit_layers'].append(gate_to_dict(layer.box, layer.left.count(qubit))) + + for i, layer in enumerate(gates): + circuit_dict['gates'].append(gate_to_dict(layer.box, layer.left.count(qubit))) + + + return circuit_dict + +#def qubit_to_dict(qubit: Box) -> Dict: +# qdict = {} + + +def gate_to_dict(box: Box, offset:int) -> Dict: + + gdict = {} + gdict['type'] = box.name + gdict['qubits'] = [offset + j for j in range(len(box.dom))] + + is_dagger = False + if isinstance(box, Daggered): + box = box.dagger() + is_dagger = True + + gdict['dagger'] = is_dagger + + i_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() + + gdict['phase'] = 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] + + gdict['control'] = [i_qubits[i] for i in rel_idx[:-1]] + gdict['gate_q'] = offset + rel_idx[-1] + #gdict['i_qubits'] = [i_qubits[i] for i in rel_idx] + + + name = box.name.split('(')[0] + gdict['type'] = name + + if name in ('CRx', 'CRz'): + gdict['phase'] = box.phase + if isinstance(box.phase, Symbol): + # Tket uses sympy, lambeq uses custom symbol + gdict['phase'] = box.phase.to_sympy() + +# 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) +# +# tk_circ.add_gate(op, i_qubits) + + return gdict + + +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_ket1(_, box: Box) -> Diagram | Box: + ob_map: dict[Box, Diagram] + ob_map = {Ket(1): Ket(0) >> X} # 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_ket1)(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 + elif isinstance(theta, sympy.Symbol): + return Symbol(theta.name) + elif isinstance(theta, sympy.Mul): + scale, symbol = theta.as_coeff_Mul() + if not isinstance(symbol, sympy.Symbol): + raise ValueError('Parameter must be a (possibly scaled) sympy' + 'Symbol') + return Symbol(symbol.name, scale=scale) + else: + raise ValueError('Parameter must be a (possibly scaled) sympy Symbol') + + +def from_tk(tk_circuit: tk.Circuit) -> Diagram: + """Translates from tket to a lambeq Diagram.""" + tk_circ: Circuit = Circuit.upgrade(tk_circuit) + n_qubits = tk_circ.n_qubits + + def box_and_offset_from_tk(tk_gate) -> tuple[Diagram, int]: + name: str = tk_gate.op.type.name + offset = tk_gate.args[0].index[0] + box: Box | Diagram | None = None + + if name.endswith('dg'): + new_tk_gate = Command(tk_gate.op.dagger, tk_gate.args) + undaggered_box, offset = box_and_offset_from_tk(new_tk_gate) + box = undaggered_box.dagger() + return box.to_diagram(), offset + + if len(tk_gate.args) == 1: # single qubit gate + if name == 'Rx': + box = Rx(_tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5) + elif name == 'Ry': + box = Ry(_tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5) + elif name == 'Rz': + box = Rz(_tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5) + elif name in GATES: + box = cast(Box, GATES[name]) + + if len(tk_gate.args) == 2: # two qubit gate + distance = tk_gate.args[1].index[0] - tk_gate.args[0].index[0] + offset = tk_gate.args[0].index[0] + + if distance < 0: + offset += distance + + if name == 'CRx': + box = CRx( + _tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5, distance) + elif name == 'CRy': + box = CRy( + _tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5, distance) + elif name == 'CRz': + box = CRz( + _tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5, distance) + elif name == 'SWAP': + distance = abs(distance) + idx = list(range(distance + 1)) + idx[0], idx[-1] = idx[-1], idx[0] + box = Diagram.permutation(qubit ** (distance + 1), idx) + elif name == 'CX': + box = Controlled(X, distance) + elif name == 'CY': + box = Controlled(Y, distance) + elif name == 'CZ': + box = Controlled(Z, distance) + + if len(tk_gate.args) == 3: # three qubit gate + controls = (tk_gate.args[0].index[0], tk_gate.args[1].index[0]) + target = tk_gate.args[2].index[0] + span = max(controls + (target,)) - min(controls + (target,)) + 1 + if name == 'CCX': + box = Id(qubit**span).apply_gate(CCX, *controls, target) + elif name == 'CCZ': + box = Id(qubit**span).apply_gate(CCZ, *controls, target) + offset = min(controls + (target,)) + + if box is None: + raise NotImplementedError + else: + return box.to_diagram(), offset # type: ignore [return-value] + + circuit = Ket(*(0, ) * n_qubits).to_diagram() + bras = {} + for tk_gate in tk_circ.get_commands(): + if tk_gate.op.type.name == 'Measure': + offset: int = tk_gate.qubits[0].index[0] + bit_index: int = tk_gate.bits[0].index[0] + if bit_index in tk_circ.post_selection: + bras[offset] = tk_circ.post_selection[bit_index] + continue # post selection happens at the end + left = circuit.cod[:offset] + right = circuit.cod[offset + 1:] + circuit = circuit >> left @ Measure() @ right + else: + box, offset = box_and_offset_from_tk(tk_gate) + left = circuit.cod[:offset] + right = circuit.cod[offset + len(box.dom):] + circuit = circuit >> left @ box @ right + circuit = circuit >> Id().tensor(*( # type: ignore[arg-type] + Bra(bras[i]) if i in bras + else Discard() if x == qubit else Id(bit) + for i, x in enumerate(circuit.cod))) + if tk_circ.scalar != 1: + circuit = circuit @ Scalar(np.sqrt(abs(tk_circ.scalar))) + return circuit >> tk_circ.post_processing # type: ignore [return-value] From 94588f77157419b260ab5d0b668200e6656db5cd Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Mon, 21 Oct 2024 02:43:00 +0100 Subject: [PATCH 04/40] Added to_pennylane and restructured --- lambeq/backend/converters/tk.py | 39 +++- lambeq/backend/grammar.py | 4 +- lambeq/backend/pennylane.py | 123 +++++++++++- lambeq/backend/quantum.py | 346 ++------------------------------ 4 files changed, 181 insertions(+), 331 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 3cee41ad..3c046e04 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -35,7 +35,7 @@ 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) + Rx, Ry, Rz, Scalar, Swap, X, Y, Z, is_circuital, circuital_to_dict, to_circuital) OPTYPE_MAP = {'H': OpType.H, 'X': OpType.X, @@ -192,7 +192,7 @@ def get_counts(self, return counts -def to_tk(circuit: Diagram): +def to_tk_old(circuit: Diagram): """ Takes a :py:class:`lambeq.quantum.Diagram`, returns a :py:class:`Circuit`. @@ -362,6 +362,41 @@ def _tk_to_lmbq_param(theta): raise ValueError('Parameter must be a (possibly scaled) sympy Symbol') +def to_tk(diagram): + + if not is_circuital(diagram): + diagram = to_circuital(diagram) + + circuit_dict = circuital_to_dict(diagram) + + circuit = Circuit(circuit_dict["qubits"], circuit_dict["qubits"]) + + for gate in circuit_dict["gates"]: + + if not gate["type"] in OPTYPE_MAP: + raise NotImplementedError(f"Gate {gate} not supported") + + if "phase" in gate: + op = Op.create(OPTYPE_MAP[gate["type"]], 2 * gate["phase"]) + else: + op = Op.create(OPTYPE_MAP[gate["type"]]) + + if gate["dagger"]: + op = op.dagger + + qubits = gate["qubits"] + + circuit.add_gate(op, qubits) + + for measure in circuit_dict["measures"]: + if measure["type"] == "Measure": + circuit.Measure(measure["qubit"], measure["qubit"]) + elif measure["type"] == "Bra": + circuit.post_select({measure["qubit"]: measure["qubit"]}) + + 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..af1d9948 100644 --- a/lambeq/backend/grammar.py +++ b/lambeq/backend/grammar.py @@ -1912,14 +1912,14 @@ 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 :align: center >>> F = Functor(grammar, lambda _, ty : ty @ ty) >>> F(diag).draw( - ... figsize=(2, 2), path='./snake-2.png') + ... figsize=(2, 2), path='./docs/_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..a4264aff 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -50,11 +50,34 @@ import sympy import torch -from lambeq.backend.quantum import Measure, Scalar +from lambeq.backend.quantum import Scalar, is_circuital, to_circuital, circuital_to_dict if TYPE_CHECKING: from lambeq.backend.quantum import Diagram +OP_MAP_COMPOSED = { + '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, +} + OP_MAP = { OpType.X: qml.PauliX, OpType.Y: qml.PauliY, @@ -122,6 +145,99 @@ def tk_op_to_pennylane(tk_op): return OP_MAP[tk_op.op.type], remapped_params, symbols, wires +def extract_ops_from_circuital(circuit_dict): + print(circuit_dict) + + ops = [OP_MAP_COMPOSED[x["type"]] for x in circuit_dict["gates"]] + qubits = [x["qubits"] for x in circuit_dict["gates"]] + params = [x["phase"] if "phase" in x else [] for x in circuit_dict["gates"]] + + symbols = set() + + remapped_params = [] + for param in params: + + # 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 + return ops, remapped_params, symbols, qubits + + +def to_pennylane(diagram: Diagram, probabilities=False, + backend_config=None, diff_method='best'): + """ + Return a PennyLaneCircuit equivalent to the input lambeq + circuit. `probabilities` determines whether the PennyLaneCircuit + returns states (as in lambeq), or probabilities (to be more + compatible with automatic differentiation in PennyLane). + + Parameters + ---------- + lambeq_circuit : :class:`lambeq.backend.quantum.Diagram` + The lambeq circuit to convert to PennyLane. + probabilities : bool, default: False + Determines whether the PennyLane + circuit outputs states or un-normalized probabilities. + Probabilities can be used with more PennyLane backpropagation + methods. + backend_config : dict, default: None + A dictionary of PennyLane backend configration options, + including the provider (e.g. IBM or Honeywell), the device, + the number of shots, etc. See the `PennyLane plugin + documentation `_ + for more details. + diff_method : str, default: "best" + The differentiation method to use to obtain gradients for the + PennyLane circuit. Some gradient methods are only compatible + with simulated circuits. See the `PennyLane documentation + `_ + for more details. + + Returns + ------- + :class:`PennyLaneCircuit` + The PennyLane circuit equivalent to the input lambeq circuit. + + """ + + if not is_circuital(diagram): + diagram = to_circuital(diagram) + + circuit_dict = circuital_to_dict(diagram) + + op_list, params_list, symbols_set, wires_list = extract_ops_from_circuital(circuit_dict) + + # Get post selection bits + post_selection = {} + for measure in circuit_dict["measures"] : + post_selection[measure["qubit"]] = measure["qubit"] + + scalar = 1 + for gate in circuit_dict["gates"]: + if gate["type"] == "Scalar": + scalar *= gate["array"] + + return PennyLaneCircuit(op_list, + list(symbols_set), + params_list, + wires_list, + probabilities, + post_selection, + scalar, + circuit_dict["qubits"], + backend_config, + diff_method) + + def extract_ops_from_tk(tk_circ): """ Extract the operations, and corresponding parameters and wires, @@ -181,7 +297,8 @@ def get_post_selection_dict(tk_circ): return q_post_sels -def to_pennylane(lambeq_circuit: Diagram, probabilities=False, + +def to_pennylane_old(lambeq_circuit: Diagram, probabilities=False, backend_config=None, diff_method='best'): """ Return a PennyLaneCircuit equivalent to the input lambeq @@ -366,7 +483,7 @@ def draw(self): wires = (qml.draw(self._circuit) (self._concrete_params).split('\n')) for k, v in self._post_selection.items(): - wires[k] = wires[k].split('┤')[0] + '┤' + str(v) + '>' + wires[k] = wires[k].split('┤')[0] + '┤' + str(v) + '⟩' print('\n'.join(wires)) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index d18fd825..335dc980 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -37,8 +37,10 @@ import numpy as np import tensornetwork as tn from typing_extensions import Any, Self +from pytket.circuit import (Bit, Command, Op, OpType, Qubit) +from pytket.utils import probs_from_counts -from lambeq.backend import grammar, tensor +from lambeq.backend import grammar, tensor, Functor, Symbol from lambeq.backend.numerical_backend import backend, get_backend from lambeq.backend.symbol import lambdify @@ -1197,14 +1199,7 @@ def is_circuital(diagram: Diagram) -> bool: return True -from pytket.circuit import (Bit, Command, Op, OpType, Qubit) -from pytket.utils import probs_from_counts -from lambeq.backend import Functor, Symbol -from lambeq.backend.converters.tk import Circuit - - - -def make_circuital(circuit: Diagram): +def to_circuital(circuit: Diagram): """ Takes a :py:class:`lambeq.quantum.Diagram`, returns a :py:class:`Circuit`. @@ -1265,6 +1260,7 @@ def add_measure(bits: list[int], def pull_through(layer, offset:idx, gates): + # Pulls a qubit up to the top, with the correct cod and dom. # Modify gates to account for the new qubit being pulled to the top. @@ -1344,7 +1340,7 @@ def pull_through(layer, offset:idx, gates): else: gates.append(layer) - # reverse and add kets + # reverse and add bras # Assumes that once you hit a bra there won't be any more kets. post_gates = [] for i, layer in reversed(list(enumerate(layers))): @@ -1401,23 +1397,7 @@ def build_from_layers(layers: list[Layer]) -> Diagram: return diag -# String -> OpType mapping -OPTYPE_MAP = {'H': OpType.H, - 'X': OpType.X, - 'Y': OpType.Y, - 'Z': OpType.Z, - 'S': OpType.S, - 'T': OpType.T, - 'Rx': OpType.Rx, - 'Ry': OpType.Ry, - 'Rz': OpType.Rz, - 'CX': OpType.CX, - 'CZ': OpType.CZ, - 'CRx': OpType.CRx, - 'CRy': OpType.CRy, - 'CRz': OpType.CRz, - 'CCX': OpType.CCX, - 'Swap': OpType.SWAP} + def circuital_to_dict(diagram): @@ -1436,24 +1416,30 @@ def circuital_to_dict(diagram): circuit_dict['num_qubits'] = num_qubits circuit_dict['qubits'] = len(qubit_layers) circuit_dict['gates'] = [] - -# for i, layer in enumerate(qubit_layers): -# circuit_dict['qubit_layers'].append(gate_to_dict(layer.box, layer.left.count(qubit))) + circuit_dict['measures'] = [] for i, layer in enumerate(gates): circuit_dict['gates'].append(gate_to_dict(layer.box, layer.left.count(qubit))) + for i, measure in enumerate(measure_layers): - return circuit_dict + if isinstance(measure.box, Measure): + circuit_dict['measures'].append({'type': 'Measure', 'qubit': i}) + elif isinstance(measure.box, Bra): + circuit_dict['measures'].append({'type': 'Bra', 'qubit': i}) + elif isinstance(measure.box, Discard): + circuit_dict['measures'].append({'type': 'Discard', 'qubit': i}) + else: + raise NotImplementedError(measure.box) -#def qubit_to_dict(qubit: Box) -> Dict: -# qdict = {} + return circuit_dict def gate_to_dict(box: Box, offset:int) -> Dict: gdict = {} - gdict['type'] = box.name + gdict['name'] = box.name + gdict['type'] = box.name.split('(')[0] gdict['qubits'] = [offset + j for j in range(len(box.dom))] is_dagger = False @@ -1474,8 +1460,6 @@ def gate_to_dict(box: Box, offset:int) -> Dict: gdict['phase'] = 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 @@ -1499,299 +1483,13 @@ def gate_to_dict(box: Box, offset:int) -> Dict: gdict['control'] = [i_qubits[i] for i in rel_idx[:-1]] gdict['gate_q'] = offset + rel_idx[-1] - #gdict['i_qubits'] = [i_qubits[i] for i in rel_idx] - - - name = box.name.split('(')[0] - gdict['type'] = name + gdict['qubits'] = i_qubits - if name in ('CRx', 'CRz'): + if gdict['type'] in ('CRx', 'CRz'): gdict['phase'] = box.phase if isinstance(box.phase, Symbol): # Tket uses sympy, lambeq uses custom symbol gdict['phase'] = box.phase.to_sympy() -# 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) -# -# tk_circ.add_gate(op, i_qubits) return gdict - - -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_ket1(_, box: Box) -> Diagram | Box: - ob_map: dict[Box, Diagram] - ob_map = {Ket(1): Ket(0) >> X} # 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_ket1)(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 - elif isinstance(theta, sympy.Symbol): - return Symbol(theta.name) - elif isinstance(theta, sympy.Mul): - scale, symbol = theta.as_coeff_Mul() - if not isinstance(symbol, sympy.Symbol): - raise ValueError('Parameter must be a (possibly scaled) sympy' - 'Symbol') - return Symbol(symbol.name, scale=scale) - else: - raise ValueError('Parameter must be a (possibly scaled) sympy Symbol') - - -def from_tk(tk_circuit: tk.Circuit) -> Diagram: - """Translates from tket to a lambeq Diagram.""" - tk_circ: Circuit = Circuit.upgrade(tk_circuit) - n_qubits = tk_circ.n_qubits - - def box_and_offset_from_tk(tk_gate) -> tuple[Diagram, int]: - name: str = tk_gate.op.type.name - offset = tk_gate.args[0].index[0] - box: Box | Diagram | None = None - - if name.endswith('dg'): - new_tk_gate = Command(tk_gate.op.dagger, tk_gate.args) - undaggered_box, offset = box_and_offset_from_tk(new_tk_gate) - box = undaggered_box.dagger() - return box.to_diagram(), offset - - if len(tk_gate.args) == 1: # single qubit gate - if name == 'Rx': - box = Rx(_tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5) - elif name == 'Ry': - box = Ry(_tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5) - elif name == 'Rz': - box = Rz(_tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5) - elif name in GATES: - box = cast(Box, GATES[name]) - - if len(tk_gate.args) == 2: # two qubit gate - distance = tk_gate.args[1].index[0] - tk_gate.args[0].index[0] - offset = tk_gate.args[0].index[0] - - if distance < 0: - offset += distance - - if name == 'CRx': - box = CRx( - _tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5, distance) - elif name == 'CRy': - box = CRy( - _tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5, distance) - elif name == 'CRz': - box = CRz( - _tk_to_lmbq_param(tk_gate.op.params[0]) * 0.5, distance) - elif name == 'SWAP': - distance = abs(distance) - idx = list(range(distance + 1)) - idx[0], idx[-1] = idx[-1], idx[0] - box = Diagram.permutation(qubit ** (distance + 1), idx) - elif name == 'CX': - box = Controlled(X, distance) - elif name == 'CY': - box = Controlled(Y, distance) - elif name == 'CZ': - box = Controlled(Z, distance) - - if len(tk_gate.args) == 3: # three qubit gate - controls = (tk_gate.args[0].index[0], tk_gate.args[1].index[0]) - target = tk_gate.args[2].index[0] - span = max(controls + (target,)) - min(controls + (target,)) + 1 - if name == 'CCX': - box = Id(qubit**span).apply_gate(CCX, *controls, target) - elif name == 'CCZ': - box = Id(qubit**span).apply_gate(CCZ, *controls, target) - offset = min(controls + (target,)) - - if box is None: - raise NotImplementedError - else: - return box.to_diagram(), offset # type: ignore [return-value] - - circuit = Ket(*(0, ) * n_qubits).to_diagram() - bras = {} - for tk_gate in tk_circ.get_commands(): - if tk_gate.op.type.name == 'Measure': - offset: int = tk_gate.qubits[0].index[0] - bit_index: int = tk_gate.bits[0].index[0] - if bit_index in tk_circ.post_selection: - bras[offset] = tk_circ.post_selection[bit_index] - continue # post selection happens at the end - left = circuit.cod[:offset] - right = circuit.cod[offset + 1:] - circuit = circuit >> left @ Measure() @ right - else: - box, offset = box_and_offset_from_tk(tk_gate) - left = circuit.cod[:offset] - right = circuit.cod[offset + len(box.dom):] - circuit = circuit >> left @ box @ right - circuit = circuit >> Id().tensor(*( # type: ignore[arg-type] - Bra(bras[i]) if i in bras - else Discard() if x == qubit else Id(bit) - for i, x in enumerate(circuit.cod))) - if tk_circ.scalar != 1: - circuit = circuit @ Scalar(np.sqrt(abs(tk_circ.scalar))) - return circuit >> tk_circ.post_processing # type: ignore [return-value] From ea3dd273c62f7487ddc1393215e09001f4666f9c Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Wed, 6 Nov 2024 01:21:36 +0000 Subject: [PATCH 05/40] Cleaned up. Pennylane working --- lambeq/backend/converters/tk.py | 18 +- lambeq/backend/grammar.py | 31 ++- lambeq/backend/pennylane.py | 27 +-- lambeq/backend/quantum.py | 331 +++++++++++++++++--------------- 4 files changed, 226 insertions(+), 181 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 3c046e04..62761c77 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -35,7 +35,9 @@ 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, is_circuital, circuital_to_dict, to_circuital) + Rx, Ry, Rz, Scalar, Swap, X, Y, Z, + is_circuital, circuital_to_dict, + to_circuital) OPTYPE_MAP = {'H': OpType.H, 'X': OpType.X, @@ -369,7 +371,8 @@ def to_tk(diagram): circuit_dict = circuital_to_dict(diagram) - circuit = Circuit(circuit_dict["qubits"], circuit_dict["qubits"]) + circuit = Circuit(circuit_dict["qubits"]["total"], + len(circuit_dict["qubits"]["bitmap"])) for gate in circuit_dict["gates"]: @@ -385,14 +388,13 @@ def to_tk(diagram): op = op.dagger qubits = gate["qubits"] - circuit.add_gate(op, qubits) - for measure in circuit_dict["measures"]: - if measure["type"] == "Measure": - circuit.Measure(measure["qubit"], measure["qubit"]) - elif measure["type"] == "Bra": - circuit.post_select({measure["qubit"]: measure["qubit"]}) + for measure in circuit_dict["measurements"]["measure"]: + circuit.Measure(measure["qubit"], measure["bit"]) + + for postselect in circuit_dict["measurements"]["post"]: + circuit.post_select({postselect["qubit"]: postselect["bit"]}) return circuit diff --git a/lambeq/backend/grammar.py b/lambeq/backend/grammar.py index af1d9948..bf3ea57c 100644 --- a/lambeq/backend/grammar.py +++ b/lambeq/backend/grammar.py @@ -203,6 +203,31 @@ def __getitem__(self, index: int | slice) -> Self: else: return self._fromiter(objects[index]) + 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.""" @@ -970,8 +995,10 @@ def then(self, *diagrams: Diagrammable) -> Self: cod = self.cod for n, diagram in enumerate(diags): if diagram.dom != cod: - raise ValueError(f'Diagram {n} (cod={cod}) does not compose ' - f'with diagram {n+1} (dom={diagram.dom})') + raise ValueError(f'Diagram {n} ' + f'(cod={cod.__repr__()}) ' + f'does not compose with diagram {n+1} ' + f'(dom={diagram.dom.__repr__()})') cod = diagram.cod layers.extend(diagram.layers) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index a4264aff..826a7f9a 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -50,7 +50,10 @@ import sympy import torch -from lambeq.backend.quantum import Scalar, is_circuital, to_circuital, circuital_to_dict +from lambeq.backend.quantum import (Scalar, + is_circuital, + to_circuital, + circuital_to_dict) if TYPE_CHECKING: from lambeq.backend.quantum import Diagram @@ -73,7 +76,9 @@ 'CRx': qml.CRX, 'CRy': qml.CRY, 'CRz': qml.CRZ, - 'CU1': lambda a, wires: qml.ctrl(qml.U1(a, wires=wires[1]), control=wires[0]), + 'CU1': lambda a, wires: qml.ctrl(qml.U1(a, + wires=wires[1]), + control=wires[0]), 'Swap': qml.SWAP, 'noop': qml.Identity, } @@ -145,12 +150,12 @@ def tk_op_to_pennylane(tk_op): return OP_MAP[tk_op.op.type], remapped_params, symbols, wires -def extract_ops_from_circuital(circuit_dict): - print(circuit_dict) +def extract_ops_from_circuital(circuit_dict: dict): - ops = [OP_MAP_COMPOSED[x["type"]] for x in circuit_dict["gates"]] + ops = [OP_MAP_COMPOSED[x["type"]] for x in circuit_dict["gates"]] qubits = [x["qubits"] for x in circuit_dict["gates"]] - params = [x["phase"] if "phase" in x else [] for x in circuit_dict["gates"]] + params = [x["phase"] if "phase" in x else [] + for x in circuit_dict["gates"]] symbols = set() @@ -168,7 +173,6 @@ def extract_ops_from_circuital(circuit_dict): remapped_params.append([param]) - #return OP_MAP[tk_op.op.type], remapped_params, symbols, wires return ops, remapped_params, symbols, qubits @@ -218,8 +222,8 @@ def to_pennylane(diagram: Diagram, probabilities=False, # Get post selection bits post_selection = {} - for measure in circuit_dict["measures"] : - post_selection[measure["qubit"]] = measure["qubit"] + for postselect in circuit_dict["measurements"]["post"] : + post_selection[postselect["qubit"]] = postselect["bit"] scalar = 1 for gate in circuit_dict["gates"]: @@ -233,7 +237,7 @@ def to_pennylane(diagram: Diagram, probabilities=False, probabilities, post_selection, scalar, - circuit_dict["qubits"], + circuit_dict["qubits"]["total"], backend_config, diff_method) @@ -297,9 +301,8 @@ def get_post_selection_dict(tk_circ): return q_post_sels - def to_pennylane_old(lambeq_circuit: Diagram, probabilities=False, - backend_config=None, diff_method='best'): + backend_config=None, diff_method='best'): """ Return a PennyLaneCircuit equivalent to the input lambeq circuit. `probabilities` determines whether the PennyLaneCircuit diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 335dc980..52f2195c 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -33,12 +33,12 @@ from dataclasses import dataclass, field, replace from functools import partial from typing import cast, Dict +import copy import numpy as np import tensornetwork as tn from typing_extensions import Any, Self -from pytket.circuit import (Bit, Command, Op, OpType, Qubit) -from pytket.utils import probs_from_counts + from lambeq.backend import grammar, tensor, Functor, Symbol from lambeq.backend.numerical_backend import backend, get_backend @@ -1165,10 +1165,14 @@ def CRz(phi, distance=1): return Controlled(Rz(phi), distance) # noqa: E731 'CRz': CRz, } -def is_circuital(diagram: Diagram) -> bool: - """Check if a diagram is a quantum circuit diagram. - Adapted from :py:class:`...`. +def is_circuital(diagram: Diagram) -> bool: + """ + Takes a :py:class:`lambeq.quantum.Diagram`, + checks if a diagram is a quantum 'circuital' diagram. + A circuital diagram is a diagram with qubits at the top, + followed by gates, + and then measurements at the bottom. Returns ------- @@ -1177,15 +1181,16 @@ def is_circuital(diagram: Diagram) -> bool: """ - if diagram.dom: return False layers = diagram.layers # Check if the first and last layers are all qubits and measurements - num_qubits = sum([1 for l in layers if isinstance(l.box, Ket)]) - num_measures = sum([1 for l in layers if isinstance(l.box, (Bra, Measure, Discard))]) + num_qubits = sum([1 for layer in layers + if isinstance(layer.box, Ket)]) + num_measures = sum([1 for layer in layers + if isinstance(layer.box, (Bra, Measure, Discard))]) qubit_layers = layers[:num_qubits] measure_layers = layers[-num_measures:] @@ -1193,9 +1198,14 @@ def is_circuital(diagram: Diagram) -> bool: if not all([isinstance(layer.box, Ket) for layer in qubit_layers]): return False - if not all([isinstance(layer.box, (Bra, Measure, Discard)) for layer in measure_layers]): + if not all([isinstance(layer.box, (Bra, Measure, Discard)) + for layer in measure_layers]): return False + for qubit_layer in qubit_layers: + if len(qubit_layer.right): + return False + return True @@ -1203,102 +1213,127 @@ def to_circuital(circuit: Diagram): """ Takes a :py:class:`lambeq.quantum.Diagram`, returns a :py:class:`Circuit`. - The returned circuit diagram has all qubits at the top with layer depth equal to qubit index, + The returned circuit diagram has all qubits at the top + with layer depth equal to qubit index, followed by gates, and then measurements at the bottom. """ # 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. + circuit = copy.deepcopy(circuit) qubits = [] - bits = [] gates = [] + measures = [] + postselect = [] + circuit = circuit.init_and_discard() - circuit = circuit.init_and_discard() # Keep. - - # Cleans up any '1' kets and converts them to X|0> -> |1> - # Keep this in make_circuital + # Cleans up any '1' kets and converts them to X|0> -> |1> def remove_ket1(_, box: Box) -> Diagram | Box: ob_map: dict[Box, Diagram] ob_map = {Ket(1): Ket(0) >> X} # type: ignore[dict-item] return ob_map.get(box, box) def add_qubit(qubits: list[int], - layer: Layer, - offset: int, - gates) -> list[int]: + layer: Layer, + offset: int, + gates) -> list[int]: # Adds a qubit to the qubit list - # Appends shifts all the gates + # Appends shifts all the gates + # Assumes we only add one qubit at a time. + # No bits - # Will I ever have types other than single qubits? - BW for qubit_layer in qubits: - if qubit_layer.left.count(qubit) >= offset: - qubit_layer.left = qubit >> qubit_layer.left + from_left = qubit_layer.left.count(qubit) + if from_left >= offset: + qubit_layer.left = qubit_layer.left.insert(qubit, offset) layer.right = Ty() qubits.insert(offset, layer) - return qubits, pull_through(layer, offset, gates) + return qubits, pull_qubit_through(offset, gates) - def add_measure(bits: list[int], - layer: Layer, - r_offset: int, - gates) -> list[int]: + def pull_bit(offset, gates): - # Insert measurements on the right - for bit_layer in bits: - if bit_layer.right.count(qubit) >= r_offset: - bit_layer.right = qubit >> bit_layer.right + for gate_layer in gates: + if offset < len(gate_layer.left): + gate_layer.left = gate_layer.replace(qubit, offset) + elif offset < (len(gate_layer.left) + + len(gate_layer.box.dom)): + gate_layer.box.dom = gate_layer.box.dom.replace(qubit, offset - len(gate_layer.left)) + elif offset < (len(gate_layer.left) + + len(gate_layer.box.dom) + + len(gate_layer.right)): + gate_layer.right = gate_layer.right.replace(qubit, offset - len(gate_layer.left) - len(gate_layer.right)) + else: + raise IndexError("list index out of range") + return gates + + def construct_measurements(last_layer, post_selects): - offset = layer.left.count(qubit) - layer.left = Ty() - bits.insert(-r_offset if r_offset > 0 else len(bits), layer) + total_qubits = (len(last_layer.left) + + len(last_layer.box.cod) + + len(last_layer.right)) - return bits, pull_through(layer, offset, gates) + 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)]) + new_postselects = [] + for key in sorted(q_idx.keys()): + bits_left = sum([1 for i in bit_idx if i < key]) + bits_right = total_qubits - key - 1 + q_idx[key].left = qubit ** bits_left + q_idx[key].right = qubit ** bits_right + new_postselects.append(q_idx[key]) - def pull_through(layer, offset:idx, gates): - # Pulls a qubit up to the top, with the correct cod and dom. + return new_postselects + def pull_qubit_through(offset: int, gates: list[Layer]) -> list[Layer]: + """ + Inserts a qubit type into every layer at the appropriate index + offset: idx - index of where to insert the gate. + """ - # Modify gates to account for the new qubit being pulled to the top. for gate_layer in gates: - box = gate_layer.box - # Idx of the first qubit in the gate before adding the new qubit - qubit_start = gate_layer.left.count(qubit) - orig_qubits = [qubit_start + j for j in range(len(box.dom))] - num_qubits = len(orig_qubits) - qubit_last = orig_qubits[-1] + gate_qubits = [len(gate_layer.left) + j + for j in range(len(gate_layer.box.dom))] # Checks if we have to bring the qubit up through the gate. # Only if past the first qubit - gate_contains_qubit = qubit_start < offset and offset <= qubit_last + gate_contains_qubit = (gate_qubits[0] < offset + and offset <= gate_qubits[-1]) - if num_qubits == 1 or not gate_contains_qubit: - if qubit_start >= offset: - gate_layer.left = qubit >> gate_layer.left - else: - gate_layer.right = qubit >> gate_layer.right + if not gate_contains_qubit: + if gate_qubits[0] >= offset: + gate_layer.left = gate_layer.left.insert(qubit, offset) + else: + gate_layer.right = gate_layer.right.insert(qubit, + (offset + - gate_qubits[-1] + - 1) + ) else: - if isinstance(box, Controlled): + if isinstance(gate_layer.box, Controlled): # Initial control qubit box dists = [0] - curr_box: Box | Controlled = box + curr_box: Box | Controlled = gate_layer.box while isinstance(curr_box, Controlled): - # Append the relative index of the next qubit in the sequence - # The one furthest left relative to the initial control qubit - # tells us the distance from the left of the box + # Compute relative index control qubits dists.append(curr_box.distance + sum(dists)) curr_box = curr_box.controlled - # Obtain old absolute index of the old controlled qubit - prev_pos = -1 * min(dists) + qubit_start - curr_box: Box | Controlled = box + prev_pos = -1 * min(dists) + gate_qubits[0] + curr_box: Box | Controlled = gate_layer.box while isinstance(curr_box, Controlled): curr_pos = prev_pos + curr_box.distance @@ -1310,19 +1345,24 @@ def pull_through(layer, offset:idx, gates): prev_pos = curr_pos curr_box = curr_box.controlled - box.dom = qubit >> box.dom - box.cod = qubit >> box.cod - if isinstance(box, Swap): + gate_layer.box.dom = gate_layer.box.dom.insert(qubit, offset - gate_qubits[0]) + gate_layer.box.cod = gate_layer.box.cod.insert(qubit, offset - gate_qubits[0]) + + 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 + # Swaps are 2 wide, so if a qubit is pulled through we # have to use the pulled qubit as an temp ancillary. - gates.append(Layer(Swap(qubit, qubit), layer.left, qubit >> layer.right)) - gates.append(Layer(Swap(qubit, qubit), qubit >> layer.left, layer.right)) - gates.append(Layer(Swap(qubit, qubit), layer.left, qubit >> layer.right)) - - + gates.append(Layer(gate_layer.left, + Swap(qubit, qubit), + qubit >> gate_layer.right)) + gates.append(Layer(qubit >> gate_layer.left, + Swap(qubit, qubit), + gate_layer.right)) + gates.append(Layer(gate_layer.left, + Swap(qubit, qubit), + qubit >> gate_layer.right)) return gates @@ -1331,111 +1371,98 @@ def pull_through(layer, offset:idx, gates): ar=remove_ket1)(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, layer.left.count(qubit), gates) + qubits, gates = add_qubit(qubits, + layer, + layer.left.count(qubit), + gates) elif isinstance(layer.box, (Measure, Bra, Discard)): br_i = i break else: gates.append(layer) - # reverse and add bras - # Assumes that once you hit a bra there won't be any more kets. + # Reverse and add bras, measurements, and discards from below + # Assumes hitting a bra, measure, or discard implies no more kets. post_gates = [] for i, layer in reversed(list(enumerate(layers))): - - box = layer.box - if isinstance(box, (Measure, Bra, Discard)): - bits, post_gates = add_measure(bits, layers[i], layer.right.count(qubit), post_gates) + + if isinstance(layer.box, (Bra, Discard)): + post_gates = pull_qubit_through(len(layer.left), post_gates) + postselect.insert(0, layer) + elif isinstance(layer.box, (Measure)): + measures.insert(0, layer) else: post_gates.insert(0, layer) if br_i == i: break - #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 - - qubitDom = qubit ** len(qubits) - - def build_from_layers(layers: list[Layer]) -> Diagram: - # Type checking at the end - layerDiags = [Diagram(dom=layer.dom, cod = layer.cod, layers = [layer]) for layer in layers] - layerD = layerDiags[0] - for layer in layerDiags[1:]: - layerD = layerD >> layer - return layerD - - diag = build_from_layers(qubits + gates + post_gates + bits) - if diag.dom != circuit.dom or diag.cod != circuit.cod: - raise ValueError('Circuit conversion failed. The domain and codomain of the circuit do not match the domain and codomain of the diagram.') - - return diag + postselect = construct_measurements(post_gates[-1], postselect) + new_layers = qubits + gates + post_gates + postselect + measures + diags = [Diagram(dom=layer.dom, cod=layer.cod, layers=[layer]) + for layer in new_layers] + layerD = diags[0] + for layer in diags[1:]: + layerD = layerD >> layer + + return layerD def circuital_to_dict(diagram): assert is_circuital(diagram) - circuit_dict = {} layers = diagram.layers - num_qubits = sum([1 for l in layers if isinstance(l.box, Ket)]) - num_measures = sum([1 for l in layers if isinstance(l.box, (Bra, Measure, Discard))]) - - qubit_layers = layers[:num_qubits] - measure_layers = layers[-num_measures:] - gates = layers[num_qubits:-num_measures] + num_qubits = sum([1 for layer in layers if isinstance(layer.box, Ket)]) + available_qubits = list(range(num_qubits)) - circuit_dict['num_qubits'] = num_qubits - circuit_dict['qubits'] = len(qubit_layers) + circuit_dict = {} circuit_dict['gates'] = [] - circuit_dict['measures'] = [] + circuit_dict['measurements'] = {'post': [], 'discard': [], + 'measure': []} + circuit_dict['qubits'] = {'total': num_qubits, 'bitmap': {}, + 'post': [], 'discard': [], 'measure': []} - for i, layer in enumerate(gates): - circuit_dict['gates'].append(gate_to_dict(layer.box, layer.left.count(qubit))) + for i, layer in enumerate(layers): - for i, measure in enumerate(measure_layers): + qi = available_qubits[len(layer.left)] - if isinstance(measure.box, Measure): - circuit_dict['measures'].append({'type': 'Measure', 'qubit': i}) - elif isinstance(measure.box, Bra): - circuit_dict['measures'].append({'type': 'Bra', 'qubit': i}) - elif isinstance(measure.box, Discard): - circuit_dict['measures'].append({'type': 'Discard', 'qubit': i}) + if isinstance(layer.box, Ket): + pass + elif isinstance(layer.box, Measure): + available_qubits.remove(qi) + circuit_dict['qubits']['bitmap'][qi] = len(circuit_dict['qubits']['bitmap']) + circuit_dict['qubits']['measure'].append(qi) + circuit_dict['measurements']['measure'].append( + {'type': 'Measure', 'qubit': qi, + 'bit': circuit_dict['qubits']['bitmap'][qi]} + ) + elif isinstance(layer.box, Bra): + available_qubits.remove(qi) + circuit_dict['qubits']['bitmap'][qi] = len(circuit_dict['qubits']['bitmap']) + circuit_dict['qubits']['post'].append(qi) + circuit_dict['measurements']['post'].append( + {'type': 'Bra', 'qubit': qi, + 'bit': circuit_dict['qubits']['bitmap'][qi]} + ) + elif isinstance(layer.box, Discard): + available_qubits.remove(qi) + circuit_dict['measurements']['discard'].append( + {'type': 'Discard', 'qubit': qi} + ) + circuit_dict['qubits']['discard'].append(qi) else: - raise NotImplementedError(measure.box) + circuit_dict['gates'].append(gate_to_dict(layer.box, qi)) return circuit_dict -def gate_to_dict(box: Box, offset:int) -> Dict: +def gate_to_dict(box: Box, offset: int) -> Dict: gdict = {} gdict['name'] = box.name @@ -1449,8 +1476,6 @@ def gate_to_dict(box: Box, offset:int) -> Dict: gdict['dagger'] = is_dagger - i_qubits = [offset + j for j in range(len(box.dom))] - if isinstance(box, (Rx, Ry, Rz)): phase = box.phase if isinstance(box.phase, Symbol): @@ -1461,29 +1486,18 @@ def gate_to_dict(box: Box, offset:int) -> Dict: elif isinstance(box, Controlled): # reverse the distance order - dists = [] + dists = [0] curr_box: Box | Controlled = box while isinstance(curr_box, Controlled): - dists.append(curr_box.distance) + # Append the relative index of the next qubit in the sequence + # The one furthest left relative to the initial control qubit + # tells us the distance from the left of the box + dists.append(curr_box.distance + sum(dists)) 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] - - gdict['control'] = [i_qubits[i] for i in rel_idx[:-1]] - gdict['gate_q'] = offset + rel_idx[-1] - gdict['qubits'] = i_qubits + gdict['qubits'] = [x - min(dists) + offset for x in dists] + gdict['control'] = sorted(gdict['qubits'][:-1]) + gdict['gate_q'] = gdict['qubits'][-1] if gdict['type'] in ('CRx', 'CRz'): gdict['phase'] = box.phase @@ -1491,5 +1505,4 @@ def gate_to_dict(box: Box, offset:int) -> Dict: # Tket uses sympy, lambeq uses custom symbol gdict['phase'] = box.phase.to_sympy() - return gdict From e08ed62c64cb0b0418fb0f8d6fcb4787b675d8ba Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Thu, 7 Nov 2024 12:53:24 +0000 Subject: [PATCH 06/40] Simplified pulling qubits through --- lambeq/backend/converters/tk.py | 5 +- lambeq/backend/pennylane.py | 3 +- lambeq/backend/quantum.py | 95 ++++++++++++++++++--------------- 3 files changed, 58 insertions(+), 45 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 62761c77..d17f14be 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -376,7 +376,10 @@ def to_tk(diagram): for gate in circuit_dict["gates"]: - if not gate["type"] in OPTYPE_MAP: + if gate["type"] == "Scalar": + circuit.scale(abs(gate["phase"])**2) + continue + elif not gate["type"] in OPTYPE_MAP: raise NotImplementedError(f"Gate {gate} not supported") if "phase" in gate: diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 826a7f9a..bf42327e 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -218,7 +218,8 @@ def to_pennylane(diagram: Diagram, probabilities=False, circuit_dict = circuital_to_dict(diagram) - op_list, params_list, symbols_set, wires_list = extract_ops_from_circuital(circuit_dict) + ex_ops = extract_ops_from_circuital(circuit_dict) + op_list, params_list, symbols_set, wires_list = ex_ops # Get post selection bits post_selection = {} diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 52f2195c..35450b10 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1253,7 +1253,7 @@ def add_qubit(qubits: list[int], layer.right = Ty() qubits.insert(offset, layer) - return qubits, pull_qubit_through(offset, gates) + return qubits, pull_qubit_through(offset, gates)[0] def pull_bit(offset, gates): @@ -1295,34 +1295,31 @@ def construct_measurements(last_layer, post_selects): return new_postselects - def pull_qubit_through(offset: int, gates: list[Layer]) -> list[Layer]: + def pull_qubit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: """ Inserts a qubit type into every layer at the appropriate index - offset: idx - index of where to insert the gate. + q_idx: idx - index of where to insert the gate. """ for gate_layer in gates: - gate_qubits = [len(gate_layer.left) + j - for j in range(len(gate_layer.box.dom))] + # Index relative to the right, first qubit in the right = 0 idx + right_rel = q_idx - (len(gate_layer.left) + len(gate_layer.box.dom)) - # Checks if we have to bring the qubit up through the gate. - # Only if past the first qubit - gate_contains_qubit = (gate_qubits[0] < offset - and offset <= gate_qubits[-1]) + # Inserting to the left is always trivial + if q_idx <= len(gate_layer.left) : + gate_layer.left = gate_layer.left.insert(qubit, q_idx) + # Qubit is on the right of the gate. Also handles 1 qubit gates because l(dom) = 1 + elif q_idx > len(gate_layer.left) + len(gate_layer.box.dom) - 1: + # Insert on the right and update the relative index from the left + gate_layer.right = gate_layer.right.insert(qubit, right_rel) - if not gate_contains_qubit: + q_idx = right_rel + len(gate_layer.left) + len(gate_layer.box.cod) # Or q + cod - dom - if gate_qubits[0] >= offset: - gate_layer.left = gate_layer.left.insert(qubit, offset) - else: - gate_layer.right = gate_layer.right.insert(qubit, - (offset - - gate_qubits[-1] - - 1) - ) 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] @@ -1337,17 +1334,17 @@ def pull_qubit_through(offset: int, gates: list[Layer]) -> list[Layer]: while isinstance(curr_box, Controlled): curr_pos = prev_pos + curr_box.distance - if prev_pos < offset and offset <= curr_pos: + if prev_pos < q_idx and q_idx <= curr_pos: curr_box.distance = curr_box.distance + 1 - elif offset <= prev_pos and offset > curr_pos: + 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 - gate_layer.box.dom = gate_layer.box.dom.insert(qubit, offset - gate_qubits[0]) - gate_layer.box.cod = gate_layer.box.cod.insert(qubit, offset - gate_qubits[0]) + gate_layer.box.dom = gate_layer.box.dom.insert(qubit, q_idx - gate_qubits[0]) + gate_layer.box.cod = gate_layer.box.cod.insert(qubit, q_idx - gate_qubits[0]) if isinstance(gate_layer.box, Swap): @@ -1364,7 +1361,7 @@ def pull_qubit_through(offset: int, gates: list[Layer]) -> list[Layer]: Swap(qubit, qubit), qubit >> gate_layer.right)) - return gates + return gates, q_idx circuit = Functor(target_category=quantum, # type: ignore [assignment] ob=lambda _, x: x, @@ -1372,39 +1369,46 @@ def pull_qubit_through(offset: int, gates: list[Layer]) -> list[Layer]: layers = circuit.layers + # Remove measurements to the end + for layer in layers: + if isinstance(layer.box, Measure): + measures.append(layer) + layers.remove(layer) + for i, layer in enumerate(layers): if isinstance(layer.box, Ket): qubits, gates = add_qubit(qubits, layer, layer.left.count(qubit), gates) - elif isinstance(layer.box, (Measure, Bra, Discard)): - br_i = i - break - else: - gates.append(layer) - # Reverse and add bras, measurements, and discards from below - # Assumes hitting a bra, measure, or discard implies no more kets. - post_gates = [] - for i, layer in reversed(list(enumerate(layers))): + elif isinstance(layer.box, (Bra, Discard)): + + layers[i+1:], q_idx = pull_qubit_through(len(layer.left), layers[i+1:]) + + # We assume that the left and right are constructable from the last gate + # and the left position of the bra. (We avoid type checking until the end.) + layer.left = Ty() + if q_idx > 0: + layer.left = qubit ** q_idx + + r_len = len(gates[-1].right) + len(gates[-1].box.cod) + len(gates[-1].left) - q_idx - 1 + + layer.right = Ty() + if r_len > 0: + layer.right = qubit ** r_len - if isinstance(layer.box, (Bra, Discard)): - post_gates = pull_qubit_through(len(layer.left), post_gates) postselect.insert(0, layer) - elif isinstance(layer.box, (Measure)): - measures.insert(0, layer) - else: - post_gates.insert(0, layer) - if br_i == i: - break + else: + gates.append(layer) - postselect = construct_measurements(post_gates[-1], postselect) - new_layers = qubits + gates + post_gates + postselect + measures + postselect = construct_measurements(gates[-1], postselect) diags = [Diagram(dom=layer.dom, cod=layer.cod, layers=[layer]) - for layer in new_layers] + for layer in qubits + gates + postselect + measures] + + # Ensure type checking layerD = diags[0] for layer in diags[1:]: layerD = layerD >> layer @@ -1505,4 +1509,9 @@ def gate_to_dict(box: Box, offset: int) -> Dict: # Tket uses sympy, lambeq uses custom symbol gdict['phase'] = box.phase.to_sympy() + elif isinstance(box, Scalar): + gdict['type'] = 'Scalar' + # Just a placeholder + gdict['phase'] = box.array + return gdict From a14bd84b792d7586d432e3d87d931daa172647be Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 8 Nov 2024 10:05:18 +0000 Subject: [PATCH 07/40] Fixed edge cases in testing --- lambeq/backend/converters/tk.py | 58 +++++++----- lambeq/backend/grammar.py | 31 +++++++ lambeq/backend/pennylane.py | 29 +++--- lambeq/backend/quantum.py | 154 ++++++++++++++++++++++---------- 4 files changed, 189 insertions(+), 83 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index d17f14be..713c05c1 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -32,12 +32,14 @@ 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, - is_circuital, circuital_to_dict, - to_circuital) +from lambeq.backend.quantum import (bit, Box, Bra, CCX, CCZ, + circuital_to_dict, + Controlled, CRx, CRy, CRz, Daggered, + Diagram, Discard, GATES, Id, + is_circuital, Ket, Measure, quantum, + qubit, Rx, Ry, Rz, Scalar, Swap, + to_circuital, X, Y, Z + ) OPTYPE_MAP = {'H': OpType.H, 'X': OpType.X, @@ -365,39 +367,51 @@ def _tk_to_lmbq_param(theta): def to_tk(diagram): + """Convert diagram to t|ket>. + + Returns + ------- + tk_circuit : lambeq.backend.converters.tk.Circuit + 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) circuit_dict = circuital_to_dict(diagram) - circuit = Circuit(circuit_dict["qubits"]["total"], - len(circuit_dict["qubits"]["bitmap"])) + circuit = Circuit(circuit_dict['qubits']['total'], + len(circuit_dict['qubits']['bitmap'])) - for gate in circuit_dict["gates"]: + for gate in circuit_dict['gates']: - if gate["type"] == "Scalar": - circuit.scale(abs(gate["phase"])**2) + if gate['type'] == 'Scalar': + circuit.scale(abs(gate['phase'])**2) continue - elif not gate["type"] in OPTYPE_MAP: - raise NotImplementedError(f"Gate {gate} not supported") + elif not gate['type'] in OPTYPE_MAP: + raise NotImplementedError(f'Gate {gate} not supported') - if "phase" in gate: - op = Op.create(OPTYPE_MAP[gate["type"]], 2 * gate["phase"]) + if 'phase' in gate: + op = Op.create(OPTYPE_MAP[gate['type']], 2 * gate['phase']) else: - op = Op.create(OPTYPE_MAP[gate["type"]]) + op = Op.create(OPTYPE_MAP[gate['type']]) - if gate["dagger"]: + if gate['dagger']: op = op.dagger - qubits = gate["qubits"] + qubits = gate['qubits'] circuit.add_gate(op, qubits) - for measure in circuit_dict["measurements"]["measure"]: - circuit.Measure(measure["qubit"], measure["bit"]) + for measure in circuit_dict['measurements']['measure']: + circuit.Measure(measure['qubit'], measure['bit']) - for postselect in circuit_dict["measurements"]["post"]: - circuit.post_select({postselect["qubit"]: postselect["bit"]}) + for postselect in circuit_dict['measurements']['post']: + circuit.post_select({postselect['qubit']: postselect['bit']}) return circuit diff --git a/lambeq/backend/grammar.py b/lambeq/backend/grammar.py index bf3ea57c..fb3f21a1 100644 --- a/lambeq/backend/grammar.py +++ b/lambeq/backend/grammar.py @@ -203,6 +203,37 @@ 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. diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index bf42327e..6f8a1940 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -51,9 +51,9 @@ import torch from lambeq.backend.quantum import (Scalar, + circuital_to_dict, is_circuital, - to_circuital, - circuital_to_dict) + to_circuital) if TYPE_CHECKING: from lambeq.backend.quantum import Diagram @@ -152,10 +152,10 @@ def tk_op_to_pennylane(tk_op): def extract_ops_from_circuital(circuit_dict: dict): - ops = [OP_MAP_COMPOSED[x["type"]] for x in circuit_dict["gates"]] - qubits = [x["qubits"] for x in circuit_dict["gates"]] - params = [x["phase"] if "phase" in x else [] - for x in circuit_dict["gates"]] + ops = [OP_MAP_COMPOSED[x['type']] for x in circuit_dict['gates']] + qubits = [x['qubits'] for x in circuit_dict['gates']] + params = [x['phase'] if 'phase' in x else [] + for x in circuit_dict['gates']] symbols = set() @@ -218,18 +218,19 @@ def to_pennylane(diagram: Diagram, probabilities=False, circuit_dict = circuital_to_dict(diagram) + scalar = 1 + for gate in circuit_dict['gates']: + if gate['type'] == 'Scalar': + scalar *= gate['phase'] + circuit_dict['gates'].remove(gate) + ex_ops = extract_ops_from_circuital(circuit_dict) op_list, params_list, symbols_set, wires_list = ex_ops # Get post selection bits post_selection = {} - for postselect in circuit_dict["measurements"]["post"] : - post_selection[postselect["qubit"]] = postselect["bit"] - - scalar = 1 - for gate in circuit_dict["gates"]: - if gate["type"] == "Scalar": - scalar *= gate["array"] + for postselect in circuit_dict['measurements']['post'] : + post_selection[postselect['qubit']] = postselect['bit'] return PennyLaneCircuit(op_list, list(symbols_set), @@ -238,7 +239,7 @@ def to_pennylane(diagram: Diagram, probabilities=False, probabilities, post_selection, scalar, - circuit_dict["qubits"]["total"], + circuit_dict['qubits']['total'], backend_config, diff_method) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 35450b10..c7e0cf78 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -32,15 +32,15 @@ from collections.abc import Callable from dataclasses import dataclass, field, replace from functools import partial -from typing import cast, Dict import copy +from typing import cast, Dict import numpy as np import tensornetwork as tn from typing_extensions import Any, Self -from lambeq.backend import grammar, tensor, Functor, Symbol +from lambeq.backend import Functor, grammar, Symbol, tensor from lambeq.backend.numerical_backend import backend, get_backend from lambeq.backend.symbol import lambdify @@ -1255,23 +1255,6 @@ def add_qubit(qubits: list[int], return qubits, pull_qubit_through(offset, gates)[0] - def pull_bit(offset, gates): - - for gate_layer in gates: - if offset < len(gate_layer.left): - gate_layer.left = gate_layer.replace(qubit, offset) - elif offset < (len(gate_layer.left) - + len(gate_layer.box.dom)): - gate_layer.box.dom = gate_layer.box.dom.replace(qubit, offset - len(gate_layer.left)) - elif offset < (len(gate_layer.left) - + len(gate_layer.box.dom) - + len(gate_layer.right)): - gate_layer.right = gate_layer.right.replace(qubit, offset - len(gate_layer.left) - len(gate_layer.right)) - else: - raise IndexError("list index out of range") - - return gates - def construct_measurements(last_layer, post_selects): total_qubits = (len(last_layer.left) @@ -1295,6 +1278,44 @@ def construct_measurements(last_layer, post_selects): return new_postselects + def pull_bit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: + """ + Inserts a qubit type into every layer at the appropriate index + q_idx: idx - index of where to insert the gate. + """ + + for gate_layer in gates: + + 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: + 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}') + + return gates, q_idx + def pull_qubit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: """ Inserts a qubit type into every layer at the appropriate index @@ -1303,18 +1324,21 @@ def pull_qubit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: for gate_layer in gates: - # Index relative to the right, first qubit in the right = 0 idx - right_rel = q_idx - (len(gate_layer.left) + len(gate_layer.box.dom)) + l_size = len(gate_layer.left) # Inserting to the left is always trivial - if q_idx <= len(gate_layer.left) : + if q_idx <= l_size: gate_layer.left = gate_layer.left.insert(qubit, q_idx) - # Qubit is on the right of the gate. Also handles 1 qubit gates because l(dom) = 1 - elif q_idx > len(gate_layer.left) + len(gate_layer.box.dom) - 1: - # Insert on the right and update the relative index from the left - gate_layer.right = gate_layer.right.insert(qubit, right_rel) + # Qubit on right of gate. Handles 1 qubit gates by l(dom) = 1 + elif q_idx > l_size + len(gate_layer.box.dom) - 1: - q_idx = right_rel + len(gate_layer.left) + len(gate_layer.box.cod) # Or q + cod - dom + # 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(qubit, r_rel) + + q_idx = r_rel + l_size + len(gate_layer.box.cod) else: if isinstance(gate_layer.box, Controlled): @@ -1343,8 +1367,10 @@ def pull_qubit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: prev_pos = curr_pos curr_box = curr_box.controlled - gate_layer.box.dom = gate_layer.box.dom.insert(qubit, q_idx - gate_qubits[0]) - gate_layer.box.cod = gate_layer.box.cod.insert(qubit, q_idx - gate_qubits[0]) + box = gate_layer.box + + box.dom = box.dom.insert(qubit, q_idx - l_size) + box.cod = box.cod.insert(qubit, q_idx - l_size) if isinstance(gate_layer.box, Swap): @@ -1363,19 +1389,45 @@ def pull_qubit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: return gates, q_idx + def build_left_right(q_idx, layer, layers): + """ + We assume that the left and right are constructable + from the last gate + and the left position of the bra. + (We avoid type checking until the end.) + Rebuild left and right based on the last layer + """ + if len(layers) == 0: + return layer + + gate_layer = layers[-1] + + # Inserting to the left is always trivial + 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-1]: + layer.left = Ty() + else: + layer.left = layer.left._fromiter(total_layer[:q_idx-1]) + + 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, # type: ignore [assignment] ob=lambda _, x: x, ar=remove_ket1)(circuit) # type: ignore [arg-type] layers = circuit.layers - # Remove measurements to the end for layer in layers: - if isinstance(layer.box, Measure): - measures.append(layer) - layers.remove(layer) - for i, layer in enumerate(layers): if isinstance(layer.box, Ket): qubits, gates = add_qubit(qubits, layer, @@ -1384,21 +1436,26 @@ def pull_qubit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: elif isinstance(layer.box, (Bra, Discard)): - layers[i+1:], q_idx = pull_qubit_through(len(layer.left), layers[i+1:]) + q_idx = len(layer.left) - # We assume that the left and right are constructable from the last gate - # and the left position of the bra. (We avoid type checking until the end.) - layer.left = Ty() - if q_idx > 0: - layer.left = qubit ** q_idx + layers[i+1:], q_idx = pull_qubit_through(q_idx, layers[i+1:]) + layer = build_left_right(q_idx, layer, layers[i+1 :]) - r_len = len(gates[-1].right) + len(gates[-1].box.cod) + len(gates[-1].left) - q_idx - 1 + postselect.insert(0, layer) - layer.right = Ty() - if r_len > 0: - layer.right = qubit ** r_len + elif isinstance(layer.box, Measure): - postselect.insert(0, layer) + q_idx = len(layer.left) + layers[i+1:], q_idx = pull_bit_through(q_idx, layers[i+1:]) + layer = build_left_right(q_idx, layer, layers[i+1:]) + + q_idx = len(layer.left) + + if postselect: + postselect, q_idx = pull_bit_through(q_idx, postselect) + layer = build_left_right(q_idx, layer, postselect) + + measures.insert(0, layer) else: gates.append(layer) @@ -1431,6 +1488,7 @@ def circuital_to_dict(diagram): 'measure': []} circuit_dict['qubits'] = {'total': num_qubits, 'bitmap': {}, 'post': [], 'discard': [], 'measure': []} + bitmap = {} for i, layer in enumerate(layers): @@ -1440,7 +1498,7 @@ def circuital_to_dict(diagram): pass elif isinstance(layer.box, Measure): available_qubits.remove(qi) - circuit_dict['qubits']['bitmap'][qi] = len(circuit_dict['qubits']['bitmap']) + bitmap[qi] = len(bitmap) circuit_dict['qubits']['measure'].append(qi) circuit_dict['measurements']['measure'].append( {'type': 'Measure', 'qubit': qi, @@ -1448,7 +1506,7 @@ def circuital_to_dict(diagram): ) elif isinstance(layer.box, Bra): available_qubits.remove(qi) - circuit_dict['qubits']['bitmap'][qi] = len(circuit_dict['qubits']['bitmap']) + bitmap[qi] = len(bitmap) circuit_dict['qubits']['post'].append(qi) circuit_dict['measurements']['post'].append( {'type': 'Bra', 'qubit': qi, @@ -1463,6 +1521,8 @@ def circuital_to_dict(diagram): else: circuit_dict['gates'].append(gate_to_dict(layer.box, qi)) + circuit_dict['qubits']['bitmap'] = bitmap + return circuit_dict From a47a99499ed12898223f9f4cddcf633541b4da34 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 8 Nov 2024 10:14:42 +0000 Subject: [PATCH 08/40] Fixed idx after linting --- lambeq/backend/quantum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index c7e0cf78..7a72e664 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1426,7 +1426,7 @@ def build_left_right(q_idx, layer, layers): layers = circuit.layers - for layer in layers: + for i, layer in enumerate(layers): if isinstance(layer.box, Ket): qubits, gates = add_qubit(qubits, From 96e8a1632ce8750dc59a8daa07201aa2f38de303 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 8 Nov 2024 10:17:46 +0000 Subject: [PATCH 09/40] Linting --- lambeq/backend/pennylane.py | 4 ++-- lambeq/backend/quantum.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 6f8a1940..aa881457 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -50,9 +50,9 @@ import sympy import torch -from lambeq.backend.quantum import (Scalar, - circuital_to_dict, +from lambeq.backend.quantum import (circuital_to_dict, is_circuital, + Scalar, to_circuital) if TYPE_CHECKING: diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 7a72e664..001ea82d 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -30,9 +30,9 @@ from __future__ import annotations from collections.abc import Callable +import copy from dataclasses import dataclass, field, replace from functools import partial -import copy from typing import cast, Dict import numpy as np @@ -1490,7 +1490,7 @@ def circuital_to_dict(diagram): 'post': [], 'discard': [], 'measure': []} bitmap = {} - for i, layer in enumerate(layers): + for layer in layers: qi = available_qubits[len(layer.left)] From 383c37c1eee48c66e60c42ca8a1809db81a405c6 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 8 Nov 2024 10:24:10 +0000 Subject: [PATCH 10/40] Fixed a silly bug --- lambeq/backend/quantum.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 001ea82d..3d078ed3 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1502,7 +1502,7 @@ def circuital_to_dict(diagram): circuit_dict['qubits']['measure'].append(qi) circuit_dict['measurements']['measure'].append( {'type': 'Measure', 'qubit': qi, - 'bit': circuit_dict['qubits']['bitmap'][qi]} + 'bit': bitmap[qi]} ) elif isinstance(layer.box, Bra): available_qubits.remove(qi) @@ -1510,7 +1510,7 @@ def circuital_to_dict(diagram): circuit_dict['qubits']['post'].append(qi) circuit_dict['measurements']['post'].append( {'type': 'Bra', 'qubit': qi, - 'bit': circuit_dict['qubits']['bitmap'][qi]} + 'bit': bitmap[qi]} ) elif isinstance(layer.box, Discard): available_qubits.remove(qi) From 9a41779edc0b3994c42d036e49c79a31e908f286 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 8 Nov 2024 14:36:45 +0000 Subject: [PATCH 11/40] Fixed postselection bits --- lambeq/backend/converters/tk.py | 6 +++--- lambeq/backend/pennylane.py | 4 ++-- lambeq/backend/quantum.py | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 713c05c1..58e416fb 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -390,6 +390,9 @@ def to_tk(diagram): for gate in circuit_dict['gates']: + if gate['dagger']: + op = op.dagger + if gate['type'] == 'Scalar': circuit.scale(abs(gate['phase'])**2) continue @@ -401,9 +404,6 @@ def to_tk(diagram): else: op = Op.create(OPTYPE_MAP[gate['type']]) - if gate['dagger']: - op = op.dagger - qubits = gate['qubits'] circuit.add_gate(op, qubits) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index aa881457..34c74573 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -230,7 +230,7 @@ def to_pennylane(diagram: Diagram, probabilities=False, # Get post selection bits post_selection = {} for postselect in circuit_dict['measurements']['post'] : - post_selection[postselect['qubit']] = postselect['bit'] + post_selection[postselect['qubit']] = postselect['phase'] return PennyLaneCircuit(op_list, list(symbols_set), @@ -488,7 +488,7 @@ def draw(self): wires = (qml.draw(self._circuit) (self._concrete_params).split('\n')) for k, v in self._post_selection.items(): - wires[k] = wires[k].split('┤')[0] + '┤' + str(v) + '⟩' + wires[k] = wires[k].split('┤')[0] + '┤' + str(v) + '>' print('\n'.join(wires)) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 3d078ed3..bfd54c3b 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1510,7 +1510,7 @@ def circuital_to_dict(diagram): circuit_dict['qubits']['post'].append(qi) circuit_dict['measurements']['post'].append( {'type': 'Bra', 'qubit': qi, - 'bit': bitmap[qi]} + 'bit': bitmap[qi], 'phase': layer.box.bit } ) elif isinstance(layer.box, Discard): available_qubits.remove(qi) @@ -1537,6 +1537,7 @@ def gate_to_dict(box: Box, offset: int) -> Dict: if isinstance(box, Daggered): box = box.dagger() is_dagger = True + gdict['type'] = box.name.split('(')[0] gdict['dagger'] = is_dagger From ecaa0f8ab83e18a2750856c4f3e8ca7e43df072b Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 8 Nov 2024 14:53:06 +0000 Subject: [PATCH 12/40] Fixed dagger error --- lambeq/backend/converters/tk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 58e416fb..713c05c1 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -390,9 +390,6 @@ def to_tk(diagram): for gate in circuit_dict['gates']: - if gate['dagger']: - op = op.dagger - if gate['type'] == 'Scalar': circuit.scale(abs(gate['phase'])**2) continue @@ -404,6 +401,9 @@ def to_tk(diagram): else: op = Op.create(OPTYPE_MAP[gate['type']]) + if gate['dagger']: + op = op.dagger + qubits = gate['qubits'] circuit.add_gate(op, qubits) From de6e7e75962e778700d86ec9d3cb3ce89015a9d0 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Sun, 10 Nov 2024 17:50:37 +0000 Subject: [PATCH 13/40] Cleaned up mypy --- lambeq/backend/converters/tk.py | 2 +- lambeq/backend/pennylane.py | 4 ++-- lambeq/backend/quantum.py | 38 ++++++++++++++++----------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 713c05c1..e2252671 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -411,7 +411,7 @@ def to_tk(diagram): circuit.Measure(measure['qubit'], measure['bit']) for postselect in circuit_dict['measurements']['post']: - circuit.post_select({postselect['qubit']: postselect['bit']}) + circuit.post_select({postselect['qubit']: postselect['phase']}) return circuit diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 34c74573..527d2d07 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -43,7 +43,7 @@ from itertools import product import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Union import pennylane as qml from pytket import OpType @@ -159,7 +159,7 @@ def extract_ops_from_circuital(circuit_dict: dict): symbols = set() - remapped_params = [] + remapped_params: list[Union[sympy.Expr, torch.Tensor]] = [] for param in params: # Check if the param contains a symbol diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index bfd54c3b..d266c293 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -33,7 +33,7 @@ import copy from dataclasses import dataclass, field, replace from functools import partial -from typing import cast, Dict +from typing import cast, Dict, List, Tuple import numpy as np import tensornetwork as tn @@ -1223,10 +1223,10 @@ def to_circuital(circuit: Diagram): # Necessary to ensure editing boxes is localized. circuit = copy.deepcopy(circuit) - qubits = [] - gates = [] - measures = [] - postselect = [] + 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> @@ -1235,10 +1235,10 @@ def remove_ket1(_, box: Box) -> Diagram | Box: ob_map = {Ket(1): Ket(0) >> X} # type: ignore[dict-item] return ob_map.get(box, box) - def add_qubit(qubits: list[int], + def add_qubit(qubits: list[Layer], layer: Layer, offset: int, - gates) -> list[int]: + gates: list[Layer]) -> Tuple[list[Layer], list[Layer]]: # Adds a qubit to the qubit list # Appends shifts all the gates @@ -1278,7 +1278,7 @@ def construct_measurements(last_layer, post_selects): return new_postselects - def pull_bit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: + def pull_bit_through(q_idx: int, gates: list[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. @@ -1316,7 +1316,7 @@ def pull_bit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: return gates, q_idx - def pull_qubit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: + def pull_qubit_through(q_idx: int, gates: list[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. @@ -1354,7 +1354,7 @@ def pull_qubit_through(q_idx: int, gates: list[Layer]) -> list[Layer]: curr_box = curr_box.controlled prev_pos = -1 * min(dists) + gate_qubits[0] - curr_box: Box | Controlled = gate_layer.box + curr_box = gate_layer.box while isinstance(curr_box, Controlled): curr_pos = prev_pos + curr_box.distance @@ -1462,13 +1462,13 @@ def build_left_right(q_idx, layer, layers): postselect = construct_measurements(gates[-1], postselect) - diags = [Diagram(dom=layer.dom, cod=layer.cod, layers=[layer]) + # Rebuild the diagram + diags = [Diagram(dom=layer.dom, cod=layer.cod, layers=[layer]) # type: ignore [arg-type] for layer in qubits + gates + postselect + measures] - # Ensure type checking layerD = diags[0] - for layer in diags[1:]: - layerD = layerD >> layer + for diagram in diags[1:]: + layerD = layerD >> diagram return layerD @@ -1510,7 +1510,7 @@ def circuital_to_dict(diagram): circuit_dict['qubits']['post'].append(qi) circuit_dict['measurements']['post'].append( {'type': 'Bra', 'qubit': qi, - 'bit': bitmap[qi], 'phase': layer.box.bit } + 'bit': bitmap[qi], 'phase': layer.box.bit} ) elif isinstance(layer.box, Discard): available_qubits.remove(qi) @@ -1528,18 +1528,18 @@ def circuital_to_dict(diagram): def gate_to_dict(box: Box, offset: int) -> Dict: - gdict = {} + gdict:Dict = {} gdict['name'] = box.name gdict['type'] = box.name.split('(')[0] gdict['qubits'] = [offset + j for j in range(len(box.dom))] + gdict['phase'] = 0 + gdict['dagger'] = False - is_dagger = False if isinstance(box, Daggered): box = box.dagger() - is_dagger = True + gdict['dagger'] = True gdict['type'] = box.name.split('(')[0] - gdict['dagger'] = is_dagger if isinstance(box, (Rx, Ry, Rz)): phase = box.phase From b60d01393b7525b8ba3df059f05e76c2015d8ead Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Sun, 10 Nov 2024 18:19:28 +0000 Subject: [PATCH 14/40] Fixed flake again --- lambeq/backend/quantum.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index d266c293..d75b87d5 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -33,7 +33,7 @@ import copy from dataclasses import dataclass, field, replace from functools import partial -from typing import cast, Dict, List, Tuple +from typing import cast, Dict, Tuple import numpy as np import tensornetwork as tn @@ -1278,7 +1278,8 @@ def construct_measurements(last_layer, post_selects): return new_postselects - def pull_bit_through(q_idx: int, gates: list[Layer]) -> tuple[list[Layer], int]: + def pull_bit_through(q_idx: int, + gates: list[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. @@ -1316,7 +1317,8 @@ def pull_bit_through(q_idx: int, gates: list[Layer]) -> tuple[list[Layer], int]: return gates, q_idx - def pull_qubit_through(q_idx: int, gates: list[Layer]) -> tuple[list[Layer], int]: + def pull_qubit_through(q_idx: int, + gates: list[Layer]) -> 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. @@ -1463,7 +1465,7 @@ def build_left_right(q_idx, layer, layers): postselect = construct_measurements(gates[-1], postselect) # Rebuild the diagram - diags = [Diagram(dom=layer.dom, cod=layer.cod, layers=[layer]) # type: ignore [arg-type] + 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] @@ -1528,7 +1530,7 @@ def circuital_to_dict(diagram): def gate_to_dict(box: Box, offset: int) -> Dict: - gdict:Dict = {} + gdict: Dict = {} gdict['name'] = box.name gdict['type'] = box.name.split('(')[0] gdict['qubits'] = [offset + j for j in range(len(box.dom))] @@ -1540,7 +1542,6 @@ def gate_to_dict(box: Box, offset: int) -> Dict: gdict['dagger'] = True gdict['type'] = box.name.split('(')[0] - if isinstance(box, (Rx, Ry, Rz)): phase = box.phase if isinstance(box.phase, Symbol): From aebf47329ec5f429eaa06f5c8b9363da0b9eed1f Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Mon, 11 Nov 2024 13:53:29 +0000 Subject: [PATCH 15/40] Fixed tket tests --- lambeq/backend/converters/tk.py | 6 +- lambeq/backend/quantum.py | 102 +++++++++++++++++--------------- 2 files changed, 58 insertions(+), 50 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index e2252671..83662d6e 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -56,7 +56,7 @@ 'CRy': OpType.CRy, 'CRz': OpType.CRz, 'CCX': OpType.CCX, - 'Swap': OpType.SWAP} + 'SWAP': OpType.SWAP} class Circuit(tk.Circuit): @@ -389,6 +389,7 @@ def to_tk(diagram): len(circuit_dict['qubits']['bitmap'])) for gate in circuit_dict['gates']: + print("Gate: ", gate) if gate['type'] == 'Scalar': circuit.scale(abs(gate['phase'])**2) @@ -396,7 +397,7 @@ def to_tk(diagram): elif not gate['type'] in OPTYPE_MAP: raise NotImplementedError(f'Gate {gate} not supported') - if 'phase' in gate: + if 'phase' in gate and gate['phase']: op = Op.create(OPTYPE_MAP[gate['type']], 2 * gate['phase']) else: op = Op.create(OPTYPE_MAP[gate['type']]) @@ -412,6 +413,7 @@ def to_tk(diagram): for postselect in circuit_dict['measurements']['post']: circuit.post_select({postselect['qubit']: postselect['phase']}) + circuit.Measure(postselect['qubit'], postselect['bit']) return circuit diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index d75b87d5..f257197d 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1189,19 +1189,12 @@ def is_circuital(diagram: Diagram) -> bool: # Check if the first and last layers are all qubits and measurements num_qubits = sum([1 for layer in layers if isinstance(layer.box, Ket)]) - num_measures = sum([1 for layer in layers - if isinstance(layer.box, (Bra, Measure, Discard))]) qubit_layers = layers[:num_qubits] - measure_layers = layers[-num_measures:] if not all([isinstance(layer.box, Ket) for layer in qubit_layers]): return False - if not all([isinstance(layer.box, (Bra, Measure, Discard)) - for layer in measure_layers]): - return False - for qubit_layer in qubit_layers: if len(qubit_layer.right): return False @@ -1248,14 +1241,16 @@ def add_qubit(qubits: list[Layer], for qubit_layer in qubits: from_left = qubit_layer.left.count(qubit) if from_left >= offset: - qubit_layer.left = qubit_layer.left.insert(qubit, offset) + qubit_layer.left = qubit_layer.left.insert(layer.box.cod, + offset) layer.right = Ty() qubits.insert(offset, layer) - return qubits, pull_qubit_through(offset, gates)[0] + return qubits, pull_qubit_through(offset, gates, dom=layer.box.cod)[0] def construct_measurements(last_layer, post_selects): + # Change to accommodate measurements before total_qubits = (len(last_layer.left) + len(last_layer.box.cod) @@ -1268,31 +1263,37 @@ def construct_measurements(last_layer, post_selects): 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]) - bits_right = total_qubits - key - 1 - q_idx[key].left = qubit ** bits_left - q_idx[key].right = qubit ** bits_right + 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]) -> tuple[list[Layer], 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 gate_layer in gates: + for i, gate_layer in enumerate(gates): 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: + 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: @@ -1315,10 +1316,15 @@ def pull_bit_through(q_idx: int, 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]) -> tuple[list[Layer], int]: # noqa: E501 + 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. @@ -1330,7 +1336,7 @@ def pull_qubit_through(q_idx: int, # Inserting to the left is always trivial if q_idx <= l_size: - gate_layer.left = gate_layer.left.insert(qubit, q_idx) + gate_layer.left = gate_layer.left.insert(dom, 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: @@ -1338,7 +1344,7 @@ def pull_qubit_through(q_idx: int, 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(qubit, r_rel) + gate_layer.right = gate_layer.right.insert(dom, r_rel) q_idx = r_rel + l_size + len(gate_layer.box.cod) @@ -1371,8 +1377,8 @@ def pull_qubit_through(q_idx: int, box = gate_layer.box - box.dom = box.dom.insert(qubit, q_idx - l_size) - box.cod = box.cod.insert(qubit, q_idx - l_size) + box.dom = box.dom.insert(dom, q_idx - l_size) + box.cod = box.cod.insert(dom, q_idx - l_size) if isinstance(gate_layer.box, Swap): @@ -1381,13 +1387,13 @@ def pull_qubit_through(q_idx: int, # have to use the pulled qubit as an temp ancillary. gates.append(Layer(gate_layer.left, Swap(qubit, qubit), - qubit >> gate_layer.right)) - gates.append(Layer(qubit >> gate_layer.left, + dom >> gate_layer.right)) + gates.append(Layer(dom >> gate_layer.left, Swap(qubit, qubit), gate_layer.right)) gates.append(Layer(gate_layer.left, Swap(qubit, qubit), - qubit >> gate_layer.right)) + dom >> gate_layer.right)) return gates, q_idx @@ -1396,7 +1402,7 @@ def build_left_right(q_idx, layer, layers): We assume that the left and right are constructable from the last gate and the left position of the bra. - (We avoid type checking until the end.) + (We type check at the end.) Rebuild left and right based on the last layer """ if len(layers) == 0: @@ -1445,24 +1451,11 @@ def build_left_right(q_idx, layer, layers): postselect.insert(0, layer) - elif isinstance(layer.box, Measure): - - q_idx = len(layer.left) - layers[i+1:], q_idx = pull_bit_through(q_idx, layers[i+1:]) - layer = build_left_right(q_idx, layer, layers[i+1:]) - - q_idx = len(layer.left) - - if postselect: - postselect, q_idx = pull_bit_through(q_idx, postselect) - layer = build_left_right(q_idx, layer, postselect) - - measures.insert(0, layer) - else: gates.append(layer) - postselect = construct_measurements(gates[-1], postselect) + 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 @@ -1493,12 +1486,10 @@ def circuital_to_dict(diagram): bitmap = {} for layer in layers: - - qi = available_qubits[len(layer.left)] - 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) circuit_dict['qubits']['measure'].append(qi) @@ -1507,6 +1498,7 @@ def circuital_to_dict(diagram): 'bit': bitmap[qi]} ) elif isinstance(layer.box, Bra): + qi = available_qubits[layer.left.count(qubit)] available_qubits.remove(qi) bitmap[qi] = len(bitmap) circuit_dict['qubits']['post'].append(qi) @@ -1515,12 +1507,14 @@ def circuital_to_dict(diagram): 'bit': bitmap[qi], 'phase': layer.box.bit} ) elif isinstance(layer.box, Discard): + qi = available_qubits[layer.left.count(qubit)] available_qubits.remove(qi) circuit_dict['measurements']['discard'].append( {'type': 'Discard', 'qubit': qi} ) circuit_dict['qubits']['discard'].append(qi) else: + qi = len(layer.left) circuit_dict['gates'].append(gate_to_dict(layer.box, qi)) circuit_dict['qubits']['bitmap'] = bitmap @@ -1551,17 +1545,29 @@ def gate_to_dict(box: Box, offset: int) -> Dict: gdict['phase'] = phase elif isinstance(box, Controlled): + # reverse the distance order - dists = [0] + dists = [] curr_box: Box | Controlled = box while isinstance(curr_box, Controlled): - # Append the relative index of the next qubit in the sequence - # The one furthest left relative to the initial control qubit - # tells us the distance from the left of the box - dists.append(curr_box.distance + sum(dists)) + 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 = [gdict["qubits"][i] for i in rel_idx] - gdict['qubits'] = [x - min(dists) + offset for x in dists] + gdict['qubits'] = i_qubits gdict['control'] = sorted(gdict['qubits'][:-1]) gdict['gate_q'] = gdict['qubits'][-1] From 2b1d692793589d1142825a779ccec35997ea2f6f Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Mon, 11 Nov 2024 14:06:05 +0000 Subject: [PATCH 16/40] Fixed lint and converter test --- lambeq/backend/quantum.py | 4 ++-- tests/backend/converters/test_tket_conversion.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index f257197d..76355bea 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1284,7 +1284,7 @@ def pull_bit_through(q_idx: int, q_idx: idx - index of where to insert the gate. """ - for i, gate_layer in enumerate(gates): + for i, gate_layer in enumerate(gates): # noqa: B007 l_size = len(gate_layer.left) c_size = len(gate_layer.box.cod) @@ -1565,7 +1565,7 @@ def gate_to_dict(box: Box, offset: int) -> Dict: right_most_idx = max(rel_idx) rel_idx.insert(-1, right_most_idx - dist) - i_qubits = [gdict["qubits"][i] for i in rel_idx] + i_qubits = [gdict['qubits'][i] for i in rel_idx] gdict['qubits'] = i_qubits gdict['control'] = sorted(gdict['qubits'][:-1]) 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(): From 9f0b14421004493f966a302e8f1c72fb0f70d0b2 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Mon, 11 Nov 2024 14:09:09 +0000 Subject: [PATCH 17/40] Linting --- lambeq/backend/converters/tk.py | 1 - lambeq/backend/quantum.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 83662d6e..7a613f03 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -389,7 +389,6 @@ def to_tk(diagram): len(circuit_dict['qubits']['bitmap'])) for gate in circuit_dict['gates']: - print("Gate: ", gate) if gate['type'] == 'Scalar': circuit.scale(abs(gate['phase'])**2) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 76355bea..6b3c4c18 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1284,7 +1284,7 @@ def pull_bit_through(q_idx: int, q_idx: idx - index of where to insert the gate. """ - for i, gate_layer in enumerate(gates): # noqa: B007 + for i, gate_layer in enumerate(gates): # noqa: B007 l_size = len(gate_layer.left) c_size = len(gate_layer.box.cod) From 3476fb83b8f3cf770db58cb0c0393003c069e55a Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Mon, 11 Nov 2024 15:14:25 +0000 Subject: [PATCH 18/40] Fixed penny --- lambeq/backend/pennylane.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 527d2d07..a32bbc8a 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -154,7 +154,7 @@ def extract_ops_from_circuital(circuit_dict: dict): ops = [OP_MAP_COMPOSED[x['type']] for x in circuit_dict['gates']] qubits = [x['qubits'] for x in circuit_dict['gates']] - params = [x['phase'] if 'phase' in x else [] + params = [x['phase'] if 'phase' in x and x['phase'] else [] for x in circuit_dict['gates']] symbols = set() From df26e0fe544981fb52153f6fffc98a8214e96236 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Mon, 11 Nov 2024 16:00:13 +0000 Subject: [PATCH 19/40] Fixed pennylane --- lambeq/backend/quantum.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 6b3c4c18..13fcfb40 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1239,12 +1239,16 @@ def add_qubit(qubits: list[Layer], # No bits for qubit_layer in qubits: - from_left = qubit_layer.left.count(qubit) + 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] @@ -1439,7 +1443,7 @@ def build_left_right(q_idx, layer, layers): if isinstance(layer.box, Ket): qubits, gates = add_qubit(qubits, layer, - layer.left.count(qubit), + len(layer.left), gates) elif isinstance(layer.box, (Bra, Discard)): From 45816281a53cfee18c648d7459c60166fa221946 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 12 Nov 2024 14:29:20 +0000 Subject: [PATCH 20/40] fixed post selection bug --- lambeq/backend/converters/tk.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 7a613f03..cdc4cf86 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -385,8 +385,12 @@ def to_tk(diagram): circuit_dict = circuital_to_dict(diagram) + post_select = {postselect['qubit']: postselect['phase'] for postselect in circuit_dict['measurements']['post']} + circuit = Circuit(circuit_dict['qubits']['total'], - len(circuit_dict['qubits']['bitmap'])) + len(circuit_dict['qubits']['bitmap']), + post_selection=post_select + ) for gate in circuit_dict['gates']: @@ -411,7 +415,6 @@ def to_tk(diagram): circuit.Measure(measure['qubit'], measure['bit']) for postselect in circuit_dict['measurements']['post']: - circuit.post_select({postselect['qubit']: postselect['phase']}) circuit.Measure(postselect['qubit'], postselect['bit']) return circuit From 8846e073274d7eae8d082bfd4eae7b6bb483e71b Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 12 Nov 2024 14:43:12 +0000 Subject: [PATCH 21/40] fixed flake --- lambeq/backend/converters/tk.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index cdc4cf86..41499e28 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -385,7 +385,9 @@ def to_tk(diagram): circuit_dict = circuital_to_dict(diagram) - post_select = {postselect['qubit']: postselect['phase'] for postselect in circuit_dict['measurements']['post']} + post_select = {postselect['qubit']: postselect['phase'] + for postselect in + circuit_dict['measurements']['post']} circuit = Circuit(circuit_dict['qubits']['total'], len(circuit_dict['qubits']['bitmap']), From d75e524b82bdfcbf98ea398ebb930a13650f46a4 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Wed, 13 Nov 2024 10:59:16 +0000 Subject: [PATCH 22/40] Added doc strings --- lambeq/backend/converters/tk.py | 172 ++--------------------------- lambeq/backend/pennylane.py | 189 +------------------------------- lambeq/backend/quantum.py | 39 ++++++- 3 files changed, 50 insertions(+), 350 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 41499e28..c09c2eb8 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -26,17 +26,17 @@ 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 import Symbol from lambeq.backend.quantum import (bit, Box, Bra, CCX, CCZ, circuital_to_dict, - Controlled, CRx, CRy, CRz, Daggered, + Controlled, CRx, CRy, CRz, Diagram, Discard, GATES, Id, - is_circuital, Ket, Measure, quantum, + is_circuital, Ket, Measure, qubit, Rx, Ry, Rz, Scalar, Swap, to_circuital, X, Y, Z ) @@ -196,161 +196,6 @@ def get_counts(self, return counts -def to_tk_old(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 @@ -366,12 +211,15 @@ def _tk_to_lmbq_param(theta): raise ValueError('Parameter must be a (possibly scaled) sympy Symbol') -def to_tk(diagram): - """Convert diagram to t|ket>. +def to_tk(diagram: Diagram): + """ + Takes a :py:class:`lambeq.quantum.Diagram`, returns + a :class:`lambeq.backend.converters.tk.Circuit` + for t|ket>. Returns ------- - tk_circuit : lambeq.backend.converters.tk.Circuit + tk_circuit : lambeq.backend.quantum A :class:`lambeq.backend.converters.tk.Circuit`. Note diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index a32bbc8a..023e4e0c 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -46,19 +46,17 @@ from typing import TYPE_CHECKING, Union import pennylane as qml -from pytket import OpType import sympy import torch from lambeq.backend.quantum import (circuital_to_dict, is_circuital, - Scalar, to_circuital) if TYPE_CHECKING: from lambeq.backend.quantum import Diagram -OP_MAP_COMPOSED = { +OP_MAP = { 'H': qml.Hadamard, 'X': qml.PauliX, 'Y': qml.PauliY, @@ -83,41 +81,17 @@ 'noop': qml.Identity, } -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, -} - -def tk_op_to_pennylane(tk_op): +def extract_ops_from_circuital(circuit_dict: dict): """ 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 ------- @@ -132,27 +106,7 @@ def tk_op_to_pennylane(tk_op): The wires/qubits to apply the operation to. """ - wires = [x.index[0] for x in tk_op.qubits] - params = tk_op.op.params - symbols = set() - - remapped_params = [] - 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): - 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_circuital(circuit_dict: dict): - - ops = [OP_MAP_COMPOSED[x['type']] for x in circuit_dict['gates']] + ops = [OP_MAP[x['type']] for x in circuit_dict['gates']] qubits = [x['qubits'] for x in circuit_dict['gates']] params = [x['phase'] if 'phase' in x and x['phase'] else [] for x in circuit_dict['gates']] @@ -244,137 +198,6 @@ def to_pennylane(diagram: Diagram, probabilities=False, diff_method) -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. - - """ - 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 - - -def to_pennylane_old(lambeq_circuit: Diagram, probabilities=False, - backend_config=None, diff_method='best'): - """ - Return a PennyLaneCircuit equivalent to the input lambeq - circuit. `probabilities` determines whether the PennyLaneCircuit - returns states (as in lambeq), or probabilities (to be more - compatible with automatic differentiation in PennyLane). - - Parameters - ---------- - lambeq_circuit : :class:`lambeq.backend.quantum.Diagram` - The lambeq circuit to convert to PennyLane. - probabilities : bool, default: False - Determines whether the PennyLane - circuit outputs states or un-normalized probabilities. - Probabilities can be used with more PennyLane backpropagation - methods. - backend_config : dict, default: None - A dictionary of PennyLane backend configration options, - including the provider (e.g. IBM or Honeywell), the device, - the number of shots, etc. See the `PennyLane plugin - documentation `_ - for more details. - diff_method : str, default: "best" - The differentiation method to use to obtain gradients for the - PennyLane circuit. Some gradient methods are only compatible - with simulated circuits. See the `PennyLane documentation - `_ - for more details. - - Returns - ------- - :class:`PennyLaneCircuit` - The PennyLane circuit equivalent to the input lambeq circuit. - - """ - - if any(isinstance(box, Measure) for box in lambeq_circuit.boxes): - raise ValueError('Only pure circuits, or circuits with discards' - ' are currently supported.') - - if lambeq_circuit.is_mixed and lambeq_circuit.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) - ) - - post_selection = get_post_selection_dict(tk_circ) - - scalar = 1 - for box in lambeq_circuit.boxes: - if isinstance(box, Scalar): - scalar *= box.array - - return PennyLaneCircuit(op_list, - list(symbols_set), - params_list, - wires_list, - probabilities, - post_selection, - lambeq_circuit.is_mixed, - scalar, - tk_circ.n_qubits, - backend_config, - diff_method) - - STATE_BACKENDS = ['default.qubit', 'lightning.qubit', 'qiskit.aer'] STATE_DEVICES = ['aer_simulator_statevector', 'statevector_simulator'] diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 13fcfb40..b3dabda1 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1205,10 +1205,17 @@ def is_circuital(diagram: Diagram) -> bool: def to_circuital(circuit: Diagram): """ Takes a :py:class:`lambeq.quantum.Diagram`, returns - a :py:class:`Circuit`. + a modified :py:class:`lambeq.quantum.Diagram`. + The returned circuit diagram has all qubits at the top with layer depth equal to qubit index, - followed by gates, and then measurements at the bottom. + followed by gates, and then post-selection + measurements at the bottom. + + Returns + ------- + :py:class:`lambeq.quantum.Diagram` + Circuital diagram. """ # bits and qubits are lists of register indices, at layer i we want @@ -1472,7 +1479,29 @@ def build_left_right(q_idx, layer, layers): return layerD -def circuital_to_dict(diagram): +def circuital_to_dict(diagram: Diagram): + """ + Takes a :py:class:`lambeq.quantum.Diagram`, returns + a dictionary for converting to a circuit. + + Returns + ------- + Dict: + d['gates'] : list of gates + d['gates'][:] : {'name': 'name', + 'type': 'repr(lambeq.quantum.Parameterized)', + 'qubits': All qubits for box [q_i, q_{i+1}, ...], + 'dagger': bool, + 'phase': (Optional) float for parameter values, + post-selection bits, etc. + 'control': (Optional) control qubits + 'gate_q' : (Optional) gate qubit if control gate + } + + d['measurements'] : Dictionary storing discard, post-selection, + and measurement information. + d['qubits'] : Dictionary containing qubit information. + """ assert is_circuital(diagram) @@ -1481,13 +1510,13 @@ def circuital_to_dict(diagram): num_qubits = sum([1 for layer in layers if isinstance(layer.box, Ket)]) available_qubits = list(range(num_qubits)) - circuit_dict = {} + circuit_dict: dict = {} circuit_dict['gates'] = [] circuit_dict['measurements'] = {'post': [], 'discard': [], 'measure': []} circuit_dict['qubits'] = {'total': num_qubits, 'bitmap': {}, 'post': [], 'discard': [], 'measure': []} - bitmap = {} + bitmap: dict = {} for layer in layers: if isinstance(layer.box, Ket): From a401b9da58c2133ae4cf52219e8757e17593b5fa Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Wed, 13 Nov 2024 11:13:06 +0000 Subject: [PATCH 23/40] Fixed mixed pennylane --- lambeq/backend/pennylane.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 023e4e0c..57f79469 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -166,6 +166,9 @@ def to_pennylane(diagram: Diagram, probabilities=False, The PennyLane circuit equivalent to the input lambeq circuit. """ + if diagram.is_mixed: + raise ValueError('Only pure quantum circuits are currently ' + 'supported.') if not is_circuital(diagram): diagram = to_circuital(diagram) From 4fc95d0132d91a89f9b0ab8ef09636817d5b3e0d Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Wed, 13 Nov 2024 15:42:49 +0000 Subject: [PATCH 24/40] Debugged tket. --- lambeq/backend/quantum.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index b3dabda1..8d4ca697 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1262,7 +1262,6 @@ def add_qubit(qubits: list[Layer], def construct_measurements(last_layer, post_selects): # Change to accommodate measurements before - total_qubits = (len(last_layer.left) + len(last_layer.box.cod) + len(last_layer.right)) @@ -1421,16 +1420,16 @@ def build_left_right(q_idx, layer, layers): gate_layer = layers[-1] - # Inserting to the left is always trivial 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-1]: + if q_idx == 0 or not total_layer[:q_idx]: layer.left = Ty() else: - layer.left = layer.left._fromiter(total_layer[:q_idx-1]) + 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() @@ -1456,7 +1455,6 @@ def build_left_right(q_idx, layer, layers): 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 :]) From 347f93df973ad75a107bc5b668db8ba8bf9651b4 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 15 Nov 2024 11:51:34 +0000 Subject: [PATCH 25/40] Added doc strings and tests --- lambeq/backend/converters/tk.py | 10 ++++-- lambeq/backend/quantum.py | 49 ++++++++++++++++++++++------- tests/backend/test_quantum.py | 56 +++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index c09c2eb8..ac423741 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -212,11 +212,17 @@ def _tk_to_lmbq_param(theta): def to_tk(diagram: Diagram): - """ - Takes a :py:class:`lambeq.quantum.Diagram`, returns + """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 diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 8d4ca697..bae29cb5 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1170,9 +1170,10 @@ def is_circuital(diagram: Diagram) -> bool: """ Takes a :py:class:`lambeq.quantum.Diagram`, checks if a diagram is a quantum 'circuital' diagram. - A circuital diagram is a diagram with qubits at the top, - followed by gates, - and then measurements at the bottom. + A circuital diagram is a diagram with qubits at the top. + + Used to check for measurements and post-selection + at the bottom but there are too many edge cases. Returns ------- @@ -1186,7 +1187,7 @@ def is_circuital(diagram: Diagram) -> bool: layers = diagram.layers - # Check if the first and last layers are all qubits and measurements + # Check if the first layers are all qubits and measurements num_qubits = sum([1 for layer in layers if isinstance(layer.box, Ket)]) @@ -1199,13 +1200,31 @@ def is_circuital(diagram: Diagram) -> bool: 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(circuit: Diagram): - """ - Takes a :py:class:`lambeq.quantum.Diagram`, returns - a modified :py:class:`lambeq.quantum.Diagram`. +def to_circuital(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, @@ -1215,13 +1234,13 @@ def to_circuital(circuit: Diagram): Returns ------- :py:class:`lambeq.quantum.Diagram` - Circuital 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. - circuit = copy.deepcopy(circuit) + circuit = copy.deepcopy(diagram) qubits: list[Layer] = [] gates: list[Layer] = [] @@ -1478,9 +1497,15 @@ def build_left_right(q_idx, layer, layers): def circuital_to_dict(diagram: Diagram): - """ - Takes a :py:class:`lambeq.quantum.Diagram`, returns + """Takes a circuital :py:class:`lambeq.quantum.Diagram`, returns a dictionary for converting to a circuit. + 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 ------- diff --git a/tests/backend/test_quantum.py b/tests/backend/test_quantum.py index a20b5a84..d0afb9cf 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,20 @@ 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 = circuital_to_dict(circ) + assert cdict['gates'] == [{'name': 'H', 'qubits': [0], 'type': 'H', 'phase': 0.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) + + + From 243fa51a88e1c1c88791d9fc0ca86539a07f8865 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Mon, 2 Dec 2024 15:11:24 +0000 Subject: [PATCH 26/40] Removed sys import --- lambeq/backend/pennylane.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 57f79469..50d03484 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -42,7 +42,6 @@ from __future__ import annotations from itertools import product -import sys from typing import TYPE_CHECKING, Union import pennylane as qml From f77912249b5e586c1e7e37a405b2d5333612e2a4 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 3 Dec 2024 12:18:08 +0000 Subject: [PATCH 27/40] Pennylane interface --- lambeq/backend/pennylane.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 50d03484..1a3fbae0 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -165,9 +165,6 @@ def to_pennylane(diagram: Diagram, probabilities=False, The PennyLane circuit equivalent to the input lambeq circuit. """ - if diagram.is_mixed: - raise ValueError('Only pure quantum circuits are currently ' - 'supported.') if not is_circuital(diagram): diagram = to_circuital(diagram) @@ -194,6 +191,7 @@ def to_pennylane(diagram: Diagram, probabilities=False, wires_list, probabilities, post_selection, + diagram.is_mixed, scalar, circuit_dict['qubits']['total'], backend_config, From 7e55448d33d9a2c44bdf60b6bdde623787631b70 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 3 Dec 2024 12:42:25 +0000 Subject: [PATCH 28/40] Merged with recent pennylane pr --- lambeq/backend/pennylane.py | 10 ++++++++++ lambeq/backend/quantum.py | 7 ++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 1a3fbae0..bce0fda2 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -47,6 +47,7 @@ import pennylane as qml import sympy import torch +import sys from lambeq.backend.quantum import (circuital_to_dict, is_circuital, @@ -165,6 +166,15 @@ def to_pennylane(diagram: Diagram, probabilities=False, The PennyLane circuit equivalent to the input lambeq circuit. """ + if any(isinstance(box, Measure) for box in diagram.boxes): + raise ValueError('Only pure circuits, or circuits with discards' + ' are currently supported.') + + 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) if not is_circuital(diagram): diagram = to_circuital(diagram) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index bae29cb5..6b32c709 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1249,9 +1249,10 @@ def to_circuital(diagram: Diagram): circuit = circuit.init_and_discard() # Cleans up any '1' kets and converts them to X|0> -> |1> - def remove_ket1(_, box: Box) -> Diagram | Box: + def remove_ketbra1(_, box: Box) -> Diagram | Box: ob_map: dict[Box, Diagram] - ob_map = {Ket(1): Ket(0) >> X} # type: ignore[dict-item] + ob_map = {Ket(1): Ket(0) >> X, + Bra(1): X >> Ket(0)} # type: ignore[dict-item] return ob_map.get(box, box) def add_qubit(qubits: list[Layer], @@ -1459,7 +1460,7 @@ def build_left_right(q_idx, layer, layers): circuit = Functor(target_category=quantum, # type: ignore [assignment] ob=lambda _, x: x, - ar=remove_ket1)(circuit) # type: ignore [arg-type] + ar=remove_ketbra1)(circuit) # type: ignore [arg-type] layers = circuit.layers From e5bdbac2229cdc858be2afe1d2dd263ab984197b Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 3 Dec 2024 12:45:51 +0000 Subject: [PATCH 29/40] Measure import --- lambeq/backend/pennylane.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index bce0fda2..8a08fef7 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -50,7 +50,7 @@ import sys from lambeq.backend.quantum import (circuital_to_dict, - is_circuital, + is_circuital, Measure, to_circuital) if TYPE_CHECKING: From 2bffca52c57efa76510030c4c8be0a35f3fee0fc Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 3 Dec 2024 12:58:50 +0000 Subject: [PATCH 30/40] Small changes --- lambeq/backend/pennylane.py | 2 +- lambeq/backend/quantum.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 8a08fef7..181aa654 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -42,12 +42,12 @@ from __future__ import annotations from itertools import product +import sys from typing import TYPE_CHECKING, Union import pennylane as qml import sympy import torch -import sys from lambeq.backend.quantum import (circuital_to_dict, is_circuital, Measure, diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 6b32c709..b4aec732 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1251,8 +1251,8 @@ def to_circuital(diagram: Diagram): # 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, - Bra(1): X >> Ket(0)} # type: ignore[dict-item] + 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], From e60bf89202a44eacc46b73016b116792d66e54a6 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Fri, 6 Dec 2024 14:04:03 +0000 Subject: [PATCH 31/40] Replaced is_mixed with False in to_pennylane --- lambeq/backend/pennylane.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 181aa654..a80a3bdf 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -201,7 +201,7 @@ def to_pennylane(diagram: Diagram, probabilities=False, wires_list, probabilities, post_selection, - diagram.is_mixed, + False, scalar, circuit_dict['qubits']['total'], backend_config, From 330537e1ab4c05e84ce096a7e2eb6fb1b6e5ba8d Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Mon, 9 Dec 2024 12:17:24 +0000 Subject: [PATCH 32/40] Moved mixed_circuit check --- lambeq/backend/pennylane.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index a80a3bdf..98cfb59c 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -176,6 +176,8 @@ def to_pennylane(diagram: Diagram, probabilities=False, ' wires. All open wires will be discarded during conversion', file=sys.stderr) + is_mixed = diagram.is_mixed + if not is_circuital(diagram): diagram = to_circuital(diagram) @@ -201,7 +203,7 @@ def to_pennylane(diagram: Diagram, probabilities=False, wires_list, probabilities, post_selection, - False, + is_mixed, scalar, circuit_dict['qubits']['total'], backend_config, From 3a56070562daf9e4554b8d7538b9ffb284667ea1 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 10 Dec 2024 15:54:11 +0000 Subject: [PATCH 33/40] Fixed SWAPs --- lambeq/backend/pennylane.py | 2 +- lambeq/backend/quantum.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 98cfb59c..c27a9889 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -77,7 +77,7 @@ 'CU1': lambda a, wires: qml.ctrl(qml.U1(a, wires=wires[1]), control=wires[0]), - 'Swap': qml.SWAP, + 'SWAP': qml.SWAP, 'noop': qml.Identity, } diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index b4aec732..95b79e58 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1359,7 +1359,7 @@ def pull_qubit_through(q_idx: int, 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) @@ -1367,6 +1367,7 @@ def pull_qubit_through(q_idx: int, # 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: @@ -1377,6 +1378,7 @@ def pull_qubit_through(q_idx: int, 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): @@ -1409,23 +1411,24 @@ def pull_qubit_through(q_idx: int, 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. - gates.append(Layer(gate_layer.left, - Swap(qubit, qubit), - dom >> gate_layer.right)) - gates.append(Layer(dom >> gate_layer.left, - Swap(qubit, qubit), - gate_layer.right)) - gates.append(Layer(gate_layer.left, - Swap(qubit, qubit), - dom >> gate_layer.right)) - - return gates, q_idx + 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, layer, layers): """ From a31ad394c082302f7955925e8a7b05124be58268 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 7 Jan 2025 15:40:34 +0000 Subject: [PATCH 34/40] Pickle trick to replace deep copy and dedicated dataclass for CircuitInfo --- lambeq/backend/converters/tk.py | 43 +++--- lambeq/backend/pennylane.py | 36 +++-- lambeq/backend/quantum.py | 249 +++++++++++++++++++------------- 3 files changed, 184 insertions(+), 144 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index ac423741..0e1dfdfc 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -33,11 +33,11 @@ from lambeq.backend import Symbol from lambeq.backend.quantum import (bit, Box, Bra, CCX, CCZ, - circuital_to_dict, Controlled, CRx, CRy, CRz, Diagram, Discard, GATES, Id, is_circuital, Ket, Measure, - qubit, Rx, Ry, Rz, Scalar, Swap, + qubit, readoff_circuital, + Rx, Ry, Rz, Scalar, Swap, to_circuital, X, Y, Z ) @@ -237,41 +237,34 @@ def to_tk(diagram: Diagram): if not is_circuital(diagram): diagram = to_circuital(diagram) - circuit_dict = circuital_to_dict(diagram) + circuitInfo = readoff_circuital(diagram) - post_select = {postselect['qubit']: postselect['phase'] - for postselect in - circuit_dict['measurements']['post']} - - circuit = Circuit(circuit_dict['qubits']['total'], - len(circuit_dict['qubits']['bitmap']), - post_selection=post_select + circuit = Circuit(circuitInfo.totalQubits, + len(circuitInfo.bitmap), + post_selection=circuitInfo.postmap ) - for gate in circuit_dict['gates']: + for gate in circuitInfo.gates: - if gate['type'] == 'Scalar': - circuit.scale(abs(gate['phase'])**2) + if gate.gtype == 'Scalar': + circuit.scale(abs(gate.phase)**2) continue - elif not gate['type'] in OPTYPE_MAP: - raise NotImplementedError(f'Gate {gate} not supported') + elif gate.gtype not in OPTYPE_MAP: + raise NotImplementedError(f'Gate {gate.gtype} not supported') - if 'phase' in gate and gate['phase']: - op = Op.create(OPTYPE_MAP[gate['type']], 2 * gate['phase']) + if gate.phase: + op = Op.create(OPTYPE_MAP[gate.gtype], 2 * gate.phase) else: - op = Op.create(OPTYPE_MAP[gate['type']]) + op = Op.create(OPTYPE_MAP[gate.gtype]) - if gate['dagger']: + if gate.dagger: op = op.dagger - qubits = gate['qubits'] + qubits = gate.qubits circuit.add_gate(op, qubits) - for measure in circuit_dict['measurements']['measure']: - circuit.Measure(measure['qubit'], measure['bit']) - - for postselect in circuit_dict['measurements']['post']: - circuit.Measure(postselect['qubit'], postselect['bit']) + for mq, bi in circuitInfo.bitmap.items(): + circuit.Measure(mq, bi) return circuit diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index c27a9889..9e04bc3c 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -49,8 +49,8 @@ import sympy import torch -from lambeq.backend.quantum import (circuital_to_dict, - is_circuital, Measure, +from lambeq.backend.quantum import (Gate, is_circuital, Measure, + readoff_circuital, to_circuital) if TYPE_CHECKING: @@ -82,7 +82,7 @@ } -def extract_ops_from_circuital(circuit_dict: dict): +def extract_ops_from_circuital(gates: list[Gate]): """ Extract the operation, parameters and wires from a circuital diagram dictionary, and return the corresponding PennyLane @@ -106,10 +106,10 @@ def extract_ops_from_circuital(circuit_dict: dict): The wires/qubits to apply the operation to. """ - ops = [OP_MAP[x['type']] for x in circuit_dict['gates']] - qubits = [x['qubits'] for x in circuit_dict['gates']] - params = [x['phase'] if 'phase' in x and x['phase'] else [] - for x in circuit_dict['gates']] + 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() @@ -140,7 +140,7 @@ def to_pennylane(diagram: 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 @@ -181,21 +181,19 @@ def to_pennylane(diagram: Diagram, probabilities=False, if not is_circuital(diagram): diagram = to_circuital(diagram) - circuit_dict = circuital_to_dict(diagram) + circuitInfo = readoff_circuital(diagram) - scalar = 1 - for gate in circuit_dict['gates']: - if gate['type'] == 'Scalar': - scalar *= gate['phase'] - circuit_dict['gates'].remove(gate) + scalar = 1.0 + for gate in circuitInfo.gates: + if gate.gtype == 'Scalar' and not gate.phase is None: + scalar *= gate.phase + circuitInfo.gates.remove(gate) - ex_ops = extract_ops_from_circuital(circuit_dict) + ex_ops = extract_ops_from_circuital(circuitInfo.gates) op_list, params_list, symbols_set, wires_list = ex_ops # Get post selection bits - post_selection = {} - for postselect in circuit_dict['measurements']['post'] : - post_selection[postselect['qubit']] = postselect['phase'] + post_selection = circuitInfo.postmap return PennyLaneCircuit(op_list, list(symbols_set), @@ -205,7 +203,7 @@ def to_pennylane(diagram: Diagram, probabilities=False, post_selection, is_mixed, scalar, - circuit_dict['qubits']['total'], + circuitInfo.totalQubits, backend_config, diff_method) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 95b79e58..7b5398e5 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -30,12 +30,12 @@ from __future__ import annotations from collections.abc import Callable -import copy from dataclasses import dataclass, field, replace from functools import partial -from typing import cast, Dict, Tuple +from typing import cast, Dict, Tuple, Union, Optional import numpy as np +import pickle import tensornetwork as tn from typing_extensions import Any, Self @@ -1240,7 +1240,8 @@ def to_circuital(diagram: Diagram): # 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. - circuit = copy.deepcopy(diagram) + serializedcircuit = pickle.dumps(diagram) + circuit = pickle.loads(serializedcircuit) qubits: list[Layer] = [] gates: list[Layer] = [] @@ -1500,101 +1501,59 @@ def build_left_right(q_idx, layer, layers): return layerD -def circuital_to_dict(diagram: Diagram): - """Takes a circuital :py:class:`lambeq.quantum.Diagram`, returns - a dictionary for converting to a circuit. - Will check if the diagram is circuital before converting. +@dataclass +class Gate: + """Gate information for backend circuit construction. Parameters ---------- - diagram : :py:class:`~lambeq.backend.quantum.Diagram` - The :py:class:`Circuits ` - to be converted to dictionary. - - Returns - ------- - Dict: - d['gates'] : list of gates - d['gates'][:] : {'name': 'name', - 'type': 'repr(lambeq.quantum.Parameterized)', - 'qubits': All qubits for box [q_i, q_{i+1}, ...], - 'dagger': bool, - 'phase': (Optional) float for parameter values, - post-selection bits, etc. - 'control': (Optional) control qubits - 'gate_q' : (Optional) gate qubit if control gate - } - - d['measurements'] : Dictionary storing discard, post-selection, - and measurement information. - d['qubits'] : Dictionary containing qubit information. + 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. """ - - assert is_circuital(diagram) - - layers = diagram.layers - - num_qubits = sum([1 for layer in layers if isinstance(layer.box, Ket)]) - available_qubits = list(range(num_qubits)) - - circuit_dict: dict = {} - circuit_dict['gates'] = [] - circuit_dict['measurements'] = {'post': [], 'discard': [], - 'measure': []} - circuit_dict['qubits'] = {'total': num_qubits, 'bitmap': {}, - 'post': [], 'discard': [], 'measure': []} - bitmap: dict = {} - - 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) - circuit_dict['qubits']['measure'].append(qi) - circuit_dict['measurements']['measure'].append( - {'type': 'Measure', 'qubit': qi, - 'bit': bitmap[qi]} - ) - elif isinstance(layer.box, Bra): - qi = available_qubits[layer.left.count(qubit)] - available_qubits.remove(qi) - bitmap[qi] = len(bitmap) - circuit_dict['qubits']['post'].append(qi) - circuit_dict['measurements']['post'].append( - {'type': 'Bra', 'qubit': qi, - 'bit': bitmap[qi], 'phase': layer.box.bit} - ) - elif isinstance(layer.box, Discard): - qi = available_qubits[layer.left.count(qubit)] - available_qubits.remove(qi) - circuit_dict['measurements']['discard'].append( - {'type': 'Discard', 'qubit': qi} - ) - circuit_dict['qubits']['discard'].append(qi) - else: - qi = len(layer.left) - circuit_dict['gates'].append(gate_to_dict(layer.box, qi)) - - circuit_dict['qubits']['bitmap'] = bitmap - - return circuit_dict + 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 gate_to_dict(box: Box, offset: int) -> Dict: +def gateFromBox(box: Box, offset: int) -> Gate: + """Constructs Gate for backend circuit construction + from a Box. - gdict: Dict = {} - gdict['name'] = box.name - gdict['type'] = box.name.split('(')[0] - gdict['qubits'] = [offset + j for j in range(len(box.dom))] - gdict['phase'] = 0 - gdict['dagger'] = False + 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() - gdict['dagger'] = True - gdict['type'] = box.name.split('(')[0] + dagger = True + gtype = box.name.split('(')[0] if isinstance(box, (Rx, Ry, Rz)): phase = box.phase @@ -1602,8 +1561,6 @@ def gate_to_dict(box: Box, offset: int) -> Dict: # Tket uses sympy, lambeq uses custom symbol phase = box.phase.to_sympy() - gdict['phase'] = phase - elif isinstance(box, Controlled): # reverse the distance order @@ -1625,21 +1582,113 @@ def gate_to_dict(box: Box, offset: int) -> Dict: right_most_idx = max(rel_idx) rel_idx.insert(-1, right_most_idx - dist) - i_qubits = [gdict['qubits'][i] for i in rel_idx] + i_qubits = [qubits[i] for i in rel_idx] - gdict['qubits'] = i_qubits - gdict['control'] = sorted(gdict['qubits'][:-1]) - gdict['gate_q'] = gdict['qubits'][-1] + qubits = i_qubits + control = sorted(qubits[:-1]) + gate_q = qubits[-1] - if gdict['type'] in ('CRx', 'CRz'): - gdict['phase'] = box.phase + if gtype in ('CRx', 'CRz'): + phase = box.phase if isinstance(box.phase, Symbol): # Tket uses sympy, lambeq uses custom symbol - gdict['phase'] = box.phase.to_sympy() + phase = box.phase.to_sympy() elif isinstance(box, Scalar): - gdict['type'] = 'Scalar' + gtype = 'Scalar' # Just a placeholder - gdict['phase'] = box.array + 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 gdict + return CircuitInfo(totalQubits, + gates, + bitmap, + postmap, + discards) From 4df0eac02eab42a19d1c05d99ae3e7171aec2b6b Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 7 Jan 2025 16:51:39 +0000 Subject: [PATCH 35/40] Fixed linting etc --- lambeq/backend/converters/tk.py | 7 +++++-- lambeq/backend/pennylane.py | 8 +++++--- lambeq/backend/quantum.py | 7 +++---- tests/backend/test_quantum.py | 7 +++++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 0e1dfdfc..dc1c5d66 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -247,8 +247,11 @@ def to_tk(diagram: Diagram): for gate in circuitInfo.gates: if gate.gtype == 'Scalar': - circuit.scale(abs(gate.phase)**2) - continue + if isinstance(gate.phase, (int, float)): + circuit.scale(abs(gate.phase)**2) + continue + else: + raise ValueError(f'Scalar gate {gate} has phase type None') elif gate.gtype not in OPTYPE_MAP: raise NotImplementedError(f'Gate {gate.gtype} not supported') diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 9e04bc3c..42925caa 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -108,8 +108,10 @@ def extract_ops_from_circuital(gates: list[Gate]): """ 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] + params: list[Union[sympy.Expr, float, int, list]] = [x.phase + if x.phase + else [] + for x in gates] symbols = set() @@ -185,7 +187,7 @@ def to_pennylane(diagram: Diagram, probabilities=False, scalar = 1.0 for gate in circuitInfo.gates: - if gate.gtype == 'Scalar' and not gate.phase is None: + if gate.gtype == 'Scalar' and gate.phase is not None: scalar *= gate.phase circuitInfo.gates.remove(gate) diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 7b5398e5..4cb730c6 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -32,14 +32,13 @@ from collections.abc import Callable from dataclasses import dataclass, field, replace from functools import partial -from typing import cast, Dict, Tuple, Union, Optional +from typing import cast, Dict, Optional, Tuple, Union +import pickle import numpy as np -import pickle import tensornetwork as tn from typing_extensions import Any, Self - from lambeq.backend import Functor, grammar, Symbol, tensor from lambeq.backend.numerical_backend import backend, get_backend from lambeq.backend.symbol import lambdify @@ -1462,7 +1461,7 @@ def build_left_right(q_idx, layer, layers): return layer - circuit = Functor(target_category=quantum, # type: ignore [assignment] + circuit = Functor(target_category=quantum, ob=lambda _, x: x, ar=remove_ketbra1)(circuit) # type: ignore [arg-type] diff --git a/tests/backend/test_quantum.py b/tests/backend/test_quantum.py index d0afb9cf..c2fce679 100644 --- a/tests/backend/test_quantum.py +++ b/tests/backend/test_quantum.py @@ -213,8 +213,11 @@ def test_eval_w_aer_backend(): def test_to_circuital(): circ = to_circuital((Ket(0) >> H >> Measure())) assert is_circuital(circ) - cdict = circuital_to_dict(circ) - assert cdict['gates'] == [{'name': 'H', 'qubits': [0], 'type': 'H', 'phase': 0.0, 'dagger': False},] + 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(): From 96d5e0e2f2f6a326719532fa0d66fdaa7b73857b Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 7 Jan 2025 17:02:32 +0000 Subject: [PATCH 36/40] Fixed linting etc --- lambeq/backend/converters/tk.py | 6 +++--- lambeq/backend/quantum.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index dc1c5d66..08f133f7 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -247,11 +247,11 @@ def to_tk(diagram: Diagram): for gate in circuitInfo.gates: if gate.gtype == 'Scalar': - if isinstance(gate.phase, (int, float)): + if gate.phase is None: + raise ValueError(f'Scalar gate {gate} has phase type None') + else: circuit.scale(abs(gate.phase)**2) continue - else: - raise ValueError(f'Scalar gate {gate} has phase type None') elif gate.gtype not in OPTYPE_MAP: raise NotImplementedError(f'Gate {gate.gtype} not supported') diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 4cb730c6..8dc6b74f 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -32,8 +32,8 @@ from collections.abc import Callable from dataclasses import dataclass, field, replace from functools import partial -from typing import cast, Dict, Optional, Tuple, Union import pickle +from typing import cast, Dict, Optional, Tuple, Union import numpy as np import tensornetwork as tn From f7d78b4ee5adfb2733d41e485226ac4840b79205 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 7 Jan 2025 20:00:38 +0000 Subject: [PATCH 37/40] Fixed mypy --- lambeq/backend/converters/tk.py | 4 ++-- lambeq/backend/grammar.py | 6 +++--- lambeq/backend/pennylane.py | 22 ++++++++++++++------ lambeq/backend/quantum.py | 37 +++++++++++++++++++-------------- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/lambeq/backend/converters/tk.py b/lambeq/backend/converters/tk.py index 08f133f7..49530648 100644 --- a/lambeq/backend/converters/tk.py +++ b/lambeq/backend/converters/tk.py @@ -211,7 +211,7 @@ def _tk_to_lmbq_param(theta): raise ValueError('Parameter must be a (possibly scaled) sympy Symbol') -def to_tk(diagram: Diagram): +def to_tk(diagram: Diagram) -> Circuit: """Takes a :py:class:`lambeq.quantum.Diagram`, returns a :class:`lambeq.backend.converters.tk.Circuit` for t|ket>. @@ -250,7 +250,7 @@ def to_tk(diagram: Diagram): if gate.phase is None: raise ValueError(f'Scalar gate {gate} has phase type None') else: - circuit.scale(abs(gate.phase)**2) + 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') diff --git a/lambeq/backend/grammar.py b/lambeq/backend/grammar.py index fb3f21a1..ac8dd23f 100644 --- a/lambeq/backend/grammar.py +++ b/lambeq/backend/grammar.py @@ -280,7 +280,7 @@ def tensor(self, other: Iterable[Self]) -> Self: ... @overload def tensor(self, other: Self, *rest: Self) -> Self: ... - def tensor(self, other: Self | Iterable[Self], *rest: Self) -> Self: + def tensor(self, other: Self | Iterable[Self], *rest: Self) -> Self | Any: try: tys = [*other, *rest] except TypeError: @@ -957,7 +957,7 @@ def lift(cls, diagrams: Iterable[Diagrammable | Ty]) -> list[Self]: return diags # type: ignore[return-value] - def tensor(self, *diagrams: Diagrammable | Ty) -> Self: + def tensor(self, *diagrams: Diagrammable | Ty) -> Self | Any: try: diags = self.lift([self, *diagrams]) except ValueError: @@ -1016,7 +1016,7 @@ def __getitem__(self, key: int | slice) -> Self: return self[key:key + 1] raise TypeError - def then(self, *diagrams: Diagrammable) -> Self: + def then(self, *diagrams: Diagrammable) -> Self | Any: try: diags = self.lift(diagrams) except ValueError: diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index 42925caa..c101cb56 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -43,12 +43,13 @@ from itertools import product import sys -from typing import TYPE_CHECKING, Union +from typing import List, TYPE_CHECKING, Union, Set, Tuple import pennylane as qml import sympy import torch +from lambeq.backend import Symbol from lambeq.backend.quantum import (Gate, is_circuital, Measure, readoff_circuital, to_circuital) @@ -82,7 +83,14 @@ } -def extract_ops_from_circuital(gates: list[Gate]): +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 circuital diagram dictionary, and return the corresponding PennyLane @@ -95,14 +103,14 @@ def extract_ops_from_circuital(gates: list[Gate]): 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. """ @@ -132,8 +140,10 @@ def extract_ops_from_circuital(gates: list[Gate]): return ops, remapped_params, symbols, qubits -def to_pennylane(diagram: 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 diff --git a/lambeq/backend/quantum.py b/lambeq/backend/quantum.py index 8dc6b74f..81eecc23 100644 --- a/lambeq/backend/quantum.py +++ b/lambeq/backend/quantum.py @@ -1169,10 +1169,12 @@ def is_circuital(diagram: Diagram) -> bool: """ Takes a :py:class:`lambeq.quantum.Diagram`, checks if a diagram is a quantum 'circuital' diagram. - A circuital diagram is a diagram with qubits at the top. - Used to check for measurements and post-selection - at the bottom but there are too many edge cases. + Circuital means: + 1. All initial layers are qubits + 2. All post selections are at the end + + Allows for mixed_circuit measurements Returns ------- @@ -1186,7 +1188,6 @@ def is_circuital(diagram: Diagram) -> bool: layers = diagram.layers - # Check if the first layers are all qubits and measurements num_qubits = sum([1 for layer in layers if isinstance(layer.box, Ket)]) @@ -1214,7 +1215,7 @@ def is_circuital(diagram: Diagram) -> bool: return True -def to_circuital(diagram: Diagram): +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 @@ -1259,11 +1260,11 @@ 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 - # Appends shifts all the gates - # Assumes we only add one qubit at a time. - # No bits + """ + 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) @@ -1280,7 +1281,8 @@ def add_qubit(qubits: list[Layer], return qubits, pull_qubit_through(offset, gates, dom=layer.box.cod)[0] - def construct_measurements(last_layer, post_selects): + 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) @@ -1415,9 +1417,11 @@ def pull_qubit_through(q_idx: int, 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. + """ + 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)) @@ -1430,7 +1434,9 @@ def pull_qubit_through(q_idx: int, return new_gates, q_idx - def build_left_right(q_idx, layer, layers): + 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 @@ -1595,7 +1601,6 @@ def gateFromBox(box: Box, offset: int) -> Gate: elif isinstance(box, Scalar): gtype = 'Scalar' - # Just a placeholder phase = box.array return Gate( From 062031e84d87501ca00cbd5f02d542dc1b9115c1 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 7 Jan 2025 20:39:30 +0000 Subject: [PATCH 38/40] Fixed mypy again --- lambeq/backend/grammar.py | 16 ++++++++-------- lambeq/backend/pennylane.py | 2 +- lambeq/backend/pregroup_tree.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lambeq/backend/grammar.py b/lambeq/backend/grammar.py index ac8dd23f..b758de0b 100644 --- a/lambeq/backend/grammar.py +++ b/lambeq/backend/grammar.py @@ -280,20 +280,20 @@ def tensor(self, other: Iterable[Self]) -> Self: ... @overload def tensor(self, other: Self, *rest: Self) -> Self: ... - def tensor(self, other: Self | Iterable[Self], *rest: Self) -> Self | Any: + def tensor(self, other: Self | Iterable[Self], *rest: Self) -> Self: try: tys = [*other, *rest] except TypeError: - return NotImplemented + raise NotImplementedError # Diagrams are iterable - the identity diagram has # an empty list for its layers but may still contain types if getattr(other, 'is_id', False): - return NotImplemented + raise NotImplementedError if any(not isinstance(ty, type(self)) or self.category != ty.category for ty in tys): - return NotImplemented + raise NotImplementedError return self._fromiter(ob for ty in (self, *tys) for ob in ty) @@ -957,11 +957,11 @@ def lift(cls, diagrams: Iterable[Diagrammable | Ty]) -> list[Self]: return diags # type: ignore[return-value] - def tensor(self, *diagrams: Diagrammable | Ty) -> Self | Any: + def tensor(self, *diagrams: Diagrammable | Ty) -> Self: try: diags = self.lift([self, *diagrams]) except ValueError: - return NotImplemented + raise NotImplementedError right = dom = self.dom.tensor(*[ diagram.to_diagram().dom for diagram in diagrams @@ -1016,11 +1016,11 @@ def __getitem__(self, key: int | slice) -> Self: return self[key:key + 1] raise TypeError - def then(self, *diagrams: Diagrammable) -> Self | Any: + def then(self, *diagrams: Diagrammable) -> Self: try: diags = self.lift(diagrams) except ValueError: - return NotImplemented + raise NotImplementedError layers = [*self.layers] cod = self.cod diff --git a/lambeq/backend/pennylane.py b/lambeq/backend/pennylane.py index c101cb56..26cdc4ef 100644 --- a/lambeq/backend/pennylane.py +++ b/lambeq/backend/pennylane.py @@ -43,7 +43,7 @@ from itertools import product import sys -from typing import List, TYPE_CHECKING, Union, Set, Tuple +from typing import List, Set, Tuple, TYPE_CHECKING, Union import pennylane as qml import sympy 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) From 7b1d4eefa8fda7d747004c4678146f7982acca58 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 7 Jan 2025 20:55:14 +0000 Subject: [PATCH 39/40] Removed mypy fix to fix tests --- lambeq/backend/grammar.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lambeq/backend/grammar.py b/lambeq/backend/grammar.py index b758de0b..fb3f21a1 100644 --- a/lambeq/backend/grammar.py +++ b/lambeq/backend/grammar.py @@ -284,16 +284,16 @@ def tensor(self, other: Self | Iterable[Self], *rest: Self) -> Self: try: tys = [*other, *rest] except TypeError: - raise NotImplementedError + return NotImplemented # Diagrams are iterable - the identity diagram has # an empty list for its layers but may still contain types if getattr(other, 'is_id', False): - raise NotImplementedError + return NotImplemented if any(not isinstance(ty, type(self)) or self.category != ty.category for ty in tys): - raise NotImplementedError + return NotImplemented return self._fromiter(ob for ty in (self, *tys) for ob in ty) @@ -961,7 +961,7 @@ def tensor(self, *diagrams: Diagrammable | Ty) -> Self: try: diags = self.lift([self, *diagrams]) except ValueError: - raise NotImplementedError + return NotImplemented right = dom = self.dom.tensor(*[ diagram.to_diagram().dom for diagram in diagrams @@ -1020,7 +1020,7 @@ def then(self, *diagrams: Diagrammable) -> Self: try: diags = self.lift(diagrams) except ValueError: - raise NotImplementedError + return NotImplemented layers = [*self.layers] cod = self.cod From 4d987333c12f2d5cce6893a15079b5363d7f9e66 Mon Sep 17 00:00:00 2001 From: Blake Wilson Date: Tue, 7 Jan 2025 21:21:04 +0000 Subject: [PATCH 40/40] Fixed snake and other minor changes --- lambeq/backend/grammar.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lambeq/backend/grammar.py b/lambeq/backend/grammar.py index fb3f21a1..7bd213a1 100644 --- a/lambeq/backend/grammar.py +++ b/lambeq/backend/grammar.py @@ -1026,10 +1026,8 @@ def then(self, *diagrams: Diagrammable) -> Self: cod = self.cod for n, diagram in enumerate(diags): if diagram.dom != cod: - raise ValueError(f'Diagram {n} ' - f'(cod={cod.__repr__()}) ' - f'does not compose with diagram {n+1} ' - f'(dom={diagram.dom.__repr__()})') + raise ValueError(f'Diagram {n} (cod={cod}) does not compose ' + f'with diagram {n+1} (dom={diagram.dom})') cod = diagram.cod layers.extend(diagram.layers) @@ -1972,14 +1970,14 @@ class Functor: >>> diag.draw( ... 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='./docs/_static/images/snake-2.png') + ... figsize=(2, 2), path='./snake-2.png') - .. image:: ./_static/images/snake-2.png + .. image:: ../_static/images/snake-2.png :align: center """