From 62782b637174363d127f2eeddd21242c7892f925 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Thu, 13 Feb 2025 16:11:02 +0100 Subject: [PATCH 01/10] Planted Noisy kXOR: Implement Kikuchi graph oracles --- .../qualtran_dev_tools/notebook_specs.py | 31 +- docs/bloqs/index.rst | 4 + .../bloqs/optimization/k_xor_sat/__init__.py | 3 + .../k_xor_sat/kikuchi_adjacency_list.ipynb | 180 +++++++++ .../k_xor_sat/kikuchi_adjacency_list.py | 362 ++++++++++++++++++ .../k_xor_sat/kikuchi_adjacency_list_test.py | 78 ++++ .../k_xor_sat/kikuchi_adjacency_matrix.ipynb | 197 ++++++++++ .../k_xor_sat/kikuchi_adjacency_matrix.py | 184 +++++++++ .../kikuchi_adjacency_matrix_test.py | 93 +++++ .../k_xor_sat/kikuchi_block_encoding.ipynb | 183 +++++++++ .../k_xor_sat/kikuchi_block_encoding.py | 236 ++++++++++++ .../k_xor_sat/kikuchi_block_encoding_test.py | 58 +++ .../k_xor_sat/load_kxor_instance.ipynb | 207 ++++++++++ .../k_xor_sat/load_kxor_instance.py | 354 +++++++++++++++++ .../k_xor_sat/load_kxor_instance_test.py | 49 +++ 15 files changed, 2218 insertions(+), 1 deletion(-) create mode 100644 qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.ipynb create mode 100644 qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py create mode 100644 qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py create mode 100644 qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.ipynb create mode 100644 qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py create mode 100644 qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py create mode 100644 qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.ipynb create mode 100644 qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.py create mode 100644 qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py create mode 100644 qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.ipynb create mode 100644 qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py create mode 100644 qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance_test.py diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py index 0f9840569f..ab9721d806 100644 --- a/dev_tools/qualtran_dev_tools/notebook_specs.py +++ b/dev_tools/qualtran_dev_tools/notebook_specs.py @@ -104,6 +104,8 @@ import qualtran.bloqs.multiplexers.black_box_select import qualtran.bloqs.multiplexers.select_base import qualtran.bloqs.multiplexers.select_pauli_lcu +import qualtran.bloqs.optimization.k_xor_sat +import qualtran.bloqs.optimization.k_xor_sat.guided_hamiltonian import qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state import qualtran.bloqs.phase_estimation.lp_resource_state import qualtran.bloqs.phase_estimation.qubitization_qpe @@ -804,6 +806,12 @@ # ----- Optimization --------------------------------------------------- # -------------------------------------------------------------------------- OPTIMIZATION: List[NotebookSpecV2] = [ + # ----- Algorithm ------------------------------------------ + NotebookSpecV2( + title='kXOR: Instance load Oracles', + module=qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance, + bloq_specs=[qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance._LOAD_INSTANCE_DOC], + ), NotebookSpecV2( title='Planted Noisy kXOR - Kikuchi Guiding State', module=qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state, @@ -811,7 +819,28 @@ qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state._SIMPLE_GUIDING_STATE_DOC, qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state._GUIDING_STATE_DOC, ], - ) + ), + NotebookSpecV2( + title='Planted Noisy kXOR: Kikuchi Adjacency List', + module=qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list, + bloq_specs=[ + qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list._KIKUCHI_NONZERO_INDEX_DOC + ], + ), + NotebookSpecV2( + title='Planted Noisy kXOR: Kikuchi Adjacency Matrix', + module=qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_matrix, + bloq_specs=[ + qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_matrix._KIKUCHI_MATRIX_ENTRY_DOC + ], + ), + NotebookSpecV2( + title='Planted Noisy kXOR: Block-encoding the Kikuchi Matrix', + module=qualtran.bloqs.optimization.k_xor_sat.kikuchi_block_encoding, + bloq_specs=[ + qualtran.bloqs.optimization.k_xor_sat.kikuchi_block_encoding._KIKUCHI_HAMILTONIAN_DOC + ], + ), ] # -------------------------------------------------------------------------- diff --git a/docs/bloqs/index.rst b/docs/bloqs/index.rst index 3f0dbbac0a..ee9d363e6d 100644 --- a/docs/bloqs/index.rst +++ b/docs/bloqs/index.rst @@ -147,7 +147,11 @@ Bloqs Library :maxdepth: 2 :caption: Optimization: + optimization/k_xor_sat/load_kxor_instance.ipynb optimization/k_xor_sat/kikuchi_guiding_state.ipynb + optimization/k_xor_sat/kikuchi_adjacency_list.ipynb + optimization/k_xor_sat/kikuchi_adjacency_matrix.ipynb + optimization/k_xor_sat/kikuchi_block_encoding.ipynb .. toctree:: :maxdepth: 2 diff --git a/qualtran/bloqs/optimization/k_xor_sat/__init__.py b/qualtran/bloqs/optimization/k_xor_sat/__init__.py index 4cbf2722c9..4e65bfd7b9 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/__init__.py +++ b/qualtran/bloqs/optimization/k_xor_sat/__init__.py @@ -11,5 +11,8 @@ # 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 .kikuchi_adjacency_list import KikuchiNonZeroIndex +from .kikuchi_adjacency_matrix import KikuchiMatrixEntry +from .kikuchi_block_encoding import KikuchiHamiltonian, KikuchiMatrixEntry, KikuchiNonZeroIndex from .kikuchi_guiding_state import GuidingState, SimpleGuidingState from .kxor_instance import Constraint, KXorInstance diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.ipynb b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.ipynb new file mode 100644 index 0000000000..efb128a69b --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.ipynb @@ -0,0 +1,180 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3c1703d8", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# Planted Noisy kXOR: Kikuchi Adjacency List" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ece15719", + "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": "c187b17d", + "metadata": { + "cq.autogen": "KikuchiNonZeroIndex.bloq_doc.md" + }, + "source": [ + "## `KikuchiNonZeroIndex`\n", + "Adjacency list oracle $O_F$ for the Kikuchi matrix.\n", + "\n", + "The oracle $O_F$ (Definition 4.5) takes in $i, k$,\n", + "and outputs $i, f(i, k)$ where $f(i, k)$ is\n", + "index of the $k$-th non-zero entry in row $i$.\n", + "\n", + "As the Kikuchi matrix is symmetric, we can use the same oracle for both rows and columns.\n", + "\n", + "The Kikuchi matrix is indexed by $S \\in {[n] \\choose k}$.\n", + "For a given row $S$ and column $T$, the entry $\\mathcal{K}_{k}_{S, T}$\n", + "is potentially non-zero if $S \\Delta T = U_j$ for some $j$, which is\n", + "equivalent to $T = S \\Delta U_j$.\n", + "Here, $U_j$ is the $j$-th unique scope in the instance $\\mathcal{I}$.\n", + "\n", + "To find the $k$-th non-zero entry, we use two oracles:\n", + "1. $(S, k) \\mapsto f(S, k)$, implemented by `ColumnOfKthNonZeroEntry`\n", + "2. $(S, f(S, k)) \\mapsto k$, implemented by `IndexOfNonZeroColumn`.\n", + "\n", + "Both these above oracles are unitary: they do not have any entangled ancilla/junk registers.\n", + "\n", + "\n", + "Note on sparsity: This bloq expects the user to provide the sparsity, as it is in general\n", + "difficult to compute the precise sparsity of the Kikuchi matrix efficiently. As long as the\n", + "provided number is at least the true sparsity, the algorithm will work as expected.\n", + "In case the provides sparsity is smaller, it is equivalent to making the remaining entries zero in the final block encoding.\n", + "\n", + "#### Parameters\n", + " - `inst`: the kXOR instance $\\mathcal{I}$.\n", + " - `ell`: Kikuchi parameter $\\ell$.\n", + " - `s`: sparsity, i.e. max number of non-zero entries in a row/column. \n", + "\n", + "#### Registers\n", + " - `i`: integer in [2^N]\n", + " - `k`: integer in [2^N] \n", + "\n", + "#### References\n", + " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Theorem 4.17, proof para 4 (top of page 39).\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d329e657", + "metadata": { + "cq.autogen": "KikuchiNonZeroIndex.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.optimization.k_xor_sat import KikuchiNonZeroIndex" + ] + }, + { + "cell_type": "markdown", + "id": "8516f446", + "metadata": { + "cq.autogen": "KikuchiNonZeroIndex.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "409ea009", + "metadata": { + "cq.autogen": "KikuchiNonZeroIndex.kikuchi_nonzero_index" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import example_kxor_instance\n", + "\n", + "inst = example_kxor_instance()\n", + "ell = 8\n", + "s = inst.brute_force_sparsity(ell)\n", + "\n", + "kikuchi_nonzero_index = KikuchiNonZeroIndex(inst, ell, s=s)" + ] + }, + { + "cell_type": "markdown", + "id": "c08eb466", + "metadata": { + "cq.autogen": "KikuchiNonZeroIndex.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7478a9a", + "metadata": { + "cq.autogen": "KikuchiNonZeroIndex.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([kikuchi_nonzero_index],\n", + " ['`kikuchi_nonzero_index`'])" + ] + }, + { + "cell_type": "markdown", + "id": "2e07ff5a", + "metadata": { + "cq.autogen": "KikuchiNonZeroIndex.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "138befd7", + "metadata": { + "cq.autogen": "KikuchiNonZeroIndex.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "kikuchi_nonzero_index_g, kikuchi_nonzero_index_sigma = kikuchi_nonzero_index.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(kikuchi_nonzero_index_g)\n", + "show_counts_sigma(kikuchi_nonzero_index_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/optimization/k_xor_sat/kikuchi_adjacency_list.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py new file mode 100644 index 0000000000..8b970e6272 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py @@ -0,0 +1,362 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections import Counter + +import numpy as np +import sympy +from attrs import frozen + +from qualtran import ( + Bloq, + bloq_example, + BloqBuilder, + BloqDocSpec, + QBit, + QUInt, + Register, + Signature, + Soquet, + SoquetT, +) +from qualtran.bloqs.arithmetic import AddK, Equals, Xor +from qualtran.bloqs.arithmetic.lists import SymmetricDifference +from qualtran.bloqs.basic_gates import CNOT, ZeroEffect, ZeroState +from qualtran.bloqs.mcmt import And +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator +from qualtran.symbolics import SymbolicInt + +from .kxor_instance import KXorInstance + + +@frozen +class ColumnOfKthNonZeroEntry(Bloq): + r"""Given $(S, k)$, compute the column of the $k$-th non-zero entry in row $S$. + + If the output is denoted as $f(S, k)$, then this bloq maps + $(S, k, z, b)$ to $(S, k, z \oplus f'(S, k), b \oplus (k \ge s))$. + where $s$ is the sparsity, and $f'(S, k)$ is by extending $f$ + such that for all $k \ge s$, $f'(S, k) = k$. + Using $f'$ ensures the computation is reversible. + Note: we must use the same extension $f'$ for both oracles. + + This algorithm is described by the following pseudo-code: + ``` + def forward(S, k) -> f_S_k: + nnz := 0 # counter + for j in range(\bar{m}): + T := S \Delta U_j + if |T| == l: + nnz := nnz + 1 + if nnz == k: + f_S_k ^= T + ``` + + Args: + inst: the kXOR instance $\mathcal{I}$. + ell: Kikuchi parameter $\ell$. + + Registers: + S: index register to store $S \in {[n] \choose \ell}$. + k: non-zero entry index register + T: index register to store output $T = f(S, k) \in {[n] \choose \ell}$. + """ + + inst: KXorInstance + ell: SymbolicInt + + @property + def signature(self) -> 'Signature': + return Signature( + [ + Register('S', self.index_dtype, shape=(self.ell,)), + Register('k', self.index_dtype, shape=(self.ell,)), + Register('T', self.index_dtype, shape=(self.ell,)), + Register('flag', QBit()), + ] + ) + + @property + def index_dtype(self) -> QUInt: + return QUInt(self.inst.index_bitsize) + + def adjoint(self) -> 'ColumnOfKthNonZeroEntry': + return self + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': + m = self.inst.num_unique_constraints + ell, k = self.ell, self.inst.k + + counts_forward = Counter[Bloq]() + + # compute symmetric differences for each constraint + counts_forward[SymmetricDifference(ell, k, ell, self.index_dtype)] += m + + # counter + counts_forward[AddK(self.index_dtype, 1).controlled()] += m + + # compare counter each time + counts_forward[Equals(self.index_dtype)] += m + + # when counter is equal (and updated in this iteration), we can copy the result + counts_forward[And()] += m + counts_forward[CNOT()] += m # flip the final flag (flipped at most once) + + ### all counts + counts = Counter[Bloq]() + + # copy the index (controlled by the final flag) + counts[Xor(self.index_dtype).controlled()] += m + + # if nothing matched (final flag = 0), copy k and flip the flag bit + counts[Xor(self.index_dtype).controlled()] += 1 + counts[Xor(QBit())] += 1 + + for bloq, nb in counts_forward.items(): + # compute and uncompute all intermediate values. + counts[bloq] += nb + counts[bloq.adjoint()] += nb + + return counts + + +@bloq_example +def _col_kth_nz() -> ColumnOfKthNonZeroEntry: + from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import example_kxor_instance + + inst = example_kxor_instance() + ell = 8 + + col_kth_nz = ColumnOfKthNonZeroEntry(inst, ell) + return col_kth_nz + + +@bloq_example +def _col_kth_nz_symb() -> ColumnOfKthNonZeroEntry: + n, m, k, c, s = sympy.symbols("n m k c s", positive=True, integer=True) + inst = KXorInstance.symbolic(n=n, m=m, k=k) + ell = c * k + + col_kth_nz_symb = ColumnOfKthNonZeroEntry(inst, ell) + return col_kth_nz_symb + + +@frozen +class IndexOfNonZeroColumn(Bloq): + r"""Given $(S, T)$, compute $k$ such that $T$ is the $k$-th non-zero entry in row $S$. + + If $f(S, k)$ denotes the $k$-th non-zero entry in row $S$, + then this bloq maps $(S, f'(S, k), z, b)$ to $(S, f'(S, k), z \oplus k, b \oplus )$. + where $s$ is the sparsity, and $f'(S, k)$ is by extending $f$ + such that for all $k \ge s$, $f'(S, k) = k$. + Using $f'$ ensures the computation is reversible. + Note: we must use the same extension $f'$ for both oracles. + + This algorithm is described by the following pseudo-code: + ``` + def reverse(S, f_S_k) -> k: + nnz := 0 # counter + for j in range(\bar{m}): + T := S \Delta U_j + if |T| == l: + nnz := nnz + 1 + if T == f_S_k: + k ^= nnz + ``` + + Args: + inst: the kXOR instance $\mathcal{I}$. + ell: Kikuchi parameter $\ell$. + + Registers: + S: index register to store $S \in {[n] \choose \ell}$. + k: non-zero entry index register + """ + + inst: KXorInstance + ell: SymbolicInt + + @property + def signature(self) -> 'Signature': + return Signature( + [ + Register('S', self.index_dtype, shape=(self.ell,)), + Register('k', self.index_dtype, shape=(self.ell,)), + Register('T', self.index_dtype, shape=(self.ell,)), + Register('flag', QBit()), + ] + ) + + @property + def index_dtype(self) -> QUInt: + return QUInt(self.inst.index_bitsize) + + def adjoint(self) -> 'IndexOfNonZeroColumn': + return self + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': + m = self.inst.num_unique_constraints + ell, k = self.ell, self.inst.k + + counts_forward = Counter[Bloq]() + + # compute symmetric differences for each constraint + counts_forward[SymmetricDifference(ell, k, ell, self.index_dtype)] += m + + # counter + counts_forward[AddK(self.index_dtype, 1).controlled()] += m + + # compare T to f_S_k each time + counts_forward[Equals(self.index_dtype)] += m + + # when T is equal (and counter is updated in this iteration), we can copy the result + counts_forward[And()] += m + counts_forward[CNOT()] += m # flip the final flag (flipped at most once) + + ### all counts + counts = Counter[Bloq]() + + # copy the value of nnz (when final flag = 1) + counts[Xor(self.index_dtype).controlled()] += m + + # if nothing matched (final flag = 0), copy k and flip the flag bit + counts[Xor(self.index_dtype).controlled()] += 1 + counts[Xor(QBit())] += 1 + + for bloq, nb in counts_forward.items(): + # compute and uncompute all intermediate values. + counts[bloq] += nb + counts[bloq.adjoint()] += nb + + return counts + + +@frozen +class KikuchiNonZeroIndex(Bloq): + r"""Adjacency list oracle $O_F$ for the Kikuchi matrix. + + The oracle $O_F$ (Definition 4.5) takes in $i, k$, + and outputs $i, f(i, k)$ where $f(i, k)$ is + index of the $k$-th non-zero entry in row $i$. + + As the Kikuchi matrix is symmetric, we can use the same oracle for both rows and columns. + + The Kikuchi matrix is indexed by $S \in {[n] \choose k}$. + For a given row $S$ and column $T$, the entry $\mathcal{K}_{k}_{S, T}$ + is potentially non-zero if $S \Delta T = U_j$ for some $j$, which is + equivalent to $T = S \Delta U_j$. + Here, $U_j$ is the $j$-th unique scope in the instance $\mathcal{I}$. + + To find the $k$-th non-zero entry, we use two oracles: + 1. $(S, k) \mapsto f(S, k)$, implemented by `ColumnOfKthNonZeroEntry` + 2. $(S, f(S, k)) \mapsto k$, implemented by `IndexOfNonZeroColumn`. + + Both these above oracles are unitary: they do not have any entangled ancilla/junk registers. + + + Note on sparsity: This bloq expects the user to provide the sparsity, as it is in general + difficult to compute the precise sparsity of the Kikuchi matrix efficiently. As long as the + provided number is at least the true sparsity, the algorithm will work as expected. + In case the provides sparsity is smaller, it is equivalent to making the remaining entries zero in the final block encoding. + + Args: + inst: the kXOR instance $\mathcal{I}$. + ell: Kikuchi parameter $\ell$. + s: sparsity, i.e. max number of non-zero entries in a row/column. + + Registers: + i: integer in [2^N] + k: integer in [2^N] + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Theorem 4.17, proof para 4 (top of page 39). + """ + + inst: KXorInstance + ell: SymbolicInt + s: SymbolicInt + + @property + def signature(self) -> 'Signature': + return Signature( + [ + Register('S', self.index_dtype, shape=(self.ell,)), + Register('k', self.index_dtype, shape=(self.ell,)), + ] + ) + + @property + def index_dtype(self) -> QUInt: + return QUInt(self.inst.index_bitsize) + + def build_composite_bloq( + self, bb: 'BloqBuilder', S: 'Soquet', k: 'Soquet' + ) -> dict[str, 'SoquetT']: + T = np.array([bb.allocate(dtype=self.index_dtype) for _ in range(int(self.ell))]) + flag = bb.add(ZeroState()) + S, k, T, flag = bb.add( + ColumnOfKthNonZeroEntry(self.inst, self.ell), S=S, k=k, T=T, flag=flag + ) + S, T, k, flag = bb.add(IndexOfNonZeroColumn(self.inst, self.ell), S=S, T=T, k=k, flag=flag) + for soq in k: + bb.free(soq) + bb.add(ZeroEffect(), q=flag) + return dict(S=S, k=T) + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': + return { + ColumnOfKthNonZeroEntry(self.inst, self.ell): 1, + IndexOfNonZeroColumn(self.inst, self.ell): 1, + } + + +@bloq_example +def _kikuchi_nonzero_index() -> KikuchiNonZeroIndex: + from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import example_kxor_instance + + inst = example_kxor_instance() + ell = 8 + s = inst.brute_force_sparsity(ell) + + kikuchi_nonzero_index = KikuchiNonZeroIndex(inst, ell, s=s) + return kikuchi_nonzero_index + + +@bloq_example +def _kikuchi_nonzero_index_symb() -> KikuchiNonZeroIndex: + from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance + + n, m, k, c, s = sympy.symbols("n m k c s", positive=True, integer=True) + inst = KXorInstance.symbolic(n=n, m=m, k=k) + ell = c * k + + kikuchi_nonzero_index_symb = KikuchiNonZeroIndex(inst, ell, s=s) + return kikuchi_nonzero_index_symb + + +_KIKUCHI_NONZERO_INDEX_DOC = BloqDocSpec( + bloq_cls=KikuchiNonZeroIndex, examples=[_kikuchi_nonzero_index] +) diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py new file mode 100644 index 0000000000..fd6875bdf1 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py @@ -0,0 +1,78 @@ +# 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.resource_counting import big_O, GateCounts, get_cost_value, QECGatesCost +from qualtran.symbolics import ceil, log2 + +from .kikuchi_adjacency_list import ( + _col_kth_nz, + _col_kth_nz_symb, + _kikuchi_nonzero_index, + _kikuchi_nonzero_index_symb, +) + + +@pytest.mark.parametrize( + "bloq_ex", + [_col_kth_nz, _col_kth_nz_symb, _kikuchi_nonzero_index, _kikuchi_nonzero_index_symb], + ids=lambda bloq_ex: bloq_ex.name, +) +def test_examples(bloq_autotester, bloq_ex): + if bloq_autotester.check_name == 'serialize': + pytest.skip() + + bloq_autotester(bloq_ex) + + +def test_cost_col_kth_nz(): + n, m, k, c, s = sympy.symbols("n m k c s", positive=True, integer=True) + l = c * k + logn = ceil(log2(n)) + logl = ceil(log2(l)) + + bloq = _col_kth_nz_symb() + cost = get_cost_value(bloq, QECGatesCost()) + assert cost == GateCounts( + toffoli=(m + 1) * logn, + cswap=4 * l * m * (logl + 1) * logn, + and_bloq=( + 4 * m * (logn - 1) + + ( + 2 + * m + * ( + 2 * l * ((2 * logn + 1) * (logl + 1)) + + l + + k + + 2 * ((logn - 1) * (l + k - 1)) + + 2 * ceil(log2(l + k)) + - 4 + ) + ) + + m + ), + clifford=ANY, + measurement=ANY, + ) + assert big_O(cost.total_t_count()) == big_O(l * m * logn * logl) + + +@pytest.mark.notebook +def test_notebook(): + qlt_testing.execute_notebook('kikuchi_adjacency_list') diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.ipynb b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.ipynb new file mode 100644 index 0000000000..599e88a21a --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f82c4ce3", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# Planted Noisy kXOR: Kikuchi Adjacency Matrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8910a435", + "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": "3702603c", + "metadata": { + "cq.autogen": "KikuchiMatrixEntry.bloq_doc.md" + }, + "source": [ + "## `KikuchiMatrixEntry`\n", + "Adjacency matrix oracle for the Kikuchi matrix.\n", + "\n", + "Given a kXOR instance $\\mathcal{I}$ with $n$ variables, $m$ constraints,\n", + "the Kikuchi matrix with parameter $\\ell$ is indexed by ${[n] \\choose l}$.\n", + "For $S, T \\in {[n] \\choose l}$, the entry is given by\n", + "$H_{S, T} = B_{\\mathcal{I}}(S \\Delta T)/M$, where $M$ is the max entry.\n", + "\n", + "This bloq implements the transform:\n", + " $$\n", + " |0 \\rangle |S\\rangle |T\\rangle\n", + " \\mapsto\n", + " (\\sqrt{H_{S, T}}|0\\rangle + \\sqrt{1 - |H_{S, T}|}|1\\rangle)|S\\rangle |T\\rangle\n", + " $$\n", + "\n", + "This is equivalent to $O_H$ (Def. 4.3) from the paper, but is optimized to classically\n", + "compute the `arccos` of the entries, and directly apply the rotation,\n", + "instead of computing them using a quantum circuit.\n", + "\n", + "This bloq performs the following steps\n", + "1. Compute the symmetric difference $D = S \\Delta T$.\n", + "2. Compute the index $j$ s.t. $U_j = D$ (where $U_j$ are a list of unique scopes)\n", + "4. Apply a controlled Y-rotation with angle for the $j$-th entry.\n", + "5. Uncompute steps 3, 2, 1.\n", + "\n", + "#### Parameters\n", + " - `inst`: k-XOR instance\n", + " - `ell`: the Kikuchi parameter $\\ell$, must be a multiple of $k$.\n", + " - `entry_bitsize`: number of bits to approximate each rotation angle to.\n", + " - `cv`: single bit control value (0 or 1), or None for uncontrolled (default). \n", + "\n", + "#### Registers\n", + " - `S`: row index\n", + " - `T`: column index\n", + " - `q`: the qubit to rotate by $Ry(2 \\arccos(\\sqrt{H_{S,T} / M}))$ as defined above. \n", + "\n", + "#### References\n", + " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Definition 4.3. Theorem 4.17 para 3.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "06c85c6c", + "metadata": { + "cq.autogen": "KikuchiMatrixEntry.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.optimization.k_xor_sat import KikuchiMatrixEntry" + ] + }, + { + "cell_type": "markdown", + "id": "f45d82af", + "metadata": { + "cq.autogen": "KikuchiMatrixEntry.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5a5035e", + "metadata": { + "cq.autogen": "KikuchiMatrixEntry.kikuchi_matrix_entry_symb" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance\n", + "\n", + "n, m, k, c = sympy.symbols(\"n m k c\", positive=True, integer=True)\n", + "inst = KXorInstance.symbolic(n=n, m=m, k=k)\n", + "ell = c * k\n", + "\n", + "kikuchi_matrix_entry_symb = KikuchiMatrixEntry(inst, ell, entry_bitsize=3)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "802ff581", + "metadata": { + "cq.autogen": "KikuchiMatrixEntry.kikuchi_matrix_entry" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import example_kxor_instance\n", + "\n", + "inst = example_kxor_instance()\n", + "ell = 8\n", + "\n", + "kikuchi_matrix_entry = KikuchiMatrixEntry(inst, ell, entry_bitsize=3)" + ] + }, + { + "cell_type": "markdown", + "id": "fd2fd175", + "metadata": { + "cq.autogen": "KikuchiMatrixEntry.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d9b2e3a", + "metadata": { + "cq.autogen": "KikuchiMatrixEntry.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([kikuchi_matrix_entry_symb, kikuchi_matrix_entry],\n", + " ['`kikuchi_matrix_entry_symb`', '`kikuchi_matrix_entry`'])" + ] + }, + { + "cell_type": "markdown", + "id": "649be2c9", + "metadata": { + "cq.autogen": "KikuchiMatrixEntry.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a901dca4", + "metadata": { + "cq.autogen": "KikuchiMatrixEntry.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "kikuchi_matrix_entry_symb_g, kikuchi_matrix_entry_symb_sigma = kikuchi_matrix_entry_symb.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(kikuchi_matrix_entry_symb_g)\n", + "show_counts_sigma(kikuchi_matrix_entry_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/optimization/k_xor_sat/kikuchi_adjacency_matrix.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py new file mode 100644 index 0000000000..c198e7800a --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py @@ -0,0 +1,184 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from collections import Counter +from functools import cached_property + +import attrs +import sympy +from attrs import frozen + +from qualtran import ( + AddControlledT, + Bloq, + bloq_example, + BloqDocSpec, + CtrlSpec, + QAny, + QBit, + QFxp, + QUInt, + Signature, +) +from qualtran.bloqs.arithmetic.lists import SymmetricDifference +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator +from qualtran.symbolics import SymbolicInt + +from .kxor_instance import KXorInstance +from .load_kxor_instance import LoadUniqueScopeIndex, PRGAUniqueConstraintRHS + + +@frozen +class KikuchiMatrixEntry(Bloq): + r"""Adjacency matrix oracle for the Kikuchi matrix. + + Given a kXOR instance $\mathcal{I}$ with $n$ variables, $m$ constraints, + the Kikuchi matrix with parameter $\ell$ is indexed by ${[n] \choose l}$. + For $S, T \in {[n] \choose l}$, the entry is given by + $H_{S, T} = B_{\mathcal{I}}(S \Delta T)/M$, where $M$ is the max entry. + + This bloq implements the transform: + $$ + |0 \rangle |S\rangle |T\rangle + \mapsto + (\sqrt{H_{S, T}}|0\rangle + \sqrt{1 - |H_{S, T}|}|1\rangle)|S\rangle |T\rangle + $$ + + This is equivalent to $O_H$ (Def. 4.3) from the paper, but is optimized to classically + compute the `arccos` of the entries, and directly apply the rotation, + instead of computing them using a quantum circuit. + + This bloq performs the following steps + 1. Compute the symmetric difference $D = S \Delta T$. + 2. Compute the index $j$ s.t. $U_j = D$ (where $U_j$ are a list of unique scopes) + 4. Apply a controlled Y-rotation with angle for the $j$-th entry. + 5. Uncompute steps 3, 2, 1. + + Args: + inst: k-XOR instance + ell: the Kikuchi parameter $\ell$, must be a multiple of $k$. + entry_bitsize: number of bits to approximate each rotation angle to. + cv: single bit control value (0 or 1), or None for uncontrolled (default). + + Registers: + S: row index + T: column index + q: the qubit to rotate by $Ry(2 \arccos(\sqrt{H_{S,T} / M}))$ as defined above. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Definition 4.3. Theorem 4.17 para 3. + """ + + inst: KXorInstance + ell: SymbolicInt + entry_bitsize: SymbolicInt + is_controlled: bool = False + + @property + def signature(self) -> 'Signature': + return Signature.build_from_dtypes( + ctrl=QAny(1 if self.is_controlled else 0), + S=QAny(self.composite_index_bitsize), + T=QAny(self.composite_index_bitsize), + q=QBit(), + ) + + @cached_property + def index_dtype(self) -> QUInt: + return QUInt(self.inst.index_bitsize) + + @cached_property + def composite_index_bitsize(self) -> SymbolicInt: + """total number of bits to store `l` indices in `[n]`.""" + return self.ell * self.inst.index_bitsize + + @cached_property + def rotation_angle_dtype(self): + return QFxp(self.entry_bitsize, self.entry_bitsize) + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT: + counts = Counter[Bloq]() + + # S \Delta T + symm_diff = SymmetricDifference(self.ell, self.ell, self.inst.k, self.index_dtype) + counts[symm_diff] += 1 + counts[symm_diff.adjoint()] += 1 + + # Map S to j, such that U_j = S + load_idx = LoadUniqueScopeIndex(self.inst) + counts[load_idx] += 1 + counts[load_idx.adjoint()] += 1 + + # apply the rotation + rotation: Bloq = PRGAUniqueConstraintRHS(self.inst, self.entry_bitsize) + if self.is_controlled: + rotation = rotation.controlled() + counts[rotation] += 1 + + return counts + + 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 + + ctrl_bit, ctrl_bloq = ( + (1, self) if self.is_controlled else (None, attrs.evolve(self, is_controlled=True)) + ) + + return get_ctrl_system_1bit_cv_from_bloqs( + self, + ctrl_spec, + current_ctrl_bit=ctrl_bit, + bloq_with_ctrl=ctrl_bloq, + ctrl_reg_name='ctrl', + ) + + +@bloq_example +def _kikuchi_matrix_entry() -> KikuchiMatrixEntry: + from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import example_kxor_instance + + inst = example_kxor_instance() + ell = 8 + + kikuchi_matrix_entry = KikuchiMatrixEntry(inst, ell, entry_bitsize=3) + return kikuchi_matrix_entry + + +@bloq_example +def _kikuchi_matrix_entry_symb() -> KikuchiMatrixEntry: + from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance + + n, m, k, c = sympy.symbols("n m k c", positive=True, integer=True) + inst = KXorInstance.symbolic(n=n, m=m, k=k) + ell = c * k + + kikuchi_matrix_entry_symb = KikuchiMatrixEntry(inst, ell, entry_bitsize=3) + return kikuchi_matrix_entry_symb + + +_KIKUCHI_MATRIX_ENTRY_DOC = BloqDocSpec( + bloq_cls=KikuchiMatrixEntry, examples=[_kikuchi_matrix_entry_symb, _kikuchi_matrix_entry] +) diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py new file mode 100644 index 0000000000..6159296be3 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py @@ -0,0 +1,93 @@ +# 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 +from attrs import evolve + +import qualtran.testing as qlt_testing +from qualtran.resource_counting import big_O, GateCounts, get_cost_value, QECGatesCost +from qualtran.symbolics import ceil, log2 + +from .kikuchi_adjacency_matrix import _kikuchi_matrix_entry, _kikuchi_matrix_entry_symb + + +@pytest.mark.parametrize("bloq_ex", [_kikuchi_matrix_entry, _kikuchi_matrix_entry_symb]) +def test_examples(bloq_autotester, bloq_ex): + if bloq_autotester.check_name == 'serialize': + pytest.skip() + + bloq_autotester(bloq_ex) + + +def test_controlled_cost(): + bloq = _kikuchi_matrix_entry() + _, sigma = bloq.call_graph(max_depth=2) + _, ctrl_sigma = bloq.controlled().call_graph(max_depth=2) + + # should only differ in QROM call for loading absolute amplitudes + a_minus_b = set(sigma.items()) - set(ctrl_sigma.items()) + b_minus_a = set(ctrl_sigma.items()) - set(sigma.items()) + assert len(a_minus_b) == 1 + assert len(b_minus_a) == 1 + + ((qrom, na),) = a_minus_b + ((ctrl_qrom, nb),) = b_minus_a + assert na == nb + assert evolve(qrom, num_controls=1) == ctrl_qrom # type: ignore + + +def test_cost(): + bloq = _kikuchi_matrix_entry() + + gc = get_cost_value(bloq, QECGatesCost()) + assert gc == GateCounts( + cswap=512, and_bloq=1301, clifford=12518, measurement=1301, rotation=ANY + ) + + +def test_cost_symb(): + bloq = _kikuchi_matrix_entry_symb() + n, m, k, c = sympy.symbols("n m k c", positive=True, integer=True) + + l = c * k + logl = ceil(log2(l)) + logn = ceil(log2(n)) + logm = ceil(log2(m)) + + gc = get_cost_value(bloq, QECGatesCost()) + assert gc == GateCounts( + cswap=4 * l * (logl + 1) * logn, + and_bloq=( + 4 * l * ((2 * logn + 1) * (logl + 1)) + + 4 * l + + 2 * m * (k * logn - 1) + + 2 * m + + 4 * ((2 * l - 1) * (logn - 1)) + + logm + + 4 * ceil(log2(2 * l)) + - 10 + ), + rotation=ANY, + clifford=ANY, + measurement=ANY, + ) + + assert big_O(gc.total_t_count()) == big_O(l * logn * logl + k * m * logn) + + +@pytest.mark.notebook +def test_notebook(): + qlt_testing.execute_notebook('kikuchi_adjacency_matrix') diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.ipynb b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.ipynb new file mode 100644 index 0000000000..78031452ac --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.ipynb @@ -0,0 +1,183 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e1597556", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# Planted Noisy kXOR: Block-encoding the Kikuchi Matrix\n", + "\n", + "Section 4.4.2 Simulating the Kikuchi Hamiltonian\n", + "\n", + "This module contains oracles to implement the block-encoding of the Kikuchi\n", + "Hamiltonian corresponding to an input k-XOR-SAT instance.\n", + "\n", + "References:\n", + " [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)\n", + " Section 4.4.2 for algorithm. Section 2.4 for definitions and notation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d69b0bd", + "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": "4e549ab8", + "metadata": { + "cq.autogen": "KikuchiHamiltonian.bloq_doc.md" + }, + "source": [ + "## `KikuchiHamiltonian`\n", + "Block encoding of the Kikuchi matrix $\\mathcal{K}_\\ell$.\n", + "\n", + "This is implemented by a sparse matrix block encoding using the adjacency matrix\n", + "and adjacency list oracles.\n", + "\n", + "This assumes a default sparsity of $\\bar{m}$, which is the number of unique\n", + "scopes in the instance $\\mathcal{I}$.\n", + "If a better bound on sparsity is known, it can be passed in by the user.\n", + "\n", + "#### Parameters\n", + " - `inst`: kXOR instance $\\mathcal{I}$.\n", + " - `ell`: Kikuchi parameter $\\ell$.\n", + " - `entry_bitsize`: Number of bits $b$ to approximate the matrix entries (angles) to.\n", + " - `s`: sparsity of the Kikuchi matrix, defaults to $\\bar{m}$.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a89ce06", + "metadata": { + "cq.autogen": "KikuchiHamiltonian.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.optimization.k_xor_sat import KikuchiHamiltonian" + ] + }, + { + "cell_type": "markdown", + "id": "9b0195ad", + "metadata": { + "cq.autogen": "KikuchiHamiltonian.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8660eb37", + "metadata": { + "cq.autogen": "KikuchiHamiltonian.kikuchi_matrix" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import example_kxor_instance\n", + "\n", + "inst = example_kxor_instance()\n", + "ell = 8\n", + "\n", + "kikuchi_matrix = KikuchiHamiltonian(inst, ell)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f6f324e", + "metadata": { + "cq.autogen": "KikuchiHamiltonian.kikuchi_matrix_symb" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance\n", + "\n", + "n, m, k, c = sympy.symbols(\"n m k c\", positive=True, integer=True)\n", + "inst = KXorInstance.symbolic(n=n, m=m, k=k)\n", + "ell = c * k\n", + "\n", + "kikuchi_matrix_symb = KikuchiHamiltonian(inst, ell)" + ] + }, + { + "cell_type": "markdown", + "id": "312f1765", + "metadata": { + "cq.autogen": "KikuchiHamiltonian.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1419c085", + "metadata": { + "cq.autogen": "KikuchiHamiltonian.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([kikuchi_matrix, kikuchi_matrix_symb],\n", + " ['`kikuchi_matrix`', '`kikuchi_matrix_symb`'])" + ] + }, + { + "cell_type": "markdown", + "id": "720f08a4", + "metadata": { + "cq.autogen": "KikuchiHamiltonian.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c492110d", + "metadata": { + "cq.autogen": "KikuchiHamiltonian.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "kikuchi_matrix_g, kikuchi_matrix_sigma = kikuchi_matrix.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(kikuchi_matrix_g)\n", + "show_counts_sigma(kikuchi_matrix_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/optimization/k_xor_sat/kikuchi_block_encoding.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.py new file mode 100644 index 0000000000..3862563fb4 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.py @@ -0,0 +1,236 @@ +# 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. +"""Section 4.4.2 Simulating the Kikuchi Hamiltonian + +This module contains oracles to implement the block-encoding of the Kikuchi +Hamiltonian corresponding to an input k-XOR-SAT instance. + +References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Section 4.4.2 for algorithm. Section 2.4 for definitions and notation. +""" +from functools import cached_property + +import sympy +from attrs import field, frozen + +from qualtran import ( + bloq_example, + BloqBuilder, + BloqDocSpec, + BQUInt, + QAny, + QBit, + QUInt, + Signature, + Soquet, + SoquetT, +) +from qualtran.bloqs.block_encoding import BlockEncoding +from qualtran.bloqs.block_encoding.sparse_matrix import RowColumnOracle +from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import ( + SparseMatrixHermitian, + SqrtEntryOracle, +) +from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator +from qualtran.symbolics import is_symbolic, SymbolicFloat, SymbolicInt + +from .kikuchi_adjacency_list import KikuchiNonZeroIndex +from .kikuchi_adjacency_matrix import KikuchiMatrixEntry +from .kxor_instance import KXorInstance + + +@frozen +class BlackBoxKikuchiEntryOracle(SqrtEntryOracle): + r"""Wrapper around the adjacency matrix oracle $O_H$ of the Kikuchi graph.""" + + O_H: KikuchiMatrixEntry + + @cached_property + def signature(self) -> Signature: + return Signature.build_from_dtypes( + q=QBit(), i=QAny(self.system_bitsize), j=QAny(self.system_bitsize) + ) + + @property + def system_bitsize(self) -> SymbolicInt: + return self.O_H.composite_index_bitsize + + @property + def epsilon(self) -> SymbolicFloat: + """precision due to fixed-point approximation of entries. + + In the good case, whp (i.e. 1 - o(1)), the entries are in [-2, 2], + whose corresponding angles can be represented exactly with 3 bits. + I.e. `arccos(sqrt(x / 2)) / pi` for `x in [-2, 2]` are `2, 1.5, 1, 0.5, 0`. + """ + return 0 + + @property + def _phasegrad_bitsize(self) -> SymbolicInt: + return self.O_H.entry_bitsize + + def build_composite_bloq( + self, bb: 'BloqBuilder', q: 'Soquet', i: 'Soquet', j: 'Soquet' + ) -> dict[str, 'SoquetT']: + i, j, q = bb.add(self.O_H, S=i, T=j, q=q) + return dict(q=q, i=i, j=j) + + +@frozen +class BlackBoxKikuchiRowColumnOracle(RowColumnOracle): + r"""Wrapper around the adjacency list oracle $O_F$ of the Kikuchi graph.""" + + O_F: KikuchiNonZeroIndex + + @cached_property + def signature(self) -> Signature: + return Signature.build_from_dtypes( + l=BQUInt(self.system_bitsize, self.num_nonzero), i=QUInt(self.system_bitsize) + ) + + @property + def system_bitsize(self) -> SymbolicInt: + return self.O_F.index_dtype.num_qubits * self.O_F.ell + + @property + def num_nonzero(self) -> SymbolicInt: + return self.O_F.s + + def build_composite_bloq( + self, bb: 'BloqBuilder', l: 'Soquet', i: 'Soquet' + ) -> dict[str, 'SoquetT']: + i, l = bb.add(self.O_F, S=i, k=l) + return dict(l=l, i=i) + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': + return {self.O_F: 1} + + +@frozen +class KikuchiHamiltonian(BlockEncoding): + r"""Block encoding of the Kikuchi matrix $\mathcal{K}_\ell$. + + This is implemented by a sparse matrix block encoding using the adjacency matrix + and adjacency list oracles. + + This assumes a default sparsity of $\bar{m}$, which is the number of unique + scopes in the instance $\mathcal{I}$. + If a better bound on sparsity is known, it can be passed in by the user. + + Args: + inst: kXOR instance $\mathcal{I}$. + ell: Kikuchi parameter $\ell$. + entry_bitsize: Number of bits $b$ to approximate the matrix entries (angles) to. + s: sparsity of the Kikuchi matrix, defaults to $\bar{m}$. + """ + + inst: KXorInstance + ell: SymbolicInt + entry_bitsize: SymbolicInt = field() + s: SymbolicInt = field() + + @s.default + def _default_sparsity(self) -> SymbolicInt: + return self.inst.num_unique_constraints + + @entry_bitsize.default + def _default_entry_bitsize(self): + if is_symbolic(self.inst.max_rhs) or self.inst.max_rhs == 2: + # one T gate suffices! + return 3 + raise ValueError("Entries outside range [-2, 2], please specify an explicit entry_bitsize.") + + @cached_property + def signature(self) -> 'Signature': + return Signature.build( + system=self.system_bitsize, ancilla=self.ancilla_bitsize, resource=self.resource_bitsize + ) + + @cached_property + def _sparse_matrix_encoding(self) -> SparseMatrixHermitian: + blackbox_O_F = BlackBoxKikuchiRowColumnOracle(self.oracle_O_F) + blackbox_O_H = BlackBoxKikuchiEntryOracle(self.oracle_O_H) + return SparseMatrixHermitian( + col_oracle=blackbox_O_F, entry_oracle=blackbox_O_H, eps=blackbox_O_H.epsilon + ) + + @cached_property + def oracle_O_H(self) -> KikuchiMatrixEntry: + r"""Maps $|i, j\rangle |0\rangle$ to $|i, j\rangle (\sqrt{A_{ij}} |0\rangle + \sqrt{1 - |A_{ij}|} |1\rangle)""" + return KikuchiMatrixEntry(inst=self.inst, ell=self.ell, entry_bitsize=self.entry_bitsize) + + @cached_property + def oracle_O_F(self) -> KikuchiNonZeroIndex: + r"""Maps `i, k` to `i, f(i, k)` where `f(i, k)` is the column of the `k`-th nonzero entry in row `i`.""" + return KikuchiNonZeroIndex(inst=self.inst, ell=self.ell, s=self.s) + + @property + def alpha(self) -> SymbolicFloat: + return self._sparse_matrix_encoding.alpha + + @property + def system_bitsize(self) -> SymbolicInt: + return self._sparse_matrix_encoding.system_bitsize + + @property + def ancilla_bitsize(self) -> SymbolicInt: + return self._sparse_matrix_encoding.ancilla_bitsize + + @property + def resource_bitsize(self) -> SymbolicInt: + return self._sparse_matrix_encoding.resource_bitsize + + @property + def epsilon(self) -> SymbolicFloat: + return self._sparse_matrix_encoding.epsilon + + @property + def signal_state(self) -> BlackBoxPrepare: + return self._sparse_matrix_encoding.signal_state + + def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> dict[str, 'SoquetT']: + return bb.add_d(self._sparse_matrix_encoding, **soqs) + + def __str__(self): + return 'B[K_l]' + + +@bloq_example +def _kikuchi_matrix() -> KikuchiHamiltonian: + from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import example_kxor_instance + + inst = example_kxor_instance() + ell = 8 + + kikuchi_matrix = KikuchiHamiltonian(inst, ell) + return kikuchi_matrix + + +@bloq_example +def _kikuchi_matrix_symb() -> KikuchiHamiltonian: + from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance + + n, m, k, c = sympy.symbols("n m k c", positive=True, integer=True) + inst = KXorInstance.symbolic(n=n, m=m, k=k) + ell = c * k + + kikuchi_matrix_symb = KikuchiHamiltonian(inst, ell) + return kikuchi_matrix_symb + + +_KIKUCHI_HAMILTONIAN_DOC = BloqDocSpec( + bloq_cls=KikuchiHamiltonian, examples=[_kikuchi_matrix, _kikuchi_matrix_symb] +) diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py new file mode 100644 index 0000000000..ad8ef09518 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py @@ -0,0 +1,58 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +import qualtran.testing as qlt_testing +from qualtran.bloqs.basic_gates import Swap +from qualtran.resource_counting import get_cost_value, QECGatesCost + +from .kikuchi_block_encoding import _kikuchi_matrix, _kikuchi_matrix_symb + + +@pytest.mark.parametrize("bloq_ex", [_kikuchi_matrix, _kikuchi_matrix_symb]) +def test_examples(bloq_autotester, bloq_ex): + if bloq_autotester.check_name == 'serialize': + pytest.skip() + + bloq_autotester(bloq_ex) + + +@pytest.mark.notebook +def test_notebook(): + qlt_testing.execute_notebook('kikuchi_block_encoding') + + +def test_controlled_cost(): + bloq = _kikuchi_matrix() + _, sigma = bloq.call_graph(max_depth=2) + _, ctrl_sigma = bloq.controlled().call_graph(max_depth=2) + + assert set(sigma.items()) - set(ctrl_sigma.items()) == {(Swap(32), 1), (Swap(1), 1)} + assert set(ctrl_sigma.items()) - set(sigma.items()) == { + (Swap(32).controlled(), 1), + (Swap(1).controlled(), 1), + } + + +def test_cost(): + bloq = _kikuchi_matrix() + + _ = get_cost_value(bloq, QECGatesCost()) + + +def test_cost_symb(): + bloq = _kikuchi_matrix_symb() + + _ = get_cost_value(bloq, QECGatesCost()) + print(_) diff --git a/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.ipynb b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.ipynb new file mode 100644 index 0000000000..7987d624e4 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.ipynb @@ -0,0 +1,207 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "448cdbc3", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# kXOR: Instance load Oracles\n", + "\n", + "We define three oracles that load a kXOR instance, which are used in the algorithm.\n", + "\n", + "We are given a kXOR instance $\\mathcal{I}$ of $n$ variables,\n", + "with $\\bar{m}$ unique scopes $\\{U_j | j \\in [\\bar{m}]\\}$.\n", + "We provide oracles to:\n", + "1. `LoadConstraintScopes`: Given $j \\in [\\bar{m}]$, compute $U_j$.\n", + "2. `LoadUniqueScopeIndex`: Given $U_j$, compute $j \\in [\\bar{m}]$\n", + "3. `PRGAUniqueConstraintRHS` Given $j$, apply $Rx(arccos(\\sqrt{B_\\mathcal{I}(S)/M}))$ on a target qubit.\n", + "(for an appropriate normalization $M$).\n", + "\n", + "\n", + "The first two oracles are independent of the RHS.\n", + "All these oracles can output arbitrary values for invalid inputs.\n", + "\n", + "References:\n", + " [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)\n", + " Notation 2.24 for $B_\\mathcal{I}$.\n", + " Theorem 4.17, proof para 2 for $U_j$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "611e4ef6", + "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": "b5e118d0", + "metadata": { + "cq.autogen": "LoadConstraintScopes.bloq_doc.md" + }, + "source": [ + "## `LoadConstraintScopes`\n", + "Given an index $j$, load the scope of the $j$-th unique constraint.\n", + "\n", + "Given a $k$-XOR-SAT instance `inst` with $n$ variables and $m$ constraints.\n", + "Assuming `inst` has $\\bar{m}$ unique constraints, we define $U_j \\in {[n] \\choose k}$\n", + "for $j \\in [\\bar{m}]$ as the $j$-th unique constraint scope.\n", + "\n", + "The scopes are loaded using a QROM.\n", + "\n", + "If the input contains an invalid index, then any arbitrary value can be output.\n", + "\n", + "#### Registers\n", + " - `j`: a number in [\\bar{m}]\n", + " - `U`: $j$-th unique scope\n", + " - `ancilla`: entangled intermediate qubits, to be uncomputed by the adjoint. \n", + "\n", + "#### References\n", + " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Theorem 4.17, proof para 2.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51cbd971", + "metadata": { + "cq.autogen": "LoadConstraintScopes.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.optimization.k_xor_sat import LoadConstraintScopes" + ] + }, + { + "cell_type": "markdown", + "id": "45f7354e", + "metadata": { + "cq.autogen": "LoadConstraintScopes.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "137d0cb6", + "metadata": { + "cq.autogen": "LoadConstraintScopes.load_scopes_symb" + }, + "outputs": [], + "source": [ + "import sympy\n", + "\n", + "from qualtran.bloqs.optimization.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=n, m=m, k=k)\n", + "load_scopes_symb = LoadConstraintScopes(inst)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1a93c7c", + "metadata": { + "cq.autogen": "LoadConstraintScopes.load_scopes" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import Constraint, KXorInstance\n", + "\n", + "inst = KXorInstance(\n", + " n=6,\n", + " k=4,\n", + " constraints=(\n", + " Constraint(S=(0, 1, 2, 3), b=1),\n", + " Constraint(S=(0, 1, 4, 5), b=-1),\n", + " Constraint(S=(1, 2, 4, 5), b=1),\n", + " Constraint(S=(0, 3, 4, 5), b=1),\n", + " Constraint(S=(2, 3, 4, 5), b=1),\n", + " Constraint(S=(0, 1, 2, 3), b=1),\n", + " Constraint(S=(0, 3, 4, 5), b=1),\n", + " Constraint(S=(2, 3, 4, 5), b=1),\n", + " ),\n", + ")\n", + "load_scopes = LoadConstraintScopes(inst)" + ] + }, + { + "cell_type": "markdown", + "id": "107c977f", + "metadata": { + "cq.autogen": "LoadConstraintScopes.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a998692", + "metadata": { + "cq.autogen": "LoadConstraintScopes.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([load_scopes_symb, load_scopes],\n", + " ['`load_scopes_symb`', '`load_scopes`'])" + ] + }, + { + "cell_type": "markdown", + "id": "8b3ac93d", + "metadata": { + "cq.autogen": "LoadConstraintScopes.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29b2841a", + "metadata": { + "cq.autogen": "LoadConstraintScopes.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "load_scopes_symb_g, load_scopes_symb_sigma = load_scopes_symb.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(load_scopes_symb_g)\n", + "show_counts_sigma(load_scopes_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/optimization/k_xor_sat/load_kxor_instance.py b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py new file mode 100644 index 0000000000..3d379f974f --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py @@ -0,0 +1,354 @@ +# 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"""We define three oracles that load a kXOR instance, which are used in the algorithm. + +We are given a kXOR instance $\mathcal{I}$ of $n$ variables, +with $\bar{m}$ unique scopes $\{U_j | j \in [\bar{m}]\}$. +We provide oracles to: +1. `LoadConstraintScopes`: Given $j \in [\bar{m}]$, compute $U_j$. +2. `LoadUniqueScopeIndex`: Given $U_j$, compute $j \in [\bar{m}]$ +3. `PRGAUniqueConstraintRHS` Given $j$, apply $Rx(arccos(\sqrt{B_\mathcal{I}(S)/M}))$ on a target qubit. +(for an appropriate normalization $M$). + + +The first two oracles are independent of the RHS. +All these oracles can output arbitrary values for invalid inputs. + +References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Notation 2.24 for $B_\mathcal{I}$. + Theorem 4.17, proof para 2 for $U_j$. +""" +from functools import cached_property +from typing import Counter, Sequence, Union + +import attrs +import numpy as np +from attrs import frozen + +from qualtran import ( + AddControlledT, + Bloq, + bloq_example, + BloqBuilder, + BloqDocSpec, + BQUInt, + CtrlSpec, + DecomposeTypeError, + QAny, + QBit, + QFxp, + Register, + Side, + Signature, + Soquet, + SoquetT, +) +from qualtran.bloqs.arithmetic import EqualsAConstant, LessThanConstant +from qualtran.bloqs.basic_gates import Hadamard, SGate +from qualtran.bloqs.bookkeeping import Partition +from qualtran.bloqs.data_loading import QROM +from qualtran.bloqs.rotations.rz_via_phase_gradient import RzViaPhaseGradient +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator +from qualtran.symbolics import ceil, HasLength, is_symbolic, is_zero, log2, SymbolicInt + +from .kxor_instance import KXorInstance + + +@frozen +class LoadConstraintScopes(Bloq): + r"""Given an index $j$, load the scope of the $j$-th unique constraint. + + Given a $k$-XOR-SAT instance `inst` with $n$ variables and $m$ constraints. + Assuming `inst` has $\bar{m}$ unique constraints, we define $U_j \in {[n] \choose k}$ + for $j \in [\bar{m}]$ as the $j$-th unique constraint scope. + + The scopes are loaded using a QROM. + + If the input contains an invalid index, then any arbitrary value can be output. + + Registers: + j: a number in [\bar{m}] + U (RIGHT): $j$-th unique scope + ancilla (RIGHT): entangled intermediate qubits, to be uncomputed by the adjoint. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Theorem 4.17, proof para 2. + """ + + inst: KXorInstance + + @cached_property + def signature(self) -> 'Signature': + registers: list[Register] = [ + Register('j', self.m_dtype), + Register('U', QAny(self.scope_bitsize), side=Side.RIGHT), + ] + + if not is_zero(self.ancilla_bitsize): + registers.append(Register('ancilla', QAny(self.ancilla_bitsize), side=Side.RIGHT)) + + return Signature(registers) + + @cached_property + def scope_bitsize(self) -> SymbolicInt: + """total number of bits to store `k` indices in `[n]`.""" + return self.inst.k * self.inst.index_bitsize + + @cached_property + def ancilla_bitsize(self) -> SymbolicInt: + """ancillas used by the underlying QRO(A)M""" + return 0 + + @cached_property + def m_dtype(self): + r"""number of bits to store $j \in [\bar{m}]$.""" + m = self.inst.num_unique_constraints + bitsize = ceil(log2(m)) + return BQUInt(bitsize, m) + + @cached_property + def _qrom_bloq(self) -> QROM: + # TODO use QROAMClean? + + if self.inst.is_symbolic(): + return QROM.build_from_bitsize(self.inst.num_unique_constraints, self.scope_bitsize) + + assert isinstance(self.inst.batched_scopes, tuple) + scopes = np.array([S for S, _ in self.inst.batched_scopes], dtype=int) + assert scopes.shape == (self.inst.num_unique_constraints, self.inst.k) + return QROM.build_from_data( + *scopes.T, target_bitsizes=(self.inst.index_bitsize,) * self.inst.k + ) + + def build_composite_bloq(self, bb: 'BloqBuilder', j: 'Soquet') -> dict[str, 'SoquetT']: + if self.inst.is_symbolic(): + raise DecomposeTypeError(f"cannot decompose symbolic {self}") + + targets = { + f'target{i}_': bb.allocate(self.inst.index_bitsize) for i in range(int(self.inst.k)) + } + targets = bb.add_d(self._qrom_bloq, selection=j, **targets) + j = targets.pop('selection') + + U = bb.add( + Partition(self.scope_bitsize, self._qrom_bloq.target_registers).adjoint(), **targets + ) + return {'j': j, 'U': U} + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT: + return {self._qrom_bloq: 1} + + +@bloq_example +def _load_scopes() -> LoadConstraintScopes: + from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import Constraint, KXorInstance + + inst = KXorInstance( + n=6, + k=4, + constraints=( + Constraint(S=(0, 1, 2, 3), b=1), + Constraint(S=(0, 1, 4, 5), b=-1), + Constraint(S=(1, 2, 4, 5), b=1), + Constraint(S=(0, 3, 4, 5), b=1), + Constraint(S=(2, 3, 4, 5), b=1), + Constraint(S=(0, 1, 2, 3), b=1), + Constraint(S=(0, 3, 4, 5), b=1), + Constraint(S=(2, 3, 4, 5), b=1), + ), + ) + load_scopes = LoadConstraintScopes(inst) + return load_scopes + + +@bloq_example +def _load_scopes_symb() -> LoadConstraintScopes: + import sympy + + from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance + + n, m, k = sympy.symbols("n m k", positive=True, integer=True) + inst = KXorInstance.symbolic(n=n, m=m, k=k) + load_scopes_symb = LoadConstraintScopes(inst) + return load_scopes_symb + + +_LOAD_INSTANCE_DOC = BloqDocSpec( + bloq_cls=LoadConstraintScopes, examples=[_load_scopes_symb, _load_scopes] +) + + +@frozen +class LoadUniqueScopeIndex(Bloq): + r"""Given a scope $S$, load $j$ such that $S = U_j$, the $j$-th unique scope. + + If the input contains an invalid scope, then any arbitrary value can be output. + + Registers: + S: A scope $S \in {[n] \choose k}$. + j (RIGHT): a number in $[\bar{m}]$ s.t. $S = U_j$. + ancilla (RIGHT): entangled intermediate qubits, to be uncomputed by the adjoint. + """ + + inst: KXorInstance + + @cached_property + def signature(self) -> 'Signature': + return Signature.build_from_dtypes(j=self.m_dtype, U=QAny(self.scope_bitsize)) + + @cached_property + def scope_bitsize(self) -> SymbolicInt: + """total number of bits to store `k` indices in `[n]`.""" + return self.inst.k * self.inst.index_bitsize + + @cached_property + def m_dtype(self): + r"""number of bits to store $j \in [\bar{m}]$.""" + m = self.inst.num_unique_constraints + bitsize = ceil(log2(m)) + return BQUInt(bitsize, m) + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT: + counts = Counter[Bloq]() + + c = ssa.new_symbol("c") + counts[EqualsAConstant(self.scope_bitsize, c)] += self.inst.num_unique_constraints + + return counts + + +@frozen +class PRGAUniqueConstraintRHS(Bloq): + r"""Map $|j\rangle |0\rangle$ to $|j\rangle (\sqrt{E_j} |0\rangle + \sqrt{1 - |E_j|}|1\rangle)$ + + Given an instance $\mathcal{I}$, with unique scopes $U_j$ and corresponding RHS values + $E_j = B_\mathcal{I}(U_j)/M$ (where $M$ is the max. abs. entry, usually 2) + apply the above rotation on the target qubit. + + This is done by first rotating for $|E_j|$ (i.e. ignoring the sign), + by loading the values $\arccos{\sqrt{|E_j|}} / (2 * \pi)$, + and applying an `Rx` using an `RzViaPhaseGradient` surrounded by `H`. + + We then apply the sign correction of $i$ for the negative entries by an $S$ gate. + We ensure that the input data is sorted, therefore we can simply compare $j$ + with the largest negative index, and apply a `CS` gate. + + Args: + inst: kXOR instance $\mathcal{I}$. + angle_bitsize: number of bits to load the amplitude rotation angles to. + + Registers: + j: Selection index, loads the value of $E_j = B_\mathcal{I}(U_j)/M$ + q: rotation target. + """ + + inst: KXorInstance + angle_bitsize: SymbolicInt + is_controlled: bool = False + + @cached_property + def signature(self) -> 'Signature': + return Signature.build_from_dtypes(ctrl=QAny(self.n_ctrl), j=self.m_dtype, q=QBit()) + + @property + def n_ctrl(self) -> int: + return 1 if self.is_controlled else 0 + + @cached_property + def m_dtype(self): + r"""number of bits to store $j \in [\bar{m}]$.""" + m = self.inst.num_unique_constraints + bitsize = ceil(log2(m)) + return BQUInt(bitsize, m) + + @cached_property + def _angle_dtype(self): + return QFxp(self.angle_bitsize, self.angle_bitsize) + + @cached_property + def _qrom_angle_data( + self, + ) -> tuple[Union[HasLength, Sequence[int]], Union[HasLength, Sequence[int]]]: + M = self.inst.max_rhs + scopes = self.inst.batched_scopes + if is_symbolic(M) or is_symbolic(scopes): + m = self.inst.num_unique_constraints + return HasLength(m), HasLength(m) + + b = [b for _, b in scopes] + assert np.all(b == np.sort(b)), "data must be sorted!" + + amplitude_angles = np.arccos(np.sqrt(np.abs(b) / M)) + amplitude_angles_int = np.round(amplitude_angles * 2**self.angle_bitsize) + + signs = tuple(np.sign(b)) + return amplitude_angles_int, signs + + @cached_property + def _amplitude_qrom(self) -> QROM: + data, _ = self._qrom_angle_data + if is_symbolic(data): + return QROM.build_from_bitsize( + data_len_or_shape=self.inst.num_unique_constraints, + target_bitsizes=self.angle_bitsize, + num_controls=self.n_ctrl, + ) + + return QROM.build_from_data( + data, target_bitsizes=(self.angle_bitsize,), num_controls=self.n_ctrl + ) + + @cached_property + def _num_negative(self) -> SymbolicInt: + """returns $k$ s.t. the first $k$ elements are negative.""" + _, signs = self._qrom_angle_data + if is_symbolic(signs): + return self.inst.num_unique_constraints // 2 + + assert np.all(signs == np.sort(signs)), "data must be sorted!" + return int(np.searchsorted(signs, 0)) + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT: + counts = Counter[Bloq]() + + # load the amplitudes + counts[self._amplitude_qrom] += 1 + + # apply a Rx rotation using Rx = H Rz H + counts[Hadamard()] += 2 + counts[RzViaPhaseGradient(self._angle_dtype, self._angle_dtype)] += 1 + + # apply the sign correction + # TODO use the half-bloq once implemented to wire this correctly + sign_compare = LessThanConstant(self.m_dtype.num_qubits, self._num_negative) + counts[sign_compare] += 1 + counts[SGate().controlled()] += 1 + + # unload amplitudes + counts[self._amplitude_qrom.adjoint()] += 1 + + return counts + + 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=self if self.is_controlled else attrs.evolve(self, is_controlled=True), + ctrl_reg_name='ctrl', + ) diff --git a/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance_test.py b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance_test.py new file mode 100644 index 0000000000..97f833b455 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance_test.py @@ -0,0 +1,49 @@ +# 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 import Bloq +from qualtran.resource_counting import GateCounts, get_cost_value, QECGatesCost + +from .load_kxor_instance import _load_scopes, _load_scopes_symb + + +@pytest.mark.parametrize("bloq", [_load_scopes, _load_scopes_symb]) +def test_examples(bloq_autotester, bloq: Bloq): + if bloq_autotester.check_name == 'serialize': + pytest.skip() + + bloq_autotester(bloq) + + +def test_load_instance(): + bloq = _load_scopes() + + gc = get_cost_value(bloq, QECGatesCost()) + assert gc == GateCounts(and_bloq=3, clifford=ANY, measurement=ANY) + + # classical action + for j, (S, _) in enumerate(tuple(bloq.inst.batched_scopes)): # type: ignore + assert bloq.call_classically(j=j) == (j, bloq.inst.scope_as_int(S)) + + +def test_load_instance_cost_symb(): + bloq = _load_scopes_symb() + + m, k = bloq.inst.m, bloq.inst.k + logn = bloq.inst.index_bitsize + gc = get_cost_value(bloq, QECGatesCost()) + assert gc == GateCounts(and_bloq=m - 2, clifford=k * m * logn + m - 2, measurement=m - 2) From 9d9b8cb698d1ed1a73386fe263f138e4a8baf963 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Fri, 14 Feb 2025 12:52:12 +0100 Subject: [PATCH 02/10] fix serialization test skips --- .../bloqs/optimization/k_xor_sat/__init__.py | 1 + .../k_xor_sat/kikuchi_adjacency_list_test.py | 3 --- .../kikuchi_adjacency_matrix_test.py | 7 +++---- .../k_xor_sat/kikuchi_block_encoding_test.py | 5 +---- .../k_xor_sat/kikuchi_guiding_state_test.py | 3 --- .../k_xor_sat/load_kxor_instance_test.py | 14 ++++++++------ qualtran/conftest.py | 19 +++++++++++++++++++ qualtran/serialization/resolver_dict.py | 19 +++++++++++++++++++ 8 files changed, 51 insertions(+), 20 deletions(-) diff --git a/qualtran/bloqs/optimization/k_xor_sat/__init__.py b/qualtran/bloqs/optimization/k_xor_sat/__init__.py index 4e65bfd7b9..79b00094a6 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/__init__.py +++ b/qualtran/bloqs/optimization/k_xor_sat/__init__.py @@ -16,3 +16,4 @@ from .kikuchi_block_encoding import KikuchiHamiltonian, KikuchiMatrixEntry, KikuchiNonZeroIndex from .kikuchi_guiding_state import GuidingState, SimpleGuidingState from .kxor_instance import Constraint, KXorInstance +from .load_kxor_instance import LoadConstraintScopes, LoadUniqueScopeIndex, PRGAUniqueConstraintRHS diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py index fd6875bdf1..9693310a98 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py @@ -34,9 +34,6 @@ ids=lambda bloq_ex: bloq_ex.name, ) def test_examples(bloq_autotester, bloq_ex): - if bloq_autotester.check_name == 'serialize': - pytest.skip() - bloq_autotester(bloq_ex) diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py index 6159296be3..95f34b0ded 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py @@ -24,11 +24,10 @@ from .kikuchi_adjacency_matrix import _kikuchi_matrix_entry, _kikuchi_matrix_entry_symb -@pytest.mark.parametrize("bloq_ex", [_kikuchi_matrix_entry, _kikuchi_matrix_entry_symb]) +@pytest.mark.parametrize( + "bloq_ex", [_kikuchi_matrix_entry, _kikuchi_matrix_entry_symb], ids=lambda be: be.name +) def test_examples(bloq_autotester, bloq_ex): - if bloq_autotester.check_name == 'serialize': - pytest.skip() - bloq_autotester(bloq_ex) diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py index ad8ef09518..7d22817c07 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py @@ -20,11 +20,8 @@ from .kikuchi_block_encoding import _kikuchi_matrix, _kikuchi_matrix_symb -@pytest.mark.parametrize("bloq_ex", [_kikuchi_matrix, _kikuchi_matrix_symb]) +@pytest.mark.parametrize("bloq_ex", [_kikuchi_matrix, _kikuchi_matrix_symb], ids=lambda be: be.name) def test_examples(bloq_autotester, bloq_ex): - if bloq_autotester.check_name == 'serialize': - pytest.skip() - bloq_autotester(bloq_ex) diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_guiding_state_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_guiding_state_test.py index 0e75a04649..c215aad251 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_guiding_state_test.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_guiding_state_test.py @@ -40,9 +40,6 @@ ids=lambda b: b.name, ) def test_examples(bloq_autotester, bloq_ex): - if bloq_autotester.check_name == 'serialize': - pytest.skip() - bloq_autotester(bloq_ex) diff --git a/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance_test.py b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance_test.py index 97f833b455..4df56e76b4 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance_test.py +++ b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance_test.py @@ -15,17 +15,14 @@ import pytest +import qualtran.testing as qlt_testing from qualtran import Bloq +from qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance import _load_scopes, _load_scopes_symb from qualtran.resource_counting import GateCounts, get_cost_value, QECGatesCost -from .load_kxor_instance import _load_scopes, _load_scopes_symb - -@pytest.mark.parametrize("bloq", [_load_scopes, _load_scopes_symb]) +@pytest.mark.parametrize("bloq", [_load_scopes, _load_scopes_symb], ids=lambda be: be.name) def test_examples(bloq_autotester, bloq: Bloq): - if bloq_autotester.check_name == 'serialize': - pytest.skip() - bloq_autotester(bloq) @@ -47,3 +44,8 @@ def test_load_instance_cost_symb(): logn = bloq.inst.index_bitsize gc = get_cost_value(bloq, QECGatesCost()) assert gc == GateCounts(and_bloq=m - 2, clifford=k * m * logn + m - 2, measurement=m - 2) + + +@pytest.mark.notebook +def test_notebook(): + qlt_testing.execute_notebook('load_kxor_instance') diff --git a/qualtran/conftest.py b/qualtran/conftest.py index 439ae61455..cf623f1b49 100644 --- a/qualtran/conftest.py +++ b/qualtran/conftest.py @@ -158,6 +158,25 @@ def assert_bloq_example_serializes_for_pytest(bloq_ex: BloqExample): ]: pytest.xfail("Skipping serialization test for bloqs that use ECPoint.") + if bloq_ex.name in [ + 'col_kth_nz', + 'col_kth_nz_symb', + 'kikuchi_nonzero_index', + 'kikuchi_nonzero_index_symb', + 'simple_guiding_state', + 'simple_guiding_state_symb', + 'guiding_state', + 'guiding_state_symb', + 'guiding_state_symb_c', + 'kikuchi_matrix_entry', + 'kikuchi_matrix_entry_symb', + 'kikuchi_matrix', + 'kikuchi_matrix_symb', + 'load_scopes', + 'load_scopes_symb', + ]: + pytest.xfail("Skipping serialization test for bloqs that use KXorInstance.") + try: qlt_testing.assert_bloq_example_serializes(bloq_ex) except qlt_testing.BloqCheckException as bce: diff --git a/qualtran/serialization/resolver_dict.py b/qualtran/serialization/resolver_dict.py index 8eaef75788..30e9d2b8e6 100644 --- a/qualtran/serialization/resolver_dict.py +++ b/qualtran/serialization/resolver_dict.py @@ -128,6 +128,12 @@ import qualtran.bloqs.multiplexers.select_pauli_lcu import qualtran.bloqs.multiplexers.selected_majorana_fermion import qualtran.bloqs.multiplexers.unary_iteration_bloq +import qualtran.bloqs.optimization.k_xor_sat +import qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list +import qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_matrix +import qualtran.bloqs.optimization.k_xor_sat.kikuchi_block_encoding +import qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state +import qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance import qualtran.bloqs.phase_estimation.kaiser_window_state import qualtran.bloqs.phase_estimation.lp_resource_state import qualtran.bloqs.phase_estimation.qpe_window_state @@ -403,6 +409,19 @@ "qualtran.bloqs.multiplexers.select_pauli_lcu.SelectPauliLCU": qualtran.bloqs.multiplexers.select_pauli_lcu.SelectPauliLCU, "qualtran.bloqs.multiplexers.selected_majorana_fermion.SelectedMajoranaFermion": qualtran.bloqs.multiplexers.selected_majorana_fermion.SelectedMajoranaFermion, "qualtran.bloqs.multiplexers.unary_iteration_bloq.UnaryIterationGate": qualtran.bloqs.multiplexers.unary_iteration_bloq.UnaryIterationGate, + "qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list.ColumnOfKthNonZeroEntry": qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list.ColumnOfKthNonZeroEntry, + "qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list.IndexOfNonZeroColumn": qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list.IndexOfNonZeroColumn, + "qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list.KikuchiNonZeroIndex": qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list.KikuchiNonZeroIndex, + "qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_matrix.KikuchiMatrixEntry": qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_matrix.KikuchiMatrixEntry, + "qualtran.bloqs.optimization.k_xor_sat.kikuchi_block_encoding.BlackBoxKikuchiEntryOracle": qualtran.bloqs.optimization.k_xor_sat.kikuchi_block_encoding.BlackBoxKikuchiEntryOracle, + "qualtran.bloqs.optimization.k_xor_sat.kikuchi_block_encoding.BlackBoxKikuchiRowColumnOracle": qualtran.bloqs.optimization.k_xor_sat.kikuchi_block_encoding.BlackBoxKikuchiRowColumnOracle, + "qualtran.bloqs.optimization.k_xor_sat.kikuchi_block_encoding.KikuchiHamiltonian": qualtran.bloqs.optimization.k_xor_sat.kikuchi_block_encoding.KikuchiHamiltonian, + "qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state.GuidingState": qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state.GuidingState, + "qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state.ProbabilisticUncompute": qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state.ProbabilisticUncompute, + "qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state.SimpleGuidingState": qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state.SimpleGuidingState, + "qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance.LoadConstraintScopes": qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance.LoadConstraintScopes, + "qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance.LoadUniqueScopeIndex": qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance.LoadUniqueScopeIndex, + "qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance.PRGAUniqueConstraintRHS": qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance.PRGAUniqueConstraintRHS, "qualtran.bloqs.phase_estimation.kaiser_window_state.KaiserWindowState": qualtran.bloqs.phase_estimation.kaiser_window_state.KaiserWindowState, "qualtran.bloqs.phase_estimation.qpe_window_state.RectangularWindowState": qualtran.bloqs.phase_estimation.qpe_window_state.RectangularWindowState, "qualtran.bloqs.phase_estimation.lp_resource_state.LPRSInterimPrep": qualtran.bloqs.phase_estimation.lp_resource_state.LPRSInterimPrep, From bbe5eb2f218da3f41ec9e62f89361fa14176e6c6 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Fri, 14 Feb 2025 13:37:17 +0100 Subject: [PATCH 03/10] fix import --- dev_tools/qualtran_dev_tools/notebook_specs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py index ab9721d806..7567a77c2f 100644 --- a/dev_tools/qualtran_dev_tools/notebook_specs.py +++ b/dev_tools/qualtran_dev_tools/notebook_specs.py @@ -105,7 +105,6 @@ import qualtran.bloqs.multiplexers.select_base import qualtran.bloqs.multiplexers.select_pauli_lcu import qualtran.bloqs.optimization.k_xor_sat -import qualtran.bloqs.optimization.k_xor_sat.guided_hamiltonian import qualtran.bloqs.optimization.k_xor_sat.kikuchi_guiding_state import qualtran.bloqs.phase_estimation.lp_resource_state import qualtran.bloqs.phase_estimation.qubitization_qpe From 4ac3f2067bfb18791daca79a93a0a74f95637afd Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Fri, 14 Feb 2025 14:00:57 +0100 Subject: [PATCH 04/10] absolute imports --- .../optimization/k_xor_sat/kikuchi_adjacency_list.py | 3 +-- .../optimization/k_xor_sat/kikuchi_adjacency_list_test.py | 7 +++---- .../optimization/k_xor_sat/kikuchi_adjacency_matrix.py | 8 +++++--- .../k_xor_sat/kikuchi_adjacency_matrix_test.py | 6 ++++-- .../optimization/k_xor_sat/kikuchi_block_encoding.py | 7 +++---- .../optimization/k_xor_sat/kikuchi_block_encoding_test.py | 6 ++++-- .../bloqs/optimization/k_xor_sat/kikuchi_guiding_state.py | 3 +-- .../bloqs/optimization/k_xor_sat/kxor_instance_test.py | 2 +- .../bloqs/optimization/k_xor_sat/load_kxor_instance.py | 3 +-- 9 files changed, 23 insertions(+), 22 deletions(-) diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py index 8b970e6272..249e209fbf 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py @@ -45,11 +45,10 @@ from qualtran.bloqs.arithmetic.lists import SymmetricDifference from qualtran.bloqs.basic_gates import CNOT, ZeroEffect, ZeroState from qualtran.bloqs.mcmt import And +from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator from qualtran.symbolics import SymbolicInt -from .kxor_instance import KXorInstance - @frozen class ColumnOfKthNonZeroEntry(Bloq): diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py index 9693310a98..88e97c19d8 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py @@ -17,15 +17,14 @@ import sympy import qualtran.testing as qlt_testing -from qualtran.resource_counting import big_O, GateCounts, get_cost_value, QECGatesCost -from qualtran.symbolics import ceil, log2 - -from .kikuchi_adjacency_list import ( +from qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list import ( _col_kth_nz, _col_kth_nz_symb, _kikuchi_nonzero_index, _kikuchi_nonzero_index_symb, ) +from qualtran.resource_counting import big_O, GateCounts, get_cost_value, QECGatesCost +from qualtran.symbolics import ceil, log2 @pytest.mark.parametrize( diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py index c198e7800a..babc2c4404 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py @@ -43,12 +43,14 @@ Signature, ) from qualtran.bloqs.arithmetic.lists import SymmetricDifference +from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance +from qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance import ( + LoadUniqueScopeIndex, + PRGAUniqueConstraintRHS, +) from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator from qualtran.symbolics import SymbolicInt -from .kxor_instance import KXorInstance -from .load_kxor_instance import LoadUniqueScopeIndex, PRGAUniqueConstraintRHS - @frozen class KikuchiMatrixEntry(Bloq): diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py index 95f34b0ded..af1fdd8bdc 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py @@ -18,11 +18,13 @@ from attrs import evolve import qualtran.testing as qlt_testing +from qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_matrix import ( + _kikuchi_matrix_entry, + _kikuchi_matrix_entry_symb, +) from qualtran.resource_counting import big_O, GateCounts, get_cost_value, QECGatesCost from qualtran.symbolics import ceil, log2 -from .kikuchi_adjacency_matrix import _kikuchi_matrix_entry, _kikuchi_matrix_entry_symb - @pytest.mark.parametrize( "bloq_ex", [_kikuchi_matrix_entry, _kikuchi_matrix_entry_symb], ids=lambda be: be.name diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.py index 3862563fb4..1929db7ae2 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.py @@ -43,14 +43,13 @@ SparseMatrixHermitian, SqrtEntryOracle, ) +from qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_list import KikuchiNonZeroIndex +from qualtran.bloqs.optimization.k_xor_sat.kikuchi_adjacency_matrix import KikuchiMatrixEntry +from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator from qualtran.symbolics import is_symbolic, SymbolicFloat, SymbolicInt -from .kikuchi_adjacency_list import KikuchiNonZeroIndex -from .kikuchi_adjacency_matrix import KikuchiMatrixEntry -from .kxor_instance import KXorInstance - @frozen class BlackBoxKikuchiEntryOracle(SqrtEntryOracle): diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py index 7d22817c07..c3a11d5b6f 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py @@ -15,10 +15,12 @@ import qualtran.testing as qlt_testing from qualtran.bloqs.basic_gates import Swap +from qualtran.bloqs.optimization.k_xor_sat.kikuchi_block_encoding import ( + _kikuchi_matrix, + _kikuchi_matrix_symb, +) from qualtran.resource_counting import get_cost_value, QECGatesCost -from .kikuchi_block_encoding import _kikuchi_matrix, _kikuchi_matrix_symb - @pytest.mark.parametrize("bloq_ex", [_kikuchi_matrix, _kikuchi_matrix_symb], ids=lambda be: be.name) def test_examples(bloq_autotester, bloq_ex): diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_guiding_state.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_guiding_state.py index 0631e83d13..0a96fbb1e6 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_guiding_state.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_guiding_state.py @@ -41,6 +41,7 @@ from qualtran.bloqs.basic_gates import Hadamard, OnEach, XGate from qualtran.bloqs.bookkeeping import Partition from qualtran.bloqs.mcmt import MultiControlX +from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance from qualtran.bloqs.state_preparation.prepare_base import PrepareOracle from qualtran.bloqs.state_preparation.sparse_state_preparation_via_rotations import ( SparseStatePreparationViaRotations, @@ -48,8 +49,6 @@ 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): diff --git a/qualtran/bloqs/optimization/k_xor_sat/kxor_instance_test.py b/qualtran/bloqs/optimization/k_xor_sat/kxor_instance_test.py index 700dfd6eb6..9b80e6b70b 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kxor_instance_test.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kxor_instance_test.py @@ -14,7 +14,7 @@ import numpy as np import pytest -from .kxor_instance import KXorInstance +from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance @pytest.mark.slow diff --git a/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py index 3d379f974f..932592e8dd 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py +++ b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py @@ -59,12 +59,11 @@ from qualtran.bloqs.basic_gates import Hadamard, SGate from qualtran.bloqs.bookkeeping import Partition from qualtran.bloqs.data_loading import QROM +from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance from qualtran.bloqs.rotations.rz_via_phase_gradient import RzViaPhaseGradient from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator from qualtran.symbolics import ceil, HasLength, is_symbolic, is_zero, log2, SymbolicInt -from .kxor_instance import KXorInstance - @frozen class LoadConstraintScopes(Bloq): From 10d64ac3254f9ab26e96f622ee6afef122b03003 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Tue, 29 Apr 2025 13:15:09 +0200 Subject: [PATCH 05/10] use `Always` --- .../block_encoding/sparse_matrix_hermitian.py | 71 ++++++------------- .../k_xor_sat/kikuchi_adjacency_matrix.py | 42 ++--------- 2 files changed, 25 insertions(+), 88 deletions(-) diff --git a/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.py b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.py index 0097a2bbf8..95b077d8eb 100644 --- a/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.py +++ b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.py @@ -15,18 +15,15 @@ from collections import Counter from functools import cached_property -import attrs import numpy as np import sympy from attrs import frozen from qualtran import ( - AddControlledT, Bloq, bloq_example, BloqBuilder, BloqDocSpec, - CtrlSpec, DecomposeTypeError, QAny, QBit, @@ -35,10 +32,10 @@ Soquet, SoquetT, ) -from qualtran.bloqs.basic_gates import CSwap, Ry, Swap +from qualtran.bloqs.basic_gates import Ry, Swap from qualtran.bloqs.block_encoding import BlockEncoding from qualtran.bloqs.block_encoding.sparse_matrix import RowColumnOracle -from qualtran.bloqs.bookkeeping import Partition +from qualtran.bloqs.bookkeeping import Always, Partition from qualtran.bloqs.bookkeeping.auto_partition import AutoPartition, Unused from qualtran.bloqs.reflections.prepare_identity import PrepareIdentity from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare @@ -133,7 +130,6 @@ class SparseMatrixHermitian(BlockEncoding): col_oracle: RowColumnOracle entry_oracle: SqrtEntryOracle eps: SymbolicFloat - is_controlled: bool = False def __attrs_post_init__(self): if self.col_oracle.system_bitsize != self.entry_oracle.system_bitsize: @@ -141,10 +137,7 @@ def __attrs_post_init__(self): @cached_property def signature(self) -> Signature: - n_ctrls = 1 if self.is_controlled else 0 - return Signature.build_from_dtypes( - ctrl=QAny(n_ctrls), system=QAny(self.system_bitsize), ancilla=QAny(self.ancilla_bitsize), resource=QAny(self.resource_bitsize), # if resource_bitsize is 0, not present @@ -190,29 +183,23 @@ def diffusion(self): def build_call_graph(self, ssa: SympySymbolAllocator) -> BloqCountDictT: counts = Counter[Bloq]() - counts[self.diffusion] += 1 - counts[self.col_oracle] += 1 - counts[self.entry_oracle] += 1 - if self.is_controlled: - counts[CSwap(self.system_bitsize)] += 1 - counts[CSwap(1)] += 1 - else: - counts[Swap(self.system_bitsize)] += 1 - counts[Swap(1)] += 1 - counts[self.entry_oracle.adjoint()] += 1 - counts[self.col_oracle.adjoint()] += 1 - counts[self.diffusion.adjoint()] += 1 + counts[Always(self.diffusion)] += 1 + counts[Always(self.col_oracle)] += 1 + counts[Always(self.entry_oracle)] += 1 + counts[Swap(self.system_bitsize)] += 1 + counts[Swap(1)] += 1 + counts[Always(self.entry_oracle.adjoint())] += 1 + counts[Always(self.col_oracle.adjoint())] += 1 + counts[Always(self.diffusion.adjoint())] += 1 return counts def build_composite_bloq( - self, bb: BloqBuilder, system: SoquetT, ancilla: SoquetT, **soqs + self, bb: BloqBuilder, system: SoquetT, ancilla: SoquetT ) -> dict[str, SoquetT]: if is_symbolic(self.system_bitsize) or is_symbolic(self.col_oracle.num_nonzero): raise DecomposeTypeError(f"Cannot decompose symbolic {self=}") - ctrl = soqs.pop('ctrl', None) - assert not isinstance(ancilla, np.ndarray) partition_ancilla = Partition( n=self.ancilla_bitsize, @@ -225,42 +212,24 @@ def build_composite_bloq( a, l, b = bb.add(partition_ancilla, x=ancilla) - l = bb.add(self.diffusion, target=l) - l, system = bb.add(self.col_oracle, l=l, i=system) - b, l, system = bb.add(self.entry_oracle, q=b, i=l, j=system) + l = bb.add(Always(self.diffusion), target=l) + l, system = bb.add(Always(self.col_oracle), l=l, i=system) + b, l, system = bb.add(Always(self.entry_oracle), q=b, i=l, j=system) - if self.is_controlled: - ctrl, l, system = bb.add(CSwap(self.system_bitsize), ctrl=ctrl, x=l, y=system) - ctrl, a, b = bb.add(CSwap(1), ctrl=ctrl, x=a, y=b) - else: - l, system = bb.add(Swap(self.system_bitsize), x=l, y=system) - a, b = bb.add(Swap(1), x=a, y=b) + l, system = bb.add(Swap(self.system_bitsize), x=l, y=system) + a, b = bb.add(Swap(1), x=a, y=b) - b, l, system = bb.add(self.entry_oracle.adjoint(), q=b, i=l, j=system) - l, system = bb.add(self.col_oracle.adjoint(), l=l, i=system) - l = bb.add(self.diffusion.adjoint(), target=l) + b, l, system = bb.add(Always(self.entry_oracle.adjoint()), q=b, i=l, j=system) + l, system = bb.add(Always(self.col_oracle.adjoint()), l=l, i=system) + l = bb.add(Always(self.diffusion.adjoint()), target=l) ancilla = bb.add(partition_ancilla.adjoint(), a=a, l=l, b=b) - out_soqs = {"system": system, "ancilla": ancilla} - if self.is_controlled: - out_soqs |= {"ctrl": ctrl} - return out_soqs + return {"system": system, "ancilla": ancilla} def adjoint(self) -> 'SparseMatrixHermitian': 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=self if self.is_controlled else attrs.evolve(self, is_controlled=True), - ctrl_reg_name='ctrl', - ) - @frozen class UniformSqrtEntryOracle(SqrtEntryOracle): diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py index babc2c4404..fbd3829978 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py @@ -26,23 +26,12 @@ from collections import Counter from functools import cached_property -import attrs import sympy from attrs import frozen -from qualtran import ( - AddControlledT, - Bloq, - bloq_example, - BloqDocSpec, - CtrlSpec, - QAny, - QBit, - QFxp, - QUInt, - Signature, -) +from qualtran import Bloq, bloq_example, BloqDocSpec, QAny, QBit, QFxp, QUInt, Signature from qualtran.bloqs.arithmetic.lists import SymmetricDifference +from qualtran.bloqs.bookkeeping import Always from qualtran.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance from qualtran.bloqs.optimization.k_xor_sat.load_kxor_instance import ( LoadUniqueScopeIndex, @@ -97,15 +86,11 @@ class KikuchiMatrixEntry(Bloq): inst: KXorInstance ell: SymbolicInt entry_bitsize: SymbolicInt - is_controlled: bool = False @property def signature(self) -> 'Signature': return Signature.build_from_dtypes( - ctrl=QAny(1 if self.is_controlled else 0), - S=QAny(self.composite_index_bitsize), - T=QAny(self.composite_index_bitsize), - q=QBit(), + S=QAny(self.composite_index_bitsize), T=QAny(self.composite_index_bitsize), q=QBit() ) @cached_property @@ -125,38 +110,21 @@ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT: counts = Counter[Bloq]() # S \Delta T - symm_diff = SymmetricDifference(self.ell, self.ell, self.inst.k, self.index_dtype) + symm_diff = Always(SymmetricDifference(self.ell, self.ell, self.inst.k, self.index_dtype)) counts[symm_diff] += 1 counts[symm_diff.adjoint()] += 1 # Map S to j, such that U_j = S - load_idx = LoadUniqueScopeIndex(self.inst) + load_idx = Always(LoadUniqueScopeIndex(self.inst)) counts[load_idx] += 1 counts[load_idx.adjoint()] += 1 # apply the rotation rotation: Bloq = PRGAUniqueConstraintRHS(self.inst, self.entry_bitsize) - if self.is_controlled: - rotation = rotation.controlled() counts[rotation] += 1 return counts - 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 - - ctrl_bit, ctrl_bloq = ( - (1, self) if self.is_controlled else (None, attrs.evolve(self, is_controlled=True)) - ) - - return get_ctrl_system_1bit_cv_from_bloqs( - self, - ctrl_spec, - current_ctrl_bit=ctrl_bit, - bloq_with_ctrl=ctrl_bloq, - ctrl_reg_name='ctrl', - ) - @bloq_example def _kikuchi_matrix_entry() -> KikuchiMatrixEntry: From 28685736ae5d51d220236df0e22c8510630b0a7f Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 18 Aug 2025 22:26:55 +0200 Subject: [PATCH 06/10] doc nits --- .../optimization/k_xor_sat/kikuchi_adjacency_list.py | 11 +++++++++-- .../bloqs/optimization/k_xor_sat/kxor_instance.py | 10 ++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py index 249e209fbf..9668cd5f28 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py @@ -61,6 +61,8 @@ class ColumnOfKthNonZeroEntry(Bloq): Using $f'$ ensures the computation is reversible. Note: we must use the same extension $f'$ for both oracles. + See docstring for :class:`KXorInstance` for the overall problem definition. + This algorithm is described by the following pseudo-code: ``` def forward(S, k) -> f_S_k: @@ -173,6 +175,8 @@ class IndexOfNonZeroColumn(Bloq): Using $f'$ ensures the computation is reversible. Note: we must use the same extension $f'$ for both oracles. + See docstring for :class:`KXorInstance` for the overall problem definition. + This algorithm is described by the following pseudo-code: ``` def reverse(S, f_S_k) -> k: @@ -266,7 +270,10 @@ class KikuchiNonZeroIndex(Bloq): For a given row $S$ and column $T$, the entry $\mathcal{K}_{k}_{S, T}$ is potentially non-zero if $S \Delta T = U_j$ for some $j$, which is equivalent to $T = S \Delta U_j$. - Here, $U_j$ is the $j$-th unique scope in the instance $\mathcal{I}$. + Here, $U_j$ is the $j$-th unique scope in the instance $\mathcal{I}$, + and $\Delta$ is the symmetric difference operator. + + See docstring for :class:`KXorInstance` for the overall problem definition. To find the $k$-th non-zero entry, we use two oracles: 1. $(S, k) \mapsto f(S, k)$, implemented by `ColumnOfKthNonZeroEntry` @@ -278,7 +285,7 @@ class KikuchiNonZeroIndex(Bloq): Note on sparsity: This bloq expects the user to provide the sparsity, as it is in general difficult to compute the precise sparsity of the Kikuchi matrix efficiently. As long as the provided number is at least the true sparsity, the algorithm will work as expected. - In case the provides sparsity is smaller, it is equivalent to making the remaining entries zero in the final block encoding. + In case the provided sparsity is smaller, it is equivalent to making the remaining entries zero in the final block encoding. Args: inst: the kXOR instance $\mathcal{I}$. diff --git a/qualtran/bloqs/optimization/k_xor_sat/kxor_instance.py b/qualtran/bloqs/optimization/k_xor_sat/kxor_instance.py index 913ae06ae0..e2a19f7c4d 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kxor_instance.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kxor_instance.py @@ -93,8 +93,9 @@ 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\}$. + consists of a multiset of constraints $\mathcal{C} = (S, b)$, where each + $S \subseteq [n]$ has cardinality $k$ and is called a scope, and each right-hand side + satisfies $b \in \{\pm 1\}$. Attributes: n: number of variables. @@ -200,6 +201,11 @@ def num_unique_constraints(self) -> SymbolicInt: def batched_scopes(self) -> Union[tuple[tuple[Scope, int], ...], HasLength]: r"""Group all the constraints by Scope, and add up the $b$ values. + A scope is a subset of variables of size $k$. + Given an instance $\mathcal{I} = \{ (S_i, b_i) \}_i$, this function groups + all equal sets $S_i$ by summing up the rhs-values $b_i$ corresponding to them. + The resulting sequence therefore has all unique scopes. + This is a classical preprocessing step. Time $k m \log m$. """ if self.is_symbolic(): From b6a33530231ac21d7660bf07141af6c24357056e Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 18 Aug 2025 22:30:49 +0200 Subject: [PATCH 07/10] override `Always.build_call_graph` --- qualtran/bloqs/bookkeeping/always.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qualtran/bloqs/bookkeeping/always.py b/qualtran/bloqs/bookkeeping/always.py index 070e34eaf9..7df1845b37 100644 --- a/qualtran/bloqs/bookkeeping/always.py +++ b/qualtran/bloqs/bookkeeping/always.py @@ -25,6 +25,7 @@ Signature, SoquetT, ) +from qualtran.resource_counting import SympySymbolAllocator, BloqCountDictT @attrs.frozen @@ -67,6 +68,9 @@ def signature(self) -> 'Signature': def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> dict[str, 'SoquetT']: return bb.add_d(self.subbloq, **soqs) + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': + return self.subbloq.build_call_graph(ssa) + def get_ctrl_system( self, ctrl_spec: Optional['CtrlSpec'] = None ) -> tuple['Bloq', 'AddControlledT']: From 3ff73b63a372f4c9602c2df2ca945a3a1d7c3e80 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 18 Aug 2025 22:33:34 +0200 Subject: [PATCH 08/10] notebooks --- .../optimization/k_xor_sat/kikuchi_adjacency_list.ipynb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.ipynb b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.ipynb index efb128a69b..7ed33e3840 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.ipynb +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.ipynb @@ -48,7 +48,10 @@ "For a given row $S$ and column $T$, the entry $\\mathcal{K}_{k}_{S, T}$\n", "is potentially non-zero if $S \\Delta T = U_j$ for some $j$, which is\n", "equivalent to $T = S \\Delta U_j$.\n", - "Here, $U_j$ is the $j$-th unique scope in the instance $\\mathcal{I}$.\n", + "Here, $U_j$ is the $j$-th unique scope in the instance $\\mathcal{I}$,\n", + "and $\\Delta$ is the symmetric difference operator.\n", + "\n", + "See docstring for :class:`KXorInstance` for the overall problem definition.\n", "\n", "To find the $k$-th non-zero entry, we use two oracles:\n", "1. $(S, k) \\mapsto f(S, k)$, implemented by `ColumnOfKthNonZeroEntry`\n", @@ -60,7 +63,7 @@ "Note on sparsity: This bloq expects the user to provide the sparsity, as it is in general\n", "difficult to compute the precise sparsity of the Kikuchi matrix efficiently. As long as the\n", "provided number is at least the true sparsity, the algorithm will work as expected.\n", - "In case the provides sparsity is smaller, it is equivalent to making the remaining entries zero in the final block encoding.\n", + "In case the provided sparsity is smaller, it is equivalent to making the remaining entries zero in the final block encoding.\n", "\n", "#### Parameters\n", " - `inst`: the kXOR instance $\\mathcal{I}$.\n", From bc98260ae08be3e21809c09345d768551b8b7eda Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 18 Aug 2025 22:35:02 +0200 Subject: [PATCH 09/10] lint --- qualtran/bloqs/bookkeeping/always.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qualtran/bloqs/bookkeeping/always.py b/qualtran/bloqs/bookkeeping/always.py index 7df1845b37..905f79a2fe 100644 --- a/qualtran/bloqs/bookkeeping/always.py +++ b/qualtran/bloqs/bookkeeping/always.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Iterable, Optional, Sequence +from typing import Iterable, Optional, Sequence, Union import attrs @@ -25,7 +25,7 @@ Signature, SoquetT, ) -from qualtran.resource_counting import SympySymbolAllocator, BloqCountDictT +from qualtran.resource_counting import BloqCountDictT, BloqCountT, SympySymbolAllocator @attrs.frozen @@ -68,7 +68,9 @@ def signature(self) -> 'Signature': def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> dict[str, 'SoquetT']: return bb.add_d(self.subbloq, **soqs) - def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': + def build_call_graph( + self, ssa: 'SympySymbolAllocator' + ) -> Union['BloqCountDictT', set['BloqCountT']]: return self.subbloq.build_call_graph(ssa) def get_ctrl_system( From d6b4b5bef099a5a1a6b35b459fece5977006a443 Mon Sep 17 00:00:00 2001 From: Anurudh Peduri Date: Mon, 18 Aug 2025 22:48:49 +0200 Subject: [PATCH 10/10] docs: refer to `KXorInstance` --- .../bloqs/optimization/k_xor_sat/kxor_instance.py | 6 ++++++ .../optimization/k_xor_sat/load_kxor_instance.ipynb | 8 +++++--- .../optimization/k_xor_sat/load_kxor_instance.py | 12 +++++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/qualtran/bloqs/optimization/k_xor_sat/kxor_instance.py b/qualtran/bloqs/optimization/k_xor_sat/kxor_instance.py index e2a19f7c4d..70f08821a6 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/kxor_instance.py +++ b/qualtran/bloqs/optimization/k_xor_sat/kxor_instance.py @@ -207,6 +207,12 @@ def batched_scopes(self) -> Union[tuple[tuple[Scope, int], ...], HasLength]: The resulting sequence therefore has all unique scopes. This is a classical preprocessing step. Time $k m \log m$. + + See the reference for the definition of the distinct scopes. + + References: + [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) + Theorem 4.17, proof paragraph 2. """ if self.is_symbolic(): return HasLength(self.m) diff --git a/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.ipynb b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.ipynb index 7987d624e4..9a08818e67 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.ipynb +++ b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.ipynb @@ -19,10 +19,11 @@ "3. `PRGAUniqueConstraintRHS` Given $j$, apply $Rx(arccos(\\sqrt{B_\\mathcal{I}(S)/M}))$ on a target qubit.\n", "(for an appropriate normalization $M$).\n", "\n", - "\n", "The first two oracles are independent of the RHS.\n", "All these oracles can output arbitrary values for invalid inputs.\n", "\n", + "See :class:`KXorInstance` for the overall problem definition.\n", + "\n", "References:\n", " [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)\n", " Notation 2.24 for $B_\\mathcal{I}$.\n", @@ -61,9 +62,10 @@ "Assuming `inst` has $\\bar{m}$ unique constraints, we define $U_j \\in {[n] \\choose k}$\n", "for $j \\in [\\bar{m}]$ as the $j$-th unique constraint scope.\n", "\n", - "The scopes are loaded using a QROM.\n", + "See :class:`KXorInstance` for the overall problem definition.\n", "\n", - "If the input contains an invalid index, then any arbitrary value can be output.\n", + "The scopes are loaded using a QROM. If the input contains an invalid index, then\n", + "the output can be an arbitrary value.\n", "\n", "#### Registers\n", " - `j`: a number in [\\bar{m}]\n", diff --git a/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py index 932592e8dd..21a481e8a5 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py +++ b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py @@ -21,10 +21,11 @@ 3. `PRGAUniqueConstraintRHS` Given $j$, apply $Rx(arccos(\sqrt{B_\mathcal{I}(S)/M}))$ on a target qubit. (for an appropriate normalization $M$). - The first two oracles are independent of the RHS. All these oracles can output arbitrary values for invalid inputs. +See :class:`KXorInstance` for the overall problem definition. + References: [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1) Notation 2.24 for $B_\mathcal{I}$. @@ -73,9 +74,10 @@ class LoadConstraintScopes(Bloq): Assuming `inst` has $\bar{m}$ unique constraints, we define $U_j \in {[n] \choose k}$ for $j \in [\bar{m}]$ as the $j$-th unique constraint scope. - The scopes are loaded using a QROM. + See :class:`KXorInstance` for the overall problem definition. - If the input contains an invalid index, then any arbitrary value can be output. + The scopes are loaded using a QROM. If the input contains an invalid index, then + the output can be an arbitrary value. Registers: j: a number in [\bar{m}] @@ -196,6 +198,8 @@ class LoadUniqueScopeIndex(Bloq): If the input contains an invalid scope, then any arbitrary value can be output. + See :class:`KXorInstance` for the overall problem definition. + Registers: S: A scope $S \in {[n] \choose k}$. j (RIGHT): a number in $[\bar{m}]$ s.t. $S = U_j$. @@ -245,6 +249,8 @@ class PRGAUniqueConstraintRHS(Bloq): We ensure that the input data is sorted, therefore we can simply compare $j$ with the largest negative index, and apply a `CS` gate. + See :class:`KXorInstance` for the overall problem definition. + Args: inst: kXOR instance $\mathcal{I}$. angle_bitsize: number of bits to load the amplitude rotation angles to.