diff --git a/qualtran/__init__.py b/qualtran/__init__.py index 98ee00bd7..14a904a4e 100644 --- a/qualtran/__init__.py +++ b/qualtran/__init__.py @@ -58,4 +58,6 @@ Soquet, ) +from ._infra.gate_with_registers import GateWithRegisters + # -------------------------------------------------------------------------------------------------- diff --git a/qualtran/_infra/gate_with_registers.ipynb b/qualtran/_infra/gate_with_registers.ipynb new file mode 100644 index 000000000..2f371e41b --- /dev/null +++ b/qualtran/_infra/gate_with_registers.ipynb @@ -0,0 +1,244 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "3b990f88", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2023 The Cirq Developers\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "id": "bf9c80ce", + "metadata": {}, + "source": [ + "# Gate with Registers\n", + "\n", + "This package includes a subclass of `cirq.Gate` called `GateWithRegisters`. Instead of operating on a flat list of `cirq.Qid`, this lets the developer define gates in terms of named registers of given widths." + ] + }, + { + "cell_type": "markdown", + "id": "c0833444", + "metadata": {}, + "source": [ + "## `Signature`\n", + "\n", + "`Register` objects have a name, a bitsize and a shape. `Signature` is an ordered collection of `Register` with some helpful methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c75414f2", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran import Register, Signature\n", + "\n", + "control_reg = Register(name='control', bitsize=2)\n", + "target_reg = Register(name='target', bitsize=3)\n", + "control_reg, target_reg" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b38d210c", + "metadata": {}, + "outputs": [], + "source": [ + "r = Signature([control_reg, target_reg])\n", + "r" + ] + }, + { + "cell_type": "markdown", + "id": "2b32274b", + "metadata": {}, + "source": [ + "You can also use the `build` factory method to quickly define a set of registers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f10e66e", + "metadata": {}, + "outputs": [], + "source": [ + "r == Signature.build(\n", + " control=2,\n", + " target=3,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a5955208", + "metadata": {}, + "source": [ + "### `GateWithRegisters`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3957db4", + "metadata": {}, + "outputs": [], + "source": [ + "import cirq\n", + "from qualtran import GateWithRegisters\n", + "\n", + "class MyGate(GateWithRegisters):\n", + " \n", + " @property\n", + " def signature(self):\n", + " return Signature.build(\n", + " control=2,\n", + " target=3,\n", + " )\n", + " \n", + " def decompose_from_registers(self, context, control, target):\n", + " assert len(control) == 2\n", + " assert len(target) == 3\n", + " \n", + " for c in control:\n", + " for t in target:\n", + " yield cirq.CNOT(c, t)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2de931eb", + "metadata": {}, + "outputs": [], + "source": [ + "gate = MyGate()\n", + "gate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef98f3a2", + "metadata": {}, + "outputs": [], + "source": [ + "# Number of qubits is derived from registers\n", + "cirq.num_qubits(gate)" + ] + }, + { + "cell_type": "markdown", + "id": "2d725646", + "metadata": {}, + "source": [ + "The `Signature` object can allocate a dictionary of `cirq.NamedQubit` that we can use to turn our `Gate` into an `Operation`. `GateWithRegisters` exposes an `on_registers` method to compliment Cirq's `on` method where we can use names to make sure each qubit is used appropriately." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "057148da", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran._infra.gate_with_registers import get_named_qubits\n", + "\n", + "r = gate.signature\n", + "quregs = get_named_qubits(r)\n", + "quregs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0257d8f1", + "metadata": {}, + "outputs": [], + "source": [ + "operation = gate.on_registers(**quregs)\n", + "operation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "541f2e91", + "metadata": {}, + "outputs": [], + "source": [ + "from cirq.contrib.svg import SVGCircuit\n", + "SVGCircuit(cirq.Circuit(operation))" + ] + }, + { + "cell_type": "markdown", + "id": "6686f7f8", + "metadata": {}, + "source": [ + "## `GateHelper`\n", + "\n", + "Since `GateWithRegisters` contains enough metadata to derive qubits, an operation, and a circuit we provide a helper class to provide easy access to these quantities." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93a6c8f2", + "metadata": {}, + "outputs": [], + "source": [ + "import qualtran.cirq_interop.testing as cq_testing\n", + "\n", + "g = cq_testing.GateHelper(gate)\n", + "\n", + "print('r:', g.r)\n", + "print('quregs:', g.quregs)\n", + "print('operation:', g.operation)\n", + "print('\\ncircuit:\\n', g.circuit)\n", + "print('\\n\\ndecomposed circuit:\\n', cirq.Circuit(cirq.decompose_once(g.operation)))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/qualtran/_infra/gate_with_registers.py b/qualtran/_infra/gate_with_registers.py new file mode 100644 index 000000000..5946ca7dd --- /dev/null +++ b/qualtran/_infra/gate_with_registers.py @@ -0,0 +1,246 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import itertools +from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, TYPE_CHECKING, Union + +import attr +import cirq +import numpy as np +import quimb.tensor as qtn +from numpy.typing import NDArray + +from qualtran._infra.bloq import Bloq +from qualtran._infra.composite_bloq import CompositeBloq, SoquetT +from qualtran._infra.quantum_graph import Soquet +from qualtran._infra.registers import Register, Side, Signature + +if TYPE_CHECKING: + from qualtran.cirq_interop import CirqQuregT + + +def total_bits(registers: Iterable[Register]) -> int: + """Sum of `reg.total_bits()` for each register `reg` in input `signature`.""" + return sum(reg.total_bits() for reg in registers) + + +def split_qubits( + registers: Iterable[Register], qubits: Sequence[cirq.Qid] +) -> Dict[str, NDArray[cirq.Qid]]: # type: ignore[type-var] + """Splits the flat list of qubits into a dictionary of appropriately shaped qubit arrays.""" + + qubit_regs = {} + base = 0 + for reg in registers: + qubit_regs[reg.name] = np.array(qubits[base : base + reg.total_bits()]).reshape( + reg.shape + (reg.bitsize,) + ) + base += reg.total_bits() + return qubit_regs + + +def merge_qubits( + registers: Iterable[Register], + **qubit_regs: Union[cirq.Qid, Sequence[cirq.Qid], NDArray[cirq.Qid]], +) -> List[cirq.Qid]: + """Merges the dictionary of appropriately shaped qubit arrays into a flat list of qubits.""" + + ret: List[cirq.Qid] = [] + for reg in registers: + if reg.name not in qubit_regs: + raise ValueError(f"All qubit registers must be present. {reg.name} not in qubit_regs") + qubits = qubit_regs[reg.name] + qubits = np.array([qubits] if isinstance(qubits, cirq.Qid) else qubits) + full_shape = reg.shape + (reg.bitsize,) + if qubits.shape != full_shape: + raise ValueError( + f'{reg.name} register must of shape {full_shape} but is of shape {qubits.shape}' + ) + ret += qubits.flatten().tolist() + return ret + + +def get_named_qubits(registers: Iterable[Register]) -> Dict[str, NDArray[cirq.Qid]]: + """Returns a dictionary of appropriately shaped named qubit signature for input `signature`.""" + + def _qubit_array(reg: Register): + qubits = np.empty(reg.shape + (reg.bitsize,), dtype=object) + for ii in reg.all_idxs(): + for j in range(reg.bitsize): + prefix = "" if not ii else f'[{", ".join(str(i) for i in ii)}]' + suffix = "" if reg.bitsize == 1 else f"[{j}]" + qubits[ii + (j,)] = cirq.NamedQubit(reg.name + prefix + suffix) + return qubits + + def _qubits_for_reg(reg: Register): + if len(reg.shape) > 0: + return _qubit_array(reg) + + return np.array( + [cirq.NamedQubit(f"{reg.name}")] + if reg.total_bits() == 1 + else cirq.NamedQubit.range(reg.total_bits(), prefix=reg.name), + dtype=object, + ) + + return {reg.name: _qubits_for_reg(reg) for reg in registers} + + +class GateWithRegisters(Bloq, cirq.Gate, metaclass=abc.ABCMeta): + """`cirq.Gate`s extension with support for composite gates acting on multiple qubit registers. + + Though Cirq was nominally designed for circuit construction for near-term devices the core + concept of the `cirq.Gate`, a programmatic representation of an operation on a state without + a complete qubit address specification, can be leveraged to describe more abstract algorithmic + primitives. To define composite gates, users derive from `cirq.Gate` and implement the + `_decompose_` method that yields the sub-operations provided a flat list of qubits. + + This API quickly becomes inconvenient when defining operations that act on multiple qubit + registers of variable sizes. Cirq-FT extends the `cirq.Gate` idea by introducing a new abstract + base class `cirq_ft.GateWithRegisters` containing abstract methods `registers` and optional + method `decompose_from_registers` that provides an overlay to the Cirq flat address API. + + As an example, in the following code snippet we use the `cirq_ft.GateWithRegisters` to + construct a multi-target controlled swap operation: + + >>> import attr + >>> import cirq + >>> import qualtran + >>> + >>> @attr.frozen + ... class MultiTargetCSwap(qualtran.GateWithRegisters): + ... bitsize: int + ... + ... @property + ... def signature(self) -> qualtran.Signature: + ... return qualtran.Signature.build(ctrl=1, x=self.bitsize, y=self.bitsize) + ... + ... def decompose_from_registers(self, context, ctrl, x, y) -> cirq.OP_TREE: + ... yield [cirq.CSWAP(*ctrl, qx, qy) for qx, qy in zip(x, y)] + ... + >>> op = MultiTargetCSwap(2).on_registers( + ... ctrl=[cirq.q('ctrl')], + ... x=cirq.NamedQubit.range(2, prefix='x'), + ... y=cirq.NamedQubit.range(2, prefix='y'), + ... ) + >>> print(cirq.Circuit(op)) + ctrl: ───MultiTargetCSwap─── + │ + x0: ─────x────────────────── + │ + x1: ─────x────────────────── + │ + y0: ─────y────────────────── + │ + y1: ─────y──────────────────""" + + # Part-1: Bloq interface is automatically available for users, via default convertors. + + def decompose_bloq(self) -> 'CompositeBloq': + """Decompose this Bloq into its constituent parts contained in a CompositeBloq. + + Bloq users can call this function to delve into the definition of a Bloq. If you're + trying to define a bloq's decomposition, consider overriding `build_composite_bloq` + which provides helpful arguments for implementers. + + Returns: + A CompositeBloq containing the decomposition of this Bloq. + + Raises: + NotImplementedError: If there is no decomposition defined; namely: if + `build_composite_bloq` returns `NotImplemented`. + """ + from qualtran.cirq_interop._cirq_to_bloq import decompose_from_cirq_op + + return decompose_from_cirq_op(self, decompose_once=True) + + def add_my_tensors( + self, + tn: qtn.TensorNetwork, + tag: Any, + *, + incoming: Dict[str, 'SoquetT'], + outgoing: Dict[str, 'SoquetT'], + ): + from qualtran.cirq_interop._cirq_to_bloq import _add_my_tensors_from_gate + + _add_my_tensors_from_gate( + self, + self.signature, + self.short_name(), + tn=tn, + tag=tag, + incoming=incoming, + outgoing=outgoing, + ) + + def as_cirq_op( + self, qubit_manager: 'cirq.QubitManager', **cirq_quregs: 'CirqQuregT' + ) -> Tuple[Union['cirq.Operation', None], Dict[str, 'CirqQuregT']]: + from qualtran.cirq_interop._bloq_to_cirq import _construct_op_from_gate + + return _construct_op_from_gate( + self, + in_quregs={k: np.array(v) for k, v in cirq_quregs.items()}, + qubit_manager=qubit_manager, + ) + + def t_complexity(self) -> 'TComplexity': + from qualtran.cirq_interop.t_complexity_protocol import t_complexity + + return t_complexity(self) + + def wire_symbol(self, soq: 'Soquet') -> 'WireSymbol': + from qualtran.cirq_interop._cirq_to_bloq import _wire_symbol_from_gate + + return _wire_symbol_from_gate(self, self.signature, soq) + + # Part-2: Cirq-FT style interface can be used to implemented algorithms by Bloq authors. + + def _num_qubits_(self) -> int: + return total_bits(self.signature) + + def decompose_from_registers( + self, *, context: cirq.DecompositionContext, **quregs: NDArray[cirq.Qid] + ) -> cirq.OP_TREE: + return NotImplemented + + def _decompose_with_context_( + self, qubits: Sequence[cirq.Qid], context: Optional[cirq.DecompositionContext] = None + ) -> cirq.OP_TREE: + qubit_regs = split_qubits(self.signature, qubits) + if context is None: + context = cirq.DecompositionContext(cirq.ops.SimpleQubitManager()) + return self.decompose_from_registers(context=context, **qubit_regs) + + def _decompose_(self, qubits: Sequence[cirq.Qid]) -> cirq.OP_TREE: + return self._decompose_with_context_(qubits) + + def on_registers( + self, **qubit_regs: Union[cirq.Qid, Sequence[cirq.Qid], NDArray[cirq.Qid]] + ) -> cirq.Operation: + return self.on(*merge_qubits(self.signature, **qubit_regs)) + + def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs) -> cirq.CircuitDiagramInfo: + """Default diagram info that uses register names to name the boxes in multi-qubit gates. + + Descendants can override this method with more meaningful circuit diagram information. + """ + wire_symbols = [] + for reg in self.signature: + wire_symbols += [reg.name] * reg.total_bits() + + wire_symbols[0] = self.__class__.__name__ + return cirq.CircuitDiagramInfo(wire_symbols=wire_symbols) diff --git a/qualtran/_infra/gate_with_registers_test.py b/qualtran/_infra/gate_with_registers_test.py new file mode 100644 index 000000000..b25432d90 --- /dev/null +++ b/qualtran/_infra/gate_with_registers_test.py @@ -0,0 +1,49 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cirq + +from qualtran import GateWithRegisters, Register, Signature +from qualtran.testing import execute_notebook + + +class _TestGate(GateWithRegisters): + @property + def signature(self) -> Signature: + r1 = Register("r1", 5) + r2 = Register("r2", 2) + r3 = Register("r3", 1) + regs = Signature([r1, r2, r3]) + return regs + + def decompose_from_registers(self, *, context, **quregs) -> cirq.OP_TREE: + yield cirq.H.on_each(quregs['r1']) + yield cirq.X.on_each(quregs['r2']) + yield cirq.X.on_each(quregs['r3']) + + +def test_gate_with_registers(): + tg = _TestGate() + assert tg._num_qubits_() == 8 + qubits = cirq.LineQubit.range(8) + circ = cirq.Circuit(tg._decompose_(qubits)) + assert circ.operation_at(cirq.LineQubit(3), 0).gate == cirq.H + + op1 = tg.on_registers(r1=qubits[:5], r2=qubits[6:], r3=qubits[5]) + op2 = tg.on(*qubits[:5], *qubits[6:], qubits[5]) + assert op1 == op2 + + +def test_notebook(): + execute_notebook('gate_with_registers') diff --git a/qualtran/_infra/registers.py b/qualtran/_infra/registers.py index d37aee001..7b9f5f569 100644 --- a/qualtran/_infra/registers.py +++ b/qualtran/_infra/registers.py @@ -19,7 +19,7 @@ from typing import Dict, Iterable, Iterator, List, overload, Tuple, TYPE_CHECKING import numpy as np -from attr import frozen +from attr import field, frozen from numpy.typing import NDArray if TYPE_CHECKING: @@ -61,7 +61,9 @@ class Register: name: str bitsize: int - shape: Tuple[int, ...] = tuple() + shape: Tuple[int, ...] = field( + default=tuple(), converter=lambda v: (v,) if isinstance(v, int) else tuple(v) + ) side: Side = Side.THRU def all_idxs(self) -> Iterable[Tuple[int, ...]]: diff --git a/qualtran/bloqs/and_bloq.ipynb b/qualtran/bloqs/and_bloq.ipynb index ac94ec430..3ce8835bf 100644 --- a/qualtran/bloqs/and_bloq.ipynb +++ b/qualtran/bloqs/and_bloq.ipynb @@ -12,12 +12,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "47e0d2c9", "metadata": { "cq.autogen": "top_imports" }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "OMP: Info #270: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n" + ] + } + ], "source": [ "from qualtran import Bloq, CompositeBloq, BloqBuilder, Signature, Register\n", "from qualtran.drawing import show_bloq\n", @@ -49,12 +57,103 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "4c4ec376", "metadata": { "cq.autogen": "_make_and.py" }, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "my_graph\n", + "\n", + "\n", + "\n", + "ctrl_G3\n", + "ctrl[0]\n", + "\n", + "\n", + "\n", + "And\n", + "\n", + "And\n", + "\n", + "ctrl[0]\n", + "\n", + "ctrl[1]\n", + "\n", + "\n", + "target\n", + "\n", + "\n", + "\n", + "ctrl_G3:e->And:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G1\n", + "ctrl[1]\n", + "\n", + "\n", + "\n", + "ctrl_G1:e->And:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G0\n", + "ctrl[0]\n", + "\n", + "\n", + "\n", + "And:e->ctrl_G0:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G5\n", + "ctrl[1]\n", + "\n", + "\n", + "\n", + "And:e->ctrl_G5:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "target_G2\n", + "target\n", + "\n", + "\n", + "\n", + "And:e->target_G2:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "from qualtran.bloqs.and_bloq import And\n", "\n", @@ -64,10 +163,64 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "310e538e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "counts\n", + "\n", + "\n", + "\n", + "b0\n", + "\n", + "And\n", + "And(cv1=1, cv2=1, adjoint=False)\n", + "\n", + "\n", + "\n", + "b1\n", + "\n", + "ArbitraryClifford\n", + "ArbitraryClifford(n=2)\n", + "\n", + "\n", + "\n", + "b0->b1\n", + "\n", + "\n", + "9\n", + "\n", + "\n", + "\n", + "b2\n", + "\n", + "TGate\n", + "TGate()\n", + "\n", + "\n", + "\n", + "b0->b2\n", + "\n", + "\n", + "4\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from qualtran.resource_counting import get_bloq_counts_graph, GraphvizCounts, SympySymbolAllocator\n", "import attrs\n", @@ -86,10 +239,123 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "3750053b", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "my_graph\n", + "\n", + "\n", + "\n", + "q0_G7\n", + "q0\n", + "\n", + "\n", + "\n", + "And_G0\n", + "\n", + "And\n", + "\n", + "ctrl[0]\n", + "\n", + "ctrl[1]\n", + "\n", + "\n", + "target\n", + "\n", + "\n", + "\n", + "q0_G7:e->And_G0:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "q1_G8\n", + "q1\n", + "\n", + "\n", + "\n", + "q1_G8:e->And_G0:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "And\n", + "\n", + "And†\n", + "\n", + "ctrl[0]\n", + "\n", + "ctrl[1]\n", + "\n", + "target\n", + "\n", + "\n", + "\n", + "\n", + "q0\n", + "q0\n", + "\n", + "\n", + "\n", + "And:e->q0:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "q1\n", + "q1\n", + "\n", + "\n", + "\n", + "And:e->q1:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "And_G0:e->And:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "And_G0:e->And:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "And_G0:e->And:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "bb = BloqBuilder()\n", "q0 = bb.add_register('q0', 1)\n", @@ -103,10 +369,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "a65ce1f3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "array([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],\n", + " [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],\n", + " [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],\n", + " [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]])" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "mat = cbloq.tensor_contract()\n", "np.testing.assert_allclose(np.eye(4), mat)\n", @@ -134,12 +414,185 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "51233de9", "metadata": { "cq.autogen": "_make_multi_and.py" }, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "my_graph\n", + "\n", + "\n", + "\n", + "ctrl_G9\n", + "ctrl[0]\n", + "\n", + "\n", + "\n", + "MultiAnd\n", + "\n", + "And\n", + "\n", + "ctrl[0]\n", + "\n", + "ctrl[1]\n", + "\n", + "ctrl[2]\n", + "\n", + "ctrl[3]\n", + "\n", + "\n", + "junk[0]\n", + "\n", + "\n", + "junk[1]\n", + "\n", + "\n", + "target\n", + "\n", + "\n", + "\n", + "ctrl_G9:e->MultiAnd:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G6\n", + "ctrl[1]\n", + "\n", + "\n", + "\n", + "ctrl_G6:e->MultiAnd:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G12\n", + "ctrl[2]\n", + "\n", + "\n", + "\n", + "ctrl_G12:e->MultiAnd:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G17\n", + "ctrl[3]\n", + "\n", + "\n", + "\n", + "ctrl_G17:e->MultiAnd:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G10\n", + "ctrl[0]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->ctrl_G10:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G7\n", + "ctrl[1]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->ctrl_G7:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G13\n", + "ctrl[2]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->ctrl_G13:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G1\n", + "ctrl[3]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->ctrl_G1:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "junk_G4\n", + "junk[0]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->junk_G4:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "junk_G11\n", + "junk[1]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->junk_G11:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "target_G8\n", + "target\n", + "\n", + "\n", + "\n", + "MultiAnd:e->target_G8:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "from qualtran.bloqs.and_bloq import MultiAnd\n", "\n", @@ -149,10 +602,183 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "dac3eaba", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "my_graph\n", + "\n", + "\n", + "\n", + "ctrl_G9\n", + "ctrl[0]\n", + "\n", + "\n", + "\n", + "MultiAnd\n", + "\n", + "And\n", + "\n", + "ctrl[0]\n", + "\n", + "ctrl[1]\n", + "\n", + "ctrl[2]\n", + "\n", + "ctrl[3]\n", + "\n", + "\n", + "junk[0]\n", + "\n", + "\n", + "junk[1]\n", + "\n", + "\n", + "target\n", + "\n", + "\n", + "\n", + "ctrl_G9:e->MultiAnd:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G6\n", + "ctrl[1]\n", + "\n", + "\n", + "\n", + "ctrl_G6:e->MultiAnd:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G12\n", + "ctrl[2]\n", + "\n", + "\n", + "\n", + "ctrl_G12:e->MultiAnd:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G17\n", + "ctrl[3]\n", + "\n", + "\n", + "\n", + "ctrl_G17:e->MultiAnd:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G10\n", + "ctrl[0]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->ctrl_G10:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G7\n", + "ctrl[1]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->ctrl_G7:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G13\n", + "ctrl[2]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->ctrl_G13:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "ctrl_G1\n", + "ctrl[3]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->ctrl_G1:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "junk_G4\n", + "junk[0]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->junk_G4:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "junk_G11\n", + "junk[1]\n", + "\n", + "\n", + "\n", + "MultiAnd:e->junk_G11:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "\n", + "target_G8\n", + "target\n", + "\n", + "\n", + "\n", + "MultiAnd:e->target_G8:w\n", + "\n", + "\n", + "1\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "bloq = MultiAnd(cvs=(1, 1, 1, 1))\n", "show_bloq(bloq.decompose_bloq())" @@ -160,10 +786,50 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "a61eb113", "metadata": {}, - "outputs": [], + "outputs": [ + { + "ename": "RecursionError", + "evalue": "maximum recursion depth exceeded while calling a Python object", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mRecursionError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[8], line 1\u001B[0m\n\u001B[0;32m----> 1\u001B[0m graph, sigma \u001B[38;5;241m=\u001B[39m \u001B[43mget_bloq_counts_graph\u001B[49m\u001B[43m(\u001B[49m\u001B[43mbloq\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 2\u001B[0m GraphvizCounts(graph)\u001B[38;5;241m.\u001B[39mget_svg()\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/resource_counting/bloq_counts.py:176\u001B[0m, in \u001B[0;36mget_bloq_counts_graph\u001B[0;34m(bloq, generalizer, ssa, keep)\u001B[0m\n\u001B[1;32m 174\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m bloq \u001B[38;5;129;01mis\u001B[39;00m \u001B[38;5;28;01mNone\u001B[39;00m:\n\u001B[1;32m 175\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mYou can\u001B[39m\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mt generalize away the root bloq.\u001B[39m\u001B[38;5;124m\"\u001B[39m)\n\u001B[0;32m--> 176\u001B[0m sigma \u001B[38;5;241m=\u001B[39m \u001B[43m_descend_counts\u001B[49m\u001B[43m(\u001B[49m\u001B[43mbloq\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mg\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mssa\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mgeneralizer\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mkeep\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 177\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m g, sigma\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/resource_counting/bloq_counts.py:130\u001B[0m, in \u001B[0;36m_descend_counts\u001B[0;34m(parent, g, ssa, generalizer, keep)\u001B[0m\n\u001B[1;32m 127\u001B[0m g\u001B[38;5;241m.\u001B[39madd_edge(parent, child, n\u001B[38;5;241m=\u001B[39mn)\n\u001B[1;32m 129\u001B[0m \u001B[38;5;66;03m# Do the recursive step, which will continue to mutate `g`\u001B[39;00m\n\u001B[0;32m--> 130\u001B[0m child_counts \u001B[38;5;241m=\u001B[39m \u001B[43m_descend_counts\u001B[49m\u001B[43m(\u001B[49m\u001B[43mchild\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mg\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mssa\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mgeneralizer\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mkeep\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 132\u001B[0m \u001B[38;5;66;03m# Update `sigma` with the recursion results.\u001B[39;00m\n\u001B[1;32m 133\u001B[0m \u001B[38;5;28;01mfor\u001B[39;00m k \u001B[38;5;129;01min\u001B[39;00m child_counts\u001B[38;5;241m.\u001B[39mkeys():\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/resource_counting/bloq_counts.py:130\u001B[0m, in \u001B[0;36m_descend_counts\u001B[0;34m(parent, g, ssa, generalizer, keep)\u001B[0m\n\u001B[1;32m 127\u001B[0m g\u001B[38;5;241m.\u001B[39madd_edge(parent, child, n\u001B[38;5;241m=\u001B[39mn)\n\u001B[1;32m 129\u001B[0m \u001B[38;5;66;03m# Do the recursive step, which will continue to mutate `g`\u001B[39;00m\n\u001B[0;32m--> 130\u001B[0m child_counts \u001B[38;5;241m=\u001B[39m \u001B[43m_descend_counts\u001B[49m\u001B[43m(\u001B[49m\u001B[43mchild\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mg\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mssa\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mgeneralizer\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mkeep\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 132\u001B[0m \u001B[38;5;66;03m# Update `sigma` with the recursion results.\u001B[39;00m\n\u001B[1;32m 133\u001B[0m \u001B[38;5;28;01mfor\u001B[39;00m k \u001B[38;5;129;01min\u001B[39;00m child_counts\u001B[38;5;241m.\u001B[39mkeys():\n", + " \u001B[0;31m[... skipping similar frames: _descend_counts at line 130 (2944 times)]\u001B[0m\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/resource_counting/bloq_counts.py:130\u001B[0m, in \u001B[0;36m_descend_counts\u001B[0;34m(parent, g, ssa, generalizer, keep)\u001B[0m\n\u001B[1;32m 127\u001B[0m g\u001B[38;5;241m.\u001B[39madd_edge(parent, child, n\u001B[38;5;241m=\u001B[39mn)\n\u001B[1;32m 129\u001B[0m \u001B[38;5;66;03m# Do the recursive step, which will continue to mutate `g`\u001B[39;00m\n\u001B[0;32m--> 130\u001B[0m child_counts \u001B[38;5;241m=\u001B[39m \u001B[43m_descend_counts\u001B[49m\u001B[43m(\u001B[49m\u001B[43mchild\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mg\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mssa\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mgeneralizer\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mkeep\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 132\u001B[0m \u001B[38;5;66;03m# Update `sigma` with the recursion results.\u001B[39;00m\n\u001B[1;32m 133\u001B[0m \u001B[38;5;28;01mfor\u001B[39;00m k \u001B[38;5;129;01min\u001B[39;00m child_counts\u001B[38;5;241m.\u001B[39mkeys():\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/resource_counting/bloq_counts.py:111\u001B[0m, in \u001B[0;36m_descend_counts\u001B[0;34m(parent, g, ssa, generalizer, keep)\u001B[0m\n\u001B[1;32m 109\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m {parent: \u001B[38;5;241m1\u001B[39m}\n\u001B[1;32m 110\u001B[0m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[0;32m--> 111\u001B[0m count_decomp \u001B[38;5;241m=\u001B[39m \u001B[43mparent\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mbloq_counts\u001B[49m\u001B[43m(\u001B[49m\u001B[43mssa\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 112\u001B[0m \u001B[38;5;28;01mexcept\u001B[39;00m \u001B[38;5;167;01mNotImplementedError\u001B[39;00m:\n\u001B[1;32m 113\u001B[0m \u001B[38;5;66;03m# Base case 2: Decomposition (or `bloq_counts`) is not implemented. This is left as a\u001B[39;00m\n\u001B[1;32m 114\u001B[0m \u001B[38;5;66;03m# leaf node.\u001B[39;00m\n\u001B[1;32m 115\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m {parent: \u001B[38;5;241m1\u001B[39m}\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/_infra/bloq.py:243\u001B[0m, in \u001B[0;36mBloq.bloq_counts\u001B[0;34m(self, ssa)\u001B[0m\n\u001B[1;32m 234\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mbloq_counts\u001B[39m(\u001B[38;5;28mself\u001B[39m, ssa: Optional[\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mSympySymbolAllocator\u001B[39m\u001B[38;5;124m'\u001B[39m] \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;01mNone\u001B[39;00m) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m Set[\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mBloqCountT\u001B[39m\u001B[38;5;124m'\u001B[39m]:\n\u001B[1;32m 235\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"Return a set of `(n, bloq)` tuples where bloq is used `n` times in the decomposition.\u001B[39;00m\n\u001B[1;32m 236\u001B[0m \n\u001B[1;32m 237\u001B[0m \u001B[38;5;124;03m By default, this method will use `self.decompose_bloq()` to count up bloqs.\u001B[39;00m\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 241\u001B[0m \u001B[38;5;124;03m sympy symbols (perhaps with the aid of the provided `SympySymbolAllocator`).\u001B[39;00m\n\u001B[1;32m 242\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[0;32m--> 243\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mdecompose_bloq\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\u001B[38;5;241m.\u001B[39mbloq_counts(ssa)\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/_infra/gate_with_registers.py:166\u001B[0m, in \u001B[0;36mGateWithRegisters.decompose_bloq\u001B[0;34m(self)\u001B[0m\n\u001B[1;32m 151\u001B[0m \u001B[38;5;250m\u001B[39m\u001B[38;5;124;03m\"\"\"Decompose this Bloq into its constituent parts contained in a CompositeBloq.\u001B[39;00m\n\u001B[1;32m 152\u001B[0m \n\u001B[1;32m 153\u001B[0m \u001B[38;5;124;03mBloq users can call this function to delve into the definition of a Bloq. If you're\u001B[39;00m\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 162\u001B[0m \u001B[38;5;124;03m `build_composite_bloq` returns `NotImplemented`.\u001B[39;00m\n\u001B[1;32m 163\u001B[0m \u001B[38;5;124;03m\"\"\"\u001B[39;00m\n\u001B[1;32m 164\u001B[0m \u001B[38;5;28;01mfrom\u001B[39;00m \u001B[38;5;21;01mqualtran\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01mcirq_interop\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01m_cirq_to_bloq\u001B[39;00m \u001B[38;5;28;01mimport\u001B[39;00m decompose_from_cirq_op\n\u001B[0;32m--> 166\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mdecompose_from_cirq_op\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[43m)\u001B[49m\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/cirq_interop/_cirq_to_bloq.py:391\u001B[0m, in \u001B[0;36mdecompose_from_cirq_op\u001B[0;34m(bloq)\u001B[0m\n\u001B[1;32m 389\u001B[0m qubit_manager \u001B[38;5;241m=\u001B[39m InteropQubitManager()\n\u001B[1;32m 390\u001B[0m in_quregs \u001B[38;5;241m=\u001B[39m get_cirq_quregs(bloq\u001B[38;5;241m.\u001B[39msignature, qubit_manager)\n\u001B[0;32m--> 391\u001B[0m cirq_op, out_quregs \u001B[38;5;241m=\u001B[39m \u001B[43mbloq\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mas_cirq_op\u001B[49m\u001B[43m(\u001B[49m\u001B[43mqubit_manager\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43min_quregs\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 392\u001B[0m \u001B[38;5;28;01mfrom\u001B[39;00m \u001B[38;5;21;01mqualtran\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01mcirq_interop\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01m_bloq_to_cirq\u001B[39;00m \u001B[38;5;28;01mimport\u001B[39;00m BloqAsCirqGate\n\u001B[1;32m 394\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m cirq_op \u001B[38;5;129;01mis\u001B[39;00m \u001B[38;5;28;01mNone\u001B[39;00m \u001B[38;5;129;01mor\u001B[39;00m (\n\u001B[1;32m 395\u001B[0m \u001B[38;5;28misinstance\u001B[39m(cirq_op, cirq\u001B[38;5;241m.\u001B[39mOperation) \u001B[38;5;129;01mand\u001B[39;00m \u001B[38;5;28misinstance\u001B[39m(cirq_op\u001B[38;5;241m.\u001B[39mgate, BloqAsCirqGate)\n\u001B[1;32m 396\u001B[0m ):\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/_infra/gate_with_registers.py:193\u001B[0m, in \u001B[0;36mGateWithRegisters.as_cirq_op\u001B[0;34m(self, qubit_manager, **cirq_quregs)\u001B[0m\n\u001B[1;32m 188\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mas_cirq_op\u001B[39m(\n\u001B[1;32m 189\u001B[0m \u001B[38;5;28mself\u001B[39m, qubit_manager: \u001B[38;5;124m'\u001B[39m\u001B[38;5;124mcirq.QubitManager\u001B[39m\u001B[38;5;124m'\u001B[39m, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mcirq_quregs: \u001B[38;5;124m'\u001B[39m\u001B[38;5;124mCirqQuregT\u001B[39m\u001B[38;5;124m'\u001B[39m\n\u001B[1;32m 190\u001B[0m ) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m Tuple[Union[\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mcirq.Operation\u001B[39m\u001B[38;5;124m'\u001B[39m, \u001B[38;5;28;01mNone\u001B[39;00m], Dict[\u001B[38;5;28mstr\u001B[39m, \u001B[38;5;124m'\u001B[39m\u001B[38;5;124mCirqQuregT\u001B[39m\u001B[38;5;124m'\u001B[39m]]:\n\u001B[1;32m 191\u001B[0m \u001B[38;5;28;01mfrom\u001B[39;00m \u001B[38;5;21;01mqualtran\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01mcirq_interop\u001B[39;00m\u001B[38;5;21;01m.\u001B[39;00m\u001B[38;5;21;01m_bloq_to_cirq\u001B[39;00m \u001B[38;5;28;01mimport\u001B[39;00m _construct_op_from_gate\n\u001B[0;32m--> 193\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43m_construct_op_from_gate\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m 194\u001B[0m \u001B[43m \u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[43m,\u001B[49m\n\u001B[1;32m 195\u001B[0m \u001B[43m \u001B[49m\u001B[43min_quregs\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43m{\u001B[49m\u001B[43mk\u001B[49m\u001B[43m:\u001B[49m\u001B[43m \u001B[49m\u001B[43mnp\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43marray\u001B[49m\u001B[43m(\u001B[49m\u001B[43mv\u001B[49m\u001B[43m)\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;28;43;01mfor\u001B[39;49;00m\u001B[43m \u001B[49m\u001B[43mk\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mv\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;129;43;01min\u001B[39;49;00m\u001B[43m \u001B[49m\u001B[43mcirq_quregs\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mitems\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\u001B[43m}\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 196\u001B[0m \u001B[43m \u001B[49m\u001B[43mqubit_manager\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mqubit_manager\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 197\u001B[0m \u001B[43m \u001B[49m\u001B[43m)\u001B[49m\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/cirq_interop/_bloq_to_cirq.py:295\u001B[0m, in \u001B[0;36m_construct_op_from_gate\u001B[0;34m(gate, in_quregs, qubit_manager)\u001B[0m\n\u001B[1;32m 292\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m reg\u001B[38;5;241m.\u001B[39mside \u001B[38;5;241m&\u001B[39m Side\u001B[38;5;241m.\u001B[39mRIGHT:\n\u001B[1;32m 293\u001B[0m \u001B[38;5;66;03m# Right registers should be part of the output.\u001B[39;00m\n\u001B[1;32m 294\u001B[0m out_quregs[reg\u001B[38;5;241m.\u001B[39mname] \u001B[38;5;241m=\u001B[39m all_quregs[reg\u001B[38;5;241m.\u001B[39mname]\n\u001B[0;32m--> 295\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mgate\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mon_registers\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43mall_quregs\u001B[49m\u001B[43m)\u001B[49m, out_quregs\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/_infra/gate_with_registers.py:233\u001B[0m, in \u001B[0;36mGateWithRegisters.on_registers\u001B[0;34m(self, **qubit_regs)\u001B[0m\n\u001B[1;32m 230\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mon_registers\u001B[39m(\n\u001B[1;32m 231\u001B[0m \u001B[38;5;28mself\u001B[39m, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mqubit_regs: Union[cirq\u001B[38;5;241m.\u001B[39mQid, Sequence[cirq\u001B[38;5;241m.\u001B[39mQid], NDArray[cirq\u001B[38;5;241m.\u001B[39mQid]]\n\u001B[1;32m 232\u001B[0m ) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m cirq\u001B[38;5;241m.\u001B[39mOperation:\n\u001B[0;32m--> 233\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mon\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43mmerge_qubits\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43msignature\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43mqubit_regs\u001B[49m\u001B[43m)\u001B[49m\u001B[43m)\u001B[49m\n", + "File \u001B[0;32m~/quantum/Cirq/cirq-core/cirq/ops/raw_types.py:233\u001B[0m, in \u001B[0;36mGate.on\u001B[0;34m(self, *qubits)\u001B[0m\n\u001B[1;32m 224\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mon\u001B[39m(\u001B[38;5;28mself\u001B[39m, \u001B[38;5;241m*\u001B[39mqubits: Qid) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m \u001B[38;5;124m'\u001B[39m\u001B[38;5;124mOperation\u001B[39m\u001B[38;5;124m'\u001B[39m:\n\u001B[1;32m 225\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"Returns an application of this gate to the given qubits.\u001B[39;00m\n\u001B[1;32m 226\u001B[0m \n\u001B[1;32m 227\u001B[0m \u001B[38;5;124;03m Args:\u001B[39;00m\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 231\u001B[0m \u001B[38;5;124;03m qubits.\u001B[39;00m\n\u001B[1;32m 232\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[0;32m--> 233\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mops\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mgate_operation\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mGateOperation\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;28;43mlist\u001B[39;49m\u001B[43m(\u001B[49m\u001B[43mqubits\u001B[49m\u001B[43m)\u001B[49m\u001B[43m)\u001B[49m\n", + "File \u001B[0;32m~/quantum/Cirq/cirq-core/cirq/ops/gate_operation.py:60\u001B[0m, in \u001B[0;36mGateOperation.__init__\u001B[0;34m(self, gate, qubits)\u001B[0m\n\u001B[1;32m 53\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21m__init__\u001B[39m(\u001B[38;5;28mself\u001B[39m, gate: \u001B[38;5;124m'\u001B[39m\u001B[38;5;124mcirq.Gate\u001B[39m\u001B[38;5;124m'\u001B[39m, qubits: Sequence[\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mcirq.Qid\u001B[39m\u001B[38;5;124m'\u001B[39m]) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m \u001B[38;5;28;01mNone\u001B[39;00m:\n\u001B[1;32m 54\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"Inits GateOperation.\u001B[39;00m\n\u001B[1;32m 55\u001B[0m \n\u001B[1;32m 56\u001B[0m \u001B[38;5;124;03m Args:\u001B[39;00m\n\u001B[1;32m 57\u001B[0m \u001B[38;5;124;03m gate: The gate to apply.\u001B[39;00m\n\u001B[1;32m 58\u001B[0m \u001B[38;5;124;03m qubits: The qubits to operate on.\u001B[39;00m\n\u001B[1;32m 59\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[0;32m---> 60\u001B[0m \u001B[43mgate\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mvalidate_args\u001B[49m\u001B[43m(\u001B[49m\u001B[43mqubits\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 61\u001B[0m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m_gate \u001B[38;5;241m=\u001B[39m gate\n\u001B[1;32m 62\u001B[0m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39m_qubits \u001B[38;5;241m=\u001B[39m \u001B[38;5;28mtuple\u001B[39m(qubits)\n", + "File \u001B[0;32m~/quantum/Cirq/cirq-core/cirq/ops/raw_types.py:222\u001B[0m, in \u001B[0;36mGate.validate_args\u001B[0;34m(self, qubits)\u001B[0m\n\u001B[1;32m 205\u001B[0m \u001B[38;5;250m\u001B[39m\u001B[38;5;124;03m\"\"\"Checks if this gate can be applied to the given qubits.\u001B[39;00m\n\u001B[1;32m 206\u001B[0m \n\u001B[1;32m 207\u001B[0m \u001B[38;5;124;03mBy default checks that:\u001B[39;00m\n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 219\u001B[0m \u001B[38;5;124;03m ValueError: The gate can't be applied to the qubits.\u001B[39;00m\n\u001B[1;32m 220\u001B[0m \u001B[38;5;124;03m\"\"\"\u001B[39;00m\n\u001B[1;32m 221\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m __cirq_debug__\u001B[38;5;241m.\u001B[39mget():\n\u001B[0;32m--> 222\u001B[0m \u001B[43m_validate_qid_shape\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mqubits\u001B[49m\u001B[43m)\u001B[49m\n", + "File \u001B[0;32m~/quantum/Cirq/cirq-core/cirq/ops/raw_types.py:1042\u001B[0m, in \u001B[0;36m_validate_qid_shape\u001B[0;34m(val, qubits)\u001B[0m\n\u001B[1;32m 1036\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21m_validate_qid_shape\u001B[39m(val: Any, qubits: Sequence[\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mcirq.Qid\u001B[39m\u001B[38;5;124m'\u001B[39m]) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m \u001B[38;5;28;01mNone\u001B[39;00m:\n\u001B[1;32m 1037\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"Helper function to validate qubits for gates and operations.\u001B[39;00m\n\u001B[1;32m 1038\u001B[0m \n\u001B[1;32m 1039\u001B[0m \u001B[38;5;124;03m Raises:\u001B[39;00m\n\u001B[1;32m 1040\u001B[0m \u001B[38;5;124;03m ValueError: The operation had qids that don't match it's qid shape.\u001B[39;00m\n\u001B[1;32m 1041\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[0;32m-> 1042\u001B[0m qid_shape \u001B[38;5;241m=\u001B[39m \u001B[43mprotocols\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mqid_shape\u001B[49m\u001B[43m(\u001B[49m\u001B[43mval\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 1043\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28mlen\u001B[39m(qubits) \u001B[38;5;241m!=\u001B[39m \u001B[38;5;28mlen\u001B[39m(qid_shape):\n\u001B[1;32m 1044\u001B[0m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mValueError\u001B[39;00m(\n\u001B[1;32m 1045\u001B[0m \u001B[38;5;124mf\u001B[39m\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mWrong number of qubits for <\u001B[39m\u001B[38;5;132;01m{\u001B[39;00mval\u001B[38;5;132;01m!r}\u001B[39;00m\u001B[38;5;124m>. \u001B[39m\u001B[38;5;124m'\u001B[39m\n\u001B[1;32m 1046\u001B[0m \u001B[38;5;124mf\u001B[39m\u001B[38;5;124m'\u001B[39m\u001B[38;5;124mExpected \u001B[39m\u001B[38;5;132;01m{\u001B[39;00m\u001B[38;5;28mlen\u001B[39m(qid_shape)\u001B[38;5;132;01m}\u001B[39;00m\u001B[38;5;124m qubits but got <\u001B[39m\u001B[38;5;132;01m{\u001B[39;00mqubits\u001B[38;5;132;01m!r}\u001B[39;00m\u001B[38;5;124m>.\u001B[39m\u001B[38;5;124m'\u001B[39m\n\u001B[1;32m 1047\u001B[0m )\n", + "File \u001B[0;32m~/quantum/Cirq/cirq-core/cirq/protocols/qid_shape_protocol.py:107\u001B[0m, in \u001B[0;36mqid_shape\u001B[0;34m(val, default)\u001B[0m\n\u001B[1;32m 84\u001B[0m \u001B[38;5;250m\u001B[39m\u001B[38;5;124;03m\"\"\"Returns a tuple describing the number of quantum levels of each\u001B[39;00m\n\u001B[1;32m 85\u001B[0m \u001B[38;5;124;03mqubit/qudit/qid `val` operates on.\u001B[39;00m\n\u001B[1;32m 86\u001B[0m \n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 104\u001B[0m \u001B[38;5;124;03m was specified.\u001B[39;00m\n\u001B[1;32m 105\u001B[0m \u001B[38;5;124;03m\"\"\"\u001B[39;00m\n\u001B[1;32m 106\u001B[0m getter \u001B[38;5;241m=\u001B[39m \u001B[38;5;28mgetattr\u001B[39m(val, \u001B[38;5;124m'\u001B[39m\u001B[38;5;124m_qid_shape_\u001B[39m\u001B[38;5;124m'\u001B[39m, \u001B[38;5;28;01mNone\u001B[39;00m)\n\u001B[0;32m--> 107\u001B[0m result \u001B[38;5;241m=\u001B[39m \u001B[38;5;28mNotImplemented\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m getter \u001B[38;5;129;01mis\u001B[39;00m \u001B[38;5;28;01mNone\u001B[39;00m \u001B[38;5;28;01melse\u001B[39;00m \u001B[43mgetter\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 108\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m result \u001B[38;5;129;01mis\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m \u001B[38;5;28mNotImplemented\u001B[39m:\n\u001B[1;32m 109\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m result\n", + "File \u001B[0;32m~/quantum/Cirq/cirq-core/cirq/value/abc_alt.py:137\u001B[0m, in \u001B[0;36mABCMetaImplementAnyOneOf.__new__..wrap_scope..impl_of_abstract\u001B[0;34m(*args, **kwargs)\u001B[0m\n\u001B[1;32m 136\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mimpl_of_abstract\u001B[39m(\u001B[38;5;241m*\u001B[39margs, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mkwargs):\n\u001B[0;32m--> 137\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mimpl\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43margs\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43mkwargs\u001B[49m\u001B[43m)\u001B[49m\n", + "File \u001B[0;32m~/quantum/Cirq/cirq-core/cirq/ops/raw_types.py:445\u001B[0m, in \u001B[0;36mGate._default_shape_from_num_qubits\u001B[0;34m(self)\u001B[0m\n\u001B[1;32m 444\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21m_default_shape_from_num_qubits\u001B[39m(\u001B[38;5;28mself\u001B[39m) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m Tuple[\u001B[38;5;28mint\u001B[39m, \u001B[38;5;241m.\u001B[39m\u001B[38;5;241m.\u001B[39m\u001B[38;5;241m.\u001B[39m]:\n\u001B[0;32m--> 445\u001B[0m num_qubits \u001B[38;5;241m=\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m_num_qubits_\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m 446\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m num_qubits \u001B[38;5;129;01mis\u001B[39;00m \u001B[38;5;28mNotImplemented\u001B[39m:\n\u001B[1;32m 447\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mNotImplemented\u001B[39m\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/_infra/gate_with_registers.py:212\u001B[0m, in \u001B[0;36mGateWithRegisters._num_qubits_\u001B[0;34m(self)\u001B[0m\n\u001B[1;32m 211\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21m_num_qubits_\u001B[39m(\u001B[38;5;28mself\u001B[39m) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m \u001B[38;5;28mint\u001B[39m:\n\u001B[0;32m--> 212\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mtotal_bits\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43msignature\u001B[49m\u001B[43m)\u001B[49m\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/_infra/gate_with_registers.py:35\u001B[0m, in \u001B[0;36mtotal_bits\u001B[0;34m(registers)\u001B[0m\n\u001B[1;32m 33\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mtotal_bits\u001B[39m(registers: Iterable[Register]) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m \u001B[38;5;28mint\u001B[39m:\n\u001B[1;32m 34\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"Sum of `reg.total_bits()` for each register `reg` in input `signature`.\"\"\"\u001B[39;00m\n\u001B[0;32m---> 35\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28msum\u001B[39m(reg\u001B[38;5;241m.\u001B[39mtotal_bits() \u001B[38;5;28;01mfor\u001B[39;00m reg \u001B[38;5;129;01min\u001B[39;00m registers)\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/_infra/gate_with_registers.py:35\u001B[0m, in \u001B[0;36m\u001B[0;34m(.0)\u001B[0m\n\u001B[1;32m 33\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mtotal_bits\u001B[39m(registers: Iterable[Register]) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m \u001B[38;5;28mint\u001B[39m:\n\u001B[1;32m 34\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"Sum of `reg.total_bits()` for each register `reg` in input `signature`.\"\"\"\u001B[39;00m\n\u001B[0;32m---> 35\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28msum\u001B[39m(\u001B[43mreg\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mtotal_bits\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m \u001B[38;5;28;01mfor\u001B[39;00m reg \u001B[38;5;129;01min\u001B[39;00m registers)\n", + "File \u001B[0;32m~/quantum/Qualtran/qualtran/_infra/registers.py:76\u001B[0m, in \u001B[0;36mRegister.total_bits\u001B[0;34m(self)\u001B[0m\n\u001B[1;32m 71\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mtotal_bits\u001B[39m(\u001B[38;5;28mself\u001B[39m) \u001B[38;5;241m-\u001B[39m\u001B[38;5;241m>\u001B[39m \u001B[38;5;28mint\u001B[39m:\n\u001B[1;32m 72\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"The total number of bits in this register.\u001B[39;00m\n\u001B[1;32m 73\u001B[0m \n\u001B[1;32m 74\u001B[0m \u001B[38;5;124;03m This is the product of bitsize and each of the dimensions in `shape`.\u001B[39;00m\n\u001B[1;32m 75\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[0;32m---> 76\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mbitsize \u001B[38;5;241m*\u001B[39m \u001B[38;5;28mint\u001B[39m(\u001B[43mnp\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mproduct\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mshape\u001B[49m\u001B[43m)\u001B[49m)\n", + "File \u001B[0;32m<__array_function__ internals>:200\u001B[0m, in \u001B[0;36mproduct\u001B[0;34m(*args, **kwargs)\u001B[0m\n", + "File \u001B[0;32m~/opt/anaconda3/envs/qualtran/lib/python3.11/site-packages/numpy/core/fromnumeric.py:3775\u001B[0m, in \u001B[0;36mproduct\u001B[0;34m(*args, **kwargs)\u001B[0m\n\u001B[1;32m 3766\u001B[0m \u001B[38;5;129m@array_function_dispatch\u001B[39m(_prod_dispatcher, verify\u001B[38;5;241m=\u001B[39m\u001B[38;5;28;01mFalse\u001B[39;00m)\n\u001B[1;32m 3767\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mproduct\u001B[39m(\u001B[38;5;241m*\u001B[39margs, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mkwargs):\n\u001B[1;32m 3768\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"\u001B[39;00m\n\u001B[1;32m 3769\u001B[0m \u001B[38;5;124;03m Return the product of array elements over a given axis.\u001B[39;00m\n\u001B[1;32m 3770\u001B[0m \n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 3773\u001B[0m \u001B[38;5;124;03m prod : equivalent function; see for details.\u001B[39;00m\n\u001B[1;32m 3774\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[0;32m-> 3775\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43mprod\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43margs\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[38;5;241;43m*\u001B[39;49m\u001B[43mkwargs\u001B[49m\u001B[43m)\u001B[49m\n", + "File \u001B[0;32m<__array_function__ internals>:200\u001B[0m, in \u001B[0;36mprod\u001B[0;34m(*args, **kwargs)\u001B[0m\n", + "File \u001B[0;32m~/opt/anaconda3/envs/qualtran/lib/python3.11/site-packages/numpy/core/fromnumeric.py:3076\u001B[0m, in \u001B[0;36mprod\u001B[0;34m(a, axis, dtype, out, keepdims, initial, where)\u001B[0m\n\u001B[1;32m 2955\u001B[0m \u001B[38;5;129m@array_function_dispatch\u001B[39m(_prod_dispatcher)\n\u001B[1;32m 2956\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21mprod\u001B[39m(a, axis\u001B[38;5;241m=\u001B[39m\u001B[38;5;28;01mNone\u001B[39;00m, dtype\u001B[38;5;241m=\u001B[39m\u001B[38;5;28;01mNone\u001B[39;00m, out\u001B[38;5;241m=\u001B[39m\u001B[38;5;28;01mNone\u001B[39;00m, keepdims\u001B[38;5;241m=\u001B[39mnp\u001B[38;5;241m.\u001B[39m_NoValue,\n\u001B[1;32m 2957\u001B[0m initial\u001B[38;5;241m=\u001B[39mnp\u001B[38;5;241m.\u001B[39m_NoValue, where\u001B[38;5;241m=\u001B[39mnp\u001B[38;5;241m.\u001B[39m_NoValue):\n\u001B[1;32m 2958\u001B[0m \u001B[38;5;250m \u001B[39m\u001B[38;5;124;03m\"\"\"\u001B[39;00m\n\u001B[1;32m 2959\u001B[0m \u001B[38;5;124;03m Return the product of array elements over a given axis.\u001B[39;00m\n\u001B[1;32m 2960\u001B[0m \n\u001B[0;32m (...)\u001B[0m\n\u001B[1;32m 3074\u001B[0m \u001B[38;5;124;03m 10\u001B[39;00m\n\u001B[1;32m 3075\u001B[0m \u001B[38;5;124;03m \"\"\"\u001B[39;00m\n\u001B[0;32m-> 3076\u001B[0m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[43m_wrapreduction\u001B[49m\u001B[43m(\u001B[49m\u001B[43ma\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mnp\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mmultiply\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[38;5;124;43mprod\u001B[39;49m\u001B[38;5;124;43m'\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43maxis\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mdtype\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mout\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m 3077\u001B[0m \u001B[43m \u001B[49m\u001B[43mkeepdims\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mkeepdims\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43minitial\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43minitial\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mwhere\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43mwhere\u001B[49m\u001B[43m)\u001B[49m\n", + "File \u001B[0;32m~/opt/anaconda3/envs/qualtran/lib/python3.11/site-packages/numpy/core/fromnumeric.py:70\u001B[0m, in \u001B[0;36m_wrapreduction\u001B[0;34m(obj, ufunc, method, axis, dtype, out, **kwargs)\u001B[0m\n\u001B[1;32m 69\u001B[0m \u001B[38;5;28;01mdef\u001B[39;00m \u001B[38;5;21m_wrapreduction\u001B[39m(obj, ufunc, method, axis, dtype, out, \u001B[38;5;241m*\u001B[39m\u001B[38;5;241m*\u001B[39mkwargs):\n\u001B[0;32m---> 70\u001B[0m passkwargs \u001B[38;5;241m=\u001B[39m {k: v \u001B[38;5;28;01mfor\u001B[39;00m k, v \u001B[38;5;129;01min\u001B[39;00m kwargs\u001B[38;5;241m.\u001B[39mitems()\n\u001B[1;32m 71\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m v \u001B[38;5;129;01mis\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m np\u001B[38;5;241m.\u001B[39m_NoValue}\n\u001B[1;32m 73\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28mtype\u001B[39m(obj) \u001B[38;5;129;01mis\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m mu\u001B[38;5;241m.\u001B[39mndarray:\n\u001B[1;32m 74\u001B[0m \u001B[38;5;28;01mtry\u001B[39;00m:\n", + "\u001B[0;31mRecursionError\u001B[0m: maximum recursion depth exceeded while calling a Python object" + ] + } + ], "source": [ "graph, sigma = get_bloq_counts_graph(bloq)\n", "GraphvizCounts(graph).get_svg()" @@ -279,9 +945,7 @@ "cell_type": "code", "execution_count": null, "id": "f93ce8ed", - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "from qualtran.bloqs.and_bloq import MultiAnd\n", @@ -354,9 +1018,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.11.4" } }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/qualtran/bloqs/and_bloq.py b/qualtran/bloqs/and_bloq.py index b0ab95f9c..d0c2216a3 100644 --- a/qualtran/bloqs/and_bloq.py +++ b/qualtran/bloqs/and_bloq.py @@ -16,22 +16,24 @@ from functools import cached_property from typing import Any, Dict, Optional, Set, Tuple +import cirq import numpy as np import quimb.tensor as qtn import sympy from attrs import field, frozen from numpy.typing import NDArray -from qualtran import Bloq, Register, Side, Signature, Soquet, SoquetT +from qualtran import Bloq, GateWithRegisters, Register, Side, Signature, Soquet, SoquetT from qualtran.bloqs.basic_gates import TGate from qualtran.bloqs.util_bloqs import ArbitraryClifford +from qualtran.cirq_interop.t_complexity_protocol import TComplexity from qualtran.drawing import Circle, directional_text_box, WireSymbol from qualtran.resource_counting import big_O, SympySymbolAllocator @frozen -class And(Bloq): - """A two-bit 'and' operation. +class And(GateWithRegisters): + """A two-bit 'and' operation optimized for T-count. Args: cv1: Whether the first bit is a positive control. @@ -90,7 +92,6 @@ def add_my_tensors( incoming: Dict[str, SoquetT], outgoing: Dict[str, SoquetT], ): - # Fill in our tensor using "and" logic. data = np.zeros((2, 2, 2, 2, 2), dtype=np.complex128) for c1, c2 in itertools.product((0, 1), repeat=2): @@ -127,9 +128,62 @@ def wire_symbol(self, soq: 'Soquet') -> 'WireSymbol': filled = bool(self.cv1 if c_idx == 0 else self.cv2) return Circle(filled) + def decompose_from_registers( + self, *, context: cirq.DecompositionContext, **quregs: NDArray[cirq.Qid] + ) -> cirq.OP_TREE: + """Decomposes a single `And` gate on 2 controls and 1 target in terms of Clifford+T gates. + + * And(cv).on(c1, c2, target) uses 4 T-gates and assumes target is in |0> state. + * And(cv, adjoint=True).on(c1, c2, target) uses measurement based un-computation + (0 T-gates) and will always leave the target in |0> state. + """ + (c1, c2), (target,) = (quregs['ctrl'].flatten(), quregs['target'].flatten()) + pre_post_ops = [cirq.X(q) for (q, v) in zip([c1, c2], [self.cv1, self.cv2]) if v == 0] + yield pre_post_ops + if self.adjoint: + yield cirq.H(target) + yield cirq.measure(target, key=f"{target}") + yield cirq.CZ(c1, c2).with_classical_controls(f"{target}") + yield cirq.reset(target) + else: + yield [cirq.H(target), cirq.T(target)] + yield [cirq.CNOT(c1, target), cirq.CNOT(c2, target)] + yield [cirq.CNOT(target, c1), cirq.CNOT(target, c2)] + yield [cirq.T(c1) ** -1, cirq.T(c2) ** -1, cirq.T(target)] + yield [cirq.CNOT(target, c1), cirq.CNOT(target, c2)] + yield [cirq.H(target), cirq.S(target)] + yield pre_post_ops + + def __pow__(self, power: int) -> "And": + if power == 1: + return self + if power == -1: + return And(self.cv1, self.cv2, adjoint=self.adjoint ^ True) + return NotImplemented # pragma: no cover + + def __str__(self) -> str: + suffix = "" if self.cv1 == self.cv2 == 1 else str((self.cv1, self.cv2)) + return f"And†{suffix}" if self.adjoint else f"And{suffix}" + + def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs) -> cirq.CircuitDiagramInfo: + controls = ["(0)", "@"] + target = "And†" if self.adjoint else "And" + wire_symbols = [controls[self.cv1], controls[self.cv2], target] + return cirq.CircuitDiagramInfo(wire_symbols=wire_symbols) + + def _has_unitary_(self) -> bool: + return not self.adjoint + + def _t_complexity_(self) -> TComplexity: + pre_post_cliffords = 2 - self.cv1 - self.cv2 # number of zeros in self.cv + if self.adjoint: + return TComplexity(clifford=4 + 2 * pre_post_cliffords) + else: + return TComplexity(t=4 * 1, clifford=9 + 2 * pre_post_cliffords) + @frozen -class MultiAnd(Bloq): +class MultiAnd(GateWithRegisters): """A many-bit (multi-control) 'and' operation. Args: @@ -160,11 +214,11 @@ def pretty_name(self) -> str: dag = '†' if self.adjoint else '' return f'And{dag}' - def decompose_bloq(self) -> 'CompositeBloq': - cbloq = super().decompose_bloq() - if self.adjoint: - raise NotImplementedError("Come back soon.") - return cbloq + # def decompose_bloq(self) -> 'CompositeBloq': + # cbloq = Bloq.decompose_bloq(self) + # if self.adjoint: + # raise NotImplementedError("Come back soon.") + # return cbloq def build_composite_bloq( self, bb: 'BloqBuilder', *, ctrl: NDArray[Soquet] @@ -205,3 +259,71 @@ def on_classical_vals(self, ctrl: NDArray[np.uint8]) -> Dict[str, NDArray[np.uin accumulate_and = np.bitwise_and.accumulate(np.equal(ctrl, self.cvs).astype(np.uint8)) junk, target = accumulate_and[1:-1], accumulate_and[-1] return {'ctrl': ctrl, 'junk': junk, 'target': target} + + def __pow__(self, power: int) -> "And": + if power == 1: + return self + if power == -1: + return And(self.cvs, adjoint=self.adjoint ^ True) + return NotImplemented # pragma: no cover + + def __str__(self) -> str: + suffix = "" if self.cvs == (1,) * len(self.cvs) else str(self.cvs) + return f"And†{suffix}" if self.adjoint else f"And{suffix}" + + def _circuit_diagram_info_(self, args: cirq.CircuitDiagramInfoArgs) -> cirq.CircuitDiagramInfo: + controls = ["(0)", "@"] + target = "And†" if self.adjoint else "And" + wire_symbols = [controls[c] for c in self.cvs] + wire_symbols += ["Anc"] * (len(self.cvs) - 2) + wire_symbols += [target] + return cirq.CircuitDiagramInfo(wire_symbols=wire_symbols) + + def _has_unitary_(self) -> bool: + return not self.adjoint + + def _decompose_via_tree( + self, + controls: NDArray[cirq.Qid], + control_values: Tuple[int, ...], + ancillas: NDArray[cirq.Qid], + target: cirq.Qid, + ) -> cirq.ops.op_tree.OpTree: + """Decomposes multi-controlled `And` in-terms of an `And` ladder of size #controls- 2.""" + + if len(controls) == 2: + yield And(*control_values, adjoint=self.adjoint).on(*controls, target) + return + new_controls = np.concatenate([ancillas[0:1], controls[2:]]) + new_control_values = (1, *control_values[2:]) + and_op = And(*control_values[:2], adjoint=self.adjoint).on(*controls[:2], ancillas[0]) + if self.adjoint: + yield from self._decompose_via_tree( + new_controls, new_control_values, ancillas[1:], target + ) + yield and_op + else: + yield and_op + yield from self._decompose_via_tree( + new_controls, new_control_values, ancillas[1:], target + ) + + def decompose_from_registers( + self, *, context: cirq.DecompositionContext, **quregs: NDArray[cirq.Qid] + ) -> cirq.OP_TREE: + control, ancilla, target = ( + quregs['ctrl'].flatten(), + quregs.get('junk', np.array([])).flatten(), + quregs['target'].flatten(), + ) + yield self._decompose_via_tree(control, self.cvs, ancilla, *target) + + def _t_complexity_(self) -> TComplexity: + pre_post_cliffords = len(self.cvs) - sum(self.cvs) # number of zeros in self.cv + num_single_and = len(self.cv) - 1 + if self.adjoint: + return TComplexity(clifford=4 * num_single_and + 2 * pre_post_cliffords) + else: + return TComplexity( + t=4 * num_single_and, clifford=9 * num_single_and + 2 * pre_post_cliffords + ) diff --git a/qualtran/bloqs/and_bloq_test.py b/qualtran/bloqs/and_bloq_test.py index 334eb59ee..8d44c3177 100644 --- a/qualtran/bloqs/and_bloq_test.py +++ b/qualtran/bloqs/and_bloq_test.py @@ -144,7 +144,7 @@ def test_multi_truth_table(): # Tensor simulation vec = cbloq.tensor_contract() should_be = np.all(ctrl_string == cvs) - *junk_is, res_i = np.where(vec.reshape((2,) * (n - 1))) + *junk_is, res_i = np.where(abs(vec.reshape((2,) * (n - 1))) > 1e-10) assert res_i == should_be, ctrl_string # Classical simulation diff --git a/qualtran/cirq_interop/_bloq_to_cirq.py b/qualtran/cirq_interop/_bloq_to_cirq.py index dd545901e..bc9c0f154 100644 --- a/qualtran/cirq_interop/_bloq_to_cirq.py +++ b/qualtran/cirq_interop/_bloq_to_cirq.py @@ -18,16 +18,25 @@ from typing import Callable, Dict, Iterable, List, Optional, Tuple import cirq -import cirq_ft import networkx as nx import numpy as np -from qualtran import Bloq, Connection, LeftDangle, Register, RightDangle, Side, Signature, Soquet +from qualtran import ( + Bloq, + Connection, + GateWithRegisters, + LeftDangle, + Register, + RightDangle, + Side, + Signature, + Soquet, +) from qualtran._infra.composite_bloq import _binst_to_cxns from qualtran.cirq_interop._cirq_to_bloq import _QReg, CirqQuregInT, CirqQuregT -class BloqAsCirqGate(cirq_ft.GateWithRegisters): +class BloqAsCirqGate(GateWithRegisters): """A shim for using bloqs in a Cirq circuit. Args: @@ -52,19 +61,9 @@ def bloq(self) -> Bloq: return self._bloq @cached_property - def signature(self) -> cirq_ft.Signature: + def signature(self) -> Signature: """`cirq_ft.GateWithRegisters` registers.""" - legacy_regs: List[cirq_ft.Register] = [] - for reg in self.bloq.signature: - legacy_regs.append( - cirq_ft.Register( - name=reg.name, - shape=reg.shape, - bitsize=reg.bitsize, - side=cirq_ft.infra.Side(reg.side.value), - ) - ) - return cirq_ft.Signature(legacy_regs) + return self.bloq.signature @classmethod def bloq_on( @@ -254,14 +253,12 @@ def _f_quregs(reg: Register) -> CirqQuregT: def _construct_op_from_gate( - gate: cirq_ft.GateWithRegisters, - in_quregs: Dict[str, 'CirqQuregT'], - qubit_manager: cirq.QubitManager, + gate: GateWithRegisters, in_quregs: Dict[str, 'CirqQuregT'], qubit_manager: cirq.QubitManager ) -> Tuple[cirq.Operation, Dict[str, 'CirqQuregT']]: """Allocates / Deallocates qubits for RIGHT / LEFT only registers to construct a Cirq operation Args: - gate: A `cirq_ft.GateWithRegisters` which specifies a signature. + gate: A `GateWithRegisters` which specifies a signature. in_quregs: Mapping from LEFT register names of `gate` and corresponding cirq qubits. qubit_manager: For allocating / deallocating qubits for RIGHT / LEFT only registers. @@ -271,25 +268,26 @@ def _construct_op_from_gate( """ all_quregs: Dict[str, 'CirqQuregT'] = {} out_quregs: Dict[str, 'CirqQuregT'] = {} + # def _cmp() for reg in gate.signature: full_shape = reg.shape + (reg.bitsize,) - if reg.side & cirq_ft.infra.Side.LEFT: + if Side(reg.side.value) & Side.LEFT: if reg.name not in in_quregs or in_quregs[reg.name].shape != full_shape: # Left registers should exist as input to `as_cirq_op`. raise ValueError(f'Compatible {reg=} must exist in {in_quregs=}') all_quregs[reg.name] = in_quregs[reg.name] - if reg.side == cirq_ft.infra.Side.RIGHT: + if Side(reg.side.value) == Side.RIGHT: # Right only registers will get allocated as part of `as_cirq_op`. if reg.name in in_quregs: raise ValueError(f"RIGHT register {reg=} shouldn't exist in {in_quregs=}.") all_quregs[reg.name] = np.array(qubit_manager.qalloc(reg.total_bits())).reshape( full_shape ) - if reg.side == cirq_ft.infra.Side.LEFT: + if Side(reg.side.value) == Side.LEFT: # LEFT only registers should be de-allocated and not be part of output. qubit_manager.qfree(in_quregs[reg.name].flatten()) - if reg.side & cirq_ft.infra.Side.RIGHT: + if Side(reg.side.value) & Side.RIGHT: # Right registers should be part of the output. out_quregs[reg.name] = all_quregs[reg.name] return gate.on_registers(**all_quregs), out_quregs diff --git a/qualtran/cirq_interop/_cirq_to_bloq.py b/qualtran/cirq_interop/_cirq_to_bloq.py index 79c6cd417..b71c03aaa 100644 --- a/qualtran/cirq_interop/_cirq_to_bloq.py +++ b/qualtran/cirq_interop/_cirq_to_bloq.py @@ -19,14 +19,15 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, TYPE_CHECKING, Union import cirq -import cirq_ft import numpy as np import quimb.tensor as qtn from attrs import field, frozen from numpy.typing import NDArray from qualtran import Bloq, BloqBuilder, CompositeBloq, Register, Side, Signature, Soquet, SoquetT +from qualtran._infra.gate_with_registers import split_qubits from qualtran.cirq_interop._interop_qubit_manager import InteropQubitManager +from qualtran.cirq_interop.t_complexity_protocol import t_complexity, TComplexity if TYPE_CHECKING: from qualtran.drawing import WireSymbol @@ -56,33 +57,21 @@ def short_name(self) -> str: @cached_property def signature(self) -> 'Signature': - return Signature( - [ - Register(reg.name, bitsize=reg.bitsize, shape=reg.shape, side=Side(reg.side.value)) - for reg in self.cirq_registers - ] - ) + if isinstance(self.gate, Bloq): + return self.gate.signature + import cirq_ft - @cached_property - def cirq_registers(self) -> cirq_ft.Signature: if isinstance(self.gate, cirq_ft.GateWithRegisters): - return self.gate.signature - else: - return cirq_ft.Signature( - [cirq_ft.Register('qubits', shape=(cirq.num_qubits(self.gate),), bitsize=1)] + return Signature( + [ + Register(reg.name, reg.bitsize, reg.shape, Side(reg.side.value)) + for reg in self.gate.signature + ] ) + return Signature([Register('qubits', shape=cirq.num_qubits(self.gate), bitsize=1)]) def decompose_bloq(self) -> 'CompositeBloq': - qubit_manager = InteropQubitManager() - in_quregs = get_cirq_quregs(self.signature, qubit_manager) - cirq_op, out_quregs = self.as_cirq_op(qubit_manager, **in_quregs) - context = cirq.DecompositionContext(qubit_manager=qubit_manager) - decomposed_optree = cirq.decompose_once(cirq_op, context=context, default=None) - if decomposed_optree is None: - raise NotImplementedError(f"{self} does not support decomposition.") - return cirq_optree_to_cbloq( - decomposed_optree, signature=self.signature, in_quregs=in_quregs, out_quregs=out_quregs - ) + return decompose_from_cirq_op(self, decompose_once=True) def add_my_tensors( self, @@ -92,65 +81,25 @@ def add_my_tensors( incoming: Dict[str, 'SoquetT'], outgoing: Dict[str, 'SoquetT'], ): - if not cirq.has_unitary(self.gate): - raise NotImplementedError( - f"CirqGateAsBloq.add_my_tensors is currently supported only for unitary gates. " - f"Found {self.gate}." - ) - unitary_shape = [] - reg_to_idx = defaultdict(list) - for reg in self.cirq_registers: - start = len(unitary_shape) - for i in range(int(np.prod(reg.shape))): - reg_to_idx[reg.name].append(start + i) - unitary_shape.append(2**reg.bitsize) - - unitary_shape = (*unitary_shape, *unitary_shape) - unitary = cirq.unitary(self.gate).reshape(unitary_shape) - idx: List[Union[int, slice]] = [slice(x) for x in unitary_shape] - n = len(unitary_shape) // 2 - for reg in self.signature: - if reg.side == Side.LEFT: - for i in reg_to_idx[reg.name]: - # LEFT register ends, extract right subspace that's equivalent to 0. - idx[i] = 0 - if reg.side == Side.RIGHT: - for i in reg_to_idx[reg.name]: - # Right register begins, extract the left subspace that's equivalent to 0. - idx[i + n] = 0 - unitary = unitary[tuple(idx)] - new_shape = tuple( - [ - *itertools.chain.from_iterable( - (2**reg.bitsize,) * int(np.prod(reg.shape)) - for reg in [*self.signature.rights(), *self.signature.lefts()] - ) - ] - ) - assert unitary.shape == new_shape - incoming_list = [ - *itertools.chain.from_iterable( - [np.array(incoming[reg.name]).flatten() for reg in self.signature.lefts()] - ) - ] - outgoing_list = [ - *itertools.chain.from_iterable( - [np.array(outgoing[reg.name]).flatten() for reg in self.signature.rights()] - ) - ] - - tn.add( - qtn.Tensor( - data=unitary, inds=outgoing_list + incoming_list, tags=[self.short_name(), tag] - ) + _add_my_tensors_from_gate( + self.gate, + self.signature, + self.short_name(), + tn=tn, + tag=tag, + incoming=incoming, + outgoing=outgoing, ) def as_cirq_op( self, qubit_manager: 'cirq.QubitManager', **cirq_quregs: 'CirqQuregT' ) -> Tuple['cirq.Operation', Dict[str, 'CirqQuregT']]: + import cirq_ft + + from qualtran import GateWithRegisters from qualtran.cirq_interop._bloq_to_cirq import _construct_op_from_gate - if not isinstance(self.gate, cirq_ft.GateWithRegisters): + if not isinstance(self.gate, (cirq_ft.GateWithRegisters, GateWithRegisters)): return self.gate.on(*cirq_quregs['qubits'].flatten()), cirq_quregs return _construct_op_from_gate( self.gate, @@ -158,21 +107,84 @@ def as_cirq_op( qubit_manager=qubit_manager, ) - def t_complexity(self) -> 'cirq_ft.TComplexity': - return cirq_ft.t_complexity(self.gate) + def t_complexity(self) -> 'TComplexity': + return t_complexity(self.gate) def wire_symbol(self, soq: 'Soquet') -> 'WireSymbol': - from qualtran.drawing import directional_text_box + return _wire_symbol_from_gate(self.gate, self.signature, soq) + + +def _wire_symbol_from_gate(gate: cirq.Gate, signature: Signature, soq: 'Soquet') -> 'WireSymbol': + from qualtran.drawing import directional_text_box - wire_symbols = cirq.circuit_diagram_info(self.gate).wire_symbols - begin = 0 - symbol: str = soq.pretty() - for reg in self.signature: - finish = begin + int(np.prod(reg.shape)) - if reg == soq.reg: - symbol = np.array(wire_symbols[begin:finish]).reshape(reg.shape)[soq.idx] - begin = finish - return directional_text_box(text=symbol, side=soq.reg.side) + wire_symbols = cirq.circuit_diagram_info(gate).wire_symbols + begin = 0 + symbol: str = soq.pretty() + for reg in signature: + finish = begin + int(np.prod(reg.shape)) + if reg == soq.reg: + symbol = np.array(wire_symbols[begin:finish]).reshape(reg.shape)[soq.idx] + begin = finish + return directional_text_box(text=symbol, side=soq.reg.side) + + +def _add_my_tensors_from_gate( + gate: cirq.Gate, + signature: Signature, + short_name: str, + tn: qtn.TensorNetwork, + tag: Any, + *, + incoming: Dict[str, 'SoquetT'], + outgoing: Dict[str, 'SoquetT'], +): + if not cirq.has_unitary(gate): + raise NotImplementedError( + f"CirqGateAsBloq.add_my_tensors is currently supported only for unitary gates. " + f"Found {gate}." + ) + unitary_shape = [] + reg_to_idx = defaultdict(list) + for reg in signature: + start = len(unitary_shape) + for i in range(int(np.prod(reg.shape))): + reg_to_idx[reg.name].append(start + i) + unitary_shape.append(2**reg.bitsize) + + unitary_shape = (*unitary_shape, *unitary_shape) + unitary = cirq.unitary(gate).reshape(unitary_shape) + idx: List[Union[int, slice]] = [slice(x) for x in unitary_shape] + n = len(unitary_shape) // 2 + for reg in signature: + if reg.side == Side.LEFT: + for i in reg_to_idx[reg.name]: + # LEFT register ends, extract right subspace that's equivalent to 0. + idx[i] = 0 + if reg.side == Side.RIGHT: + for i in reg_to_idx[reg.name]: + # Right register begins, extract the left subspace that's equivalent to 0. + idx[i + n] = 0 + unitary = unitary[tuple(idx)] + new_shape = tuple( + [ + *itertools.chain.from_iterable( + (2**reg.bitsize,) * int(np.prod(reg.shape)) + for reg in [*signature.rights(), *signature.lefts()] + ) + ] + ) + assert unitary.shape == new_shape + incoming_list = [ + *itertools.chain.from_iterable( + [np.array(incoming[reg.name]).flatten() for reg in signature.lefts()] + ) + ] + outgoing_list = [ + *itertools.chain.from_iterable( + [np.array(outgoing[reg.name]).flatten() for reg in signature.rights()] + ) + ] + tn.add(qtn.Tensor(data=unitary, inds=outgoing_list + incoming_list, tags=[short_name, tag])) @frozen @@ -309,11 +321,11 @@ def cirq_optree_to_cbloq( if op.gate is None: raise ValueError(f"Only gate operations are supported, not {op}.") - bloq = CirqGateAsBloq(op.gate) + bloq = op.gate if isinstance(op.gate, Bloq) else CirqGateAsBloq(op.gate) # 3.1 Find input / output registers. all_op_quregs: Dict[str, NDArray[_QReg]] = { k: np.apply_along_axis(_QReg, -1, v) - for k, v in cirq_ft.infra.split_qubits(bloq.cirq_registers, op.qubits).items() + for k, v in split_qubits(bloq.signature, op.qubits).items() } in_op_quregs: Dict[str, NDArray[_QReg]] = { reg.name: all_op_quregs[reg.name] for reg in bloq.signature.lefts() @@ -350,7 +362,7 @@ def cirq_optree_to_cbloq( return bb.finalize(**final_soqs_dict) -def decompose_from_cirq_op(bloq: 'Bloq') -> 'CompositeBloq': +def decompose_from_cirq_op(bloq: 'Bloq', *, decompose_once: bool = False) -> 'CompositeBloq': """Returns a CompositeBloq constructed using Cirq operations obtained via `bloq.as_cirq_op`. This method first checks whether `bloq.signature` is parameterized. If yes, it raises a @@ -368,13 +380,14 @@ def decompose_from_cirq_op(bloq: 'Bloq') -> 'CompositeBloq': qubit_manager = InteropQubitManager() in_quregs = get_cirq_quregs(bloq.signature, qubit_manager) cirq_op, out_quregs = bloq.as_cirq_op(qubit_manager, **in_quregs) - from qualtran.cirq_interop._bloq_to_cirq import BloqAsCirqGate + context = cirq.DecompositionContext(qubit_manager=qubit_manager) + decomposed_optree = ( + cirq.decompose_once(cirq_op, context=context, default=None) if decompose_once else cirq_op + ) - if cirq_op is None or ( - isinstance(cirq_op, cirq.Operation) and isinstance(cirq_op.gate, BloqAsCirqGate) - ): + if decomposed_optree is None: raise NotImplementedError(f"{bloq} does not support decomposition.") return cirq_optree_to_cbloq( - cirq_op, signature=bloq.signature, in_quregs=in_quregs, out_quregs=out_quregs + decomposed_optree, signature=bloq.signature, in_quregs=in_quregs, out_quregs=out_quregs ) diff --git a/qualtran/cirq_interop/cirq_interop.ipynb b/qualtran/cirq_interop/cirq_interop.ipynb index dff67c4aa..56039948c 100644 --- a/qualtran/cirq_interop/cirq_interop.ipynb +++ b/qualtran/cirq_interop/cirq_interop.ipynb @@ -203,7 +203,7 @@ "bloq = CirqGateAsBloq(select.gate)\n", "fig, ax = draw_musical_score(get_musical_score_data(bloq.decompose_bloq()))\n", "fig.set_size_inches(30, 12)\n", - "assert bloq.t_complexity() == cirq_ft.t_complexity(select.gate)" + "assert bloq.t_complexity().t == cirq_ft.t_complexity(select.gate).t" ] }, { @@ -217,7 +217,7 @@ "bloq = CirqGateAsBloq(prepare.gate)\n", "fig, ax = draw_musical_score(get_musical_score_data(bloq.decompose_bloq()))\n", "fig.set_size_inches(30, 12)\n", - "assert bloq.t_complexity() == cirq_ft.t_complexity(prepare.gate)" + "assert bloq.t_complexity().t == cirq_ft.t_complexity(prepare.gate).t" ] }, { @@ -637,4 +637,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/qualtran/cirq_interop/decompose_protocol.py b/qualtran/cirq_interop/decompose_protocol.py new file mode 100644 index 000000000..9f0acd23f --- /dev/null +++ b/qualtran/cirq_interop/decompose_protocol.py @@ -0,0 +1,98 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, FrozenSet, Sequence + +import cirq +from cirq.protocols.decompose_protocol import DecomposeResult + +_FREDKIN_GATESET = cirq.Gateset(cirq.FREDKIN, unroll_circuit_op=False) + + +def _fredkin(qubits: Sequence[cirq.Qid], context: cirq.DecompositionContext) -> cirq.OP_TREE: + """Decomposition with 7 T and 10 clifford operations from https://arxiv.org/abs/1308.4134""" + c, t1, t2 = qubits + yield [cirq.CNOT(t2, t1)] + yield [cirq.CNOT(c, t1), cirq.H(t2)] + yield [cirq.T(c), cirq.T(t1) ** -1, cirq.T(t2)] + yield [cirq.CNOT(t2, t1)] + yield [cirq.CNOT(c, t2), cirq.T(t1)] + yield [cirq.CNOT(c, t1), cirq.T(t2) ** -1] + yield [cirq.T(t1) ** -1, cirq.CNOT(c, t2)] + yield [cirq.CNOT(t2, t1)] + yield [cirq.T(t1), cirq.H(t2)] + yield [cirq.CNOT(t2, t1)] + + +def _try_decompose_from_known_decompositions( + val: Any, context: cirq.DecompositionContext +) -> DecomposeResult: + """Returns a flattened decomposition of the object into operations, if possible. + + Args: + val: The object to decompose. + context: Decomposition context storing common configurable options for `cirq.decompose`. + + Returns: + A flattened decomposition of `val` if it's a gate or operation with a known decomposition. + """ + if not isinstance(val, (cirq.Gate, cirq.Operation)): + return None + qubits = cirq.LineQid.for_gate(val) if isinstance(val, cirq.Gate) else val.qubits + known_decompositions = [(_FREDKIN_GATESET, _fredkin)] + + classical_controls: FrozenSet[cirq.Condition] = frozenset() + if isinstance(val, cirq.ClassicallyControlledOperation): + classical_controls = val.classical_controls + val = val.without_classical_controls() + + decomposition = None + for gateset, decomposer in known_decompositions: + if val in gateset: + decomposition = cirq.flatten_to_ops(decomposer(qubits, context)) + break + return ( + tuple(op.with_classical_controls(*classical_controls) for op in decomposition) + if decomposition + else None + ) + + +def _decompose_once_considering_known_decomposition(val: Any) -> DecomposeResult: + """Decomposes a value into operations, if possible. + + Args: + val: The value to decompose into operations. + + Returns: + A tuple of operations if decomposition succeeds. + """ + import uuid + + context = cirq.DecompositionContext( + qubit_manager=cirq.GreedyQubitManager(prefix=f'_{uuid.uuid4()}', maximize_reuse=True) + ) + + decomposed = _try_decompose_from_known_decompositions(val, context) + if decomposed is not None: + return decomposed + + if isinstance(val, cirq.Gate): + decomposed = cirq.decompose_once_with_qubits( + val, cirq.LineQid.for_gate(val), context=context, flatten=False, default=None + ) + else: + decomposed = cirq.decompose_once(val, context=context, flatten=False, default=None) + + return [*cirq.flatten_to_ops(decomposed)] if decomposed is not None else None diff --git a/qualtran/cirq_interop/decompose_protocol_test.py b/qualtran/cirq_interop/decompose_protocol_test.py new file mode 100644 index 000000000..43770ab7a --- /dev/null +++ b/qualtran/cirq_interop/decompose_protocol_test.py @@ -0,0 +1,58 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cirq +import numpy as np +import pytest +from cirq_ft.infra.decompose_protocol import ( + _decompose_once_considering_known_decomposition, + _fredkin, + _try_decompose_from_known_decompositions, +) + + +def test_fredkin_unitary(): + c, t1, t2 = cirq.LineQid.for_gate(cirq.FREDKIN) + context = cirq.DecompositionContext(cirq.ops.SimpleQubitManager()) + np.testing.assert_allclose( + cirq.Circuit(_fredkin((c, t1, t2), context)).unitary(), + cirq.unitary(cirq.FREDKIN(c, t1, t2)), + atol=1e-8, + ) + + +@pytest.mark.parametrize('gate', [cirq.FREDKIN, cirq.FREDKIN**-1]) +def test_decompose_fredkin(gate): + c, t1, t2 = cirq.LineQid.for_gate(cirq.FREDKIN) + op = cirq.FREDKIN(c, t1, t2) + context = cirq.DecompositionContext(cirq.ops.SimpleQubitManager()) + want = tuple(cirq.flatten_op_tree(_fredkin((c, t1, t2), context))) + assert want == _try_decompose_from_known_decompositions(op, context) + + op = cirq.FREDKIN(c, t1, t2).with_classical_controls('key') + classical_controls = op.classical_controls + want = tuple( + o.with_classical_controls(*classical_controls) + for o in cirq.flatten_op_tree(_fredkin((c, t1, t2), context)) + ) + assert want == _try_decompose_from_known_decompositions(op, context) + + +def test_known_decomposition_empty_unitary(): + class DecomposeEmptyList(cirq.testing.SingleQubitGate): + def _decompose_(self, _): + return [] + + gate = DecomposeEmptyList() + assert _decompose_once_considering_known_decomposition(gate) == [] diff --git a/qualtran/cirq_interop/t_complexity.ipynb b/qualtran/cirq_interop/t_complexity.ipynb new file mode 100644 index 000000000..3697188b9 --- /dev/null +++ b/qualtran/cirq_interop/t_complexity.ipynb @@ -0,0 +1,174 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "51db731e", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright 2023 The Cirq Developers\n", + "#\n", + "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "# you may not use this file except in compliance with the License.\n", + "# You may obtain a copy of the License at\n", + "#\n", + "# https://www.apache.org/licenses/LICENSE-2.0\n", + "#\n", + "# Unless required by applicable law or agreed to in writing, software\n", + "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "# See the License for the specific language governing permissions and\n", + "# limitations under the License." + ] + }, + { + "cell_type": "markdown", + "id": "33438ed0", + "metadata": {}, + "source": [ + "# T Complexity" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abed0743", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "from cirq_ft import And, t_complexity, infra" + ] + }, + { + "cell_type": "markdown", + "id": "29f4c77d", + "metadata": {}, + "source": [ + "## Two Qubit And Gate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "635a411e", + "metadata": {}, + "outputs": [], + "source": [ + "# And of two qubits\n", + "gate = And() # create an And gate\n", + "# create an operation\n", + "operation = gate.on_registers(**infra.get_named_qubits(gate.signature))\n", + "# this operation doesn't directly support TComplexity but it's decomposable and its components are simple.\n", + "print(t_complexity(operation))" + ] + }, + { + "cell_type": "markdown", + "id": "88cfc5f4", + "metadata": {}, + "source": [ + "## Adjoint of two qubit And gate" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c0301d9", + "metadata": {}, + "outputs": [], + "source": [ + "gate = And() ** -1 # adjoint of And\n", + "operation = gate.on_registers(**infra.get_named_qubits(gate.signature))\n", + "# the deomposition is H, measure, CZ, and Reset\n", + "print(t_complexity(operation))" + ] + }, + { + "cell_type": "markdown", + "id": "8e585436", + "metadata": {}, + "source": [ + "## And gate on n qubits" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a207ea1a", + "metadata": {}, + "outputs": [], + "source": [ + "n = 5\n", + "gate = And((1, )*n)\n", + "operation = gate.on_registers(**infra.get_named_qubits(gate.signature))\n", + "print(t_complexity(operation))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "394032a5", + "metadata": {}, + "outputs": [], + "source": [ + "def Generate(n_max: int = 10):\n", + " \"\"\"Returns the #T when the number of qubits is between 2 and n_max inclusive\"\"\"\n", + " n_controls = []\n", + " t_count = []\n", + " for n in range(2, n_max + 2):\n", + " n_controls.append(n)\n", + " gate = And(cv=(1, )*n)\n", + " op = gate.on_registers(**infra.get_named_qubits(gate.signature))\n", + " c = t_complexity(op)\n", + " t_count.append(c.t)\n", + " return n_controls, t_count" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "919ca418", + "metadata": {}, + "outputs": [], + "source": [ + "n_controls, t_count = Generate()\n", + "plt.plot(n_controls, t_count, label='T')\n", + "plt.ylabel('count')\n", + "plt.xlabel('number of qubits')\n", + "plt.title('And gate')\n", + "plt.legend()\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16" + }, + "vscode": { + "interpreter": { + "hash": "1882f3b63550a2f9350e6532bf63174910df57e92f62a2be07440f9e606398c8" + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/qualtran/cirq_interop/t_complexity_protocol.py b/qualtran/cirq_interop/t_complexity_protocol.py new file mode 100644 index 000000000..77a262f1a --- /dev/null +++ b/qualtran/cirq_interop/t_complexity_protocol.py @@ -0,0 +1,195 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Callable, Hashable, Iterable, Optional, overload, Union + +import attr +import cachetools +import cirq +from typing_extensions import Literal, Protocol + +from qualtran.cirq_interop.decompose_protocol import _decompose_once_considering_known_decomposition + +_T_GATESET = cirq.Gateset(cirq.T, cirq.T**-1, unroll_circuit_op=False) + + +@attr.frozen +class TComplexity: + """Dataclass storing counts of logical T-gates, Clifford gates and single qubit rotations.""" + + t: int = 0 + clifford: int = 0 + rotations: int = 0 + + def __add__(self, other: 'TComplexity') -> 'TComplexity': + return TComplexity( + self.t + other.t, self.clifford + other.clifford, self.rotations + other.rotations + ) + + def __mul__(self, other: int) -> 'TComplexity': + return TComplexity(self.t * other, self.clifford * other, self.rotations * other) + + def __rmul__(self, other: int) -> 'TComplexity': + return self.__mul__(other) + + def __str__(self) -> str: + return ( + f'T-count: {self.t:g}\n' + f'Rotations: {self.rotations:g}\n' + f'Cliffords: {self.clifford:g}\n' + ) + + +class SupportsTComplexity(Protocol): + """An object whose TComplexity can be computed. + + An object whose TComplexity can be computed either implements the `_t_complexity_` function + or is of a type that SupportsDecompose. + """ + + def _t_complexity_(self) -> TComplexity: + """Returns the TComplexity.""" + + +def _has_t_complexity(stc: Any, fail_quietly: bool) -> Optional[TComplexity]: + """Returns TComplexity of stc by calling `stc._t_complexity_()` method, if it exists.""" + estimator = getattr(stc, '_t_complexity_', None) + if estimator is not None: + result = estimator() + if result is not NotImplemented: + return result + if isinstance(stc, cirq.Operation) and stc.gate is not None: + return _has_t_complexity(stc.gate, fail_quietly) + return None + + +def _is_clifford_or_t(stc: Any, fail_quietly: bool) -> Optional[TComplexity]: + """Attempts to infer the type of a gate/operation as one of clifford, T or Rotation.""" + if not isinstance(stc, (cirq.Gate, cirq.Operation)): + return None + + if isinstance(stc, cirq.ClassicallyControlledOperation): + stc = stc.without_classical_controls() + + if cirq.has_stabilizer_effect(stc): + # Clifford operation. + return TComplexity(clifford=1) + + if stc in _T_GATESET: + # T-gate. + return TComplexity(t=1) # T gate + + if cirq.num_qubits(stc) == 1 and cirq.has_unitary(stc): + # Single qubit rotation operation. + return TComplexity(rotations=1) + return None + + +def _is_iterable(it: Any, fail_quietly: bool) -> Optional[TComplexity]: + if not isinstance(it, Iterable): + return None + t = TComplexity() + for v in it: + r = t_complexity(v, fail_quietly=fail_quietly) + if r is None: + return None + t = t + r + return t + + +def _from_decomposition(stc: Any, fail_quietly: bool) -> Optional[TComplexity]: + # Decompose the object and recursively compute the complexity. + decomposition = _decompose_once_considering_known_decomposition(stc) + if decomposition is None: + return None + return _is_iterable(decomposition, fail_quietly=fail_quietly) + + +def _get_hash(val: Any, fail_quietly: bool = False): + """Returns hash keys for caching a cirq.Operation and cirq.Gate. + + The hash of a cirq.Operation changes depending on its qubits, tags, + classical controls, and other properties it has, none of these properties + affect the TComplexity. + For gates and gate backed operations we intend to compute the hash of the + gate which is a property of the Gate. + + Args: + val: object to compute its hash. + + Returns: + - `val.gate` if `val` is a `cirq.Operation` which has an underlying `val.gate`. + - `val` otherwise + """ + if isinstance(val, cirq.Operation) and val.gate is not None: + val = val.gate + return val + + +def _t_complexity_from_strategies( + stc: Any, fail_quietly: bool, strategies: Iterable[Callable[[Any, bool], Optional[TComplexity]]] +): + ret = None + for strategy in strategies: + ret = strategy(stc, fail_quietly) + if ret is not None: + break + return ret + + +@cachetools.cached(cachetools.LRUCache(128), key=_get_hash, info=True) +def _t_complexity_for_gate_or_op( + gate_or_op: Union[cirq.Gate, cirq.Operation], fail_quietly: bool +) -> Optional[TComplexity]: + strategies = [_has_t_complexity, _is_clifford_or_t, _from_decomposition] + return _t_complexity_from_strategies(gate_or_op, fail_quietly, strategies) + + +@overload +def t_complexity(stc: Any, fail_quietly: Literal[False] = False) -> TComplexity: + ... + + +@overload +def t_complexity(stc: Any, fail_quietly: bool) -> Optional[TComplexity]: + ... + + +def t_complexity(stc: Any, fail_quietly: bool = False) -> Optional[TComplexity]: + """Returns the TComplexity. + + Args: + stc: an object to compute its TComplexity. + fail_quietly: bool whether to return None on failure or raise an error. + + Returns: + The TComplexity of the given object or None on failure (and fail_quietly=True). + + Raises: + TypeError: if fail_quietly=False and the methods fails to compute TComplexity. + """ + if isinstance(stc, (cirq.Gate, cirq.Operation)) and isinstance(stc, Hashable): + ret = _t_complexity_for_gate_or_op(stc, fail_quietly) + else: + strategies = [_has_t_complexity, _is_clifford_or_t, _from_decomposition, _is_iterable] + ret = _t_complexity_from_strategies(stc, fail_quietly, strategies) + + if ret is None and not fail_quietly: + raise TypeError("couldn't compute TComplexity of:\n" f"type: {type(stc)}\n" f"value: {stc}") + return ret + + +t_complexity.cache_clear = _t_complexity_for_gate_or_op.cache_clear # type: ignore[attr-defined] +t_complexity.cache_info = _t_complexity_for_gate_or_op.cache_info # type: ignore[attr-defined] +t_complexity.cache = _t_complexity_for_gate_or_op.cache # type: ignore[attr-defined] diff --git a/qualtran/cirq_interop/t_complexity_protocol_test.py b/qualtran/cirq_interop/t_complexity_protocol_test.py new file mode 100644 index 000000000..3bfee03fc --- /dev/null +++ b/qualtran/cirq_interop/t_complexity_protocol_test.py @@ -0,0 +1,205 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import cirq +import pytest + +from qualtran import GateWithRegisters, Signature +from qualtran._infra.gate_with_registers import get_named_qubits +from qualtran.bloqs.and_bloq import And +from qualtran.cirq_interop.t_complexity_protocol import t_complexity, TComplexity +from qualtran.cirq_interop.testing import GateHelper +from qualtran.testing import execute_notebook + + +class SupportTComplexity: + def _t_complexity_(self) -> TComplexity: + return TComplexity(t=1) + + +class DoesNotSupportTComplexity: + ... + + +class SupportsTComplexityGateWithRegisters(GateWithRegisters): + @property + def signature(self) -> Signature: + return Signature.build(s=1, t=2) + + def _t_complexity_(self) -> TComplexity: + return TComplexity(t=1, clifford=2) + + +class SupportTComplexityGate(cirq.Gate): + def _num_qubits_(self) -> int: + return 1 + + def _t_complexity_(self) -> TComplexity: + return TComplexity(t=1) + + +class DoesNotSupportTComplexityGate(cirq.Gate): + def _num_qubits_(self): + return 1 + + +def test_t_complexity(): + with pytest.raises(TypeError): + _ = t_complexity(DoesNotSupportTComplexity()) + + with pytest.raises(TypeError): + _ = t_complexity(DoesNotSupportTComplexityGate()) + + assert t_complexity(DoesNotSupportTComplexity(), fail_quietly=True) is None + assert t_complexity([DoesNotSupportTComplexity()], fail_quietly=True) is None + assert t_complexity(DoesNotSupportTComplexityGate(), fail_quietly=True) is None + + assert t_complexity(SupportTComplexity()) == TComplexity(t=1) + assert t_complexity(SupportTComplexityGate().on(cirq.q('t'))) == TComplexity(t=1) + + g = GateHelper(SupportsTComplexityGateWithRegisters()) + assert g.gate._decompose_with_context_(g.operation.qubits) is NotImplemented + assert t_complexity(g.gate) == TComplexity(t=1, clifford=2) + assert t_complexity(g.operation) == TComplexity(t=1, clifford=2) + + assert t_complexity([cirq.T, cirq.X]) == TComplexity(t=1, clifford=1) + + q = cirq.NamedQubit('q') + assert t_complexity([cirq.T(q), cirq.X(q)]) == TComplexity(t=1, clifford=1) + + +def test_gates(): + # T gate and its adjoint + assert t_complexity(cirq.T) == TComplexity(t=1) + assert t_complexity(cirq.T**-1) == TComplexity(t=1) + + assert t_complexity(cirq.H) == TComplexity(clifford=1) # Hadamard + assert t_complexity(cirq.CNOT) == TComplexity(clifford=1) # CNOT + assert t_complexity(cirq.S) == TComplexity(clifford=1) # S + assert t_complexity(cirq.S**-1) == TComplexity(clifford=1) # S† + + # Pauli operators are clifford + assert t_complexity(cirq.X) == TComplexity(clifford=1) + assert t_complexity(cirq.Y) == TComplexity(clifford=1) + assert t_complexity(cirq.Z) == TComplexity(clifford=1) + + # Rotation about X, Y, and Z axes + assert t_complexity(cirq.Rx(rads=2)) == TComplexity(rotations=1) + assert t_complexity(cirq.Ry(rads=2)) == TComplexity(rotations=1) + assert t_complexity(cirq.Rz(rads=2)) == TComplexity(rotations=1) + + # clifford+T + assert t_complexity(And()) == TComplexity(t=4, clifford=9) + assert t_complexity(And() ** -1) == TComplexity(clifford=4) + + assert t_complexity(cirq.FREDKIN) == TComplexity(t=7, clifford=10) + + +def test_operations(): + q = cirq.NamedQubit('q') + assert t_complexity(cirq.T(q)) == TComplexity(t=1) + + gate = And() + op = gate.on_registers(**get_named_qubits(gate.signature)) + assert t_complexity(op) == TComplexity(t=4, clifford=9) + + gate = And() ** -1 + op = gate.on_registers(**get_named_qubits(gate.signature)) + assert t_complexity(op) == TComplexity(clifford=4) + + +def test_circuits(): + q = cirq.NamedQubit('q') + circuit = cirq.Circuit( + cirq.Rz(rads=0.6)(q), + cirq.T(q), + cirq.X(q) ** 0.5, + cirq.Rx(rads=0.1)(q), + cirq.Ry(rads=0.6)(q), + cirq.measure(q, key='m'), + ) + assert t_complexity(circuit) == TComplexity(clifford=2, rotations=3, t=1) + + circuit = cirq.FrozenCircuit(cirq.T(q) ** -1, cirq.Rx(rads=0.1)(q), cirq.measure(q, key='m')) + assert t_complexity(circuit) == TComplexity(clifford=1, rotations=1, t=1) + + +def test_circuit_operations(): + q = cirq.NamedQubit('q') + circuit = cirq.FrozenCircuit( + cirq.T(q), cirq.X(q) ** 0.5, cirq.Rx(rads=0.1)(q), cirq.measure(q, key='m') + ) + assert t_complexity(cirq.CircuitOperation(circuit)) == TComplexity(clifford=2, rotations=1, t=1) + assert t_complexity(cirq.CircuitOperation(circuit, repetitions=10)) == TComplexity( + clifford=20, rotations=10, t=10 + ) + + circuit = cirq.FrozenCircuit(cirq.T(q) ** -1, cirq.Rx(rads=0.1)(q), cirq.measure(q, key='m')) + assert t_complexity(cirq.CircuitOperation(circuit)) == TComplexity(clifford=1, rotations=1, t=1) + assert t_complexity(cirq.CircuitOperation(circuit, repetitions=3)) == TComplexity( + clifford=3, rotations=3, t=3 + ) + + +def test_classically_controlled_operations(): + q = cirq.NamedQubit('q') + assert t_complexity(cirq.X(q).with_classical_controls('c')) == TComplexity(clifford=1) + assert t_complexity(cirq.Rx(rads=0.1)(q).with_classical_controls('c')) == TComplexity( + rotations=1 + ) + assert t_complexity(cirq.T(q).with_classical_controls('c')) == TComplexity(t=1) + + +def test_tagged_operations(): + q = cirq.NamedQubit('q') + assert t_complexity(cirq.X(q).with_tags('tag1')) == TComplexity(clifford=1) + assert t_complexity(cirq.T(q).with_tags('tage1')) == TComplexity(t=1) + assert t_complexity(cirq.Ry(rads=0.1)(q).with_tags('tag1', 'tag2')) == TComplexity(rotations=1) + + +def test_cache_clear(): + class IsCachable(cirq.Operation): + def __init__(self) -> None: + super().__init__() + self.num_calls = 0 + self._gate = cirq.X + + def _t_complexity_(self) -> TComplexity: + self.num_calls += 1 + return TComplexity() + + @property + def qubits(self): + return [cirq.LineQubit(3)] # pragma: no cover + + def with_qubits(self, _): + ... + + @property + def gate(self): + return self._gate + + assert t_complexity(cirq.X) == TComplexity(clifford=1) + # Using a global cache will result in a failure of this test since `cirq.X` has + # `T-complexity(clifford=1)` but we explicitly return `TComplexity()` for IsCachable + # operation; for which the hash would be equivalent to the hash of it's subgate i.e. `cirq.X`. + t_complexity.cache_clear() + op = IsCachable() + assert t_complexity([op, op]) == TComplexity() + assert op.num_calls == 1 + t_complexity.cache_clear() + + +def test_notebook(): + execute_notebook('t_complexity') diff --git a/qualtran/cirq_interop/testing.py b/qualtran/cirq_interop/testing.py new file mode 100644 index 000000000..caa5d738c --- /dev/null +++ b/qualtran/cirq_interop/testing.py @@ -0,0 +1,137 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass +from typing import Any, Dict, List, Sequence, Tuple + +import cirq +import numpy as np +from cirq._compat import cached_property +from numpy.typing import NDArray + +from qualtran import Signature +from qualtran._infra.gate_with_registers import GateWithRegisters, get_named_qubits, merge_qubits +from qualtran.cirq_interop.decompose_protocol import _decompose_once_considering_known_decomposition +from qualtran.cirq_interop.t_complexity_protocol import t_complexity + + +@dataclass(frozen=True) +class GateHelper: + """A collection of related objects derivable from a `GateWithRegisters`. + + These are likely useful to have at one's fingertips while writing tests or + demo notebooks. + + Attributes: + gate: The gate from which all other objects are derived. + """ + + gate: GateWithRegisters + context: cirq.DecompositionContext = cirq.DecompositionContext(cirq.ops.SimpleQubitManager()) + + @cached_property + def r(self) -> Signature: + """The Signature system for the gate.""" + return self.gate.signature + + @cached_property + def quregs(self) -> Dict[str, NDArray[cirq.Qid]]: # type: ignore[type-var] + """A dictionary of named qubits appropriate for the signature for the gate.""" + return get_named_qubits(self.r) + + @cached_property + def all_qubits(self) -> List[cirq.Qid]: + """All qubits in Register order.""" + merged_qubits = merge_qubits(self.r, **self.quregs) + decomposed_qubits = self.decomposed_circuit.all_qubits() + return merged_qubits + sorted(decomposed_qubits - frozenset(merged_qubits)) + + @cached_property + def operation(self) -> cirq.Operation: + """The `gate` applied to example qubits.""" + return self.gate.on_registers(**self.quregs) + + @cached_property + def circuit(self) -> cirq.Circuit: + """The `gate` applied to example qubits wrapped in a `cirq.Circuit`.""" + return cirq.Circuit(self.operation) + + @cached_property + def decomposed_circuit(self) -> cirq.Circuit: + """The `gate` applied to example qubits, decomposed and wrapped in a `cirq.Circuit`.""" + return cirq.Circuit(cirq.decompose(self.operation, context=self.context)) + + +def assert_circuit_inp_out_cirqsim( + circuit: cirq.AbstractCircuit, + qubit_order: Sequence[cirq.Qid], + inputs: Sequence[int], + outputs: Sequence[int], + decimals: int = 2, +): + """Use a Cirq simulator to test that `circuit` behaves correctly on an input. + + Args: + circuit: The circuit representing the reversible classical operation. + qubit_order: The qubit order to pass to the cirq simulator. + inputs: The input state bit values. + outputs: The (correct) output state bit values. + decimals: The number of decimals of precision to use when comparing + amplitudes. Reversible classical operations should produce amplitudes + that are 0 or 1. + """ + actual, should_be = get_circuit_inp_out_cirqsim(circuit, qubit_order, inputs, outputs, decimals) + assert actual == should_be + + +def get_circuit_inp_out_cirqsim( + circuit: cirq.AbstractCircuit, + qubit_order: Sequence[cirq.Qid], + inputs: Sequence[int], + outputs: Sequence[int], + decimals: int = 2, +) -> Tuple[str, str]: + """Use a Cirq simulator to get a outputs of a `circuit`. + + Args: + circuit: The circuit representing the reversible classical operation. + qubit_order: The qubit order to pass to the cirq simulator. + inputs: The input state bit values. + outputs: The (correct) output state bit values. + decimals: The number of decimals of precision to use when comparing + amplitudes. Reversible classical operations should produce amplitudes + that are 0 or 1. + + Returns: + actual: The simulated output state as a string bitstring. + should_be: The outputs argument formatted as a string bitstring for ease of comparison. + """ + result = cirq.Simulator(dtype=np.complex128).simulate( + circuit, initial_state=inputs, qubit_order=qubit_order + ) + actual = result.dirac_notation(decimals=decimals)[1:-1] + should_be = "".join(str(x) for x in outputs) + return actual, should_be + + +def assert_decompose_is_consistent_with_t_complexity(val: Any): + t_complexity_method = getattr(val, '_t_complexity_', None) + expected = NotImplemented if t_complexity_method is None else t_complexity_method() + if expected is NotImplemented or expected is None: + return + decomposition = _decompose_once_considering_known_decomposition(val) + if decomposition is None: + return + from_decomposition = t_complexity(decomposition, fail_quietly=False) + assert expected == from_decomposition, f'{expected} != {from_decomposition}' diff --git a/qualtran/cirq_interop/testing_test.py b/qualtran/cirq_interop/testing_test.py new file mode 100644 index 000000000..400389137 --- /dev/null +++ b/qualtran/cirq_interop/testing_test.py @@ -0,0 +1,92 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import cirq +import numpy as np +import pytest + +from qualtran import Register, Side, Signature +from qualtran.bloqs.and_bloq import And, MultiAnd +from qualtran.cirq_interop import testing +from qualtran.cirq_interop.t_complexity_protocol import TComplexity + + +def test_assert_circuit_inp_out_cirqsim(): + qubits = cirq.LineQubit.range(4) + initial_state = [0, 1, 0, 0] + circuit = cirq.Circuit(cirq.X(qubits[3])) + final_state = [0, 1, 0, 1] + + testing.assert_circuit_inp_out_cirqsim(circuit, qubits, initial_state, final_state) + + final_state = [0, 0, 0, 1] + with pytest.raises(AssertionError): + testing.assert_circuit_inp_out_cirqsim(circuit, qubits, initial_state, final_state) + + +def test_gate_helper(): + g = testing.GateHelper(MultiAnd(cvs=(1, 0, 1, 0))) + assert g.gate == MultiAnd(cvs=(1, 0, 1, 0)) + assert g.r == Signature( + [ + Register('ctrl', bitsize=1, shape=4), + Register('junk', bitsize=1, shape=2, side=Side.RIGHT), + Register('target', bitsize=1, side=Side.RIGHT), + ] + ) + expected_quregs = { + 'ctrl': np.array([[cirq.q(f'ctrl[{i}]')] for i in range(4)]), + 'junk': np.array([[cirq.q(f'junk[{i}]')] for i in range(2)]), + 'target': [cirq.NamedQubit('target')], + } + for key in expected_quregs: + assert np.array_equal(g.quregs[key], expected_quregs[key]) + assert g.operation.qubits == tuple(g.all_qubits) + assert len(g.circuit) == 1 + + +class DoesNotDecompose(cirq.Operation): + def _t_complexity_(self) -> TComplexity: + return TComplexity(t=1, clifford=2, rotations=3) + + @property + def qubits(self): + return [] + + def with_qubits(self, _): + pass + + +class InconsistentDecompostion(cirq.Operation): + def _t_complexity_(self) -> TComplexity: + return TComplexity(rotations=1) + + def _decompose_(self) -> cirq.OP_TREE: + yield cirq.X(self.qubits[0]) + + @property + def qubits(self): + return tuple(cirq.LineQubit(3).range(3)) + + def with_qubits(self, _): + pass + + +@pytest.mark.parametrize("val", [cirq.T, DoesNotDecompose(), And()]) +def test_assert_decompose_is_consistent_with_t_complexity(val): + testing.assert_decompose_is_consistent_with_t_complexity(val) + + +def test_assert_decompose_is_consistent_with_t_complexity_raises(): + with pytest.raises(AssertionError): + testing.assert_decompose_is_consistent_with_t_complexity(InconsistentDecompostion())