From 1012595709477c656b3155753c0892342e7dbd55 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Sat, 30 Nov 2024 20:56:18 +0100 Subject: [PATCH 1/4] Add list primitives from Quartic speedups paper --- .../qualtran_dev_tools/notebook_specs.py | 10 + qualtran/bloqs/arithmetic/lists/__init__.py | 16 ++ .../bloqs/arithmetic/lists/has_duplicates.py | 177 ++++++++++++++++++ .../arithmetic/lists/has_duplicates_test.py | 57 ++++++ .../bloqs/arithmetic/lists/sort_in_place.py | 99 ++++++++++ .../arithmetic/lists/symmetric_difference.py | 130 +++++++++++++ .../lists/symmetric_difference_test.py | 63 +++++++ qualtran/bloqs/arithmetic/sorting.py | 11 +- 8 files changed, 558 insertions(+), 5 deletions(-) create mode 100644 qualtran/bloqs/arithmetic/lists/__init__.py create mode 100644 qualtran/bloqs/arithmetic/lists/has_duplicates.py create mode 100644 qualtran/bloqs/arithmetic/lists/has_duplicates_test.py create mode 100644 qualtran/bloqs/arithmetic/lists/sort_in_place.py create mode 100644 qualtran/bloqs/arithmetic/lists/symmetric_difference.py create mode 100644 qualtran/bloqs/arithmetic/lists/symmetric_difference_test.py diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py index a35c468ce..8289e00ce 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 @@ -488,6 +489,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/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..cce9c88d2 --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/has_duplicates.py @@ -0,0 +1,177 @@ +# 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, _has_duplicates_symb_len], +) 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..62ef23aa3 --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/has_duplicates_test.py @@ -0,0 +1,57 @@ +# 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 .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/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..3d18d5689 --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/symmetric_difference.py @@ -0,0 +1,130 @@ +# 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: + 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 = SymmetricDifference(n_lhs=c * k, n_rhs=k, n_diff=c * k, dtype=dtype) + return symm_diff + + +@bloq_example +def _symm_diff_equal_size() -> 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 = SymmetricDifference(n_lhs=c * k, n_rhs=c * k, n_diff=k, dtype=dtype) + return symm_diff_equal_size + + +_SYMMETRIC_DIFFERENCE_DOC = BloqDocSpec( + bloq_cls=SymmetricDifference, examples=[_symm_diff, _symm_diff_equal_size] +) 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..fa3765f70 --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/symmetric_difference_test.py @@ -0,0 +1,63 @@ +# 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 + +from qualtran.resource_counting import big_O, GateCounts, get_cost_value, QECGatesCost +from qualtran.symbolics import ceil, log2 + +from .symmetric_difference import _symm_diff, _symm_diff_equal_size + + +@pytest.mark.parametrize("bloq_ex", [_symm_diff, _symm_diff_equal_size]) +def test_examples(bloq_autotester, bloq_ex): + if bloq_autotester.check_name == 'serialize': + pytest.skip() + + bloq_autotester(bloq_ex) + + +@pytest.mark.parametrize("bloq_ex", [_symm_diff, _symm_diff_equal_size]) +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(): + from qualtran.testing import execute_notebook + + execute_notebook('arithmetic') diff --git a/qualtran/bloqs/arithmetic/sorting.py b/qualtran/bloqs/arithmetic/sorting.py index d18db924e..c500effc9 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]) From bced152ce62bef8f4fedad09912b9486ff719865 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Sun, 1 Dec 2024 19:18:38 +0100 Subject: [PATCH 2/4] notebook --- docs/bloqs/index.rst | 1 + qualtran/bloqs/arithmetic/lists/lists.ipynb | 380 ++++++++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 qualtran/bloqs/arithmetic/lists/lists.ipynb diff --git a/docs/bloqs/index.rst b/docs/bloqs/index.rst index d827dfaad..f886288fd 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/lists.ipynb b/qualtran/bloqs/arithmetic/lists/lists.ipynb new file mode 100644 index 000000000..cd9ebf669 --- /dev/null +++ b/qualtran/bloqs/arithmetic/lists/lists.ipynb @@ -0,0 +1,380 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4b6002bc", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# List Functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73f38d89", + "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": "75d1819f", + "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": "fcb6c012", + "metadata": { + "cq.autogen": "SortInPlace.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.arithmetic.lists import SortInPlace" + ] + }, + { + "cell_type": "markdown", + "id": "e878a52b", + "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": "ccaa8a4b", + "metadata": { + "cq.autogen": "SymmetricDifference.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.arithmetic.lists import SymmetricDifference" + ] + }, + { + "cell_type": "markdown", + "id": "6cefce9f", + "metadata": { + "cq.autogen": "SymmetricDifference.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7ffc68f", + "metadata": { + "cq.autogen": "SymmetricDifference.symm_diff" + }, + "outputs": [], + "source": [ + "import sympy\n", + "\n", + "from qualtran.symbolics import bit_length\n", + "\n", + "n, k, c = sympy.symbols(\"n k c\", positive=True, integer=True)\n", + "dtype = QUInt(bit_length(n - 1))\n", + "symm_diff = SymmetricDifference(n_lhs=c * k, n_rhs=k, n_diff=c * k, dtype=dtype)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4da0b292", + "metadata": { + "cq.autogen": "SymmetricDifference.symm_diff_equal_size" + }, + "outputs": [], + "source": [ + "import sympy\n", + "\n", + "from qualtran.symbolics import bit_length\n", + "\n", + "n, k, c = sympy.symbols(\"n k c\", positive=True, integer=True)\n", + "dtype = QUInt(bit_length(n - 1))\n", + "symm_diff_equal_size = SymmetricDifference(n_lhs=c * k, n_rhs=c * k, n_diff=k, dtype=dtype)" + ] + }, + { + "cell_type": "markdown", + "id": "79bb967b", + "metadata": { + "cq.autogen": "SymmetricDifference.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb210e38", + "metadata": { + "cq.autogen": "SymmetricDifference.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([symm_diff, symm_diff_equal_size],\n", + " ['`symm_diff`', '`symm_diff_equal_size`'])" + ] + }, + { + "cell_type": "markdown", + "id": "946e2cef", + "metadata": { + "cq.autogen": "SymmetricDifference.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a25a154", + "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": "a78e7e32", + "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": "0707263c", + "metadata": { + "cq.autogen": "HasDuplicates.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.arithmetic.lists import HasDuplicates" + ] + }, + { + "cell_type": "markdown", + "id": "eae0fee2", + "metadata": { + "cq.autogen": "HasDuplicates.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "588c7e8a", + "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": "d7d63e51", + "metadata": { + "cq.autogen": "HasDuplicates.has_duplicates" + }, + "outputs": [], + "source": [ + "has_duplicates = HasDuplicates(4, QUInt(3))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a12dbdf3", + "metadata": { + "cq.autogen": "HasDuplicates.has_duplicates_symb_len" + }, + "outputs": [], + "source": [ + "import sympy\n", + "\n", + "l, n = sympy.symbols(\"l n\")\n", + "has_duplicates_symb_len = HasDuplicates(l, QUInt(n))" + ] + }, + { + "cell_type": "markdown", + "id": "d189b98d", + "metadata": { + "cq.autogen": "HasDuplicates.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b905935f", + "metadata": { + "cq.autogen": "HasDuplicates.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([has_duplicates_symb, has_duplicates, has_duplicates_symb_len],\n", + " ['`has_duplicates_symb`', '`has_duplicates`', '`has_duplicates_symb_len`'])" + ] + }, + { + "cell_type": "markdown", + "id": "2156df99", + "metadata": { + "cq.autogen": "HasDuplicates.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d05f52a0", + "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 +} From af6fd74db4fdd5a5753ce56297a098aa32193d9f Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Sun, 1 Dec 2024 22:54:42 +0100 Subject: [PATCH 3/4] fix tests --- .../bloqs/arithmetic/lists/has_duplicates.py | 3 +- .../arithmetic/lists/has_duplicates_test.py | 3 +- qualtran/bloqs/arithmetic/lists/lists.ipynb | 92 ++++++------------- .../arithmetic/lists/symmetric_difference.py | 21 +++-- .../lists/symmetric_difference_test.py | 19 ++-- qualtran/bloqs/arithmetic/sorting.py | 2 +- qualtran/conftest.py | 3 + qualtran/serialization/resolver_dict.py | 4 + 8 files changed, 59 insertions(+), 88 deletions(-) diff --git a/qualtran/bloqs/arithmetic/lists/has_duplicates.py b/qualtran/bloqs/arithmetic/lists/has_duplicates.py index cce9c88d2..4da8d2a61 100644 --- a/qualtran/bloqs/arithmetic/lists/has_duplicates.py +++ b/qualtran/bloqs/arithmetic/lists/has_duplicates.py @@ -172,6 +172,5 @@ def _has_duplicates_symb_len() -> HasDuplicates: _HAS_DUPLICATES_DOC = BloqDocSpec( - bloq_cls=HasDuplicates, - examples=[_has_duplicates_symb, _has_duplicates, _has_duplicates_symb_len], + 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 index 62ef23aa3..0a02fa83a 100644 --- a/qualtran/bloqs/arithmetic/lists/has_duplicates_test.py +++ b/qualtran/bloqs/arithmetic/lists/has_duplicates_test.py @@ -18,8 +18,7 @@ import qualtran.testing as qlt_testing from qualtran import QInt, QUInt - -from .has_duplicates import ( +from qualtran.bloqs.arithmetic.lists.has_duplicates import ( _has_duplicates, _has_duplicates_symb, _has_duplicates_symb_len, diff --git a/qualtran/bloqs/arithmetic/lists/lists.ipynb b/qualtran/bloqs/arithmetic/lists/lists.ipynb index cd9ebf669..2251b288d 100644 --- a/qualtran/bloqs/arithmetic/lists/lists.ipynb +++ b/qualtran/bloqs/arithmetic/lists/lists.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "4b6002bc", + "id": "46306919", "metadata": { "cq.autogen": "title_cell" }, @@ -13,7 +13,7 @@ { "cell_type": "code", "execution_count": null, - "id": "73f38d89", + "id": "47761dec", "metadata": { "cq.autogen": "top_imports" }, @@ -30,7 +30,7 @@ }, { "cell_type": "markdown", - "id": "75d1819f", + "id": "19f11879", "metadata": { "cq.autogen": "SortInPlace.bloq_doc.md" }, @@ -79,7 +79,7 @@ { "cell_type": "code", "execution_count": null, - "id": "fcb6c012", + "id": "ce69b6e8", "metadata": { "cq.autogen": "SortInPlace.bloq_doc.py" }, @@ -90,7 +90,7 @@ }, { "cell_type": "markdown", - "id": "e878a52b", + "id": "e84df89b", "metadata": { "cq.autogen": "SymmetricDifference.bloq_doc.md" }, @@ -121,7 +121,7 @@ { "cell_type": "code", "execution_count": null, - "id": "ccaa8a4b", + "id": "d3a9d9ea", "metadata": { "cq.autogen": "SymmetricDifference.bloq_doc.py" }, @@ -132,7 +132,7 @@ }, { "cell_type": "markdown", - "id": "6cefce9f", + "id": "f58e0eba", "metadata": { "cq.autogen": "SymmetricDifference.example_instances.md" }, @@ -143,42 +143,19 @@ { "cell_type": "code", "execution_count": null, - "id": "b7ffc68f", + "id": "29fc34f0", "metadata": { "cq.autogen": "SymmetricDifference.symm_diff" }, "outputs": [], "source": [ - "import sympy\n", - "\n", - "from qualtran.symbolics import bit_length\n", - "\n", - "n, k, c = sympy.symbols(\"n k c\", positive=True, integer=True)\n", - "dtype = QUInt(bit_length(n - 1))\n", - "symm_diff = SymmetricDifference(n_lhs=c * k, n_rhs=k, n_diff=c * k, dtype=dtype)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4da0b292", - "metadata": { - "cq.autogen": "SymmetricDifference.symm_diff_equal_size" - }, - "outputs": [], - "source": [ - "import sympy\n", - "\n", - "from qualtran.symbolics import bit_length\n", - "\n", - "n, k, c = sympy.symbols(\"n k c\", positive=True, integer=True)\n", - "dtype = QUInt(bit_length(n - 1))\n", - "symm_diff_equal_size = SymmetricDifference(n_lhs=c * k, n_rhs=c * k, n_diff=k, dtype=dtype)" + "dtype = QUInt(4)\n", + "symm_diff = SymmetricDifference(n_lhs=4, n_rhs=2, n_diff=4, dtype=dtype)" ] }, { "cell_type": "markdown", - "id": "79bb967b", + "id": "70608811", "metadata": { "cq.autogen": "SymmetricDifference.graphical_signature.md" }, @@ -189,20 +166,20 @@ { "cell_type": "code", "execution_count": null, - "id": "fb210e38", + "id": "9df9334c", "metadata": { "cq.autogen": "SymmetricDifference.graphical_signature.py" }, "outputs": [], "source": [ "from qualtran.drawing import show_bloqs\n", - "show_bloqs([symm_diff, symm_diff_equal_size],\n", - " ['`symm_diff`', '`symm_diff_equal_size`'])" + "show_bloqs([symm_diff],\n", + " ['`symm_diff`'])" ] }, { "cell_type": "markdown", - "id": "946e2cef", + "id": "476c580a", "metadata": { "cq.autogen": "SymmetricDifference.call_graph.md" }, @@ -213,7 +190,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9a25a154", + "id": "a6e6f812", "metadata": { "cq.autogen": "SymmetricDifference.call_graph.py" }, @@ -227,7 +204,7 @@ }, { "cell_type": "markdown", - "id": "a78e7e32", + "id": "15e88a40", "metadata": { "cq.autogen": "HasDuplicates.bloq_doc.md" }, @@ -254,7 +231,7 @@ { "cell_type": "code", "execution_count": null, - "id": "0707263c", + "id": "101f6899", "metadata": { "cq.autogen": "HasDuplicates.bloq_doc.py" }, @@ -265,7 +242,7 @@ }, { "cell_type": "markdown", - "id": "eae0fee2", + "id": "6ceded7e", "metadata": { "cq.autogen": "HasDuplicates.example_instances.md" }, @@ -276,7 +253,7 @@ { "cell_type": "code", "execution_count": null, - "id": "588c7e8a", + "id": "d5f5efa7", "metadata": { "cq.autogen": "HasDuplicates.has_duplicates_symb" }, @@ -291,7 +268,7 @@ { "cell_type": "code", "execution_count": null, - "id": "d7d63e51", + "id": "a5412e0d", "metadata": { "cq.autogen": "HasDuplicates.has_duplicates" }, @@ -300,24 +277,9 @@ "has_duplicates = HasDuplicates(4, QUInt(3))" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "a12dbdf3", - "metadata": { - "cq.autogen": "HasDuplicates.has_duplicates_symb_len" - }, - "outputs": [], - "source": [ - "import sympy\n", - "\n", - "l, n = sympy.symbols(\"l n\")\n", - "has_duplicates_symb_len = HasDuplicates(l, QUInt(n))" - ] - }, { "cell_type": "markdown", - "id": "d189b98d", + "id": "7c1c62b4", "metadata": { "cq.autogen": "HasDuplicates.graphical_signature.md" }, @@ -328,20 +290,20 @@ { "cell_type": "code", "execution_count": null, - "id": "b905935f", + "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, has_duplicates_symb_len],\n", - " ['`has_duplicates_symb`', '`has_duplicates`', '`has_duplicates_symb_len`'])" + "show_bloqs([has_duplicates_symb, has_duplicates],\n", + " ['`has_duplicates_symb`', '`has_duplicates`'])" ] }, { "cell_type": "markdown", - "id": "2156df99", + "id": "f6b8cde0", "metadata": { "cq.autogen": "HasDuplicates.call_graph.md" }, @@ -352,7 +314,7 @@ { "cell_type": "code", "execution_count": null, - "id": "d05f52a0", + "id": "75afba07", "metadata": { "cq.autogen": "HasDuplicates.call_graph.py" }, diff --git a/qualtran/bloqs/arithmetic/lists/symmetric_difference.py b/qualtran/bloqs/arithmetic/lists/symmetric_difference.py index 3d18d5689..72d13f822 100644 --- a/qualtran/bloqs/arithmetic/lists/symmetric_difference.py +++ b/qualtran/bloqs/arithmetic/lists/symmetric_difference.py @@ -103,28 +103,33 @@ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT: @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 = SymmetricDifference(n_lhs=c * k, n_rhs=k, n_diff=c * k, dtype=dtype) - return symm_diff + 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() -> SymmetricDifference: +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 = SymmetricDifference(n_lhs=c * k, n_rhs=c * k, n_diff=k, dtype=dtype) - return symm_diff_equal_size + 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, _symm_diff_equal_size] -) +_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 index fa3765f70..0ad5a356b 100644 --- a/qualtran/bloqs/arithmetic/lists/symmetric_difference_test.py +++ b/qualtran/bloqs/arithmetic/lists/symmetric_difference_test.py @@ -15,21 +15,22 @@ 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 -from .symmetric_difference import _symm_diff, _symm_diff_equal_size - -@pytest.mark.parametrize("bloq_ex", [_symm_diff, _symm_diff_equal_size]) +@pytest.mark.parametrize("bloq_ex", [_symm_diff, _symm_diff_symb, _symm_diff_equal_size_symb]) def test_examples(bloq_autotester, bloq_ex): - if bloq_autotester.check_name == 'serialize': - pytest.skip() - bloq_autotester(bloq_ex) -@pytest.mark.parametrize("bloq_ex", [_symm_diff, _symm_diff_equal_size]) +@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()) @@ -58,6 +59,4 @@ def test_cost(bloq_ex): @pytest.mark.notebook def test_notebook(): - from qualtran.testing import execute_notebook - - execute_notebook('arithmetic') + qlt_testing.execute_notebook('lists') diff --git a/qualtran/bloqs/arithmetic/sorting.py b/qualtran/bloqs/arithmetic/sorting.py index c500effc9..a6e9937cc 100644 --- a/qualtran/bloqs/arithmetic/sorting.py +++ b/qualtran/bloqs/arithmetic/sorting.py @@ -251,7 +251,7 @@ def build_composite_bloq( k = self.half_length if is_symbolic(k): raise DecomposeTypeError(f"Cannot decompose symbolic {self=}") - if (k & (k - 1)) == 0: + 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") 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 cd01026fb..0075dd2b4 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 @@ -190,6 +191,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, From 670cc42a25cc3210268c65f1c5b36369e2fed736 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Tue, 3 Dec 2024 03:37:52 +0100 Subject: [PATCH 4/4] Implement Kikuchi guiding state preparation --- qualtran/bloqs/max_k_xor_sat/__init__.py | 15 + qualtran/bloqs/max_k_xor_sat/guiding_state.py | 445 ++++++++++++++++++ .../bloqs/max_k_xor_sat/guiding_state_test.py | 132 ++++++ .../guiding_state_tutorial.ipynb | 212 +++++++++ qualtran/bloqs/max_k_xor_sat/kxor_instance.py | 273 +++++++++++ .../bloqs/max_k_xor_sat/kxor_instance_test.py | 30 ++ 6 files changed, 1107 insertions(+) create mode 100644 qualtran/bloqs/max_k_xor_sat/__init__.py create mode 100644 qualtran/bloqs/max_k_xor_sat/guiding_state.py create mode 100644 qualtran/bloqs/max_k_xor_sat/guiding_state_test.py create mode 100644 qualtran/bloqs/max_k_xor_sat/guiding_state_tutorial.ipynb create mode 100644 qualtran/bloqs/max_k_xor_sat/kxor_instance.py create mode 100644 qualtran/bloqs/max_k_xor_sat/kxor_instance_test.py diff --git a/qualtran/bloqs/max_k_xor_sat/__init__.py b/qualtran/bloqs/max_k_xor_sat/__init__.py new file mode 100644 index 000000000..827b32662 --- /dev/null +++ b/qualtran/bloqs/max_k_xor_sat/__init__.py @@ -0,0 +1,15 @@ +# 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 .guiding_state import GuidingState, SimpleGuidingState +from .kxor_instance import Constraint, KXorInstance diff --git a/qualtran/bloqs/max_k_xor_sat/guiding_state.py b/qualtran/bloqs/max_k_xor_sat/guiding_state.py new file mode 100644 index 000000000..012237dbd --- /dev/null +++ b/qualtran/bloqs/max_k_xor_sat/guiding_state.py @@ -0,0 +1,445 @@ +# 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. +r"""Prepare the guiding state for a kXOR instance $\mathcal{I}$ with +Kikuchi parameter $\ell$. + +References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Section 4.4.1, Theorem 4.15. +""" +from functools import cached_property + +from attrs import evolve, field, frozen + +from qualtran import ( + Bloq, + bloq_example, + BloqBuilder, + BloqDocSpec, + CtrlSpec, + DecomposeTypeError, + QAny, + QBit, + QUInt, + Register, + Signature, + Soquet, + SoquetT, +) +from qualtran.bloqs.arithmetic.lists import HasDuplicates, SortInPlace +from qualtran.bloqs.basic_gates import Hadamard, OnEach, XGate +from qualtran.bloqs.bookkeeping import Partition +from qualtran.bloqs.mcmt import MultiControlX +from qualtran.bloqs.state_preparation.prepare_base import PrepareOracle +from qualtran.bloqs.state_preparation.sparse_state_preparation_via_rotations import ( + SparseStatePreparationViaRotations, +) +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator +from qualtran.symbolics import ceil, is_symbolic, log2, pi, SymbolicFloat, SymbolicInt + +from .kxor_instance import KXorInstance + + +@frozen +class SimpleGuidingState(PrepareOracle): + r"""Prepare the guiding state for $\ell = k$. + + Given an kXOR instance $\mathcal{I}$, prepare the guiding state for + parameter $\ell = k$ (i.e. $c = 1$), defined in Eq 134: + $$ + |\phi\rangle + \propto + |\Gamma^k(\mathcal{A})\rangle + = + \frac{1}{\sqrt{\tilde{m}}} + \sum_{S \in {[n] \choose k}} B_\mathcal{I}(S) |S\rangle + $$ + + Here, $\tilde{m}$ is the number of constraints in the input instance $\mathcal{I}$, + and $\mathcal{A} = \sqrt{\frac{{n\choose k}}{\tilde{m}}} \mathcal{I}$. + + This bloq has a gate cost of $O(\tilde{m} \log n)$ (see Eq 142 and paragraph below). + + Args: + inst: the kXOR instance $\mathcal{I}$. + eps: Precision of the prepared state (defaults to 1e-6). + + Registers: + S: a scope of $k$ variables, each in $[n]$. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Equation 134. + """ + + inst: KXorInstance + eps: SymbolicFloat = field(default=1e-6, kw_only=True) + + @property + def signature(self) -> 'Signature': + return Signature.build_from_dtypes(S=QAny(self.target_bitsize)) + + @property + def target_bitsize(self): + """number of bits to represent a k-subset S""" + return self.inst.k * self.inst.index_bitsize + + @property + def selection_registers(self) -> tuple[Register, ...]: + return (Register('S', QAny(self.target_bitsize)),) + + @property + def phasegrad_bitsize(self) -> SymbolicInt: + return ceil(log2(2 * pi(self.eps) / self.eps)) + + @property + def _state_prep_bloq(self) -> SparseStatePreparationViaRotations: + N = 2**self.target_bitsize + + if self.inst.is_symbolic(): + bloq = SparseStatePreparationViaRotations.from_n_coeffs( + N, self.inst.num_unique_constraints, phase_bitsize=self.phasegrad_bitsize + ) + else: + assert not is_symbolic(self.inst.batched_scopes) + + bloq = SparseStatePreparationViaRotations.from_coefficient_map( + N, + {self.inst.scope_as_int(S): B_I for S, B_I in self.inst.batched_scopes}, + self.phasegrad_bitsize, + ) + + bloq = evolve(bloq, target_bitsize=self.target_bitsize) + return bloq + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': + return {self._state_prep_bloq: 1} + + +@bloq_example +def _simple_guiding_state() -> SimpleGuidingState: + from qualtran.bloqs.max_k_xor_sat import Constraint, KXorInstance + + inst = KXorInstance( + n=4, + k=2, + constraints=( + Constraint(S=(0, 1), b=1), + Constraint(S=(2, 3), b=-1), + Constraint(S=(1, 2), b=1), + ), + ) + simple_guiding_state = SimpleGuidingState(inst) + return simple_guiding_state + + +@bloq_example +def _simple_guiding_state_symb() -> SimpleGuidingState: + import sympy + + from qualtran.bloqs.max_k_xor_sat import KXorInstance + + n, m, k = sympy.symbols("n m k", positive=True, integer=True) + inst = KXorInstance.symbolic(n=n, m=m, k=k) + simple_guiding_state_symb = SimpleGuidingState(inst) + return simple_guiding_state_symb + + +_SIMPLE_GUIDING_STATE_DOC = BloqDocSpec( + bloq_cls=SimpleGuidingState, examples=[_simple_guiding_state_symb, _simple_guiding_state] +) + + +@frozen +class ProbabilisticUncompute(Bloq): + """Probabilistically uncompute a register using hadamards, and mark success in a flag qubit + + Apply hadamards to the register, and mark the flag conditioned on all input qubits being 0. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Eq. 129 and Eq. 130. + """ + + bitsize: SymbolicInt + + @property + def signature(self) -> 'Signature': + return Signature.build_from_dtypes(q=QAny(self.bitsize), flag=QBit()) + + def build_composite_bloq( + self, bb: 'BloqBuilder', q: 'Soquet', flag: 'Soquet' + ) -> dict[str, 'SoquetT']: + q = bb.add(OnEach(self.bitsize, Hadamard()), q=q) + q, flag = bb.add( + XGate().controlled(CtrlSpec(qdtypes=QAny(self.bitsize), cvs=0)), ctrl=q, q=flag + ) + return {'q': q, 'flag': flag} + + +@frozen +class GuidingState(PrepareOracle): + r"""Prepare a guiding state for a kXOR instance with parameter $\ell$. + + Given an kXOR instance $\mathcal{I}$, and parameter $\ell$ (a multiple of $k$), + we want to prepare the unit-length guiding state $|\mathbb{\Psi}\rangle$ (Eq 135): + + $$ + |\mathbb{\Psi}\rangle + \propto + |\Gamma^\ell(\mathcal{A})\rangle + \propto + \sum_{T \in {[n] \choose \ell}} + \sum_{\{S_1, \ldots, S_c\} \in \text{Part}_k(T)} + \left( + \prod_{j = 1}^c B_{\mathcal{I}}(S) + \right) + |T\rangle + $$ + + This bloq prepares the state (Eq 136): + $$ \beta |\mathbb{\Psi}\rangle |0^{\ell \log \ell + 3}\rangle + + |\perp\rangle |1\rangle + $$ + where $\beta \ge \Omega(1 / \ell^{\ell/2})$, + and $\tilde{m}$ is the number of constraints in $\mathcal{I}$. + + This has a gate cost of $O(\ell \tilde{m} \log n)$. + + Args: + inst: the kXOR instance $\mathcal{I}$. + ell: the Kikuchi parameter $\ell$. + amplitude_good_part: (optional) the amplitude $\beta$ of the guiding state $|\Psi\rangle$ + Defaults to $\beta = 0.99 / \ell^{\ell/2}$. + eps: Precision of the prepared state (defaults to 1e-6). + + Registers: + T: $\ell$ indices each in $[n]$. + ancilla (RIGHT): (entangled) $\ell\log\ell+3$ ancilla qubits used for state preparation. + The all zeros state of the ancilla is the good subspace. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Section 4.4.1 "Preparing the guiding state", Theorem 4.15. Eq 136. + """ + + inst: KXorInstance + ell: SymbolicInt + amplitude_good_part: SymbolicFloat = field(kw_only=True) + eps: SymbolicFloat = field(default=1e-6, kw_only=True) + + @amplitude_good_part.default + def _default_amplitude(self): + return self.coeff_good + + @property + def signature(self) -> 'Signature': + return Signature( + [ + Register('T', QAny(self.target_bitsize)), + Register('ancilla', QAny(self.ancilla_bitsize)), + ] + ) + + @property + def target_bitsize(self) -> SymbolicInt: + return self._index_dtype.num_qubits * self.ell + + @property + def ancilla_bitsize(self) -> SymbolicInt: + r"""total number of entangled ancilla. + + $\ell \log \ell$ for sorting, and 3 flag qubits. + """ + return self.sort_ancilla_bitsize + 3 + + @property + def selection_registers(self) -> tuple[Register, ...]: + return (Register('T', QAny(self.target_bitsize)),) + + @property + def junk_registers(self) -> tuple[Register, ...]: + return (Register('ancilla', QAny(self.ancilla_bitsize)),) + + @property + def sort_ancilla_bitsize(self): + r"""Number of entangled ancilla generated by the sorting algorithm. + + This is a sequence of $\ell$ numbers, each in $[\ell]$, therefore is $\ell \lceil \log \ell \rceil$. + """ + logl = ceil(log2(self.ell)) + return self.ell * logl + + @property + def c(self) -> SymbolicInt: + r"""Value of $c = \ell / k$.""" + c = self.ell // self.inst.k + try: + return int(c) + except TypeError: + pass + return c + + @property + def simple_guiding_state(self) -> SimpleGuidingState: + r"""The simple guiding state $|\phi\rangle$ + + This is the simple guiding state defined in Eq. 142, + which is proportional to $|\Gamma^k\rangle$ (Eq. 134). + We will use $c$ copies of this state to prepare the required guiding state. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Section 4.4.1 "Preparing the guiding state", Eq. 134. + """ + return SimpleGuidingState(self.inst, eps=self.eps / self.c) + + @property + def _index_dtype(self) -> QUInt: + return QUInt(self.inst.index_bitsize) + + def build_composite_bloq( + self, bb: 'BloqBuilder', T: 'Soquet', ancilla: 'Soquet' + ) -> dict[str, 'SoquetT']: + if is_symbolic(self.c): + raise DecomposeTypeError(f"cannot decompose {self} with symbolic c=l/k={self.c}") + + partition_ancilla = Partition( + self.ancilla_bitsize, + ( + Register('ancilla', QAny(self.sort_ancilla_bitsize)), + Register('flags', QBit(), shape=(3,)), + ), + ) + + ancilla, [flag_duplicates, flag_uncompute, flag] = bb.add(partition_ancilla, x=ancilla) + + # Equation 144: |Phi> = |phi>^{\otimes c} + partition_T_to_S = Partition( + self.target_bitsize, + (Register('S', dtype=QAny(self.simple_guiding_state.target_bitsize), shape=(self.c,)),), + ) + S = bb.add(partition_T_to_S, x=T) + for i in range(self.c): + S[i] = bb.add(self.simple_guiding_state, S=S[i]) + T = bb.add(partition_T_to_S.adjoint(), S=S) + + # sort T using `l log l` entangled clean ancilla + T, ancilla = bb.add_and_partition( + SortInPlace(self.ell, self._index_dtype), + [ + (Register('T', QAny(self.target_bitsize)), ['xs']), + (Register('ancilla', QAny(self.sort_ancilla_bitsize)), ['pi']), + ], + T=T, + ancilla=ancilla, + ) + + # mark if T has duplicates (i.e. not disjoint) (Eq 145) + T, flag_duplicates = bb.add_and_partition( + HasDuplicates(self.ell, self._index_dtype), + [ + (Register('T', QAny(self.target_bitsize)), ['xs']), + (Register('flag', QBit()), ['flag']), + ], + T=T, + flag=flag_duplicates, + ) + + # probabilistically uncompute the sorting ancilla, and mark in a flag bit + # note: flag is 0 for success (like syscall/c exit codes) + ancilla, flag_uncompute = bb.add( + ProbabilisticUncompute(self.sort_ancilla_bitsize), q=ancilla, flag=flag_uncompute + ) + + # compute the overall flag using OR, to obtain Eq 130. + [flag_duplicates, flag_uncompute], flag = bb.add( + MultiControlX(cvs=(0, 0)), controls=[flag_duplicates, flag_uncompute], target=flag + ) + flag = bb.add(XGate(), q=flag) + + # join all the ancilla into a single bag of bits + ancilla = bb.add( + partition_ancilla.adjoint(), + ancilla=ancilla, + flags=[flag_duplicates, flag_uncompute, flag], + ) + + return {'T': T, 'ancilla': ancilla} + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT: + return { + self.simple_guiding_state: self.c, + SortInPlace(self.ell, self._index_dtype): 1, + HasDuplicates(self.ell, self._index_dtype): 1, + ProbabilisticUncompute(self.sort_ancilla_bitsize): 1, + MultiControlX(cvs=(0, 0)): 1, + XGate(): 1, + } + + @cached_property + def coeff_good(self): + """lower bound on beta, the coefficient of the good state. + + Sentence below Eq. 147. + """ + return 0.99 / 2 ** (self.sort_ancilla_bitsize / 2) + + +@bloq_example +def _guiding_state() -> GuidingState: + from qualtran.bloqs.max_k_xor_sat import Constraint, KXorInstance + + inst = KXorInstance( + n=4, + k=2, + constraints=( + Constraint(S=(0, 1), b=1), + Constraint(S=(2, 3), b=-1), + Constraint(S=(1, 2), b=1), + ), + ) + guiding_state = GuidingState(inst, ell=4) + return guiding_state + + +@bloq_example +def _guiding_state_symb() -> GuidingState: + import sympy + + from qualtran.bloqs.max_k_xor_sat import KXorInstance + + n, m, k = sympy.symbols("n m k", positive=True, integer=True) + inst = KXorInstance.symbolic(n=n, m=m, k=k) + c = 2 + guiding_state_symb = GuidingState(inst, ell=c * inst.k) + return guiding_state_symb + + +@bloq_example +def _guiding_state_symb_c() -> GuidingState: + import sympy + + from qualtran.bloqs.max_k_xor_sat import KXorInstance + + n, m, c = sympy.symbols("n m c", positive=True, integer=True) + k = sympy.symbols("k", positive=True, integer=True, even=True) + inst = KXorInstance.symbolic(n=n, m=m, k=k) + guiding_state_symb_c = GuidingState(inst, ell=c * k) + return guiding_state_symb_c + + +_GUIDING_STATE_DOC = BloqDocSpec( + bloq_cls=GuidingState, examples=[_guiding_state_symb_c, _guiding_state_symb, _guiding_state] +) diff --git a/qualtran/bloqs/max_k_xor_sat/guiding_state_test.py b/qualtran/bloqs/max_k_xor_sat/guiding_state_test.py new file mode 100644 index 000000000..f95c33ca9 --- /dev/null +++ b/qualtran/bloqs/max_k_xor_sat/guiding_state_test.py @@ -0,0 +1,132 @@ +# 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 sympy + +import qualtran.testing as qlt_testing +from qualtran.bloqs.max_k_xor_sat.guiding_state import ( + _guiding_state, + _guiding_state_symb, + _guiding_state_symb_c, + _simple_guiding_state, + _simple_guiding_state_symb, +) +from qualtran.resource_counting import big_O, GateCounts, get_cost_value, QECGatesCost +from qualtran.symbolics import bit_length, ceil, log2 + + +@pytest.mark.parametrize( + "bloq_ex", + [ + _simple_guiding_state, + _simple_guiding_state_symb, + _guiding_state, + _guiding_state_symb, + _guiding_state_symb_c, + ], + ids=lambda b: b.name, +) +def test_examples(bloq_autotester, bloq_ex): + if bloq_autotester.check_name == 'serialize': + pytest.skip() + + bloq_autotester(bloq_ex) + + +def test_t_cost_simple(): + bloq = _simple_guiding_state() + gc = get_cost_value(bloq, QECGatesCost()) + B_GRAD = bloq.phasegrad_bitsize + + assert gc == GateCounts(and_bloq=24, toffoli=3 * (B_GRAD - 2), clifford=ANY, measurement=ANY) + + +def test_t_cost_simple_symb(): + bloq = _simple_guiding_state_symb() + gc = get_cost_value(bloq, QECGatesCost()) + B_GRAD = bloq.phasegrad_bitsize + + n, m, k = bloq.inst.n, bloq.inst.m, bloq.inst.k + klogn = k * ceil(log2(n)) + # https://github.com/quantumlib/Qualtran/issues/1341 + klogn_roundtrip = bit_length(2**klogn - 1) + + assert gc == GateCounts( + # O(k m log n) + and_bloq=4 * m + (2 * m + 1) * (klogn_roundtrip - 1) - 4, + toffoli=2 * (B_GRAD - 2), + clifford=ANY, + measurement=ANY, + ) + + +def test_t_cost(): + bloq = _guiding_state() + gc = get_cost_value(bloq, QECGatesCost()) + B_GRAD = bloq.simple_guiding_state.phasegrad_bitsize + + assert gc == GateCounts( + and_bloq=352, toffoli=6 * (B_GRAD - 2), cswap=192, clifford=ANY, measurement=ANY + ) + + +@pytest.mark.parametrize("bloq_ex", [_guiding_state_symb, _guiding_state_symb_c]) +def test_t_cost_symb_c(bloq_ex): + bloq = bloq_ex() + gc = get_cost_value(bloq, QECGatesCost()) + B_GRAD = bloq.simple_guiding_state.phasegrad_bitsize + + n, m, k = bloq.inst.n, bloq.inst.m, bloq.inst.k + l, c = bloq.ell, bloq.c + + logn = ceil(log2(n)) + logl = ceil(log2(l)) + + klogn = k * logn + # https://github.com/quantumlib/Qualtran/issues/1341 + klogn_roundtrip = bit_length(2**klogn - 1) + + assert gc == GateCounts( + and_bloq=( + 6 * l**2 * (2 * logn + 1) + + l * logl + + l + + c * (4 * m + (2 * m + 1) * (klogn_roundtrip - 1) - 4) + + (l - 1) * logn + - 2 + ), + toffoli=c * (2 * (B_GRAD - 2)), + cswap=6 * l**2 * logn, + clifford=ANY, + measurement=ANY, + ) + + # verify big_O + t_cost = gc.total_t_count() + t_cost = sympy.sympify(t_cost) + t_cost = t_cost.subs(klogn_roundtrip, klogn) + t_cost = t_cost.simplify() + assert t_cost in big_O(l * m * logn + l**2 * logn + B_GRAD * c) + + +@pytest.mark.notebook +def test_notebook(): + qlt_testing.execute_notebook('guiding_state') + + +@pytest.mark.notebook +def test_tutorial(): + qlt_testing.execute_notebook('guiding_state_tutorial') diff --git a/qualtran/bloqs/max_k_xor_sat/guiding_state_tutorial.ipynb b/qualtran/bloqs/max_k_xor_sat/guiding_state_tutorial.ipynb new file mode 100644 index 000000000..82681faf6 --- /dev/null +++ b/qualtran/bloqs/max_k_xor_sat/guiding_state_tutorial.ipynb @@ -0,0 +1,212 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import sympy\n", + "from qualtran.drawing import show_bloq, show_call_graph" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "Let us start with a kXOR instance with $n$ variables and $m$ constraints." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.bloqs.max_k_xor_sat.kxor_instance import KXorInstance\n", + "\n", + "n, m, k = sympy.symbols(\"n m k\", positive=True, integer=True)\n", + "inst = KXorInstance.symbolic(n, m, k)\n", + "inst" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "We first prepare the guiding state to use in the guided sparse hamiltonian algorithm.\n", + "The guiding state is defined by the instance, and a parameter $\\ell$ (a multiple of $k$)\n", + "\n", + "From Theorem 4.15 of the paper, this should be a circuit of $O(\\ell m \\log n)$ gates,\n", + "and prepare the state $\\beta |\\Psi\\rangle|0^{\\ell \\log \\ell}\\rangle + |\\perp\\rangle|1\\rangle$,\n", + "where $\\beta \\ge 0.99 / \\ell^{\\ell/2}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.bloqs.max_k_xor_sat.guiding_state import GuidingState\n", + "\n", + "c = sympy.symbols(\"c\", positive=True, integer=True)\n", + "l = c * k\n", + "guiding_state = GuidingState(inst, l)\n", + "show_call_graph(guiding_state.call_graph(max_depth=1)[0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "guiding_state_3 = GuidingState(inst, 3 * k)\n", + "show_bloq(guiding_state_3.decompose_bloq())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "We can also build the guiding state for a concrete (non symbolic) instance:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "inst = KXorInstance.random_instance(n=20, m=100, k=4, planted_advantage=0.8, rng=np.random.default_rng(100))\n", + "guiding_state_concrete = GuidingState(inst, ell=12)\n", + "show_bloq(guiding_state_concrete)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "show_bloq(guiding_state_concrete.decompose_bloq())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "show_bloq(guiding_state_concrete.decompose_bloq().flatten_once())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "Let us evaluate the gate cost for the above bloqs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.resource_counting import get_cost_value, QECGatesCost\n", + "\n", + "get_cost_value(guiding_state_concrete, QECGatesCost())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "gc = get_cost_value(guiding_state, QECGatesCost())\n", + "t_cost = gc.total_t_count(ts_per_toffoli=4, ts_per_cswap=4, ts_per_and_bloq=4)\n", + "t_cost" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "from qualtran.symbolics import ceil, log2, floor\n", + "from qualtran.resource_counting import big_O\n", + "\n", + "# simplify some expressions that sympy could not\n", + "klogn = k * ceil(log2(n))\n", + "klogn_long = ceil(log2(floor(2**klogn)))\n", + "t_cost = t_cost.subs(klogn_long, klogn)\n", + "t_cost = t_cost.simplify()\n", + "\n", + "# replace l with a symbol\n", + "l_symb = sympy.symbols(r\"\\ell\", positive=True, integer=True)\n", + "t_cost = t_cost.subs(c * k, l_symb)\n", + "\n", + "big_O(t_cost) # matches paper Theorem 4.15 (as c, l are O(m))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "show_call_graph(guiding_state_concrete, max_depth=3)" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "As we know that $c = \\ell/k \\le \\ell$ and $\\ell \\le m$, the above expression matches the paper result of $O(\\ell m \\log_2(n))$ 1/2-qubit gates.\n", + "" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/qualtran/bloqs/max_k_xor_sat/kxor_instance.py b/qualtran/bloqs/max_k_xor_sat/kxor_instance.py new file mode 100644 index 000000000..913ae06ae --- /dev/null +++ b/qualtran/bloqs/max_k_xor_sat/kxor_instance.py @@ -0,0 +1,273 @@ +# 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 itertools +from collections import defaultdict +from functools import cached_property +from typing import cast, Sequence, TypeAlias, Union + +import numpy as np +import sympy +from attrs import evolve, field, frozen +from numpy.typing import NDArray + +from qualtran.symbolics import bit_length, ceil, HasLength, is_symbolic, log2, slen, SymbolicInt + +Scope: TypeAlias = Union[tuple[int, ...], HasLength] +"""A subset of variables""" + + +def _sort_scope(S: Scope) -> Scope: + if is_symbolic(S): + return S + return tuple(sorted(S)) + + +@frozen +class Constraint: + """A single kXOR constraint. + + Definition 2.1. + + Note: n, k are not stored here, but only in the instance. + + Attributes: + S: the scope - subset of `[n]` of size k. + b: +1 or -1. + """ + + S: Scope = field(converter=_sort_scope) + b: SymbolicInt = field() + + @classmethod + def random(cls, n: int, k: int, *, rng: np.random.Generator): + """Single random constraint, Notation 2.3.""" + S = tuple(rng.choice(n, k, replace=False)) + b = rng.choice([-1, +1]) + return cls(S, b) + + @classmethod + def random_planted(cls, n: int, k: int, *, rho: float, z: NDArray, rng: np.random.Generator): + """Single planted constraint, Notation 2.4.""" + S = tuple(rng.choice(n, k, replace=False)) + eta = (-1) ** (rng.random() < (1 + rho) / 2) # i.e. expectation rho. + unplanted = cls(S, 1) # supporting constraint to evaluate z^S + b = eta * unplanted.evaluate(z) + return cls(S, b) + + @classmethod + def symbolic(cls, n: SymbolicInt, ix: int): + return cls(HasLength(n), sympy.Symbol(f"b_{ix}")) + + def is_symbolic(self): + return is_symbolic(self.S, self.b) + + def evaluate(self, x: NDArray[np.integer]): + return np.prod(x[np.array(self.S)]) + + +@frozen +class KXorInstance: + r"""A kXOR instance $\mathcal{I}$. + + Definition 2.1: A kXOR instance $\mathcal{I}$ over variables indexed by $[n]$ + consists of a multiset of constraints $\mathcal{C} = (S, b)$, where each scope + $S \subseteq [n]$ has cardinality $k$, and each right-hand side $b \in \{\pm 1\}$. + + Attributes: + n: number of variables. + k: number of variables per clause. + constraints: a tuple of `m` Constraints. + max_rhs: maximum value of the RHS polynomial $B_\mathcal{I}(S)$. + see default constructor for default value. In case the instance is symbolic, + the user can specify an expression for this, to avoid the default value. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Definition 2.1. + """ + + n: SymbolicInt + k: SymbolicInt + constraints: Union[tuple[Constraint, ...], HasLength] + max_rhs: SymbolicInt = field() + + @max_rhs.default + def _default_max_rhs(self): + """With very high probability, the max entry will be quite small. + + This is a classical preprocesing step. Time $m$. + """ + if is_symbolic(self.constraints) or is_symbolic(*self.constraints): + # user did not provide a value, assume some small constant + return 2 + + # instance is not symbolic, so we can compute the exact value. + assert isinstance(self.batched_scopes, tuple) + return max(abs(b) for _, b in self.batched_scopes) + + @cached_property + def m(self): + return slen(self.constraints) + + @classmethod + def random_instance( + cls, n: int, m: int, k: int, *, planted_advantage: float = 0, rng: np.random.Generator + ): + r"""Generate a random kXOR instance with the given planted advantage. + + `planted_advantage=0` generates random instances, and `1` generates a + linear system with a solution. + + Args: + n: number of variables + m: number of clauses + k: number of terms per clause + planted_advantage: $\rho$ + rng: random generator + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Notation 2.4. + """ + # planted vector + z = rng.choice([-1, +1], size=n) + + # constraints + constraints = tuple( + Constraint.random_planted(n=n, k=k, rho=planted_advantage, z=z, rng=rng) + for _ in range(m) + ) + + return cls(n=n, k=k, constraints=constraints) + + @classmethod + def symbolic(cls, n: SymbolicInt, m: SymbolicInt, k: SymbolicInt, *, max_rhs: SymbolicInt = 2): + """Create a symbolic instance with n variables, m constraints.""" + constraints = HasLength(m) + return cls(n=n, k=k, constraints=constraints, max_rhs=max_rhs) + + def is_symbolic(self): + if is_symbolic(self.n, self.m, self.k, self.constraints): + return True + assert isinstance(self.constraints, tuple) + return is_symbolic(*self.constraints) + + def subset(self, indices: Union[Sequence[int], HasLength]) -> 'KXorInstance': + """Pick a subset of clauses defined by the set of indices provided.""" + if self.is_symbolic() or is_symbolic(indices): + return evolve(self, constraints=HasLength(slen(indices))) + assert isinstance(self.constraints, tuple) + + constraints = tuple(self.constraints[i] for i in indices) + return evolve(self, constraints=constraints) + + @cached_property + def index_bitsize(self): + """number of bits required to represent the index of a variable, i.e. `[n]` + + We assume zero-indexing. + """ + return ceil(log2(self.n)) + + @cached_property + def num_unique_constraints(self) -> SymbolicInt: + return slen(self.batched_scopes) + + @cached_property + def batched_scopes(self) -> Union[tuple[tuple[Scope, int], ...], HasLength]: + r"""Group all the constraints by Scope, and add up the $b$ values. + + This is a classical preprocessing step. Time $k m \log m$. + """ + if self.is_symbolic(): + return HasLength(self.m) + + assert isinstance(self.constraints, tuple) + + batches: dict[Scope, int] = defaultdict(lambda: 0) + for con in self.constraints: + assert isinstance(con.S, tuple) + batches[con.S] += con.b + + batches_sorted = sorted(batches.items(), key=lambda c: c[1]) + return tuple(batches_sorted) + + @cached_property + def rhs_sum_bitsize(self): + r"""number of bits to represent the RHS polynomial $B_{\mathcal{I}}(S)$.""" + return bit_length(2 * self.max_rhs) + + def scope_as_int(self, S: Scope) -> int: + r"""Convert a scope into a single integer. + + Given a scope `S = (x_1, x_2, ..., x_k)`, and a bitsize `r` for each index, + the integer representation is given by concatenating `r`-bit unsigned repr + of each `x_i`. That is, $\sum_i r^{k - i} x_i$. + + This uses Big-endian representation, like all qualtran dtypes. + + The bitsize `r` is picked as `ceil(log(n))` for an n-variable instance. + """ + assert not is_symbolic(S) + + bitsize = self.index_bitsize + + result = 0 + for x in S: + result = (result << bitsize) + x + return result + + def brute_force_sparsity(self, ell: int) -> int: + r"""Compute the sparsity of the Kikuchi matrix with parameter $\ell$ by brute force. + + Takes time `O(C(n, l) * m * l)`. Extremely slow, use with caution. + """ + assert isinstance(self.n, int) + s = 0 + for S in itertools.combinations(range(self.n), ell): + nz = 0 + for U, _ in cast(tuple, self.batched_scopes): + T = set(S).symmetric_difference(U) + if len(T) == ell: + nz += 1 + s = max(s, nz) + return s + + +def example_kxor_instance() -> KXorInstance: + n, k = 10, 4 + cs = ( + Constraint((0, 1, 2, 3), -1), + Constraint((0, 2, 4, 5), 1), + Constraint((0, 3, 4, 5), 1), + Constraint((0, 3, 4, 5), 1), + Constraint((1, 2, 3, 4), -1), + Constraint((1, 3, 4, 5), -1), + Constraint((1, 3, 4, 5), -1), + Constraint((2, 3, 4, 5), 1), + ) + inst = KXorInstance(n, k, cs) + return inst diff --git a/qualtran/bloqs/max_k_xor_sat/kxor_instance_test.py b/qualtran/bloqs/max_k_xor_sat/kxor_instance_test.py new file mode 100644 index 000000000..700dfd6eb --- /dev/null +++ b/qualtran/bloqs/max_k_xor_sat/kxor_instance_test.py @@ -0,0 +1,30 @@ +# 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 numpy as np +import pytest + +from .kxor_instance import KXorInstance + + +@pytest.mark.slow +@pytest.mark.parametrize("rho", [0, 0.8, 0.9]) +def test_max_rhs(rho: float): + rng = np.random.default_rng(402) + + rhs = [] + for i in range(100): + inst = KXorInstance.random_instance(n=100, m=1000, k=4, planted_advantage=rho, rng=rng) + rhs.append(inst.max_rhs) + + assert max(rhs) == 2