diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py
index 79e61306d..4c8cc8e56 100644
--- a/dev_tools/qualtran_dev_tools/notebook_specs.py
+++ b/dev_tools/qualtran_dev_tools/notebook_specs.py
@@ -93,6 +93,8 @@
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.guided_hamiltonian
import qualtran.bloqs.mcmt.and_bloq
import qualtran.bloqs.mcmt.controlled_via_and
import qualtran.bloqs.mcmt.ctrl_spec_and
@@ -904,6 +906,49 @@
),
]
+# --------------------------------------------------------------------------
+# ----- 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,
+ ],
+ ),
+ # ----- 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],
+ ),
+]
+
NB_BY_SECTION = [
('Basic Gates', BASIC_GATES),
('Chemistry', CHEMISTRY),
@@ -912,5 +957,6 @@
('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 969a42419..3040dc2dd 100644
--- a/docs/bloqs/index.rst
+++ b/docs/bloqs/index.rst
@@ -137,6 +137,16 @@ Bloqs Library
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/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/max_k_xor_sat/__init__.py b/qualtran/bloqs/max_k_xor_sat/__init__.py
index 827b32662..e13ed5ffc 100644
--- a/qualtran/bloqs/max_k_xor_sat/__init__.py
+++ b/qualtran/bloqs/max_k_xor_sat/__init__.py
@@ -12,4 +12,8 @@
# 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/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..c9d611d64
--- /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 to accept any block-encoding):\n",
+ "In the Guided Hamiltonian problem we are given the following as input:\n",
+ "\n",
+ "1. A $(\\sqrt{2} s, \\cdot, 0)$-block-encoding of a Hamiltonian $H$ such that $\\|H\\|_\\max \\le s$.\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..8f20efdc6
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guided_hamiltonian/guided_hamiltonian.py
@@ -0,0 +1,345 @@
+# 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,
+ Signature,
+ Soquet,
+ SoquetT,
+)
+from qualtran.bloqs.block_encoding import BlockEncoding
+from qualtran.bloqs.phase_estimation import KaiserWindowState, QubitizationQPE
+from qualtran.bloqs.phase_estimation.qpe_window_state import QPEWindowStateBase
+from qualtran.bloqs.reflections.prepare_identity import PrepareIdentity
+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 ceil, is_symbolic, is_zero, ln, log2, pi, SymbolicFloat, SymbolicInt
+
+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"
+
+ assert self.signature == self.qpe_bloq.signature
+
+ @cached_property
+ def signature(self) -> 'Signature':
+ return Signature.build_from_dtypes(
+ phase_estimate=self.qpe_bloq.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 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,
+ )
+
+ @cached_property
+ def _refl_guide_ancilla(self) -> ReflectionUsingPrepare:
+ return ReflectionUsingPrepare.reflection_around_zero(
+ bitsizes=[self.guiding_state.junk_bitsize], global_phase=-1
+ )
+
+ @cached_property
+ def _refl_all(self) -> ReflectionUsingPrepare:
+ return ReflectionUsingPrepare(PrepareIdentity(tuple(self.signature)), global_phase=-1)
+
+ 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(self._refl_guide_ancilla, reg0_=soqs['guide_ancilla'])
+
+ ### reflect about prepared state
+ soqs = bb.add_d(self.qpe_bloq.adjoint(), **soqs)
+ soqs = bb.add_d(self._refl_all, **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 ancilla being all 0
+ counts[self._refl_guide_ancilla] += self.n_rounds_amplification
+
+ # reflect about the prepared state
+ counts[self.qpe_bloq.adjoint()] += self.n_rounds_amplification
+ counts[self._refl_all] += self.n_rounds_amplification
+ counts[self.qpe_bloq] += 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..c593fce46
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/guiding_state.ipynb
@@ -0,0 +1,381 @@
+{
+ "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",
+ " - `eps`: Precision of the prepared state (defaults to 1e-6). \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",
+ "#### Parameters\n",
+ " - `inst`: the kXOR instance $\\mathcal{I}$.\n",
+ " - `ell`: the Kikuchi parameter $\\ell$.\n",
+ " - `amplitude_good_part`: (optional) the amplitude $\\beta$ of the guiding state $|\\Psi\\rangle$ Defaults to $\\beta = 0.99 / \\ell^{\\ell/2}$.\n",
+ " - `eps`: Precision of the prepared state (defaults to 1e-6). \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/kikuchi_adjacency_list.py b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_list.py
new file mode 100644
index 000000000..f746fdee0
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_list.py
@@ -0,0 +1,362 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from collections import Counter
+
+import numpy as np
+import sympy
+from attrs import frozen
+
+from qualtran import (
+ Bloq,
+ bloq_example,
+ BloqBuilder,
+ BloqDocSpec,
+ QBit,
+ QUInt,
+ Register,
+ Signature,
+ Soquet,
+ SoquetT,
+)
+from qualtran.bloqs.arithmetic import AddK, Equals, Xor
+from qualtran.bloqs.arithmetic.lists import SymmetricDifference
+from qualtran.bloqs.basic_gates import CNOT, ZeroEffect, ZeroState
+from qualtran.bloqs.mcmt import And
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import SymbolicInt
+
+from .kxor_instance import KXorInstance
+
+
+@frozen
+class ColumnOfKthNonZeroEntry(Bloq):
+ r"""Given $(S, k)$, compute the column of the $k$-th non-zero entry in row $S$.
+
+ If the output is denoted as $f(S, k)$, then this bloq maps
+ $(S, k, z, b)$ to $(S, k, z \oplus f'(S, k), b \oplus (k \ge s))$.
+ where $s$ is the sparsity, and $f'(S, k)$ is by extending $f$
+ such that for all $k \ge s$, $f'(S, k) = k$.
+ Using $f'$ ensures the computation is reversible.
+ Note: we must use the same extension $f'$ for both oracles.
+
+ This algorithm is described by the following pseudo-code:
+ ```
+ def forward(S, k) -> f_S_k:
+ nnz := 0 # counter
+ for j in range(\bar{m}):
+ T := S \Delta U_j
+ if |T| == l:
+ nnz := nnz + 1
+ if nnz == k:
+ f_S_k ^= T
+ ```
+
+ Args:
+ inst: the kXOR instance $\mathcal{I}$.
+ ell: Kikuchi parameter $\ell$.
+
+ Registers:
+ S: index register to store $S \in {[n] \choose \ell}$.
+ k: non-zero entry index register
+ T: index register to store output $T = f(S, k) \in {[n] \choose \ell}$.
+ """
+
+ inst: KXorInstance
+ ell: SymbolicInt
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature(
+ [
+ Register('S', self.index_dtype, shape=(self.ell,)),
+ Register('k', self.index_dtype, shape=(self.ell,)),
+ Register('T', self.index_dtype, shape=(self.ell,)),
+ Register('flag', QBit()),
+ ]
+ )
+
+ @property
+ def index_dtype(self) -> QUInt:
+ return QUInt(self.inst.index_bitsize)
+
+ def adjoint(self) -> 'ColumnOfKthNonZeroEntry':
+ return self
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT':
+ m = self.inst.num_unique_constraints
+ ell, k = self.ell, self.inst.k
+
+ counts_forward = Counter[Bloq]()
+
+ # compute symmetric differences for each constraint
+ counts_forward[SymmetricDifference(ell, k, ell, self.index_dtype)] += m
+
+ # counter
+ counts_forward[AddK(self.index_dtype, 1).controlled()] += m
+
+ # compare counter each time
+ counts_forward[Equals(self.index_dtype)] += m
+
+ # when counter is equal (and updated in this iteration), we can copy the result
+ counts_forward[And()] += m
+ counts_forward[CNOT()] += m # flip the final flag (flipped at most once)
+
+ ### all counts
+ counts = Counter[Bloq]()
+
+ # copy the index (controlled by the final flag)
+ counts[Xor(self.index_dtype).controlled()] += m
+
+ # if nothing matched (final flag = 0), copy k and flip the flag bit
+ counts[Xor(self.index_dtype).controlled()] += 1
+ counts[Xor(QBit())] += 1
+
+ for bloq, nb in counts_forward.items():
+ # compute and uncompute all intermediate values.
+ counts[bloq] += nb
+ counts[bloq.adjoint()] += nb
+
+ return counts
+
+
+@bloq_example
+def _col_kth_nz() -> ColumnOfKthNonZeroEntry:
+ from qualtran.bloqs.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(
+ [
+ Register('S', self.index_dtype, shape=(self.ell,)),
+ Register('k', self.index_dtype, shape=(self.ell,)),
+ Register('T', self.index_dtype, shape=(self.ell,)),
+ Register('flag', QBit()),
+ ]
+ )
+
+ @property
+ def index_dtype(self) -> QUInt:
+ return QUInt(self.inst.index_bitsize)
+
+ def adjoint(self) -> 'IndexOfNonZeroColumn':
+ return self
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT':
+ m = self.inst.num_unique_constraints
+ ell, k = self.ell, self.inst.k
+
+ counts_forward = Counter[Bloq]()
+
+ # compute symmetric differences for each constraint
+ counts_forward[SymmetricDifference(ell, k, ell, self.index_dtype)] += m
+
+ # counter
+ counts_forward[AddK(self.index_dtype, 1).controlled()] += m
+
+ # compare T to f_S_k each time
+ counts_forward[Equals(self.index_dtype)] += m
+
+ # when T is equal (and counter is updated in this iteration), we can copy the result
+ counts_forward[And()] += m
+ counts_forward[CNOT()] += m # flip the final flag (flipped at most once)
+
+ ### all counts
+ counts = Counter[Bloq]()
+
+ # copy the value of nnz (when final flag = 1)
+ counts[Xor(self.index_dtype).controlled()] += m
+
+ # if nothing matched (final flag = 0), copy k and flip the flag bit
+ counts[Xor(self.index_dtype).controlled()] += 1
+ counts[Xor(QBit())] += 1
+
+ for bloq, nb in counts_forward.items():
+ # compute and uncompute all intermediate values.
+ counts[bloq] += nb
+ counts[bloq.adjoint()] += nb
+
+ return counts
+
+
+@frozen
+class KikuchiNonZeroIndex(Bloq):
+ r"""Adjacency list oracle $O_F$ for the Kikuchi matrix.
+
+ The oracle $O_F$ (Definition 4.5) takes in $i, k$,
+ and outputs $i, f(i, k)$ where $f(i, k)$ is
+ index of the $k$-th non-zero entry in row $i$.
+
+ As the Kikuchi matrix is symmetric, we can use the same oracle for both rows and columns.
+
+ The Kikuchi matrix is indexed by $S \in {[n] \choose k}$.
+ For a given row $S$ and column $T$, the entry $\mathcal{K}_{k}_{S, T}$
+ is potentially non-zero if $S \Delta T = U_j$ for some $j$, which is
+ equivalent to $T = S \Delta U_j$.
+ Here, $U_j$ is the $j$-th unique scope in the instance $\mathcal{I}$.
+
+ To find the $k$-th non-zero entry, we use two oracles:
+ 1. $(S, k) \mapsto f(S, k)$, implemented by `ColumnOfKthNonZeroEntry`
+ 2. $(S, f(S, k)) \mapsto k$, implemented by `IndexOfNonZeroColumn`.
+
+ Both these above oracles are unitary: they do not have any entangled ancilla/junk registers.
+
+
+ Note on sparsity: This bloq expects the user to provide the sparsity, as it is in general
+ difficult to compute the precise sparsity of the Kikuchi matrix efficiently. As long as the
+ provided number is at least the true sparsity, the algorithm will work as expected.
+ In case the provides sparsity is smaller, it is equivalent to making the remaining entries zero in the final block encoding.
+
+ Args:
+ inst: the kXOR instance $\mathcal{I}$.
+ ell: Kikuchi parameter $\ell$.
+ s: sparsity, i.e. max number of non-zero entries in a row/column.
+
+ Registers:
+ i: integer in [2^N]
+ k: integer in [2^N]
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Theorem 4.17, proof para 4 (top of page 39).
+ """
+
+ inst: KXorInstance
+ ell: SymbolicInt
+ s: SymbolicInt
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature(
+ [
+ Register('S', self.index_dtype, shape=(self.ell,)),
+ Register('k', self.index_dtype, shape=(self.ell,)),
+ ]
+ )
+
+ @property
+ def index_dtype(self) -> QUInt:
+ return QUInt(self.inst.index_bitsize)
+
+ def build_composite_bloq(
+ self, bb: 'BloqBuilder', S: 'Soquet', k: 'Soquet'
+ ) -> dict[str, 'SoquetT']:
+ T = np.array([bb.allocate(dtype=self.index_dtype) for _ in range(int(self.ell))])
+ flag = bb.add(ZeroState())
+ S, k, T, flag = bb.add(
+ ColumnOfKthNonZeroEntry(self.inst, self.ell), S=S, k=k, T=T, flag=flag
+ )
+ S, T, k, flag = bb.add(IndexOfNonZeroColumn(self.inst, self.ell), S=S, T=T, k=k, flag=flag)
+ for soq in k:
+ bb.free(soq)
+ bb.add(ZeroEffect(), q=flag)
+ return dict(S=S, k=T)
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT':
+ return {
+ ColumnOfKthNonZeroEntry(self.inst, self.ell): 1,
+ IndexOfNonZeroColumn(self.inst, self.ell): 1,
+ }
+
+
+@bloq_example
+def _kikuchi_nonzero_index() -> KikuchiNonZeroIndex:
+ from qualtran.bloqs.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..cc87eb8c6
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_adjacency_matrix.py
@@ -0,0 +1,179 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from collections import Counter
+from functools import cached_property
+
+import attrs
+import sympy
+from attrs import frozen
+
+from qualtran import (
+ AddControlledT,
+ Bloq,
+ bloq_example,
+ BloqDocSpec,
+ CtrlSpec,
+ QAny,
+ QBit,
+ QFxp,
+ Signature,
+)
+from qualtran.bloqs.arithmetic.lists import SymmetricDifference
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import SymbolicInt
+
+from .kxor_instance import KXorInstance
+from .load_kxor_instance import LoadUniqueScopeIndex, PRGAUniqueConstraintRHS
+
+
+@frozen
+class KikuchiMatrixEntry(Bloq):
+ r"""Adjacency matrix oracle for the Kikuchi matrix.
+
+ Given a kXOR instance $\mathcal{I}$ with $n$ variables, $m$ constraints,
+ the Kikuchi matrix with parameter $\ell$ is indexed by ${[n] \choose l}$.
+ For $S, T \in {[n] \choose l}$, the entry is given by
+ $H_{S, T} = B_{\mathcal{I}}(S \Delta T)/M$, where $M$ is the max entry.
+
+ This bloq implements the transform:
+ $$
+ |0 \rangle |S\rangle |T\rangle
+ \mapsto
+ (\sqrt{H_{S, T}}|0\rangle + \sqrt{1 - |H_{S, T}|}|1\rangle)|S\rangle |T\rangle
+ $$
+
+ This is equivalent to $O_H$ (Def. 4.3) from the paper, but is optimized to classically
+ compute the `arccos` of the entries, and directly apply the rotation,
+ instead of computing them using a quantum circuit.
+
+ This bloq performs the following steps
+ 1. Compute the symmetric difference $D = S \Delta T$.
+ 2. Compute the index $j$ s.t. $U_j = D$ (where $U_j$ are a list of unique scopes)
+ 4. Apply a controlled Y-rotation with angle for the $j$-th entry.
+ 5. Uncompute steps 3, 2, 1.
+
+ Args:
+ inst: k-XOR instance
+ ell: the Kikuchi parameter $\ell$, must be a multiple of $k$.
+ entry_bitsize: number of bits to approximate each rotation angle to.
+ cv: single bit control value (0 or 1), or None for uncontrolled (default).
+
+ Registers:
+ S: row index
+ T: column index
+ q: the qubit to rotate by $Ry(2 \arccos(\sqrt{H_{S,T} / M}))$ as defined above.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Definition 4.3. Theorem 4.17 para 3.
+ """
+
+ inst: KXorInstance
+ ell: SymbolicInt
+ entry_bitsize: SymbolicInt
+ is_controlled: bool = False
+
+ @property
+ def signature(self) -> 'Signature':
+ return Signature.build_from_dtypes(
+ ctrl=QAny(1 if self.is_controlled else 0),
+ S=QAny(self.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.is_controlled:
+ rotation = rotation.controlled()
+ counts[rotation] += 1
+
+ return counts
+
+ def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> tuple['Bloq', 'AddControlledT']:
+ from qualtran.bloqs.mcmt.specialized_ctrl import get_ctrl_system_1bit_cv_from_bloqs
+
+ ctrl_bit, ctrl_bloq = (
+ (1, self) if self.is_controlled else (None, attrs.evolve(self, is_controlled=True))
+ )
+
+ return get_ctrl_system_1bit_cv_from_bloqs(
+ self,
+ ctrl_spec,
+ current_ctrl_bit=ctrl_bit,
+ bloq_with_ctrl=ctrl_bloq,
+ ctrl_reg_name='ctrl',
+ )
+
+
+@bloq_example
+def _kikuchi_matrix_entry() -> KikuchiMatrixEntry:
+ from qualtran.bloqs.max_k_xor_sat.kxor_instance import example_kxor_instance
+
+ inst = example_kxor_instance()
+ ell = 8
+
+ kikuchi_matrix_entry = KikuchiMatrixEntry(inst, ell, entry_bitsize=3)
+ return kikuchi_matrix_entry
+
+
+@bloq_example
+def _kikuchi_matrix_entry_symb() -> KikuchiMatrixEntry:
+ from qualtran.bloqs.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..e3f240389
--- /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=12518, measurement=1301, rotation=ANY
+ )
+
+
+def test_cost_symb():
+ bloq = _kikuchi_matrix_entry_symb()
+ n, m, k, c = sympy.symbols("n m k c", positive=True, integer=True)
+
+ l = c * k
+ logl = ceil(log2(l))
+ logn = ceil(log2(n))
+ logm = ceil(log2(m))
+
+ gc = get_cost_value(bloq, QECGatesCost())
+ assert gc == GateCounts(
+ cswap=4 * l * (logl + 1) * logn,
+ and_bloq=(
+ 4 * l * ((2 * logn + 1) * (logl + 1))
+ + 4 * l
+ + 2 * m * (k * logn - 1)
+ + 2 * m
+ + 4 * ((2 * l - 1) * (logn - 1))
+ + logm
+ + 4 * ceil(log2(2 * l))
+ - 10
+ ),
+ rotation=ANY,
+ clifford=ANY,
+ measurement=ANY,
+ )
+
+ assert big_O(gc.total_t_count()) == big_O(l * logn * logl + k * m * logn)
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..19b0c849b
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding.ipynb
@@ -0,0 +1,1221 @@
+{
+ "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 the same oracle for both rows and columns.\n",
+ "\n",
+ "The Kikuchi matrix is indexed by $S \\in {[n] \\choose k}$.\n",
+ "For a given row $S$ and column $T$, the entry $\\mathcal{K}_{k}_{S, T}$\n",
+ "is potentially non-zero if $S \\Delta T = U_j$ for some $j$, which is\n",
+ "equivalent to $T = S \\Delta U_j$.\n",
+ "Here, $U_j$ is the $j$-th unique scope in the instance $\\mathcal{I}$.\n",
+ "\n",
+ "To find the $k$-th non-zero entry, we use two oracles:\n",
+ "1. $(S, k) \\mapsto f(S, k)$, implemented by `ColumnOfKthNonZeroEntry`\n",
+ "2. $(S, f(S, k)) \\mapsto k$, implemented by `IndexOfNonZeroColumn`.\n",
+ "\n",
+ "Both these above oracles are unitary: they do not have any entangled ancilla/junk registers.\n",
+ "\n",
+ "\n",
+ "Note on sparsity: This bloq expects the user to provide the sparsity, as it is in general\n",
+ "difficult to compute the precise sparsity of the Kikuchi matrix efficiently. As long as the\n",
+ "provided number is at least the true sparsity, the algorithm will work as expected.\n",
+ "In case the provides sparsity is smaller, it is equivalent to making the remaining entries zero in the final block encoding.\n",
+ "\n",
+ "#### Parameters\n",
+ " - `inst`: the kXOR instance $\\mathcal{I}$.\n",
+ " - `ell`: Kikuchi parameter $\\ell$.\n",
+ " - `s`: sparsity, i.e. max number of non-zero entries in a row/column. \n",
+ "\n",
+ "#### Registers\n",
+ " - `i`: integer in [2^N]\n",
+ " - `k`: integer in [2^N] \n",
+ "\n",
+ "#### References\n",
+ " - [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1). Theorem 4.17, proof para 4 (top of page 39).\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "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)"
+ ]
+ },
+ {
+ "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 example_kxor_instance\n",
+ "\n",
+ "inst = example_kxor_instance()\n",
+ "ell = 8\n",
+ "\n",
+ "kikuchi_matrix = KikuchiHamiltonian(inst, ell)"
+ ]
+ },
+ {
+ "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 example_kxor_instance\n",
+ "\n",
+ "inst = example_kxor_instance()\n",
+ "ell = 8\n",
+ "\n",
+ "kikuchi_matrix_entry = KikuchiMatrixEntry(inst, ell, entry_bitsize=3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "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 example_kxor_instance\n",
+ "\n",
+ "inst = example_kxor_instance()\n",
+ "ell = 8\n",
+ "s = inst.brute_force_sparsity(ell)\n",
+ "\n",
+ "kikuchi_nonzero_index = KikuchiNonZeroIndex(inst, ell, s=s)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "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..b5c567f8f
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/kikuchi_block_encoding.py
@@ -0,0 +1,236 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Section 4.4.2 Simulating the Kikuchi Hamiltonian
+
+This module contains oracles to implement the block-encoding of the Kikuchi
+Hamiltonian corresponding to an input k-XOR-SAT instance.
+
+References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Section 4.4.2 for algorithm. Section 2.4 for definitions and notation.
+"""
+from functools import cached_property
+
+import sympy
+from attrs import field, frozen
+
+from qualtran import (
+ bloq_example,
+ BloqBuilder,
+ BloqDocSpec,
+ BQUInt,
+ QAny,
+ QBit,
+ QUInt,
+ Signature,
+ Soquet,
+ SoquetT,
+)
+from qualtran.bloqs.block_encoding import BlockEncoding
+from qualtran.bloqs.block_encoding.sparse_matrix import RowColumnOracle
+from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import (
+ SparseMatrixHermitian,
+ SqrtEntryOracle,
+)
+from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import is_symbolic, SymbolicFloat, SymbolicInt
+
+from .kikuchi_adjacency_list import KikuchiNonZeroIndex
+from .kikuchi_adjacency_matrix import KikuchiMatrixEntry
+from .kxor_instance import KXorInstance
+
+
+@frozen
+class BlackBoxKikuchiEntryOracle(SqrtEntryOracle):
+ r"""Wrapper around the adjacency matrix oracle $O_H$ of the Kikuchi graph."""
+
+ O_H: KikuchiMatrixEntry
+
+ @cached_property
+ def signature(self) -> Signature:
+ return Signature.build_from_dtypes(
+ q=QBit(), i=QAny(self.system_bitsize), j=QAny(self.system_bitsize)
+ )
+
+ @property
+ def system_bitsize(self) -> SymbolicInt:
+ return self.O_H.index_bitsize
+
+ @property
+ def epsilon(self) -> SymbolicFloat:
+ """precision due to fixed-point approximation of entries.
+
+ In the good case, whp (i.e. 1 - o(1)), the entries are in [-2, 2],
+ whose corresponding angles can be represented exactly with 3 bits.
+ I.e. `arccos(sqrt(x / 2)) / pi` for `x in [-2, 2]` are `2, 1.5, 1, 0.5, 0`.
+ """
+ return 0
+
+ @property
+ def _phasegrad_bitsize(self) -> SymbolicInt:
+ return self.O_H.entry_bitsize
+
+ def build_composite_bloq(
+ self, bb: 'BloqBuilder', q: 'Soquet', i: 'Soquet', j: 'Soquet'
+ ) -> dict[str, 'SoquetT']:
+ i, j, q = bb.add(self.O_H, S=i, T=j, q=q)
+ return dict(q=q, i=i, j=j)
+
+
+@frozen
+class BlackBoxKikuchiRowColumnOracle(RowColumnOracle):
+ r"""Wrapper around the adjacency list oracle $O_F$ of the Kikuchi graph."""
+
+ O_F: KikuchiNonZeroIndex
+
+ @cached_property
+ def signature(self) -> Signature:
+ return Signature.build_from_dtypes(
+ l=BQUInt(self.system_bitsize, self.num_nonzero), i=QUInt(self.system_bitsize)
+ )
+
+ @property
+ def system_bitsize(self) -> SymbolicInt:
+ return self.O_F.index_dtype.num_qubits * self.O_F.ell
+
+ @property
+ def num_nonzero(self) -> SymbolicInt:
+ return self.O_F.s
+
+ def build_composite_bloq(
+ self, bb: 'BloqBuilder', l: 'Soquet', i: 'Soquet'
+ ) -> dict[str, 'SoquetT']:
+ i, l = bb.add(self.O_F, S=i, k=l)
+ return dict(l=l, i=i)
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT':
+ return {self.O_F: 1}
+
+
+@frozen
+class KikuchiHamiltonian(BlockEncoding):
+ r"""Block encoding of the Kikuchi matrix $\mathcal{K}_\ell$.
+
+ This is implemented by a sparse matrix block encoding using the adjacency matrix
+ and adjacency list oracles.
+
+ This assumes a default sparsity of $\bar{m}$, which is the number of unique
+ scopes in the instance $\mathcal{I}$.
+ If a better bound on sparsity is known, it can be passed in by the user.
+
+ Args:
+ inst: kXOR instance $\mathcal{I}$.
+ ell: Kikuchi parameter $\ell$.
+ entry_bitsize: Number of bits $b$ to approximate the matrix entries (angles) to.
+ s: sparsity of the Kikuchi matrix, defaults to $\bar{m}$.
+ """
+
+ inst: KXorInstance
+ ell: SymbolicInt
+ entry_bitsize: SymbolicInt = field()
+ s: SymbolicInt = field()
+
+ @s.default
+ def _default_sparsity(self) -> SymbolicInt:
+ return self.inst.num_unique_constraints
+
+ @entry_bitsize.default
+ def _default_entry_bitsize(self):
+ if is_symbolic(self.inst.max_rhs) or self.inst.max_rhs == 2:
+ # one T gate suffices!
+ return 3
+ raise ValueError("Entries outside range [-2, 2], please specify an explicit entry_bitsize.")
+
+ @cached_property
+ def signature(self) -> 'Signature':
+ return Signature.build(
+ system=self.system_bitsize, ancilla=self.ancilla_bitsize, resource=self.resource_bitsize
+ )
+
+ @cached_property
+ def _sparse_matrix_encoding(self) -> SparseMatrixHermitian:
+ blackbox_O_F = BlackBoxKikuchiRowColumnOracle(self.oracle_O_F)
+ blackbox_O_H = BlackBoxKikuchiEntryOracle(self.oracle_O_H)
+ return SparseMatrixHermitian(
+ col_oracle=blackbox_O_F, entry_oracle=blackbox_O_H, eps=blackbox_O_H.epsilon
+ )
+
+ @cached_property
+ def oracle_O_H(self) -> KikuchiMatrixEntry:
+ r"""Maps $|i, j\rangle |0\rangle$ to $|i, j\rangle (\sqrt{A_{ij}} |0\rangle + \sqrt{1 - |A_{ij}|} |1\rangle)"""
+ return KikuchiMatrixEntry(inst=self.inst, ell=self.ell, entry_bitsize=self.entry_bitsize)
+
+ @cached_property
+ def oracle_O_F(self) -> KikuchiNonZeroIndex:
+ r"""Maps `i, k` to `i, f(i, k)` where `f(i, k)` is the column of the `k`-th nonzero entry in row `i`."""
+ return KikuchiNonZeroIndex(inst=self.inst, ell=self.ell, s=self.s)
+
+ @property
+ def alpha(self) -> SymbolicFloat:
+ return self._sparse_matrix_encoding.alpha
+
+ @property
+ def system_bitsize(self) -> SymbolicInt:
+ return self._sparse_matrix_encoding.system_bitsize
+
+ @property
+ def ancilla_bitsize(self) -> SymbolicInt:
+ return self._sparse_matrix_encoding.ancilla_bitsize
+
+ @property
+ def resource_bitsize(self) -> SymbolicInt:
+ return self._sparse_matrix_encoding.resource_bitsize
+
+ @property
+ def epsilon(self) -> SymbolicFloat:
+ return self._sparse_matrix_encoding.epsilon
+
+ @property
+ def signal_state(self) -> BlackBoxPrepare:
+ return self._sparse_matrix_encoding.signal_state
+
+ def build_composite_bloq(self, bb: 'BloqBuilder', **soqs: 'SoquetT') -> dict[str, 'SoquetT']:
+ return bb.add_d(self._sparse_matrix_encoding, **soqs)
+
+ def __str__(self):
+ return 'B[K_l]'
+
+
+@bloq_example
+def _kikuchi_matrix() -> KikuchiHamiltonian:
+ from qualtran.bloqs.max_k_xor_sat.kxor_instance import example_kxor_instance
+
+ inst = example_kxor_instance()
+ ell = 8
+
+ kikuchi_matrix = KikuchiHamiltonian(inst, ell)
+ return kikuchi_matrix
+
+
+@bloq_example
+def _kikuchi_matrix_symb() -> KikuchiHamiltonian:
+ from qualtran.bloqs.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)
+ 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/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..62eaeac33
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/load_kxor_instance.py
@@ -0,0 +1,354 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+r"""We define three oracles that load a kXOR instance, which are used in the algorithm.
+
+We are given a kXOR instance $\mathcal{I}$ of $n$ variables,
+with $\bar{m}$ unique scopes $\{U_j | j \in [\bar{m}]\}$.
+We provide oracles to:
+1. `LoadConstraintScopes`: Given $j \in [\bar{m}]$, compute $U_j$.
+2. `LoadUniqueScopeIndex`: Given $U_j$, compute $j \in [\bar{m}]$
+3. `PRGAUniqueConstraintRHS` Given $j$, apply $Rx(arccos(\sqrt{B_\mathcal{I}(S)/M}))$ on a target qubit.
+(for an appropriate normalization $M$).
+
+
+The first two oracles are independent of the RHS.
+All these oracles can output arbitrary values for invalid inputs.
+
+References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Notation 2.24 for $B_\mathcal{I}$.
+ Theorem 4.17, proof para 2 for $U_j$.
+"""
+from functools import cached_property
+from typing import Counter, Sequence, Union
+
+import attrs
+import numpy as np
+from attrs import frozen
+
+from qualtran import (
+ AddControlledT,
+ Bloq,
+ bloq_example,
+ BloqBuilder,
+ BloqDocSpec,
+ BQUInt,
+ CtrlSpec,
+ DecomposeTypeError,
+ QAny,
+ QBit,
+ QFxp,
+ Register,
+ Side,
+ Signature,
+ Soquet,
+ SoquetT,
+)
+from qualtran.bloqs.arithmetic import EqualsAConstant, LessThanConstant
+from qualtran.bloqs.basic_gates import Hadamard, SGate
+from qualtran.bloqs.bookkeeping import Partition
+from qualtran.bloqs.data_loading import QROM
+from qualtran.bloqs.rotations.rz_via_phase_gradient import RzViaPhaseGradient
+from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator
+from qualtran.symbolics import ceil, HasLength, is_symbolic, is_zero, log2, SymbolicInt
+
+from .kxor_instance import KXorInstance
+
+
+@frozen
+class LoadConstraintScopes(Bloq):
+ r"""Given an index $j$, load the scope of the $j$-th unique constraint.
+
+ Given a $k$-XOR-SAT instance `inst` with $n$ variables and $m$ constraints.
+ Assuming `inst` has $\bar{m}$ unique constraints, we define $U_j \in {[n] \choose k}$
+ for $j \in [\bar{m}]$ as the $j$-th unique constraint scope.
+
+ The scopes are loaded using a QROM.
+
+ If the input contains an invalid index, then any arbitrary value can be output.
+
+ Registers:
+ j: a number in [\bar{m}]
+ U (RIGHT): $j$-th unique scope
+ ancilla (RIGHT): entangled intermediate qubits, to be uncomputed by the adjoint.
+
+ References:
+ [Quartic quantum speedups for planted inference](https://arxiv.org/abs/2406.19378v1)
+ Theorem 4.17, proof para 2.
+ """
+
+ inst: KXorInstance
+
+ @cached_property
+ def signature(self) -> 'Signature':
+ registers: list[Register] = [
+ Register('j', self.m_dtype),
+ Register('U', QAny(self.scope_bitsize), side=Side.RIGHT),
+ ]
+
+ if not is_zero(self.ancilla_bitsize):
+ registers.append(Register('ancilla', QAny(self.ancilla_bitsize), side=Side.RIGHT))
+
+ return Signature(registers)
+
+ @cached_property
+ def scope_bitsize(self) -> SymbolicInt:
+ """total number of bits to store `k` indices in `[n]`."""
+ return self.inst.k * self.inst.index_bitsize
+
+ @cached_property
+ def ancilla_bitsize(self) -> SymbolicInt:
+ """ancillas used by the underlying QRO(A)M"""
+ return 0
+
+ @cached_property
+ def m_dtype(self):
+ r"""number of bits to store $j \in [\bar{m}]$."""
+ m = self.inst.num_unique_constraints
+ bitsize = ceil(log2(m))
+ return BQUInt(bitsize, m)
+
+ @cached_property
+ def _qrom_bloq(self) -> QROM:
+ # TODO use QROAMClean?
+
+ if self.inst.is_symbolic():
+ return QROM.build_from_bitsize(self.inst.num_unique_constraints, self.scope_bitsize)
+
+ assert isinstance(self.inst.batched_scopes, tuple)
+ scopes = np.array([S for S, _ in self.inst.batched_scopes], dtype=int)
+ assert scopes.shape == (self.inst.num_unique_constraints, self.inst.k)
+ return QROM.build_from_data(
+ *scopes.T, target_bitsizes=(self.inst.index_bitsize,) * self.inst.k
+ )
+
+ def build_composite_bloq(self, bb: 'BloqBuilder', j: 'Soquet') -> dict[str, 'SoquetT']:
+ if self.inst.is_symbolic():
+ raise DecomposeTypeError(f"cannot decompose symbolic {self}")
+
+ targets = {
+ f'target{i}_': bb.allocate(self.inst.index_bitsize) for i in range(int(self.inst.k))
+ }
+ targets = bb.add_d(self._qrom_bloq, selection=j, **targets)
+ j = targets.pop('selection')
+
+ U = bb.add(
+ Partition(self.scope_bitsize, self._qrom_bloq.target_registers).adjoint(), **targets
+ )
+ return {'j': j, 'U': U}
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
+ return {self._qrom_bloq: 1}
+
+
+@bloq_example
+def _load_scopes() -> LoadConstraintScopes:
+ from qualtran.bloqs.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
+ is_controlled: bool = False
+
+ @cached_property
+ def signature(self) -> 'Signature':
+ return Signature.build_from_dtypes(ctrl=QAny(self.n_ctrl), j=self.m_dtype, q=QBit())
+
+ @property
+ def n_ctrl(self) -> int:
+ return 1 if self.is_controlled else 0
+
+ @cached_property
+ def m_dtype(self):
+ r"""number of bits to store $j \in [\bar{m}]$."""
+ m = self.inst.num_unique_constraints
+ bitsize = ceil(log2(m))
+ return BQUInt(bitsize, m)
+
+ @cached_property
+ def _angle_dtype(self):
+ return QFxp(self.angle_bitsize, self.angle_bitsize)
+
+ @cached_property
+ def _qrom_angle_data(
+ self,
+ ) -> tuple[Union[HasLength, Sequence[int]], Union[HasLength, Sequence[int]]]:
+ M = self.inst.max_rhs
+ scopes = self.inst.batched_scopes
+ if is_symbolic(M) or is_symbolic(scopes):
+ m = self.inst.num_unique_constraints
+ return HasLength(m), HasLength(m)
+
+ b = [b for _, b in scopes]
+ assert np.all(b == np.sort(b)), "data must be sorted!"
+
+ amplitude_angles = np.arccos(np.sqrt(np.abs(b) / M))
+ amplitude_angles_int = np.round(amplitude_angles * 2**self.angle_bitsize)
+
+ signs = tuple(np.sign(b))
+ return amplitude_angles_int, signs
+
+ @cached_property
+ def _amplitude_qrom(self) -> QROM:
+ data, _ = self._qrom_angle_data
+ if is_symbolic(data):
+ return QROM.build_from_bitsize(
+ data_len_or_shape=self.inst.num_unique_constraints,
+ target_bitsizes=self.angle_bitsize,
+ num_controls=self.n_ctrl,
+ )
+
+ return QROM.build_from_data(
+ data, target_bitsizes=(self.angle_bitsize,), num_controls=self.n_ctrl
+ )
+
+ @cached_property
+ def _num_negative(self) -> SymbolicInt:
+ """returns $k$ s.t. the first $k$ elements are negative."""
+ _, signs = self._qrom_angle_data
+ if is_symbolic(signs):
+ return self.inst.num_unique_constraints // 2
+
+ assert np.all(signs == np.sort(signs)), "data must be sorted!"
+ return int(np.searchsorted(signs, 0))
+
+ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> BloqCountDictT:
+ counts = Counter[Bloq]()
+
+ # load the amplitudes
+ counts[self._amplitude_qrom] += 1
+
+ # apply a Rx rotation using Rx = H Rz H
+ counts[Hadamard()] += 2
+ counts[RzViaPhaseGradient(self._angle_dtype, self._angle_dtype)] += 1
+
+ # apply the sign correction
+ # TODO use the half-bloq once implemented to wire this correctly
+ sign_compare = LessThanConstant(self.m_dtype.num_qubits, self._num_negative)
+ counts[sign_compare] += 1
+ counts[SGate().controlled()] += 1
+
+ # unload amplitudes
+ counts[self._amplitude_qrom.adjoint()] += 1
+
+ return counts
+
+ def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> tuple['Bloq', 'AddControlledT']:
+ from qualtran.bloqs.mcmt.specialized_ctrl import get_ctrl_system_1bit_cv_from_bloqs
+
+ return get_ctrl_system_1bit_cv_from_bloqs(
+ self,
+ ctrl_spec,
+ current_ctrl_bit=1 if self.is_controlled else None,
+ bloq_with_ctrl=self if self.is_controlled else attrs.evolve(self, is_controlled=True),
+ ctrl_reg_name='ctrl',
+ )
diff --git a/qualtran/bloqs/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..37a415332
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/planted_noisy_kxor_test.py
@@ -0,0 +1,116 @@
+# 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):
+ if c * k == 32 and n <= 40:
+ pytest.skip("too small n")
+
+ bloq = example_random_instance(k=k, c=c, n=n, m=n, seed=142)
+ cost = get_cost_value(bloq, QECGatesCost())
+ print(cost)
+
+
+@pytest.mark.slow
+@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/shims.py b/qualtran/bloqs/max_k_xor_sat/shims.py
new file mode 100644
index 000000000..720998180
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/shims.py
@@ -0,0 +1,77 @@
+# 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 qualtran import Bloq, CtrlSpec, Signature
+from qualtran.resource_counting import CostKey, GateCounts, QECGatesCost
+from qualtran.symbolics import SymbolicInt
+
+
+@attrs.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=1 + self.n_ctrls)
+
+ 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
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..33c53c298
--- /dev/null
+++ b/qualtran/bloqs/max_k_xor_sat/tutorial_guiding_state.ipynb
@@ -0,0 +1,3414 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "82896538a83e86e0",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2024-11-19T18:07:27.043429Z",
+ "start_time": "2024-11-19T18:07:25.335018Z"
+ }
+ },
+ "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": 2,
+ "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": 2,
+ "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": 3,
+ "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": 4,
+ "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": 5,
+ "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": 6,
+ "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": "code",
+ "execution_count": 7,
+ "id": "77bdc034-5517-4828-b16a-c2d8c8a2d6ec",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/svg+xml": [
+ ""
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "show_bloq(guiding_state_concrete.decompose_bloq().flatten_once())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "140a7fb3e4417c87",
+ "metadata": {},
+ "source": [
+ "Let us evaluate the gate cost for the above bloqs."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "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=552, cswap=4320, and_bloq=25175, clifford=170846, rotation=0, measurement=25175)"
+ ]
+ },
+ "execution_count": 8,
+ "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": 9,
+ "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 24 c^{2} k^{2} \\cdot \\left(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(n \\right)}}\\right\\rceil + 1\\right) + 24 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(2 \\left\\lceil{\\operatorname{log}_{2}{\\left(2000000.0 \\pi c \\right)}}\\right\\rceil - 4\\right) + 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) + 4 \\left(c k - 1\\right) \\left\\lceil{\\operatorname{log}_{2}{\\left(n \\right)}}\\right\\rceil - 8$"
+ ],
+ "text/plain": [
+ "24*c**2*k**2*(2*ceiling(log2(n)) + 1) + 24*c**2*k**2*ceiling(log2(n)) + 4*c*k*ceiling(log2(c*k)) + 4*c*k + 4*c*(2*ceiling(log2(2000000.0*pi*c)) - 4) + 4*c*(4*m + (2*m + 1)*(ceiling(log2(floor(2**(k*ceiling(log2(n)))))) - 1) - 4) + 4*(c*k - 1)*ceiling(log2(n)) - 8"
+ ]
+ },
+ "execution_count": 9,
+ "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": 10,
+ "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 + c \\operatorname{log}_{2}{\\left(2000000.0 \\pi c \\right)}; \\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 + c*log2(2000000.0*pi*c), (\\ell, oo), (c, oo), (m, oo), (n, oo))"
+ ]
+ },
+ "execution_count": 10,
+ "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": 11,
+ "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/phase_estimation/lp_resource_state.py b/qualtran/bloqs/phase_estimation/lp_resource_state.py
index 53cadba44..f58776ac0 100644
--- a/qualtran/bloqs/phase_estimation/lp_resource_state.py
+++ b/qualtran/bloqs/phase_estimation/lp_resource_state.py
@@ -140,8 +140,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/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