From 6f4f5d9d80e3c0eb4eaa4b6bf7ab9a37ffb3a8b8 Mon Sep 17 00:00:00 2001 From: Charles Yuan Date: Tue, 2 Jul 2024 02:50:57 +0200 Subject: [PATCH 1/2] Generalize `symbolics.prod` to allow `SymbolicFloat` or `SymbolicInt` (#1101) * Generalize `symbolics.prod` to allow `SymbolicFloat` or `SymbolicInt` * Better document scope of AnySymbolic * Rename to SymbolicT --- qualtran/bloqs/data_loading/qrom.py | 4 ++-- qualtran/bloqs/data_loading/select_swap_qrom.py | 2 +- qualtran/bloqs/swap_network/swap_with_zero.py | 2 +- qualtran/symbolics/math_funcs.py | 14 +++++++++++--- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/qualtran/bloqs/data_loading/qrom.py b/qualtran/bloqs/data_loading/qrom.py index f1c82b9a8d..db2607a574 100644 --- a/qualtran/bloqs/data_loading/qrom.py +++ b/qualtran/bloqs/data_loading/qrom.py @@ -219,8 +219,8 @@ def nth_operation_callgraph(self, **kwargs: int) -> Set['BloqCountT']: def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: if self.has_data(): return super().build_call_graph(ssa=ssa) - n_and = prod(*self.data_shape) - 2 + self.num_controls - n_cnot = prod(*self.target_bitsizes, *self.data_shape) + n_and = prod(self.data_shape) - 2 + self.num_controls + n_cnot = prod(self.target_bitsizes) * prod(self.data_shape) return {(And(), n_and), (And().adjoint(), n_and), (CNOT(), n_cnot)} diff --git a/qualtran/bloqs/data_loading/select_swap_qrom.py b/qualtran/bloqs/data_loading/select_swap_qrom.py index a7179493a9..d2710f3383 100644 --- a/qualtran/bloqs/data_loading/select_swap_qrom.py +++ b/qualtran/bloqs/data_loading/select_swap_qrom.py @@ -120,7 +120,7 @@ def signature(self) -> Signature: @log_block_sizes.default def _default_block_sizes(self) -> Tuple[SymbolicInt, ...]: target_bitsize = sum(self.target_bitsizes) * sum( - prod(*shape) for shape in self.target_shapes + prod(shape) for shape in self.target_shapes ) return tuple(find_optimal_log_block_size(ilen, target_bitsize) for ilen in self.data_shape) diff --git a/qualtran/bloqs/swap_network/swap_with_zero.py b/qualtran/bloqs/swap_network/swap_with_zero.py index 77ac6868f7..be301aa5a6 100644 --- a/qualtran/bloqs/swap_network/swap_with_zero.py +++ b/qualtran/bloqs/swap_network/swap_with_zero.py @@ -152,7 +152,7 @@ def build_composite_bloq( return sel | {'targets': targets} def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: - num_swaps = prod(*[x for x in self.n_target_registers]) - 1 + num_swaps = prod(x for x in self.n_target_registers) - 1 return {(CSwapApprox(self.target_bitsize), num_swaps)} def _circuit_diagram_info_(self, args) -> cirq.CircuitDiagramInfo: diff --git a/qualtran/symbolics/math_funcs.py b/qualtran/symbolics/math_funcs.py index fa2db971f4..a87929eb50 100644 --- a/qualtran/symbolics/math_funcs.py +++ b/qualtran/symbolics/math_funcs.py @@ -11,7 +11,7 @@ # 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 cast, overload, Sized, Tuple, Union +from typing import cast, Iterable, overload, Sized, Tuple, TypeVar, Union import numpy as np import sympy @@ -82,8 +82,16 @@ def smin(*args): return min(*args) -def prod(*args: SymbolicInt) -> SymbolicInt: - ret: SymbolicInt = 1 +# This is only used in the type signature of functions that should be generic over different +# symbolic types, in situations where Union and @overload are not sufficient. +# The user should not need to invoke it directly. Rather, the user can use a function that +# takes SymbolicT by calling it with e.g. a SymbolicInt. Correspondingly, if the type signature +# of the function returns SymbolicT, then this call will then return a SymbolicInt. +SymbolicT = TypeVar('SymbolicT', SymbolicInt, SymbolicFloat, SymbolicComplex) + + +def prod(args: Iterable[SymbolicT]) -> SymbolicT: + ret: SymbolicT = 1 for arg in args: ret = ret * arg return ret From bc32731565eaabb25deabfa3e1dc850ff24b4a21 Mon Sep 17 00:00:00 2001 From: Charles Yuan Date: Tue, 2 Jul 2024 20:35:53 +0200 Subject: [PATCH 2/2] Add `Unitary` and `TensorProduct` block encodings (#1094) * Add `Unitary` and `TensorProduct` block encodings * Update edge case tests * Add explicit test for zero-size registers * Fix and clarify behavior of `Signature.build` with zero bitsize * Update copyright * Address comments * Address comments * Add symbolics * Use symbolic prod and sum * Fix pylint * Remove zero-sized registers and add tests * Add Cirq decomposition test --- dev_tools/autogenerate-bloqs-notebooks-v2.py | 2 + qualtran/bloqs/block_encoding/__init__.py | 2 + .../bloqs/block_encoding/block_encoding.ipynb | 298 +++++++++++++++++- .../block_encoding/block_encoding_base.py | 26 ++ .../bloqs/block_encoding/tensor_product.py | 237 ++++++++++++++ .../block_encoding/tensor_product_test.py | 123 ++++++++ qualtran/bloqs/block_encoding/unitary.py | 139 ++++++++ qualtran/bloqs/block_encoding/unitary_test.py | 83 +++++ qualtran/bloqs/bookkeeping/partition.py | 11 +- qualtran/conftest.py | 5 + qualtran/serialization/resolver_dict.py | 4 + qualtran/symbolics/__init__.py | 1 + qualtran/symbolics/math_funcs.py | 7 + 13 files changed, 932 insertions(+), 6 deletions(-) create mode 100644 qualtran/bloqs/block_encoding/tensor_product.py create mode 100644 qualtran/bloqs/block_encoding/tensor_product_test.py create mode 100644 qualtran/bloqs/block_encoding/unitary.py create mode 100644 qualtran/bloqs/block_encoding/unitary_test.py diff --git a/dev_tools/autogenerate-bloqs-notebooks-v2.py b/dev_tools/autogenerate-bloqs-notebooks-v2.py index 5946ed0622..4358844cc5 100644 --- a/dev_tools/autogenerate-bloqs-notebooks-v2.py +++ b/dev_tools/autogenerate-bloqs-notebooks-v2.py @@ -570,6 +570,8 @@ qualtran.bloqs.block_encoding.lcu_block_encoding._LCU_BLOCK_ENCODING_DOC, qualtran.bloqs.block_encoding.lcu_block_encoding._LCU_ZERO_STATE_BLOCK_ENCODING_DOC, qualtran.bloqs.block_encoding.chebyshev_polynomial._CHEBYSHEV_BLOQ_DOC, + qualtran.bloqs.block_encoding.unitary._UNITARY_DOC, + qualtran.bloqs.block_encoding.tensor_product._TENSOR_PRODUCT_DOC, ], directory=f'{SOURCE_DIR}/bloqs/block_encoding/', ), diff --git a/qualtran/bloqs/block_encoding/__init__.py b/qualtran/bloqs/block_encoding/__init__.py index 22a74ba08d..7b05237906 100644 --- a/qualtran/bloqs/block_encoding/__init__.py +++ b/qualtran/bloqs/block_encoding/__init__.py @@ -21,3 +21,5 @@ LCUBlockEncoding, LCUBlockEncodingZeroState, ) +from qualtran.bloqs.block_encoding.tensor_product import TensorProduct +from qualtran.bloqs.block_encoding.unitary import Unitary diff --git a/qualtran/bloqs/block_encoding/block_encoding.ipynb b/qualtran/bloqs/block_encoding/block_encoding.ipynb index e4fbb63dd9..d5400fcbc5 100644 --- a/qualtran/bloqs/block_encoding/block_encoding.ipynb +++ b/qualtran/bloqs/block_encoding/block_encoding.ipynb @@ -594,6 +594,302 @@ "show_call_graph(chebyshev_poly_g)\n", "show_counts_sigma(chebyshev_poly_sigma)" ] + }, + { + "cell_type": "markdown", + "id": "700368a9", + "metadata": { + "cq.autogen": "Unitary.bloq_doc.md" + }, + "source": [ + "## `Unitary`\n", + "Trivial block encoding of a unitary operator.\n", + "\n", + "Builds the block encoding as\n", + "$\n", + " B[U] = U\n", + "$\n", + "where $U$ is a unitary operator. Here, $B[U]$ is a $(1, 0, 0)$-block encoding of $U$.\n", + "\n", + "#### Parameters\n", + " - `U`: The unitary operator to block-encode.\n", + " - `alpha`: The normalization factor (default 1).\n", + " - `ancilla_bitsize`: The number of ancilla bits (default 0).\n", + " - `resource_bitsize`: The number of resource bits (default 0).\n", + " - `epsilon`: The precision parameter (default 0). \n", + "\n", + "#### Registers\n", + " - `system`: The system register.\n", + " - `ancilla`: The ancilla register (present only if bitsize > 0).\n", + " - `resource`: The resource register (present only if bitsize > 0).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69e80c37", + "metadata": { + "cq.autogen": "Unitary.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.block_encoding import Unitary" + ] + }, + { + "cell_type": "markdown", + "id": "853ccd95", + "metadata": { + "cq.autogen": "Unitary.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af1f5871", + "metadata": { + "cq.autogen": "Unitary.unitary_block_encoding" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.basic_gates import TGate\n", + "\n", + "unitary_block_encoding = Unitary(TGate())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "421a0137", + "metadata": { + "cq.autogen": "Unitary.unitary_block_encoding_override" + }, + "outputs": [], + "source": [ + "from attrs import evolve\n", + "\n", + "from qualtran.bloqs.basic_gates import TGate\n", + "\n", + "unitary_block_encoding_override = evolve(\n", + " Unitary(TGate()), alpha=0.5, ancilla_bitsize=2, resource_bitsize=1, epsilon=0.01\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "85b9c065", + "metadata": { + "cq.autogen": "Unitary.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47675855", + "metadata": { + "cq.autogen": "Unitary.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([unitary_block_encoding, unitary_block_encoding_override],\n", + " ['`unitary_block_encoding`', '`unitary_block_encoding_override`'])" + ] + }, + { + "cell_type": "markdown", + "id": "36167a23", + "metadata": { + "cq.autogen": "Unitary.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97a218a8", + "metadata": { + "cq.autogen": "Unitary.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "unitary_block_encoding_g, unitary_block_encoding_sigma = unitary_block_encoding.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(unitary_block_encoding_g)\n", + "show_counts_sigma(unitary_block_encoding_sigma)" + ] + }, + { + "cell_type": "markdown", + "id": "2c274284", + "metadata": { + "cq.autogen": "TensorProduct.bloq_doc.md" + }, + "source": [ + "## `TensorProduct`\n", + "Tensor product of a sequence of block encodings.\n", + "\n", + "Builds the block encoding as\n", + "$$\n", + " B[U_1 ⊗ U_2 ⊗ \\cdots ⊗ U_n] = B[U_1] ⊗ B[U_2] ⊗ \\cdots ⊗ B[U_n]\n", + "$$\n", + "\n", + "When each $B[U_i]$ is a $(\\alpha_i, a_i, \\epsilon_i)$-block encoding of $U_i$, we have that\n", + "$B[U_1 ⊗ \\cdots ⊗ U_n]$ is a $(\\prod_i \\alpha_i, \\sum_i a_i, \\sum_i \\alpha_i \\epsilon_i)$-block\n", + "encoding of $U_1 ⊗ \\cdots ⊗ U_n$.\n", + "\n", + "#### Parameters\n", + " - `block_encodings`: A sequence of block encodings. \n", + "\n", + "#### Registers\n", + " - `system`: The system register.\n", + " - `ancilla`: The ancilla register (present only if bitsize > 0).\n", + " - `resource`: The resource register (present only if bitsize > 0). \n", + "\n", + "#### References\n", + " - [Quantum algorithms: A survey of applications and end-to-end complexities](https://arxiv.org/abs/2310.03011). Dalzell et al. (2023). Ch. 10.2.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0940792a", + "metadata": { + "cq.autogen": "TensorProduct.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.block_encoding import TensorProduct" + ] + }, + { + "cell_type": "markdown", + "id": "9199daa6", + "metadata": { + "cq.autogen": "TensorProduct.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "488f9bb9", + "metadata": { + "cq.autogen": "TensorProduct.tensor_product_block_encoding" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.basic_gates import Hadamard, TGate\n", + "from qualtran.bloqs.block_encoding.unitary import Unitary\n", + "\n", + "tensor_product_block_encoding = TensorProduct((Unitary(TGate()), Unitary(Hadamard())))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b1b4b8c", + "metadata": { + "cq.autogen": "TensorProduct.tensor_product_block_encoding_override" + }, + "outputs": [], + "source": [ + "from attrs import evolve\n", + "\n", + "from qualtran.bloqs.basic_gates import CNOT, TGate\n", + "from qualtran.bloqs.block_encoding.unitary import Unitary\n", + "\n", + "u1 = evolve(Unitary(TGate()), alpha=0.5, ancilla_bitsize=2, resource_bitsize=1, epsilon=0.01)\n", + "u2 = evolve(Unitary(CNOT()), alpha=0.5, ancilla_bitsize=1, resource_bitsize=1, epsilon=0.1)\n", + "tensor_product_block_encoding_override = TensorProduct((u1, u2))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e41f94fa", + "metadata": { + "cq.autogen": "TensorProduct.tensor_product_block_encoding_symb" + }, + "outputs": [], + "source": [ + "import sympy\n", + "\n", + "from qualtran.bloqs.basic_gates import Hadamard, TGate\n", + "from qualtran.bloqs.block_encoding.unitary import Unitary\n", + "\n", + "alpha1 = sympy.Symbol('alpha1')\n", + "a1 = sympy.Symbol('a1')\n", + "eps1 = sympy.Symbol('eps1')\n", + "alpha2 = sympy.Symbol('alpha2')\n", + "a2 = sympy.Symbol('a2')\n", + "eps2 = sympy.Symbol('eps2')\n", + "tensor_product_block_encoding_symb = TensorProduct(\n", + " (\n", + " Unitary(TGate(), alpha=alpha1, ancilla_bitsize=a1, epsilon=eps1),\n", + " Unitary(Hadamard(), alpha=alpha2, ancilla_bitsize=a2, epsilon=eps2),\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "605e1129", + "metadata": { + "cq.autogen": "TensorProduct.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23f6bacf", + "metadata": { + "cq.autogen": "TensorProduct.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([tensor_product_block_encoding, tensor_product_block_encoding_override, tensor_product_block_encoding_symb],\n", + " ['`tensor_product_block_encoding`', '`tensor_product_block_encoding_override`', '`tensor_product_block_encoding_symb`'])" + ] + }, + { + "cell_type": "markdown", + "id": "c6b0e68e", + "metadata": { + "cq.autogen": "TensorProduct.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bfab456d", + "metadata": { + "cq.autogen": "TensorProduct.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "tensor_product_block_encoding_g, tensor_product_block_encoding_sigma = tensor_product_block_encoding.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(tensor_product_block_encoding_g)\n", + "show_counts_sigma(tensor_product_block_encoding_sigma)" + ] } ], "metadata": { @@ -612,7 +908,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/qualtran/bloqs/block_encoding/block_encoding_base.py b/qualtran/bloqs/block_encoding/block_encoding_base.py index 85e821ad84..a35306b488 100644 --- a/qualtran/bloqs/block_encoding/block_encoding_base.py +++ b/qualtran/bloqs/block_encoding/block_encoding_base.py @@ -16,6 +16,7 @@ from qualtran import Bloq, BloqDocSpec, Register from qualtran.bloqs.block_encoding.lcu_select_and_prepare import PrepareOracle +from qualtran.symbolics import SymbolicFloat, SymbolicInt class BlockEncoding(Bloq): @@ -67,6 +68,31 @@ class `BlockEncoding` bloq, which expects values for $\alpha$, $\epsilon$, def pretty_name(self) -> str: return 'B[H]' + @property + def alpha(self) -> SymbolicFloat: + """The normalization constant.""" + raise NotImplementedError + + @property + def system_bitsize(self) -> SymbolicInt: + """The number of qubits that represent the system being block encoded.""" + raise NotImplementedError + + @property + def ancilla_bitsize(self) -> SymbolicInt: + """The number of ancilla qubits.""" + raise NotImplementedError + + @property + def resource_bitsize(self) -> SymbolicInt: + """The number of resource qubits not counted in ancillas.""" + raise NotImplementedError + + @property + def epsilon(self) -> SymbolicFloat: + """The precision to which the block encoding is to be prepared.""" + raise NotImplementedError + @property @abc.abstractmethod def selection_registers(self) -> Tuple[Register, ...]: diff --git a/qualtran/bloqs/block_encoding/tensor_product.py b/qualtran/bloqs/block_encoding/tensor_product.py new file mode 100644 index 0000000000..50a5bf8395 --- /dev/null +++ b/qualtran/bloqs/block_encoding/tensor_product.py @@ -0,0 +1,237 @@ +# Copyright 2024 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 collections import Counter +from functools import cached_property +from typing import cast, Dict, Set, Tuple + +from attrs import evolve, field, frozen, validators + +from qualtran import ( + bloq_example, + BloqBuilder, + BloqDocSpec, + DecomposeTypeError, + QAny, + Register, + Signature, + SoquetT, +) +from qualtran.bloqs.block_encoding import BlockEncoding +from qualtran.bloqs.block_encoding.lcu_select_and_prepare import PrepareOracle +from qualtran.bloqs.bookkeeping import Partition +from qualtran.resource_counting import BloqCountT, SympySymbolAllocator +from qualtran.symbolics import is_symbolic, prod, ssum, SymbolicFloat, SymbolicInt + + +@frozen +class TensorProduct(BlockEncoding): + r"""Tensor product of a sequence of block encodings. + + Builds the block encoding as + $$ + B[U_1 ⊗ U_2 ⊗ \cdots ⊗ U_n] = B[U_1] ⊗ B[U_2] ⊗ \cdots ⊗ B[U_n] + $$ + + When each $B[U_i]$ is a $(\alpha_i, a_i, \epsilon_i)$-block encoding of $U_i$, we have that + $B[U_1 ⊗ \cdots ⊗ U_n]$ is a $(\prod_i \alpha_i, \sum_i a_i, \sum_i \alpha_i \epsilon_i)$-block + encoding of $U_1 ⊗ \cdots ⊗ U_n$. + + Args: + block_encodings: A sequence of block encodings. + + Registers: + system: The system register. + ancilla: The ancilla register (present only if bitsize > 0). + resource: The resource register (present only if bitsize > 0). + + References: + [Quantum algorithms: A survey of applications and end-to-end complexities](https://arxiv.org/abs/2310.03011). Dalzell et al. (2023). Ch. 10.2. + """ + + block_encodings: Tuple[BlockEncoding, ...] = field( + converter=lambda x: x if isinstance(x, tuple) else tuple(x), validator=validators.min_len(1) + ) + + @cached_property + def signature(self) -> Signature: + return Signature.build_from_dtypes( + system=QAny(self.system_bitsize), + ancilla=QAny(self.ancilla_bitsize), # if ancilla_bitsize is 0, not present + resource=QAny(self.resource_bitsize), # if resource_bitsize is 0, not present + ) + + @cached_property + def system_bitsize(self) -> SymbolicInt: + return ssum(u.system_bitsize for u in self.block_encodings) + + def pretty_name(self) -> str: + return f"B[{'⊗'.join(u.pretty_name()[2:-1] for u in self.block_encodings)}]" + + @cached_property + def alpha(self) -> SymbolicFloat: + return prod(u.alpha for u in self.block_encodings) + + @cached_property + def ancilla_bitsize(self) -> SymbolicInt: + return ssum(u.ancilla_bitsize for u in self.block_encodings) + + @cached_property + def resource_bitsize(self) -> SymbolicInt: + return ssum(u.resource_bitsize for u in self.block_encodings) + + @cached_property + def epsilon(self) -> SymbolicFloat: + return ssum(u.alpha * u.epsilon for u in self.block_encodings) + + @property + def target_registers(self) -> Tuple[Register, ...]: + return (self.signature.get_right("system"),) + + @property + def junk_registers(self) -> Tuple[Register, ...]: + return (self.signature.get_right("resource"),) if self.resource_bitsize > 0 else () + + @property + def selection_registers(self) -> Tuple[Register, ...]: + return (self.signature.get_right("ancilla"),) if self.ancilla_bitsize > 0 else () + + @property + def signal_state(self) -> PrepareOracle: + """This method will be implemented in the future after PrepareOracle is updated for the BlockEncoding interface.""" + raise NotImplementedError + + def build_call_graph(self, ssa: SympySymbolAllocator) -> Set[BloqCountT]: + return set(Counter(self.block_encodings).items()) + + def build_composite_bloq( + self, bb: BloqBuilder, system: SoquetT, **soqs: SoquetT + ) -> Dict[str, SoquetT]: + if ( + is_symbolic(self.system_bitsize) + or is_symbolic(self.ancilla_bitsize) + or is_symbolic(self.resource_bitsize) + ): + raise DecomposeTypeError(f"Cannot decompose symbolic {self=}") + + sys_regs = tuple( + evolve(u.signature.get_left("system"), name=f"system{i}_") + for i, u in enumerate(self.block_encodings) + ) + anc_regs = tuple( + evolve(u.signature.get_left("ancilla"), name=f"ancilla{i}_") + for i, u in enumerate(self.block_encodings) + if "ancilla" in u.signature._lefts + ) + res_regs = tuple( + evolve(u.signature.get_left("resource"), name=f"resource{i}_") + for i, u in enumerate(self.block_encodings) + if "resource" in u.signature._lefts + ) + + sys_part = Partition(cast(int, self.system_bitsize), regs=sys_regs) + sys_out_regs = list(bb.add_t(sys_part, x=system)) + if len(anc_regs) > 0: + anc_part = Partition(cast(int, self.ancilla_bitsize), regs=anc_regs) + anc_out_regs = list(bb.add_t(anc_part, x=soqs["ancilla"])) + if len(res_regs) > 0: + res_part = Partition(cast(int, self.resource_bitsize), regs=res_regs) + res_out_regs = list(bb.add_t(res_part, x=soqs["resource"])) + sys_i = 0 + anc_i = 0 + res_i = 0 + for u in self.block_encodings: + u_soqs = dict() + u_soqs["system"] = sys_out_regs[sys_i] + if "ancilla" in u.signature._lefts: + u_soqs["ancilla"] = anc_out_regs[anc_i] + if "resource" in u.signature._lefts: + u_soqs["resource"] = res_out_regs[res_i] + u_soqs_out = bb.add_d(u, **u_soqs) + sys_out_regs[sys_i] = u_soqs_out["system"] + sys_i += 1 + if "ancilla" in u.signature._lefts: + anc_out_regs[anc_i] = u_soqs_out["ancilla"] + anc_i += 1 + if "resource" in u.signature._lefts: + res_out_regs[res_i] = u_soqs_out["resource"] + res_i += 1 + soqs_out = dict() + soqs_out["system"] = bb.add( + sys_part.adjoint(), **{r.name: sp for r, sp in zip(sys_regs, sys_out_regs)} + ) + if len(anc_regs) > 0: + soqs_out["ancilla"] = bb.add( + anc_part.adjoint(), **{r.name: ap for r, ap in zip(anc_regs, anc_out_regs)} + ) + if len(res_regs) > 0: + soqs_out["resource"] = bb.add( + res_part.adjoint(), **{r.name: ap for r, ap in zip(res_regs, res_out_regs)} + ) + return soqs_out + + +@bloq_example +def _tensor_product_block_encoding() -> TensorProduct: + from qualtran.bloqs.basic_gates import Hadamard, TGate + from qualtran.bloqs.block_encoding.unitary import Unitary + + tensor_product_block_encoding = TensorProduct((Unitary(TGate()), Unitary(Hadamard()))) + return tensor_product_block_encoding + + +@bloq_example +def _tensor_product_block_encoding_override() -> TensorProduct: + from attrs import evolve + + from qualtran.bloqs.basic_gates import CNOT, TGate + from qualtran.bloqs.block_encoding.unitary import Unitary + + u1 = evolve(Unitary(TGate()), alpha=0.5, ancilla_bitsize=2, resource_bitsize=1, epsilon=0.01) + u2 = evolve(Unitary(CNOT()), alpha=0.5, ancilla_bitsize=1, resource_bitsize=1, epsilon=0.1) + tensor_product_block_encoding_override = TensorProduct((u1, u2)) + return tensor_product_block_encoding_override + + +@bloq_example +def _tensor_product_block_encoding_symb() -> TensorProduct: + import sympy + + from qualtran.bloqs.basic_gates import Hadamard, TGate + from qualtran.bloqs.block_encoding.unitary import Unitary + + alpha1 = sympy.Symbol('alpha1') + a1 = sympy.Symbol('a1') + eps1 = sympy.Symbol('eps1') + alpha2 = sympy.Symbol('alpha2') + a2 = sympy.Symbol('a2') + eps2 = sympy.Symbol('eps2') + tensor_product_block_encoding_symb = TensorProduct( + ( + Unitary(TGate(), alpha=alpha1, ancilla_bitsize=a1, epsilon=eps1), + Unitary(Hadamard(), alpha=alpha2, ancilla_bitsize=a2, epsilon=eps2), + ) + ) + return tensor_product_block_encoding_symb + + +_TENSOR_PRODUCT_DOC = BloqDocSpec( + bloq_cls=TensorProduct, + import_line="from qualtran.bloqs.block_encoding import TensorProduct", + examples=[ + _tensor_product_block_encoding, + _tensor_product_block_encoding_override, + _tensor_product_block_encoding_symb, + ], +) diff --git a/qualtran/bloqs/block_encoding/tensor_product_test.py b/qualtran/bloqs/block_encoding/tensor_product_test.py new file mode 100644 index 0000000000..1f94a0a38a --- /dev/null +++ b/qualtran/bloqs/block_encoding/tensor_product_test.py @@ -0,0 +1,123 @@ +# Copyright 2024 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 cast + +import cirq +import numpy as np +import pytest +import sympy + +from qualtran import BloqBuilder, QAny, Register, Signature, Soquet +from qualtran.bloqs.basic_gates import CNOT, Hadamard, TGate, XGate, ZeroEffect, ZeroState +from qualtran.bloqs.block_encoding.tensor_product import ( + _tensor_product_block_encoding, + _tensor_product_block_encoding_override, + _tensor_product_block_encoding_symb, + TensorProduct, +) +from qualtran.bloqs.block_encoding.unitary import Unitary +from qualtran.cirq_interop.testing import assert_circuit_inp_out_cirqsim + + +def test_tensor_product(bloq_autotester): + bloq_autotester(_tensor_product_block_encoding) + bloq_autotester(_tensor_product_block_encoding_symb) + + +def test_tensor_product_signature(): + assert _tensor_product_block_encoding().signature == Signature([Register("system", QAny(2))]) + assert _tensor_product_block_encoding_override().signature == Signature( + [Register("system", QAny(3)), Register("ancilla", QAny(3)), Register("resource", QAny(2))] + ) + assert _tensor_product_block_encoding_symb().signature == Signature( + [ + Register("system", QAny(2)), + Register("ancilla", QAny(sympy.Symbol('a1') + sympy.Symbol('a2'))), + ] + ) + with pytest.raises(ValueError): + _ = TensorProduct(()) + + +def test_tensor_product_params(): + bloq = _tensor_product_block_encoding() + assert bloq.system_bitsize == 1 + 1 + assert bloq.alpha == 1 + assert bloq.epsilon == 0 + assert bloq.ancilla_bitsize == 0 + assert bloq.resource_bitsize == 0 + + bloq = _tensor_product_block_encoding_override() + assert bloq.system_bitsize == 1 + 2 + assert bloq.alpha == 0.5 * 0.5 + assert bloq.epsilon == 0.5 * 0.01 + 0.5 * 0.1 + assert bloq.ancilla_bitsize == 2 + 1 + assert bloq.resource_bitsize == 1 + 1 + + bloq = _tensor_product_block_encoding_symb() + assert bloq.system_bitsize == 2 + assert bloq.alpha == sympy.Symbol('alpha1') * sympy.Symbol('alpha2') + assert bloq.epsilon == sympy.Symbol('alpha1') * sympy.Symbol('eps1') + sympy.Symbol( + 'alpha2' + ) * sympy.Symbol('eps2') + assert bloq.ancilla_bitsize == sympy.Symbol('a1') + sympy.Symbol('a2') + assert bloq.resource_bitsize == 0 + + +def test_tensor_product_tensors(): + from_gate = TGate().tensor_contract() + from_tensors = TensorProduct((Unitary(TGate()),)).tensor_contract() + np.testing.assert_allclose(from_gate, from_tensors) + + from_gate = np.kron(TGate().tensor_contract(), Hadamard().tensor_contract()) + from_tensors = _tensor_product_block_encoding().tensor_contract() + np.testing.assert_allclose(from_gate, from_tensors) + + +def test_tensor_product_override_tensors(): + bb = BloqBuilder() + system = bb.add_register("system", 3) + ancilla = bb.join(np.array([bb.add(ZeroState()), bb.add(ZeroState()), bb.add(ZeroState())])) + resource = bb.join(np.array([bb.add(ZeroState()), bb.add(ZeroState())])) + system, ancilla, resource = bb.add_t( + _tensor_product_block_encoding_override(), system=system, ancilla=ancilla, resource=resource + ) + for q in bb.split(cast(Soquet, ancilla)): + bb.add(ZeroEffect(), q=q) + for q in bb.split(cast(Soquet, resource)): + bb.add(ZeroEffect(), q=q) + bloq = bb.finalize(system=system) + + from_gate = np.kron(TGate().tensor_contract(), CNOT().tensor_contract()) + from_tensors = bloq.tensor_contract() + np.testing.assert_allclose(from_gate, from_tensors) + + +def test_tensor_product_cirq(): + qubits = cirq.LineQubit.range(4) + op = TensorProduct( + (TensorProduct((Unitary(XGate()), Unitary(XGate()))), Unitary(CNOT())) + ).on_registers(system=qubits) + circuit = cirq.Circuit( + cirq.decompose_once( + op, + context=cirq.DecompositionContext( + cirq.GreedyQubitManager(prefix="_a", maximize_reuse=True) + ), + ) + ) + initial_state = [0, 1, 1, 1] + final_state = [1, 0, 1, 0] + assert_circuit_inp_out_cirqsim(circuit, qubits, initial_state, final_state) diff --git a/qualtran/bloqs/block_encoding/unitary.py b/qualtran/bloqs/block_encoding/unitary.py new file mode 100644 index 0000000000..6539576015 --- /dev/null +++ b/qualtran/bloqs/block_encoding/unitary.py @@ -0,0 +1,139 @@ +# Copyright 2024 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 functools import cached_property +from typing import Dict, Set, Tuple + +from attrs import frozen + +from qualtran import ( + Bloq, + bloq_example, + BloqBuilder, + BloqDocSpec, + QAny, + Register, + Side, + Signature, + SoquetT, +) +from qualtran.bloqs.block_encoding import BlockEncoding +from qualtran.bloqs.block_encoding.lcu_select_and_prepare import PrepareOracle +from qualtran.resource_counting import BloqCountT, SympySymbolAllocator +from qualtran.symbolics import SymbolicFloat, SymbolicInt + + +@frozen +class Unitary(BlockEncoding): + r"""Trivial block encoding of a unitary operator. + + Builds the block encoding as + $ + B[U] = U + $ + where $U$ is a unitary operator. Here, $B[U]$ is a $(1, 0, 0)$-block encoding of $U$. + + Args: + U: The unitary operator to block-encode. + alpha: The normalization factor (default 1). + ancilla_bitsize: The number of ancilla bits (default 0). + resource_bitsize: The number of resource bits (default 0). + epsilon: The precision parameter (default 0). + + Registers: + system: The system register. + ancilla: The ancilla register (present only if bitsize > 0). + resource: The resource register (present only if bitsize > 0). + """ + + U: Bloq + alpha: SymbolicFloat = 1 + ancilla_bitsize: SymbolicInt = 0 + resource_bitsize: SymbolicInt = 0 + epsilon: SymbolicFloat = 0 + + @cached_property + def system_bitsize(self) -> SymbolicInt: + return sum(r.bitsize for r in self.U.signature) + + def __attrs_post_init__(self): + if not all(r.side == Side.THRU for r in self.U.signature): + raise ValueError("Block encoded unitary must have all THRU registers.") + + @cached_property + def signature(self) -> Signature: + return Signature.build_from_dtypes( + system=QAny(self.system_bitsize), + ancilla=QAny(self.ancilla_bitsize), # if ancilla_bitsize is 0, not present + resource=QAny(self.resource_bitsize), # if resource_bitsize is 0, not present + ) + + def pretty_name(self) -> str: + return f"B[{self.U.pretty_name()}]" + + @property + def target_registers(self) -> Tuple[Register, ...]: + return tuple(self.signature.rights()) + + @property + def junk_registers(self) -> Tuple[Register, ...]: + return (self.signature.get_right("resource"),) if self.resource_bitsize > 0 else () + + @property + def selection_registers(self) -> Tuple[Register, ...]: + return (self.signature.get_right("ancilla"),) if self.ancilla_bitsize > 0 else () + + @property + def signal_state(self) -> PrepareOracle: + """This method will be implemented in the future after PrepareOracle is updated for the BlockEncoding interface.""" + raise NotImplementedError + + def build_call_graph(self, ssa: SympySymbolAllocator) -> Set[BloqCountT]: + return {(self.U, 1)} + + def build_composite_bloq( + self, bb: BloqBuilder, system: SoquetT, **soqs: SoquetT + ) -> Dict[str, SoquetT]: + partitions = [(self.signature.get_left("system"), tuple(r.name for r in self.U.signature))] + return { + "system": bb.add_and_partition(self.U, partitions=partitions, system=system), + **soqs, + } + + +@bloq_example +def _unitary_block_encoding() -> Unitary: + from qualtran.bloqs.basic_gates import TGate + + unitary_block_encoding = Unitary(TGate()) + return unitary_block_encoding + + +@bloq_example +def _unitary_block_encoding_override() -> Unitary: + from attrs import evolve + + from qualtran.bloqs.basic_gates import TGate + + unitary_block_encoding_override = evolve( + Unitary(TGate()), alpha=0.5, ancilla_bitsize=2, resource_bitsize=1, epsilon=0.01 + ) + return unitary_block_encoding_override + + +_UNITARY_DOC = BloqDocSpec( + bloq_cls=Unitary, + import_line="from qualtran.bloqs.block_encoding import Unitary", + examples=[_unitary_block_encoding, _unitary_block_encoding_override], +) diff --git a/qualtran/bloqs/block_encoding/unitary_test.py b/qualtran/bloqs/block_encoding/unitary_test.py new file mode 100644 index 0000000000..f2152a0014 --- /dev/null +++ b/qualtran/bloqs/block_encoding/unitary_test.py @@ -0,0 +1,83 @@ +# Copyright 2024 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 cast + +import numpy as np +import pytest + +from qualtran import BloqBuilder, QAny, Register, Signature, Soquet +from qualtran.bloqs.basic_gates import IntState, TGate, ZeroEffect, ZeroState +from qualtran.bloqs.block_encoding.unitary import ( + _unitary_block_encoding, + _unitary_block_encoding_override, + Unitary, +) + + +def test_unitary(bloq_autotester): + bloq_autotester(_unitary_block_encoding) + + +def test_unitary_signature(): + assert _unitary_block_encoding().signature == Signature([Register("system", QAny(1))]) + + assert _unitary_block_encoding_override().signature == Signature( + [Register("system", QAny(1)), Register("ancilla", QAny(2)), Register("resource", QAny(1))] + ) + + with pytest.raises(ValueError): + _ = Unitary(IntState(55, bitsize=8)) + + +def test_unitary_params(): + bloq = _unitary_block_encoding() + assert bloq.alpha == 1 + assert bloq.epsilon == 0 + assert bloq.ancilla_bitsize == 0 + assert bloq.resource_bitsize == 0 + + bloq = _unitary_block_encoding_override() + assert bloq.system_bitsize == 1 + assert bloq.alpha == 0.5 + assert bloq.epsilon == 0.01 + assert bloq.ancilla_bitsize == 2 + assert bloq.resource_bitsize == 1 + + +def test_unitary_tensors(): + from_gate = TGate().tensor_contract() + from_tensors = _unitary_block_encoding().tensor_contract() + np.testing.assert_allclose(from_gate, from_tensors) + + +def test_unitary_override_tensors(): + bb = BloqBuilder() + system = bb.add_register("system", 1) + ancilla = bb.join(np.array([bb.add(ZeroState()), bb.add(ZeroState())])) + resource = bb.add(ZeroState()) + system, ancilla, resource = bb.add_t( + _unitary_block_encoding_override(), + system=system, + ancilla=ancilla, + resource=cast(Soquet, resource), + ) + for q in bb.split(cast(Soquet, ancilla)): + bb.add(ZeroEffect(), q=q) + bb.add(ZeroEffect(), q=resource) + bloq = bb.finalize(system=system) + + from_gate = TGate().tensor_contract() + from_tensors = bloq.tensor_contract() + np.testing.assert_allclose(from_gate, from_tensors) diff --git a/qualtran/bloqs/bookkeeping/partition.py b/qualtran/bloqs/bookkeeping/partition.py index 8a5560eb03..7be28353af 100644 --- a/qualtran/bloqs/bookkeeping/partition.py +++ b/qualtran/bloqs/bookkeeping/partition.py @@ -14,9 +14,8 @@ from functools import cached_property from typing import Any, Dict, Tuple, TYPE_CHECKING -import attrs import numpy as np -from attrs import frozen +from attrs import evolve, field, frozen, validators from qualtran import ( bloq_example, @@ -55,7 +54,9 @@ class Partition(_BookkeepingBloq): """ n: int - regs: Tuple[Register, ...] + regs: Tuple[Register, ...] = field( + converter=lambda x: x if isinstance(x, tuple) else tuple(x), validator=validators.min_len(1) + ) partition: bool = True @cached_property @@ -65,14 +66,14 @@ def signature(self) -> 'Signature': return Signature( [Register('x', QAny(bitsize=self.n), side=lumped)] - + [attrs.evolve(reg, side=partitioned) for reg in self.regs] + + [evolve(reg, side=partitioned) for reg in self.regs] ) def decompose_bloq(self) -> 'CompositeBloq': raise DecomposeTypeError(f'{self} is atomic') def adjoint(self): - return attrs.evolve(self, partition=not self.partition) + return evolve(self, partition=not self.partition) def as_cirq_op(self, qubit_manager, **cirq_quregs) -> Tuple[None, Dict[str, 'CirqQuregT']]: if self.partition: diff --git a/qualtran/conftest.py b/qualtran/conftest.py index 98f360fd0b..429c3e10d0 100644 --- a/qualtran/conftest.py +++ b/qualtran/conftest.py @@ -92,6 +92,11 @@ def assert_bloq_example_serializes_for_pytest(bloq_ex: BloqExample): 'symbolic_hamsim_by_gqsp', 'gqsp_1d_ising', 'auto_partition', + 'unitary_block_encoding', + 'unitary_block_encoding_override', + 'tensor_product_block_encoding', + 'tensor_product_block_encoding_override', + 'tensor_product_block_encoding_symb', ]: pytest.xfail("Skipping serialization test for bloq examples that cannot yet be serialized.") diff --git a/qualtran/serialization/resolver_dict.py b/qualtran/serialization/resolver_dict.py index 6885fc4202..8a039515fc 100644 --- a/qualtran/serialization/resolver_dict.py +++ b/qualtran/serialization/resolver_dict.py @@ -34,6 +34,8 @@ import qualtran.bloqs.basic_gates.z_basis import qualtran.bloqs.block_encoding import qualtran.bloqs.block_encoding.lcu_select_and_prepare +import qualtran.bloqs.block_encoding.tensor_product +import qualtran.bloqs.block_encoding.unitary import qualtran.bloqs.bookkeeping import qualtran.bloqs.chemistry.black_boxes import qualtran.bloqs.chemistry.df.double_factorization @@ -188,6 +190,8 @@ "qualtran.bloqs.block_encoding.lcu_block_encoding.BlackBoxPrepare": qualtran.bloqs.block_encoding.lcu_block_encoding.BlackBoxPrepare, "qualtran.bloqs.block_encoding.lcu_block_encoding.BlackBoxSelect": qualtran.bloqs.block_encoding.lcu_block_encoding.BlackBoxSelect, "qualtran.bloqs.block_encoding.chebyshev_polynomial.ChebyshevPolynomial": qualtran.bloqs.block_encoding.chebyshev_polynomial.ChebyshevPolynomial, + "qualtran.bloqs.block_encoding.unitary.Unitary": qualtran.bloqs.block_encoding.unitary.Unitary, + "qualtran.bloqs.block_encoding.tensor_product.TensorProduct": qualtran.bloqs.block_encoding.tensor_product.TensorProduct, "qualtran.bloqs.bookkeeping.allocate.Allocate": qualtran.bloqs.bookkeeping.allocate.Allocate, "qualtran.bloqs.bookkeeping.arbitrary_clifford.ArbitraryClifford": qualtran.bloqs.bookkeeping.arbitrary_clifford.ArbitraryClifford, "qualtran.bloqs.bookkeeping.auto_partition.AutoPartition": qualtran.bloqs.bookkeeping.auto_partition.AutoPartition, diff --git a/qualtran/symbolics/__init__.py b/qualtran/symbolics/__init__.py index 10ad6bf009..ef0b181f3f 100644 --- a/qualtran/symbolics/__init__.py +++ b/qualtran/symbolics/__init__.py @@ -27,6 +27,7 @@ slen, smax, smin, + ssum, ) from qualtran.symbolics.types import ( HasLength, diff --git a/qualtran/symbolics/math_funcs.py b/qualtran/symbolics/math_funcs.py index a87929eb50..2add3071c4 100644 --- a/qualtran/symbolics/math_funcs.py +++ b/qualtran/symbolics/math_funcs.py @@ -97,6 +97,13 @@ def prod(args: Iterable[SymbolicT]) -> SymbolicT: return ret +def ssum(args: Iterable[SymbolicT]) -> SymbolicT: + ret: SymbolicT = 0 + for arg in args: + ret = ret + arg + return ret + + def acos(x: SymbolicFloat) -> SymbolicFloat: if not isinstance(x, sympy.Basic): return np.arccos(x)