diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py index 292f56869..79e61306d 100644 --- a/dev_tools/qualtran_dev_tools/notebook_specs.py +++ b/dev_tools/qualtran_dev_tools/notebook_specs.py @@ -37,6 +37,7 @@ import qualtran.bloqs.arithmetic.controlled_add_or_subtract import qualtran.bloqs.arithmetic.controlled_addition import qualtran.bloqs.arithmetic.conversions +import qualtran.bloqs.arithmetic.lists import qualtran.bloqs.arithmetic.multiplication import qualtran.bloqs.arithmetic.negate import qualtran.bloqs.arithmetic.permutation @@ -492,6 +493,15 @@ module=qualtran.bloqs.arithmetic.trigonometric, bloq_specs=[qualtran.bloqs.arithmetic.trigonometric.arcsin._ARCSIN_DOC], ), + NotebookSpecV2( + title='List Functions', + module=qualtran.bloqs.arithmetic.lists, + bloq_specs=[ + qualtran.bloqs.arithmetic.lists.sort_in_place._SORT_IN_PLACE_DOC, + qualtran.bloqs.arithmetic.lists.symmetric_difference._SYMMETRIC_DIFFERENCE_DOC, + qualtran.bloqs.arithmetic.lists.has_duplicates._HAS_DUPLICATES_DOC, + ], + ), ] MOD_ARITHMETIC = [ diff --git a/docs/bloqs/index.rst b/docs/bloqs/index.rst index 3ed5dfe35..969a42419 100644 --- a/docs/bloqs/index.rst +++ b/docs/bloqs/index.rst @@ -75,6 +75,7 @@ Bloqs Library arithmetic/permutation.ipynb arithmetic/bitwise.ipynb arithmetic/trigonometric/trigonometric.ipynb + arithmetic/lists/lists.ipynb .. toctree:: :maxdepth: 2 diff --git a/qualtran/bloqs/arithmetic/lists/__init__.py b/qualtran/bloqs/arithmetic/lists/__init__.py new file mode 100644 index 000000000..7ed84457f --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/__init__.py @@ -0,0 +1,16 @@ +# 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 .has_duplicates import HasDuplicates +from .sort_in_place import SortInPlace +from .symmetric_difference import SymmetricDifference diff --git a/qualtran/bloqs/arithmetic/lists/has_duplicates.py b/qualtran/bloqs/arithmetic/lists/has_duplicates.py new file mode 100644 index 000000000..4da8d2a61 --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/has_duplicates.py @@ -0,0 +1,176 @@ +# 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 typing import Union + +import attrs +import numpy as np +from attrs import frozen + +from qualtran import ( + AddControlledT, + Bloq, + bloq_example, + BloqBuilder, + BloqDocSpec, + CtrlSpec, + QBit, + QInt, + QUInt, + Register, + Signature, + Soquet, + SoquetT, +) +from qualtran.bloqs.arithmetic import LinearDepthHalfLessThan +from qualtran.bloqs.basic_gates import CNOT, XGate +from qualtran.bloqs.mcmt import MultiControlX +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator +from qualtran.simulation.classical_sim import ClassicalValT +from qualtran.symbolics import HasLength, is_symbolic, SymbolicInt + + +@frozen +class HasDuplicates(Bloq): + r"""Given a sorted list of `l` numbers, check if it contains any duplicates. + + Produces a single qubit which is `1` if there are duplicates, and `0` if all are disjoint. + It compares every adjacent pair, and therefore uses `l - 1` comparisons. + It then uses a single MCX on `l - 1` bits gate to compute the flag. + + Args: + l: number of elements in the list + dtype: type of each element to store `[n]`. + + Registers: + xs: a list of `l` registers of `dtype`. + flag: single qubit. Value is flipped if the input list has duplicates, otherwise stays same. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Lemma 4.12. Eq. 122. + """ + + l: SymbolicInt + dtype: Union[QUInt, QInt] + is_controlled: bool = False + + @property + def signature(self) -> 'Signature': + registers = [Register('xs', self.dtype, shape=(self.l,)), Register('flag', QBit())] + if self.is_controlled: + registers.append(Register('ctrl', QBit())) + return Signature(registers) + + @property + def _le_bloq(self) -> LinearDepthHalfLessThan: + return LinearDepthHalfLessThan(self.dtype) + + def build_composite_bloq( + self, bb: 'BloqBuilder', xs: 'SoquetT', flag: 'Soquet', **extra_soqs: 'SoquetT' + ) -> dict[str, 'SoquetT']: + assert not is_symbolic(self.l) + assert isinstance(xs, np.ndarray) + + cs = [] + oks = [] + if self.is_controlled: + oks = [extra_soqs.pop('ctrl')] + assert not extra_soqs + + for i in range(1, self.l): + xs[i - 1], xs[i], c, ok = bb.add(self._le_bloq, a=xs[i - 1], b=xs[i]) + cs.append(c) + oks.append(ok) + + oks, flag = bb.add(MultiControlX((1,) * len(oks)), controls=np.array(oks), target=flag) + if not self.is_controlled: + flag = bb.add(XGate(), q=flag) + else: + oks[0], flag = bb.add(CNOT(), ctrl=oks[0], target=flag) + + oks = list(oks) + for i in reversed(range(1, self.l)): + xs[i - 1], xs[i] = bb.add( + self._le_bloq.adjoint(), a=xs[i - 1], b=xs[i], c=cs.pop(), target=oks.pop() + ) + + if self.is_controlled: + extra_soqs = {'ctrl': oks.pop()} + assert not oks + + return {'xs': xs, 'flag': flag} | extra_soqs + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT: + counts = Counter[Bloq]() + + counts[self._le_bloq] += self.l - 1 + counts[self._le_bloq.adjoint()] += self.l - 1 + + n_ctrls = self.l - (1 if not self.is_controlled else 0) + counts[MultiControlX(HasLength(n_ctrls))] += 1 + + counts[XGate() if not self.is_controlled else CNOT()] += 1 + + return counts + + def on_classical_vals(self, **vals: 'ClassicalValT') -> dict[str, 'ClassicalValT']: + xs = np.asarray(vals['xs']) + assert np.all(xs == np.sort(xs)) + if np.any(xs[:-1] == xs[1:]): + vals['flag'] ^= 1 + return vals + + def adjoint(self) -> 'HasDuplicates': + return self + + def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> tuple['Bloq', 'AddControlledT']: + from qualtran.bloqs.mcmt.specialized_ctrl import get_ctrl_system_1bit_cv_from_bloqs + + return get_ctrl_system_1bit_cv_from_bloqs( + self, + ctrl_spec, + current_ctrl_bit=1 if self.is_controlled else None, + bloq_with_ctrl=attrs.evolve(self, is_controlled=True), + ctrl_reg_name='ctrl', + ) + + +@bloq_example +def _has_duplicates() -> HasDuplicates: + has_duplicates = HasDuplicates(4, QUInt(3)) + return has_duplicates + + +@bloq_example +def _has_duplicates_symb() -> HasDuplicates: + import sympy + + n = sympy.Symbol("n") + has_duplicates_symb = HasDuplicates(4, QUInt(n)) + return has_duplicates_symb + + +@bloq_example +def _has_duplicates_symb_len() -> HasDuplicates: + import sympy + + l, n = sympy.symbols("l n") + has_duplicates_symb_len = HasDuplicates(l, QUInt(n)) + return has_duplicates_symb_len + + +_HAS_DUPLICATES_DOC = BloqDocSpec( + bloq_cls=HasDuplicates, examples=[_has_duplicates_symb, _has_duplicates] +) diff --git a/qualtran/bloqs/arithmetic/lists/has_duplicates_test.py b/qualtran/bloqs/arithmetic/lists/has_duplicates_test.py new file mode 100644 index 000000000..0a02fa83a --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/has_duplicates_test.py @@ -0,0 +1,56 @@ +# 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. +import itertools + +import numpy as np +import pytest + +import qualtran.testing as qlt_testing +from qualtran import QInt, QUInt +from qualtran.bloqs.arithmetic.lists.has_duplicates import ( + _has_duplicates, + _has_duplicates_symb, + _has_duplicates_symb_len, + HasDuplicates, +) + + +@pytest.mark.parametrize( + "bloq_ex", + [_has_duplicates, _has_duplicates_symb, _has_duplicates_symb_len], + ids=lambda b: b.name, +) +def test_examples(bloq_autotester, bloq_ex): + bloq_autotester(bloq_ex) + + +@pytest.mark.parametrize("bloq_ex", [_has_duplicates, _has_duplicates_symb], ids=lambda b: b.name) +def test_counts(bloq_ex): + qlt_testing.assert_equivalent_bloq_counts(bloq_ex()) + + +@pytest.mark.parametrize("l", [2, 3, pytest.param(4, marks=pytest.mark.slow)]) +@pytest.mark.parametrize( + "dtype", [QUInt(2), QInt(2), pytest.param(QUInt(3), marks=pytest.mark.slow)] +) +def test_classical_action(l, dtype): + bloq = HasDuplicates(l, dtype) + cbloq = bloq.decompose_bloq() + + for xs_t in itertools.product(dtype.get_classical_domain(), repeat=l): + xs = np.sort(xs_t) + for flag in [0, 1]: + np.testing.assert_equal( + cbloq.call_classically(xs=xs, flag=flag), bloq.call_classically(xs=xs, flag=flag) + ) diff --git a/qualtran/bloqs/arithmetic/lists/lists.ipynb b/qualtran/bloqs/arithmetic/lists/lists.ipynb new file mode 100644 index 000000000..2251b288d --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/lists.ipynb @@ -0,0 +1,342 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "46306919", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# List Functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47761dec", + "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": "19f11879", + "metadata": { + "cq.autogen": "SortInPlace.bloq_doc.md" + }, + "source": [ + "## `SortInPlace`\n", + "Sort a list of $\\ell$ numbers in place using $\\ell \\log \\ell$ ancilla bits.\n", + "\n", + "Applies the map:\n", + "$$\n", + " |x_1, x_2, \\ldots, x_l\\rangle\n", + " |0^{\\ell \\log \\ell}\\rangle\n", + " \\mapsto\n", + " |x_{\\pi_1}, x_{\\pi_2}, \\ldots, x_{\\pi_\\ell})\\rangle\n", + " |\\pi_1, \\pi_2, \\ldots, \\pi_\\ell\\rangle\n", + "$$\n", + "where $x_{\\pi_1} \\le x_{\\pi_2} \\ldots \\le x_{\\pi_\\ell}$ is the sorted list,\n", + "and the ancilla are entangled.\n", + "\n", + "To apply this, we first use any sorting algorithm to output the sorted list\n", + "in a clean register. And then use the following algorithm from Lemma 4.12 of Ref [1]\n", + "that applies the map:\n", + "\n", + "$$\n", + " |x_1, ..., x_l\\rangle|x_{\\pi(1)}, ..., x_{\\pi(l)})\\rangle\n", + " \\mapsto\n", + " |x_l, ..., x_l\\rangle|\\pi(1), ..., \\pi(l))\\rangle\n", + "$$\n", + "\n", + "where $x_i \\in [n]$ and $\\pi(i) \\in [l]$.\n", + "This second algorithm (Lemma 4.12) has two steps, each with $l^2$ comparisons:\n", + "1. compute `pi(1) ... pi(l)` given `x_1 ... x_l` and `x_{pi(1)} ... x{pi(l)}`.\n", + "1. (un)compute `x_{pi(1)} ... x{pi(l)}` using `pi(1) ... pi(l)` given `x_1 ... x_l`.\n", + "\n", + "#### Parameters\n", + " - `l`: number of elements in the list\n", + " - `dtype`: type of each element to store `[n]`. \n", + "\n", + "#### Registers\n", + " - `input`: the entire input as a single register\n", + " - `ancilla`: the generated (entangled) register storing `pi`. \n", + "\n", + "#### References\n", + " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Lemma 4.12. Eq. 122.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce69b6e8", + "metadata": { + "cq.autogen": "SortInPlace.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.arithmetic.lists import SortInPlace" + ] + }, + { + "cell_type": "markdown", + "id": "e84df89b", + "metadata": { + "cq.autogen": "SymmetricDifference.bloq_doc.md" + }, + "source": [ + "## `SymmetricDifference`\n", + "Given two sorted sets $S, T$ of unique elements, compute their symmetric difference.\n", + "\n", + "This accepts an integer `n_diff`, and marks a flag qubit if the symmetric difference\n", + "set is of size exactly `n_diff`. If the flag is marked (1), then the output of `n_diff`\n", + "numbers is the symmetric difference, otherwise it may be arbitrary.\n", + "\n", + "#### Parameters\n", + " - `n_lhs`: number of elements in $S$\n", + " - `n_rhs`: number of elements in $T$\n", + " - `n_diff`: expected number of elements in the difference $S \\Delta T$.\n", + " - `dtype`: type of each element. \n", + "\n", + "#### Registers\n", + " - `S`: list of `n_lhs` numbers.\n", + " - `T`: list of `n_rhs` numbers.\n", + " - `diff`: output register of `n_diff` numbers.\n", + " - `flag`: 1 if there are duplicates, 0 if all are unique. \n", + "\n", + "#### References\n", + " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Theorem 4.17, proof para 3, page 38.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3a9d9ea", + "metadata": { + "cq.autogen": "SymmetricDifference.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.arithmetic.lists import SymmetricDifference" + ] + }, + { + "cell_type": "markdown", + "id": "f58e0eba", + "metadata": { + "cq.autogen": "SymmetricDifference.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29fc34f0", + "metadata": { + "cq.autogen": "SymmetricDifference.symm_diff" + }, + "outputs": [], + "source": [ + "dtype = QUInt(4)\n", + "symm_diff = SymmetricDifference(n_lhs=4, n_rhs=2, n_diff=4, dtype=dtype)" + ] + }, + { + "cell_type": "markdown", + "id": "70608811", + "metadata": { + "cq.autogen": "SymmetricDifference.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9df9334c", + "metadata": { + "cq.autogen": "SymmetricDifference.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([symm_diff],\n", + " ['`symm_diff`'])" + ] + }, + { + "cell_type": "markdown", + "id": "476c580a", + "metadata": { + "cq.autogen": "SymmetricDifference.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6e6f812", + "metadata": { + "cq.autogen": "SymmetricDifference.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "symm_diff_g, symm_diff_sigma = symm_diff.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(symm_diff_g)\n", + "show_counts_sigma(symm_diff_sigma)" + ] + }, + { + "cell_type": "markdown", + "id": "15e88a40", + "metadata": { + "cq.autogen": "HasDuplicates.bloq_doc.md" + }, + "source": [ + "## `HasDuplicates`\n", + "Given a sorted list of `l` numbers, check if it contains any duplicates.\n", + "\n", + "Produces a single qubit which is `1` if there are duplicates, and `0` if all are disjoint.\n", + "It compares every adjacent pair, and therefore uses `l - 1` comparisons.\n", + "It then uses a single MCX on `l - 1` bits gate to compute the flag.\n", + "\n", + "#### Parameters\n", + " - `l`: number of elements in the list\n", + " - `dtype`: type of each element to store `[n]`. \n", + "\n", + "#### Registers\n", + " - `xs`: a list of `l` registers of `dtype`.\n", + " - `flag`: single qubit. Value is flipped if the input list has duplicates, otherwise stays same. \n", + "\n", + "#### References\n", + " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Lemma 4.12. Eq. 122.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "101f6899", + "metadata": { + "cq.autogen": "HasDuplicates.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.arithmetic.lists import HasDuplicates" + ] + }, + { + "cell_type": "markdown", + "id": "6ceded7e", + "metadata": { + "cq.autogen": "HasDuplicates.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5f5efa7", + "metadata": { + "cq.autogen": "HasDuplicates.has_duplicates_symb" + }, + "outputs": [], + "source": [ + "import sympy\n", + "\n", + "n = sympy.Symbol(\"n\")\n", + "has_duplicates_symb = HasDuplicates(4, QUInt(n))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5412e0d", + "metadata": { + "cq.autogen": "HasDuplicates.has_duplicates" + }, + "outputs": [], + "source": [ + "has_duplicates = HasDuplicates(4, QUInt(3))" + ] + }, + { + "cell_type": "markdown", + "id": "7c1c62b4", + "metadata": { + "cq.autogen": "HasDuplicates.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1198bb5d", + "metadata": { + "cq.autogen": "HasDuplicates.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([has_duplicates_symb, has_duplicates],\n", + " ['`has_duplicates_symb`', '`has_duplicates`'])" + ] + }, + { + "cell_type": "markdown", + "id": "f6b8cde0", + "metadata": { + "cq.autogen": "HasDuplicates.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75afba07", + "metadata": { + "cq.autogen": "HasDuplicates.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "has_duplicates_symb_g, has_duplicates_symb_sigma = has_duplicates_symb.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(has_duplicates_symb_g)\n", + "show_counts_sigma(has_duplicates_symb_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/lists/sort_in_place.py b/qualtran/bloqs/arithmetic/lists/sort_in_place.py new file mode 100644 index 000000000..d4da3727a --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/sort_in_place.py @@ -0,0 +1,99 @@ +# 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 attrs import frozen + +from qualtran import Bloq, BloqDocSpec, BQUInt, QDType, Register, Signature +from qualtran.bloqs.arithmetic import Xor +from qualtran.bloqs.arithmetic.sorting import Comparator +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator +from qualtran.symbolics import ceil, log2, SymbolicInt + + +@frozen +class SortInPlace(Bloq): + r"""Sort a list of $\ell$ numbers in place using $\ell \log \ell$ ancilla bits. + + Applies the map: + $$ + |x_1, x_2, \ldots, x_l\rangle + |0^{\ell \log \ell}\rangle + \mapsto + |x_{\pi_1}, x_{\pi_2}, \ldots, x_{\pi_\ell})\rangle + |\pi_1, \pi_2, \ldots, \pi_\ell\rangle + $$ + where $x_{\pi_1} \le x_{\pi_2} \ldots \le x_{\pi_\ell}$ is the sorted list, + and the ancilla are entangled. + + To apply this, we first use any sorting algorithm to output the sorted list + in a clean register. And then use the following algorithm from Lemma 4.12 of Ref [1] + that applies the map: + + $$ + |x_1, ..., x_l\rangle|x_{\pi(1)}, ..., x_{\pi(l)})\rangle + \mapsto + |x_l, ..., x_l\rangle|\pi(1), ..., \pi(l))\rangle + $$ + + where $x_i \in [n]$ and $\pi(i) \in [l]$. + This second algorithm (Lemma 4.12) has two steps, each with $l^2$ comparisons: + 1. compute `pi(1) ... pi(l)` given `x_1 ... x_l` and `x_{pi(1)} ... x{pi(l)}`. + 1. (un)compute `x_{pi(1)} ... x{pi(l)}` using `pi(1) ... pi(l)` given `x_1 ... x_l`. + + Args: + l: number of elements in the list + dtype: type of each element to store `[n]`. + + Registers: + input: the entire input as a single register + ancilla (RIGHT): the generated (entangled) register storing `pi`. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Lemma 4.12. Eq. 122. + """ + + l: SymbolicInt + dtype: QDType + + @property + def signature(self) -> 'Signature': + return Signature( + [ + Register('xs', self.dtype, shape=(self.l,)), + Register('pi', self.index_dtype, shape=(self.l,)), + ] + ) + + @property + def index_dtype(self) -> QDType: + """dtype to represent an index in range `[l]`""" + bitsize = ceil(log2(self.l)) + return BQUInt(bitsize, self.l) + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT: + compare = Comparator(self.dtype.num_qubits) + n_ops = 3 * self.l**2 + + counts = Counter[Bloq]() + + counts[compare] += n_ops + counts[compare.adjoint()] += n_ops + counts[Xor(self.dtype)] += n_ops + + return counts + + +_SORT_IN_PLACE_DOC = BloqDocSpec(bloq_cls=SortInPlace, examples=[]) diff --git a/qualtran/bloqs/arithmetic/lists/symmetric_difference.py b/qualtran/bloqs/arithmetic/lists/symmetric_difference.py new file mode 100644 index 000000000..72d13f822 --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/symmetric_difference.py @@ -0,0 +1,135 @@ +# 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 attrs import frozen + +from qualtran import Bloq, bloq_example, BloqDocSpec, QBit, QDType, QUInt, Register, Signature +from qualtran.bloqs.arithmetic import Equals, EqualsAConstant, HammingWeightCompute, Xor +from qualtran.bloqs.arithmetic.sorting import BitonicMerge +from qualtran.bloqs.basic_gates import CNOT +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator +from qualtran.symbolics import bit_length, is_symbolic, SymbolicInt + + +@frozen +class SymmetricDifference(Bloq): + r"""Given two sorted sets $S, T$ of unique elements, compute their symmetric difference. + + This accepts an integer `n_diff`, and marks a flag qubit if the symmetric difference + set is of size exactly `n_diff`. If the flag is marked (1), then the output of `n_diff` + numbers is the symmetric difference, otherwise it may be arbitrary. + + Args: + n_lhs: number of elements in $S$ + n_rhs: number of elements in $T$ + n_diff: expected number of elements in the difference $S \Delta T$. + dtype: type of each element. + + Registers: + S: list of `n_lhs` numbers. + T: list of `n_rhs` numbers. + diff: output register of `n_diff` numbers. + flag: 1 if there are duplicates, 0 if all are unique. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Theorem 4.17, proof para 3, page 38. + """ + + n_lhs: SymbolicInt + n_rhs: SymbolicInt + n_diff: SymbolicInt + dtype: QDType + + def __attrs_post_init__(self): + if not is_symbolic(self.n_lhs, self.n_rhs): + assert self.n_lhs >= self.n_rhs, "lhs must be the larger set" + + @property + def signature(self) -> 'Signature': + return Signature( + [ + Register('S', self.dtype, shape=(self.n_lhs,)), + Register('T', self.dtype, shape=(self.n_rhs,)), + Register('diff', self.dtype, shape=(self.n_diff,)), + Register('flag', QBit()), + ] + ) + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT: + # the forward pass, i.e. all bloqs that must be uncomputed + counts_forward = Counter[Bloq]() + + # merge the lists + counts_forward[BitonicMerge(self.n_lhs, self.dtype.num_qubits)] += 1 + # compare adjacents + counts_forward[Equals(self.dtype)] += self.n_lhs + self.n_rhs - 1 + # compute number of equal adjacents + counts_forward[HammingWeightCompute(self.n_lhs + self.n_rhs - 1)] += 1 + # check: 2 * n_equal = n_lhs + n_rhs - n_diff + # (note: the above eq holds as we assume all input elements are unique) + counts_forward[ + EqualsAConstant( + bit_length(self.n_lhs + self.n_rhs - 1), + (self.n_lhs + self.n_rhs - self.n_diff) // 2, + ) + ] += 1 + + # all bloqs + counts = Counter[Bloq]() + + # copy the first n_diff numbers and flag + counts[Xor(self.dtype)] += self.n_diff + counts[CNOT()] += 1 + + for bloq, n in counts_forward.items(): + counts[bloq] += n + counts[bloq.adjoint()] += n + + return counts + + +@bloq_example +def _symm_diff() -> SymmetricDifference: + dtype = QUInt(4) + symm_diff = SymmetricDifference(n_lhs=4, n_rhs=2, n_diff=4, dtype=dtype) + return symm_diff + + +@bloq_example +def _symm_diff_symb() -> SymmetricDifference: + import sympy + + from qualtran.symbolics import bit_length + + n, k, c = sympy.symbols("n k c", positive=True, integer=True) + dtype = QUInt(bit_length(n - 1)) + symm_diff_symb = SymmetricDifference(n_lhs=c * k, n_rhs=k, n_diff=c * k, dtype=dtype) + return symm_diff_symb + + +@bloq_example +def _symm_diff_equal_size_symb() -> SymmetricDifference: + import sympy + + from qualtran.symbolics import bit_length + + n, k, c = sympy.symbols("n k c", positive=True, integer=True) + dtype = QUInt(bit_length(n - 1)) + symm_diff_equal_size_symb = SymmetricDifference(n_lhs=c * k, n_rhs=c * k, n_diff=k, dtype=dtype) + return symm_diff_equal_size_symb + + +_SYMMETRIC_DIFFERENCE_DOC = BloqDocSpec(bloq_cls=SymmetricDifference, examples=[_symm_diff]) diff --git a/qualtran/bloqs/arithmetic/lists/symmetric_difference_test.py b/qualtran/bloqs/arithmetic/lists/symmetric_difference_test.py new file mode 100644 index 000000000..0ad5a356b --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/symmetric_difference_test.py @@ -0,0 +1,62 @@ +# 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 unittest.mock import ANY + +import pytest + +import qualtran.testing as qlt_testing +from qualtran.bloqs.arithmetic.lists.symmetric_difference import ( + _symm_diff, + _symm_diff_equal_size_symb, + _symm_diff_symb, +) +from qualtran.resource_counting import big_O, GateCounts, get_cost_value, QECGatesCost +from qualtran.symbolics import ceil, log2 + + +@pytest.mark.parametrize("bloq_ex", [_symm_diff, _symm_diff_symb, _symm_diff_equal_size_symb]) +def test_examples(bloq_autotester, bloq_ex): + bloq_autotester(bloq_ex) + + +@pytest.mark.parametrize("bloq_ex", [_symm_diff_symb, _symm_diff_equal_size_symb]) +def test_cost(bloq_ex): + bloq = bloq_ex() + gc = get_cost_value(bloq, QECGatesCost()) + + l, r = bloq.n_lhs, bloq.n_rhs # assumption l >= r + logn = bloq.dtype.num_qubits + logl = ceil(log2(l)) + assert gc == GateCounts( + cswap=2 * l * logn * (logl + 1), + and_bloq=( + 2 * l * (2 * logn + 1) * (logl + 1) + + l + + r + + 2 * ((logn - 1) * (l + r - 1)) + + 2 * ceil(log2(l + r)) + - 4 + ), + clifford=ANY, + measurement=ANY, + ) + + # \tilde{O}(l log n) + # Page 38, Thm 4.17, proof para 3, 3rd last line. + assert gc.total_t_count() in big_O(l * logn * logl**2) + + +@pytest.mark.notebook +def test_notebook(): + qlt_testing.execute_notebook('lists') diff --git a/qualtran/bloqs/arithmetic/sorting.py b/qualtran/bloqs/arithmetic/sorting.py index d18db924e..a6e9937cc 100644 --- a/qualtran/bloqs/arithmetic/sorting.py +++ b/qualtran/bloqs/arithmetic/sorting.py @@ -23,6 +23,7 @@ bloq_example, BloqBuilder, BloqDocSpec, + DecomposeNotImplementedError, DecomposeTypeError, QBit, QUInt, @@ -222,8 +223,6 @@ def __attrs_post_init__(self): k = self.half_length if not is_symbolic(k): assert k >= 1, "length of input lists must be positive" - # TODO(#1090) support non-power-of-two input lengths - assert (k & (k - 1)) == 0, "length of input lists must be a power of 2" @cached_property def signature(self) -> 'Signature': @@ -249,14 +248,16 @@ def is_symbolic(self): def build_composite_bloq( self, bb: 'BloqBuilder', xs: 'SoquetT', ys: 'SoquetT' ) -> dict[str, 'SoquetT']: - if is_symbolic(self.half_length): + k = self.half_length + if is_symbolic(k): raise DecomposeTypeError(f"Cannot decompose symbolic {self=}") + if (k & (k - 1)) != 0: + # TODO(#1090) support non-power-of-two input lengths + raise DecomposeNotImplementedError("length of input lists must be a power of 2") assert isinstance(xs, np.ndarray) assert isinstance(ys, np.ndarray) - k = self.half_length - first_round_junk = [] for i in range(k): xs[i], ys[k - 1 - i], anc = bb.add(Comparator(self.bitsize), a=xs[i], b=ys[k - 1 - i]) diff --git a/qualtran/conftest.py b/qualtran/conftest.py index 28d79a89f..439ae6145 100644 --- a/qualtran/conftest.py +++ b/qualtran/conftest.py @@ -141,6 +141,9 @@ def assert_bloq_example_serializes_for_pytest(bloq_ex: BloqExample): 'ctrl_on_symbolic_cv', # cannot serialize Shaped 'ctrl_on_symbolic_cv_multi', # cannot serialize Shaped 'ctrl_on_symbolic_n_ctrls', # cannot serialize Shaped + 'has_duplicates_symb_len', # cannot serialize HasLength + 'symm_diff_symb', # round trip fail: sympy assumptions not serialized + 'symm_diff_equal_size_symb', # round trip fail: sympy assumptions not serialized ]: 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 d81f9030a..83f05c786 100644 --- a/qualtran/serialization/resolver_dict.py +++ b/qualtran/serialization/resolver_dict.py @@ -23,6 +23,7 @@ import qualtran.bloqs.arithmetic.conversions.ones_complement_to_twos_complement import qualtran.bloqs.arithmetic.conversions.sign_extension import qualtran.bloqs.arithmetic.hamming_weight +import qualtran.bloqs.arithmetic.lists import qualtran.bloqs.arithmetic.multiplication import qualtran.bloqs.arithmetic.negate import qualtran.bloqs.arithmetic.permutation @@ -191,6 +192,9 @@ "qualtran.bloqs.arithmetic.conversions.sign_extension.SignExtend": qualtran.bloqs.arithmetic.conversions.sign_extension.SignExtend, "qualtran.bloqs.arithmetic.conversions.sign_extension.SignTruncate": qualtran.bloqs.arithmetic.conversions.sign_extension.SignTruncate, "qualtran.bloqs.arithmetic.hamming_weight.HammingWeightCompute": qualtran.bloqs.arithmetic.hamming_weight.HammingWeightCompute, + "qualtran.bloqs.arithmetic.lists.has_duplicates.HasDuplicates": qualtran.bloqs.arithmetic.lists.has_duplicates.HasDuplicates, + "qualtran.bloqs.arithmetic.lists.sort_in_place.SortInPlace": qualtran.bloqs.arithmetic.lists.sort_in_place.SortInPlace, + "qualtran.bloqs.arithmetic.lists.symmetric_difference.SymmetricDifference": qualtran.bloqs.arithmetic.lists.symmetric_difference.SymmetricDifference, "qualtran.bloqs.arithmetic.multiplication.InvertRealNumber": qualtran.bloqs.arithmetic.multiplication.InvertRealNumber, "qualtran.bloqs.arithmetic.multiplication.MultiplyTwoReals": qualtran.bloqs.arithmetic.multiplication.MultiplyTwoReals, "qualtran.bloqs.arithmetic.multiplication.PlusEqualProduct": qualtran.bloqs.arithmetic.multiplication.PlusEqualProduct,