diff --git a/dev_tools/autogenerate-bloqs-notebooks-v2.py b/dev_tools/autogenerate-bloqs-notebooks-v2.py
index 8d7bb9620..638ce9531 100644
--- a/dev_tools/autogenerate-bloqs-notebooks-v2.py
+++ b/dev_tools/autogenerate-bloqs-notebooks-v2.py
@@ -72,6 +72,7 @@
import qualtran.bloqs.block_encoding.phase
import qualtran.bloqs.block_encoding.product
import qualtran.bloqs.block_encoding.sparse_matrix
+import qualtran.bloqs.block_encoding.sparse_matrix_hermitian
import qualtran.bloqs.block_encoding.tensor_product
import qualtran.bloqs.block_encoding.unitary
import qualtran.bloqs.bookkeeping
@@ -111,6 +112,18 @@
import qualtran.bloqs.gf_arithmetic.gf2_multiplication
import qualtran.bloqs.gf_arithmetic.gf2_square
import qualtran.bloqs.hamiltonian_simulation.hamiltonian_simulation_by_gqsp
+import qualtran.bloqs.max_k_xor_sat
+import qualtran.bloqs.max_k_xor_sat.arithmetic
+import qualtran.bloqs.max_k_xor_sat.arithmetic.has_duplicates
+import qualtran.bloqs.max_k_xor_sat.arithmetic.sort_in_place
+import qualtran.bloqs.max_k_xor_sat.arithmetic.symmetric_difference
+import qualtran.bloqs.max_k_xor_sat.guided_hamiltonian
+import qualtran.bloqs.max_k_xor_sat.guided_hamiltonian.guided_hamiltonian
+import qualtran.bloqs.max_k_xor_sat.guided_hamiltonian.walk_operator
+import qualtran.bloqs.max_k_xor_sat.guiding_state
+import qualtran.bloqs.max_k_xor_sat.kikuchi_block_encoding
+import qualtran.bloqs.max_k_xor_sat.load_kxor_instance
+import qualtran.bloqs.max_k_xor_sat.planted_noisy_kxor
import qualtran.bloqs.mcmt.and_bloq
import qualtran.bloqs.mcmt.controlled_via_and
import qualtran.bloqs.mcmt.ctrl_spec_and
@@ -746,6 +759,13 @@
module=qualtran.bloqs.block_encoding.sparse_matrix,
bloq_specs=[qualtran.bloqs.block_encoding.sparse_matrix._SPARSE_MATRIX_DOC],
),
+ NotebookSpecV2(
+ title='Sparse Matrix (Hermitian)',
+ module=qualtran.bloqs.block_encoding.sparse_matrix_hermitian,
+ bloq_specs=[
+ qualtran.bloqs.block_encoding.sparse_matrix_hermitian._SPARSE_MATRIX_HERMITIAN_DOC
+ ],
+ ),
NotebookSpecV2(
title='Chebyshev Polynomial',
module=qualtran.bloqs.block_encoding.chebyshev_polynomial,
@@ -875,6 +895,57 @@
],
),
]
+# --------------------------------------------------------------------------
+# ----- Quartic Speedups paper ------------------------------------------
+# --------------------------------------------------------------------------
+ALGO_QUARTIC_SPEEDUPS = [
+ # ----- Preliminaries ------------------------------------------
+ NotebookSpecV2(
+ title='Guided (sparse) Hamiltonian Problem',
+ module=qualtran.bloqs.max_k_xor_sat.guided_hamiltonian.guided_hamiltonian,
+ bloq_specs=[
+ qualtran.bloqs.max_k_xor_sat.guided_hamiltonian.guided_hamiltonian._GUIDED_HAMILTONIAN_DOC,
+ qualtran.bloqs.max_k_xor_sat.guided_hamiltonian.guided_hamiltonian._GUIDED_HAMILTONIAN_PHASE_ESTIMATION_DOC,
+ ],
+ ),
+ NotebookSpecV2(
+ title='Arithmetic Primitives',
+ module=qualtran.bloqs.max_k_xor_sat.arithmetic,
+ bloq_specs=[
+ qualtran.bloqs.max_k_xor_sat.arithmetic.sort_in_place._SORT_IN_PLACE_DOC,
+ qualtran.bloqs.max_k_xor_sat.arithmetic.symmetric_difference._SYMMETRIC_DIFFERENCE_DOC,
+ qualtran.bloqs.max_k_xor_sat.arithmetic.has_duplicates._HAS_DUPLICATES_DOC,
+ ],
+ ),
+ # ----- Algorithm ------------------------------------------
+ NotebookSpecV2(
+ title='kXOR: Instance load Oracles',
+ module=qualtran.bloqs.max_k_xor_sat.load_kxor_instance,
+ bloq_specs=[qualtran.bloqs.max_k_xor_sat.load_kxor_instance._LOAD_INSTANCE_DOC],
+ ),
+ NotebookSpecV2(
+ title='Noisy kXOR: Guiding State',
+ module=qualtran.bloqs.max_k_xor_sat.guiding_state,
+ bloq_specs=[
+ qualtran.bloqs.max_k_xor_sat.guiding_state._SIMPLE_GUIDING_STATE_DOC,
+ qualtran.bloqs.max_k_xor_sat.guiding_state._GUIDING_STATE_DOC,
+ ],
+ ),
+ NotebookSpecV2(
+ title='Noisy kXOR: Block-encoding the Kikuchi Matrix',
+ module=qualtran.bloqs.max_k_xor_sat.kikuchi_block_encoding,
+ bloq_specs=[
+ qualtran.bloqs.max_k_xor_sat.kikuchi_adjacency_matrix._KIKUCHI_MATRIX_ENTRY_DOC,
+ qualtran.bloqs.max_k_xor_sat.kikuchi_adjacency_list._KIKUCHI_NONZERO_INDEX_DOC,
+ qualtran.bloqs.max_k_xor_sat.kikuchi_block_encoding._KIKUCHI_HAMILTONIAN_DOC,
+ ],
+ ),
+ NotebookSpecV2(
+ title='Algorithm: Planted Noise kXOR',
+ module=qualtran.bloqs.max_k_xor_sat.planted_noisy_kxor,
+ bloq_specs=[qualtran.bloqs.max_k_xor_sat.planted_noisy_kxor._PLANTED_NOISY_KXOR_DOC],
+ ),
+]
# --------------------------------------------------------------------------
# ----- Concepts -------------------------------------------------------
@@ -902,6 +973,7 @@
('GF Arithmetic', GF_ARITHMETIC),
('Rotations', ROT_QFT_PE),
('Block Encoding', BLOCK_ENCODING),
+ ('Paper: Quartic Quantum Speedups for Planted Inference', ALGO_QUARTIC_SPEEDUPS),
('Other', OTHER),
]
diff --git a/docs/bloqs/index.rst b/docs/bloqs/index.rst
index 0906dd71b..c56bf3a06 100644
--- a/docs/bloqs/index.rst
+++ b/docs/bloqs/index.rst
@@ -129,9 +129,21 @@ Bloqs Library
block_encoding/phase.ipynb
block_encoding/linear_combination.ipynb
block_encoding/sparse_matrix.ipynb
+ block_encoding/sparse_matrix_hermitian.ipynb
block_encoding/chebyshev_polynomial.ipynb
block_encoding/lcu_block_encoding.ipynb
+.. toctree::
+ :maxdepth: 2
+ :caption: Paper: Quartic Quantum Speedups for Planted Inference:
+
+ max_k_xor_sat/guided_hamiltonian/guided_hamiltonian.ipynb
+ max_k_xor_sat/arithmetic/arithmetic.ipynb
+ max_k_xor_sat/load_kxor_instance.ipynb
+ max_k_xor_sat/guiding_state.ipynb
+ max_k_xor_sat/kikuchi_block_encoding.ipynb
+ max_k_xor_sat/planted_noisy_kxor.ipynb
+
.. toctree::
:maxdepth: 2
:caption: Other:
diff --git a/qualtran/bloqs/arithmetic/sorting.py b/qualtran/bloqs/arithmetic/sorting.py
index d18db924e..c500effc9 100644
--- a/qualtran/bloqs/arithmetic/sorting.py
+++ b/qualtran/bloqs/arithmetic/sorting.py
@@ -23,6 +23,7 @@
bloq_example,
BloqBuilder,
BloqDocSpec,
+ DecomposeNotImplementedError,
DecomposeTypeError,
QBit,
QUInt,
@@ -222,8 +223,6 @@ def __attrs_post_init__(self):
k = self.half_length
if not is_symbolic(k):
assert k >= 1, "length of input lists must be positive"
- # TODO(#1090) support non-power-of-two input lengths
- assert (k & (k - 1)) == 0, "length of input lists must be a power of 2"
@cached_property
def signature(self) -> 'Signature':
@@ -249,14 +248,16 @@ def is_symbolic(self):
def build_composite_bloq(
self, bb: 'BloqBuilder', xs: 'SoquetT', ys: 'SoquetT'
) -> dict[str, 'SoquetT']:
- if is_symbolic(self.half_length):
+ k = self.half_length
+ if is_symbolic(k):
raise DecomposeTypeError(f"Cannot decompose symbolic {self=}")
+ if (k & (k - 1)) == 0:
+ # TODO(#1090) support non-power-of-two input lengths
+ raise DecomposeNotImplementedError("length of input lists must be a power of 2")
assert isinstance(xs, np.ndarray)
assert isinstance(ys, np.ndarray)
- k = self.half_length
-
first_round_junk = []
for i in range(k):
xs[i], ys[k - 1 - i], anc = bb.add(Comparator(self.bitsize), a=xs[i], b=ys[k - 1 - i])
diff --git a/qualtran/bloqs/basic_gates/x_basis.py b/qualtran/bloqs/basic_gates/x_basis.py
index 76fd85dcb..3f32efd1d 100644
--- a/qualtran/bloqs/basic_gates/x_basis.py
+++ b/qualtran/bloqs/basic_gates/x_basis.py
@@ -227,13 +227,14 @@ def my_tensors(
def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> Tuple['Bloq', 'AddControlledT']:
from qualtran.bloqs.basic_gates import CNOT, Toffoli
+ from qualtran.bloqs.mcmt import ControlledViaAnd
if ctrl_spec == CtrlSpec():
bloq: 'Bloq' = CNOT()
elif ctrl_spec == CtrlSpec(cvs=(1, 1)):
bloq = Toffoli()
else:
- return super().get_ctrl_system(ctrl_spec)
+ return ControlledViaAnd.make_ctrl_system(self, ctrl_spec)
def add_controlled(
bb: 'BloqBuilder', ctrl_soqs: Sequence['SoquetT'], in_soqs: Dict[str, 'SoquetT']
diff --git a/qualtran/bloqs/block_encoding/__init__.py b/qualtran/bloqs/block_encoding/__init__.py
index 6bf24ff4a..03c07b298 100644
--- a/qualtran/bloqs/block_encoding/__init__.py
+++ b/qualtran/bloqs/block_encoding/__init__.py
@@ -23,5 +23,6 @@
from qualtran.bloqs.block_encoding.phase import Phase
from qualtran.bloqs.block_encoding.product import Product
from qualtran.bloqs.block_encoding.sparse_matrix import SparseMatrix
+from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import SparseMatrixHermitian
from qualtran.bloqs.block_encoding.tensor_product import TensorProduct
from qualtran.bloqs.block_encoding.unitary import Unitary
diff --git a/qualtran/bloqs/block_encoding/block_encoding_base.py b/qualtran/bloqs/block_encoding/block_encoding_base.py
index 93e631529..76c9bdb51 100644
--- a/qualtran/bloqs/block_encoding/block_encoding_base.py
+++ b/qualtran/bloqs/block_encoding/block_encoding_base.py
@@ -84,6 +84,14 @@ def ancilla_bitsize(self) -> SymbolicInt:
def resource_bitsize(self) -> SymbolicInt:
"""The number of resource qubits not counted in ancillas."""
+ @property
+ def ctrl_bitsize(self) -> SymbolicInt:
+ """The number of control qubits, useful to define optimized custom controlled circuits.
+
+ Usually either 0 or 1, as all other control cases can be reduced to 1.
+ """
+ return 0
+
@property
@abc.abstractmethod
def epsilon(self) -> SymbolicFloat:
diff --git a/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.ipynb b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.ipynb
new file mode 100644
index 000000000..c8bd6bf0a
--- /dev/null
+++ b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.ipynb
@@ -0,0 +1,203 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "30c4ebd9",
+ "metadata": {
+ "cq.autogen": "title_cell"
+ },
+ "source": [
+ "# Sparse Matrix (Hermitian)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "e5d9072c",
+ "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": "9cb4d637",
+ "metadata": {
+ "cq.autogen": "SparseMatrixHermitian.bloq_doc.md"
+ },
+ "source": [
+ "## `SparseMatrixHermitian`\n",
+ "Hermitian Block encoding of a sparse-access Hermitian matrix.\n",
+ "\n",
+ "Given column and entry oracles $O_c$ and $O_A$ for an $s$-sparse Hermitian matrix\n",
+ "$A \\in \\mathbb{C}^{2^n \\times 2^n}$, i.e. one where each row / column has exactly $s$ non-zero\n",
+ "entries, computes a $(s, n+1, \\epsilon)$-block encoding of $A$ as follows:\n",
+ "```\n",
+ " ┌────┐\n",
+ "a |0> ─┤ ├─ |0> ───────────────────────X────────────────────\n",
+ " │ │ ┌──┐ | ┌──┐\n",
+ " │ U │ = │ n│ ┌────┐ ┌────┐ | ┌────┐ ┌────┐ │ n│\n",
+ "l |0^n> ─┤ A ├─ |0^n> ─┤H ├─┤ O ├─┤ ├─X──|─┤ ├─┤ O* ├─┤H ├─\n",
+ " │ │ └──┘ | c | │ │ | | │ │ | c | └──┘\n",
+ " │ │ └────┘ │ O │ │ | │ O* │ └────┘\n",
+ "b |0> ─┤ ├─ |0> ────────|────┤ A ├─|──X─┤ A ├───|─────────\n",
+ " | | ┌────┐ | | | | | ┌────┐\n",
+ " | | | O | | | | | | | O* |\n",
+ "j |Psi> ─┤ ├─ |Psi> ──────┤ c ├─┤ ├─X────┤ ├─┤ c ├──────\n",
+ " └────┘ └────┘ └────┘ └────┘ └────┘\n",
+ "```\n",
+ "\n",
+ "To encode a matrix of irregular dimension, the matrix should first be embedded into one of\n",
+ "dimension $2^n \\times 2^n$ for suitable $n$.\n",
+ "To encode a matrix where each row / column has at most $s$ non-zero entries, some zeroes should\n",
+ "be treated as if they were non-zero so that each row / column has exactly $s$ non-zero entries.\n",
+ "\n",
+ "For encoding a non-hermitian matrix, or a slightly more efficient (but non Hermitian-encoding)\n",
+ "of a matrix, use :class:`SparseMatrix` instead.\n",
+ "\n",
+ "#### Parameters\n",
+ " - `col_oracle`: The column oracle $O_c$. See `RowColumnOracle` for definition.\n",
+ " - `entry_oracle`: The entry oracle $O_A$. See `EntryOracle` for definition.\n",
+ " - `eps`: The precision of the block encoding. \n",
+ "\n",
+ "#### Registers\n",
+ " - `ctrl`: The single qubit control register. (present only if `cv` is not `None`)\n",
+ " - `system`: The system register.\n",
+ " - `ancilla`: The ancilla register.\n",
+ " - `resource`: The resource register (present only if `bitsize > 0`). \n",
+ "\n",
+ "#### References\n",
+ " - [Lecture Notes on Quantum Algorithms for Scientific Computation](https://arxiv.org/abs/2201.08309). Lin Lin (2022). Ch. 6.5. Proposition 6.8, Fig 6.7.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "01cdcc22",
+ "metadata": {
+ "cq.autogen": "SparseMatrixHermitian.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.block_encoding import SparseMatrixHermitian"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6bd99e38",
+ "metadata": {
+ "cq.autogen": "SparseMatrixHermitian.example_instances.md"
+ },
+ "source": [
+ "### Example Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8337e6aa",
+ "metadata": {
+ "cq.autogen": "SparseMatrixHermitian.sparse_matrix_symb_hermitian_block_encoding"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.block_encoding.sparse_matrix import TopLeftRowColumnOracle\n",
+ "from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import UniformSqrtEntryOracle\n",
+ "\n",
+ "n = sympy.Symbol('n', positive=True, integer=True)\n",
+ "col_oracle = TopLeftRowColumnOracle(system_bitsize=n)\n",
+ "entry_oracle = UniformSqrtEntryOracle(system_bitsize=n, entry=0.3)\n",
+ "sparse_matrix_symb_hermitian_block_encoding = SparseMatrixHermitian(\n",
+ " col_oracle, entry_oracle, eps=0\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "642141ad",
+ "metadata": {
+ "cq.autogen": "SparseMatrixHermitian.sparse_matrix_hermitian_block_encoding"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.block_encoding.sparse_matrix import TopLeftRowColumnOracle\n",
+ "from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import UniformSqrtEntryOracle\n",
+ "\n",
+ "col_oracle = TopLeftRowColumnOracle(system_bitsize=2)\n",
+ "entry_oracle = UniformSqrtEntryOracle(system_bitsize=2, entry=0.3)\n",
+ "sparse_matrix_hermitian_block_encoding = SparseMatrixHermitian(col_oracle, entry_oracle, eps=0)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4f3434b5",
+ "metadata": {
+ "cq.autogen": "SparseMatrixHermitian.graphical_signature.md"
+ },
+ "source": [
+ "#### Graphical Signature"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4b875699",
+ "metadata": {
+ "cq.autogen": "SparseMatrixHermitian.graphical_signature.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.drawing import show_bloqs\n",
+ "show_bloqs([sparse_matrix_symb_hermitian_block_encoding, sparse_matrix_hermitian_block_encoding],\n",
+ " ['`sparse_matrix_symb_hermitian_block_encoding`', '`sparse_matrix_hermitian_block_encoding`'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a0918562",
+ "metadata": {
+ "cq.autogen": "SparseMatrixHermitian.call_graph.md"
+ },
+ "source": [
+ "### Call Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8128a66e",
+ "metadata": {
+ "cq.autogen": "SparseMatrixHermitian.call_graph.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.resource_counting.generalizers import ignore_split_join\n",
+ "sparse_matrix_symb_hermitian_block_encoding_g, sparse_matrix_symb_hermitian_block_encoding_sigma = sparse_matrix_symb_hermitian_block_encoding.call_graph(max_depth=1, generalizer=ignore_split_join)\n",
+ "show_call_graph(sparse_matrix_symb_hermitian_block_encoding_g)\n",
+ "show_counts_sigma(sparse_matrix_symb_hermitian_block_encoding_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/block_encoding/sparse_matrix_hermitian.py b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.py
new file mode 100644
index 000000000..76515252b
--- /dev/null
+++ b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.py
@@ -0,0 +1,318 @@
+# 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 abc
+from collections import Counter
+from functools import cached_property
+from typing import Optional
+
+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,
+ Register,
+ Signature,
+ Soquet,
+ SoquetT,
+)
+from qualtran.bloqs.basic_gates import CSwap, 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.auto_partition import AutoPartition, Unused
+from qualtran.bloqs.reflections.prepare_identity import PrepareIdentity
+from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare
+from qualtran.bloqs.state_preparation.prepare_uniform_superposition import (
+ PrepareUniformSuperposition,
+)
+from qualtran.resource_counting import BloqCountT, SympySymbolAllocator
+from qualtran.symbolics import is_symbolic, SymbolicFloat, SymbolicInt
+from qualtran.symbolics.math_funcs import bit_length
+
+
+class SqrtEntryOracle(Bloq):
+ r"""Oracle specifying the sqrt of entries of a sparse-access matrix.
+
+ In the reference, this is the interface of
+ $$O_A : \ket{0}\ket{i}\ket{j} \mapsto (\sqrt{A_{ij}} \ket{0} + \sqrt{1 - |A_{ij}|}\ket{i}\ket{j}).$$
+
+ Registers:
+ q: The flag qubit that is rotated proportionally to the value of the entry.
+ i: The row index.
+ j: The column index.
+
+ References:
+ [Lecture Notes on Quantum Algorithms for Scientific Computation](https://arxiv.org/abs/2201.08309). Lin Lin (2022). Ch. 6.5.
+ """
+
+ @property
+ @abc.abstractmethod
+ def system_bitsize(self) -> SymbolicInt:
+ """The number of bits used to represent an index."""
+
+ @property
+ @abc.abstractmethod
+ def epsilon(self) -> SymbolicFloat:
+ """The number of bits used to represent an index."""
+
+ @cached_property
+ def signature(self) -> Signature:
+ return Signature.build_from_dtypes(
+ q=QBit(), i=QAny(self.system_bitsize), j=QAny(self.system_bitsize)
+ )
+
+
+@frozen
+class SparseMatrixHermitian(BlockEncoding):
+ r"""Hermitian Block encoding of a sparse-access Hermitian matrix.
+
+ Given column and entry oracles $O_c$ and $O_A$ for an $s$-sparse Hermitian matrix
+ $A \in \mathbb{C}^{2^n \times 2^n}$, i.e. one where each row / column has exactly $s$ non-zero
+ entries, computes a $(s, n+1, \epsilon)$-block encoding of $A$ as follows:
+ ```
+ ┌────┐
+ a |0> ─┤ ├─ |0> ───────────────────────X────────────────────
+ │ │ ┌──┐ | ┌──┐
+ │ U │ = │ n│ ┌────┐ ┌────┐ | ┌────┐ ┌────┐ │ n│
+ l |0^n> ─┤ A ├─ |0^n> ─┤H ├─┤ O ├─┤ ├─X──|─┤ ├─┤ O* ├─┤H ├─
+ │ │ └──┘ | c | │ │ | | │ │ | c | └──┘
+ │ │ └────┘ │ O │ │ | │ O* │ └────┘
+ b |0> ─┤ ├─ |0> ────────|────┤ A ├─|──X─┤ A ├───|─────────
+ | | ┌────┐ | | | | | ┌────┐
+ | | | O | | | | | | | O* |
+ j |Psi> ─┤ ├─ |Psi> ──────┤ c ├─┤ ├─X────┤ ├─┤ c ├──────
+ └────┘ └────┘ └────┘ └────┘ └────┘
+ ```
+
+ To encode a matrix of irregular dimension, the matrix should first be embedded into one of
+ dimension $2^n \times 2^n$ for suitable $n$.
+ To encode a matrix where each row / column has at most $s$ non-zero entries, some zeroes should
+ be treated as if they were non-zero so that each row / column has exactly $s$ non-zero entries.
+
+ For encoding a non-hermitian matrix, or a slightly more efficient (but non Hermitian-encoding)
+ of a matrix, use :class:`SparseMatrix` instead.
+
+ Args:
+ col_oracle: The column oracle $O_c$. See `RowColumnOracle` for definition.
+ entry_oracle: The entry oracle $O_A$. See `EntryOracle` for definition.
+ eps: The precision of the block encoding.
+
+ Registers:
+ ctrl: The single qubit control register. (present only if `cv` is not `None`)
+ system: The system register.
+ ancilla: The ancilla register.
+ resource: The resource register (present only if `bitsize > 0`).
+
+ References:
+ [Lecture Notes on Quantum Algorithms for Scientific Computation](https://arxiv.org/abs/2201.08309).
+ Lin Lin (2022). Ch. 6.5. Proposition 6.8, Fig 6.7.
+ """
+
+ col_oracle: RowColumnOracle
+ entry_oracle: SqrtEntryOracle
+ eps: SymbolicFloat
+ cv: Optional[int] = None
+
+ def __attrs_post_init__(self):
+ if self.col_oracle.system_bitsize != self.entry_oracle.system_bitsize:
+ raise ValueError("column and entry oracles must have same bitsize")
+
+ @cached_property
+ def signature(self) -> Signature:
+ return Signature.build_from_dtypes(
+ ctrl=QAny(self.ctrl_bitsize),
+ system=QAny(self.system_bitsize),
+ ancilla=QAny(self.ancilla_bitsize),
+ resource=QAny(self.resource_bitsize), # if resource_bitsize is 0, not present
+ )
+
+ @cached_property
+ def system_bitsize(self) -> SymbolicInt:
+ return self.entry_oracle.system_bitsize
+
+ def pretty_name(self) -> str:
+ return "B[SparseMatrixHermitian]"
+
+ @cached_property
+ def alpha(self) -> SymbolicFloat:
+ return self.col_oracle.num_nonzero
+
+ @cached_property
+ def ancilla_bitsize(self) -> SymbolicInt:
+ return self.system_bitsize + 2
+
+ @cached_property
+ def resource_bitsize(self) -> SymbolicInt:
+ return 0
+
+ @cached_property
+ def ctrl_bitsize(self) -> SymbolicInt:
+ return 1 if self.cv is not None else 0
+
+ @cached_property
+ def epsilon(self) -> SymbolicFloat:
+ return self.eps
+
+ @property
+ def signal_state(self) -> BlackBoxPrepare:
+ return BlackBoxPrepare(PrepareIdentity.from_bitsizes([self.ancilla_bitsize]))
+
+ @cached_property
+ def diffusion(self):
+ unused = self.system_bitsize - bit_length(self.col_oracle.num_nonzero - 1)
+ return AutoPartition(
+ PrepareUniformSuperposition(n=self.col_oracle.num_nonzero),
+ partitions=[
+ (Register("target", QAny(self.system_bitsize)), [Unused(unused), "target"])
+ ],
+ )
+
+ def build_call_graph(self, ssa: SympySymbolAllocator) -> set[BloqCountT]:
+ counts = Counter[Bloq]()
+
+ counts[self.diffusion] += 1
+ counts[self.col_oracle] += 1
+ counts[self.entry_oracle] += 1
+ if self.ctrl_bitsize:
+ 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
+
+ return set(counts.items())
+
+ def build_composite_bloq(
+ self, bb: BloqBuilder, system: SoquetT, ancilla: SoquetT, **soqs
+ ) -> 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,
+ regs=(
+ Register('a', QBit()),
+ Register('l', QAny(self.system_bitsize)),
+ Register('b', QBit()),
+ ),
+ )
+
+ 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)
+
+ if ctrl:
+ 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)
+
+ 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)
+
+ ancilla = bb.add(partition_ancilla.adjoint(), a=a, l=l, b=b)
+
+ extra_soqs = {}
+ if ctrl:
+ extra_soqs = {'ctrl': ctrl}
+
+ return {"system": system, "ancilla": ancilla} | extra_soqs
+
+ def adjoint(self) -> 'SparseMatrixHermitian':
+ return self
+
+ def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> tuple['Bloq', 'AddControlledT']:
+ from qualtran.bloqs.mcmt.bloq_with_specialized_single_qubit_control import (
+ get_ctrl_system_for_bloq_with_specialized_single_qubit_control,
+ )
+
+ return get_ctrl_system_for_bloq_with_specialized_single_qubit_control(self, ctrl_spec)
+
+ def with_cv(self, cv: Optional[int]) -> 'SparseMatrixHermitian':
+ return attrs.evolve(self, cv=cv)
+
+
+@frozen
+class UniformSqrtEntryOracle(SqrtEntryOracle):
+ """Oracle specifying the entries of a matrix with uniform entries."""
+
+ system_bitsize: SymbolicInt
+ entry: float
+ eps: float = 1e-11
+
+ @property
+ def epsilon(self) -> SymbolicFloat:
+ return self.eps
+
+ def build_composite_bloq(
+ self, bb: BloqBuilder, q: Soquet, **soqs: SoquetT
+ ) -> dict[str, SoquetT]:
+ # Either Rx or Ry work here; Rx would induce a phase on the subspace with non-zero ancilla
+ # See https://arxiv.org/abs/2302.10949 for reference that uses Rx
+ soqs["q"] = bb.add(Ry(2 * np.arccos(np.sqrt(self.entry))), q=q)
+ return soqs
+
+
+@bloq_example
+def _sparse_matrix_hermitian_block_encoding() -> SparseMatrixHermitian:
+ from qualtran.bloqs.block_encoding.sparse_matrix import TopLeftRowColumnOracle
+ from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import UniformSqrtEntryOracle
+
+ col_oracle = TopLeftRowColumnOracle(system_bitsize=2)
+ entry_oracle = UniformSqrtEntryOracle(system_bitsize=2, entry=0.3)
+ sparse_matrix_hermitian_block_encoding = SparseMatrixHermitian(col_oracle, entry_oracle, eps=0)
+ return sparse_matrix_hermitian_block_encoding
+
+
+@bloq_example
+def _sparse_matrix_symb_hermitian_block_encoding() -> SparseMatrixHermitian:
+ from qualtran.bloqs.block_encoding.sparse_matrix import TopLeftRowColumnOracle
+ from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import UniformSqrtEntryOracle
+
+ n = sympy.Symbol('n', positive=True, integer=True)
+ col_oracle = TopLeftRowColumnOracle(system_bitsize=n)
+ entry_oracle = UniformSqrtEntryOracle(system_bitsize=n, entry=0.3)
+ sparse_matrix_symb_hermitian_block_encoding = SparseMatrixHermitian(
+ col_oracle, entry_oracle, eps=0
+ )
+ return sparse_matrix_symb_hermitian_block_encoding
+
+
+_SPARSE_MATRIX_HERMITIAN_DOC = BloqDocSpec(
+ bloq_cls=SparseMatrixHermitian,
+ examples=[
+ _sparse_matrix_symb_hermitian_block_encoding,
+ _sparse_matrix_hermitian_block_encoding,
+ ],
+)
diff --git a/qualtran/bloqs/block_encoding/sparse_matrix_hermitian_test.py b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian_test.py
new file mode 100644
index 000000000..4164d8178
--- /dev/null
+++ b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian_test.py
@@ -0,0 +1,115 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import cast
+
+import numpy as np
+import sympy
+
+import qualtran.testing as qlt_testing
+from qualtran import BloqBuilder, Soquet
+from qualtran.bloqs.basic_gates import Hadamard, IntEffect, IntState
+from qualtran.bloqs.block_encoding.sparse_matrix import TopLeftRowColumnOracle
+from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import (
+ _sparse_matrix_hermitian_block_encoding,
+ _sparse_matrix_symb_hermitian_block_encoding,
+ SparseMatrixHermitian,
+ UniformSqrtEntryOracle,
+)
+from qualtran.resource_counting.generalizers import ignore_split_join
+
+
+def test_sparse_matrix(bloq_autotester):
+ bloq_autotester(_sparse_matrix_hermitian_block_encoding)
+
+
+def test_sparse_matrix_symb(bloq_autotester):
+ bloq_autotester(_sparse_matrix_symb_hermitian_block_encoding)
+
+
+def test_sparse_matrix_params():
+ bloq = _sparse_matrix_hermitian_block_encoding()
+ assert bloq.system_bitsize == 2
+ assert bloq.alpha == 4
+ assert bloq.epsilon == 0
+ assert bloq.ancilla_bitsize == 2 + 2
+ assert bloq.resource_bitsize == 0
+
+ bloq = _sparse_matrix_symb_hermitian_block_encoding()
+ n = sympy.Symbol('n', positive=True, integer=True)
+ assert bloq.system_bitsize == n
+ assert bloq.alpha == 2**n
+ assert bloq.epsilon == 0
+ assert bloq.ancilla_bitsize == n + 2
+ assert bloq.resource_bitsize == 0
+
+
+def test_call_graph():
+ bloq = _sparse_matrix_hermitian_block_encoding()
+ _, sigma = bloq.call_graph(generalizer=ignore_split_join)
+ assert sigma[Hadamard()] == 4
+
+ bloq = _sparse_matrix_symb_hermitian_block_encoding()
+ _, sigma = bloq.call_graph(generalizer=ignore_split_join)
+ n = sympy.Symbol('n', integer=True, positive=True)
+ assert sigma[Hadamard()] == 6 * n
+
+
+def test_sparse_matrix_tensors():
+ bloq = _sparse_matrix_hermitian_block_encoding()
+ alpha = bloq.alpha
+ bb = BloqBuilder()
+ system = bb.add_register("system", 2)
+ ancilla = cast(Soquet, bb.add(IntState(0, 4)))
+ system, ancilla = bb.add_t(bloq, system=system, ancilla=ancilla)
+ bb.add(IntEffect(0, 4), val=ancilla)
+ bloq = bb.finalize(system=system)
+
+ from_gate = np.full((4, 4), 0.3)
+ from_tensors = bloq.tensor_contract() * alpha
+ np.testing.assert_allclose(from_gate, from_tensors)
+
+
+topleft_matrix = [
+ [0.3, 0.3, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.3, 0.3, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.3, 0.3, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+]
+
+
+def test_top_left_matrix():
+ col_oracle = TopLeftRowColumnOracle(system_bitsize=3, num_nonzero=3)
+ entry_oracle = UniformSqrtEntryOracle(system_bitsize=3, entry=0.3)
+ bloq = SparseMatrixHermitian(col_oracle, entry_oracle, eps=0)
+ alpha = bloq.alpha
+
+ bb = BloqBuilder()
+ system = bb.add_register("system", 3)
+ ancilla = cast(Soquet, bb.add(IntState(0, 3 + 2)))
+ system, ancilla = bb.add_t(bloq, system=system, ancilla=ancilla)
+ bb.add(IntEffect(0, 3 + 2), val=ancilla)
+ bloq = bb.finalize(system=system)
+
+ from_tensors = bloq.tensor_contract() * alpha
+ np.testing.assert_allclose(topleft_matrix, from_tensors, atol=0.003)
+
+
+def test_counts():
+ qlt_testing.assert_equivalent_bloq_counts(
+ _sparse_matrix_hermitian_block_encoding(), generalizer=ignore_split_join
+ )
diff --git a/qualtran/bloqs/max_k_xor_sat/__init__.py b/qualtran/bloqs/max_k_xor_sat/__init__.py
new file mode 100644
index 000000000..e13ed5ffc
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/__init__.py
@@ -0,0 +1,19 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from .guiding_state import GuidingState, SimpleGuidingState
+from .kikuchi_adjacency_list import KikuchiNonZeroIndex
+from .kikuchi_adjacency_matrix import KikuchiMatrixEntry
+from .kikuchi_block_encoding import KikuchiHamiltonian, KikuchiMatrixEntry, KikuchiNonZeroIndex
+from .kxor_instance import Constraint, KXorInstance
+from .planted_noisy_kxor import AliceTheorem, PlantedNoisyKXOR
diff --git a/qualtran/bloqs/max_k_xor_sat/arithmetic/__init__.py b/qualtran/bloqs/max_k_xor_sat/arithmetic/__init__.py
new file mode 100644
index 000000000..7ed84457f
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/arithmetic/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from .has_duplicates import HasDuplicates
+from .sort_in_place import SortInPlace
+from .symmetric_difference import SymmetricDifference
diff --git a/qualtran/bloqs/max_k_xor_sat/arithmetic/arithmetic.ipynb b/qualtran/bloqs/max_k_xor_sat/arithmetic/arithmetic.ipynb
new file mode 100644
index 000000000..f369b025b
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/arithmetic/arithmetic.ipynb
@@ -0,0 +1,266 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "acae7447",
+ "metadata": {
+ "cq.autogen": "title_cell"
+ },
+ "source": [
+ "# Arithmetic Primitives"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ba9f152f",
+ "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": "e9eee46c",
+ "metadata": {
+ "cq.autogen": "SortInPlace.bloq_doc.md"
+ },
+ "source": [
+ "## `SortInPlace`\n",
+ "Sort a list of $\\ell$ numbers in place using $\\ell \\log \\ell$ ancilla bits.\n",
+ "\n",
+ "Lemma 4.12 of Ref [1] gives an algorithm to map\n",
+ "\n",
+ "$$\n",
+ " |x_1, ..., x_l\\rangle|x_{\\pi(1)}, ..., x_{\\pi(l)})\\rangle\n",
+ " \\mapsto\n",
+ " |x_l, ..., x_l\\rangle|\\pi(1), ..., \\pi(l))\\rangle\n",
+ "$$\n",
+ "\n",
+ "where $x_i \\in [n]$ and $\\pi(i) \\in [l]$.\n",
+ "This algorithm has two steps, each with $l^2$ comparisons:\n",
+ "1. compute `pi(1) ... pi(l)` given `x_1 ... x_l` and `x_{pi(1)} ... x{pi(l)}`.\n",
+ "1. (un)compute `x_{pi(1)} ... x{pi(l)}` using `pi(1) ... pi(l)` given `x_1 ... x_l`.\n",
+ "\n",
+ "#### Parameters\n",
+ " - `l`: number of elements in the list\n",
+ " - `dtype`: type of each element to store `[n]`. \n",
+ "\n",
+ "#### Registers\n",
+ " - `input`: the entire input as a single register\n",
+ " - `ancilla`: the generated (entangled) register storing `pi`. \n",
+ "\n",
+ "#### References\n",
+ " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Lemma 4.12. Eq. 122.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d7e360aa",
+ "metadata": {
+ "cq.autogen": "SortInPlace.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.arithmetic import SortInPlace"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "49eb1524",
+ "metadata": {
+ "cq.autogen": "SymmetricDifference.bloq_doc.md"
+ },
+ "source": [
+ "## `SymmetricDifference`\n",
+ "Given two sorted sets $S, T$ of unique elements, compute their symmetric difference.\n",
+ "\n",
+ "This accepts an integer `n_diff`, and marks a flag qubit if the symmetric difference\n",
+ "set is of size exactly `n_diff`. If the flag is marked (1), then the output of `n_diff`\n",
+ "numbers is the symmetric difference, otherwise it may be arbitrary.\n",
+ "\n",
+ "#### Parameters\n",
+ " - `n_lhs`: number of elements in $S$\n",
+ " - `n_rhs`: number of elements in $T$\n",
+ " - `n_diff`: expected number of elements in the difference $S \\Delta T$.\n",
+ " - `bitsize`: number of bits of each element. \n",
+ "\n",
+ "#### Registers\n",
+ " - `S`: list of `n_lhs` numbers.\n",
+ " - `T`: list of `n_rhs` numbers.\n",
+ " - `diff`: output register of `n_diff` numbers.\n",
+ " - `flag`: 1 if there are duplicates, 0 if all are unique. \n",
+ "\n",
+ "#### References\n",
+ " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Theorem 4.17, proof para 3, page 38.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2d90b223",
+ "metadata": {
+ "cq.autogen": "SymmetricDifference.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.arithmetic import SymmetricDifference"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "98c40f78",
+ "metadata": {
+ "cq.autogen": "SymmetricDifference.example_instances.md"
+ },
+ "source": [
+ "### Example Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d0255609",
+ "metadata": {
+ "cq.autogen": "SymmetricDifference.symm_diff"
+ },
+ "outputs": [],
+ "source": [
+ "import sympy\n",
+ "\n",
+ "from qualtran.symbolics import bit_length\n",
+ "\n",
+ "n, k, c = sympy.symbols(\"n k c\", positive=True, integer=True)\n",
+ "symm_diff = SymmetricDifference(n_lhs=c * k, n_rhs=k, n_diff=c * k, bitsize=bit_length(n - 1))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0a66c7fe",
+ "metadata": {
+ "cq.autogen": "SymmetricDifference.symm_diff_equal_size"
+ },
+ "outputs": [],
+ "source": [
+ "import sympy\n",
+ "\n",
+ "from qualtran.symbolics import bit_length\n",
+ "\n",
+ "n, k, c = sympy.symbols(\"n k c\", positive=True, integer=True)\n",
+ "symm_diff_equal_size = SymmetricDifference(\n",
+ " n_lhs=c * k, n_rhs=c * k, n_diff=k, bitsize=bit_length(n - 1)\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ffabfb9e",
+ "metadata": {
+ "cq.autogen": "SymmetricDifference.graphical_signature.md"
+ },
+ "source": [
+ "#### Graphical Signature"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5236e7e1",
+ "metadata": {
+ "cq.autogen": "SymmetricDifference.graphical_signature.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.drawing import show_bloqs\n",
+ "show_bloqs([symm_diff, symm_diff_equal_size],\n",
+ " ['`symm_diff`', '`symm_diff_equal_size`'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9eab4ef5",
+ "metadata": {
+ "cq.autogen": "SymmetricDifference.call_graph.md"
+ },
+ "source": [
+ "### Call Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "01b8dc0c",
+ "metadata": {
+ "cq.autogen": "SymmetricDifference.call_graph.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.resource_counting.generalizers import ignore_split_join\n",
+ "symm_diff_g, symm_diff_sigma = symm_diff.call_graph(max_depth=1, generalizer=ignore_split_join)\n",
+ "show_call_graph(symm_diff_g)\n",
+ "show_counts_sigma(symm_diff_sigma)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3a6ec6d0",
+ "metadata": {
+ "cq.autogen": "HasDuplicates.bloq_doc.md"
+ },
+ "source": [
+ "## `HasDuplicates`\n",
+ "Given a sorted list of `l` numbers, check if it contains any duplicates.\n",
+ "\n",
+ "Produces a single qubit which is `1` if there are duplicates, and `0` if all are disjoint.\n",
+ "It compares every adjacent pair, and therefore uses `l - 1` comparisons.\n",
+ "It then uses a single MCX on `l - 1` bits gate to compute the flag.\n",
+ "\n",
+ "#### Parameters\n",
+ " - `l`: number of elements in the list\n",
+ " - `dtype`: type of each element to store `[n]`. \n",
+ "\n",
+ "#### Registers\n",
+ " - `input`: the entire input as a single register\n",
+ " - `flag`: 1 if there are duplicates, 0 if all are unique. \n",
+ "\n",
+ "#### References\n",
+ " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Lemma 4.12. Eq. 122.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "58923ab1",
+ "metadata": {
+ "cq.autogen": "HasDuplicates.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.arithmetic import HasDuplicates"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/qualtran/bloqs/max_k_xor_sat/arithmetic/has_duplicates.py b/qualtran/bloqs/max_k_xor_sat/arithmetic/has_duplicates.py
new file mode 100644
index 000000000..9e576db23
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/arithmetic/has_duplicates.py
@@ -0,0 +1,69 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from collections import Counter
+
+from attrs import frozen
+
+from qualtran import Bloq, BloqDocSpec, QAny, QBit, QDType, Signature
+from qualtran.bloqs.arithmetic import LessThanEqual
+from qualtran.bloqs.mcmt import MultiControlX
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import HasLength, SymbolicInt
+
+
+@frozen
+class HasDuplicates(Bloq):
+ r"""Given a sorted list of `l` numbers, check if it contains any duplicates.
+
+ Produces a single qubit which is `1` if there are duplicates, and `0` if all are disjoint.
+ It compares every adjacent pair, and therefore uses `l - 1` comparisons.
+ It then uses a single MCX on `l - 1` bits gate to compute the flag.
+
+ Args:
+ l: number of elements in the list
+ dtype: type of each element to store `[n]`.
+
+ Registers:
+ input: the entire input as a single register
+ flag (RIGHT): 1 if there are duplicates, 0 if all are unique.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Lemma 4.12. Eq. 122.
+ """
+
+ l: SymbolicInt
+ dtype: QDType
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature.build_from_dtypes(input=QAny(self.input_bitsize), flag=QBit())
+
+ @property
+ def input_bitsize(self) -> SymbolicInt:
+ return self.l * self.dtype.num_qubits
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
+ logn = self.dtype.num_qubits
+
+ counts = Counter[Bloq]()
+
+ counts[LessThanEqual(logn, logn)] += self.l - 1
+ counts[MultiControlX(cvs=HasLength(self.l - 1))] += 1
+ counts[LessThanEqual(logn, logn).adjoint()] += self.l - 1
+
+ return counts
+
+
+_HAS_DUPLICATES_DOC = BloqDocSpec(bloq_cls=HasDuplicates, examples=[])
diff --git a/qualtran/bloqs/max_k_xor_sat/arithmetic/sort_in_place.py b/qualtran/bloqs/max_k_xor_sat/arithmetic/sort_in_place.py
new file mode 100644
index 000000000..2cb9fb3c4
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/arithmetic/sort_in_place.py
@@ -0,0 +1,95 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from collections import Counter
+
+from attrs import frozen
+
+from qualtran import Bloq, BloqDocSpec, QAny, QDType, Signature
+from qualtran.bloqs.arithmetic import Xor
+from qualtran.bloqs.arithmetic.sorting import Comparator
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import ceil, log2, SymbolicInt
+
+
+@frozen
+class SortInPlace(Bloq):
+ r"""Sort a list of $\ell$ numbers in place using $\ell \log \ell$ ancilla bits.
+
+ Applies the map:
+ $$
+ |x_1, x_2, \ldots, x_l\rangle
+ |0^{\ell \log \ell}\rangle
+ \mapsto
+ |x_{\pi_1}, x_{\pi_2}, \ldots, x_{\pi_\ell})\rangle
+ |\pi_1, \pi_2, \ldots, \pi_\ell\rangle
+ $$
+ where $x_{\pi_1} \le x_{\pi_2} \ldots \le x_{\pi_\ell}$ is the sorted list,
+ and the ancilla are entangled.
+
+ To apply this, we first use any sorting algorithm to output the sorted list
+ in a clean register. And then use the following algorithm from Lemma 4.12 of Ref [1]
+ that applies the map:
+
+ $$
+ |x_1, ..., x_l\rangle|x_{\pi(1)}, ..., x_{\pi(l)})\rangle
+ \mapsto
+ |x_l, ..., x_l\rangle|\pi(1), ..., \pi(l))\rangle
+ $$
+
+ where $x_i \in [n]$ and $\pi(i) \in [l]$.
+ This second algorithm (Lemma 4.12) has two steps, each with $l^2$ comparisons:
+ 1. compute `pi(1) ... pi(l)` given `x_1 ... x_l` and `x_{pi(1)} ... x{pi(l)}`.
+ 1. (un)compute `x_{pi(1)} ... x{pi(l)}` using `pi(1) ... pi(l)` given `x_1 ... x_l`.
+
+ Args:
+ l: number of elements in the list
+ dtype: type of each element to store `[n]`.
+
+ Registers:
+ input: the entire input as a single register
+ ancilla (RIGHT): the generated (entangled) register storing `pi`.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Lemma 4.12. Eq. 122.
+ """
+
+ l: SymbolicInt
+ dtype: QDType
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature.build_from_dtypes(
+ input=QAny(self.l * self.dtype.num_qubits), ancilla=QAny(self.ancilla_bitsize)
+ )
+
+ @property
+ def ancilla_bitsize(self) -> SymbolicInt:
+ """total number of entangled ancilla qubits generated"""
+ return self.l * ceil(log2(self.l))
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
+ compare = Comparator(self.dtype.num_qubits)
+ n_ops = 3 * self.l**2
+
+ counts = Counter[Bloq]()
+
+ counts[compare] += n_ops
+ counts[compare.adjoint()] += n_ops
+ counts[Xor(self.dtype)] += n_ops
+
+ return counts
+
+
+_SORT_IN_PLACE_DOC = BloqDocSpec(bloq_cls=SortInPlace, examples=[])
diff --git a/qualtran/bloqs/max_k_xor_sat/arithmetic/symmetric_difference.py b/qualtran/bloqs/max_k_xor_sat/arithmetic/symmetric_difference.py
new file mode 100644
index 000000000..8f24fb333
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/arithmetic/symmetric_difference.py
@@ -0,0 +1,127 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from collections import Counter
+
+from attrs import frozen
+
+from qualtran import Bloq, bloq_example, BloqDocSpec, QAny, QBit, Signature
+from qualtran.bloqs.arithmetic import Equals, EqualsAConstant, HammingWeightCompute
+from qualtran.bloqs.arithmetic.sorting import BitonicMerge
+from qualtran.bloqs.basic_gates import CNOT
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import bit_length, is_symbolic, SymbolicInt
+
+
+@frozen
+class SymmetricDifference(Bloq):
+ r"""Given two sorted sets $S, T$ of unique elements, compute their symmetric difference.
+
+ This accepts an integer `n_diff`, and marks a flag qubit if the symmetric difference
+ set is of size exactly `n_diff`. If the flag is marked (1), then the output of `n_diff`
+ numbers is the symmetric difference, otherwise it may be arbitrary.
+
+ Args:
+ n_lhs: number of elements in $S$
+ n_rhs: number of elements in $T$
+ n_diff: expected number of elements in the difference $S \Delta T$.
+ bitsize: number of bits of each element.
+
+ Registers:
+ S: list of `n_lhs` numbers.
+ T: list of `n_rhs` numbers.
+ diff: output register of `n_diff` numbers.
+ flag: 1 if there are duplicates, 0 if all are unique.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Theorem 4.17, proof para 3, page 38.
+ """
+
+ n_lhs: SymbolicInt
+ n_rhs: SymbolicInt
+ n_diff: SymbolicInt
+ bitsize: SymbolicInt
+
+ def __attrs_post_init__(self):
+ if not is_symbolic(self.n_lhs, self.n_rhs):
+ assert self.n_lhs >= self.n_rhs, "lhs must be the larger set"
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature.build_from_dtypes(
+ S=QAny(self.n_lhs * self.bitsize),
+ T=QAny(self.n_rhs * self.bitsize),
+ diff=QAny(self.n_diff * self.bitsize),
+ flag=QBit(),
+ )
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
+ # the forward pass, i.e. all bloqs that must be uncomputed
+ counts_forward = Counter[Bloq]()
+
+ # merge the lists
+ counts_forward[BitonicMerge(self.n_lhs, self.bitsize)] += 1
+ # compare adjacents
+ counts_forward[Equals(QAny(self.bitsize))] += self.n_lhs + self.n_rhs - 1
+ # compute number of equal adjacents
+ counts_forward[HammingWeightCompute(self.n_lhs + self.n_rhs - 1)] += 1
+ # check: 2 * n_equal = n_lhs + n_rhs - n_diff
+ # (note: the above eq holds as we assume all input elements are unique)
+ counts_forward[
+ EqualsAConstant(
+ bit_length(self.n_lhs + self.n_rhs - 1),
+ (self.n_lhs + self.n_rhs - self.n_diff) // 2,
+ )
+ ] += 1
+
+ # all bloqs
+ counts = Counter[Bloq]()
+
+ # copy the first n_diff numbers and flag
+ counts[CNOT()] += self.n_diff * self.bitsize + 1
+
+ for bloq, n in counts_forward.items():
+ counts[bloq] += n
+ counts[bloq.adjoint()] += n
+
+ return counts
+
+
+@bloq_example
+def _symm_diff() -> SymmetricDifference:
+ import sympy
+
+ from qualtran.symbolics import bit_length
+
+ n, k, c = sympy.symbols("n k c", positive=True, integer=True)
+ symm_diff = SymmetricDifference(n_lhs=c * k, n_rhs=k, n_diff=c * k, bitsize=bit_length(n - 1))
+ return symm_diff
+
+
+@bloq_example
+def _symm_diff_equal_size() -> SymmetricDifference:
+ import sympy
+
+ from qualtran.symbolics import bit_length
+
+ n, k, c = sympy.symbols("n k c", positive=True, integer=True)
+ symm_diff_equal_size = SymmetricDifference(
+ n_lhs=c * k, n_rhs=c * k, n_diff=k, bitsize=bit_length(n - 1)
+ )
+ return symm_diff_equal_size
+
+
+_SYMMETRIC_DIFFERENCE_DOC = BloqDocSpec(
+ bloq_cls=SymmetricDifference, examples=[_symm_diff, _symm_diff_equal_size]
+)
diff --git a/qualtran/bloqs/max_k_xor_sat/arithmetic/symmetric_difference_test.py b/qualtran/bloqs/max_k_xor_sat/arithmetic/symmetric_difference_test.py
new file mode 100644
index 000000000..76a320f95
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/arithmetic/symmetric_difference_test.py
@@ -0,0 +1,63 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from unittest.mock import ANY
+
+import pytest
+
+from qualtran.resource_counting import big_O, GateCounts, get_cost_value, QECGatesCost
+from qualtran.symbolics import ceil, log2
+
+from .symmetric_difference import _symm_diff, _symm_diff_equal_size
+
+
+@pytest.mark.parametrize("bloq_ex", [_symm_diff, _symm_diff_equal_size])
+def test_examples(bloq_autotester, bloq_ex):
+ if bloq_autotester.check_name == 'serialize':
+ pytest.skip()
+
+ bloq_autotester(bloq_ex)
+
+
+@pytest.mark.parametrize("bloq_ex", [_symm_diff, _symm_diff_equal_size])
+def test_cost(bloq_ex):
+ bloq = bloq_ex()
+ gc = get_cost_value(bloq, QECGatesCost())
+
+ l, r = bloq.n_lhs, bloq.n_rhs # assumption l >= r
+ logn = bloq.bitsize
+ logl = ceil(log2(l))
+ assert gc == GateCounts(
+ cswap=2 * l * logn * (logl + 1),
+ and_bloq=(
+ 2 * l * (2 * logn + 1) * (logl + 1)
+ + l
+ + r
+ + 2 * ((logn - 1) * (l + r - 1))
+ + 2 * ceil(log2(l + r))
+ - 4
+ ),
+ clifford=ANY,
+ measurement=ANY,
+ )
+
+ # \tilde{O}(l log n)
+ # Page 38, Thm 4.17, proof para 3, 3rd last line.
+ assert gc.total_t_count() in big_O(l * logn * logl**2)
+
+
+@pytest.mark.notebook
+def test_notebook():
+ from qualtran.testing import execute_notebook
+
+ execute_notebook('arithmetic')
diff --git a/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/__init__.py b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/__init__.py
new file mode 100644
index 000000000..09dfda120
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from .guided_hamiltonian import GuidedHamiltonian, GuidedHamiltonianPhaseEstimation
+from .walk_operator import QubitizedWalkOperator
diff --git a/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/guided_hamiltonian.ipynb b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/guided_hamiltonian.ipynb
new file mode 100644
index 000000000..a32bcd317
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/guided_hamiltonian.ipynb
@@ -0,0 +1,311 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "e299e7a9",
+ "metadata": {
+ "cq.autogen": "title_cell"
+ },
+ "source": [
+ "# Guided (sparse) Hamiltonian Problem\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": "e2be674c",
+ "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": "8d10248d",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonian.bloq_doc.md"
+ },
+ "source": [
+ "## `GuidedHamiltonian`\n",
+ "Solve the guided (sparse) hamiltonian problem.\n",
+ "\n",
+ "Definition 4.8 (modified with sparsity generalized to any):\n",
+ "In the Guided Hamiltonian problem we are given the following as input:\n",
+ "\n",
+ "1. A Hamiltonian $H$ with $\\|H\\|_\\max \\le 1$, specified via a block-encoding.\n",
+ "2. A unitary program that prepares $|\\Psi\\rangle|0^A\\rangle$.\n",
+ "3. Parameters $\\lambda \\in [-\\Lambda, \\Lambda]$, $\\alpha \\in (0, 1)$, $\\gamma \\in (0, 1]$.\n",
+ "\n",
+ "and we should output\n",
+ "\n",
+ "- YES (1) if $\\| \\Pi_{\\ge \\lambda} (H) |\\Psi\\rangle \\| \\ge \\gamma$\n",
+ "- NO (0) if $\\|H\\| \\le (1 - \\alpha) \\lambda$\n",
+ "\n",
+ "Note that the above drops the sparse requirement, and accepts any\n",
+ "$(\\alpha_H, \\cdot, \\cdot)$-block-encoding of $H$.\n",
+ "In the sparse Hamiltonian case, $\\alpha_H = s$ (where $s$ is the sparsity).\n",
+ "\n",
+ "Algorithm (Theorem 4.9):\n",
+ " This uses phase estimation on the block-encoding of $e^{iHt}$, and then uses\n",
+ " amplitude amplification to increase the success probability to $1 - o(1)$.\n",
+ "\n",
+ "We instead directly do phase-estimation on the qubitized (Szegedy) walk operator for $H$\n",
+ "\n",
+ "#### Parameters\n",
+ " - `hamiltonian`: the block-encoding of $H$\n",
+ " - `guiding_state`: the unitary that prepares $|\\Psi\\rangle$\n",
+ " - `lambd`: parameter $\\lambda$\n",
+ " - `alpha`: parameter $\\alpha$\n",
+ " - `gamma`: parameter $\\gamma$ \n",
+ "\n",
+ "#### References\n",
+ " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Section 4.2 \"Guided Sparse Hamiltonian Problem\".\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "29d536c3",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonian.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.guided_hamiltonian import GuidedHamiltonian"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a216ac70",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonian.example_instances.md"
+ },
+ "source": [
+ "### Example Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fabf68e4",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonian.guided_hamiltonian"
+ },
+ "outputs": [],
+ "source": [
+ "import sympy\n",
+ "\n",
+ "from qualtran.bloqs.max_k_xor_sat import GuidingState, KikuchiHamiltonian, KXorInstance\n",
+ "from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare\n",
+ "from qualtran.symbolics import ceil, log2\n",
+ "\n",
+ "n, k, m, c = sympy.symbols(\"n k m c\", positive=True, integer=True)\n",
+ "zeta = sympy.symbols(r\"\\zeta\", positive=True)\n",
+ "\n",
+ "inst_guide = KXorInstance.symbolic(n, (1 - zeta) * m, k, max_rhs=2)\n",
+ "inst_solve = KXorInstance.symbolic(n, zeta * m, k, max_rhs=2)\n",
+ "l = c * k\n",
+ "s = l * ceil(log2(n))\n",
+ "\n",
+ "Psi = GuidingState(inst_guide, l)\n",
+ "H = KikuchiHamiltonian(inst_solve, c * k, s)\n",
+ "\n",
+ "lambd, alpha, gamma = sympy.symbols(r\"\\lambda \\alpha \\gamma\", positive=True, real=True)\n",
+ "guided_hamiltonian = GuidedHamiltonian(H, BlackBoxPrepare(Psi), lambd, alpha, gamma)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7ad9d994",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonian.graphical_signature.md"
+ },
+ "source": [
+ "#### Graphical Signature"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5ae17ad5",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonian.graphical_signature.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.drawing import show_bloqs\n",
+ "show_bloqs([guided_hamiltonian],\n",
+ " ['`guided_hamiltonian`'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1b3e1663",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonian.call_graph.md"
+ },
+ "source": [
+ "### Call Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "08946e48",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonian.call_graph.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.resource_counting.generalizers import ignore_split_join\n",
+ "guided_hamiltonian_g, guided_hamiltonian_sigma = guided_hamiltonian.call_graph(max_depth=1, generalizer=ignore_split_join)\n",
+ "show_call_graph(guided_hamiltonian_g)\n",
+ "show_counts_sigma(guided_hamiltonian_sigma)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0b22b193",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonianPhaseEstimation.bloq_doc.md"
+ },
+ "source": [
+ "## `GuidedHamiltonianPhaseEstimation`\n",
+ "Implement the phase estimation algorithm $U_\\text{PE}$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b3806eb0",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonianPhaseEstimation.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.guided_hamiltonian import GuidedHamiltonianPhaseEstimation"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "500f891d",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonianPhaseEstimation.example_instances.md"
+ },
+ "source": [
+ "### Example Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "bfdbf9a6",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonianPhaseEstimation.guided_phase_estimate_symb"
+ },
+ "outputs": [],
+ "source": [
+ "import sympy\n",
+ "\n",
+ "from qualtran.bloqs.max_k_xor_sat import GuidingState, KikuchiHamiltonian, KXorInstance\n",
+ "from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare\n",
+ "from qualtran.symbolics import ceil, log2\n",
+ "\n",
+ "n, k, c = sympy.symbols(\"n k c\", positive=True, integer=True)\n",
+ "m_guide, m_solve = sympy.symbols(\"m_1 m_2\", positive=True, integer=True)\n",
+ "\n",
+ "inst_guide = KXorInstance.symbolic(n, m_guide, k, max_rhs=2)\n",
+ "inst_solve = KXorInstance.symbolic(n, m_solve, k, max_rhs=2)\n",
+ "l = c * k\n",
+ "s = l * ceil(log2(n))\n",
+ "\n",
+ "Psi = GuidingState(inst_guide, l)\n",
+ "H = KikuchiHamiltonian(inst_solve, c * k, s)\n",
+ "\n",
+ "eps, delta = sympy.symbols(r\"\\epsilon_{PE} \\delta_{PE}\", positive=True, real=True)\n",
+ "guided_phase_estimate_symb = GuidedHamiltonianPhaseEstimation(\n",
+ " H, BlackBoxPrepare(Psi), eps, delta\n",
+ ")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d6408318",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonianPhaseEstimation.graphical_signature.md"
+ },
+ "source": [
+ "#### Graphical Signature"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3a1d74b2",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonianPhaseEstimation.graphical_signature.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.drawing import show_bloqs\n",
+ "show_bloqs([guided_phase_estimate_symb],\n",
+ " ['`guided_phase_estimate_symb`'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a5d710cd",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonianPhaseEstimation.call_graph.md"
+ },
+ "source": [
+ "### Call Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "749b6440",
+ "metadata": {
+ "cq.autogen": "GuidedHamiltonianPhaseEstimation.call_graph.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.resource_counting.generalizers import ignore_split_join\n",
+ "guided_phase_estimate_symb_g, guided_phase_estimate_symb_sigma = guided_phase_estimate_symb.call_graph(max_depth=1, generalizer=ignore_split_join)\n",
+ "show_call_graph(guided_phase_estimate_symb_g)\n",
+ "show_counts_sigma(guided_phase_estimate_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/max_k_xor_sat/guided_hamiltonian/guided_hamiltonian.py b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/guided_hamiltonian.py
new file mode 100644
index 000000000..1c9553b68
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/guided_hamiltonian.py
@@ -0,0 +1,348 @@
+# 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.
+"""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 collections import Counter
+from functools import cached_property
+
+from attrs import frozen
+
+from qualtran import (
+ Bloq,
+ bloq_example,
+ BloqBuilder,
+ BloqDocSpec,
+ DecomposeTypeError,
+ QAny,
+ Register,
+ Signature,
+ Soquet,
+ SoquetT,
+)
+from qualtran.bloqs.basic_gates import ZGate
+from qualtran.bloqs.block_encoding import BlockEncoding
+from qualtran.bloqs.mcmt import MultiControlZ
+from qualtran.bloqs.phase_estimation import KaiserWindowState, QubitizationQPE
+from qualtran.bloqs.phase_estimation.qpe_window_state import QPEWindowStateBase
+from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import (
+ ceil,
+ HasLength,
+ is_symbolic,
+ is_zero,
+ ln,
+ log2,
+ pi,
+ SymbolicFloat,
+ SymbolicInt,
+)
+
+from ..shims import ReflectAboutZero
+from .walk_operator import QubitizedWalkOperator
+
+
+@frozen
+class GuidedHamiltonianPhaseEstimation(Bloq):
+ r"""Implement the phase estimation algorithm $U_\text{PE}$"""
+
+ hamiltonian: BlockEncoding
+ guiding_state: BlackBoxPrepare
+ precision: SymbolicFloat
+ fail_prob: SymbolicFloat
+
+ def __attrs_post_init__(self):
+ assert (
+ self.hamiltonian.resource_bitsize == 0
+ ), "block encoding with resource regs not supported"
+
+ assert self.hamiltonian.system_bitsize == self.guiding_state.selection_bitsize
+
+ @cached_property
+ def signature(self) -> 'Signature':
+ return Signature.build_from_dtypes(
+ phase_estimate=self.qpe_window_state.m_register.dtype,
+ system=QAny(self.hamiltonian.system_bitsize),
+ walk_ancilla=QAny(self.hamiltonian.ancilla_bitsize),
+ guide_ancilla=QAny(self.guiding_state.junk_bitsize),
+ )
+
+ @cached_property
+ def walk_operator(self) -> QubitizedWalkOperator:
+ return QubitizedWalkOperator(self.hamiltonian)
+
+ @cached_property
+ def qpe_window_state(self) -> QPEWindowStateBase:
+ """Kaiser Window state.
+ Computes a slightly larger value for a simpler expression.
+ https://arxiv.org/abs/2209.13581, Eq D14, D15
+ """
+ eps, delta = self.precision, self.fail_prob
+
+ alpha = ln(1 / delta) / pi(delta)
+
+ N = (1 / eps) * ln(1 / delta)
+ m_bits = ceil(log2(N))
+ return KaiserWindowState(bitsize=m_bits, alpha=alpha)
+
+ @cached_property
+ def qpe_bloq(self) -> QubitizationQPE:
+ return QubitizationQPE(self.walk_operator, self.qpe_window_state) # type: ignore
+
+ def build_composite_bloq(
+ self,
+ bb: 'BloqBuilder',
+ phase_estimate: Soquet,
+ system: Soquet,
+ walk_ancilla: Soquet,
+ **soqs: SoquetT,
+ ) -> dict[str, 'SoquetT']:
+
+ # prepare the guiding state
+ if is_zero(self.guiding_state.junk_bitsize):
+ system = bb.add(self.guiding_state, selection=system)
+ else:
+ system, guide_ancilla = bb.add(
+ self.guiding_state, selection=system, junk=soqs.pop('guide_ancilla')
+ )
+ soqs['guide_ancilla'] = guide_ancilla
+
+ # apply QPE
+ phase_estimate, system, walk_ancilla = bb.add(
+ self.qpe_bloq, qpe_reg=phase_estimate, system=system, ancilla=walk_ancilla
+ )
+
+ return {
+ 'phase_estimate': phase_estimate,
+ 'system': system,
+ 'walk_ancilla': walk_ancilla,
+ } | soqs
+
+
+@bloq_example
+def _guided_phase_estimate_symb() -> GuidedHamiltonianPhaseEstimation:
+ import sympy
+
+ from qualtran.bloqs.max_k_xor_sat import GuidingState, KikuchiHamiltonian, KXorInstance
+ from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare
+ from qualtran.symbolics import ceil, log2
+
+ n, k, c = sympy.symbols("n k c", positive=True, integer=True)
+ m_guide, m_solve = sympy.symbols("m_1 m_2", positive=True, integer=True)
+
+ inst_guide = KXorInstance.symbolic(n, m_guide, k, max_rhs=2)
+ inst_solve = KXorInstance.symbolic(n, m_solve, k, max_rhs=2)
+ l = c * k
+ s = l * ceil(log2(n))
+
+ Psi = GuidingState(inst_guide, l)
+ H = KikuchiHamiltonian(inst_solve, c * k, s)
+
+ eps, delta = sympy.symbols(r"\epsilon_{PE} \delta_{PE}", positive=True, real=True)
+ guided_phase_estimate_symb = GuidedHamiltonianPhaseEstimation(
+ H, BlackBoxPrepare(Psi), eps, delta
+ )
+
+ return guided_phase_estimate_symb
+
+
+_GUIDED_HAMILTONIAN_PHASE_ESTIMATION_DOC = BloqDocSpec(
+ bloq_cls=GuidedHamiltonianPhaseEstimation, examples=[_guided_phase_estimate_symb]
+)
+
+
+@frozen
+class GuidedHamiltonian(Bloq):
+ r"""Solve the guided (sparse) hamiltonian problem.
+
+ Definition 4.8 (modified to accept any block-encoding):
+ In the Guided Hamiltonian problem we are given the following as input:
+
+ 1. A $(\sqrt{2} s, \cdot, 0)$-block-encoding of a Hamiltonian $H$ such that $\|H\|_\max \le s$.
+ 2. A unitary program that prepares $|\Psi\rangle|0^A\rangle$.
+ 3. Parameters $\lambda \in [-\Lambda, \Lambda]$, $\alpha \in (0, 1)$, $\gamma \in (0, 1]$.
+
+ and we should output
+
+ - YES (1) if $\| \Pi_{\ge \lambda} (H) |\Psi\rangle \| \ge \gamma$
+ - NO (0) if $\|H\| \le (1 - \alpha) \lambda$
+
+ Note that the above drops the sparse requirement, and accepts any
+ $(\alpha_H, \cdot, \cdot)$-block-encoding of $H$.
+ In the sparse Hamiltonian case, $\alpha_H = s$ (where $s$ is the sparsity).
+
+ Algorithm (Theorem 4.9):
+ This uses phase estimation on the block-encoding of $e^{iHt}$, and then uses
+ amplitude amplification to increase the success probability to $1 - o(1)$.
+
+ We instead directly do phase-estimation on the qubitized (Szegedy) walk operator for $H$
+
+ Args:
+ hamiltonian: the block-encoding of $H$
+ guiding_state: the unitary that prepares $|\Psi\rangle$
+ lambd: parameter $\lambda$
+ alpha: parameter $\alpha$
+ gamma: parameter $\gamma$
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Section 4.2 "Guided Sparse Hamiltonian Problem".
+ """
+
+ hamiltonian: BlockEncoding
+ guiding_state: BlackBoxPrepare
+ lambd: SymbolicFloat
+ alpha: SymbolicFloat
+ gamma: SymbolicFloat
+
+ def __attrs_post_init__(self):
+ assert self.hamiltonian.resource_bitsize == 0, "resource not supported"
+ assert (
+ self.hamiltonian.system_bitsize == self.guiding_state.selection_bitsize
+ ), "system registers must match"
+
+ @property
+ def signature(self) -> 'Signature':
+ return self.qpe_bloq.signature
+
+ @cached_property
+ def qpe_precision(self) -> SymbolicFloat:
+ r"""The precision for phase estimation.
+
+ Page 31, Eq 100 of the reference gives the precision value for estimating phases
+ of $e^{iHt}$ with $t = \pi/(2s)$. But this bloq does phase estimation directly
+ on the walk operator, with eigenphases $e^{-i \arccos(H/s)}$.
+
+ To bound this, consider the two eigenvalues that are to be distinguished:
+ $\lambda$ and $(1 - \alpha)\lambda$. We can bound the difference in estimated phases as
+
+ $$
+ |\arccos(\lambda / s) - \arccos((1-\alpha)\lambda / s)|
+ \le \frac{\alpha \lambda}{s} \frac{1}{1 - ((1 - \alpha)\lambda / s)^2}
+ $$
+
+ As we know $\|H\| \le s/\sqrt{2}$, it means $\lambda/s \le 1/\sqrt{2}$,
+ therefore the second term is at most $\sqrt{2}$.
+
+ In the sparse encoding case, we can increase the sparsity to $\sqrt{2} s$
+ when block-encoding the input, to ensure that we have an epsilon bound of
+ $\alpha \lambda / s$.
+ """
+ return self.alpha * self.lambd / self.hamiltonian.alpha
+
+ @cached_property
+ def qpe_fail_prob(self) -> SymbolicFloat:
+ """Page 31, above Eq 104."""
+ return self.gamma**3
+
+ @cached_property
+ def n_rounds_amplification(self) -> SymbolicInt:
+ return ceil(1 / self.gamma)
+
+ @cached_property
+ def qpe_bloq(self) -> GuidedHamiltonianPhaseEstimation:
+ return GuidedHamiltonianPhaseEstimation(
+ hamiltonian=self.hamiltonian,
+ guiding_state=self.guiding_state,
+ precision=self.qpe_precision,
+ fail_prob=self.qpe_fail_prob,
+ )
+
+ def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> dict[str, 'SoquetT']:
+ if is_symbolic(self.n_rounds_amplification):
+ raise DecomposeTypeError(
+ f'cannot decompose {self} with symbolic number of rounds {self.n_rounds_amplification}'
+ )
+
+ soqs = bb.add_d(self.qpe_bloq, **soqs)
+ for _ in range(self.n_rounds_amplification):
+ # reflect about bad state
+ soqs['guide_ancilla'] = bb.add(
+ ReflectAboutZero(
+ (Register('guide_ancilla', QAny(self.guiding_state.junk_bitsize)),),
+ global_phase=-1,
+ ),
+ guide_ancilla=soqs['guide_ancilla'],
+ )
+
+ # reflect about prepared state
+ soqs = bb.add_d(self.qpe_bloq.adjoint(), **soqs)
+ soqs = bb.add_d(ReflectAboutZero(tuple(self.signature), global_phase=-1), **soqs)
+ soqs = bb.add_d(self.qpe_bloq, **soqs)
+
+ return soqs
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
+ counts = Counter[Bloq]()
+
+ # prepare the initial state
+ counts[self.qpe_bloq] += 1
+
+ # reflect about the prepared state
+ counts[self.qpe_bloq.adjoint()] += self.n_rounds_amplification
+ counts[
+ MultiControlZ(HasLength(self.qpe_bloq.signature.n_qubits()))
+ ] += self.n_rounds_amplification
+ counts[self.qpe_bloq] += self.n_rounds_amplification
+
+ # reflect about the flag qubit
+ counts[ZGate()] += self.n_rounds_amplification
+
+ return counts
+
+
+@bloq_example
+def _guided_hamiltonian() -> GuidedHamiltonian:
+ import sympy
+
+ from qualtran.bloqs.max_k_xor_sat import GuidingState, KikuchiHamiltonian, KXorInstance
+ from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare
+ from qualtran.symbolics import ceil, log2
+
+ n, k, m, c = sympy.symbols("n k m c", positive=True, integer=True)
+ zeta = sympy.symbols(r"\zeta", positive=True)
+
+ inst_guide = KXorInstance.symbolic(n, (1 - zeta) * m, k, max_rhs=2)
+ inst_solve = KXorInstance.symbolic(n, zeta * m, k, max_rhs=2)
+ l = c * k
+ s = l * ceil(log2(n))
+
+ Psi = GuidingState(inst_guide, l)
+ H = KikuchiHamiltonian(inst_solve, c * k, s)
+
+ lambd, alpha, gamma = sympy.symbols(r"\lambda \alpha \gamma", positive=True, real=True)
+ guided_hamiltonian = GuidedHamiltonian(H, BlackBoxPrepare(Psi), lambd, alpha, gamma)
+ return guided_hamiltonian
+
+
+_GUIDED_HAMILTONIAN_DOC = BloqDocSpec(bloq_cls=GuidedHamiltonian, examples=[_guided_hamiltonian])
diff --git a/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/guided_hamiltonian_test.py b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/guided_hamiltonian_test.py
new file mode 100644
index 000000000..1ab6c5465
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/guided_hamiltonian_test.py
@@ -0,0 +1,31 @@
+# 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
+
+from .guided_hamiltonian import _guided_hamiltonian, _guided_phase_estimate_symb
+
+
+@pytest.mark.parametrize("bloq_ex", [_guided_hamiltonian, _guided_phase_estimate_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():
+ from qualtran.testing import execute_notebook
+
+ execute_notebook('guided_hamiltonian')
diff --git a/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/tutorial_guided_hamiltonian.ipynb b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/tutorial_guided_hamiltonian.ipynb
new file mode 100644
index 000000000..fe31ae38e
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/tutorial_guided_hamiltonian.ipynb
@@ -0,0 +1,2005 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "6d27900d7109a493",
+ "metadata": {},
+ "source": [
+ "# Guided (sparse) Hamiltonian Problem"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "ec7403af5f8f9302",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-27T18:07:47.146430Z",
+ "start_time": "2024-08-27T18:07:44.824865Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from attrs import frozen\n",
+ "import attrs\n",
+ "from qualtran import Bloq, Signature, Register, QAny\n",
+ "from qualtran.drawing import show_bloq, show_call_graph, show_counts_sigma\n",
+ "from qualtran.symbolics import SymbolicInt, ceil, log2, ln, is_symbolic\n",
+ "from qualtran.resource_counting import big_O\n",
+ "from qualtran.resource_counting.generalizers import ignore_alloc_free, ignore_split_join, ignore_cliffords\n",
+ "from qualtran.bloqs.block_encoding import BlockEncoding\n",
+ "from qualtran.bloqs.reflections.reflection_using_prepare import ReflectionUsingPrepare\n",
+ "from qualtran.bloqs.max_k_xor_sat.guided_hamiltonian import GuidedHamiltonian\n",
+ "from qualtran.bloqs.max_k_xor_sat.shims import ArbitraryGate # arbitrary 1/2-qubit gate, for costing."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fd4d302a-8cfe-48b5-8e44-a9b99a0b5e5b",
+ "metadata": {},
+ "source": [
+ "## `GuidedHamiltonian`\n",
+ "Solve the guided (sparse) hamiltonian problem.\n",
+ "\n",
+ "Definition 4.8 (modified with sparsity generalized to any):\n",
+ "In the Guided Hamiltonian problem we are given the following as input:\n",
+ "\n",
+ "1. A Hamiltonian $H$ with $\\|H\\|_\\max \\le 1$, specified via a block-encoding.\n",
+ "2. A unitary program that takes $|0^N\\rangle|0^A\\rangle$ and prepares $|\\Psi\\rangle|0^A\\rangle$.\n",
+ "3. Parameters $\\lambda \\in [-\\Lambda, \\Lambda]$, $\\alpha \\in (0, 1)$, $\\gamma \\in (0, 1]$.\n",
+ "\n",
+ "and we should output\n",
+ "\n",
+ "- YES if $\\| \\Pi_{\\ge \\lambda} (H) |\\Psi\\rangle \\| \\ge \\gamma$\n",
+ "- NO if $\\|H\\| \\le (1 - \\alpha) \\lambda$\n",
+ "\n",
+ "Note that the above drops the sparse requirement, and accepts any\n",
+ "$(\\alpha_H, \\cdot, \\cdot)$-block-encoding of $H$.\n",
+ "In the sparse Hamiltonian case, $\\alpha_H = s$ (where $s$ is the sparsity).\n",
+ "\n",
+ "Algorithm (Theorem 4.9):\n",
+ " This uses phase estimation on the block-encoding of $e^{iHt}$, and then uses\n",
+ " amplitude amplification to increase the success probability to $1 - o(1)$.\n",
+ "\n",
+ "We instead directly do phase-estimation on the qubitized (Szegedy) walk operator for $H$.\n",
+ "\n",
+ "#### Parameters\n",
+ " - `hamiltonian`: the block-encoding of $H$\n",
+ " - `guiding_state`: the unitary that prepares $|\\Psi\\rangle$\n",
+ " - `lambd`: parameter $\\lambda$\n",
+ " - `alpha`: parameter $\\alpha$\n",
+ " - `gamma`: parameter $\\gamma$ "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "06cdbc3b-b4bd-47da-84ba-cd7b0059bb6f",
+ "metadata": {},
+ "source": [
+ "# Computing Query Costs with Qualtran\n",
+ "\n",
+ "We will first create black-boxes for a Hamiltonian and a guiding state preparation, that can be passed to the `GuidedHamiltonian` bloq to count number of queries and gates."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fb7db14a-d5d1-4f02-b082-43ada6fa1aee",
+ "metadata": {},
+ "source": [
+ "## Building black-box oracles"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b50f74cc-e8dc-4214-a020-af0e80ef5469",
+ "metadata": {},
+ "source": [
+ "### Graph oracles for the Hamiltonian\n",
+ "We first build the oracles $O_F$ and $O_H$, and use them to block-encode a sparse matrix."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "4a490bf8-e83f-497a-a422-2e1eb2444e70",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "@frozen\n",
+ "class oracle_O_H(Bloq):\n",
+ " \"\"\"given (i, j), output H_{i, j}\"\"\"\n",
+ " N: SymbolicInt\n",
+ " entry_bits: SymbolicInt\n",
+ " \n",
+ " @property\n",
+ " def signature(self):\n",
+ " return Signature.build(i=self.N, j=self.N, entry=self.entry_bits)\n",
+ "\n",
+ " def adjoint(self):\n",
+ " return self\n",
+ "\n",
+ "@frozen\n",
+ "class oracle_O_F(Bloq):\n",
+ " \"\"\"Given (i, k), output (i, f(i, k)) s.t. f(i, k) is the k-th non zero entry in row i\"\"\"\n",
+ " N: SymbolicInt\n",
+ " reverse: bool = False\n",
+ " \n",
+ " @property\n",
+ " def signature(self):\n",
+ " return Signature.build(i=self.N, k=self.N)\n",
+ "\n",
+ " def adjoint(self):\n",
+ " return oracle_O_F(self.N, reverse=not self.reverse)\n",
+ "\n",
+ "\n",
+ "@frozen\n",
+ "class EncodeSparseHamiltonian(BlockEncoding):\n",
+ " \"\"\"(s, N+1, 0)-block-encoding of s-sparse NxN matrix H\"\"\"\n",
+ " N: SymbolicInt\n",
+ " s: SymbolicInt # sparsity\n",
+ " O_F: oracle_O_F\n",
+ " O_H: oracle_O_H\n",
+ "\n",
+ " @property\n",
+ " def signature(self) -> Signature:\n",
+ " return Signature.build_from_dtypes(\n",
+ " system=QAny(self.system_bitsize),\n",
+ " ancilla=QAny(self.ancilla_bitsize),\n",
+ " )\n",
+ "\n",
+ " @property\n",
+ " def system_bitsize(self):\n",
+ " return self.N\n",
+ "\n",
+ " @property\n",
+ " def ancilla_bitsize(self):\n",
+ " return self.N + 1\n",
+ "\n",
+ " @property\n",
+ " def resource_bitsize(self):\n",
+ " return 0\n",
+ "\n",
+ " @property\n",
+ " def alpha(self):\n",
+ " return self.s\n",
+ "\n",
+ " @property\n",
+ " def epsilon(self):\n",
+ " return 0\n",
+ "\n",
+ " @property\n",
+ " def signal_state(self):\n",
+ " from qualtran.bloqs.reflections.prepare_identity import PrepareIdentity\n",
+ " from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare\n",
+ " \n",
+ " return BlackBoxPrepare(PrepareIdentity.from_bitsizes([self.ancilla_bitsize]))\n",
+ "\n",
+ " def build_call_graph(self, ssa):\n",
+ " \"\"\"\n",
+ " References:\n",
+ " [Lecture Notes on Quantum Algorithms for Scientific Computation](https://arxiv.org/abs/2201.08309). Lin Lin (2022). Ch. 6.5.\n",
+ " \"\"\"\n",
+ " log_s = ceil(log2(self.s))\n",
+ " return {(self.O_F, 2), (self.O_H, 2), (ArbitraryGate(), 2*log_s)}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "60df1af0-0a6f-4645-8739-b75f4ece3553",
+ "metadata": {},
+ "source": [
+ "### State-preparation Oracle for the guiding state\n",
+ "\n",
+ "Point 2. Quantum circuit that uses $G$ gates and maps $|0^N\\rangle|0^A\\rangle$ to $|\\Psi\\rangle|0^A\\rangle$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "0a17eab5-0a44-4526-9b08-483086c655bf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "@frozen\n",
+ "class GuidingState(Bloq):\n",
+ " \"\"\"Point 2. Quantum circuit that uses G gates and maps |0^N>|0^A> to |\\Psi>|0^A>\"\"\"\n",
+ " N: SymbolicInt\n",
+ " A: SymbolicInt\n",
+ " G: SymbolicInt\n",
+ " \n",
+ " @property\n",
+ " def signature(self) -> Bloq:\n",
+ " return Signature.build(selection=self.N, junk=self.A)\n",
+ "\n",
+ " def build_call_graph(self, ssa):\n",
+ " return {(ArbitraryGate(), self.G)}\n",
+ " \n",
+ " @property\n",
+ " def selection_bitsize(self):\n",
+ " return self.N\n",
+ " @property\n",
+ " def junk_bitsize(self):\n",
+ " return self.A\n",
+ " @property\n",
+ " def selection_registers(self):\n",
+ " return (Register('selection', QAny(self.N)),)\n",
+ " @property\n",
+ " def junk_registers(self):\n",
+ " return (Register('junk', QAny(self.A)),)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f127675c-6616-43eb-b33a-479ae146dcfa",
+ "metadata": {},
+ "source": [
+ "## An example invocation\n",
+ "With these oracles in place, we can now invoke the `GuidedHamiltonian` algorithm."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "32219420-f41b-4c80-b89b-2076795d596f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sympy\n",
+ "\n",
+ "def example_bloq() -> GuidedHamiltonian:\n",
+ " N, A, G, s = sympy.symbols(\"N A G s\", positive=True, integer=True)\n",
+ " lambd, alpha, gamma = sympy.symbols(r\"\\lambda \\alpha \\gamma\", positive=True, real=True)\n",
+ "\n",
+ " O_F = oracle_O_F(N)\n",
+ " O_H = oracle_O_H(N, 10)\n",
+ " be_H = EncodeSparseHamiltonian(N, s, O_F, O_H)\n",
+ " psi = GuidingState(N, A, G)\n",
+ "\n",
+ " return GuidedHamiltonian(be_H, psi, lambd, alpha, gamma)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "13b8eb29-1af9-4d65-a954-cf96498efad1",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "bloq = example_bloq()\n",
+ "show_bloq(bloq)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ac277b8a-8ae9-47cc-bd46-7f23c56d1392",
+ "metadata": {},
+ "source": [
+ "## Circuit Diagrams\n",
+ "\n",
+ "We will now look at the decomposition of the bloq, to see the steps of the algorithm.\n",
+ "First we look at the phase estimation bloq $U_\\text{PE}$.\n",
+ "To obtain the entire algorithm, we use amplitude-amplification on $U_\\text{PE}$ for $O(1/\\gamma)$ rounds.\n",
+ "The good subspace is characterized by `guide_ancilla` being all 0."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "9d2c34d3-9a19-4d54-a60a-549c590533e7",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "show_bloq(bloq.qpe_bloq.decompose_bloq())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "08002b28-5d5f-4283-add6-d4fd914a0112",
+ "metadata": {},
+ "source": [
+ "### Full Circuit\n",
+ "For exposition, let us pick the number of rounds as a constant (say, $3$) to see the decomposition:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "46e5a6af-4270-49fd-af5a-ae066503e378",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "bloq_4_rounds = attrs.evolve(bloq, gamma=1/3)\n",
+ "show_bloq(bloq_4_rounds.decompose_bloq())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "214f731f-22d8-4767-8b23-13ad1c400abf",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "show_bloq(bloq_4_rounds.decompose_bloq().flatten_once())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f131e5ff-4d5b-487b-a84c-56c68a411f71",
+ "metadata": {},
+ "source": [
+ "## Query and gate costs\n",
+ "We will count queries to the above oracles, and arbitrary 1/2-qubit gates as described in the paper."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "id": "cf13f9e4-c65a-41f6-94ef-70168a136191",
+ "metadata": {
+ "scrolled": true
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.shims import generalize_1_2_qubit_gates\n",
+ "\n",
+ "g, sigma = bloq.call_graph(\n",
+ " generalizer=[ignore_alloc_free, ignore_split_join, generalize_1_2_qubit_gates]\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "id": "18fee717-4117-47ce-8929-1aa3e7fba91a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/markdown": [
+ "#### Counts totals:\n",
+ " - `And`: $\\displaystyle \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right) \\left(4 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} + N \\left(2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} - 2\\right) + N \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 2\\right) + N - 4\\right) + \\left(A + 2 N + \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + \\left(4 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} + N \\left(2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} - 2\\right) + N \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 2\\right) + N - 4\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil$\n",
+ " - `And†`: $\\displaystyle \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right) \\left(4 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} + N \\left(2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} - 2\\right) + N \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 2\\right) + N - 4\\right) + \\left(A + 2 N + \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + \\left(4 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} + N \\left(2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} - 2\\right) + N \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 2\\right) + N - 4\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil$\n",
+ " - `ArbitraryGate`: $\\displaystyle 2 \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right) \\left\\lceil{\\operatorname{log}_{2}{\\left(s \\right)}}\\right\\rceil + 2 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil \\left\\lceil{\\operatorname{log}_{2}{\\left(s \\right)}}\\right\\rceil$\n",
+ " - `ArbitraryGate`: $\\displaystyle \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right) \\left(4 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil + G + \\left(2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} - 2\\right) \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(s \\right)}}\\right\\rceil + 3\\right) + \\left(\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 1\\right) \\left\\lfloor{\\frac{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}{2}}\\right\\rfloor + 13 \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil + 2 \\left\\lceil{\\operatorname{log}_{2}{\\left(\\left\\lfloor{2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}}\\right\\rfloor \\right)}}\\right\\rceil + \\left\\lfloor{\\frac{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}{2}}\\right\\rfloor - 16\\right) + \\left(4 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil + G + \\left(2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} - 2\\right) \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(s \\right)}}\\right\\rceil + 3\\right) + \\left(\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 1\\right) \\left\\lfloor{\\frac{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}{2}}\\right\\rfloor + 13 \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil + 2 \\left\\lceil{\\operatorname{log}_{2}{\\left(\\left\\lfloor{2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}}\\right\\rfloor \\right)}}\\right\\rceil + \\left\\lfloor{\\frac{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}{2}}\\right\\rfloor - 16\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 2 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil$\n",
+ " - `C[oracle_O_F]`: $\\displaystyle 2 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 2$\n",
+ " - `C[oracle_O_F]`: $\\displaystyle 2 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil$\n",
+ " - `C[oracle_O_H]`: $\\displaystyle 4 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 2$\n",
+ " - `PrepareIdentity`: $\\displaystyle \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right) \\left(2 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} + 4 \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 6\\right) + \\left(2 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} + 4 \\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 6\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil$\n",
+ " - `oracle_O_F`: $\\displaystyle \\left(2 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} - 4\\right) \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right)$\n",
+ " - `oracle_O_F`: $\\displaystyle \\left(2 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} - 4\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil$\n",
+ " - `oracle_O_H`: $\\displaystyle \\left(2 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} - 4\\right) \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right) + \\left(2 \\cdot 2^{\\left\\lceil{\\operatorname{log}_{2}{\\left(\\frac{s \\log{\\left(\\frac{1}{\\gamma^{3}} \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil} - 4\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil$"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "show_counts_sigma(sigma)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a6083b59-ed3f-4543-ba28-d7f7b19d2b5a",
+ "metadata": {},
+ "source": [
+ "### Cost from the paper\n",
+ "Theorem 4.9 of the paper states that the algorithm uses:\n",
+ "1. $Q = \\widetilde{O}(s / (\\gamma \\alpha \\lambda))$ queries to oracles for H\n",
+ "2. $\\widetilde{O}(G/\\gamma + \\text{polylog}(Q)/\\gamma + QN)$ gates\n",
+ "\n",
+ "Let us simplify the symbolic costs obtained above and verify if they match."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 31,
+ "id": "9e03abf4-efd3-46b0-83d7-bf3b1b0554a5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/markdown": [
+ "#### Counts totals:\n",
+ " - `And`: $\\displaystyle \\frac{\\alpha \\lambda \\left(A + 2 N + \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right) \\left(- 2 N \\left(\\alpha \\lambda + 3 s \\log{\\left(\\gamma \\right)}\\right) + \\alpha \\lambda \\left(2 N \\left(\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 1\\right) + N - 4\\right) - 24 s \\log{\\left(\\gamma \\right)}\\right) + \\left(- 2 N \\left(\\alpha \\lambda + 3 s \\log{\\left(\\gamma \\right)}\\right) + \\alpha \\lambda \\left(2 N \\left(\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 1\\right) + N - 4\\right) - 24 s \\log{\\left(\\gamma \\right)}\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil}{\\alpha \\lambda}$\n",
+ " - `And†`: $\\displaystyle \\frac{\\alpha \\lambda \\left(A + 2 N + \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right) \\left(- 2 N \\left(\\alpha \\lambda + 3 s \\log{\\left(\\gamma \\right)}\\right) + \\alpha \\lambda \\left(2 N \\left(\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 1\\right) + N - 4\\right) - 24 s \\log{\\left(\\gamma \\right)}\\right) + \\left(- 2 N \\left(\\alpha \\lambda + 3 s \\log{\\left(\\gamma \\right)}\\right) + \\alpha \\lambda \\left(2 N \\left(\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 1\\right) + N - 4\\right) - 24 s \\log{\\left(\\gamma \\right)}\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil}{\\alpha \\lambda}$\n",
+ " - `ArbitraryGate`: $\\displaystyle 2 \\cdot \\left(2 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right) \\left\\lceil{\\operatorname{log}_{2}{\\left(s \\right)}}\\right\\rceil$\n",
+ " - `ArbitraryGate`: $\\displaystyle \\frac{2 \\alpha \\lambda \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right) \\left(\\alpha \\lambda \\left(G + \\left(\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 1\\right) \\left\\lfloor{\\frac{\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}{2}}\\right\\rfloor + 15 \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil + \\left\\lfloor{\\frac{\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}{2}}\\right\\rfloor - 16\\right) - 24 s \\log{\\left(\\gamma \\right)} \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 2 \\left(\\alpha \\lambda + 3 s \\log{\\left(\\gamma \\right)}\\right) \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(s \\right)}}\\right\\rceil + 3\\right)\\right) + \\left(\\alpha \\lambda \\left(G + \\left(\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 1\\right) \\left\\lfloor{\\frac{\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}{2}}\\right\\rfloor + 15 \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil + \\left\\lfloor{\\frac{\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}{2}}\\right\\rfloor - 16\\right) - 24 s \\log{\\left(\\gamma \\right)} \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 2 \\left(\\alpha \\lambda + 3 s \\log{\\left(\\gamma \\right)}\\right) \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(s \\right)}}\\right\\rceil + 3\\right)\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil}{\\alpha \\lambda}$\n",
+ " - `C[oracle_O_F]`: $\\displaystyle 2 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 2$\n",
+ " - `C[oracle_O_F]`: $\\displaystyle 2 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil$\n",
+ " - `C[oracle_O_H]`: $\\displaystyle 4 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 2$\n",
+ " - `PrepareIdentity`: $\\displaystyle \\frac{2 \\left(\\alpha \\lambda \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 3\\right) - 6 s \\log{\\left(\\gamma \\right)}\\right) \\left(2 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right)}{\\alpha \\lambda}$\n",
+ " - `oracle_O_F`: $\\displaystyle - \\frac{4 \\left(\\alpha \\lambda + 3 s \\log{\\left(\\gamma \\right)}\\right) \\left(\\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil + 1\\right)}{\\alpha \\lambda}$\n",
+ " - `oracle_O_F`: $\\displaystyle \\frac{4 \\left(- \\alpha \\lambda - 3 s \\log{\\left(\\gamma \\right)}\\right) \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil}{\\alpha \\lambda}$\n",
+ " - `oracle_O_H`: $\\displaystyle \\frac{4 \\left(\\alpha \\lambda + 3 s \\log{\\left(\\gamma \\right)}\\right) \\left(- 2 \\left\\lceil{\\frac{1}{\\gamma}}\\right\\rceil - 1\\right)}{\\alpha \\lambda}$"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "def simplify_2_pow_log(my_expr, inner_term):\n",
+ " # replace `2**(ceil(log2(T)))` upper bound `2T`.\n",
+ " temp = sympy.symbols(f\"_temp\", positive=True, integer=True)\n",
+ " my_expr = my_expr.replace(ceil(log2(inner_term)), temp)\n",
+ " my_expr = my_expr.replace(2**temp, 2 * inner_term)\n",
+ " my_expr = my_expr.replace(temp, ceil(log2(inner_term)))\n",
+ " return my_expr\n",
+ "\n",
+ "def simplify_expression(expr):\n",
+ " if not is_symbolic(expr): return expr\n",
+ " N, A, G, s = sympy.symbols(\"N A G s\", positive=True, integer=True)\n",
+ " lambd, alpha, gamma = sympy.symbols(r\"\\lambda \\alpha \\gamma\", positive=True, real=True)\n",
+ "\n",
+ " expr = simplify_2_pow_log(expr, (s * ln(1/gamma**3)) / (alpha * lambd))\n",
+ " expr = sympy.simplify(expr)\n",
+ " return expr\n",
+ "\n",
+ "sigma_simpl = {k: simplify_expression(v) for k, v in sigma.items()}\n",
+ "show_counts_sigma(sigma_simpl)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2676ae19-41b6-49a9-a16f-706e74b19cf9",
+ "metadata": {},
+ "source": [
+ "### Cost of Phase Estimation\n",
+ "\n",
+ "For simplicity, we can also look at the cost of a single phase estimation call (which is repeated $1/\\gamma$ times to obtain the above algorithm)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "id": "355eab2b-e136-4ff1-b131-a63248bbee31",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/markdown": [
+ "#### Counts totals:\n",
+ " - `And`: $\\displaystyle 2 N \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\log{\\left(\\gamma^{\\frac{3 s}{\\alpha \\lambda}} \\right)} \\right)}}\\right\\rceil - 3 N - 4 - \\frac{\\log{\\left(\\gamma^{24 s} \\gamma^{6 N s} \\right)}}{\\alpha \\lambda}$\n",
+ " - `And†`: $\\displaystyle 2 N \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\log{\\left(\\gamma^{\\frac{3 s}{\\alpha \\lambda}} \\right)} \\right)}}\\right\\rceil - 3 N - 4 - \\frac{\\log{\\left(\\gamma^{24 s} \\gamma^{6 N s} \\right)}}{\\alpha \\lambda}$\n",
+ " - `ArbitraryGate`: $\\displaystyle 2 \\left\\lceil{\\operatorname{log}_{2}{\\left(s \\right)}}\\right\\rceil$\n",
+ " - `ArbitraryGate`: $\\displaystyle G - 4 \\left\\lceil{\\operatorname{log}_{2}{\\left(s \\right)}}\\right\\rceil + \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil \\left\\lfloor{\\frac{\\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}{2}}\\right\\rfloor + 15 \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 22 - \\frac{12 s \\log{\\left(\\gamma \\right)} \\left\\lceil{\\operatorname{log}_{2}{\\left(s \\right)}}\\right\\rceil}{\\alpha \\lambda} - \\frac{24 s \\log{\\left(\\gamma \\right)} \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil}{\\alpha \\lambda} - \\frac{18 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda}$\n",
+ " - `C[oracle_O_F]`: 2\n",
+ " - `C[oracle_O_H]`: 2\n",
+ " - `PrepareIdentity`: $\\displaystyle 4 \\left\\lceil{\\operatorname{log}_{2}{\\left(- \\frac{3 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda} \\right)}}\\right\\rceil - 6 - \\frac{12 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda}$\n",
+ " - `oracle_O_F`: $\\displaystyle -4 - \\frac{12 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda}$\n",
+ " - `oracle_O_H`: $\\displaystyle -4 - \\frac{12 s \\log{\\left(\\gamma \\right)}}{\\alpha \\lambda}$"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "g_pe, sigma_pe = bloq.qpe_bloq.call_graph(\n",
+ " generalizer=[ignore_alloc_free, ignore_split_join, generalize_1_2_qubit_gates]\n",
+ ")\n",
+ "sigma_pe = {k: simplify_expression(v) for k, v in sigma_pe.items()}\n",
+ "show_counts_sigma(sigma_pe)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8cda3ad1-8183-4091-9faf-6c777a283951",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/walk_operator.py b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/walk_operator.py
new file mode 100644
index 000000000..48cf3447e
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/walk_operator.py
@@ -0,0 +1,95 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from functools import cached_property
+
+import attrs
+
+from qualtran import BloqBuilder, Signature, SoquetT
+from qualtran.bloqs.block_encoding import BlockEncoding
+from qualtran.bloqs.reflections.reflection_using_prepare import ReflectionUsingPrepare
+from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import SymbolicFloat, SymbolicInt
+
+
+@attrs.frozen
+class QubitizedWalkOperator(BlockEncoding):
+ r"""Construct a Szegedy Quantum Walk operator of a block encoding.
+
+ Args:
+ block_encoding: The input block-encoding.
+
+ References:
+ [Encoding Electronic Spectra in Quantum Circuits with Linear T Complexity](https://arxiv.org/abs/1805.03662).
+ Babbush et. al. (2018). Figure 1.
+ """
+
+ block_encoding: BlockEncoding
+
+ @property
+ def alpha(self) -> SymbolicFloat:
+ return self.block_encoding.alpha
+
+ @property
+ def system_bitsize(self) -> SymbolicInt:
+ return self.block_encoding.system_bitsize
+
+ @property
+ def ancilla_bitsize(self) -> SymbolicInt:
+ return self.block_encoding.ancilla_bitsize
+
+ @property
+ def resource_bitsize(self) -> SymbolicInt:
+ return self.block_encoding.resource_bitsize
+
+ @property
+ def epsilon(self) -> SymbolicFloat:
+ return self.block_encoding.epsilon
+
+ @property
+ def signal_state(self) -> BlackBoxPrepare:
+ return self.block_encoding.signal_state
+
+ @cached_property
+ def signature(self) -> Signature:
+ return self.block_encoding.signature
+
+ @cached_property
+ def reflect(self) -> ReflectionUsingPrepare:
+ return ReflectionUsingPrepare(self.block_encoding.signal_state, global_phase=-1)
+
+ def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> dict[str, 'SoquetT']:
+ soqs |= bb.add_d(self.block_encoding, **soqs)
+ soqs |= bb.add_d(
+ self.reflect, **{reg.name: soqs[reg.name] for reg in self.reflect.signature}
+ )
+ return soqs
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
+ return {self.block_encoding: 1, self.reflect: 1}
+
+ def __str__(self):
+ return f'Walk[{self.block_encoding}]'
diff --git a/qualtran/bloqs/max_k_xor_sat/guiding_state.ipynb b/qualtran/bloqs/max_k_xor_sat/guiding_state.ipynb
new file mode 100644
index 000000000..f7d94295d
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guiding_state.ipynb
@@ -0,0 +1,375 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "13ac92c7",
+ "metadata": {
+ "cq.autogen": "title_cell"
+ },
+ "source": [
+ "# Noisy kXOR: Guiding State\n",
+ "\n",
+ "Prepare the guiding state for a kXOR instance $\\mathcal{I}$ with\n",
+ "Kikuchi parameter $\\ell$.\n",
+ "\n",
+ "References:\n",
+ " [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)\n",
+ " Section 4.4.1, Theorem 4.15."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1aabf565",
+ "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": "96c16e6a",
+ "metadata": {
+ "cq.autogen": "SimpleGuidingState.bloq_doc.md"
+ },
+ "source": [
+ "## `SimpleGuidingState`\n",
+ "Prepare the guiding state for $\\ell = k$.\n",
+ "\n",
+ "Given an kXOR instance $\\mathcal{I}$, prepare the guiding state for\n",
+ "parameter $\\ell = k$ (i.e. $c = 1$), defined in Eq 134:\n",
+ " $$\n",
+ " |\\phi\\rangle\n",
+ " \\propto\n",
+ " |\\Gamma^k(\\mathcal{A})\\rangle\n",
+ " =\n",
+ " \\frac{1}{\\sqrt{\\tilde{m}}}\n",
+ " \\sum_{S \\in {[n] \\choose k}} B_\\mathcal{I}(S) |S\\rangle\n",
+ " $$\n",
+ "\n",
+ "Here, $\\tilde{m}$ is the number of constraints in the input instance $\\mathcal{I}$,\n",
+ "and $\\mathcal{A} = \\sqrt{\\frac{{n\\choose k}}{\\tilde{m}}} \\mathcal{I}$.\n",
+ "\n",
+ "This bloq has a gate cost of $O(\\tilde{m} \\log n)$ (see Eq 142 and paragraph below).\n",
+ "\n",
+ "#### Parameters\n",
+ " - `inst`: the kXOR instance $\\mathcal{I}$.\n",
+ " - `phasegrad_bitsize`: number of bits of the phase-gradient state used to synthesize rotations. \n",
+ "\n",
+ "#### Registers\n",
+ " - `S`: a scope of $k$ variables, each in $[n]$. \n",
+ "\n",
+ "#### References\n",
+ " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Equation 134.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1dfcd5d5",
+ "metadata": {
+ "cq.autogen": "SimpleGuidingState.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import SimpleGuidingState"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "41cb6141",
+ "metadata": {
+ "cq.autogen": "SimpleGuidingState.example_instances.md"
+ },
+ "source": [
+ "### Example Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "86264fa1",
+ "metadata": {
+ "cq.autogen": "SimpleGuidingState.simple_guiding_state_symb"
+ },
+ "outputs": [],
+ "source": [
+ "import sympy\n",
+ "\n",
+ "from qualtran.bloqs.max_k_xor_sat 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",
+ "simple_guiding_state_symb = SimpleGuidingState(inst)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5ec2c290",
+ "metadata": {
+ "cq.autogen": "SimpleGuidingState.simple_guiding_state"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import Constraint, KXorInstance\n",
+ "\n",
+ "inst = KXorInstance(\n",
+ " n=4,\n",
+ " k=2,\n",
+ " constraints=(\n",
+ " Constraint(S=(0, 1), b=1),\n",
+ " Constraint(S=(2, 3), b=-1),\n",
+ " Constraint(S=(1, 2), b=1),\n",
+ " ),\n",
+ ")\n",
+ "simple_guiding_state = SimpleGuidingState(inst)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7142e4ee",
+ "metadata": {
+ "cq.autogen": "SimpleGuidingState.graphical_signature.md"
+ },
+ "source": [
+ "#### Graphical Signature"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "bde3329c",
+ "metadata": {
+ "cq.autogen": "SimpleGuidingState.graphical_signature.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.drawing import show_bloqs\n",
+ "show_bloqs([simple_guiding_state_symb, simple_guiding_state],\n",
+ " ['`simple_guiding_state_symb`', '`simple_guiding_state`'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "905d7bbc",
+ "metadata": {
+ "cq.autogen": "SimpleGuidingState.call_graph.md"
+ },
+ "source": [
+ "### Call Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4eff0b6e",
+ "metadata": {
+ "cq.autogen": "SimpleGuidingState.call_graph.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.resource_counting.generalizers import ignore_split_join\n",
+ "simple_guiding_state_symb_g, simple_guiding_state_symb_sigma = simple_guiding_state_symb.call_graph(max_depth=1, generalizer=ignore_split_join)\n",
+ "show_call_graph(simple_guiding_state_symb_g)\n",
+ "show_counts_sigma(simple_guiding_state_symb_sigma)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1c74c489",
+ "metadata": {
+ "cq.autogen": "GuidingState.bloq_doc.md"
+ },
+ "source": [
+ "## `GuidingState`\n",
+ "Prepare a guiding state for a kXOR instance with parameter $\\ell$.\n",
+ "\n",
+ "Given an kXOR instance $\\mathcal{I}$, and parameter $\\ell$ (a multiple of $k$),\n",
+ "we want to prepare the unit-length guiding state $|\\mathbb{\\Psi}\\rangle$ (Eq 135):\n",
+ "\n",
+ " $$\n",
+ " |\\mathbb{\\Psi}\\rangle\n",
+ " \\propto\n",
+ " |\\Gamma^\\ell(\\mathcal{A})\\rangle\n",
+ " \\propto\n",
+ " \\sum_{T \\in {[n] \\choose \\ell}}\n",
+ " \\sum_{\\{S_1, \\ldots, S_c\\} \\in \\text{Part}_k(T)}\n",
+ " \\left(\n",
+ " \\prod_{j = 1}^c B_{\\mathcal{I}}(S)\n",
+ " \\right)\n",
+ " |T\\rangle\n",
+ " $$\n",
+ "\n",
+ "This bloq prepares the state (Eq 136):\n",
+ " $$ \\beta |\\mathbb{\\Psi}\\rangle |0^{\\ell \\log \\ell + 3}\\rangle\n",
+ " + |\\perp\\rangle |1\\rangle\n",
+ " $$\n",
+ "where $\\beta \\ge \\Omega(1 / \\ell^{\\ell/2})$,\n",
+ "and $\\tilde{m}$ is the number of constraints in $\\mathcal{I}$.\n",
+ "\n",
+ "This has a gate cost of $O(\\ell \\tilde{m} \\log n)$.\n",
+ "\n",
+ "#### Registers\n",
+ " - `T`: $\\ell$ indices each in $[n]$.\n",
+ " - `ancilla`: (entangled) $\\ell\\log\\ell+3$ ancilla qubits used for state preparation. The all zeros state of the ancilla is the good subspace. \n",
+ "\n",
+ "#### References\n",
+ " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Section 4.4.1 \"Preparing the guiding state\", Theorem 4.15. Eq 136.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0ce2e5d7",
+ "metadata": {
+ "cq.autogen": "GuidingState.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import GuidingState"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "40aa8f21",
+ "metadata": {
+ "cq.autogen": "GuidingState.example_instances.md"
+ },
+ "source": [
+ "### Example Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "00cf8dde",
+ "metadata": {
+ "cq.autogen": "GuidingState.guiding_state_symb_c"
+ },
+ "outputs": [],
+ "source": [
+ "import sympy\n",
+ "\n",
+ "from qualtran.bloqs.max_k_xor_sat import KXorInstance\n",
+ "\n",
+ "n, m, c = sympy.symbols(\"n m c\", positive=True, integer=True)\n",
+ "k = sympy.symbols(\"k\", positive=True, integer=True, even=True)\n",
+ "inst = KXorInstance.symbolic(n=n, m=m, k=k)\n",
+ "guiding_state_symb_c = GuidingState(inst, ell=c * k)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "10ade09c",
+ "metadata": {
+ "cq.autogen": "GuidingState.guiding_state_symb"
+ },
+ "outputs": [],
+ "source": [
+ "import sympy\n",
+ "\n",
+ "from qualtran.bloqs.max_k_xor_sat 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",
+ "c = 2\n",
+ "guiding_state_symb = GuidingState(inst, ell=c * inst.k)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3176444f",
+ "metadata": {
+ "cq.autogen": "GuidingState.guiding_state"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import Constraint, KXorInstance\n",
+ "\n",
+ "inst = KXorInstance(\n",
+ " n=4,\n",
+ " k=2,\n",
+ " constraints=(\n",
+ " Constraint(S=(0, 1), b=1),\n",
+ " Constraint(S=(2, 3), b=-1),\n",
+ " Constraint(S=(1, 2), b=1),\n",
+ " ),\n",
+ ")\n",
+ "guiding_state = GuidingState(inst, ell=4)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "98c7518d",
+ "metadata": {
+ "cq.autogen": "GuidingState.graphical_signature.md"
+ },
+ "source": [
+ "#### Graphical Signature"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0f8e52e4",
+ "metadata": {
+ "cq.autogen": "GuidingState.graphical_signature.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.drawing import show_bloqs\n",
+ "show_bloqs([guiding_state_symb_c, guiding_state_symb, guiding_state],\n",
+ " ['`guiding_state_symb_c`', '`guiding_state_symb`', '`guiding_state`'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dc2396af",
+ "metadata": {
+ "cq.autogen": "GuidingState.call_graph.md"
+ },
+ "source": [
+ "### Call Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f37b9415",
+ "metadata": {
+ "cq.autogen": "GuidingState.call_graph.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.resource_counting.generalizers import ignore_split_join\n",
+ "guiding_state_symb_c_g, guiding_state_symb_c_sigma = guiding_state_symb_c.call_graph(max_depth=1, generalizer=ignore_split_join)\n",
+ "show_call_graph(guiding_state_symb_c_g)\n",
+ "show_counts_sigma(guiding_state_symb_c_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/max_k_xor_sat/guiding_state.py b/qualtran/bloqs/max_k_xor_sat/guiding_state.py
new file mode 100644
index 000000000..d801591ae
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guiding_state.py
@@ -0,0 +1,410 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+r"""Prepare the guiding state for a kXOR instance $\mathcal{I}$ with
+Kikuchi parameter $\ell$.
+
+References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Section 4.4.1, Theorem 4.15.
+"""
+from functools import cached_property
+
+from attrs import evolve, field, frozen
+
+from qualtran import (
+ bloq_example,
+ BloqBuilder,
+ BloqDocSpec,
+ DecomposeTypeError,
+ QAny,
+ QBit,
+ QUInt,
+ Register,
+ Signature,
+ Soquet,
+ SoquetT,
+)
+from qualtran.bloqs.basic_gates import XGate
+from qualtran.bloqs.bookkeeping import Partition
+from qualtran.bloqs.max_k_xor_sat.resource.phase_gradient import AcquirePhaseGradient
+from qualtran.bloqs.mcmt import MultiControlX
+from qualtran.bloqs.state_preparation.prepare_base import PrepareOracle
+from qualtran.bloqs.state_preparation.sparse_state_preparation_via_rotations import (
+ SparseStatePreparationViaRotations,
+)
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import ceil, is_symbolic, log2, pi, SymbolicFloat, SymbolicInt
+from qualtran.symbolics.simplification import extract_int
+
+from .arithmetic import HasDuplicates, SortInPlace
+from .kxor_instance import KXorInstance
+from .resource.phase_gradient import ignore_resource_alloc_free
+from .shims import ProbabilisticUncompute
+
+
+@frozen
+class SimpleGuidingState(PrepareOracle):
+ r"""Prepare the guiding state for $\ell = k$.
+
+ Given an kXOR instance $\mathcal{I}$, prepare the guiding state for
+ parameter $\ell = k$ (i.e. $c = 1$), defined in Eq 134:
+ $$
+ |\phi\rangle
+ \propto
+ |\Gamma^k(\mathcal{A})\rangle
+ =
+ \frac{1}{\sqrt{\tilde{m}}}
+ \sum_{S \in {[n] \choose k}} B_\mathcal{I}(S) |S\rangle
+ $$
+
+ Here, $\tilde{m}$ is the number of constraints in the input instance $\mathcal{I}$,
+ and $\mathcal{A} = \sqrt{\frac{{n\choose k}}{\tilde{m}}} \mathcal{I}$.
+
+ This bloq has a gate cost of $O(\tilde{m} \log n)$ (see Eq 142 and paragraph below).
+
+ Args:
+ inst: the kXOR instance $\mathcal{I}$.
+ eps: Precision of the prepared state (defaults to 1e-6).
+
+ Registers:
+ S: a scope of $k$ variables, each in $[n]$.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Equation 134.
+ """
+
+ inst: KXorInstance
+ eps: SymbolicFloat = field(default=1e-6, kw_only=True)
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature.build_from_dtypes(S=QAny(self.target_bitsize))
+
+ @property
+ def target_bitsize(self):
+ """number of bits to represent a k-subset S"""
+ return self.inst.k * self.inst.index_bitsize
+
+ @property
+ def selection_registers(self) -> tuple[Register, ...]:
+ """TODO upgrade this interface"""
+ return (Register('S', QAny(self.target_bitsize)),)
+
+ @property
+ def phasegrad_bitsize(self) -> SymbolicInt:
+ return ceil(log2(2 * pi(self.eps) / self.eps))
+
+ @property
+ def _state_prep_bloq(self) -> SparseStatePreparationViaRotations:
+ N = 2**self.target_bitsize
+
+ if self.inst.is_symbolic():
+ bloq = SparseStatePreparationViaRotations.from_n_coeffs(
+ N, self.inst.num_unique_constraints, phase_bitsize=self.phasegrad_bitsize
+ )
+ else:
+ assert not is_symbolic(self.inst.batched_scopes)
+
+ bloq = SparseStatePreparationViaRotations.from_coefficient_map(
+ N,
+ {self.inst.scope_as_int(S): B_I for S, B_I in self.inst.batched_scopes},
+ self.phasegrad_bitsize,
+ )
+
+ bloq = evolve(bloq, target_bitsize=self.target_bitsize)
+ return bloq
+
+ def build_composite_bloq(self, bb: 'BloqBuilder', S: Soquet) -> dict[str, 'SoquetT']:
+ phase_grad = bb.add(AcquirePhaseGradient(self.phasegrad_bitsize))
+ S, phase_grad = bb.add(self._state_prep_bloq, target_state=S, phase_gradient=phase_grad)
+ bb.add(AcquirePhaseGradient(self.phasegrad_bitsize).adjoint(), phase_grad=phase_grad)
+ return {'S': S}
+
+
+@bloq_example
+def _simple_guiding_state() -> SimpleGuidingState:
+ from qualtran.bloqs.max_k_xor_sat import Constraint, KXorInstance
+
+ inst = KXorInstance(
+ n=4,
+ k=2,
+ constraints=(
+ Constraint(S=(0, 1), b=1),
+ Constraint(S=(2, 3), b=-1),
+ Constraint(S=(1, 2), b=1),
+ ),
+ )
+ simple_guiding_state = SimpleGuidingState(inst)
+ return simple_guiding_state
+
+
+@bloq_example
+def _simple_guiding_state_symb() -> SimpleGuidingState:
+ import sympy
+
+ from qualtran.bloqs.max_k_xor_sat import KXorInstance
+
+ n, m, k = sympy.symbols("n m k", positive=True, integer=True)
+ inst = KXorInstance.symbolic(n=n, m=m, k=k)
+ simple_guiding_state_symb = SimpleGuidingState(inst)
+ return simple_guiding_state_symb
+
+
+_SIMPLE_GUIDING_STATE_DOC = BloqDocSpec(
+ bloq_cls=SimpleGuidingState, examples=[_simple_guiding_state_symb, _simple_guiding_state]
+)
+
+
+@frozen
+class GuidingState(PrepareOracle):
+ r"""Prepare a guiding state for a kXOR instance with parameter $\ell$.
+
+ Given an kXOR instance $\mathcal{I}$, and parameter $\ell$ (a multiple of $k$),
+ we want to prepare the unit-length guiding state $|\mathbb{\Psi}\rangle$ (Eq 135):
+
+ $$
+ |\mathbb{\Psi}\rangle
+ \propto
+ |\Gamma^\ell(\mathcal{A})\rangle
+ \propto
+ \sum_{T \in {[n] \choose \ell}}
+ \sum_{\{S_1, \ldots, S_c\} \in \text{Part}_k(T)}
+ \left(
+ \prod_{j = 1}^c B_{\mathcal{I}}(S)
+ \right)
+ |T\rangle
+ $$
+
+ This bloq prepares the state (Eq 136):
+ $$ \beta |\mathbb{\Psi}\rangle |0^{\ell \log \ell + 3}\rangle
+ + |\perp\rangle |1\rangle
+ $$
+ where $\beta \ge \Omega(1 / \ell^{\ell/2})$,
+ and $\tilde{m}$ is the number of constraints in $\mathcal{I}$.
+
+ This has a gate cost of $O(\ell \tilde{m} \log n)$.
+
+ Args:
+ inst: the kXOR instance $\mathcal{I}$.
+ ell: the Kikuchi parameter $\ell$.
+ amplitude_good_part: (optional) the amplitude $\beta$ of the guiding state $|\Psi\rangle$
+ Defaults to $\beta = 0.99 / \ell^{\ell/2}$.
+ eps: Precision of the prepared state (defaults to 1e-6).
+
+ Registers:
+ T: $\ell$ indices each in $[n]$.
+ ancilla (RIGHT): (entangled) $\ell\log\ell+3$ ancilla qubits used for state preparation.
+ The all zeros state of the ancilla is the good subspace.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Section 4.4.1 "Preparing the guiding state", Theorem 4.15. Eq 136.
+ """
+
+ inst: KXorInstance
+ ell: SymbolicInt
+ amplitude_good_part: SymbolicFloat = field(kw_only=True)
+ eps: SymbolicFloat = field(default=1e-6, kw_only=True)
+
+ @amplitude_good_part.default
+ def _default_amplitude(self):
+ return self.coeff_good
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature(
+ [
+ Register('T', QAny(self.target_bitsize)),
+ Register('ancilla', QAny(self.ancilla_bitsize)),
+ ]
+ )
+
+ @property
+ def target_bitsize(self) -> SymbolicInt:
+ return self.inst.index_bitsize * self.ell
+
+ @property
+ def ancilla_bitsize(self) -> SymbolicInt:
+ r"""total number of entangled ancilla.
+
+ $\ell \log \ell$ for sorting, and 3 flag qubits.
+ """
+ return self.sort_ancilla_bitsize + 3
+
+ @property
+ def selection_registers(self) -> tuple[Register, ...]:
+ """TODO upgrade this interface"""
+ return (Register('T', QAny(self.target_bitsize)),)
+
+ @property
+ def junk_registers(self) -> tuple[Register, ...]:
+ """TODO upgrade this interface"""
+ return (Register('ancilla', QAny(self.ancilla_bitsize)),)
+
+ @property
+ def sort_ancilla_bitsize(self):
+ r"""Number of entangled ancilla generated by the sorting algorithm.
+
+ This is a sequence of $\ell$ numbers, each in $[\ell]$, therefore is $\ell \lceil \log \ell \rceil$.
+ """
+ logl = ceil(log2(self.ell))
+ return self.ell * logl
+
+ @property
+ def c(self) -> SymbolicInt:
+ r"""Value of $c = \ell / k$."""
+ c = self.ell // self.inst.k
+ c = extract_int(c)
+ return c
+
+ @property
+ def simple_guiding_state(self) -> SimpleGuidingState:
+ r"""The simple guiding state $|\phi\rangle$
+
+ This is the simple guiding state defined in Eq. 142,
+ which is proportional to $|\Gamma^k\rangle$ (Eq. 134).
+ We will use $c$ copies of this state to prepare the required guiding state.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Section 4.4.1 "Preparing the guiding state", Eq. 134.
+ """
+ return SimpleGuidingState(self.inst, eps=self.eps / self.c)
+
+ @property
+ def _index_dtype(self) -> QUInt:
+ return QUInt(self.inst.index_bitsize)
+
+ def build_composite_bloq(
+ self, bb: 'BloqBuilder', T: 'Soquet', ancilla: 'Soquet'
+ ) -> dict[str, 'SoquetT']:
+ if is_symbolic(self.c):
+ raise DecomposeTypeError(f"cannot decompose {self} with symbolic c=l/k={self.c}")
+
+ partition_T_to_S = Partition(
+ self.target_bitsize,
+ (Register('S', dtype=QAny(self.simple_guiding_state.target_bitsize), shape=(self.c,)),),
+ )
+
+ partition_ancilla = Partition(
+ self.ancilla_bitsize,
+ (
+ Register('ancilla', QAny(self.sort_ancilla_bitsize)),
+ Register('flags', QBit(), shape=(3,)),
+ ),
+ )
+
+ ancilla, [flag_duplicates, flag_uncompute, flag] = bb.add(partition_ancilla, x=ancilla)
+
+ # Equation 144: |Phi> = |phi>^{\otimes c}
+ S = bb.add(partition_T_to_S, x=T)
+ for i in range(self.c):
+ S[i] = bb.add(self.simple_guiding_state, S=S[i])
+ T = bb.add(partition_T_to_S.adjoint(), S=S)
+
+ # sort T using `l log l` entangled clean ancilla
+ T, ancilla = bb.add(SortInPlace(self.ell, self._index_dtype), input=T, ancilla=ancilla)
+
+ # mark if T has duplicates (i.e. not disjoint) (Eq 145)
+ T, flag_duplicates = bb.add(
+ HasDuplicates(self.ell, self._index_dtype), input=T, flag=flag_duplicates
+ )
+
+ # probabilistically uncompute the sorting ancilla, and mark in a flag bit
+ # note: flag is 0 for success (like syscall/c exit codes)
+ ancilla, flag_uncompute = bb.add(
+ ProbabilisticUncompute(self.sort_ancilla_bitsize), q=ancilla, flag=flag_uncompute
+ )
+
+ # compute the overall flag using OR, to obtain Eq 130.
+ [flag_duplicates, flag_uncompute], flag = bb.add(
+ MultiControlX(cvs=(0, 0)), controls=[flag_duplicates, flag_uncompute], target=flag
+ )
+ flag = bb.add(XGate(), q=flag)
+
+ # join all the ancilla into a single bag of bits
+ ancilla = bb.add(
+ partition_ancilla.adjoint(),
+ ancilla=ancilla,
+ flags=[flag_duplicates, flag_uncompute, flag],
+ )
+
+ return {'T': T, 'ancilla': ancilla}
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
+ return {
+ self.simple_guiding_state: self.c,
+ SortInPlace(self.ell, self._index_dtype): 1,
+ HasDuplicates(self.ell, self._index_dtype): 1,
+ ProbabilisticUncompute(self.sort_ancilla_bitsize): 1,
+ MultiControlX(cvs=(0, 0)): 1,
+ XGate(): 1,
+ }
+
+ @cached_property
+ def coeff_good(self):
+ """lower bound on beta, the coefficient of the good state.
+
+ Sentence below Eq. 147.
+ """
+ return 0.99 / 2 ** (self.sort_ancilla_bitsize / 2)
+
+
+@bloq_example(generalizer=[ignore_resource_alloc_free])
+def _guiding_state() -> GuidingState:
+ from qualtran.bloqs.max_k_xor_sat import Constraint, KXorInstance
+
+ inst = KXorInstance(
+ n=4,
+ k=2,
+ constraints=(
+ Constraint(S=(0, 1), b=1),
+ Constraint(S=(2, 3), b=-1),
+ Constraint(S=(1, 2), b=1),
+ ),
+ )
+ guiding_state = GuidingState(inst, ell=4)
+ return guiding_state
+
+
+@bloq_example(generalizer=[ignore_resource_alloc_free])
+def _guiding_state_symb() -> GuidingState:
+ import sympy
+
+ from qualtran.bloqs.max_k_xor_sat import KXorInstance
+
+ n, m, k = sympy.symbols("n m k", positive=True, integer=True)
+ inst = KXorInstance.symbolic(n=n, m=m, k=k)
+ c = 2
+ guiding_state_symb = GuidingState(inst, ell=c * inst.k)
+ return guiding_state_symb
+
+
+@bloq_example(generalizer=[ignore_resource_alloc_free])
+def _guiding_state_symb_c() -> GuidingState:
+ import sympy
+
+ from qualtran.bloqs.max_k_xor_sat import KXorInstance
+
+ n, m, c = sympy.symbols("n m c", positive=True, integer=True)
+ k = sympy.symbols("k", positive=True, integer=True, even=True)
+ inst = KXorInstance.symbolic(n=n, m=m, k=k)
+ guiding_state_symb_c = GuidingState(inst, ell=c * k)
+ return guiding_state_symb_c
+
+
+_GUIDING_STATE_DOC = BloqDocSpec(
+ bloq_cls=GuidingState, examples=[_guiding_state_symb_c, _guiding_state_symb, _guiding_state]
+)
diff --git a/qualtran/bloqs/max_k_xor_sat/guiding_state_test.py b/qualtran/bloqs/max_k_xor_sat/guiding_state_test.py
new file mode 100644
index 000000000..2b9e3cff7
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guiding_state_test.py
@@ -0,0 +1,128 @@
+# 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 qualtran.resource_counting import big_O, GateCounts, get_cost_value, QECGatesCost
+from qualtran.symbolics import bit_length, ceil, log2
+
+from .guiding_state import (
+ _guiding_state,
+ _guiding_state_symb,
+ _guiding_state_symb_c,
+ _simple_guiding_state,
+ _simple_guiding_state_symb,
+)
+
+
+@pytest.mark.parametrize(
+ "bloq_ex",
+ [
+ _simple_guiding_state,
+ _simple_guiding_state_symb,
+ _guiding_state,
+ _guiding_state_symb,
+ _guiding_state_symb_c,
+ ],
+)
+def test_examples(bloq_autotester, bloq_ex):
+ if bloq_autotester.check_name == 'serialize':
+ pytest.skip()
+
+ bloq_autotester(bloq_ex)
+
+
+def test_t_cost_simple():
+ bloq = _simple_guiding_state()
+ gc = get_cost_value(bloq, QECGatesCost())
+ B_GRAD = bloq.phasegrad_bitsize
+
+ assert gc == GateCounts(and_bloq=24, toffoli=3 * (B_GRAD - 2), clifford=ANY, measurement=ANY)
+
+
+def test_t_cost_simple_symb():
+ bloq = _simple_guiding_state_symb()
+ gc = get_cost_value(bloq, QECGatesCost())
+ B_GRAD = bloq.phasegrad_bitsize
+
+ n, m, k = bloq.inst.n, bloq.inst.m, bloq.inst.k
+ klogn = k * ceil(log2(n))
+ # https://github.com/quantumlib/Qualtran/issues/1341
+ klogn_roundtrip = bit_length(2**klogn - 1)
+
+ assert gc == GateCounts(
+ # O(k m log n)
+ and_bloq=4 * m + (2 * m + 1) * (klogn_roundtrip - 1) - 4,
+ toffoli=2 * (B_GRAD - 2),
+ clifford=ANY,
+ measurement=ANY,
+ )
+
+
+def test_t_cost():
+ bloq = _guiding_state()
+ gc = get_cost_value(bloq, QECGatesCost())
+ B_GRAD = bloq.simple_guiding_state.phasegrad_bitsize
+
+ assert gc == GateCounts(
+ and_bloq=364, toffoli=6 * (B_GRAD - 2), cswap=192, clifford=ANY, measurement=ANY
+ )
+
+
+@pytest.mark.parametrize("bloq_ex", [_guiding_state_symb, _guiding_state_symb_c])
+def test_t_cost_symb_c(bloq_ex):
+ bloq = bloq_ex()
+ gc = get_cost_value(bloq, QECGatesCost())
+ B_GRAD = bloq.simple_guiding_state.phasegrad_bitsize
+
+ n, m, k = bloq.inst.n, bloq.inst.m, bloq.inst.k
+ l, c = bloq.ell, bloq.c
+
+ logn = ceil(log2(n))
+ logl = ceil(log2(l))
+
+ klogn = k * logn
+ # https://github.com/quantumlib/Qualtran/issues/1341
+ klogn_roundtrip = bit_length(2**klogn - 1)
+
+ assert gc == GateCounts(
+ and_bloq=(
+ 6 * l**2 * (2 * logn + 1)
+ + l * logl
+ + l
+ + c * (4 * m + (2 * m + 1) * (klogn_roundtrip - 1) - 4)
+ + (2 * l - 2) * (2 * logn + 1)
+ - 2
+ ),
+ toffoli=c * (2 * (B_GRAD - 2)),
+ cswap=6 * l**2 * logn,
+ clifford=ANY,
+ measurement=ANY,
+ )
+
+ # verify big_O
+ t_cost = gc.total_t_count()
+ t_cost = sympy.sympify(t_cost)
+ t_cost = t_cost.subs(klogn_roundtrip, klogn)
+ t_cost = t_cost.simplify()
+ assert t_cost in big_O(l * m * logn + l**2 * logn + B_GRAD * c)
+
+
+@pytest.mark.notebook
+def test_notebook():
+ from qualtran.testing import execute_notebook
+
+ execute_notebook('guiding_state')
diff --git a/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_list.py b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_list.py
new file mode 100644
index 000000000..2032ab177
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_list.py
@@ -0,0 +1,346 @@
+# 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 sympy
+from attrs import frozen
+
+from qualtran import (
+ Bloq,
+ bloq_example,
+ BloqBuilder,
+ BloqDocSpec,
+ QAny,
+ QBit,
+ Signature,
+ Soquet,
+ SoquetT,
+)
+from qualtran.bloqs.arithmetic import AddK, Equals, Xor
+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 .arithmetic import SymmetricDifference
+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.build_from_dtypes(
+ S=QAny(self.index_bitsize),
+ k=QAny(self.index_bitsize),
+ T=QAny(self.index_bitsize),
+ flag=QBit(),
+ )
+
+ @property
+ def index_bitsize(self) -> SymbolicInt:
+ return self.ell * 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
+ logn = self.inst.index_bitsize
+
+ counts_forward = Counter[Bloq]()
+
+ # compute symmetric differences for each constraint
+ counts_forward[SymmetricDifference(ell, k, ell, logn)] += m
+
+ # counter
+ counts_forward[AddK(logn, 1).controlled()] += m
+
+ # compare counter each time
+ counts_forward[Equals(QAny(logn))] += 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(QAny(logn)).controlled()] += m
+
+ # if nothing matched (final flag = 0), copy k and flip the flag bit
+ counts[Xor(QAny(logn)).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.max_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.build_from_dtypes(
+ S=QAny(self.index_bitsize),
+ T=QAny(self.index_bitsize),
+ k=QAny(self.index_bitsize),
+ flag=QBit(),
+ )
+
+ @property
+ def index_bitsize(self) -> SymbolicInt:
+ return self.ell * 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
+ logn = self.inst.index_bitsize
+
+ counts_forward = Counter[Bloq]()
+
+ # compute symmetric differences for each constraint
+ counts_forward[SymmetricDifference(ell, k, ell, logn)] += m
+
+ # counter
+ counts_forward[AddK(logn, 1).controlled()] += m
+
+ # compare T to f_S_k each time
+ counts_forward[Equals(QAny(self.index_bitsize))] += 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(QAny(logn)).controlled()] += m
+
+ # if nothing matched (final flag = 0), copy k and flip the flag bit
+ counts[Xor(QAny(logn)).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.build_from_dtypes(S=QAny(self.index_bitsize), k=QAny(self.index_bitsize))
+
+ @property
+ def index_bitsize(self) -> SymbolicInt:
+ return self.ell * self.inst.index_bitsize
+
+ def build_composite_bloq(
+ self, bb: 'BloqBuilder', S: 'Soquet', k: 'Soquet'
+ ) -> dict[str, 'SoquetT']:
+ T = bb.allocate(self.index_bitsize)
+ 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)
+ bb.free(k)
+ bb.add(ZeroEffect(), q=flag)
+ return dict(S=S, k=T)
+
+
+@bloq_example
+def _kikuchi_nonzero_index() -> KikuchiNonZeroIndex:
+ from qualtran.bloqs.max_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.max_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_symb, _kikuchi_nonzero_index]
+)
diff --git a/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_list_test.py b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_list_test.py
new file mode 100644
index 000000000..d9523c747
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_list_test.py
@@ -0,0 +1,72 @@
+# 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 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)
diff --git a/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_matrix.py b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_matrix.py
new file mode 100644
index 000000000..8f669e8b0
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_matrix.py
@@ -0,0 +1,186 @@
+# 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
+from typing import Optional
+
+import attrs
+import sympy
+from attrs import frozen
+
+from qualtran import (
+ AddControlledT,
+ Bloq,
+ bloq_example,
+ BloqDocSpec,
+ CtrlSpec,
+ QAny,
+ QBit,
+ QFxp,
+ Signature,
+)
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import SymbolicInt
+
+from .arithmetic import SymmetricDifference
+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
+ cv: Optional[int] = None
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature.build_from_dtypes(
+ ctrl=QAny(1 if self.cv is not None else 0),
+ S=QAny(self.index_bitsize),
+ T=QAny(self.index_bitsize),
+ q=QBit(),
+ )
+
+ @cached_property
+ def 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.inst.index_bitsize)
+ 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.cv is not None:
+ rotation = rotation.controlled(CtrlSpec(cvs=self.cv))
+ counts[rotation] += 1
+
+ return counts
+
+ def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> tuple['Bloq', 'AddControlledT']:
+ from qualtran.bloqs.mcmt.bloq_with_specialized_single_qubit_control import (
+ get_ctrl_system_for_bloq_with_specialized_single_qubit_control,
+ )
+
+ return get_ctrl_system_for_bloq_with_specialized_single_qubit_control(self, ctrl_spec)
+
+ def with_cv(self, *, cv: Optional[int]) -> 'Bloq':
+ return attrs.evolve(self, cv=cv)
+
+
+@bloq_example
+def _kikuchi_matrix_entry() -> KikuchiMatrixEntry:
+ from qualtran.bloqs.max_k_xor_sat.kxor_instance import Constraint, KXorInstance
+
+ n, k = 10, 4
+ cs = (
+ Constraint((0, 1, 2, 3), -1),
+ Constraint((0, 2, 4, 5), 1),
+ Constraint((0, 3, 4, 5), 1),
+ Constraint((0, 3, 4, 5), 1),
+ Constraint((1, 2, 3, 4), -1),
+ Constraint((1, 3, 4, 5), -1),
+ Constraint((1, 3, 4, 5), -1),
+ Constraint((2, 3, 4, 5), 1),
+ )
+ inst = KXorInstance(n, k, cs)
+ 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.max_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/max_k_xor_sat/kikuchi_adjacency_matrix_test.py b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_matrix_test.py
new file mode 100644
index 000000000..d36bf25d2
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_matrix_test.py
@@ -0,0 +1,87 @@
+# 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
+
+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=11550, 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)
diff --git a/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding.ipynb b/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding.ipynb
new file mode 100644
index 000000000..38f7b8213
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding.ipynb
@@ -0,0 +1,1265 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "1b8feca5",
+ "metadata": {
+ "cq.autogen": "title_cell"
+ },
+ "source": [
+ "# 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": "9cf95753",
+ "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": "f77a3d87",
+ "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": "9a6e1a31",
+ "metadata": {
+ "cq.autogen": "KikuchiMatrixEntry.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import KikuchiMatrixEntry"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6b89e54d",
+ "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\n",
+ "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 must implement two functions:\n",
+ "$(S, k) \\mapsto f(S, k)$ and $(S, f(S, k)) \\mapsto k$.\n",
+ "\n",
+ "\n",
+ "For the second, we can use the pseudo-code:\n",
+ "```\n",
+ "def reverse(S, f_S_k) -> k:\n",
+ " nnz := 0 # counter\n",
+ " for j in range(\\bar{m}):\n",
+ " T := S \\Delta U_j\n",
+ " entry := KikuchiMatrixEntry(S, T)\n",
+ " if entry != 0:\n",
+ " nnz := nnz + 1\n",
+ " if T == f_S_k:\n",
+ " k ^= nnz\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.\n",
+ "As long as the 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": "ef63db06",
+ "metadata": {
+ "cq.autogen": "KikuchiNonZeroIndex.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import KikuchiNonZeroIndex"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1cb30021",
+ "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": "ba1fa5a1",
+ "metadata": {
+ "cq.autogen": "KikuchiHamiltonian.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import KikuchiHamiltonian"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3c9a2aea",
+ "metadata": {
+ "cq.autogen": "KikuchiHamiltonian.example_instances.md"
+ },
+ "source": [
+ "### Example Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "94da5bc4",
+ "metadata": {
+ "cq.autogen": "KikuchiHamiltonian.kikuchi_matrix_symb"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_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, entry_bitsize=10)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6655ce72",
+ "metadata": {
+ "cq.autogen": "KikuchiHamiltonian.kikuchi_matrix"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.kxor_instance import Constraint, KXorInstance\n",
+ "\n",
+ "n, k = 10, 4\n",
+ "cs = (\n",
+ " Constraint((0, 1, 2, 3), -1),\n",
+ " Constraint((0, 2, 4, 5), 1),\n",
+ " Constraint((0, 3, 4, 5), 1),\n",
+ " Constraint((0, 3, 4, 5), 1),\n",
+ " Constraint((1, 2, 3, 4), -1),\n",
+ " Constraint((1, 3, 4, 5), -1),\n",
+ " Constraint((1, 3, 4, 5), -1),\n",
+ " Constraint((2, 3, 4, 5), 1),\n",
+ ")\n",
+ "inst = KXorInstance(n, k, cs)\n",
+ "ell = 8\n",
+ "\n",
+ "kikuchi_matrix = KikuchiHamiltonian(inst, ell, entry_bitsize=10)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "852691fa",
+ "metadata": {
+ "cq.autogen": "KikuchiHamiltonian.graphical_signature.md"
+ },
+ "source": [
+ "#### Graphical Signature"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "17d9dfa4",
+ "metadata": {
+ "cq.autogen": "KikuchiHamiltonian.graphical_signature.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.drawing import show_bloqs\n",
+ "show_bloqs([kikuchi_matrix_symb, kikuchi_matrix],\n",
+ " ['`kikuchi_matrix_symb`', '`kikuchi_matrix`'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f3c5ab8a",
+ "metadata": {
+ "cq.autogen": "KikuchiHamiltonian.call_graph.md"
+ },
+ "source": [
+ "### Call Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f641df92",
+ "metadata": {
+ "cq.autogen": "KikuchiHamiltonian.call_graph.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.resource_counting.generalizers import ignore_split_join\n",
+ "kikuchi_matrix_symb_g, kikuchi_matrix_symb_sigma = kikuchi_matrix_symb.call_graph(max_depth=1, generalizer=ignore_split_join)\n",
+ "show_call_graph(kikuchi_matrix_symb_g)\n",
+ "show_counts_sigma(kikuchi_matrix_symb_sigma)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "54aa64e0-364d-425c-8ec4-f4ce988150cc",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/markdown": [
+ "#### Counts totals:\n",
+ " - `And((0, 0))`: 2\n",
+ " - `And((0, 0))†`: 2\n",
+ " - `And((1, 1))`: 26\n",
+ " - `And((1, 1))†`: 26\n",
+ " - `ArbitraryGate`: $\\displaystyle 4 \\tilde{O}{\\left(24 \\right)} + 2 \\tilde{O}{\\left(165.437600046154 \\right)}$\n",
+ " - `CNOT`: 166\n",
+ " - `H`: 18\n",
+ " - `Rz(-0.3918265520306073π)`: 2\n",
+ " - `Rz(0.3918265520306073π)`: 2\n",
+ " - `TwoBitSwap`: 25\n",
+ " - `XGate`: 28"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qualtran.resource_counting.generalizers import ignore_alloc_free\n",
+ "g, sigma = kikuchi_matrix.call_graph(generalizer=[ignore_split_join, ignore_alloc_free])\n",
+ "show_call_graph(g)\n",
+ "show_counts_sigma(sigma)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0e08ec50",
+ "metadata": {
+ "cq.autogen": "KikuchiMatrixEntry.example_instances.md"
+ },
+ "source": [
+ "### Example Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "71928579",
+ "metadata": {
+ "cq.autogen": "KikuchiMatrixEntry.kikuchi_matrix_entry_symb"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_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": "b34e5549",
+ "metadata": {
+ "cq.autogen": "KikuchiMatrixEntry.kikuchi_matrix_entry"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.kxor_instance import Constraint, KXorInstance\n",
+ "\n",
+ "n, k = 10, 4\n",
+ "cs = (\n",
+ " Constraint((0, 1, 2, 3), -1),\n",
+ " Constraint((0, 2, 4, 5), 1),\n",
+ " Constraint((0, 3, 4, 5), 1),\n",
+ " Constraint((0, 3, 4, 5), 1),\n",
+ " Constraint((1, 2, 3, 4), -1),\n",
+ " Constraint((1, 3, 4, 5), -1),\n",
+ " Constraint((1, 3, 4, 5), -1),\n",
+ " Constraint((2, 3, 4, 5), 1),\n",
+ ")\n",
+ "inst = KXorInstance(n, k, cs)\n",
+ "ell = 8\n",
+ "\n",
+ "kikuchi_matrix_entry = KikuchiMatrixEntry(inst, ell, entry_bitsize=3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1cc785d1",
+ "metadata": {
+ "cq.autogen": "KikuchiMatrixEntry.graphical_signature.md"
+ },
+ "source": [
+ "#### Graphical Signature"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "701a1d9f",
+ "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": "94309337",
+ "metadata": {
+ "cq.autogen": "KikuchiMatrixEntry.call_graph.md"
+ },
+ "source": [
+ "### Call Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a78b1259",
+ "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)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6ad50b81",
+ "metadata": {
+ "cq.autogen": "KikuchiNonZeroIndex.example_instances.md"
+ },
+ "source": [
+ "### Example Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "45907568",
+ "metadata": {
+ "cq.autogen": "KikuchiNonZeroIndex.kikuchi_nonzero_index_symb"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.kxor_instance import KXorInstance\n",
+ "\n",
+ "n, m, k, c, s = sympy.symbols(\"n m k c s\", positive=True, integer=True)\n",
+ "inst = KXorInstance.symbolic(n=n, m=m, k=k)\n",
+ "ell = c * k\n",
+ "\n",
+ "kikuchi_nonzero_index_symb = KikuchiNonZeroIndex(inst, ell, s=s)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "71fb9096",
+ "metadata": {
+ "cq.autogen": "KikuchiNonZeroIndex.kikuchi_nonzero_index"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.kxor_instance import Constraint, KXorInstance\n",
+ "\n",
+ "n, k = 10, 4\n",
+ "cs = (\n",
+ " Constraint((0, 1, 2, 3), -1),\n",
+ " Constraint((0, 2, 4, 5), 1),\n",
+ " Constraint((0, 3, 4, 5), 1),\n",
+ " Constraint((0, 3, 4, 5), 1),\n",
+ " Constraint((1, 2, 3, 4), -1),\n",
+ " Constraint((1, 3, 4, 5), -1),\n",
+ " Constraint((1, 3, 4, 5), -1),\n",
+ " Constraint((2, 3, 4, 5), 1),\n",
+ ")\n",
+ "inst = KXorInstance(n, k, cs)\n",
+ "ell = 8\n",
+ "s = inst.brute_force_sparsity(ell)\n",
+ "\n",
+ "kikuchi_nonzero_index = KikuchiNonZeroIndex(inst, ell, s=s)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "70ae2a8a",
+ "metadata": {
+ "cq.autogen": "KikuchiNonZeroIndex.graphical_signature.md"
+ },
+ "source": [
+ "#### Graphical Signature"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b4178dff",
+ "metadata": {
+ "cq.autogen": "KikuchiNonZeroIndex.graphical_signature.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.drawing import show_bloqs\n",
+ "show_bloqs([kikuchi_nonzero_index_symb, kikuchi_nonzero_index],\n",
+ " ['`kikuchi_nonzero_index_symb`', '`kikuchi_nonzero_index`'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "206645a5",
+ "metadata": {
+ "cq.autogen": "KikuchiNonZeroIndex.call_graph.md"
+ },
+ "source": [
+ "### Call Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "212a5f48",
+ "metadata": {
+ "cq.autogen": "KikuchiNonZeroIndex.call_graph.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.resource_counting.generalizers import ignore_split_join\n",
+ "kikuchi_nonzero_index_symb_g, kikuchi_nonzero_index_symb_sigma = kikuchi_nonzero_index_symb.call_graph(max_depth=1, generalizer=ignore_split_join)\n",
+ "show_call_graph(kikuchi_nonzero_index_symb_g)\n",
+ "show_counts_sigma(kikuchi_nonzero_index_symb_sigma)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding.py b/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding.py
new file mode 100644
index 000000000..48e465436
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding.py
@@ -0,0 +1,251 @@
+# 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.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.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_bitsize
+
+ @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)
+
+
+@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:
+ return SparseMatrixHermitian(
+ col_oracle=self._blackbox_O_F, entry_oracle=self._blackbox_O_H, eps=0
+ )
+
+ @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 _blackbox_O_H(self) -> SqrtEntryOracle:
+ """wrapped to match the interface"""
+ return BlackBoxKikuchiEntryOracle(self.oracle_O_H)
+
+ @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)
+
+ @cached_property
+ def _blackbox_O_F(self) -> RowColumnOracle:
+ """wrapped to match the interface"""
+ return BlackBoxKikuchiRowColumnOracle(self.oracle_O_F)
+
+ @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._blackbox_O_H.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.max_k_xor_sat.kxor_instance import Constraint, KXorInstance
+
+ n, k = 10, 4
+ cs = (
+ Constraint((0, 1, 2, 3), -1),
+ Constraint((0, 2, 4, 5), 1),
+ Constraint((0, 3, 4, 5), 1),
+ Constraint((0, 3, 4, 5), 1),
+ Constraint((1, 2, 3, 4), -1),
+ Constraint((1, 3, 4, 5), -1),
+ Constraint((1, 3, 4, 5), -1),
+ Constraint((2, 3, 4, 5), 1),
+ )
+ inst = KXorInstance(n, k, cs)
+ ell = 8
+
+ kikuchi_matrix = KikuchiHamiltonian(inst, ell, entry_bitsize=10)
+ return kikuchi_matrix
+
+
+@bloq_example
+def _kikuchi_matrix_symb() -> KikuchiHamiltonian:
+ from qualtran.bloqs.max_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, entry_bitsize=10)
+ return kikuchi_matrix_symb
+
+
+_KIKUCHI_HAMILTONIAN_DOC = BloqDocSpec(
+ bloq_cls=KikuchiHamiltonian, examples=[_kikuchi_matrix_symb, _kikuchi_matrix]
+)
diff --git a/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding_test.py b/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding_test.py
new file mode 100644
index 000000000..d056a0fb2
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding_test.py
@@ -0,0 +1,59 @@
+# 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
+
+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():
+ from qualtran.testing import execute_notebook
+
+ 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/max_k_xor_sat/kxor_instance.py b/qualtran/bloqs/max_k_xor_sat/kxor_instance.py
new file mode 100644
index 000000000..913ae06ae
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kxor_instance.py
@@ -0,0 +1,273 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import itertools
+from collections import defaultdict
+from functools import cached_property
+from typing import cast, Sequence, TypeAlias, Union
+
+import numpy as np
+import sympy
+from attrs import evolve, field, frozen
+from numpy.typing import NDArray
+
+from qualtran.symbolics import bit_length, ceil, HasLength, is_symbolic, log2, slen, SymbolicInt
+
+Scope: TypeAlias = Union[tuple[int, ...], HasLength]
+"""A subset of variables"""
+
+
+def _sort_scope(S: Scope) -> Scope:
+ if is_symbolic(S):
+ return S
+ return tuple(sorted(S))
+
+
+@frozen
+class Constraint:
+ """A single kXOR constraint.
+
+ Definition 2.1.
+
+ Note: n, k are not stored here, but only in the instance.
+
+ Attributes:
+ S: the scope - subset of `[n]` of size k.
+ b: +1 or -1.
+ """
+
+ S: Scope = field(converter=_sort_scope)
+ b: SymbolicInt = field()
+
+ @classmethod
+ def random(cls, n: int, k: int, *, rng: np.random.Generator):
+ """Single random constraint, Notation 2.3."""
+ S = tuple(rng.choice(n, k, replace=False))
+ b = rng.choice([-1, +1])
+ return cls(S, b)
+
+ @classmethod
+ def random_planted(cls, n: int, k: int, *, rho: float, z: NDArray, rng: np.random.Generator):
+ """Single planted constraint, Notation 2.4."""
+ S = tuple(rng.choice(n, k, replace=False))
+ eta = (-1) ** (rng.random() < (1 + rho) / 2) # i.e. expectation rho.
+ unplanted = cls(S, 1) # supporting constraint to evaluate z^S
+ b = eta * unplanted.evaluate(z)
+ return cls(S, b)
+
+ @classmethod
+ def symbolic(cls, n: SymbolicInt, ix: int):
+ return cls(HasLength(n), sympy.Symbol(f"b_{ix}"))
+
+ def is_symbolic(self):
+ return is_symbolic(self.S, self.b)
+
+ def evaluate(self, x: NDArray[np.integer]):
+ return np.prod(x[np.array(self.S)])
+
+
+@frozen
+class KXorInstance:
+ r"""A kXOR instance $\mathcal{I}$.
+
+ Definition 2.1: A kXOR instance $\mathcal{I}$ over variables indexed by $[n]$
+ consists of a multiset of constraints $\mathcal{C} = (S, b)$, where each scope
+ $S \subseteq [n]$ has cardinality $k$, and each right-hand side $b \in \{\pm 1\}$.
+
+ Attributes:
+ n: number of variables.
+ k: number of variables per clause.
+ constraints: a tuple of `m` Constraints.
+ max_rhs: maximum value of the RHS polynomial $B_\mathcal{I}(S)$.
+ see default constructor for default value. In case the instance is symbolic,
+ the user can specify an expression for this, to avoid the default value.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Definition 2.1.
+ """
+
+ n: SymbolicInt
+ k: SymbolicInt
+ constraints: Union[tuple[Constraint, ...], HasLength]
+ max_rhs: SymbolicInt = field()
+
+ @max_rhs.default
+ def _default_max_rhs(self):
+ """With very high probability, the max entry will be quite small.
+
+ This is a classical preprocesing step. Time $m$.
+ """
+ if is_symbolic(self.constraints) or is_symbolic(*self.constraints):
+ # user did not provide a value, assume some small constant
+ return 2
+
+ # instance is not symbolic, so we can compute the exact value.
+ assert isinstance(self.batched_scopes, tuple)
+ return max(abs(b) for _, b in self.batched_scopes)
+
+ @cached_property
+ def m(self):
+ return slen(self.constraints)
+
+ @classmethod
+ def random_instance(
+ cls, n: int, m: int, k: int, *, planted_advantage: float = 0, rng: np.random.Generator
+ ):
+ r"""Generate a random kXOR instance with the given planted advantage.
+
+ `planted_advantage=0` generates random instances, and `1` generates a
+ linear system with a solution.
+
+ Args:
+ n: number of variables
+ m: number of clauses
+ k: number of terms per clause
+ planted_advantage: $\rho$
+ rng: random generator
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Notation 2.4.
+ """
+ # planted vector
+ z = rng.choice([-1, +1], size=n)
+
+ # constraints
+ constraints = tuple(
+ Constraint.random_planted(n=n, k=k, rho=planted_advantage, z=z, rng=rng)
+ for _ in range(m)
+ )
+
+ return cls(n=n, k=k, constraints=constraints)
+
+ @classmethod
+ def symbolic(cls, n: SymbolicInt, m: SymbolicInt, k: SymbolicInt, *, max_rhs: SymbolicInt = 2):
+ """Create a symbolic instance with n variables, m constraints."""
+ constraints = HasLength(m)
+ return cls(n=n, k=k, constraints=constraints, max_rhs=max_rhs)
+
+ def is_symbolic(self):
+ if is_symbolic(self.n, self.m, self.k, self.constraints):
+ return True
+ assert isinstance(self.constraints, tuple)
+ return is_symbolic(*self.constraints)
+
+ def subset(self, indices: Union[Sequence[int], HasLength]) -> 'KXorInstance':
+ """Pick a subset of clauses defined by the set of indices provided."""
+ if self.is_symbolic() or is_symbolic(indices):
+ return evolve(self, constraints=HasLength(slen(indices)))
+ assert isinstance(self.constraints, tuple)
+
+ constraints = tuple(self.constraints[i] for i in indices)
+ return evolve(self, constraints=constraints)
+
+ @cached_property
+ def index_bitsize(self):
+ """number of bits required to represent the index of a variable, i.e. `[n]`
+
+ We assume zero-indexing.
+ """
+ return ceil(log2(self.n))
+
+ @cached_property
+ def num_unique_constraints(self) -> SymbolicInt:
+ return slen(self.batched_scopes)
+
+ @cached_property
+ def batched_scopes(self) -> Union[tuple[tuple[Scope, int], ...], HasLength]:
+ r"""Group all the constraints by Scope, and add up the $b$ values.
+
+ This is a classical preprocessing step. Time $k m \log m$.
+ """
+ if self.is_symbolic():
+ return HasLength(self.m)
+
+ assert isinstance(self.constraints, tuple)
+
+ batches: dict[Scope, int] = defaultdict(lambda: 0)
+ for con in self.constraints:
+ assert isinstance(con.S, tuple)
+ batches[con.S] += con.b
+
+ batches_sorted = sorted(batches.items(), key=lambda c: c[1])
+ return tuple(batches_sorted)
+
+ @cached_property
+ def rhs_sum_bitsize(self):
+ r"""number of bits to represent the RHS polynomial $B_{\mathcal{I}}(S)$."""
+ return bit_length(2 * self.max_rhs)
+
+ def scope_as_int(self, S: Scope) -> int:
+ r"""Convert a scope into a single integer.
+
+ Given a scope `S = (x_1, x_2, ..., x_k)`, and a bitsize `r` for each index,
+ the integer representation is given by concatenating `r`-bit unsigned repr
+ of each `x_i`. That is, $\sum_i r^{k - i} x_i$.
+
+ This uses Big-endian representation, like all qualtran dtypes.
+
+ The bitsize `r` is picked as `ceil(log(n))` for an n-variable instance.
+ """
+ assert not is_symbolic(S)
+
+ bitsize = self.index_bitsize
+
+ result = 0
+ for x in S:
+ result = (result << bitsize) + x
+ return result
+
+ def brute_force_sparsity(self, ell: int) -> int:
+ r"""Compute the sparsity of the Kikuchi matrix with parameter $\ell$ by brute force.
+
+ Takes time `O(C(n, l) * m * l)`. Extremely slow, use with caution.
+ """
+ assert isinstance(self.n, int)
+ s = 0
+ for S in itertools.combinations(range(self.n), ell):
+ nz = 0
+ for U, _ in cast(tuple, self.batched_scopes):
+ T = set(S).symmetric_difference(U)
+ if len(T) == ell:
+ nz += 1
+ s = max(s, nz)
+ return s
+
+
+def example_kxor_instance() -> KXorInstance:
+ n, k = 10, 4
+ cs = (
+ Constraint((0, 1, 2, 3), -1),
+ Constraint((0, 2, 4, 5), 1),
+ Constraint((0, 3, 4, 5), 1),
+ Constraint((0, 3, 4, 5), 1),
+ Constraint((1, 2, 3, 4), -1),
+ Constraint((1, 3, 4, 5), -1),
+ Constraint((1, 3, 4, 5), -1),
+ Constraint((2, 3, 4, 5), 1),
+ )
+ inst = KXorInstance(n, k, cs)
+ return inst
diff --git a/qualtran/bloqs/max_k_xor_sat/kxor_instance_test.py b/qualtran/bloqs/max_k_xor_sat/kxor_instance_test.py
new file mode 100644
index 000000000..700dfd6eb
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kxor_instance_test.py
@@ -0,0 +1,30 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import numpy as np
+import pytest
+
+from .kxor_instance import KXorInstance
+
+
+@pytest.mark.slow
+@pytest.mark.parametrize("rho", [0, 0.8, 0.9])
+def test_max_rhs(rho: float):
+ rng = np.random.default_rng(402)
+
+ rhs = []
+ for i in range(100):
+ inst = KXorInstance.random_instance(n=100, m=1000, k=4, planted_advantage=rho, rng=rng)
+ rhs.append(inst.max_rhs)
+
+ assert max(rhs) == 2
diff --git a/qualtran/bloqs/max_k_xor_sat/load_kxor_instance.ipynb b/qualtran/bloqs/max_k_xor_sat/load_kxor_instance.ipynb
new file mode 100644
index 000000000..21fe47dd3
--- /dev/null
+++ b/qualtran/bloqs/max_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.max_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.max_k_xor_sat.kxor_instance import KXorInstance\n",
+ "\n",
+ "n, m, k = sympy.symbols(\"n m k\", positive=True, integer=True)\n",
+ "inst = KXorInstance.symbolic(n=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.max_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/max_k_xor_sat/load_kxor_instance.py b/qualtran/bloqs/max_k_xor_sat/load_kxor_instance.py
new file mode 100644
index 000000000..a3b0db3bd
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/load_kxor_instance.py
@@ -0,0 +1,352 @@
+# 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, Optional, Sequence, Union
+
+import numpy as np
+from attrs import evolve, 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.max_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.max_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
+ cv: Optional[int] = None
+
+ @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.cv is not None 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.bloq_with_specialized_single_qubit_control import (
+ get_ctrl_system_for_bloq_with_specialized_single_qubit_control,
+ )
+
+ return get_ctrl_system_for_bloq_with_specialized_single_qubit_control(self, ctrl_spec)
+
+ def with_cv(self, *, cv: Optional[int]) -> 'Bloq':
+ return evolve(self, cv=cv)
diff --git a/qualtran/bloqs/max_k_xor_sat/load_kxor_instance_test.py b/qualtran/bloqs/max_k_xor_sat/load_kxor_instance_test.py
new file mode 100644
index 000000000..97f833b45
--- /dev/null
+++ b/qualtran/bloqs/max_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)
diff --git a/qualtran/bloqs/max_k_xor_sat/planted_noisy_kxor.ipynb b/qualtran/bloqs/max_k_xor_sat/planted_noisy_kxor.ipynb
new file mode 100644
index 000000000..d42537de5
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/planted_noisy_kxor.ipynb
@@ -0,0 +1,235 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "ee412c69",
+ "metadata": {
+ "cq.autogen": "title_cell"
+ },
+ "source": [
+ "# Algorithm: Planted Noise kXOR"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "df5a7866",
+ "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": "36559e8d",
+ "metadata": {
+ "cq.autogen": "PlantedNoisyKXOR.bloq_doc.md"
+ },
+ "source": [
+ "## `PlantedNoisyKXOR`\n",
+ "Algorithm for Planted Noisy kXOR.\n",
+ "\n",
+ "Problem (Problem 2.6 of Ref [1]):\n",
+ "\n",
+ "Given a noisy-kXOR instance $\\hat{\\mathcal{I}}$ which is drawn either:\n",
+ "\n",
+ "1. with planted advantage $\\rho$, from $\\tilde\\mathcal{P}^{z}_{n, k}(m, \\rho)$.\n",
+ "2. at random, from $\\tilde\\mathcal{R}_{n, k}(m)$.\n",
+ "\n",
+ "output a single bit such that it is whp `1` in case 1, and `0` in case 2.\n",
+ "\n",
+ "Algorithm (Section 4.4, Theorem 4.18):\n",
+ "We first split the instance into $\\hat{\\mathcal{I}} = \\mathcal{I} \\cup \\mathcal{I}_\\text{guide}$,\n",
+ "by placing each constraint independently in $\\mathcal{I}$ with prob. $1 - \\zeta$,\n",
+ "otherwise in $\\mathcal{I}_\\text{guide}$.\n",
+ "$\\zeta$ is picked to be $1 / \\ln n$.\n",
+ "\n",
+ "#### Parameters\n",
+ " - `inst_guide`: The subset of contraints $\\mathcal{I}_\\text{guide}$ for the guided state.\n",
+ " - `inst_solve`: The subset of constraints $\\mathcal{I}$ for eigenvalue estimation.\n",
+ " - `ell`: Kikuchi parameter $\\ell$.\n",
+ " - `rho`: the planted advantage $\\rho$ in the planted case. \n",
+ "\n",
+ "#### References\n",
+ " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). \n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f930da5f",
+ "metadata": {
+ "cq.autogen": "PlantedNoisyKXOR.bloq_doc.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import PlantedNoisyKXOR"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "18f43abd",
+ "metadata": {
+ "cq.autogen": "PlantedNoisyKXOR.example_instances.md"
+ },
+ "source": [
+ "### Example Instances"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1a28aa9c",
+ "metadata": {
+ "cq.autogen": "PlantedNoisyKXOR.solve_planted_symbolic"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import KXorInstance\n",
+ "from qualtran.symbolics import HasLength\n",
+ "\n",
+ "n, m = sympy.symbols(\"n m\", positive=True, integer=True)\n",
+ "k = sympy.symbols(\"k\", positive=True, integer=True, even=True)\n",
+ "c = sympy.symbols(\"c\", positive=True, integer=True)\n",
+ "ell = c * k\n",
+ "rho = sympy.Symbol(r\"\\rho\", positive=True, real=True)\n",
+ "\n",
+ "inst = KXorInstance.symbolic(n, m, k)\n",
+ "zeta = 1 / ln(n)\n",
+ "solve_planted_symbolic = PlantedNoisyKXOR(\n",
+ " inst_guide=inst.subset(HasLength((1 - zeta) * m)),\n",
+ " inst_solve=inst.subset(HasLength((zeta) * m)),\n",
+ " ell=ell,\n",
+ " rho=rho,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2df9b4ea",
+ "metadata": {
+ "cq.autogen": "PlantedNoisyKXOR.solve_planted"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import KXorInstance\n",
+ "\n",
+ "rng = np.random.default_rng(42)\n",
+ "n, m, k = 50, 1000, 4\n",
+ "ell = k\n",
+ "rho = 0.8\n",
+ "\n",
+ "inst = KXorInstance.random_instance(n, m, k, planted_advantage=rho, rng=rng)\n",
+ "solve_planted = PlantedNoisyKXOR.from_inst(inst, ell=ell, rho=rho, zeta=0.1, rng=rng)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "505c1e9c",
+ "metadata": {
+ "cq.autogen": "PlantedNoisyKXOR.graphical_signature.md"
+ },
+ "source": [
+ "#### Graphical Signature"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2ead66fe",
+ "metadata": {
+ "cq.autogen": "PlantedNoisyKXOR.graphical_signature.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.drawing import show_bloqs\n",
+ "show_bloqs([solve_planted_symbolic, solve_planted],\n",
+ " ['`solve_planted_symbolic`', '`solve_planted`'])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bd068fb3",
+ "metadata": {
+ "cq.autogen": "PlantedNoisyKXOR.call_graph.md"
+ },
+ "source": [
+ "### Call Graph"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "946fca15",
+ "metadata": {
+ "cq.autogen": "PlantedNoisyKXOR.call_graph.py"
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.resource_counting.generalizers import ignore_split_join\n",
+ "solve_planted_symbolic_g, solve_planted_symbolic_sigma = solve_planted_symbolic.call_graph(max_depth=1, generalizer=ignore_split_join)\n",
+ "show_call_graph(solve_planted_symbolic_g)\n",
+ "show_counts_sigma(solve_planted_symbolic_sigma)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "2c8e9045-060a-4386-99be-5af7ee653fd6",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/markdown": [
+ "#### Counts totals:\n",
+ " - `Adjoint(subbloq=GuidedHamiltonianPhaseEstimation)`: $\\displaystyle \\left\\lceil{\\frac{202.020202020202 c^{0.5} k^{0.5} \\left(c k\\right)^{\\frac{c k}{2}}}{Part_{k}(\\ell)^{0.5} \\rho^{0.5} \\left(\\frac{\\left(\\frac{m \\left(1 - \\frac{1}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) + \\frac{m}{\\operatorname{log}_{2}{\\left(n \\right)}}}{{\\binom{n}{k}}}\\right)^{c} \\left(\\frac{\\rho^{2} m}{\\left(m \\left(1 - \\frac{1}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) + \\frac{m}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) \\operatorname{log}_{2}{\\left(n \\right)}}\\right)^{c}}{\\log{\\left(n \\right)}^{2}}\\right)^{0.5}}}\\right\\rceil$\n",
+ " - `GuidedHamiltonianPhaseEstimation`: $\\displaystyle \\left\\lceil{\\frac{202.020202020202 c^{0.5} k^{0.5} \\left(c k\\right)^{\\frac{c k}{2}}}{Part_{k}(\\ell)^{0.5} \\rho^{0.5} \\left(\\frac{\\left(\\frac{m \\left(1 - \\frac{1}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) + \\frac{m}{\\operatorname{log}_{2}{\\left(n \\right)}}}{{\\binom{n}{k}}}\\right)^{c} \\left(\\frac{\\rho^{2} m}{\\left(m \\left(1 - \\frac{1}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) + \\frac{m}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) \\operatorname{log}_{2}{\\left(n \\right)}}\\right)^{c}}{\\log{\\left(n \\right)}^{2}}\\right)^{0.5}}}\\right\\rceil + 1$\n",
+ " - `MultiControlZ`: $\\displaystyle \\left\\lceil{\\frac{202.020202020202 c^{0.5} k^{0.5} \\left(c k\\right)^{\\frac{c k}{2}}}{Part_{k}(\\ell)^{0.5} \\rho^{0.5} \\left(\\frac{\\left(\\frac{m \\left(1 - \\frac{1}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) + \\frac{m}{\\operatorname{log}_{2}{\\left(n \\right)}}}{{\\binom{n}{k}}}\\right)^{c} \\left(\\frac{\\rho^{2} m}{\\left(m \\left(1 - \\frac{1}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) + \\frac{m}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) \\operatorname{log}_{2}{\\left(n \\right)}}\\right)^{c}}{\\log{\\left(n \\right)}^{2}}\\right)^{0.5}}}\\right\\rceil$\n",
+ " - `ZGate`: $\\displaystyle \\left\\lceil{\\frac{202.020202020202 c^{0.5} k^{0.5} \\left(c k\\right)^{\\frac{c k}{2}}}{Part_{k}(\\ell)^{0.5} \\rho^{0.5} \\left(\\frac{\\left(\\frac{m \\left(1 - \\frac{1}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) + \\frac{m}{\\operatorname{log}_{2}{\\left(n \\right)}}}{{\\binom{n}{k}}}\\right)^{c} \\left(\\frac{\\rho^{2} m}{\\left(m \\left(1 - \\frac{1}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) + \\frac{m}{\\operatorname{log}_{2}{\\left(n \\right)}}\\right) \\operatorname{log}_{2}{\\left(n \\right)}}\\right)^{c}}{\\log{\\left(n \\right)}^{2}}\\right)^{0.5}}}\\right\\rceil$"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "_, sigma = solve_planted_symbolic.call_graph(max_depth=2, generalizer=ignore_split_join)\n",
+ "show_counts_sigma(sigma) # inverse of Eq. 150"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/qualtran/bloqs/max_k_xor_sat/planted_noisy_kxor.py b/qualtran/bloqs/max_k_xor_sat/planted_noisy_kxor.py
new file mode 100644
index 000000000..3dd4078b4
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/planted_noisy_kxor.py
@@ -0,0 +1,424 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from functools import cached_property
+from typing import Optional
+
+import numpy as np
+import scipy
+import sympy
+from attrs import field, frozen
+
+from qualtran import Bloq, bloq_example, BloqBuilder, BloqDocSpec, Signature, SoquetT
+from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare
+from qualtran.bloqs.state_preparation.prepare_base import PrepareOracle
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import (
+ ceil,
+ HasLength,
+ is_symbolic,
+ ln,
+ log2,
+ prod,
+ slen,
+ ssqrt,
+ SymbolicFloat,
+ SymbolicInt,
+)
+
+from .guided_hamiltonian import GuidedHamiltonian
+from .guiding_state import GuidingState, SimpleGuidingState
+from .kikuchi_block_encoding import KikuchiHamiltonian
+from .kxor_instance import KXorInstance
+
+
+def comb(n: SymbolicInt, k: SymbolicInt) -> SymbolicInt:
+ """compute n choose k"""
+ if is_symbolic(n, k):
+ return sympy.binomial(n, k)
+ return scipy.special.comb(n, k)
+
+
+@frozen(kw_only=True)
+class KikuchiAverageDegreeTheorem:
+ """Compute the average degree of the Kikuchi matrix.
+
+ The Alice theorem (Thm 2.21) guarantees this with high probability
+ for random/planted instances.
+ """
+
+ n: SymbolicInt
+ k: SymbolicInt
+ ell: SymbolicInt
+
+ @cached_property
+ def delta(self) -> SymbolicFloat:
+ """Eq 19"""
+ n, k, l = self.n, self.k, self.ell
+ term_1 = comb(k, k // 2)
+ term_2 = comb(n - k, l - k // 2) / comb(n, l)
+ return term_1 * term_2
+
+
+@frozen(kw_only=True)
+class AliceTheorem:
+ r"""Alice theorem, E.g. Theorem 2.21.
+
+ Consider a $k$XOR instance over $n$ variables and $m$ clauses, with Kikuchi parameter $\ell$.
+
+ Assume:
+ - $\ell \ge k/2$
+ - $n \gg k \ell$
+
+ For any parameters $\kappa \le 1$ and $0 < \epsilon \le \kappa/(2+\kappa)$,
+ assume: $m$ satisfies
+ $m/n \ge C_\kappa (n/\ell)^{(k-2)/2}$
+ where
+ $C_\kappa = 2(1+\epsilon)(1+\kappa) \kappa^{-2} {k \choose k/2}^{-1} \ln n$
+
+ Then for a randomly drawn instance $\mathcal{I}$ (i.e. advantage 0),
+ except with probability $3 n^{-\epsilon \ell}$, we are guaranteed:
+
+ $\lambda_\max{\mathcal{K}_\ell(\mathcal{I})} \le \kappa d$
+ where
+ $d = \delta_{\ell,n,k} m$.
+
+ Args:
+ n: number of variables $n$
+ k: number of variables per equation $k$
+ ell: Kikuchi parameter
+ kappa: parameter $\kappa$
+ eps: parameter $\epsilon$
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ """
+
+ n: int
+ k: int
+ ell: int
+ kappa: float
+ eps: float = field()
+
+ @eps.default
+ def _default_max_eps(self):
+ return self.kappa / (2 + self.kappa)
+
+ def __attrs_post_init__(self):
+ assert self.k % 2 == 0, "k must be even"
+ assert self.ell % self.k == 0, "l must be a multiple of k"
+ # assert self.n >= self.k * self.ell, "n must be atleast lk"
+ assert 0 <= self.kappa <= 1
+ assert 0 < self.eps <= self.kappa / (2 + self.kappa)
+
+ @cached_property
+ def C_kappa(self):
+ """Eq 20 (right)"""
+ term_1 = 2 * (1 + self.eps) * (1 + self.kappa) / self.kappa**2
+ term_2 = comb(self.k, self.k // 2)
+ term_3 = np.log(self.n)
+
+ value = (term_1 / term_2) * term_3
+ return value
+
+ @cached_property
+ def fail_prob(self):
+ return 3 / self.n ** (self.eps * self.ell)
+
+ @cached_property
+ def min_m(self):
+ """Eq 20 (left)"""
+ m = self.C_kappa * (self.n / self.ell) ** (self.k // 2) * self.ell
+ return ceil(m)
+
+
+@frozen(kw_only=True)
+class GuidingStateOverlapTheorem:
+ r"""Lower-bound the overlap of the prepared guiding state with the eigenspace.
+
+ This is an implementation of Theorem 2.40.
+
+ Args:
+ n: number of variables
+ k: number of variables per constraint
+ m_hat: total number of constraints
+ ell: kikuchi parameter $\ell$
+ zeta: the probability of picking a constraint for $\mathcal{I}_\text{guide}$.
+ nu: parameter in $(0, .99]$.
+ eps: parameter.
+ rho: planted advantage.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Section 2.7, Theorem 2.40.
+ """
+
+ n: SymbolicInt
+ k: SymbolicInt
+ m_hat: SymbolicInt
+ ell: SymbolicInt
+ zeta: SymbolicFloat
+ nu: SymbolicFloat
+ eps: SymbolicFloat
+ rho: SymbolicFloat
+
+ @cached_property
+ def part_k_l(self) -> SymbolicInt:
+ ell, k = self.ell, self.k
+ if is_symbolic(ell) or is_symbolic(k):
+ return sympy.Symbol(r"Part_{k}(\ell)", positive=True, integer=True)
+ return prod([comb(ell - i * k, k) for i in range(ell // k)])
+
+ @cached_property
+ def xi(self):
+ r"""Eq 60 $\xi$"""
+ term_1 = self.part_k_l
+ term_2 = (self.rho * self.eps * self.nu) / (200 * self.ell * ln(self.n))
+ term_3 = (self.rho**2 * self.zeta) ** (self.ell // self.k)
+ return term_1 * term_2 * term_3
+
+ @cached_property
+ def overlap_probability(self) -> SymbolicFloat:
+ term_2_base = self.m_hat / comb(self.n, self.k)
+ term_2 = term_2_base ** (self.ell / self.k)
+ return self.xi * term_2
+
+
+@frozen
+class PlantedNoisyKXOR(Bloq):
+ r"""Algorithm for Planted Noisy kXOR.
+
+ Problem (Problem 2.6 of Ref [1]):
+
+ Given a noisy-kXOR instance $\hat{\mathcal{I}}$ which is drawn either:
+
+ 1. with planted advantage $\rho$, from $\tilde\mathcal{P}^{z}_{n, k}(m, \rho)$.
+ 2. at random, from $\tilde\mathcal{R}_{n, k}(m)$.
+
+ output a single bit such that it is whp `1` in case 1, and `0` in case 2.
+
+ Algorithm (Section 4.4, Theorem 4.18):
+ We first split the instance into $\hat{\mathcal{I}} = \mathcal{I} \cup \mathcal{I}_\text{guide}$,
+ by placing each constraint independently in $\mathcal{I}$ with prob. $1 - \zeta$,
+ otherwise in $\mathcal{I}_\text{guide}$.
+ $\zeta$ is picked to be $1 / \ln n$.
+
+ Args:
+ inst_guide: The subset of contraints $\mathcal{I}_\text{guide}$ for the guided state.
+ inst_solve: The subset of constraints $\mathcal{I}$ for eigenvalue estimation.
+ ell: Kikuchi parameter $\ell$.
+ rho: the planted advantage $\rho$ in the planted case.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ """
+
+ inst_guide: KXorInstance
+ inst_solve: KXorInstance
+ ell: SymbolicInt
+ rho: SymbolicFloat
+ _guiding_state_overlap: Optional[SymbolicFloat] = field(kw_only=True, default=None)
+
+ def __attrs_post_init__(self):
+ k = self.inst_guide.k
+ if not is_symbolic(k):
+ assert k % 2 == 0, f"{k=} must be even"
+
+ ell = self.ell
+ if not is_symbolic(k, ell):
+ assert ell % k == 0 and ell >= k, f"{ell=} must be a multiple of {k=}"
+
+ @cached_property
+ def signature(self) -> 'Signature':
+ return self.guided_hamiltonian_bloq.signature
+
+ @classmethod
+ def from_inst(
+ cls,
+ inst: KXorInstance,
+ ell: SymbolicInt,
+ rho: SymbolicFloat,
+ *,
+ rng: np.random.Generator,
+ zeta: Optional[SymbolicFloat] = None,
+ guiding_state_overlap: Optional[SymbolicFloat] = None,
+ ):
+ if zeta is None:
+ zeta = 1 / log2(inst.n)
+
+ (use_for_guide,) = np.nonzero(np.atleast_1d(rng.random(inst.m) < zeta))
+ inst_guide = inst.subset(tuple(use_for_guide))
+
+ if is_symbolic(inst, use_for_guide):
+ inst_solve = inst.subset(HasLength(inst.m - slen(use_for_guide)))
+ else:
+ mask = np.ones(inst.m)
+ mask[np.array(use_for_guide)] = 0
+ (rest,) = np.nonzero(mask)
+ inst_solve = inst.subset(tuple(rest))
+
+ return cls(
+ inst_guide=inst_guide,
+ inst_solve=inst_solve,
+ ell=ell,
+ rho=rho,
+ guiding_state_overlap=guiding_state_overlap,
+ )
+
+ @cached_property
+ def guiding_state_and_coefficient(self) -> tuple[PrepareOracle, SymbolicFloat]:
+ r"""Return a bloq that prepares the guiding state, and its coefficient.
+
+ If the bloq prepares $\beta |\Psi\rangle|0\rangle + |\perp\rangle|1\rangle$,
+ then this will return $|\beta|$.
+
+ The returned $\beta$ is an theoretical lower bound on the true value,
+ and is correct for $1 - o(1)$ fraction of random instances.
+ """
+ if self.ell == self.inst_guide.k:
+ return SimpleGuidingState(inst=self.inst_guide), 1
+ bloq = GuidingState(inst=self.inst_guide, ell=self.ell)
+ return bloq, bloq.amplitude_good_part
+
+ @cached_property
+ def guiding_state_overlap_guarantee(self) -> GuidingStateOverlapTheorem:
+ """Invoke Theorem 2.40 to obtain a lower bound on the guiding state overlap.
+
+ The below parameters are picked from Theorem 4.18, proof para 2.
+ """
+ n, k = self.inst_guide.n, self.inst_guide.k
+ m_guide = self.inst_guide.m
+ m_solve = self.inst_solve.m
+ m_hat = m_guide + m_solve
+ zeta = m_solve / m_hat
+ return GuidingStateOverlapTheorem(
+ n=n, k=k, ell=self.ell, m_hat=m_hat, zeta=zeta, nu=1 / ln(n), eps=0.005, rho=self.rho
+ )
+
+ @cached_property
+ def guiding_state_overlap(self) -> SymbolicFloat:
+ if self._guiding_state_overlap is not None:
+ return self.guiding_state_overlap
+ _, guiding_state_good_coeff = self.guiding_state_and_coefficient
+ return guiding_state_good_coeff
+
+ @cached_property
+ def overlap(self) -> SymbolicFloat:
+ # guiding state
+ guiding_state_good_coeff = self.guiding_state_overlap
+
+ # overlap of |\Gamma(A)> with the threshold eigenspace
+ overlap_good_eigen = self.guiding_state_overlap_guarantee.overlap_probability**0.5
+
+ # total overlap is the sqrt probability of the ancilla being 0,
+ # and the state being in the >= lambda eigenspace.
+ overlap = guiding_state_good_coeff * overlap_good_eigen
+
+ return overlap
+
+ @cached_property
+ def degree_guarantee(self) -> KikuchiAverageDegreeTheorem:
+ return KikuchiAverageDegreeTheorem(n=self.inst_solve.n, k=self.inst_solve.k, ell=self.ell)
+
+ @cached_property
+ def sparsity(self) -> SymbolicInt:
+ """sparsity of the kikuchi matrix, $d$"""
+ # d = \delta m
+ d = self.degree_guarantee.delta * self.inst_solve.m
+ if is_symbolic(d):
+ return d # type: ignore
+ return ceil(d)
+
+ @cached_property
+ def hamiltonian(self) -> KikuchiHamiltonian:
+ return KikuchiHamiltonian(
+ inst=self.inst_solve, ell=self.ell, entry_bitsize=10, s=self.sparsity
+ )
+
+ @cached_property
+ def guided_hamiltonian_bloq(self) -> GuidedHamiltonian:
+ # Thm 4.18 proof para 2.
+ # lambda = 0.995 rho d
+ eigenvalue_threshold = 0.995 * self.rho * self.sparsity
+
+ kappa = 0.99 * self.rho
+ eps = 0.005
+
+ # Thm 4.18 proof para 3
+ # kappa' <= (1 - alpha) lambda
+ # ==> alpha <= 1 - kappa'/lambda
+ # we pick kappa' s.t. it satisfies the alice theorem for inst_solve.m
+ # simple approximation: kappa' = kappa / sqrt(1-zeta)
+ zeta = self.inst_guide.m / (self.inst_guide.m + self.inst_solve.m)
+ kappa_prime = kappa / ssqrt(1 - zeta)
+ alpha = 1 - kappa_prime / eigenvalue_threshold
+ if not is_symbolic(alpha):
+ assert alpha > 0, f"got negative {alpha=}"
+
+ guiding_state, _ = self.guiding_state_and_coefficient
+
+ return GuidedHamiltonian(
+ self.hamiltonian,
+ BlackBoxPrepare(guiding_state),
+ lambd=eigenvalue_threshold,
+ alpha=alpha,
+ gamma=self.overlap,
+ )
+
+ def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> dict[str, 'SoquetT']:
+ soqs = bb.add_d(self.guided_hamiltonian_bloq, **soqs)
+ return soqs
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
+ return {self.guided_hamiltonian_bloq: 1}
+
+
+@bloq_example
+def _solve_planted() -> PlantedNoisyKXOR:
+ from qualtran.bloqs.max_k_xor_sat import KXorInstance
+
+ rng = np.random.default_rng(42)
+ n, m, k = 50, 1000, 4
+ ell = k
+ rho = 0.8
+
+ inst = KXorInstance.random_instance(n, m, k, planted_advantage=rho, rng=rng)
+ solve_planted = PlantedNoisyKXOR.from_inst(inst, ell=ell, rho=rho, zeta=0.1, rng=rng)
+ return solve_planted
+
+
+@bloq_example
+def _solve_planted_symbolic() -> PlantedNoisyKXOR:
+ from qualtran.bloqs.max_k_xor_sat import KXorInstance
+ from qualtran.symbolics import HasLength
+
+ n, m = sympy.symbols("n m", positive=True, integer=True)
+ k = sympy.symbols("k", positive=True, integer=True, even=True)
+ c = sympy.symbols("c", positive=True, integer=True)
+ ell = c * k
+ rho = sympy.Symbol(r"\rho", positive=True, real=True)
+
+ inst = KXorInstance.symbolic(n, m, k)
+ zeta = 1 / ln(n)
+ solve_planted_symbolic = PlantedNoisyKXOR(
+ inst_guide=inst.subset(HasLength((1 - zeta) * m)),
+ inst_solve=inst.subset(HasLength((zeta) * m)),
+ ell=ell,
+ rho=rho,
+ )
+ return solve_planted_symbolic
+
+
+_PLANTED_NOISY_KXOR_DOC = BloqDocSpec(
+ bloq_cls=PlantedNoisyKXOR, examples=[_solve_planted_symbolic, _solve_planted]
+)
diff --git a/qualtran/bloqs/max_k_xor_sat/planted_noisy_kxor_test.py b/qualtran/bloqs/max_k_xor_sat/planted_noisy_kxor_test.py
new file mode 100644
index 000000000..85b164294
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/planted_noisy_kxor_test.py
@@ -0,0 +1,112 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import numpy as np
+import pytest
+import sympy
+
+from qualtran.drawing import show_call_graph
+from qualtran.resource_counting import get_cost_value, QECGatesCost
+from qualtran.resource_counting.generalizers import ignore_alloc_free, ignore_split_join
+from qualtran.symbolics import ln, log2
+
+from .kxor_instance import KXorInstance
+from .planted_noisy_kxor import (
+ _solve_planted,
+ _solve_planted_symbolic,
+ AliceTheorem,
+ PlantedNoisyKXOR,
+)
+
+
+@pytest.fixture
+def rng():
+ return np.random.default_rng(42)
+
+
+@pytest.mark.xfail
+def test_alice_thm_symb():
+ n, m = sympy.symbols("n m", positive=True, integer=True)
+ k = sympy.symbols("k", positive=True, integer=True, even=True)
+ rho = sympy.symbols(r"\rho", positive=True, real=True)
+ c = sympy.symbols(r"c", positive=True, integer=True)
+ thm = AliceTheorem(n=n, k=k, ell=c * k, kappa=0.99 * rho, eps=0.005)
+ _ = thm.C_kappa()
+ _ = thm.min_m()
+ _ = thm.fail_prob()
+
+
+@pytest.mark.parametrize("bloq_ex", [_solve_planted, _solve_planted_symbolic])
+def test_examples(bloq_autotester, bloq_ex):
+ if bloq_autotester.check_name == 'serialize':
+ pytest.skip()
+
+ bloq_autotester(bloq_ex)
+
+
+def test_call_graph():
+ _solve_planted().call_graph()
+
+
+def test_call_graph_symb():
+ algo = _solve_planted_symbolic()
+ g, sigma = algo.call_graph(generalizer=[ignore_split_join, ignore_alloc_free])
+ show_call_graph(g)
+
+
+def example_random_instance(*, k=4, n=100, m=1000, c=2, rho=0.8, seed=120) -> PlantedNoisyKXOR:
+ # generate instance
+ rng = np.random.default_rng(seed)
+ ell = c * k
+ inst = KXorInstance.random_instance(n=n, m=m, k=k, planted_advantage=rho, rng=rng)
+ algo_bloq = PlantedNoisyKXOR.from_inst(inst=inst, ell=ell, rho=rho, zeta=1 / ln(n), rng=rng)
+
+ return algo_bloq
+
+
+def test_gate_cost():
+ bloq = example_random_instance()
+ gc = get_cost_value(bloq, QECGatesCost())
+ t_cost = gc.total_t_count(ts_per_cswap=4)
+
+ n = bloq.inst_guide.n
+ m = bloq.inst_guide.m + bloq.inst_solve.m
+ ell = bloq.ell
+ c = ell // bloq.inst_guide.k
+
+ big_O_expected = n ** (ell / 4) * (m**0.5) * ell**ell * log2(n) ** (c // 2)
+ print()
+ print(t_cost)
+ print(t_cost / big_O_expected)
+ print(big_O_expected)
+ print(t_cost / big_O_expected * bloq.guiding_state_overlap)
+ print(1 / bloq.guiding_state_overlap)
+ print(1 / bloq.guiding_state_overlap_guarantee.overlap_probability**0.5)
+
+
+@pytest.mark.parametrize("n", [40, 50, 60, 70, 80, 90, 100])
+@pytest.mark.parametrize("k", [4, 8])
+@pytest.mark.parametrize("c", [2, 3, 4])
+def test_more_costs(n, k, c):
+ bloq = example_random_instance(k=k, c=c, n=n, m=n, seed=142)
+ cost = get_cost_value(bloq, QECGatesCost())
+ print(cost)
+
+
+@pytest.mark.parametrize("n", [10**4, 10**5])
+def test_large(n):
+ k = 4
+ c = 32 // 4
+ bloq = example_random_instance(k=k, c=c, n=n, m=n * 10, seed=142)
+ cost = get_cost_value(bloq, QECGatesCost())
+ print(cost)
diff --git a/qualtran/bloqs/max_k_xor_sat/resource/__init__.py b/qualtran/bloqs/max_k_xor_sat/resource/__init__.py
new file mode 100644
index 000000000..b7a8a3022
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/resource/__init__.py
@@ -0,0 +1,14 @@
+# 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 .phase_gradient import AcquirePhaseGradient, ReleasePhaseGradient
diff --git a/qualtran/bloqs/max_k_xor_sat/resource/phase_gradient.py b/qualtran/bloqs/max_k_xor_sat/resource/phase_gradient.py
new file mode 100644
index 000000000..05c9683c4
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/resource/phase_gradient.py
@@ -0,0 +1,137 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import Optional, TYPE_CHECKING
+
+import attrs
+import numpy as np
+
+from qualtran import Bloq, ConnectionT, DecomposeTypeError, QDType, QFxp, Register, Side, Signature
+from qualtran.bloqs.bookkeeping._bookkeeping_bloq import _BookkeepingBloq
+from qualtran.bloqs.rotations import PhaseGradientState
+from qualtran.resource_counting.generalizers import _ignore_wrapper
+from qualtran.symbolics import is_symbolic, SymbolicInt
+
+if TYPE_CHECKING:
+ import quimb.tensor as qtn
+
+
+@attrs.frozen
+class AcquirePhaseGradient(_BookkeepingBloq):
+ r"""Acquire a phase gradient state, used to perform rotations.
+
+ This is treated as a book-keeping bloq at the moment, because it should
+ be accounted for as a one-time preparation cost.
+ If we directly used allocations of `PhaseGradientState` each time in
+ a large algorithm, their costs would add up, which is incorrect.
+
+ To capture the required resource costs of an algorithm, we can use a separate
+ cost key that only counts this bloq, and takes the max phase gradient size
+ and computes the cost to synthesize it.
+
+ Args:
+ bitsize: number of bits of the phase gradient $b_\text{grad}$.
+ eps: the precision to synthesize the state.
+
+ Registers:
+ phase_grad: A phase gradient state of `bitsize`, $|\phi_\text{grad}\rangle$.
+ """
+
+ bitsize: SymbolicInt
+ eps: float = 1e-10
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature([Register('phase_grad', self.phase_dtype, side=Side.RIGHT)])
+
+ @property
+ def phase_dtype(self) -> QDType:
+ return QFxp(self.bitsize, self.bitsize)
+
+ def decompose_bloq(self):
+ raise DecomposeTypeError(f"{self} is atomic")
+
+ def my_tensors(
+ self, incoming: dict[str, 'ConnectionT'], outgoing: dict[str, 'ConnectionT']
+ ) -> list['qtn.Tensor']:
+ import quimb.tensor as qtn
+
+ if is_symbolic(self.bitsize):
+ raise DecomposeTypeError(f"cannot compute tensors for symbolic {self}")
+
+ return [
+ qtn.Tensor(
+ data=np.array([1, np.exp(-1j * np.pi / 2**i)]) / np.sqrt(2),
+ inds=[(outgoing['phase_grad'], i)],
+ tags=[f'pg_{i}'],
+ )
+ for i in range(self.bitsize)
+ ]
+
+ @property
+ def prepare(self) -> PhaseGradientState:
+ """bloq to prepare the actual phase gradient state"""
+ return PhaseGradientState(self.bitsize, eps=self.eps)
+
+ def adjoint(self) -> 'ReleasePhaseGradient':
+ return ReleasePhaseGradient(self.bitsize, self.eps)
+
+
+@attrs.frozen
+class ReleasePhaseGradient(_BookkeepingBloq):
+ """Release a phase gradient resource state.
+
+ Helper to make diagrams cleaner. See `AcquirePhaseGradient` for details.
+ """
+
+ bitsize: SymbolicInt
+ eps: float = 1e-10
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature([Register('phase_grad', self.phase_dtype, side=Side.LEFT)])
+
+ @property
+ def phase_dtype(self) -> QDType:
+ return QFxp(self.bitsize, self.bitsize)
+
+ def decompose_bloq(self):
+ raise DecomposeTypeError(f"{self} is atomic")
+
+ def my_tensors(
+ self, incoming: dict[str, 'ConnectionT'], outgoing: dict[str, 'ConnectionT']
+ ) -> list['qtn.Tensor']:
+ import quimb.tensor as qtn
+
+ if is_symbolic(self.bitsize):
+ raise DecomposeTypeError(f"cannot compute tensors for symbolic {self}")
+
+ return [
+ qtn.Tensor(
+ data=np.array([1, np.exp(1j * np.pi / 2**i)]) / np.sqrt(2),
+ inds=[(incoming['phase_grad'], i)],
+ tags=[f'pg_{i}'],
+ )
+ for i in range(self.bitsize)
+ ]
+
+ def adjoint(self) -> 'AcquirePhaseGradient':
+ return AcquirePhaseGradient(self.bitsize, self.eps)
+
+
+def ignore_resource_alloc_free(b: Bloq) -> Optional[Bloq]:
+ """A generalizer that ignores split and join operations."""
+ if isinstance(b, (AcquirePhaseGradient, ReleasePhaseGradient)):
+ return None
+
+ return _ignore_wrapper(ignore_resource_alloc_free, b)
diff --git a/qualtran/bloqs/max_k_xor_sat/resource/phase_gradient_test.py b/qualtran/bloqs/max_k_xor_sat/resource/phase_gradient_test.py
new file mode 100644
index 000000000..5cf660cc1
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/resource/phase_gradient_test.py
@@ -0,0 +1,31 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import numpy as np
+import pytest
+
+from qualtran.bloqs.max_k_xor_sat.resource.phase_gradient import AcquirePhaseGradient
+
+
+@pytest.mark.parametrize('n', [3, 4, 5, 10, 12])
+@pytest.mark.parametrize('adjoint', [False, True])
+def test_tensor(n: int, adjoint: bool):
+ phase_grad = AcquirePhaseGradient(n)
+
+ actual = (phase_grad.adjoint() if adjoint else phase_grad).tensor_contract()
+
+ expected = phase_grad.prepare.tensor_contract()
+ if adjoint:
+ expected = expected.conj().T
+
+ np.testing.assert_allclose(actual, expected)
diff --git a/qualtran/bloqs/max_k_xor_sat/shims.py b/qualtran/bloqs/max_k_xor_sat/shims.py
new file mode 100644
index 000000000..8f3b99435
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/shims.py
@@ -0,0 +1,148 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import Optional
+
+import attrs
+from attrs import frozen
+
+from qualtran import (
+ Bloq,
+ BloqBuilder,
+ CtrlSpec,
+ DecomposeTypeError,
+ QAny,
+ QBit,
+ Register,
+ Signature,
+ Soquet,
+ SoquetT,
+)
+from qualtran.bloqs.basic_gates import Hadamard, OnEach
+from qualtran.bloqs.mcmt import MultiControlX
+from qualtran.resource_counting import (
+ BloqCountDictT,
+ CostKey,
+ GateCounts,
+ QECGatesCost,
+ SympySymbolAllocator,
+)
+from qualtran.symbolics import HasLength, is_symbolic, SymbolicInt
+
+
+@frozen
+class ArbitraryGate(Bloq):
+ """Placeholder gate for costing
+
+ Footnote 18, page 29:
+ By “gate complexity”, we mean the total number of (arbitrary) 1- and 2-qubit gates
+ used by the quantum algorithm. These gates can be further represented using a
+ finite universal gate set with a logarithmic overhead.
+
+ 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.
+ """
+
+ n_ctrls: SymbolicInt = 0
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature.build(q=2)
+
+ def my_static_costs(self, cost_key: 'CostKey'):
+ if isinstance(cost_key, QECGatesCost):
+ # placeholder cost: reduce controls to single bit, and use C-SU2 (say).
+ return GateCounts(rotation=1, and_bloq=self.n_ctrls)
+
+ return NotImplemented
+
+ def adjoint(self) -> 'Bloq':
+ return self
+
+ def get_ctrl_system(self, ctrl_spec: CtrlSpec):
+ ctrl_bloq = attrs.evolve(self, n_ctrls=self.n_ctrls + ctrl_spec.num_qubits)
+
+ return ctrl_bloq, NotImplemented
+
+
+def generalize_1_2_qubit_gates(b: Bloq) -> Optional[Bloq]:
+ from qualtran.bloqs.basic_gates import GlobalPhase, Identity
+ from qualtran.bloqs.bookkeeping import ArbitraryClifford
+ from qualtran.resource_counting.classify_bloqs import (
+ bloq_is_clifford,
+ bloq_is_rotation,
+ bloq_is_t_like,
+ )
+
+ if bloq_is_t_like(b) or bloq_is_clifford(b) or bloq_is_rotation(b):
+ return ArbitraryGate()
+
+ if isinstance(b, ArbitraryClifford):
+ return ArbitraryGate()
+
+ if isinstance(b, (GlobalPhase, Identity)):
+ return None
+
+ return b
+
+
+@frozen
+class ProbabilisticUncompute(Bloq):
+ """Probabilistically uncompute a register using hadamards, and mark success in a flag qubit
+
+ Apply hadamards to the register, and mark the flag conditioned on all input qubits being 0.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Eq. 129 and Eq. 130.
+ """
+
+ bitsize: SymbolicInt
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature([Register('q', QAny(self.bitsize)), Register('flag', QBit())])
+
+ def build_composite_bloq(
+ self, bb: 'BloqBuilder', q: 'Soquet', flag: 'Soquet'
+ ) -> dict[str, 'SoquetT']:
+ if is_symbolic(self.bitsize):
+ raise DecomposeTypeError(f"cannot decompose symbolic {self}")
+
+ q = bb.add(OnEach(self.bitsize, Hadamard()), q=q)
+
+ qs = bb.split(q)
+ qs, flag = bb.add(MultiControlX(cvs=[1] * self.bitsize), controls=qs, target=flag)
+ q = bb.join(qs)
+
+ return {'q': q, 'flag': flag}
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
+ return {OnEach(self.bitsize, Hadamard()): 1, MultiControlX(cvs=HasLength(self.bitsize)): 1}
+
+
+@frozen
+class ReflectAboutZero(Bloq):
+ registers: tuple[Register, ...]
+ global_phase: float = 1
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature(self.registers)
+
+ def my_static_costs(self, cost_key: 'CostKey'):
+ if cost_key == QECGatesCost():
+ return GateCounts(and_bloq=self.signature.n_qubits())
+
+ return NotImplemented
diff --git a/qualtran/bloqs/max_k_xor_sat/tutorial_guiding_state.ipynb b/qualtran/bloqs/max_k_xor_sat/tutorial_guiding_state.ipynb
new file mode 100644
index 000000000..5e3217022
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/tutorial_guiding_state.ipynb
@@ -0,0 +1,2133 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "82896538a83e86e0",
+ "metadata": {
+ "ExecuteTime": {
+ "start_time": "2024-08-29T16:19:45.546609Z"
+ },
+ "jupyter": {
+ "is_executing": true
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import sympy\n",
+ "from qualtran.drawing import show_bloq, show_call_graph"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "971792d050e3ef26",
+ "metadata": {},
+ "source": [
+ "Let us start with a kXOR instance with $n$ variables and $m$ constraints."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "initial_id",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-26T16:39:09.327449Z",
+ "start_time": "2024-08-26T16:39:09.321990Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "KXorInstance(n=n, k=k, constraints=HasLength(n=m), max_rhs=2)"
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.kxor_instance import KXorInstance\n",
+ "\n",
+ "n, m, k = sympy.symbols(\"n m k\", positive=True, integer=True)\n",
+ "inst = KXorInstance.symbolic(n, m, k)\n",
+ "inst"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cd204c687a2025b1",
+ "metadata": {},
+ "source": [
+ "We first prepare the guiding state to use in the guided sparse hamiltonian algorithm.\n",
+ "The guiding state is defined by the instance, and a parameter $\\ell$ (a multiple of $k$)\n",
+ "\n",
+ "From Theorem 4.15 of the paper, this should be a circuit of $O(\\ell m \\log n)$ gates,\n",
+ "and prepare the state $\\beta |\\Psi\\rangle|0^{\\ell \\log \\ell}\\rangle + |\\perp\\rangle|1\\rangle$,\n",
+ "where $\\beta \\ge 0.99 / \\ell^{\\ell/2}$."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "49b841dd4be61404",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-26T16:39:09.374257Z",
+ "start_time": "2024-08-26T16:39:09.330508Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.guiding_state import GuidingState\n",
+ "\n",
+ "c = sympy.symbols(\"c\", positive=True, integer=True)\n",
+ "l = c * k\n",
+ "guiding_state = GuidingState(inst, l)\n",
+ "show_call_graph(guiding_state.call_graph(max_depth=1)[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "4977ed3aaa506a55",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-26T16:39:09.426085Z",
+ "start_time": "2024-08-26T16:39:09.376658Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "guiding_state_3 = GuidingState(inst, 3 * k)\n",
+ "show_bloq(guiding_state_3.decompose_bloq())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "96b8c98966f509f2",
+ "metadata": {},
+ "source": [
+ "We can also build the guiding state for a concrete (non symbolic) instance:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "8c2235778858f43a",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-26T16:39:09.462970Z",
+ "start_time": "2024-08-26T16:39:09.428032Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "inst = KXorInstance.random_instance(n=20, m=100, k=4, planted_advantage=0.8, rng=np.random.default_rng(100))\n",
+ "guiding_state_concrete = GuidingState(inst, ell=12)\n",
+ "show_bloq(guiding_state_concrete)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "7bde9ab8a749a772",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-26T16:39:09.523825Z",
+ "start_time": "2024-08-26T16:39:09.465224Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "show_bloq(guiding_state_concrete.decompose_bloq())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "140a7fb3e4417c87",
+ "metadata": {},
+ "source": [
+ "Let us evaluate the gate cost for the above bloqs."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "8cf9829e6d7bce3f",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-26T16:39:09.839012Z",
+ "start_time": "2024-08-26T16:39:09.525868Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "GateCounts(t=0, toffoli=672, cswap=2880, and_bloq=22726, clifford=140518, rotation=0, measurement=22726)"
+ ]
+ },
+ "execution_count": 19,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from qualtran.resource_counting import get_cost_value, QECGatesCost\n",
+ "\n",
+ "get_cost_value(guiding_state_concrete, QECGatesCost())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "518a98ecdc4cd03",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-26T16:39:09.874380Z",
+ "start_time": "2024-08-26T16:39:09.840147Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/latex": [
+ "$\\displaystyle 16 c^{2} k^{2} \\cdot \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(n \\right)}}\\right\\rceil + 1\\right) + 16 c^{2} k^{2} \\left\\lceil{\\operatorname{log}_{2}{\\left(n \\right)}}\\right\\rceil + 4 c k \\left\\lceil{\\operatorname{log}_{2}{\\left(c k \\right)}}\\right\\rceil + 4 c k + 4 c \\left(4 m + \\left(2 m + 1\\right) \\left(\\left\\lceil{\\operatorname{log}_{2}{\\left(\\left\\lfloor{2^{k \\left\\lceil{\\operatorname{log}_{2}{\\left(n \\right)}}\\right\\rceil}}\\right\\rfloor \\right)}}\\right\\rceil - 1\\right) - 4\\right) + 224 c + 4 \\cdot \\left(2 c k - 2\\right) \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(n \\right)}}\\right\\rceil + 1\\right) - 8$"
+ ],
+ "text/plain": [
+ "16*c**2*k**2*(2*ceiling(log2(n)) + 1) + 16*c**2*k**2*ceiling(log2(n)) + 4*c*k*ceiling(log2(c*k)) + 4*c*k + 4*c*(4*m + (2*m + 1)*(ceiling(log2(floor(2**(k*ceiling(log2(n)))))) - 1) - 4) + 224*c + 4*(2*c*k - 2)*(2*ceiling(log2(n)) + 1) - 8"
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gc = get_cost_value(guiding_state, QECGatesCost())\n",
+ "t_cost = gc.total_t_count(ts_per_toffoli=4, ts_per_cswap=4, ts_per_and_bloq=4)\n",
+ "t_cost"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "66070e9c3dfd10c",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-26T16:39:11.247058Z",
+ "start_time": "2024-08-26T16:39:09.876298Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/latex": [
+ "$\\displaystyle O\\left(\\ell^{2} \\operatorname{log}_{2}{\\left(n \\right)} + \\ell m \\operatorname{log}_{2}{\\left(n \\right)} + c m; \\left( \\ell, \\ c, \\ m, \\ n\\right)\\rightarrow \\left( \\infty, \\ \\infty, \\ \\infty, \\ \\infty\\right)\\right)$"
+ ],
+ "text/plain": [
+ "O(\\ell**2*log2(n) + \\ell*m*log2(n) + c*m, (\\ell, oo), (c, oo), (m, oo), (n, oo))"
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from qualtran.symbolics import ceil, log2, floor\n",
+ "from qualtran.resource_counting import big_O\n",
+ "\n",
+ "# simplify some expressions that sympy could not\n",
+ "klogn = k * ceil(log2(n))\n",
+ "klogn_long = ceil(log2(floor(2**klogn)))\n",
+ "t_cost = t_cost.subs(klogn_long, klogn)\n",
+ "t_cost = t_cost.simplify()\n",
+ "\n",
+ "# replace l with a symbol\n",
+ "l_symb = sympy.symbols(r\"\\ell\", positive=True, integer=True)\n",
+ "t_cost = t_cost.subs(c * k, l_symb)\n",
+ "\n",
+ "big_O(t_cost) # matches paper Theorem 4.15 (as c, l are O(m))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "e47caf91-a16e-441a-a02e-6925ee3ca55a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "show_call_graph(guiding_state_concrete, max_depth=3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c459ef23c4ac7335",
+ "metadata": {},
+ "source": [
+ "As we know that $c = \\ell/k \\le \\ell$ and $\\ell \\le m$, the above expression matches the paper result of $O(\\ell m \\log_2(n))$ 1/2-qubit gates.\n",
+ ""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/qualtran/bloqs/max_k_xor_sat/tutorial_planted_noisy_kxor.py.ipynb b/qualtran/bloqs/max_k_xor_sat/tutorial_planted_noisy_kxor.py.ipynb
new file mode 100644
index 000000000..e72a1ed87
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/tutorial_planted_noisy_kxor.py.ipynb
@@ -0,0 +1,2526 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f9ab6a67-1a3c-459f-9716-cda924fa4efe",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "eceae6a5-5497-4ea9-8c24-0abd0f45554c",
+ "metadata": {},
+ "source": [
+ "### Implementing [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d5eb2c1f-a57d-4b18-8b26-454da506578b",
+ "metadata": {},
+ "source": [
+ "### Problem Definition"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f6e0f089-9c57-4d87-8478-5b6660cb6395",
+ "metadata": {},
+ "source": [
+ "**Definition 2.2 (simplified):**\n",
+ "A kXOR instance $\\mathcal{I}$ on $n$ variables in ${0, 1}$ is a collection of $m$ constraints, each of the form\n",
+ "$$ x_{c_1} \\oplus x_{c_2} \\oplus ... x_{c_k} = b $$ "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ebd9642b-99a2-4ee4-b905-826174d5a8c7",
+ "metadata": {},
+ "source": [
+ "**Notation 2.3**\n",
+ "Random Instance: Pick each clause independently:\n",
+ "- Pick $C$, a $k$ subset of $[n] = \\{1, ... n\\}$ uniformly at random.\n",
+ "- Pick $b \\in {0, 1}$ uniformly at random."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1756cf95-b8ad-42ca-bb35-59528bf96538",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat import KXorInstance, Constraint\n",
+ "\n",
+ "n, k = 10, 4\n",
+ "cs = (\n",
+ " Constraint((0, 1, 2, 3), -1), # read: x_0 ^ x_1 ^ x_2 ^ x_3 = 0\n",
+ " Constraint((0, 2, 4, 5), 1),\n",
+ " Constraint((0, 3, 4, 5), 1),\n",
+ " Constraint((0, 3, 4, 5), 1),\n",
+ " Constraint((1, 2, 3, 4), -1),\n",
+ " Constraint((1, 3, 4, 5), -1),\n",
+ " Constraint((1, 3, 4, 5), -1),\n",
+ " Constraint((2, 3, 4, 5), 1),\n",
+ ")\n",
+ "simple_inst = KXorInstance(n, k, cs)\n",
+ "simple_inst"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5fc71a1f-e1d4-44fb-b657-8d43af87626f",
+ "metadata": {},
+ "source": [
+ "**Notation 2.4 (simplified)** Planted Instance:\n",
+ "Given $\\rho \\in [0, 1]$ (the _planted advantage_),\n",
+ "\n",
+ "first pick a secret assignment $z \\in \\{0, 1\\}^n$.\n",
+ "Now pick each clause independently by: \n",
+ "- Pick $C$, a $k$ subset of $[n]$ uniformly at random.\n",
+ "- Pick noise $\\eta \\in {0, 1}$, s.t. $\\eta = 0$ with probability $(1 + \\rho)/2$\n",
+ "- Set $b = C(z) \\oplus \\eta$\n",
+ "\n",
+ "Note: when $\\rho = 0$, the noise is random, and when $\\rho = 1$, there is no noise."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2d334a8e-3792-4ece-9f9c-d77da3111aa2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "random_inst = KXorInstance.random_instance(\n",
+ " n=10, \n",
+ " m=20, \n",
+ " k=4,\n",
+ " planted_advantage=0.8,\n",
+ " rng=np.random.default_rng(42),\n",
+ ")\n",
+ "random_inst"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cd35802e-1778-44de-81bd-9356da3999a7",
+ "metadata": {},
+ "source": [
+ "## Problem\n",
+ "\n",
+ "**Problem 2.6 (Planted Noisy kXOR)**\n",
+ "Given $\\rho \\in (0, 1)$, and an instance $\\mathcal{I}$ that is promised to be either drawn from the random distribution or planted distribution (with $\\rho$), distinguish which case it is."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4e401d1e-157c-4f49-adfe-9ceeb315ba6d",
+ "metadata": {},
+ "source": [
+ "## Kikuchi Method\n",
+ "This is a technique to reduce $k$XOR problems to $2$XOR problems, on an exponentially larger set of variables (say, $O(n^k)$).\n",
+ "The 2XOR is known to be efficiently solvable by some spectral analysis.\n",
+ "\n",
+ "For this, we pick our new variables as subsets of $[n]$ of size $k$, call them $X_S$ for each subset $S$.\n",
+ "There are ${n \\choose k}$ variables now, and for $k \\ll n$, this is about $O(n^k)$.\n",
+ "\n",
+ "The equations are of the form $X_S \\oplus X_T = b(S, T)$ for every $S, T$ with $|S \\Delta T| = k$.\n",
+ "Here $b(S, T)$ is the xor of all variables in S and T (common ones cancel out, leaving only the $k$ as above)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "da3f5e5c-67dd-4717-962b-23052fdd30ad",
+ "metadata": {},
+ "source": [
+ "## Quantum Algorithm\n",
+ "\n",
+ "**Theorem 4.18 (simplified)**\n",
+ "Let $k$ (even) and $\\rho \\in (0, 1)$ be known constants.\n",
+ "\n",
+ "We are given an instance $\\mathcal{I}$ which is either random or planted (with advantage $\\rho$),\n",
+ "where the number of constraints $m$ is picked above a given threshold (see Alice Theorem).\n",
+ "\n",
+ "For a parameter $\\ell$, if we have a classical _Kikuchi style_ algorithm with complexity $\\tilde{O}(n^\\ell)$,\n",
+ "then there is a quantum algorithm with $\\tilde{O}(n^{\\ell/4} m \\ell^{O{\\ell}} \\log^{\\ell/2k}n)$."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "9b5a6bbb-e895-47d9-8ea6-5ab1f07329e3",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-27T13:03:40.693090Z",
+ "start_time": "2024-08-27T13:03:38.179464Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "from qualtran.bloqs.max_k_xor_sat.planted_noisy_kxor import PlantedNoisyKXOR\n",
+ "from qualtran.drawing import show_bloq, show_call_graph, show_counts_sigma"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "b5e3a55c-00a9-40a4-b309-79fa008b0760",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-08-27T13:03:40.693090Z",
+ "start_time": "2024-08-27T13:03:38.179464Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "def make_algo_example():\n",
+ " k = 4\n",
+ " n, m = 100, 1000\n",
+ " rho = 0.8\n",
+ " \n",
+ " c = 2 # Kikuchi param: ell = c * k\n",
+ " \n",
+ " # generate instance\n",
+ " rng = np.random.default_rng(142)\n",
+ " ell = c * k\n",
+ " inst = KXorInstance.random_instance(n=n, m=m, k=k, planted_advantage=rho, rng=rng)\n",
+ " algo_bloq = PlantedNoisyKXOR.from_inst(inst=inst, ell=ell, rho=rho, zeta=1 / np.log(n), rng=rng)\n",
+ "\n",
+ " expected_complexity = n ** (ell/4) * m * ell**ell * np.log(n)**(c//2)\n",
+ "\n",
+ " return algo_bloq, expected_complexity"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "46dc55f4-3873-40a2-9509-b723493eeb85",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "bloq, cost_O_tilde = make_algo_example()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "04de6797-5986-461e-b040-2ec7fab2bcc2",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "show_bloq(bloq)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "id": "6cdc8096-84da-4358-b068-1e4438be51e4",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "g, sigma = bloq.call_graph(max_depth=6)\n",
+ "show_call_graph(g)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "234316cd-b790-42ee-baef-d9f08bf2e19d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'toffoli': 12261972437984,\n",
+ " 'cswap': 5171509416110263,\n",
+ " 'and_bloq': 255058640186620068,\n",
+ " 'clifford': 373887003261338334,\n",
+ " 'rotation': 11450518379588*\\tilde{O}(350784) + 17649125770113,\n",
+ " 'measurement': 255058640186620068}"
+ ]
+ },
+ "execution_count": 24,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from qualtran.resource_counting import get_cost_value, QECGatesCost\n",
+ "\n",
+ "gc = get_cost_value(bloq, QECGatesCost())\n",
+ "gc.asdict()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "a3af760a-c97f-4731-b1d0-5749271f0cdb",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'toffoli': 0.0158706515045947,\n",
+ " 'cswap': 6.69347644605456,\n",
+ " 'and_bloq': 330.121994003046,\n",
+ " 'clifford': 483.921356116957,\n",
+ " 'rotation': 0.0148203878020848*\\tilde{O}(350784) + 0.0228432355295913,\n",
+ " 'measurement': 330.121994003046}"
+ ]
+ },
+ "execution_count": 25,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "(gc * (1/cost_O_tilde)).asdict()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "id": "493750c7-2cd2-40ef-a8b1-f5b52c2a5792",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'7.726193e+14'"
+ ]
+ },
+ "execution_count": 29,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "f\"{cost_O_tilde:e}\""
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.9"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/qualtran/bloqs/mcmt/and_bloq.py b/qualtran/bloqs/mcmt/and_bloq.py
index 9ca197238..49818c0f4 100644
--- a/qualtran/bloqs/mcmt/and_bloq.py
+++ b/qualtran/bloqs/mcmt/and_bloq.py
@@ -70,7 +70,7 @@
import quimb.tensor as qtn
# TODO: https://github.com/quantumlib/Qualtran/issues/1346
-FLAG_AND_AS_LEAF = False
+FLAG_AND_AS_LEAF = True
@frozen
@@ -194,7 +194,11 @@ def wire_symbol(self, reg: Optional[Register], idx: Tuple[int, ...] = tuple()) -
def __str__(self):
dag = '†' if self.uncompute else ''
- return f'And{dag}'
+ if self.cv1 != 1 or self.cv2 != 1:
+ cv = f"({self.cv1}, {self.cv2})"
+ else:
+ cv = ""
+ return f'And{cv}{dag}'
def decompose_from_registers(
self, *, context: cirq.DecompositionContext, **quregs: NDArray[cirq.Qid] # type: ignore[type-var]
diff --git a/qualtran/bloqs/mcmt/bloq_with_specialized_single_qubit_control.py b/qualtran/bloqs/mcmt/bloq_with_specialized_single_qubit_control.py
new file mode 100644
index 000000000..9e27ab57b
--- /dev/null
+++ b/qualtran/bloqs/mcmt/bloq_with_specialized_single_qubit_control.py
@@ -0,0 +1,83 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import cast, Iterable, Optional, Protocol, runtime_checkable, Sequence, TYPE_CHECKING
+
+import numpy as np
+
+if TYPE_CHECKING:
+ from qualtran import AddControlledT, Bloq, BloqBuilder, CtrlSpec, SoquetT
+
+
+@runtime_checkable
+class BloqWithSpecializedControl(Protocol):
+ """mixin for a bloq that has a specialized single qubit controlled version."""
+
+ @property
+ def cv(self) -> Optional[int]: ...
+
+ def with_cv(self, *, cv: Optional[int]) -> 'Bloq': ...
+
+
+def get_ctrl_system_for_bloq_with_specialized_single_qubit_control(
+ bloq: 'BloqWithSpecializedControl', ctrl_spec: 'CtrlSpec', *, ctrl_reg_name: str = 'ctrl'
+) -> tuple['Bloq', 'AddControlledT']:
+ from qualtran import Bloq, CtrlSpec, Soquet
+ from qualtran.bloqs.mcmt import ControlledViaAnd
+
+ if ctrl_spec != CtrlSpec():
+ assert isinstance(bloq, Bloq)
+ return ControlledViaAnd.make_ctrl_system(bloq=bloq, ctrl_spec=ctrl_spec)
+
+ assert isinstance(
+ bloq, BloqWithSpecializedControl
+ ), f"{bloq} must implement protocol {BloqWithSpecializedControl}"
+
+ if bloq.cv is None:
+ # the easy case: use the controlled bloq
+ ctrl_bloq = bloq.with_cv(cv=1)
+
+ def _adder(
+ bb: 'BloqBuilder', ctrl_soqs: Sequence['SoquetT'], in_soqs: dict[str, 'SoquetT']
+ ) -> tuple[Iterable['SoquetT'], Iterable['SoquetT']]:
+ (ctrl,) = ctrl_soqs
+ in_soqs |= {ctrl_reg_name: ctrl}
+
+ out_soqs = bb.add_d(ctrl_bloq, **in_soqs)
+
+ ctrl = out_soqs.pop(ctrl_reg_name)
+ return [ctrl], out_soqs.values()
+
+ else:
+ # the difficult case: must combine the two controls into one
+ un_ctrl_bloq = bloq.with_cv(cv=None)
+ ctrl_bloq = ControlledViaAnd(un_ctrl_bloq, CtrlSpec(cvs=[1, bloq.cv]))
+
+ def _adder(
+ bb: 'BloqBuilder', ctrl_soqs: Sequence['SoquetT'], in_soqs: dict[str, 'SoquetT']
+ ) -> tuple[Iterable['SoquetT'], Iterable['SoquetT']]:
+ # extract the two control bits
+ (ctrl0,) = ctrl_soqs
+ ctrl1 = in_soqs.pop('ctrl')
+
+ ctrl0 = cast(Soquet, ctrl0)
+ ctrl1 = cast(Soquet, ctrl1)
+
+ # add the singly controlled bloq
+ ctrls, *out_soqs = bb.add_t(ctrl_bloq, ctrl=[ctrl0, ctrl1], **in_soqs)
+ assert isinstance(ctrls, np.ndarray)
+ ctrl0, ctrl1 = ctrls
+
+ return [ctrl0], [ctrl1, *out_soqs]
+
+ return ctrl_bloq, _adder
diff --git a/qualtran/bloqs/mcmt/bloq_with_specialized_single_qubit_control_test.py b/qualtran/bloqs/mcmt/bloq_with_specialized_single_qubit_control_test.py
new file mode 100644
index 000000000..ebff8cc8d
--- /dev/null
+++ b/qualtran/bloqs/mcmt/bloq_with_specialized_single_qubit_control_test.py
@@ -0,0 +1,106 @@
+# 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 typing import Optional, Sequence, Tuple
+from unittest.mock import ANY
+
+import pytest
+from attrs import evolve, frozen
+
+from qualtran import AddControlledT, Bloq, CtrlSpec, Signature
+from qualtran.bloqs.mcmt.bloq_with_specialized_single_qubit_control import (
+ get_ctrl_system_for_bloq_with_specialized_single_qubit_control,
+)
+from qualtran.resource_counting import CostKey, GateCounts, get_cost_value, QECGatesCost
+
+
+@frozen
+class AtomWithSpecializedControl(Bloq):
+ cv: Optional[int] = None
+
+ @property
+ def signature(self) -> 'Signature':
+ n_ctrl = 1 if self.cv is not None else 0
+ return Signature.build(ctrl=n_ctrl, q=2)
+
+ def with_cv(self, *, cv: Optional[int]) -> Bloq:
+ return evolve(self, cv=cv)
+
+ def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> Tuple['Bloq', 'AddControlledT']:
+ return get_ctrl_system_for_bloq_with_specialized_single_qubit_control(self, ctrl_spec)
+
+ @staticmethod
+ def cost_expr_for_cv(cv: Optional[int]):
+ import sympy
+
+ c_unctrl = sympy.Symbol("_c_target")
+ c_ctrl = sympy.Symbol("_c_ctrl_")
+
+ if cv is None:
+ return c_unctrl
+ return c_unctrl + c_ctrl
+
+ def my_static_costs(self, cost_key: 'CostKey'):
+ if cost_key == QECGatesCost():
+ r = self.cost_expr_for_cv(self.cv)
+ return GateCounts(rotation=r)
+
+ return NotImplemented
+
+
+def ON(n: int = 1) -> CtrlSpec:
+ return CtrlSpec(cvs=[1] * n)
+
+
+def OFF(n: int = 1) -> CtrlSpec:
+ return CtrlSpec(cvs=[0] * n)
+
+
+@pytest.mark.parametrize(
+ 'ctrl_specs',
+ [
+ [ON()],
+ [OFF()],
+ [OFF(), OFF()],
+ [OFF(4)],
+ [OFF(2), OFF(2)],
+ [ON(), OFF(5)],
+ [ON(), ON(), ON()],
+ [OFF(4), ON(3), OFF(5)],
+ ],
+)
+def test_custom_controlled(ctrl_specs: Sequence[CtrlSpec]):
+ bloq: Bloq = AtomWithSpecializedControl()
+ for ctrl_spec in ctrl_specs:
+ bloq = bloq.controlled(ctrl_spec)
+ n_ctrls = sum(ctrl_spec.num_qubits for ctrl_spec in ctrl_specs)
+
+ gc = get_cost_value(bloq, QECGatesCost())
+ assert gc == GateCounts(
+ and_bloq=n_ctrls - 1,
+ rotation=AtomWithSpecializedControl.cost_expr_for_cv(1),
+ clifford=ANY,
+ measurement=ANY,
+ )
diff --git a/qualtran/bloqs/mcmt/multi_control_pauli.py b/qualtran/bloqs/mcmt/multi_control_pauli.py
index 27300e739..fea7cf547 100644
--- a/qualtran/bloqs/mcmt/multi_control_pauli.py
+++ b/qualtran/bloqs/mcmt/multi_control_pauli.py
@@ -111,7 +111,7 @@ def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> Dict[str
def __str__(self) -> str:
n = self.n_ctrls
- ctrl = f'C^{n}' if is_symbolic(n) or n > 2 else ['', 'C', 'CC'][int(n)]
+ ctrl = f'C[{n}]' if is_symbolic(n) or n > 2 else ['', 'C', 'CC'][int(n)]
return f'{ctrl}{self.target_gate!s}'
def _circuit_diagram_info_(self, _) -> cirq.CircuitDiagramInfo:
@@ -192,6 +192,9 @@ class MultiControlX(MultiControlPauli):
def _X(self):
return cirq.X
+ def adjoint(self) -> 'Bloq':
+ return self
+
@frozen
class MultiControlZ(MultiControlPauli):
@@ -205,3 +208,6 @@ class MultiControlZ(MultiControlPauli):
@target_gate.default
def _Z(self):
return cirq.Z
+
+ def adjoint(self) -> 'Bloq':
+ return self
diff --git a/qualtran/bloqs/phase_estimation/lp_resource_state.py b/qualtran/bloqs/phase_estimation/lp_resource_state.py
index d17866a49..0a68294af 100644
--- a/qualtran/bloqs/phase_estimation/lp_resource_state.py
+++ b/qualtran/bloqs/phase_estimation/lp_resource_state.py
@@ -141,8 +141,9 @@ def from_standard_deviation_eps(cls, eps: SymbolicFloat) -> 'LPResourceState':
def m_bits(self) -> SymbolicInt:
return self.bitsize
- def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> Dict[str, 'SoquetT']:
- qpe_reg = bb.allocate(dtype=self.m_register.dtype)
+ def build_composite_bloq(
+ self, bb: 'BloqBuilder', qpe_reg: 'Soquet', **soqs: 'SoquetT'
+ ) -> Dict[str, 'SoquetT']:
anc, flag = bb.allocate(dtype=QBit()), bb.allocate(dtype=QBit())
flag_angle = np.arccos(1 / (1 + 2**self.bitsize))
diff --git a/qualtran/bloqs/phase_estimation/qpe_window_state.py b/qualtran/bloqs/phase_estimation/qpe_window_state.py
index 5d6f0c373..eea507d2e 100644
--- a/qualtran/bloqs/phase_estimation/qpe_window_state.py
+++ b/qualtran/bloqs/phase_estimation/qpe_window_state.py
@@ -17,7 +17,7 @@
import attrs
-from qualtran import Bloq, bloq_example, BloqDocSpec, QFxp, Register, Side, Signature
+from qualtran import Bloq, bloq_example, BloqDocSpec, QFxp, Register, Signature
from qualtran.bloqs.basic_gates import Hadamard, OnEach
from qualtran.symbolics import ceil, log2, pi, SymbolicFloat, SymbolicInt
@@ -31,7 +31,7 @@ class QPEWindowStateBase(Bloq, metaclass=abc.ABCMeta):
@cached_property
def m_register(self) -> 'Register':
- return Register('qpe_reg', QFxp(self.m_bits, self.m_bits), side=Side.RIGHT)
+ return Register('qpe_reg', QFxp(self.m_bits, self.m_bits))
@property
@abc.abstractmethod
@@ -95,8 +95,7 @@ def from_standard_deviation_eps(cls, eps: SymbolicFloat):
"""
return cls(ceil(2 * log2(pi(eps) / eps)))
- def build_composite_bloq(self, bb: 'BloqBuilder') -> Dict[str, 'SoquetT']:
- qpe_reg = bb.allocate(dtype=self.m_register.dtype)
+ def build_composite_bloq(self, bb: 'BloqBuilder', qpe_reg) -> Dict[str, 'SoquetT']:
qpe_reg = bb.add(OnEach(self.m_bits, Hadamard()), q=qpe_reg)
return {'qpe_reg': qpe_reg}
diff --git a/qualtran/bloqs/state_preparation/black_box_prepare.py b/qualtran/bloqs/state_preparation/black_box_prepare.py
index 42aacba78..892db1735 100644
--- a/qualtran/bloqs/state_preparation/black_box_prepare.py
+++ b/qualtran/bloqs/state_preparation/black_box_prepare.py
@@ -29,8 +29,7 @@
)
from qualtran.bloqs.bookkeeping.auto_partition import AutoPartition
from qualtran.bloqs.state_preparation.prepare_base import PrepareOracle
-from qualtran.symbolics import ssum, SymbolicFloat, SymbolicInt
-from qualtran.symbolics.types import is_symbolic
+from qualtran.symbolics import is_zero, ssum, SymbolicFloat, SymbolicInt
@frozen
@@ -75,19 +74,19 @@ def signature(self) -> Signature:
return Signature.build(selection=self.selection_bitsize, junk=self.junk_bitsize)
def build_composite_bloq(self, bb: BloqBuilder, **soqs: SoquetT) -> Dict[str, SoquetT]:
- if self.selection_bitsize == 0:
+ if is_zero(self.selection_bitsize):
return soqs
partitions = [
(self.selection_registers[0], [r.name for r in self.prepare.selection_registers])
]
- if is_symbolic(self.junk_bitsize) or self.junk_bitsize > 0:
+ if not is_zero(self.junk_bitsize):
partitions.append(
(self.junk_registers[0], [r.name for r in self.prepare.junk_registers])
)
return bb.add_d(AutoPartition(self.prepare, partitions), **soqs)
def __str__(self) -> str:
- return 'Prep'
+ return f'BBPrepare[{self.prepare}]'
@bloq_example
diff --git a/qualtran/conftest.py b/qualtran/conftest.py
index e229f0a2a..ede967863 100644
--- a/qualtran/conftest.py
+++ b/qualtran/conftest.py
@@ -116,6 +116,8 @@ def assert_bloq_example_serializes_for_pytest(bloq_ex: BloqExample):
'state_prep_alias_symb', # cannot serialize Shaped
'sparse_matrix_block_encoding',
'sparse_matrix_symb_block_encoding',
+ 'sparse_matrix_hermitian_block_encoding',
+ 'sparse_matrix_symb_hermitian_block_encoding',
'sparse_state_prep_alias_symb', # cannot serialize Shaped
'sparse_permutation', # contains nested tuple of inhomogeneous shape
'permutation_cycle_symb', # cannot serialize Shaped
diff --git a/qualtran/drawing/graphviz.py b/qualtran/drawing/graphviz.py
index b6e70050d..12835098e 100644
--- a/qualtran/drawing/graphviz.py
+++ b/qualtran/drawing/graphviz.py
@@ -402,6 +402,24 @@ def cxn_edge(self, left_id: str, right_id: str, cxn: Connection) -> pydot.Edge:
arrowsize=0.25,
)
+ def cxn_label(self, cxn: Connection) -> str:
+ import sympy
+
+ from qualtran.symbolics import is_symbolic
+
+ n = cxn.shape
+ if not is_symbolic(n):
+ return str(n)
+
+ label = sympy.printing.pretty(n)
+ label_lines = label.split('\n')
+ if len(label_lines) > 1:
+ return str(n)[:10] + " ..."
+
+ if len(label) > 15:
+ return label[:15] + " ..."
+ return label
+
class TypedGraphDrawer(PrettyGraphDrawer):
@staticmethod
diff --git a/qualtran/symbolics/simplification.py b/qualtran/symbolics/simplification.py
new file mode 100644
index 000000000..6c1cfb1ad
--- /dev/null
+++ b/qualtran/symbolics/simplification.py
@@ -0,0 +1,23 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from qualtran.symbolics.types import SymbolicInt
+
+
+def extract_int(x: SymbolicInt) -> SymbolicInt:
+ """Extract a raw python int if the input is sympy.Integer, otherwise return as-is."""
+ try:
+ result = int(x)
+ return result
+ except TypeError:
+ return x