diff --git a/dev_tools/autogenerate-bloqs-notebooks-v2.py b/dev_tools/autogenerate-bloqs-notebooks-v2.py index 8a3408825..36c055b48 100644 --- a/dev_tools/autogenerate-bloqs-notebooks-v2.py +++ b/dev_tools/autogenerate-bloqs-notebooks-v2.py @@ -53,6 +53,7 @@ from qualtran_dev_tools.jupyter_autogen import NotebookSpecV2, render_notebook import qualtran.bloqs.arithmetic.addition +import qualtran.bloqs.arithmetic.permutation import qualtran.bloqs.arithmetic.sorting import qualtran.bloqs.arithmetic.subtraction import qualtran.bloqs.basic_gates.swap @@ -60,6 +61,7 @@ import qualtran.bloqs.block_encoding.chebyshev_polynomial import qualtran.bloqs.block_encoding.lcu_block_encoding import qualtran.bloqs.block_encoding.lcu_select_and_prepare +import qualtran.bloqs.block_encoding.linear_combination import qualtran.bloqs.block_encoding.phase import qualtran.bloqs.bookkeeping import qualtran.bloqs.chemistry.df.double_factorization @@ -379,6 +381,14 @@ qualtran.bloqs.arithmetic.conversions._TO_CONTG_INDX, ], ), + NotebookSpecV2( + title='Permutations', + module=qualtran.bloqs.arithmetic.permutation, + bloq_specs=[ + qualtran.bloqs.arithmetic.permutation._PERMUTATION_DOC, + qualtran.bloqs.arithmetic.permutation._PERMUTATION_CYCLE_DOC, + ], + ), ] MOD_ARITHMETIC = [ @@ -592,6 +602,7 @@ qualtran.bloqs.block_encoding.unitary._UNITARY_DOC, qualtran.bloqs.block_encoding.tensor_product._TENSOR_PRODUCT_DOC, qualtran.bloqs.block_encoding.product._PRODUCT_DOC, + qualtran.bloqs.block_encoding.linear_combination._LINEAR_COMBINATION_DOC, qualtran.bloqs.block_encoding.phase._PHASE_DOC, ], directory=f'{SOURCE_DIR}/bloqs/block_encoding/', diff --git a/docs/bloqs/index.rst b/docs/bloqs/index.rst index a039f2ae2..c7b1cc302 100644 --- a/docs/bloqs/index.rst +++ b/docs/bloqs/index.rst @@ -64,6 +64,7 @@ Bloqs Library arithmetic/comparison.ipynb arithmetic/sorting.ipynb arithmetic/conversions.ipynb + arithmetic/permutation.ipynb .. toctree:: :maxdepth: 2 diff --git a/qualtran/bloqs/arithmetic/bitwise.py b/qualtran/bloqs/arithmetic/bitwise.py new file mode 100644 index 000000000..3f80b0155 --- /dev/null +++ b/qualtran/bloqs/arithmetic/bitwise.py @@ -0,0 +1,120 @@ +# 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 cast, Dict, Optional, Set, Tuple, TYPE_CHECKING + +import numpy as np +from attrs import frozen + +from qualtran import ( + bloq_example, + BloqBuilder, + DecomposeTypeError, + QBit, + QDType, + QUInt, + Register, + Signature, + Soquet, + SoquetT, +) +from qualtran._infra.gate_with_registers import SpecializedSingleQubitControlledGate +from qualtran.bloqs.basic_gates import CNOT, XGate +from qualtran.resource_counting.generalizers import ignore_split_join +from qualtran.symbolics import is_symbolic, SymbolicInt + +if TYPE_CHECKING: + from qualtran.resource_counting import BloqCountT, SympySymbolAllocator + + +def _cvs_converter(vv): + if isinstance(vv, (int, np.integer)): + return (int(vv),) + return tuple(int(v) for v in vv) + + +@frozen +class XorK(SpecializedSingleQubitControlledGate): + r"""Maps |x> to |x \oplus k> for a constant k. + + Args: + dtype: Data type of the input register `x`. + k: The classical integer value to be XOR-ed to x. + control_val: an optional single bit control, apply the operation when + the control qubit equals the `control_val`. + + Registers: + x: A quantum register of type `self.dtype` (see above). + ctrl: A sequence of control qubits (only when `control_val` is not None). + """ + dtype: QDType + k: SymbolicInt + control_val: Optional[int] = None + + @cached_property + def signature(self) -> 'Signature': + return Signature([*self.control_registers, Register('x', self.dtype)]) + + @cached_property + def control_registers(self) -> Tuple[Register, ...]: + if self.control_val is not None: + return (Register('ctrl', QBit()),) + return () + + @cached_property + def bitsize(self) -> SymbolicInt: + return self.dtype.num_qubits + + def is_symbolic(self): + return is_symbolic(self.k, self.dtype) + + def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> Dict[str, 'SoquetT']: + if self.is_symbolic(): + raise DecomposeTypeError(f"cannot decompose symbolic {self}") + + # TODO clean this up once https://github.com/quantumlib/Qualtran/pull/1137 is merged + ctrl = soqs.pop('ctrl', None) + + xs = bb.split(cast(Soquet, soqs.pop('x'))) + + for i, bit in enumerate(self.dtype.to_bits(self.k)): + if bit == 1: + if ctrl is not None: + ctrl, xs[i] = bb.add(CNOT(), ctrl=ctrl, target=xs[i]) + else: + xs[i] = bb.add(XGate(), q=xs[i]) + + soqs['x'] = bb.join(xs) + + if ctrl is not None: + soqs['ctrl'] = ctrl + return soqs + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: + bit_flip_bloq = CNOT() if self.control_val is not None else XGate() + num_flips = self.bitsize if self.is_symbolic() else sum(self.dtype.to_bits(self.k)) + return {(bit_flip_bloq, num_flips)} + + +@bloq_example(generalizer=ignore_split_join) +def _xork() -> XorK: + xork = XorK(QUInt(8), 0b01010111) + return xork + + +@bloq_example(generalizer=ignore_split_join) +def _cxork() -> XorK: + cxork = XorK(QUInt(8), 0b01010111).controlled() + assert isinstance(cxork, XorK) + return cxork diff --git a/qualtran/bloqs/arithmetic/bitwise_test.py b/qualtran/bloqs/arithmetic/bitwise_test.py new file mode 100644 index 000000000..6dbd02473 --- /dev/null +++ b/qualtran/bloqs/arithmetic/bitwise_test.py @@ -0,0 +1,38 @@ +# 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 qualtran import QUInt +from qualtran.bloqs.arithmetic.bitwise import _cxork, _xork, XorK + + +def test_examples(bloq_autotester): + bloq_autotester(_cxork) + bloq_autotester(_xork) + + +def test_xork_classical_sim(): + k = 0b01101010 + bloq = XorK(QUInt(9), k) + cbloq = bloq.controlled() + + for x in bloq.dtype.get_classical_domain(): + (x_out,) = bloq.call_classically(x=x) + assert x_out == x ^ k + + ctrl_out, x_out = cbloq.call_classically(ctrl=0, x=x) + assert ctrl_out == 0 + assert x_out == x + + ctrl_out, x_out = cbloq.call_classically(ctrl=1, x=x) + assert ctrl_out == 1 + assert x_out == x ^ k diff --git a/qualtran/bloqs/arithmetic/comparison.ipynb b/qualtran/bloqs/arithmetic/comparison.ipynb index ecdf62c7a..d8dfa2bb9 100644 --- a/qualtran/bloqs/arithmetic/comparison.ipynb +++ b/qualtran/bloqs/arithmetic/comparison.ipynb @@ -256,7 +256,7 @@ }, "source": [ "## `EqualsAConstant`\n", - "Implements $U_a|x\\rangle = U_a|x\\rangle|z\\rangle = |x\\rangle |z \\land (x = a)\\rangle$\n", + "Implements $U_a|x\\rangle|z\\rangle = |x\\rangle |z \\oplus (x = a)\\rangle$\n", "\n", "The bloq_counts and t_complexity are derived from:\n", "https://qualtran.readthedocs.io/en/latest/bloqs/comparison_gates.html#equality-as-a-special-case\n", diff --git a/qualtran/bloqs/arithmetic/comparison.py b/qualtran/bloqs/arithmetic/comparison.py index 5cd7599e5..0408d0fac 100644 --- a/qualtran/bloqs/arithmetic/comparison.py +++ b/qualtran/bloqs/arithmetic/comparison.py @@ -38,6 +38,7 @@ Bloq, bloq_example, BloqDocSpec, + DecomposeTypeError, GateWithRegisters, QAny, QBit, @@ -48,12 +49,12 @@ Soquet, SoquetT, ) -from qualtran.bloqs.basic_gates import CNOT, TGate, XGate +from qualtran.bloqs.basic_gates import CNOT, XGate from qualtran.bloqs.mcmt.and_bloq import And, MultiAnd -from qualtran.bloqs.mcmt.multi_control_multi_target_pauli import MultiControlX +from qualtran.bloqs.mcmt.multi_control_multi_target_pauli import MultiControlPauli, MultiControlX from qualtran.drawing import WireSymbol from qualtran.drawing.musical_score import Text, TextBox -from qualtran.symbolics import is_symbolic, SymbolicInt +from qualtran.symbolics import HasLength, is_symbolic, SymbolicInt if TYPE_CHECKING: from qualtran import BloqBuilder @@ -926,7 +927,7 @@ def _gt_k() -> GreaterThanConstant: @frozen class EqualsAConstant(Bloq): - r"""Implements $U_a|x\rangle = U_a|x\rangle|z\rangle = |x\rangle |z \land (x = a)\rangle$ + r"""Implements $U_a|x\rangle|z\rangle = |x\rangle |z \oplus (x = a)\rangle$ The bloq_counts and t_complexity are derived from: https://qualtran.readthedocs.io/en/latest/bloqs/comparison_gates.html#equality-as-a-special-case @@ -940,8 +941,8 @@ class EqualsAConstant(Bloq): target: Register to hold result of comparison. """ - bitsize: int - val: int + bitsize: SymbolicInt + val: SymbolicInt @cached_property def signature(self) -> Signature: @@ -956,10 +957,33 @@ def wire_symbol(self, reg: Optional[Register], idx: Tuple[int, ...] = tuple()) - return TextBox(f"⨁(x = {self.val})") raise ValueError(f'Unknown register symbol {reg.name}') + def is_symbolic(self): + return is_symbolic(self.bitsize, self.val) + + @property + def bits_k(self) -> Union[tuple[int, ...], HasLength]: + if self.is_symbolic(): + return HasLength(self.bitsize) + + assert not isinstance(self.bitsize, sympy.Expr) + assert not isinstance(self.val, sympy.Expr) + return tuple(QUInt(self.bitsize).to_bits(self.val)) + + def build_composite_bloq( + self, bb: 'BloqBuilder', x: 'Soquet', target: 'Soquet' + ) -> Dict[str, 'SoquetT']: + if is_symbolic(self.bitsize): + raise DecomposeTypeError(f"Cannot decompose {self} with symbolic {self.bitsize=}") + + xs = bb.split(x) + xs, target = bb.add( + MultiControlPauli(self.bits_k, target_gate=cirq.X), controls=xs, target=target + ) + x = bb.join(xs) + return {'x': x, 'target': target} + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: - # See: https://github.com/quantumlib/Qualtran/issues/219 - # See: https://github.com/quantumlib/Qualtran/issues/217 - return {(TGate(), 4 * (self.bitsize - 1))} + return {(MultiControlPauli(self.bits_k, target_gate=cirq.X), 1)} def _make_equals_a_constant(): diff --git a/qualtran/bloqs/arithmetic/comparison_test.py b/qualtran/bloqs/arithmetic/comparison_test.py index c73bf0930..fa0076ccd 100644 --- a/qualtran/bloqs/arithmetic/comparison_test.py +++ b/qualtran/bloqs/arithmetic/comparison_test.py @@ -298,7 +298,9 @@ def test_equals_a_constant(): qlt_testing.assert_wire_symbols_match_expected( EqualsAConstant(bitsize, 17), ['In(x)', '⨁(x = 17)'] ) - assert t_complexity(EqualsAConstant(bitsize, 17)) == TComplexity(t=4 * (bitsize - 1)) + assert t_complexity(EqualsAConstant(bitsize, 17)) == TComplexity( + t=4 * (bitsize - 1), clifford=65 + ) @pytest.mark.notebook diff --git a/qualtran/bloqs/arithmetic/permutation.ipynb b/qualtran/bloqs/arithmetic/permutation.ipynb new file mode 100644 index 000000000..51b510d88 --- /dev/null +++ b/qualtran/bloqs/arithmetic/permutation.ipynb @@ -0,0 +1,344 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a48878df", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# Permutations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d083d346", + "metadata": { + "cq.autogen": "top_imports" + }, + "outputs": [], + "source": [ + "from qualtran import Bloq, CompositeBloq, BloqBuilder, Signature, Register\n", + "from qualtran import QBit, QInt, QUInt, QAny\n", + "from qualtran.drawing import show_bloq, show_call_graph, show_counts_sigma\n", + "from typing import *\n", + "import numpy as np\n", + "import sympy\n", + "import cirq" + ] + }, + { + "cell_type": "markdown", + "id": "56539662", + "metadata": { + "cq.autogen": "Permutation.bloq_doc.md" + }, + "source": [ + "## `Permutation`\n", + "Apply a permutation of [0, N - 1] on the basis states.\n", + "\n", + "Given a permutation $P : [0, N - 1] \\to [0, N - 1]$, this bloq applies the unitary:\n", + "\n", + "$$\n", + " U|x\\rangle = |P(x)\\rangle\n", + "$$\n", + "\n", + "Decomposes a permutation into cycles and applies them in order.\n", + "See :meth:`from_dense_permutation` to construct this bloq from a permutation,\n", + "and :meth:`from_partial_permutation_map` to construct it from a mapping.\n", + "\n", + "#### Parameters\n", + " - `N`: the total size the permutation acts on.\n", + " - `cycles`: a sequence of permutation cycles that form the permutation. \n", + "\n", + "#### Registers\n", + " - `x`: integer register storing a value in [0, ..., N - 1] \n", + "\n", + "#### References\n", + " - [A simple quantum algorithm to efficiently prepare sparse states](https://arxiv.org/abs/2310.19309v1). Appendix B.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df73124f", + "metadata": { + "cq.autogen": "Permutation.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.arithmetic.permutation import Permutation" + ] + }, + { + "cell_type": "markdown", + "id": "31147b0d", + "metadata": { + "cq.autogen": "Permutation.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec6460b6", + "metadata": { + "cq.autogen": "Permutation.permutation" + }, + "outputs": [], + "source": [ + "permutation = Permutation.from_dense_permutation([1, 3, 0, 2])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0c8e04f0", + "metadata": { + "cq.autogen": "Permutation.permutation_symb" + }, + "outputs": [], + "source": [ + "import sympy\n", + "\n", + "from qualtran.symbolics import Shaped\n", + "\n", + "N, k = sympy.symbols(\"N k\")\n", + "permutation_symb = Permutation(N, Shaped((k,)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8578d036", + "metadata": { + "cq.autogen": "Permutation.permutation_symb_with_cycles" + }, + "outputs": [], + "source": [ + "import sympy\n", + "\n", + "from qualtran.symbolics import Shaped\n", + "\n", + "N = sympy.symbols(\"N\")\n", + "n_cycles = 4\n", + "d = sympy.IndexedBase('d', shape=(n_cycles,))\n", + "permutation_symb_with_cycles = Permutation(N, tuple(Shaped((d[i],)) for i in range(n_cycles)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84b5606a", + "metadata": { + "cq.autogen": "Permutation.sparse_permutation" + }, + "outputs": [], + "source": [ + "sparse_permutation = Permutation.from_partial_permutation_map(\n", + " 16, {0: 1, 1: 3, 2: 8, 3: 15, 4: 12}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "369df9cb", + "metadata": { + "cq.autogen": "Permutation.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25b2dc2b", + "metadata": { + "cq.autogen": "Permutation.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([permutation, permutation_symb, permutation_symb_with_cycles, sparse_permutation],\n", + " ['`permutation`', '`permutation_symb`', '`permutation_symb_with_cycles`', '`sparse_permutation`'])" + ] + }, + { + "cell_type": "markdown", + "id": "ad4321ad", + "metadata": { + "cq.autogen": "Permutation.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3bfbf3f7", + "metadata": { + "cq.autogen": "Permutation.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "permutation_g, permutation_sigma = permutation.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(permutation_g)\n", + "show_counts_sigma(permutation_sigma)" + ] + }, + { + "cell_type": "markdown", + "id": "b3e895db", + "metadata": { + "cq.autogen": "PermutationCycle.bloq_doc.md" + }, + "source": [ + "## `PermutationCycle`\n", + "Apply a single permutation cycle on the basis states.\n", + "\n", + "Given a permutation cycle $C = (v_0 v_2 \\ldots v_{k - 1})$, applies the following unitary:\n", + "\n", + " $$\n", + " U|v_i\\rangle \\mapsto |v_{(i + 1)\\mod k}\\rangle\n", + " $$\n", + "\n", + "for each $i \\in [0, k)$, and\n", + "\n", + " $$\n", + " U|x\\rangle \\mapsto |x\\rangle\n", + " $$\n", + "\n", + "and for every $x \\not\\in C$.\n", + "\n", + "#### Parameters\n", + " - `N`: the total size the permutation acts on.\n", + " - `cycle`: the permutation cycle to apply. \n", + "\n", + "#### Registers\n", + " - `x`: integer register storing a value in [0, ..., N - 1] \n", + "\n", + "#### References\n", + " - [A simple quantum algorithm to efficiently prepare sparse states](https://arxiv.org/abs/2310.19309v1). Appendix B, Algorithm 7.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d569cd2c", + "metadata": { + "cq.autogen": "PermutationCycle.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.arithmetic.permutation import PermutationCycle" + ] + }, + { + "cell_type": "markdown", + "id": "a93c6e89", + "metadata": { + "cq.autogen": "PermutationCycle.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "264ba946", + "metadata": { + "cq.autogen": "PermutationCycle.permutation_cycle" + }, + "outputs": [], + "source": [ + "permutation_cycle = PermutationCycle(4, (0, 1, 2))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78572528", + "metadata": { + "cq.autogen": "PermutationCycle.permutation_cycle_symb" + }, + "outputs": [], + "source": [ + "import sympy\n", + "\n", + "from qualtran.symbolics import Shaped\n", + "\n", + "N, L = sympy.symbols(\"N L\")\n", + "cycle = Shaped((L,))\n", + "permutation_cycle_symb = PermutationCycle(N, cycle)" + ] + }, + { + "cell_type": "markdown", + "id": "87615783", + "metadata": { + "cq.autogen": "PermutationCycle.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6961010", + "metadata": { + "cq.autogen": "PermutationCycle.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([permutation_cycle, permutation_cycle_symb],\n", + " ['`permutation_cycle`', '`permutation_cycle_symb`'])" + ] + }, + { + "cell_type": "markdown", + "id": "16e1c105", + "metadata": { + "cq.autogen": "PermutationCycle.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27628f10", + "metadata": { + "cq.autogen": "PermutationCycle.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "permutation_cycle_g, permutation_cycle_sigma = permutation_cycle.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(permutation_cycle_g)\n", + "show_counts_sigma(permutation_cycle_sigma)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/qualtran/bloqs/arithmetic/permutation.py b/qualtran/bloqs/arithmetic/permutation.py new file mode 100644 index 000000000..8738fab1c --- /dev/null +++ b/qualtran/bloqs/arithmetic/permutation.py @@ -0,0 +1,316 @@ +# 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. +# +# 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 cast, Sequence, Set, TYPE_CHECKING, TypeAlias, Union + +from attrs import field, frozen + +from qualtran import ( + Bloq, + bloq_example, + BloqDocSpec, + BoundedQUInt, + DecomposeTypeError, + QBit, + QUInt, + Signature, + Soquet, +) +from qualtran.bloqs.arithmetic.bitwise import XorK +from qualtran.bloqs.arithmetic.comparison import EqualsAConstant +from qualtran.linalg.permutation import ( + CycleT, + decompose_permutation_into_cycles, + decompose_permutation_map_into_cycles, +) +from qualtran.symbolics import bit_length, is_symbolic, Shaped, slen, SymbolicInt + +if TYPE_CHECKING: + import sympy + + from qualtran import BloqBuilder, SoquetT + from qualtran.resource_counting import BloqCountT, SympySymbolAllocator + +SymbolicCycleT: TypeAlias = Union[CycleT, Shaped] + + +def _convert_cycle(cycle) -> Union[tuple[int, ...], Shaped]: + if isinstance(cycle, Shaped): + return cycle + return tuple(cycle) + + +@frozen +class PermutationCycle(Bloq): + r"""Apply a single permutation cycle on the basis states. + + Given a permutation cycle $C = (v_0 v_2 \ldots v_{k - 1})$, applies the following unitary: + + $$ + U|v_i\rangle \mapsto |v_{(i + 1)\mod k}\rangle + $$ + + for each $i \in [0, k)$, and + + $$ + U|x\rangle \mapsto |x\rangle + $$ + + and for every $x \not\in C$. + + Args: + N: the total size the permutation acts on. + cycle: the permutation cycle to apply. + + Registers: + x: integer register storing a value in [0, ..., N - 1] + + References: + [A simple quantum algorithm to efficiently prepare sparse states](https://arxiv.org/abs/2310.19309v1) + Appendix B, Algorithm 7. + """ + + N: SymbolicInt + cycle: Union[tuple[int, ...], Shaped] = field(converter=_convert_cycle) + + @cached_property + def signature(self) -> Signature: + return Signature.build_from_dtypes(x=BoundedQUInt(self.bitsize, self.N)) + + @cached_property + def bitsize(self): + return bit_length(self.N - 1) + + def is_symbolic(self): + return is_symbolic(self.N, self.cycle) + + def build_composite_bloq(self, bb: 'BloqBuilder', x: 'SoquetT') -> dict[str, 'SoquetT']: + if self.is_symbolic(): + raise DecomposeTypeError(f"cannot decompose symbolic {self}") + assert not isinstance(self.cycle, Shaped) + + a: 'SoquetT' = bb.allocate(dtype=QBit()) + + for k, x_k in enumerate(self.cycle): + x, a = bb.add_t(EqualsAConstant(self.bitsize, x_k), x=x, target=a) + + delta = x_k ^ self.cycle[(k + 1) % len(self.cycle)] + a, x = bb.add_t(XorK(QUInt(self.bitsize), delta).controlled(), ctrl=a, x=x) + + x, a = bb.add_t(EqualsAConstant(self.bitsize, self.cycle[0]), x=x, target=a) + + bb.free(cast(Soquet, a)) + + return {'x': x} + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: + if self.is_symbolic(): + x = ssa.new_symbol('x') + cycle_len = slen(self.cycle) + return { + (EqualsAConstant(self.bitsize, x), cycle_len + 1), + (XorK(QUInt(self.bitsize), x).controlled(), cycle_len), + } + + return super().build_call_graph(ssa) + + +@bloq_example +def _permutation_cycle() -> PermutationCycle: + permutation_cycle = PermutationCycle(4, (0, 1, 2)) + return permutation_cycle + + +@bloq_example +def _permutation_cycle_symb() -> PermutationCycle: + import sympy + + from qualtran.symbolics import Shaped + + N, L = sympy.symbols("N L") + cycle = Shaped((L,)) + permutation_cycle_symb = PermutationCycle(N, cycle) + return permutation_cycle_symb + + +_PERMUTATION_CYCLE_DOC = BloqDocSpec( + bloq_cls=PermutationCycle, + import_line='from qualtran.bloqs.arithmetic.permutation import PermutationCycle', + examples=[_permutation_cycle, _permutation_cycle_symb], +) + + +def _convert_cycles(cycles) -> Union[tuple[SymbolicCycleT, ...], Shaped]: + if isinstance(cycles, Shaped): + return cycles + return tuple(_convert_cycle(cycle) for cycle in cycles) + + +@frozen +class Permutation(Bloq): + r"""Apply a permutation of [0, N - 1] on the basis states. + + Given a permutation $P : [0, N - 1] \to [0, N - 1]$, this bloq applies the unitary: + + $$ + U|x\rangle = |P(x)\rangle + $$ + + Decomposes a permutation into cycles and applies them in order. + See :meth:`from_dense_permutation` to construct this bloq from a permutation, + and :meth:`from_partial_permutation_map` to construct it from a mapping. + + Args: + N: the total size the permutation acts on. + cycles: a sequence of permutation cycles that form the permutation. + + Registers: + x: integer register storing a value in [0, ..., N - 1] + + References: + [A simple quantum algorithm to efficiently prepare sparse states](https://arxiv.org/abs/2310.19309v1) + Appendix B. + """ + + N: SymbolicInt + cycles: Union[tuple[SymbolicCycleT, ...], Shaped] = field(converter=_convert_cycles) + + @cached_property + def signature(self) -> Signature: + return Signature.build_from_dtypes(x=BoundedQUInt(self.bitsize, self.N)) + + @cached_property + def bitsize(self): + return bit_length(self.N - 1) + + def is_symbolic(self): + return is_symbolic(self.N, self.cycles) or ( + isinstance(self.cycles, tuple) and is_symbolic(*self.cycles) + ) + + @classmethod + def from_dense_permutation(cls, permutation: Sequence[int]): + """Construct a permutation bloq from a dense permutation of size `N`. + + Args: + permutation: a sequence of length `N` containing a permutation of `[0, N)`. + """ + N = len(permutation) + cycles = tuple(decompose_permutation_into_cycles(permutation)) + return cls(N, cycles) + + @classmethod + def from_partial_permutation_map(cls, N: int, permutation_map: dict[int, int]): + """Construct a permutation bloq from a (partial) permutation mapping + + Constructs a permuation of `[0, N)` from a partial mapping. Any numbers that + do not occur in `permutation_map` (i.e. as keys or values) are treated as + mapping to themselves. + + Args: + N: the upper limit, i.e. permutation is on range `[0, N)` + permutation_map: a dictionary defining the permutation + """ + cycles = tuple(decompose_permutation_map_into_cycles(permutation_map)) + return cls(N, cycles) + + @classmethod + def from_cycle_lengths(cls, N: SymbolicInt, cycle_lengths: tuple[SymbolicInt, ...]): + """Construct a permutation bloq from a dense permutation of size `N`. + + Args: + N: the upper limit, i.e. permutation is on range `[0, N)` + cycle_lengths: a tuple of lengths of each non-trivial cycle (i.e. length at least 2). + """ + cycles = tuple( + Shaped((cycle_len,)) + for cycle_len in cycle_lengths + if is_symbolic(cycle_len) or cycle_len >= 2 + ) + return cls(N, cycles) + + def build_composite_bloq(self, bb: 'BloqBuilder', x: 'Soquet') -> dict[str, 'SoquetT']: + if self.is_symbolic(): + raise DecomposeTypeError(f"cannot decompose symbolic {self}") + + assert not isinstance(self.cycles, Shaped) + for cycle in self.cycles: + x = bb.add(PermutationCycle(self.N, cycle), x=x) + + return {'x': x} + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> Set['BloqCountT']: + if self.is_symbolic(): + # worst case cost: single cycle of length N + cycle = Shaped((self.N,)) + return {(PermutationCycle(self.N, cycle), 1)} + + return super().build_call_graph(ssa) + + +@bloq_example +def _permutation() -> Permutation: + permutation = Permutation.from_dense_permutation([1, 3, 0, 2]) + return permutation + + +@bloq_example +def _permutation_symb() -> Permutation: + import sympy + + from qualtran.symbolics import Shaped + + N, k = sympy.symbols("N k") + permutation_symb = Permutation(N, Shaped((k,))) + return permutation_symb + + +@bloq_example +def _permutation_symb_with_cycles() -> Permutation: + import sympy + + from qualtran.symbolics import Shaped + + N = sympy.symbols("N") + n_cycles = 4 + d = sympy.IndexedBase('d', shape=(n_cycles,)) + permutation_symb_with_cycles = Permutation(N, tuple(Shaped((d[i],)) for i in range(n_cycles))) + return permutation_symb_with_cycles + + +@bloq_example +def _sparse_permutation() -> Permutation: + sparse_permutation = Permutation.from_partial_permutation_map( + 16, {0: 1, 1: 3, 2: 8, 3: 15, 4: 12} + ) + return sparse_permutation + + +_PERMUTATION_DOC = BloqDocSpec( + bloq_cls=Permutation, + import_line='from qualtran.bloqs.arithmetic.permutation import Permutation', + examples=[_permutation, _permutation_symb, _permutation_symb_with_cycles, _sparse_permutation], +) diff --git a/qualtran/bloqs/arithmetic/permutation_test.py b/qualtran/bloqs/arithmetic/permutation_test.py new file mode 100644 index 000000000..2f0520313 --- /dev/null +++ b/qualtran/bloqs/arithmetic/permutation_test.py @@ -0,0 +1,138 @@ +# 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. +# +# 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 numpy as np +import sympy + +from qualtran import QBit +from qualtran.bloqs.arithmetic.permutation import ( + _permutation, + _permutation_cycle, + _permutation_cycle_symb, + _permutation_symb, + _permutation_symb_with_cycles, + _sparse_permutation, + Permutation, + PermutationCycle, +) +from qualtran.bloqs.basic_gates import CNOT, TGate, XGate +from qualtran.bloqs.bookkeeping import Allocate, ArbitraryClifford, Free +from qualtran.resource_counting.generalizers import ignore_split_join +from qualtran.symbolics import bit_length, slen + + +def test_examples(bloq_autotester): + bloq_autotester(_permutation_cycle) + bloq_autotester(_permutation) + bloq_autotester(_sparse_permutation) + + +def test_symbolic_examples(bloq_autotester): + bloq_autotester(_permutation_cycle_symb) + bloq_autotester(_permutation_symb) + bloq_autotester(_permutation_symb_with_cycles) + + +def test_permutation_cycle_unitary_and_call_graph(): + bloq = PermutationCycle(4, (0, 1, 2)) + + np.testing.assert_allclose( + bloq.tensor_contract(), np.array([[0, 0, 1, 0], [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) + ) + + _, sigma = bloq.call_graph(generalizer=ignore_split_join) + assert sigma == { + CNOT(): 8, + TGate(): 16, + ArbitraryClifford(n=2): 76, + Allocate(QBit()): 1, + Free(QBit()): 1, + } + + +def test_permutation_cycle_symbolic_call_graph(): + bloq = _permutation_cycle_symb() + logN, L = bit_length(bloq.N - 1), slen(bloq.cycle) + + _, sigma = bloq.call_graph() + assert sigma == { + ArbitraryClifford(n=2): (L + 1) * (13 * logN - 13), + TGate(): (L + 1) * (4 * logN - 4), + CNOT(): L * logN + L + 1, + } + + +def test_permutation_unitary_and_call_graph(): + bloq = Permutation.from_dense_permutation([1, 2, 0, 4, 3, 5, 6]) + + np.testing.assert_allclose( + bloq.tensor_contract(), + np.array( + [ + [0, 0, 1, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + ] + ), + ) + + _, sigma = bloq.call_graph(generalizer=ignore_split_join) + assert sigma == { + CNOT(): 17, + TGate(): 56, + XGate(): 56, + ArbitraryClifford(n=2): 182, + Allocate(QBit()): 2, + Free(QBit()): 2, + } + + +def test_sparse_permutation_classical_sim(): + N = 50 + perm_map = {0: 1, 1: 20, 2: 30, 3: 40} + bloq = Permutation.from_partial_permutation_map(N, perm_map) + assert bloq.bitsize == 6 + assert bloq.cycles == ((0, 1, 20), (2, 30), (3, 40)) + + for i, x in perm_map.items(): + assert bloq.call_classically(x=i) == (x,) + + +def test_permutation_symbolic_call_graph(): + N = sympy.Symbol("N") + logN = bit_length(N - 1) + bloq = _permutation_symb() + + _, sigma = bloq.call_graph() + assert sigma == { + ArbitraryClifford(n=2): (N + 1) * (13 * logN - 13), + TGate(): (N + 1) * (4 * logN - 4), + CNOT(): N * logN + N + 1, + } diff --git a/qualtran/bloqs/block_encoding/__init__.py b/qualtran/bloqs/block_encoding/__init__.py index 2e1325119..0418f6c41 100644 --- a/qualtran/bloqs/block_encoding/__init__.py +++ b/qualtran/bloqs/block_encoding/__init__.py @@ -21,6 +21,7 @@ LCUBlockEncoding, LCUBlockEncodingZeroState, ) +from qualtran.bloqs.block_encoding.linear_combination import LinearCombination from qualtran.bloqs.block_encoding.phase import Phase from qualtran.bloqs.block_encoding.product import Product from qualtran.bloqs.block_encoding.tensor_product import TensorProduct diff --git a/qualtran/bloqs/block_encoding/block_encoding.ipynb b/qualtran/bloqs/block_encoding/block_encoding.ipynb index 6ff9d4fe9..8d2a01b2e 100644 --- a/qualtran/bloqs/block_encoding/block_encoding.ipynb +++ b/qualtran/bloqs/block_encoding/block_encoding.ipynb @@ -1175,6 +1175,135 @@ "show_call_graph(phase_block_encoding_g)\n", "show_counts_sigma(phase_block_encoding_sigma)" ] + }, + { + "cell_type": "markdown", + "id": "5f9d88fa", + "metadata": { + "cq.autogen": "LinearCombination.bloq_doc.md" + }, + "source": [ + "## `LinearCombination`\n", + "Linear combination of a sequence of block encodings.\n", + "\n", + "Builds the block encoding $B[\\lambda_1 U_1 + \\lambda_2 U_2 + \\cdots + \\lambda_n U_n]$ given\n", + "block encodings $B[U_1], \\ldots, B[U_n]$ and coefficients $\\lambda_i \\in \\mathbb{R}$.\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[\\lambda_1 U_1 + \\cdots + \\lambda_n U_n]$ is a $(\\alpha, a, \\epsilon)$-block encoding\n", + "of $\\lambda_1 U_1 + \\cdots + \\lambda_n U_n$ where the normalization constant\n", + "$\\alpha = \\sum_i \\lvert\\lambda_i\\rvert\\alpha_i$, number of ancillas\n", + "$a = \\lceil \\log_2 n \\rceil + \\max_i a_i$, and precision\n", + "$\\epsilon = (\\sum_i \\lvert\\lambda_i\\rvert)\\max_i \\epsilon_i$.\n", + "\n", + "Under the hood, this bloq uses LCU Prepare and Select oracles to build the block encoding.\n", + "These oracles will be automatically instantiated if not specified by the user.\n", + "\n", + "#### Parameters\n", + " - `block_encodings`: A sequence of block encodings.\n", + " - `lambd`: Corresponding coefficients.\n", + " - `lambd_bits`: Number of bits needed to represent coefficients precisely.\n", + " - `prepare`: If specified, oracle preparing $\\sum_i \\sqrt{|\\lambda_i|} |i\\rangle$ (state should be normalized and can have junk).\n", + " - `select`: If specified, oracle taking $|i\\rangle|\\psi\\rangle \\mapsto \\text{sgn}(\\lambda_i) |i\\rangle U_i|\\psi\\rangle$. \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": "6e60f66d", + "metadata": { + "cq.autogen": "LinearCombination.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.block_encoding import LinearCombination" + ] + }, + { + "cell_type": "markdown", + "id": "7c3b1b39", + "metadata": { + "cq.autogen": "LinearCombination.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7048adb0", + "metadata": { + "cq.autogen": "LinearCombination.linear_combination_block_encoding" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.basic_gates import Hadamard, TGate, XGate, ZGate\n", + "from qualtran.bloqs.block_encoding.unitary import Unitary\n", + "\n", + "linear_combination_block_encoding = LinearCombination(\n", + " (Unitary(TGate()), Unitary(Hadamard()), Unitary(XGate()), Unitary(ZGate())),\n", + " lambd=(0.25, -0.25, 0.25, -0.25),\n", + " lambd_bits=1,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a3c0e843", + "metadata": { + "cq.autogen": "LinearCombination.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "403264e7", + "metadata": { + "cq.autogen": "LinearCombination.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([linear_combination_block_encoding],\n", + " ['`linear_combination_block_encoding`'])" + ] + }, + { + "cell_type": "markdown", + "id": "a506b326", + "metadata": { + "cq.autogen": "LinearCombination.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9df2892b", + "metadata": { + "cq.autogen": "LinearCombination.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "linear_combination_block_encoding_g, linear_combination_block_encoding_sigma = linear_combination_block_encoding.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(linear_combination_block_encoding_g)\n", + "show_counts_sigma(linear_combination_block_encoding_sigma)" + ] } ], "metadata": { diff --git a/qualtran/bloqs/block_encoding/linear_combination.py b/qualtran/bloqs/block_encoding/linear_combination.py new file mode 100644 index 000000000..2ec187b6f --- /dev/null +++ b/qualtran/bloqs/block_encoding/linear_combination.py @@ -0,0 +1,357 @@ +# 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 cast, Dict, List, Optional, Tuple, Union + +import numpy as np +from attrs import evolve, field, frozen, validators + +from qualtran import ( + bloq_example, + BloqBuilder, + BloqDocSpec, + BoundedQUInt, + QAny, + Register, + Signature, + Soquet, + SoquetT, +) +from qualtran._infra.bloq import DecomposeTypeError +from qualtran.bloqs.block_encoding import BlockEncoding +from qualtran.bloqs.block_encoding.lcu_block_encoding import BlackBoxPrepare, BlackBoxSelect +from qualtran.bloqs.block_encoding.lcu_select_and_prepare import PrepareOracle +from qualtran.bloqs.block_encoding.phase import Phase +from qualtran.bloqs.bookkeeping.auto_partition import AutoPartition, Unused +from qualtran.bloqs.bookkeeping.partition import Partition +from qualtran.linalg.lcu_util import preprocess_probabilities_for_reversible_sampling +from qualtran.symbolics import smax, ssum, SymbolicFloat, SymbolicInt +from qualtran.symbolics.types import is_symbolic + + +@frozen +class LinearCombination(BlockEncoding): + r"""Linear combination of a sequence of block encodings. + + Builds the block encoding $B[\lambda_1 U_1 + \lambda_2 U_2 + \cdots + \lambda_n U_n]$ given + block encodings $B[U_1], \ldots, B[U_n]$ and coefficients $\lambda_i \in \mathbb{R}$. + + When each $B[U_i]$ is a $(\alpha_i, a_i, \epsilon_i)$-block encoding of $U_i$, we have that + $B[\lambda_1 U_1 + \cdots + \lambda_n U_n]$ is a $(\alpha, a, \epsilon)$-block encoding + of $\lambda_1 U_1 + \cdots + \lambda_n U_n$ where the normalization constant + $\alpha = \sum_i \lvert\lambda_i\rvert\alpha_i$, number of ancillas + $a = \lceil \log_2 n \rceil + \max_i a_i$, and precision + $\epsilon = (\sum_i \lvert\lambda_i\rvert)\max_i \epsilon_i$. + + Under the hood, this bloq uses LCU Prepare and Select oracles to build the block encoding. + These oracles will be automatically instantiated if not specified by the user. + + Args: + block_encodings: A sequence of block encodings. + lambd: Corresponding coefficients. + lambd_bits: Number of bits needed to represent coefficients precisely. + prepare: If specified, oracle preparing $\sum_i \sqrt{|\lambda_i|} |i\rangle$ + (state should be normalized and can have junk). + select: If specified, oracle taking + $|i\rangle|\psi\rangle \mapsto \text{sgn}(\lambda_i) |i\rangle U_i|\psi\rangle$. + + 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(2) + ) + _lambd: Tuple[float, ...] = field(converter=lambda x: x if isinstance(x, tuple) else tuple(x)) + lambd_bits: SymbolicInt + + _prepare: Optional[BlackBoxPrepare] = None + _select: Optional[BlackBoxSelect] = None + + def __attrs_post_init__(self): + if len(self._block_encodings) != len(self._lambd): + raise ValueError("Must provide the same number of block encodings and coefficients.") + if sum(abs(x) for x in self._lambd) == 0: + raise ValueError("Coefficients must not sum to zero.") + if not all(be.system_bitsize == self.system_bitsize for be in self._block_encodings): + raise ValueError("All block encodings must have the same dtype.") + if ( + self._prepare is not None or self._select is not None + ) and self.prepare.selection_registers != self.select.selection_registers: + raise ValueError( + "If given, prepare and select oracles must have same selection registers." + ) + if self._select is not None and self._select.target_registers != ( + self.signature.get_left("system"), + ): + raise ValueError( + "If given, select oracle must have block encoding `system` register as target." + ) + + @cached_property + def signed_block_encodings(self): + """Appropriately negated constituent block encodings.""" + return tuple( + Phase(be, phi=1, eps=0) if l < 0 else be + for l, be in zip(self._lambd, self._block_encodings) + ) + + @cached_property + def rescaled_lambd(self): + """Rescaled and padded array of coefficients.""" + x = np.abs(np.array(self._lambd)) + x /= np.linalg.norm(x, ord=1) + x.resize(2 ** int(np.ceil(np.log2(len(x)))), refcheck=False) + return x + + @cached_property + def signature(self) -> Signature: + return Signature.build_from_dtypes( + system=QAny(self.system_bitsize), + ancilla=QAny(self.ancilla_bitsize), + resource=QAny(self.resource_bitsize), + ) + + @cached_property + def system_bitsize(self) -> SymbolicInt: + return self.signed_block_encodings[0].system_bitsize + + def pretty_name(self) -> str: + return f"B[{'+'.join(be.pretty_name()[2:-1] for be in self.signed_block_encodings)}]" + + @cached_property + def alpha(self) -> SymbolicFloat: + return ssum( + abs(l) * be.alpha for be, l in zip(self.signed_block_encodings, self.rescaled_lambd) + ) + + @cached_property + def be_ancilla_bitsize(self) -> SymbolicInt: + return smax(be.ancilla_bitsize for be in self.signed_block_encodings) + + @cached_property + def ancilla_bitsize(self) -> SymbolicInt: + return self.be_ancilla_bitsize + self.prepare.selection_bitsize + + @cached_property + def be_resource_bitsize(self) -> SymbolicInt: + return smax(be.resource_bitsize for be in self.signed_block_encodings) + + @cached_property + def resource_bitsize(self) -> SymbolicInt: + return self.be_resource_bitsize + self.prepare.junk_bitsize + + @cached_property + def epsilon(self) -> SymbolicFloat: + return ssum(abs(l) for l in self.rescaled_lambd) * smax( + be.epsilon for be in self.signed_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"),) + + @property + def selection_registers(self) -> Tuple[Register, ...]: + return (self.signature.get_right("ancilla"),) + + @property + def signal_state(self) -> PrepareOracle: + # This method will be implemented in the future after PrepareOracle + # is updated for the BlockEncoding interface. + # GitHub issue: https://github.com/quantumlib/Qualtran/issues/1104 + raise NotImplementedError + + @cached_property + def prepare(self) -> BlackBoxPrepare: + if self._prepare is not None: + return self._prepare + if is_symbolic(self.lambd_bits): + raise DecomposeTypeError(f"Cannot decompose symbolic {self=}") + + alt, keep, mu = preprocess_probabilities_for_reversible_sampling( + unnormalized_probabilities=tuple(self.rescaled_lambd), + sub_bit_precision=cast(int, self.lambd_bits), + ) + N = len(self.rescaled_lambd) + + # import here to avoid circular dependency of StatePreparationAliasSampling + # on PrepareOracle in qualtran.bloq.block_encoding + from qualtran.bloqs.state_preparation.state_preparation_alias_sampling import ( + StatePreparationAliasSampling, + ) + + # disable spurious pylint + # pylint: disable=abstract-class-instantiated + prep = StatePreparationAliasSampling( + selection_registers=Register('selection', BoundedQUInt((N - 1).bit_length(), N)), + alt=np.array(alt), + keep=np.array(keep), + mu=mu, + sum_of_unnormalized_probabilities=1, + ) + return BlackBoxPrepare(prep) + + @cached_property + def select(self) -> BlackBoxSelect: + if self._select is not None: + return self._select + 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=}") + assert isinstance(self.be_ancilla_bitsize, int) + assert isinstance(self.be_resource_bitsize, int) + + # make all bloqs have same ancilla and resource registers + bloqs = [] + for be in self.signed_block_encodings: + assert isinstance(be.ancilla_bitsize, int) + assert isinstance(be.resource_bitsize, int) + + partitions: List[Tuple[Register, List[Union[str, Unused]]]] = [ + (Register("system", QAny(self.system_bitsize)), ["system"]) + ] + if self.be_ancilla_bitsize > 0: + regs: List[Union[str, Unused]] = [] + if be.ancilla_bitsize > 0: + regs.append("ancilla") + if self.be_ancilla_bitsize > be.ancilla_bitsize: + regs.append(Unused(self.be_ancilla_bitsize - be.ancilla_bitsize)) + partitions.append((Register("ancilla", QAny(self.be_ancilla_bitsize)), regs)) + if self.be_resource_bitsize > 0: + regs = [] + if be.resource_bitsize > 0: + regs.append("resource") + if self.be_resource_bitsize > be.resource_bitsize: + regs.append(Unused(self.be_resource_bitsize - be.resource_bitsize)) + partitions.append((Register("resource", QAny(self.be_resource_bitsize)), regs)) + bloqs.append(AutoPartition(be, partitions, left_only=False)) + + # import here to avoid circular dependency of ApplyLthBloq + # on SelectOracle in qualtran.bloqs.block_encoding + from qualtran.bloqs.multiplexers.apply_lth_bloq import ApplyLthBloq + + return BlackBoxSelect(ApplyLthBloq(np.array(bloqs))) + + def build_composite_bloq( + self, bb: BloqBuilder, system: Soquet, ancilla: Soquet, **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=}") + assert isinstance(self.be_ancilla_bitsize, int) + assert isinstance(self.ancilla_bitsize, int) + assert isinstance(self.be_resource_bitsize, int) + assert isinstance(self.resource_bitsize, int) + + # partition ancilla register + be_system_soqs: Dict[str, SoquetT] = {"system": system} + anc_regs = [Register("selection", QAny(self.prepare.selection_bitsize))] + if self.be_ancilla_bitsize > 0: + anc_regs.append(Register("ancilla", QAny(self.be_ancilla_bitsize))) + anc_part = Partition(cast(int, self.ancilla_bitsize), tuple(anc_regs)) + anc_soqs = bb.add_d(anc_part, x=ancilla) + if self.be_ancilla_bitsize > 0: + be_system_soqs["ancilla"] = anc_soqs.pop("ancilla") + prepare_in_soqs = {"selection": anc_soqs.pop("selection")} + + # partition resource register if necessary + if self.resource_bitsize > 0: + res_regs = [] + if self.be_resource_bitsize > 0: + res_regs.append(Register("resource", QAny(self.be_resource_bitsize))) + if self.prepare.junk_bitsize > 0: + res_regs.append(Register("prepare_junk", QAny(self.prepare.junk_bitsize))) + res_part = Partition(cast(int, self.resource_bitsize), tuple(res_regs)) + res_soqs = bb.add_d(res_part, x=soqs.pop("resource")) + if self.be_resource_bitsize > 0: + be_system_soqs["resource"] = res_soqs.pop("resource") + if self.prepare.junk_bitsize > 0: + prepare_in_soqs["junk"] = res_soqs.pop("prepare_junk") + + # merge system, ancilla, resource of block encoding into system register of Select oracle + be_regs = [Register("system", QAny(self.system_bitsize))] + if self.be_ancilla_bitsize > 0: + be_regs.append(Register("ancilla", QAny(self.be_ancilla_bitsize))) + if self.be_resource_bitsize > 0: + be_regs.append(Register("resource", QAny(self.be_resource_bitsize))) + be_part = Partition(cast(int, self.select.system_bitsize), tuple(be_regs)) + + prepare_soqs = bb.add_d(self.prepare, **prepare_in_soqs) + select_out_soqs = bb.add_d( + self.select, + selection=prepare_soqs.pop("selection"), + system=cast(Soquet, bb.add(evolve(be_part, partition=False), **be_system_soqs)), + ) + prep_adj_soqs = bb.add_d( + self.prepare.adjoint(), selection=select_out_soqs.pop("selection"), **prepare_soqs + ) + + # partition system register of Select into system, ancilla, resource of block encoding + be_soqs = bb.add_d(be_part, x=select_out_soqs.pop("system")) + out: Dict[str, SoquetT] = {"system": be_soqs.pop("system")} + + # merge ancilla registers of block encoding and Prepare oracle + anc_soqs = {"selection": prep_adj_soqs.pop("selection")} + if self.be_ancilla_bitsize > 0: + anc_soqs["ancilla"] = be_soqs.pop("ancilla") + out["ancilla"] = cast(Soquet, bb.add(evolve(anc_part, partition=False), **anc_soqs)) + + # merge resource registers of block encoding and Prepare oracle + if self.resource_bitsize > 0: + res_soqs = dict() + if self.be_resource_bitsize > 0: + res_soqs["resource"] = be_soqs.pop("resource") + if self.prepare.junk_bitsize > 0: + res_soqs["prepare_junk"] = prep_adj_soqs.pop("junk") + out["resource"] = cast(Soquet, bb.add(evolve(res_part, partition=False), **res_soqs)) + + return out + + +@bloq_example +def _linear_combination_block_encoding() -> LinearCombination: + from qualtran.bloqs.basic_gates import Hadamard, TGate, XGate, ZGate + from qualtran.bloqs.block_encoding.unitary import Unitary + + linear_combination_block_encoding = LinearCombination( + (Unitary(TGate()), Unitary(Hadamard()), Unitary(XGate()), Unitary(ZGate())), + lambd=(0.25, -0.25, 0.25, -0.25), + lambd_bits=1, + ) + return linear_combination_block_encoding + + +_LINEAR_COMBINATION_DOC = BloqDocSpec( + bloq_cls=LinearCombination, + import_line="from qualtran.bloqs.block_encoding import LinearCombination", + examples=[_linear_combination_block_encoding], +) diff --git a/qualtran/bloqs/block_encoding/linear_combination_test.py b/qualtran/bloqs/block_encoding/linear_combination_test.py new file mode 100644 index 000000000..0e80ac62f --- /dev/null +++ b/qualtran/bloqs/block_encoding/linear_combination_test.py @@ -0,0 +1,183 @@ +# 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 attrs import evolve + +from qualtran import BloqBuilder, QAny, Register, Signature, Soquet +from qualtran.bloqs.basic_gates import ( + CNOT, + Hadamard, + IntEffect, + IntState, + Ry, + Swap, + TGate, + XGate, + ZGate, +) +from qualtran.bloqs.block_encoding.linear_combination import ( + _linear_combination_block_encoding, + LinearCombination, +) +from qualtran.bloqs.block_encoding.unitary import Unitary + + +def test_linear_combination(bloq_autotester): + bloq_autotester(_linear_combination_block_encoding) + + +def test_linear_combination_signature(): + assert _linear_combination_block_encoding().signature == Signature( + [Register("system", QAny(1)), Register("ancilla", QAny(2)), Register("resource", QAny(5))] + ) + + +def test_linear_combination_checks(): + with pytest.raises(ValueError): + _ = LinearCombination((), (), lambd_bits=1) + with pytest.raises(ValueError): + _ = LinearCombination((Unitary(TGate()),), (), lambd_bits=1) + with pytest.raises(ValueError): + _ = LinearCombination((Unitary(TGate()),), (1.0,), lambd_bits=1) + with pytest.raises(ValueError): + _ = LinearCombination((Unitary(TGate()), Unitary(CNOT())), (1.0,), lambd_bits=1) + with pytest.raises(ValueError): + _ = LinearCombination((Unitary(TGate()), Unitary(Hadamard())), (0.0, 0.0), lambd_bits=1) + + +def test_linear_combination_params(): + u1 = evolve(Unitary(TGate()), alpha=0.5, ancilla_bitsize=2, resource_bitsize=1, epsilon=0.01) + u2 = evolve(Unitary(Hadamard()), alpha=0.5, ancilla_bitsize=1, resource_bitsize=1, epsilon=0.1) + bloq = LinearCombination((u1, u2), (0.5, 0.5), lambd_bits=1) + assert bloq.system_bitsize == 1 + assert bloq.alpha == 0.5 * 0.5 + 0.5 * 0.5 + assert bloq.epsilon == (0.5 + 0.5) * max(0.01, 0.1) + assert bloq.ancilla_bitsize == 1 + max(1, 2) + assert bloq.resource_bitsize == max(1, 1) + 4 # dependent on state preparation + + +def get_tensors(bloq): + bb = BloqBuilder() + system = bb.add_register("system", cast(int, bloq.system_bitsize)) + ancilla = cast(Soquet, bb.add(IntState(0, bloq.ancilla_bitsize))) + resource = cast(Soquet, bb.add(IntState(0, bloq.resource_bitsize))) + system, ancilla, resource = bb.add_t(bloq, system=system, ancilla=ancilla, resource=resource) + bb.add(IntEffect(0, cast(int, bloq.ancilla_bitsize)), val=ancilla) + bb.add(IntEffect(0, cast(int, bloq.resource_bitsize)), val=resource) + bloq = bb.finalize(system=system) + return bloq.tensor_contract() + + +def test_linear_combination_tensors(): + bloq = _linear_combination_block_encoding() + from_gate = ( + 0.25 * TGate().tensor_contract() + + -0.25 * Hadamard().tensor_contract() + + 0.25 * XGate().tensor_contract() + + -0.25 * ZGate().tensor_contract() + ) + from_tensors = get_tensors(bloq) + np.testing.assert_allclose(from_gate, from_tensors) + + +def run_gate_test(gates, lambd, lambd_bits=1, atol=1e-07): + bloq = LinearCombination(tuple(Unitary(g) for g in gates), lambd, lambd_bits) + lambd = np.array(lambd) / np.linalg.norm(lambd, ord=1) + from_gate = sum(l * g.tensor_contract() for l, g in zip(lambd, gates)) + from_tensors = get_tensors(bloq) + np.testing.assert_allclose(from_gate, from_tensors, atol=atol) + + +# all coefficients are multiples of small negative powers of 2 after normalization +exact2 = [[0.0, 1.0], [1 / 3, 1 / 3], [0.5, 0.5], [0.25, 0.25], [2.0, 6.0], [1.0, 0.0]] +exact3 = [ + [0.0, 0.0, 1.0], + [1.0, -0.5, 0.5], + [1 / 4, 1 / 4, -1 / 2], + [1 / 2, 1 / 4, 1 / 4], + [3 / 16, -1 / 4, 1 / 16], + [-1.0, 0.0, 0.0], +] +exact5 = [ + [1.0, 0.0, 0.0, 0.0, 0.0], + [9 / 16, -1 / 16, 1 / 8, -1 / 8, 1 / 8], + [1 / 4, 1 / 8, -1 / 16, 1 / 32, -1 / 32], + [0.0, 0.0, 0.0, 0.0, 1.0], + [1.0, 0.5, 0.5, -1.0, 1.0], +] + + +@pytest.mark.parametrize('lambd', exact2) +def test_linear_combination2(lambd): + run_gate_test([TGate(), Hadamard()], lambd) + + +@pytest.mark.parametrize('lambd', exact2) +def test_linear_combination_twogate(lambd): + run_gate_test([CNOT(), Hadamard().controlled()], lambd) + + +@pytest.mark.parametrize('lambd', exact3) +def test_linear_combination3(lambd): + run_gate_test([TGate(), Hadamard(), XGate()], lambd) + + +@pytest.mark.parametrize('lambd', exact5) +def test_linear_combination5(lambd): + run_gate_test([TGate(), Hadamard(), XGate(), ZGate(), Ry(angle=np.pi / 4.0)], lambd) + + +# coefficients are not multiples of small negative powers of 2 after normalization +approx2 = [ + [1 / 3, 2 / 3], + [2 / 3, 1 / 3], + [1 / 2, -1 / 4], + [1 / 8, 1 / 2], + [-1 / 3, 1 / 2], + [1 / 3, 1 / 5], + [1.0, -7.0], + [3.7, 2.1], + [1.0, 0.0], +] +approx5 = [ + [1.0, 0.0, 0.0, 1.0, 0.0], + [1.0, 1.0, 1.0, -1.0, 1.0], + [1.5, 2.4, -3.3, 4.2, 5.1], + [1 / 7, -2 / 7, 3 / 7, 4 / 7, -5 / 7], +] + + +@pytest.mark.parametrize('lambd', approx2) +def test_linear_combination_approx2(lambd): + run_gate_test([TGate(), Hadamard()], lambd, lambd_bits=9, atol=0.001) + + +@pytest.mark.parametrize('lambd', approx5) +def test_linear_combination_approx5(lambd): + run_gate_test( + [ + TGate().controlled(), + Hadamard().controlled(), + CNOT(), + Swap(1), + Ry(angle=np.pi / 4.0).controlled(), + ], + lambd, + lambd_bits=9, + atol=0.001, + ) diff --git a/qualtran/bloqs/chemistry/trotter/hubbard/hopping.py b/qualtran/bloqs/chemistry/trotter/hubbard/hopping.py index b54672b4a..8db597c8d 100644 --- a/qualtran/bloqs/chemistry/trotter/hubbard/hopping.py +++ b/qualtran/bloqs/chemistry/trotter/hubbard/hopping.py @@ -13,7 +13,7 @@ # limitations under the License. """Bloqs implementing unitary evolution under the one-body hopping Hamiltonian in 2D.""" from functools import cached_property -from typing import Set, TYPE_CHECKING, Union +from typing import Set, TYPE_CHECKING from attrs import frozen @@ -67,8 +67,8 @@ class HoppingPlaquette(Bloq): page 13 Eq. E4 and E5 (Appendix E) """ - kappa: Union[SymbolicFloat] - eps: Union[SymbolicFloat] = 1e-9 + kappa: SymbolicFloat + eps: SymbolicFloat = 1e-9 @cached_property def signature(self) -> Signature: @@ -110,10 +110,10 @@ class HoppingTile(Bloq): see Eq. 21 and App E. """ - length: Union[SymbolicInt] - angle: Union[SymbolicFloat] + length: SymbolicInt + angle: SymbolicFloat tau: float = 1.0 - eps: Union[SymbolicFloat] = 1e-9 + eps: SymbolicFloat = 1e-9 pink: bool = True def __attrs_post_init__(self): diff --git a/qualtran/bloqs/chemistry/trotter/hubbard/interaction.py b/qualtran/bloqs/chemistry/trotter/hubbard/interaction.py index 3f33f6b60..a910cedcd 100644 --- a/qualtran/bloqs/chemistry/trotter/hubbard/interaction.py +++ b/qualtran/bloqs/chemistry/trotter/hubbard/interaction.py @@ -14,7 +14,7 @@ r"""Bloqs implementing unitary evolution under the interacting part of the Hubbard Hamiltonian.""" from functools import cached_property -from typing import Set, TYPE_CHECKING, Union +from typing import Set, TYPE_CHECKING from attrs import frozen @@ -53,10 +53,10 @@ class Interaction(Bloq): Eq. 6 page 2 and page 13 paragraph 1. """ - length: Union[SymbolicInt] - angle: Union[SymbolicFloat] - hubb_u: Union[SymbolicFloat] - eps: Union[SymbolicFloat] = 1e-9 + length: SymbolicInt + angle: SymbolicFloat + hubb_u: SymbolicFloat + eps: SymbolicFloat = 1e-9 @cached_property def signature(self) -> Signature: @@ -96,10 +96,10 @@ class InteractionHWP(Bloq): 14 paragraph 3 right column. The apply 2 batches of $L^2/2$ rotations. """ - length: Union[SymbolicInt] - angle: Union[SymbolicFloat] - hubb_u: Union[SymbolicFloat] - eps: Union[SymbolicFloat] = 1e-9 + length: SymbolicInt + angle: SymbolicFloat + hubb_u: SymbolicFloat + eps: SymbolicFloat = 1e-9 @cached_property def signature(self) -> Signature: diff --git a/qualtran/bloqs/chemistry/trotter/trotterized_unitary.py b/qualtran/bloqs/chemistry/trotter/trotterized_unitary.py index 78624a29a..91dd0736a 100644 --- a/qualtran/bloqs/chemistry/trotter/trotterized_unitary.py +++ b/qualtran/bloqs/chemistry/trotter/trotterized_unitary.py @@ -14,7 +14,7 @@ """Bloq for building a Trotterized unitary""" from functools import cached_property -from typing import Dict, Sequence, Union +from typing import Dict, Sequence import attrs @@ -83,8 +83,8 @@ class TrotterizedUnitary(Bloq): bloqs: Sequence[Bloq] indices: Sequence[int] - coeffs: Sequence[Union[SymbolicFloat]] - timestep: Union[SymbolicFloat] + coeffs: Sequence[SymbolicFloat] + timestep: SymbolicFloat def __attrs_post_init__(self): ref_sig = self.bloqs[0].signature diff --git a/qualtran/bloqs/mcmt/and_bloq.py b/qualtran/bloqs/mcmt/and_bloq.py index 1f9cbb572..2836ae48e 100644 --- a/qualtran/bloqs/mcmt/and_bloq.py +++ b/qualtran/bloqs/mcmt/and_bloq.py @@ -260,7 +260,7 @@ class MultiAnd(Bloq): @cvs.validator def _validate_cvs(self, field, val): if not is_symbolic(val) and len(val) < 3: - raise ValueError("MultiAnd must cvshave at least 3 control values `cvs`.") + raise ValueError("MultiAnd must have at least 3 control values `cvs`.") @property def n_ctrls(self) -> SymbolicInt: diff --git a/qualtran/bloqs/multiplexers/apply_lth_bloq.py b/qualtran/bloqs/multiplexers/apply_lth_bloq.py index 852c28534..608e858e8 100644 --- a/qualtran/bloqs/multiplexers/apply_lth_bloq.py +++ b/qualtran/bloqs/multiplexers/apply_lth_bloq.py @@ -22,13 +22,14 @@ from qualtran import Bloq, bloq_example, BloqDocSpec, BoundedQUInt, QBit, Register, Side from qualtran._infra.gate_with_registers import merge_qubits, SpecializedSingleQubitControlledGate +from qualtran.bloqs.block_encoding.lcu_select_and_prepare import SelectOracle from qualtran.bloqs.multiplexers.unary_iteration_bloq import UnaryIterationGate from qualtran.resource_counting import BloqCountT from qualtran.symbolics import ceil, log2 @frozen -class ApplyLthBloq(UnaryIterationGate, SpecializedSingleQubitControlledGate): # type: ignore[misc] +class ApplyLthBloq(UnaryIterationGate, SpecializedSingleQubitControlledGate, SelectOracle): # type: ignore[misc] r"""A SELECT operation that executes one of a list of bloqs $U_l$ based on a quantum index: $$ diff --git a/qualtran/bloqs/rotations/hamming_weight_phasing.py b/qualtran/bloqs/rotations/hamming_weight_phasing.py index 84b863389..bd59b3ecc 100644 --- a/qualtran/bloqs/rotations/hamming_weight_phasing.py +++ b/qualtran/bloqs/rotations/hamming_weight_phasing.py @@ -13,7 +13,7 @@ # limitations under the License. from functools import cached_property -from typing import Dict, Set, TYPE_CHECKING, Union +from typing import Dict, Set, TYPE_CHECKING import attrs import numpy as np @@ -73,7 +73,7 @@ class HammingWeightPhasing(GateWithRegisters): bitsize: int exponent: float = 1 - eps: Union[SymbolicFloat] = 1e-10 + eps: SymbolicFloat = 1e-10 @cached_property def signature(self) -> 'Signature': diff --git a/qualtran/bloqs/rotations/quantum_variable_rotation.py b/qualtran/bloqs/rotations/quantum_variable_rotation.py index 5fb3dc24d..1559b7e94 100644 --- a/qualtran/bloqs/rotations/quantum_variable_rotation.py +++ b/qualtran/bloqs/rotations/quantum_variable_rotation.py @@ -56,7 +56,7 @@ import abc from functools import cached_property -from typing import cast, Dict, Sequence, Set, TYPE_CHECKING, Union +from typing import cast, Dict, Sequence, Set, TYPE_CHECKING import attrs import numpy as np @@ -146,12 +146,12 @@ class QvrZPow(QvrInterface): """ cost_reg: Register - gamma: Union[SymbolicFloat] = 1.0 - eps: Union[SymbolicFloat] = 1e-9 + gamma: SymbolicFloat = 1.0 + eps: SymbolicFloat = 1e-9 @classmethod def from_bitsize( - cls, bitsize: int, gamma: Union[SymbolicFloat] = 1.0, eps: Union[SymbolicFloat] = 1e-9 + cls, bitsize: int, gamma: SymbolicFloat = 1.0, eps: SymbolicFloat = 1e-9 ) -> 'QvrZPow': cost_reg = Register("x", QFxp(bitsize, bitsize, signed=False)) return QvrZPow(cost_reg, gamma=gamma, eps=eps) @@ -379,8 +379,8 @@ class QvrPhaseGradient(QvrInterface): """ cost_reg: Register - gamma: Union[SymbolicFloat] = 1.0 - eps: Union[SymbolicFloat] = 1e-9 + gamma: SymbolicFloat = 1.0 + eps: SymbolicFloat = 1e-9 def __attrs_post_init__(self): dtype = self.cost_reg.dtype @@ -389,7 +389,7 @@ def __attrs_post_init__(self): @classmethod def from_bitsize( - cls, bitsize: int, gamma: Union[SymbolicFloat] = 1.0, eps: Union[SymbolicFloat] = 1e-9 + cls, bitsize: int, gamma: SymbolicFloat = 1.0, eps: SymbolicFloat = 1e-9 ) -> 'QvrPhaseGradient': cost_reg = Register("x", QFxp(bitsize, bitsize, signed=False)) return QvrPhaseGradient(cost_reg, gamma=gamma, eps=eps) diff --git a/qualtran/conftest.py b/qualtran/conftest.py index 66a1933ed..f631b83b9 100644 --- a/qualtran/conftest.py +++ b/qualtran/conftest.py @@ -101,8 +101,11 @@ def assert_bloq_example_serializes_for_pytest(bloq_ex: BloqExample): 'product_block_encoding_properties', 'product_block_encoding_symb', 'apply_lth_bloq', + 'linear_combination_block_encoding', 'phase_block_encoding', 'sparse_state_prep_alias_symb', # cannot serialize Shaped + 'sparse_permutation', + 'permutation_cycle_symb', ]: pytest.xfail("Skipping serialization test for bloq examples that cannot yet be serialized.") diff --git a/qualtran/linalg/permutation.py b/qualtran/linalg/permutation.py new file mode 100644 index 000000000..b09681cc0 --- /dev/null +++ b/qualtran/linalg/permutation.py @@ -0,0 +1,58 @@ +# 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 Iterator, Sequence, TypeAlias + +CycleT: TypeAlias = tuple[int, ...] + + +def decompose_permutation_into_cycles(permutation: Sequence[int]) -> Iterator[CycleT]: + """Generate all non-trivial (more than one element) cycles of a permutation of [0, ..., N - 1]""" + import networkx as nx + + G = nx.DiGraph(enumerate(permutation)) + for cycle in nx.simple_cycles(G): + if len(cycle) >= 2: + yield tuple(cycle) + + +def decompose_permutation_map_into_cycles(permutation_map: dict[int, int]) -> Iterator[CycleT]: + r"""Given a (partial) permutation map, return non-trivial cycles requiring minimum swaps. + + We are given a partial permutation on $N$ as a python dictionary. This procedure generates a + sequence of cycles such that the number of swaps required to implement this partial mapping + is minimized. + + >>> list(decompose_permutation_map_into_cycles({0: 1, 1: 5, 5: 0, 2: 6, 3: 3}, N=10)) + [(0, 1, 5), (2, 6)] + + Args: + permutation_map: a (partial) map defining the permutation. + """ + seen = set() + + for i in permutation_map: + if i in seen: + continue + + # compute the cycle starting at `i` + cycle = [] + while True: + seen.add(i) + cycle.append(i) + if i not in permutation_map: + break + i = permutation_map[i] + + if len(cycle) >= 2: + yield tuple(cycle) diff --git a/qualtran/linalg/permutation_test.py b/qualtran/linalg/permutation_test.py new file mode 100644 index 000000000..04c172eeb --- /dev/null +++ b/qualtran/linalg/permutation_test.py @@ -0,0 +1,28 @@ +# 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 qualtran.linalg.permutation import ( + decompose_permutation_into_cycles, + decompose_permutation_map_into_cycles, +) + + +def test_decompose_permutation_into_cycles(): + assert list(decompose_permutation_into_cycles([0, 1, 2])) == [] + assert list(decompose_permutation_into_cycles([1, 2, 0])) == [(0, 1, 2)] + assert sorted(decompose_permutation_into_cycles([1, 0, 2, 4, 5, 3])) == [(0, 1), (3, 4, 5)] + + +def test_decompose_sparse_prefix_permutation_into_cycles(): + assert list(decompose_permutation_map_into_cycles({0: 1, 1: 20})) == [(0, 1, 20)] + assert sorted(decompose_permutation_map_into_cycles({0: 30, 1: 50})) == [(0, 30), (1, 50)] diff --git a/qualtran/serialization/resolver_dict.py b/qualtran/serialization/resolver_dict.py index d21959cca..2107d4baa 100644 --- a/qualtran/serialization/resolver_dict.py +++ b/qualtran/serialization/resolver_dict.py @@ -15,10 +15,12 @@ from typing import Type import qualtran.bloqs.arithmetic.addition +import qualtran.bloqs.arithmetic.bitwise import qualtran.bloqs.arithmetic.comparison import qualtran.bloqs.arithmetic.conversions import qualtran.bloqs.arithmetic.hamming_weight import qualtran.bloqs.arithmetic.multiplication +import qualtran.bloqs.arithmetic.permutation import qualtran.bloqs.arithmetic.sorting import qualtran.bloqs.basic_gates.cnot import qualtran.bloqs.basic_gates.hadamard @@ -34,6 +36,7 @@ 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.linear_combination import qualtran.bloqs.block_encoding.phase import qualtran.bloqs.block_encoding.product import qualtran.bloqs.block_encoding.tensor_product @@ -132,6 +135,7 @@ "qualtran.bloqs.arithmetic.addition.Add": qualtran.bloqs.arithmetic.addition.Add, "qualtran.bloqs.arithmetic.addition.OutOfPlaceAdder": qualtran.bloqs.arithmetic.addition.OutOfPlaceAdder, "qualtran.bloqs.arithmetic.addition.AddK": qualtran.bloqs.arithmetic.AddK, + "qualtran.bloqs.arithmetic.bitwise.XorK": qualtran.bloqs.arithmetic.bitwise.XorK, "qualtran.bloqs.arithmetic.comparison.BiQubitsMixer": qualtran.bloqs.arithmetic.comparison.BiQubitsMixer, "qualtran.bloqs.arithmetic.comparison.EqualsAConstant": qualtran.bloqs.arithmetic.comparison.EqualsAConstant, "qualtran.bloqs.arithmetic.comparison.GreaterThan": qualtran.bloqs.arithmetic.comparison.GreaterThan, @@ -150,6 +154,8 @@ "qualtran.bloqs.arithmetic.multiplication.Square": qualtran.bloqs.arithmetic.multiplication.Square, "qualtran.bloqs.arithmetic.multiplication.SquareRealNumber": qualtran.bloqs.arithmetic.multiplication.SquareRealNumber, "qualtran.bloqs.arithmetic.multiplication.SumOfSquares": qualtran.bloqs.arithmetic.multiplication.SumOfSquares, + "qualtran.bloqs.arithmetic.permutation.Permutation": qualtran.bloqs.arithmetic.permutation.Permutation, + "qualtran.bloqs.arithmetic.permutation.PermutationCycle": qualtran.bloqs.arithmetic.permutation.PermutationCycle, "qualtran.bloqs.arithmetic.sorting.BitonicMerge": qualtran.bloqs.arithmetic.sorting.BitonicMerge, "qualtran.bloqs.arithmetic.sorting.BitonicSort": qualtran.bloqs.arithmetic.sorting.BitonicSort, "qualtran.bloqs.arithmetic.sorting.Comparator": qualtran.bloqs.arithmetic.sorting.Comparator, @@ -196,6 +202,7 @@ "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.block_encoding.product.Product": qualtran.bloqs.block_encoding.product.Product, + "qualtran.bloqs.block_encoding.linear_combination.LinearCombination": qualtran.bloqs.block_encoding.linear_combination.LinearCombination, "qualtran.bloqs.block_encoding.phase.phase": qualtran.bloqs.block_encoding.phase.Phase, "qualtran.bloqs.bookkeeping.allocate.Allocate": qualtran.bloqs.bookkeeping.allocate.Allocate, "qualtran.bloqs.bookkeeping.arbitrary_clifford.ArbitraryClifford": qualtran.bloqs.bookkeeping.arbitrary_clifford.ArbitraryClifford,