diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py index 9bce8ea5f..be00269fe 100644 --- a/dev_tools/qualtran_dev_tools/notebook_specs.py +++ b/dev_tools/qualtran_dev_tools/notebook_specs.py @@ -107,6 +107,7 @@ 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.kikuchi_guiding_state import qualtran.bloqs.phase_estimation.lp_resource_state import qualtran.bloqs.phase_estimation.qubitization_qpe @@ -839,6 +840,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, @@ -846,7 +853,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 bb5a021fa..b85d7ba58 100644 --- a/docs/bloqs/index.rst +++ b/docs/bloqs/index.rst @@ -156,7 +156,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/block_encoding/sparse_matrix_hermitian.py b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.py index 0097a2bbf..95b077d8e 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/bookkeeping/always.py b/qualtran/bloqs/bookkeeping/always.py index 070e34eaf..905f79a2f 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,6 +25,7 @@ Signature, SoquetT, ) +from qualtran.resource_counting import BloqCountDictT, BloqCountT, SympySymbolAllocator @attrs.frozen @@ -67,6 +68,11 @@ 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' + ) -> Union['BloqCountDictT', set['BloqCountT']]: + return self.subbloq.build_call_graph(ssa) + def get_ctrl_system( self, ctrl_spec: Optional['CtrlSpec'] = None ) -> tuple['Bloq', 'AddControlledT']: diff --git a/qualtran/bloqs/optimization/k_xor_sat/__init__.py b/qualtran/bloqs/optimization/k_xor_sat/__init__.py index 4cbf2722c..79b00094a 100644 --- a/qualtran/bloqs/optimization/k_xor_sat/__init__.py +++ b/qualtran/bloqs/optimization/k_xor_sat/__init__.py @@ -11,5 +11,9 @@ # 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 +from .load_kxor_instance import LoadConstraintScopes, LoadUniqueScopeIndex, PRGAUniqueConstraintRHS 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 000000000..7ed33e384 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.ipynb @@ -0,0 +1,183 @@ +{ + "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", + "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", + "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 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", + " - `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 000000000..9668cd5f2 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list.py @@ -0,0 +1,368 @@ +# 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.bloqs.optimization.k_xor_sat.kxor_instance import KXorInstance +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator +from qualtran.symbolics import SymbolicInt + + +@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. + + 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: + 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. + + 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: + 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}$, + 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` + 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 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}$. + 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 000000000..88e97c19d --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_list_test.py @@ -0,0 +1,74 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unittest.mock import ANY + +import pytest +import sympy + +import qualtran.testing as qlt_testing +from qualtran.bloqs.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( + "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): + 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 000000000..599e88a21 --- /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 000000000..fbd382997 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix.py @@ -0,0 +1,154 @@ +# 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 sympy +from attrs import frozen + +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, + PRGAUniqueConstraintRHS, +) +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator +from qualtran.symbolics import SymbolicInt + + +@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 + + @property + def signature(self) -> 'Signature': + return Signature.build_from_dtypes( + 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 = 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 = Always(LoadUniqueScopeIndex(self.inst)) + counts[load_idx] += 1 + counts[load_idx.adjoint()] += 1 + + # apply the rotation + rotation: Bloq = PRGAUniqueConstraintRHS(self.inst, self.entry_bitsize) + counts[rotation] += 1 + + return counts + + +@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 000000000..af1fdd8bd --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_adjacency_matrix_test.py @@ -0,0 +1,94 @@ +# 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.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 + + +@pytest.mark.parametrize( + "bloq_ex", [_kikuchi_matrix_entry, _kikuchi_matrix_entry_symb], ids=lambda be: be.name +) +def test_examples(bloq_autotester, bloq_ex): + 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 000000000..78031452a --- /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 000000000..1929db7ae --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding.py @@ -0,0 +1,235 @@ +# 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.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 + + +@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 000000000..c3a11d5b6 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_block_encoding_test.py @@ -0,0 +1,57 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +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 + + +@pytest.mark.parametrize("bloq_ex", [_kikuchi_matrix, _kikuchi_matrix_symb], ids=lambda be: be.name) +def test_examples(bloq_autotester, bloq_ex): + 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/kikuchi_guiding_state.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_guiding_state.py index 0631e83d1..0a96fbb1e 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/kikuchi_guiding_state_test.py b/qualtran/bloqs/optimization/k_xor_sat/kikuchi_guiding_state_test.py index 0e75a0464..c215aad25 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/kxor_instance.py b/qualtran/bloqs/optimization/k_xor_sat/kxor_instance.py index 913ae06ae..70f08821a 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,7 +201,18 @@ 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$. + + 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/kxor_instance_test.py b/qualtran/bloqs/optimization/k_xor_sat/kxor_instance_test.py index 700dfd6eb..9b80e6b70 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.ipynb b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.ipynb new file mode 100644 index 000000000..9a08818e6 --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.ipynb @@ -0,0 +1,209 @@ +{ + "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", + "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", + " 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", + "See :class:`KXorInstance` for the overall problem definition.\n", + "\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", + " - `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 000000000..21a481e8a --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance.py @@ -0,0 +1,359 @@ +# 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. + +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}$. + 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.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 + + +@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. + + See :class:`KXorInstance` for the overall problem definition. + + 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}] + 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. + + 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$. + 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. + + 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. + + 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 000000000..4df56e76b --- /dev/null +++ b/qualtran/bloqs/optimization/k_xor_sat/load_kxor_instance_test.py @@ -0,0 +1,51 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unittest.mock import ANY + +import pytest + +import qualtran.testing as qlt_testing +from qualtran 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 + + +@pytest.mark.parametrize("bloq", [_load_scopes, _load_scopes_symb], ids=lambda be: be.name) +def test_examples(bloq_autotester, bloq: Bloq): + 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) + + +@pytest.mark.notebook +def test_notebook(): + qlt_testing.execute_notebook('load_kxor_instance') diff --git a/qualtran/conftest.py b/qualtran/conftest.py index 90b1a09ab..11d76c0c9 100644 --- a/qualtran/conftest.py +++ b/qualtran/conftest.py @@ -164,6 +164,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 eab5d2c4b..8a8d42e4d 100644 --- a/qualtran/serialization/resolver_dict.py +++ b/qualtran/serialization/resolver_dict.py @@ -129,6 +129,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 @@ -406,6 +412,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,