diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py index 00bde2cb8..a35c468ce 100644 --- a/dev_tools/qualtran_dev_tools/notebook_specs.py +++ b/dev_tools/qualtran_dev_tools/notebook_specs.py @@ -51,6 +51,7 @@ import qualtran.bloqs.block_encoding.phase import qualtran.bloqs.block_encoding.product import qualtran.bloqs.block_encoding.sparse_matrix +import qualtran.bloqs.block_encoding.sparse_matrix_hermitian import qualtran.bloqs.block_encoding.tensor_product import qualtran.bloqs.block_encoding.unitary import qualtran.bloqs.bookkeeping @@ -740,6 +741,13 @@ module=qualtran.bloqs.block_encoding.sparse_matrix, bloq_specs=[qualtran.bloqs.block_encoding.sparse_matrix._SPARSE_MATRIX_DOC], ), + NotebookSpecV2( + title='Sparse Matrix Hermitian', + module=qualtran.bloqs.block_encoding.sparse_matrix_hermitian, + bloq_specs=[ + qualtran.bloqs.block_encoding.sparse_matrix_hermitian._SPARSE_MATRIX_HERMITIAN_DOC + ], + ), NotebookSpecV2( title='Chebyshev Polynomial', module=qualtran.bloqs.block_encoding.chebyshev_polynomial, diff --git a/docs/bloqs/index.rst b/docs/bloqs/index.rst index 7d7f1a27f..d827dfaad 100644 --- a/docs/bloqs/index.rst +++ b/docs/bloqs/index.rst @@ -130,6 +130,7 @@ Bloqs Library block_encoding/phase.ipynb block_encoding/linear_combination.ipynb block_encoding/sparse_matrix.ipynb + block_encoding/sparse_matrix_hermitian.ipynb block_encoding/chebyshev_polynomial.ipynb block_encoding/lcu_block_encoding.ipynb diff --git a/qualtran/bloqs/block_encoding/__init__.py b/qualtran/bloqs/block_encoding/__init__.py index 6bf24ff4a..03c07b298 100644 --- a/qualtran/bloqs/block_encoding/__init__.py +++ b/qualtran/bloqs/block_encoding/__init__.py @@ -23,5 +23,6 @@ from qualtran.bloqs.block_encoding.phase import Phase from qualtran.bloqs.block_encoding.product import Product from qualtran.bloqs.block_encoding.sparse_matrix import SparseMatrix +from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import SparseMatrixHermitian from qualtran.bloqs.block_encoding.tensor_product import TensorProduct from qualtran.bloqs.block_encoding.unitary import Unitary diff --git a/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.ipynb b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.ipynb new file mode 100644 index 000000000..117edcce9 --- /dev/null +++ b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.ipynb @@ -0,0 +1,204 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8e5e678f", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# Sparse Matrix Hermitian" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70980f2b", + "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": "8db414b7", + "metadata": { + "cq.autogen": "SparseMatrixHermitian.bloq_doc.md" + }, + "source": [ + "## `SparseMatrixHermitian`\n", + "Hermitian Block encoding of a sparse-access Hermitian matrix.\n", + "\n", + "Given column and entry oracles $O_c$ and $O_A$ for an $s$-sparse Hermitian matrix\n", + "$A \\in \\mathbb{C}^{2^n \\times 2^n}$, i.e. one where each row / column has exactly $s$ non-zero\n", + "entries, computes a $(s, n+1, \\epsilon)$-block encoding of $A$ as follows:\n", + "```\n", + " ┌────┐\n", + "a |0> ─┤ ├─ |0> ───────────────────────X────────────────────\n", + " │ │ ┌──┐ | ┌──┐\n", + " │ U │ = │ n│ ┌────┐ ┌────┐ | ┌────┐ ┌────┐ │ n│\n", + "l |0^n> ─┤ A ├─ |0^n> ─┤H ├─┤ O ├─┤ ├─X──|─┤ ├─┤ O* ├─┤H ├─\n", + " │ │ └──┘ | c | │ │ | | │ │ | c | └──┘\n", + " │ │ └────┘ │ O │ │ | │ O* │ └────┘\n", + "b |0> ─┤ ├─ |0> ────────|────┤ A ├─|──X─┤ A ├───|─────────\n", + " | | ┌────┐ | | | | | ┌────┐\n", + " | | | O | | | | | | | O* |\n", + "j |Psi> ─┤ ├─ |Psi> ──────┤ c ├─┤ ├─X────┤ ├─┤ c ├──────\n", + " └────┘ └────┘ └────┘ └────┘ └────┘\n", + "```\n", + "\n", + "To encode a matrix of irregular dimension, the matrix should first be embedded into one of\n", + "dimension $2^n \\times 2^n$ for suitable $n$.\n", + "To encode a matrix where each row / column has at most $s$ non-zero entries, some zeroes should\n", + "be treated as if they were non-zero so that each row / column has exactly $s$ non-zero entries.\n", + "\n", + "For encoding a non-hermitian matrix, or a slightly more efficient (but non Hermitian-encoding)\n", + "of a matrix, use :class:`SparseMatrix` instead.\n", + "\n", + "#### Parameters\n", + " - `col_oracle`: The column oracle $O_c$. See `RowColumnOracle` for definition.\n", + " - `entry_oracle`: The entry oracle $O_A$. See `EntryOracle` for definition.\n", + " - `eps`: The precision of the block encoding.\n", + " - `is_controlled`: if True, returns the controlled block-encoding. \n", + "\n", + "#### Registers\n", + " - `ctrl`: The single qubit control register. (present only if `is_controlled` is `True`)\n", + " - `system`: The system register.\n", + " - `ancilla`: The ancilla register.\n", + " - `resource`: The resource register (present only if `bitsize > 0`). \n", + "\n", + "#### References\n", + " - [Lecture Notes on Quantum Algorithms for Scientific Computation](https://arxiv.org/abs/2201.08309). Lin Lin (2022). Ch. 6.5. Proposition 6.8, Fig 6.7.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f31bfd74", + "metadata": { + "cq.autogen": "SparseMatrixHermitian.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.block_encoding import SparseMatrixHermitian" + ] + }, + { + "cell_type": "markdown", + "id": "435f31d2", + "metadata": { + "cq.autogen": "SparseMatrixHermitian.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "720f3f9b", + "metadata": { + "cq.autogen": "SparseMatrixHermitian.sparse_matrix_symb_hermitian_block_encoding" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.block_encoding.sparse_matrix import TopLeftRowColumnOracle\n", + "from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import UniformSqrtEntryOracle\n", + "\n", + "n = sympy.Symbol('n', positive=True, integer=True)\n", + "col_oracle = TopLeftRowColumnOracle(system_bitsize=n)\n", + "entry_oracle = UniformSqrtEntryOracle(system_bitsize=n, entry=0.3)\n", + "sparse_matrix_symb_hermitian_block_encoding = SparseMatrixHermitian(\n", + " col_oracle, entry_oracle, eps=0\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70e512ff", + "metadata": { + "cq.autogen": "SparseMatrixHermitian.sparse_matrix_hermitian_block_encoding" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.block_encoding.sparse_matrix import TopLeftRowColumnOracle\n", + "from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import UniformSqrtEntryOracle\n", + "\n", + "col_oracle = TopLeftRowColumnOracle(system_bitsize=2)\n", + "entry_oracle = UniformSqrtEntryOracle(system_bitsize=2, entry=0.3)\n", + "sparse_matrix_hermitian_block_encoding = SparseMatrixHermitian(col_oracle, entry_oracle, eps=0)" + ] + }, + { + "cell_type": "markdown", + "id": "6e8d3efa", + "metadata": { + "cq.autogen": "SparseMatrixHermitian.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96d58575", + "metadata": { + "cq.autogen": "SparseMatrixHermitian.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([sparse_matrix_symb_hermitian_block_encoding, sparse_matrix_hermitian_block_encoding],\n", + " ['`sparse_matrix_symb_hermitian_block_encoding`', '`sparse_matrix_hermitian_block_encoding`'])" + ] + }, + { + "cell_type": "markdown", + "id": "e0108dfc", + "metadata": { + "cq.autogen": "SparseMatrixHermitian.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87933b2f", + "metadata": { + "cq.autogen": "SparseMatrixHermitian.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "sparse_matrix_symb_hermitian_block_encoding_g, sparse_matrix_symb_hermitian_block_encoding_sigma = sparse_matrix_symb_hermitian_block_encoding.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(sparse_matrix_symb_hermitian_block_encoding_g)\n", + "show_counts_sigma(sparse_matrix_symb_hermitian_block_encoding_sigma)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.py b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.py new file mode 100644 index 000000000..43ff03547 --- /dev/null +++ b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian.py @@ -0,0 +1,316 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import abc +from collections import Counter +from functools import cached_property + +import attrs +import numpy as np +import sympy +from attrs import frozen + +from qualtran import ( + AddControlledT, + Bloq, + bloq_example, + BloqBuilder, + BloqDocSpec, + CtrlSpec, + DecomposeTypeError, + QAny, + QBit, + Register, + Signature, + Soquet, + SoquetT, +) +from qualtran.bloqs.basic_gates import CSwap, Ry, Swap +from qualtran.bloqs.block_encoding import BlockEncoding +from qualtran.bloqs.block_encoding.sparse_matrix import RowColumnOracle +from qualtran.bloqs.bookkeeping import Partition +from qualtran.bloqs.bookkeeping.auto_partition import AutoPartition, Unused +from qualtran.bloqs.reflections.prepare_identity import PrepareIdentity +from qualtran.bloqs.state_preparation.black_box_prepare import BlackBoxPrepare +from qualtran.bloqs.state_preparation.prepare_uniform_superposition import ( + PrepareUniformSuperposition, +) +from qualtran.resource_counting import BloqCountT, SympySymbolAllocator +from qualtran.symbolics import is_symbolic, SymbolicFloat, SymbolicInt +from qualtran.symbolics.math_funcs import bit_length + + +class SqrtEntryOracle(Bloq): + r"""Oracle specifying the sqrt of entries of a sparse-access matrix. + + In the reference, this is the interface of + $$O_A : \ket{0}\ket{i}\ket{j} \mapsto (\sqrt{A_{ij}} \ket{0} + \sqrt{1 - |A_{ij}|}\ket{i}\ket{j}).$$ + + Registers: + q: The flag qubit that is rotated proportionally to the value of the entry. + i: The row index. + j: The column index. + + References: + [Lecture Notes on Quantum Algorithms for Scientific Computation](https://arxiv.org/abs/2201.08309). Lin Lin (2022). Ch. 6.5. + """ + + @property + @abc.abstractmethod + def system_bitsize(self) -> SymbolicInt: + """The number of bits used to represent an index.""" + + @property + @abc.abstractmethod + def epsilon(self) -> SymbolicFloat: + """The number of bits used to represent an index.""" + + @cached_property + def signature(self) -> Signature: + return Signature.build_from_dtypes( + q=QBit(), i=QAny(self.system_bitsize), j=QAny(self.system_bitsize) + ) + + +@frozen +class SparseMatrixHermitian(BlockEncoding): + r"""Hermitian Block encoding of a sparse-access Hermitian matrix. + + Given column and entry oracles $O_c$ and $O_A$ for an $s$-sparse Hermitian matrix + $A \in \mathbb{C}^{2^n \times 2^n}$, i.e. one where each row / column has exactly $s$ non-zero + entries, computes a $(s, n+1, \epsilon)$-block encoding of $A$ as follows: + ``` + ┌────┐ + a |0> ─┤ ├─ |0> ───────────────────────X──────────────────── + │ │ ┌──┐ | ┌──┐ + │ U │ = │ n│ ┌────┐ ┌────┐ | ┌────┐ ┌────┐ │ n│ + l |0^n> ─┤ A ├─ |0^n> ─┤H ├─┤ O ├─┤ ├─X──|─┤ ├─┤ O* ├─┤H ├─ + │ │ └──┘ | c | │ │ | | │ │ | c | └──┘ + │ │ └────┘ │ O │ │ | │ O* │ └────┘ + b |0> ─┤ ├─ |0> ────────|────┤ A ├─|──X─┤ A ├───|───────── + | | ┌────┐ | | | | | ┌────┐ + | | | O | | | | | | | O* | + j |Psi> ─┤ ├─ |Psi> ──────┤ c ├─┤ ├─X────┤ ├─┤ c ├────── + └────┘ └────┘ └────┘ └────┘ └────┘ + ``` + + To encode a matrix of irregular dimension, the matrix should first be embedded into one of + dimension $2^n \times 2^n$ for suitable $n$. + To encode a matrix where each row / column has at most $s$ non-zero entries, some zeroes should + be treated as if they were non-zero so that each row / column has exactly $s$ non-zero entries. + + For encoding a non-hermitian matrix, or a slightly more efficient (but non Hermitian-encoding) + of a matrix, use :class:`SparseMatrix` instead. + + Args: + col_oracle: The column oracle $O_c$. See `RowColumnOracle` for definition. + entry_oracle: The entry oracle $O_A$. See `EntryOracle` for definition. + eps: The precision of the block encoding. + is_controlled: if True, returns the controlled block-encoding. + + Registers: + ctrl: The single qubit control register. (present only if `is_controlled` is `True`) + system: The system register. + ancilla: The ancilla register. + resource: The resource register (present only if `bitsize > 0`). + + References: + [Lecture Notes on Quantum Algorithms for Scientific Computation](https://arxiv.org/abs/2201.08309). + Lin Lin (2022). Ch. 6.5. Proposition 6.8, Fig 6.7. + """ + + col_oracle: RowColumnOracle + entry_oracle: SqrtEntryOracle + eps: SymbolicFloat + is_controlled: bool = False + + def __attrs_post_init__(self): + if self.col_oracle.system_bitsize != self.entry_oracle.system_bitsize: + raise ValueError("column and entry oracles must have same bitsize") + + @cached_property + def signature(self) -> Signature: + n_ctrls = 1 if self.is_controlled else 0 + + return Signature.build_from_dtypes( + ctrl=QAny(n_ctrls), + system=QAny(self.system_bitsize), + ancilla=QAny(self.ancilla_bitsize), + resource=QAny(self.resource_bitsize), # if resource_bitsize is 0, not present + ) + + @cached_property + def system_bitsize(self) -> SymbolicInt: + return self.entry_oracle.system_bitsize + + def __str__(self) -> str: + return "B[SparseMatrixHermitian]" + + @cached_property + def alpha(self) -> SymbolicFloat: + return self.col_oracle.num_nonzero + + @cached_property + def ancilla_bitsize(self) -> SymbolicInt: + return self.system_bitsize + 2 + + @cached_property + def resource_bitsize(self) -> SymbolicInt: + return 0 + + @cached_property + def epsilon(self) -> SymbolicFloat: + return self.eps + + @property + def signal_state(self) -> BlackBoxPrepare: + return BlackBoxPrepare(PrepareIdentity.from_bitsizes([self.ancilla_bitsize])) + + @cached_property + def diffusion(self): + unused = self.system_bitsize - bit_length(self.col_oracle.num_nonzero - 1) + return AutoPartition( + PrepareUniformSuperposition(n=self.col_oracle.num_nonzero), + partitions=[ + (Register("target", QAny(self.system_bitsize)), [Unused(unused), "target"]) + ], + ) + + def build_call_graph(self, ssa: SympySymbolAllocator) -> set[BloqCountT]: + counts = Counter[Bloq]() + + counts[self.diffusion] += 1 + counts[self.col_oracle] += 1 + counts[self.entry_oracle] += 1 + if self.is_controlled: + counts[CSwap(self.system_bitsize)] += 1 + counts[CSwap(1)] += 1 + else: + counts[Swap(self.system_bitsize)] += 1 + counts[Swap(1)] += 1 + counts[self.entry_oracle.adjoint()] += 1 + counts[self.col_oracle.adjoint()] += 1 + counts[self.diffusion.adjoint()] += 1 + + return set(counts.items()) + + def build_composite_bloq( + self, bb: BloqBuilder, system: SoquetT, ancilla: SoquetT, **soqs + ) -> dict[str, SoquetT]: + if is_symbolic(self.system_bitsize) or is_symbolic(self.col_oracle.num_nonzero): + raise DecomposeTypeError(f"Cannot decompose symbolic {self=}") + + ctrl = soqs.pop('ctrl', None) + + assert not isinstance(ancilla, np.ndarray) + partition_ancilla = Partition( + n=self.ancilla_bitsize, + regs=( + Register('a', QBit()), + Register('l', QAny(self.system_bitsize)), + Register('b', QBit()), + ), + ) + + a, l, b = bb.add(partition_ancilla, x=ancilla) + + l = bb.add(self.diffusion, target=l) + l, system = bb.add(self.col_oracle, l=l, i=system) + b, l, system = bb.add(self.entry_oracle, q=b, i=l, j=system) + + if self.is_controlled: + ctrl, l, system = bb.add(CSwap(self.system_bitsize), ctrl=ctrl, x=l, y=system) + ctrl, a, b = bb.add(CSwap(1), ctrl=ctrl, x=a, y=b) + else: + l, system = bb.add(Swap(self.system_bitsize), x=l, y=system) + a, b = bb.add(Swap(1), x=a, y=b) + + b, l, system = bb.add(self.entry_oracle.adjoint(), q=b, i=l, j=system) + l, system = bb.add(self.col_oracle.adjoint(), l=l, i=system) + l = bb.add(self.diffusion.adjoint(), target=l) + + ancilla = bb.add(partition_ancilla.adjoint(), a=a, l=l, b=b) + + out_soqs = {"system": system, "ancilla": ancilla} + if self.is_controlled: + out_soqs |= {"ctrl": ctrl} + return out_soqs + + def adjoint(self) -> 'SparseMatrixHermitian': + return self + + def get_ctrl_system(self, ctrl_spec: 'CtrlSpec') -> tuple['Bloq', 'AddControlledT']: + from qualtran.bloqs.mcmt.specialized_ctrl import get_ctrl_system_1bit_cv_from_bloqs + + return get_ctrl_system_1bit_cv_from_bloqs( + self, + ctrl_spec, + current_ctrl_bit=1 if self.is_controlled else None, + bloq_with_ctrl=self if self.is_controlled else attrs.evolve(self, is_controlled=True), + ctrl_reg_name='ctrl', + ) + + +@frozen +class UniformSqrtEntryOracle(SqrtEntryOracle): + """Oracle specifying the entries of a matrix with uniform entries.""" + + system_bitsize: SymbolicInt + entry: float + eps: float = 1e-11 + + @property + def epsilon(self) -> SymbolicFloat: + return self.eps + + def build_composite_bloq( + self, bb: BloqBuilder, q: Soquet, **soqs: SoquetT + ) -> dict[str, SoquetT]: + # Either Rx or Ry work here; Rx would induce a phase on the subspace with non-zero ancilla + # See https://arxiv.org/abs/2302.10949 for reference that uses Rx + soqs["q"] = bb.add(Ry(2 * np.arccos(np.sqrt(self.entry))), q=q) + return soqs + + +@bloq_example +def _sparse_matrix_hermitian_block_encoding() -> SparseMatrixHermitian: + from qualtran.bloqs.block_encoding.sparse_matrix import TopLeftRowColumnOracle + from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import UniformSqrtEntryOracle + + col_oracle = TopLeftRowColumnOracle(system_bitsize=2) + entry_oracle = UniformSqrtEntryOracle(system_bitsize=2, entry=0.3) + sparse_matrix_hermitian_block_encoding = SparseMatrixHermitian(col_oracle, entry_oracle, eps=0) + return sparse_matrix_hermitian_block_encoding + + +@bloq_example +def _sparse_matrix_symb_hermitian_block_encoding() -> SparseMatrixHermitian: + from qualtran.bloqs.block_encoding.sparse_matrix import TopLeftRowColumnOracle + from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import UniformSqrtEntryOracle + + n = sympy.Symbol('n', positive=True, integer=True) + col_oracle = TopLeftRowColumnOracle(system_bitsize=n) + entry_oracle = UniformSqrtEntryOracle(system_bitsize=n, entry=0.3) + sparse_matrix_symb_hermitian_block_encoding = SparseMatrixHermitian( + col_oracle, entry_oracle, eps=0 + ) + return sparse_matrix_symb_hermitian_block_encoding + + +_SPARSE_MATRIX_HERMITIAN_DOC = BloqDocSpec( + bloq_cls=SparseMatrixHermitian, + examples=[ + _sparse_matrix_symb_hermitian_block_encoding, + _sparse_matrix_hermitian_block_encoding, + ], +) diff --git a/qualtran/bloqs/block_encoding/sparse_matrix_hermitian_test.py b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian_test.py new file mode 100644 index 000000000..367de6a3a --- /dev/null +++ b/qualtran/bloqs/block_encoding/sparse_matrix_hermitian_test.py @@ -0,0 +1,118 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import cast + +import numpy as np +import sympy + +import qualtran.testing as qlt_testing +from qualtran import BloqBuilder, Soquet +from qualtran.bloqs.basic_gates import Hadamard, IntEffect, IntState +from qualtran.bloqs.block_encoding.sparse_matrix import TopLeftRowColumnOracle +from qualtran.bloqs.block_encoding.sparse_matrix_hermitian import ( + _sparse_matrix_hermitian_block_encoding, + _sparse_matrix_symb_hermitian_block_encoding, + SparseMatrixHermitian, + UniformSqrtEntryOracle, +) +from qualtran.resource_counting.generalizers import ignore_split_join + + +def test_sparse_matrix(bloq_autotester): + bloq_autotester(_sparse_matrix_hermitian_block_encoding) + + +def test_sparse_matrix_symb(bloq_autotester): + bloq_autotester(_sparse_matrix_symb_hermitian_block_encoding) + + +def test_sparse_matrix_params(): + bloq = _sparse_matrix_hermitian_block_encoding() + assert bloq.system_bitsize == 2 + assert bloq.alpha == 4 + assert bloq.epsilon == 0 + assert bloq.ancilla_bitsize == 2 + 2 + assert bloq.resource_bitsize == 0 + + bloq = _sparse_matrix_symb_hermitian_block_encoding() + n = sympy.Symbol('n', positive=True, integer=True) + assert bloq.system_bitsize == n + assert bloq.alpha == 2**n + assert bloq.epsilon == 0 + assert bloq.ancilla_bitsize == n + 2 + assert bloq.resource_bitsize == 0 + + +def test_call_graph(): + bloq = _sparse_matrix_hermitian_block_encoding() + _, sigma = bloq.call_graph(generalizer=ignore_split_join) + assert sigma[Hadamard()] == 4 + + bloq = _sparse_matrix_symb_hermitian_block_encoding() + _, sigma = bloq.call_graph(generalizer=ignore_split_join) + n = sympy.Symbol('n', integer=True, positive=True) + assert sigma[Hadamard()] == 6 * n + + +def test_sparse_matrix_tensors(): + bloq = _sparse_matrix_hermitian_block_encoding() + alpha = bloq.alpha + bb = BloqBuilder() + system = bb.add_register("system", 2) + ancilla = cast(Soquet, bb.add(IntState(0, 4))) + system, ancilla = bb.add_t(bloq, system=system, ancilla=ancilla) + bb.add(IntEffect(0, 4), val=ancilla) + bloq = bb.finalize(system=system) + + from_gate = np.full((4, 4), 0.3) + from_tensors = bloq.tensor_contract() * alpha + np.testing.assert_allclose(from_gate, from_tensors) + + +topleft_matrix = [ + [0.3, 0.3, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.3, 0.3, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.3, 0.3, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], +] + + +def test_top_left_matrix(): + col_oracle = TopLeftRowColumnOracle(system_bitsize=3, num_nonzero=3) + entry_oracle = UniformSqrtEntryOracle(system_bitsize=3, entry=0.3) + bloq = SparseMatrixHermitian(col_oracle, entry_oracle, eps=0) + alpha = bloq.alpha + + bb = BloqBuilder() + system = bb.add_register("system", 3) + ancilla = cast(Soquet, bb.add(IntState(0, 3 + 2))) + system, ancilla = bb.add_t(bloq, system=system, ancilla=ancilla) + bb.add(IntEffect(0, 3 + 2), val=ancilla) + bloq = bb.finalize(system=system) + + from_tensors = bloq.tensor_contract() * alpha + np.testing.assert_allclose(topleft_matrix, from_tensors, atol=0.003) + + +def test_counts(): + qlt_testing.assert_equivalent_bloq_counts( + _sparse_matrix_hermitian_block_encoding(), generalizer=ignore_split_join + ) + qlt_testing.assert_equivalent_bloq_counts( + _sparse_matrix_hermitian_block_encoding().controlled(), generalizer=ignore_split_join + ) diff --git a/qualtran/bloqs/factoring/ecc/ec_add_r.py b/qualtran/bloqs/factoring/ecc/ec_add_r.py index 8b2087314..9ced3a9f1 100644 --- a/qualtran/bloqs/factoring/ecc/ec_add_r.py +++ b/qualtran/bloqs/factoring/ecc/ec_add_r.py @@ -15,13 +15,30 @@ from functools import cached_property from typing import Dict, Optional, Tuple, Union +import numpy as np import sympy from attrs import frozen -from qualtran import Bloq, bloq_example, BloqDocSpec, QBit, QUInt, Register, Signature +from qualtran import ( + Bloq, + bloq_example, + BloqBuilder, + BloqDocSpec, + QBit, + QMontgomeryUInt, + QUInt, + Register, + Signature, + Soquet, + SoquetT, +) +from qualtran.bloqs.data_loading import QROAMClean from qualtran.drawing import Circle, Text, TextBox, WireSymbol +from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator from qualtran.simulation.classical_sim import ClassicalValT +from qualtran.symbolics import is_symbolic, Shaped +from .ec_add import ECAdd from .ec_point import ECPoint @@ -113,13 +130,14 @@ class ECWindowAddR(Bloq): Args: n: The bitsize of the two registers storing the elliptic curve point - window_size: The number of bits in the window. - R: The elliptic curve point to add. + R: The elliptic curve point to add (NOT in montgomery form). + add_window_size: The number of bits in the ECAdd window. + mul_window_size: The number of bits in the modular multiplication window. Registers: ctrl: `window_size` control bits. - x: The x component of the input elliptic curve point of bitsize `n`. - y: The y component of the input elliptic curve point of bitsize `n`. + x: The x component of the input elliptic curve point of bitsize `n` in montgomery form. + y: The y component of the input elliptic curve point of bitsize `n` in montgomery form. References: [How to compute a 256-bit elliptic curve private key with only 50 million Toffoli gates](https://arxiv.org/abs/2306.08585). @@ -127,19 +145,119 @@ class ECWindowAddR(Bloq): """ n: int - window_size: int R: ECPoint + add_window_size: int + mul_window_size: int = 1 @cached_property def signature(self) -> 'Signature': return Signature( [ - Register('ctrl', QBit(), shape=(self.window_size,)), + Register('ctrl', QBit(), shape=(self.add_window_size,)), Register('x', QUInt(self.n)), Register('y', QUInt(self.n)), ] ) + @cached_property + def qrom(self) -> QROAMClean: + if is_symbolic(self.n) or is_symbolic(self.add_window_size): + log_block_sizes = None + if is_symbolic(self.n) and not is_symbolic(self.add_window_size): + # We assume that bitsize is much larger than window_size + log_block_sizes = (0,) + return QROAMClean( + [ + Shaped((2**self.add_window_size,)), + Shaped((2**self.add_window_size,)), + Shaped((2**self.add_window_size,)), + ], + selection_bitsizes=(self.add_window_size,), + target_bitsizes=(self.n, self.n, self.n), + log_block_sizes=log_block_sizes, + ) + + cR = self.R + data_a, data_b, data_lam = [0], [0], [0] + mon_int = QMontgomeryUInt(self.n) + for _ in range(1, 2**self.add_window_size): + data_a.append(mon_int.uint_to_montgomery(int(cR.x), int(self.R.mod))) + data_b.append(mon_int.uint_to_montgomery(int(cR.y), int(self.R.mod))) + lam_num = (3 * cR.x**2 + cR.curve_a) % cR.mod + lam_denom = (2 * cR.y) % cR.mod + if lam_denom != 0: + lam = (lam_num * pow(lam_denom, -1, mod=cR.mod)) % cR.mod + else: + lam = 0 + data_lam.append(mon_int.uint_to_montgomery(int(lam), int(self.R.mod))) + cR = cR + self.R + + return QROAMClean( + [data_a, data_b, data_lam], + selection_bitsizes=(self.add_window_size,), + target_bitsizes=(self.n, self.n, self.n), + ) + + def build_composite_bloq( + self, bb: 'BloqBuilder', ctrl: 'SoquetT', x: 'Soquet', y: 'Soquet' + ) -> Dict[str, 'SoquetT']: + ctrl = bb.join(np.array(ctrl)) + + ctrl, a, b, lam_r, *junk = bb.add(self.qrom, selection=ctrl) + + a, b, x, y, lam_r = bb.add( + # TODO(https://github.com/quantumlib/Qualtran/issues/1476): make ECAdd accept SymbolicInt. + ECAdd(n=self.n, mod=int(self.R.mod), window_size=self.mul_window_size), + a=a, + b=b, + x=x, + y=y, + lam_r=lam_r, + ) + + if junk: + assert len(junk) == 3 + ctrl = bb.add( + self.qrom.adjoint(), + selection=ctrl, + target0_=a, + target1_=b, + target2_=lam_r, + junk_target0_=junk[0], + junk_target1_=junk[1], + junk_target2_=junk[2], + ) + else: + ctrl = bb.add( + self.qrom.adjoint(), selection=ctrl, target0_=a, target1_=b, target2_=lam_r + ) + + return {'ctrl': bb.split(ctrl), 'x': x, 'y': y} + + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': + return { + self.qrom: 1, + # TODO(https://github.com/quantumlib/Qualtran/issues/1476): make ECAdd accept SymbolicInt. + ECAdd(self.n, int(self.R.mod), self.mul_window_size): 1, + self.qrom.adjoint(): 1, + } + + def on_classical_vals(self, ctrl, x, y) -> Dict[str, Union['ClassicalValT', sympy.Expr]]: + # TODO(https://github.com/quantumlib/Qualtran/issues/1476): make ECAdd accept SymbolicInt. + A = ECPoint( + QMontgomeryUInt(self.n).montgomery_to_uint(int(x), int(self.R.mod)), + QMontgomeryUInt(self.n).montgomery_to_uint(int(y), int(self.R.mod)), + mod=self.R.mod, + curve_a=self.R.curve_a, + ) + ctrls = QUInt(self.n).from_bits(ctrl) + result: ECPoint = A + (ctrls * self.R) + return { + 'ctrl': ctrl, + 'x': QMontgomeryUInt(self.n).uint_to_montgomery(int(result.x), int(self.R.mod)), + 'y': QMontgomeryUInt(self.n).uint_to_montgomery(int(result.y), int(self.R.mod)), + } + def wire_symbol( self, reg: Optional['Register'], idx: Tuple[int, ...] = tuple() ) -> 'WireSymbol': @@ -153,16 +271,13 @@ def wire_symbol( return TextBox(f'$+{self.R.y}$') raise ValueError(f'Unrecognized register name {reg.name}') - def __str__(self): - return f'ECWindowAddR({self.n=})' - @bloq_example -def _ec_window_add() -> ECWindowAddR: - n, p = sympy.symbols('n p') - Rx, Ry = sympy.symbols('Rx Ry') - ec_window_add = ECWindowAddR(n=n, window_size=3, R=ECPoint(Rx, Ry, mod=p)) - return ec_window_add +def _ec_window_add_r_small() -> ECWindowAddR: + n = 16 + P = ECPoint(2, 2, mod=7, curve_a=3) + ec_window_add_r_small = ECWindowAddR(n=n, R=P, add_window_size=4) + return ec_window_add_r_small -_EC_WINDOW_ADD_BLOQ_DOC = BloqDocSpec(bloq_cls=ECWindowAddR, examples=[_ec_window_add]) +_EC_WINDOW_ADD_BLOQ_DOC = BloqDocSpec(bloq_cls=ECWindowAddR, examples=[_ec_window_add_r_small]) diff --git a/qualtran/bloqs/factoring/ecc/ec_add_r_test.py b/qualtran/bloqs/factoring/ecc/ec_add_r_test.py index f36ec4e11..65e437811 100644 --- a/qualtran/bloqs/factoring/ecc/ec_add_r_test.py +++ b/qualtran/bloqs/factoring/ecc/ec_add_r_test.py @@ -12,16 +12,79 @@ # See the License for the specific language governing permissions and # limitations under the License. -from qualtran.bloqs.factoring.ecc.ec_add_r import _ec_add_r, _ec_add_r_small, _ec_window_add +import numpy as np +import pytest +import qualtran.testing as qlt_testing +from qualtran import QMontgomeryUInt, QUInt +from qualtran.bloqs.factoring.ecc.ec_add_r import ( + _ec_add_r, + _ec_add_r_small, + _ec_window_add_r_small, + ECWindowAddR, +) +from qualtran.resource_counting.generalizers import ignore_alloc_free, ignore_split_join -def test_ec_add_r(bloq_autotester): - bloq_autotester(_ec_add_r) +from .ec_add_r import ECWindowAddR +from .ec_point import ECPoint -def test_ec_add_r_small(bloq_autotester): - bloq_autotester(_ec_add_r_small) +@pytest.mark.parametrize('bloq', [_ec_add_r, _ec_add_r_small, _ec_window_add_r_small]) +def test_ec_add_r(bloq_autotester, bloq): + bloq_autotester(bloq) -def test_ec_window_add(bloq_autotester): - bloq_autotester(_ec_window_add) +@pytest.mark.parametrize('a,b', [(15, 13), (0, 0)]) +@pytest.mark.parametrize( + ['n', 'window_size'], + [ + (n, window_size) + for n in range(5, 8) + for window_size in range(1, n + 1) + if n % window_size == 0 + ], +) +def test_ec_window_add_r_bloq_counts(n, window_size, a, b): + p = 17 + R = ECPoint(a, b, mod=p) + bloq = ECWindowAddR(n=n, R=R, add_window_size=window_size) + qlt_testing.assert_equivalent_bloq_counts(bloq, [ignore_alloc_free, ignore_split_join]) + + +@pytest.mark.parametrize( + ['n', 'm'], [(n, m) for n in range(4, 5) for m in range(1, n + 1) if n % m == 0] +) +@pytest.mark.parametrize('a,b', [(15, 13), (0, 0)]) +@pytest.mark.parametrize('x,y', [(15, 13), (5, 8)]) +@pytest.mark.parametrize('ctrl', [0, 1, 5]) +def test_ec_window_add_r_classical(n, m, ctrl, x, y, a, b): + p = 17 + R = ECPoint(a, b, mod=p) + x = QMontgomeryUInt(n).uint_to_montgomery(x, p) + y = QMontgomeryUInt(n).uint_to_montgomery(y, p) + ctrl = np.array(QUInt(m).to_bits(ctrl % (2**m))) + bloq = ECWindowAddR(n=n, R=R, add_window_size=m, mul_window_size=m) + ret1 = bloq.call_classically(ctrl=ctrl, x=x, y=y) + ret2 = bloq.decompose_bloq().call_classically(ctrl=ctrl, x=x, y=y) + for i, ret1_i in enumerate(ret1): + np.testing.assert_array_equal(ret1_i, ret2[i]) + + +@pytest.mark.slow +@pytest.mark.parametrize( + ['n', 'm'], [(n, m) for n in range(7, 9) for m in range(1, n + 1) if n % m == 0] +) +@pytest.mark.parametrize('a,b', [(15, 13), (0, 0)]) +@pytest.mark.parametrize('x,y', [(15, 13), (5, 8)]) +@pytest.mark.parametrize('ctrl', [0, 1, 5, 8]) +def test_ec_window_add_r_classical_slow(n, m, ctrl, x, y, a, b): + p = 17 + R = ECPoint(a, b, mod=p) + x = QMontgomeryUInt(n).uint_to_montgomery(x, p) + y = QMontgomeryUInt(n).uint_to_montgomery(y, p) + ctrl = np.array(QUInt(m).to_bits(ctrl % (2**m))) + bloq = ECWindowAddR(n=n, R=R, add_window_size=m, mul_window_size=m) + ret1 = bloq.call_classically(ctrl=ctrl, x=x, y=y) + ret2 = bloq.decompose_bloq().call_classically(ctrl=ctrl, x=x, y=y) + for i, ret1_i in enumerate(ret1): + np.testing.assert_array_equal(ret1_i, ret2[i]) diff --git a/qualtran/bloqs/factoring/ecc/ec_phase_estimate_r.py b/qualtran/bloqs/factoring/ecc/ec_phase_estimate_r.py index ffe03f2f9..d56400d20 100644 --- a/qualtran/bloqs/factoring/ecc/ec_phase_estimate_r.py +++ b/qualtran/bloqs/factoring/ecc/ec_phase_estimate_r.py @@ -12,9 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import functools from functools import cached_property -from typing import Dict +from typing import Dict, Union +import numpy as np import sympy from attrs import frozen @@ -34,7 +36,7 @@ from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator from .._factoring_shims import MeasureQFT -from .ec_add_r import ECAddR +from .ec_add_r import ECAddR, ECWindowAddR from .ec_point import ECPoint @@ -45,30 +47,65 @@ class ECPhaseEstimateR(Bloq): This is used as a subroutine in `FindECCPrivateKey`. First, we phase-estimate the addition of the base point $P$, then of the public key $Q$. + When the ellptic curve point addition window size is 1 we use the ECAddR bloq which has it's + own bespoke circuit; when it is greater than 1 we use the windowed circuit which uses + pre-computed classical additions loaded into the circuit. + Args: n: The bitsize of the elliptic curve points' x and y registers. point: The elliptic curve point to phase estimate against. + add_window_size: The number of bits in the ECAdd window. + mul_window_size: The number of bits in the modular multiplication window. """ n: int point: ECPoint + add_window_size: int = 1 + mul_window_size: int = 1 @cached_property def signature(self) -> 'Signature': return Signature([Register('x', QUInt(self.n)), Register('y', QUInt(self.n))]) + @property + def ec_add(self) -> Union[functools.partial[ECAddR], functools.partial[ECWindowAddR]]: + if self.add_window_size == 1: + return functools.partial(ECAddR, n=self.n) + return functools.partial( + ECWindowAddR, + n=self.n, + add_window_size=self.add_window_size, + mul_window_size=self.mul_window_size, + ) + + @property + def num_windows(self) -> int: + return self.n // self.add_window_size + def build_composite_bloq(self, bb: 'BloqBuilder', x: Soquet, y: Soquet) -> Dict[str, 'SoquetT']: if isinstance(self.n, sympy.Expr): raise DecomposeTypeError("Cannot decompose symbolic `n`.") ctrl = [bb.add(PlusState()) for _ in range(self.n)] - for i in range(self.n): - ctrl[i], x, y = bb.add(ECAddR(n=self.n, R=2**i * self.point), ctrl=ctrl[i], x=x, y=y) + + if self.add_window_size == 1: + for i in range(self.n): + ctrl[i], x, y = bb.add(self.ec_add(R=2**i * self.point), ctrl=ctrl[i], x=x, y=y) + else: + ctrls = np.split(np.array(ctrl), self.num_windows) + for i in range(self.num_windows): + ctrls[i], x, y = bb.add( + self.ec_add(R=2 ** (self.add_window_size * i) * self.point), + ctrl=ctrls[i], + x=x, + y=y, + ) + ctrl = np.concatenate(ctrls, axis=None) bb.add(MeasureQFT(n=self.n), x=ctrl) return {'x': x, 'y': y} def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': - return {ECAddR(n=self.n, R=self.point): self.n, MeasureQFT(n=self.n): 1} + return {self.ec_add(R=self.point): self.num_windows, MeasureQFT(n=self.n): 1} def __str__(self) -> str: return f'PE${self.point}$' @@ -76,7 +113,7 @@ def __str__(self) -> str: @bloq_example def _ec_pe() -> ECPhaseEstimateR: - n, p = sympy.symbols('n p ') + n, p = sympy.symbols('n p') Rx, Ry = sympy.symbols('R_x R_y') ec_pe = ECPhaseEstimateR(n=n, point=ECPoint(Rx, Ry, mod=p)) return ec_pe @@ -90,4 +127,4 @@ def _ec_pe_small() -> ECPhaseEstimateR: return ec_pe_small -_EC_PE_BLOQ_DOC = BloqDocSpec(bloq_cls=ECPhaseEstimateR, examples=[_ec_pe]) +_EC_PE_BLOQ_DOC = BloqDocSpec(bloq_cls=ECPhaseEstimateR, examples=[_ec_pe, _ec_pe_small]) diff --git a/qualtran/bloqs/factoring/ecc/ec_point.py b/qualtran/bloqs/factoring/ecc/ec_point.py index c17ea5957..eb885c54c 100644 --- a/qualtran/bloqs/factoring/ecc/ec_point.py +++ b/qualtran/bloqs/factoring/ecc/ec_point.py @@ -12,9 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +import sympy from attrs import frozen -from qualtran.symbolics import SymbolicInt +from qualtran.symbolics import is_symbolic, SymbolicInt + +ec_times_x = sympy.Function("ec_times_x") +ec_times_y = sympy.Function("ec_times_y") +"""Support purely-symbolic ECPoint operations. +https://docs.sympy.org/latest/guides/custom-functions.html#easy-cases-fully-symbolic-or-fully-evaluated +""" @frozen @@ -63,16 +70,26 @@ def __add__(self, other): lam_num = (other.y - self.y) % self.mod lam_denom = (other.x - self.x) % self.mod - lam = (lam_num * pow(lam_denom, -1, mod=self.mod)) % self.mod + lam = (lam_num * pow(int(lam_denom), -1, mod=int(self.mod))) % self.mod xr = (lam**2 - other.x - self.x) % self.mod yr = (lam * (self.x - xr) - self.y) % self.mod return ECPoint(xr, yr, mod=self.mod, curve_a=self.curve_a) - def __mul__(self, other): - if other == 0: + def __mul__(self, times): + if times == 0: return ECPoint.inf(mod=self.mod, curve_a=self.curve_a) + if is_symbolic(self.x, self.y): + # Symbolic case: use sympy.Function to opaquely represent the + # multiplication operation + return ECPoint( + ec_times_x(self.x, times), + ec_times_y(self.y, times), + mod=self.mod, + curve_a=self.curve_a, + ) + # Otherwise, multiplication by an integer is repeated addition x = self - for _ in range(other - 1): + for _ in range(times - 1): x = x + self return x diff --git a/qualtran/bloqs/factoring/ecc/ecc.ipynb b/qualtran/bloqs/factoring/ecc/ecc.ipynb index 9215d113d..33e82abca 100644 --- a/qualtran/bloqs/factoring/ecc/ecc.ipynb +++ b/qualtran/bloqs/factoring/ecc/ecc.ipynb @@ -89,7 +89,9 @@ "#### Parameters\n", " - `n`: The bitsize of the elliptic curve points' x and y registers.\n", " - `base_point`: The base point $P$ with unknown order $r$ such that $P = [r] P$.\n", - " - `public_key`: The public key $Q$ such that $Q = [k] P$ for private key $k$. \n", + " - `public_key`: The public key $Q$ such that $Q = [k] P$ for private key $k$.\n", + " - `add_window_size`: The number of bits in the ECAdd window.\n", + " - `mul_window_size`: The number of bits in the modular multiplication window. \n", "\n", "#### References\n", " - [How to compute a 256-bit elliptic curve private key with only 50 million Toffoli gates](https://arxiv.org/abs/2306.08585). Litinski. 2023. Figure 4 (a).\n" @@ -215,9 +217,15 @@ "This is used as a subroutine in `FindECCPrivateKey`. First, we phase-estimate the\n", "addition of the base point $P$, then of the public key $Q$.\n", "\n", + "When the ellptic curve point addition window size is 1 we use the ECAddR bloq which has it's\n", + "own bespoke circuit; when it is greater than 1 we use the windowed circuit which uses\n", + "pre-computed classical additions loaded into the circuit.\n", + "\n", "#### Parameters\n", " - `n`: The bitsize of the elliptic curve points' x and y registers.\n", - " - `point`: The elliptic curve point to phase estimate against.\n" + " - `point`: The elliptic curve point to phase estimate against.\n", + " - `add_window_size`: The number of bits in the ECAdd window.\n", + " - `mul_window_size`: The number of bits in the modular multiplication window.\n" ] }, { @@ -251,11 +259,25 @@ }, "outputs": [], "source": [ - "n, p = sympy.symbols('n p ')\n", + "n, p = sympy.symbols('n p')\n", "Rx, Ry = sympy.symbols('R_x R_y')\n", "ec_pe = ECPhaseEstimateR(n=n, point=ECPoint(Rx, Ry, mod=p))" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "4616d33a", + "metadata": { + "cq.autogen": "ECPhaseEstimateR.ec_pe_small" + }, + "outputs": [], + "source": [ + "n = 3\n", + "Rx, Ry, p = sympy.symbols('R_x R_y p')\n", + "ec_pe_small = ECPhaseEstimateR(n=n, point=ECPoint(Rx, Ry, mod=p))" + ] + }, { "cell_type": "markdown", "id": "33824ce4", @@ -276,8 +298,8 @@ "outputs": [], "source": [ "from qualtran.drawing import show_bloqs\n", - "show_bloqs([ec_pe],\n", - " ['`ec_pe`'])" + "show_bloqs([ec_pe, ec_pe_small],\n", + " ['`ec_pe`', '`ec_pe_small`'])" ] }, { @@ -463,13 +485,14 @@ "\n", "#### Parameters\n", " - `n`: The bitsize of the two registers storing the elliptic curve point\n", - " - `window_size`: The number of bits in the window.\n", - " - `R`: The elliptic curve point to add. \n", + " - `R`: The elliptic curve point to add (NOT in montgomery form).\n", + " - `add_window_size`: The number of bits in the ECAdd window.\n", + " - `mul_window_size`: The number of bits in the modular multiplication window. \n", "\n", "#### Registers\n", " - `ctrl`: `window_size` control bits.\n", - " - `x`: The x component of the input elliptic curve point of bitsize `n`.\n", - " - `y`: The y component of the input elliptic curve point of bitsize `n`. \n", + " - `x`: The x component of the input elliptic curve point of bitsize `n` in montgomery form.\n", + " - `y`: The y component of the input elliptic curve point of bitsize `n` in montgomery form. \n", "\n", "#### References\n", " - [How to compute a 256-bit elliptic curve private key with only 50 million Toffoli gates](https://arxiv.org/abs/2306.08585). Litinski. 2013. Section 1, eq. (3) and (4).\n" @@ -500,15 +523,15 @@ { "cell_type": "code", "execution_count": null, - "id": "e1a3a397", + "id": "2ab0c101", "metadata": { - "cq.autogen": "ECWindowAddR.ec_window_add" + "cq.autogen": "ECWindowAddR.ec_window_add_r_small" }, "outputs": [], "source": [ - "n, p = sympy.symbols('n p')\n", - "Rx, Ry = sympy.symbols('Rx Ry')\n", - "ec_window_add = ECWindowAddR(n=n, window_size=3, R=ECPoint(Rx, Ry, mod=p))" + "n = 16\n", + "P = ECPoint(2, 2, mod=7, curve_a=3)\n", + "ec_window_add_r_small = ECWindowAddR(n=n, R=P, add_window_size=4)" ] }, { @@ -531,8 +554,8 @@ "outputs": [], "source": [ "from qualtran.drawing import show_bloqs\n", - "show_bloqs([ec_window_add],\n", - " ['`ec_window_add`'])" + "show_bloqs([ec_window_add_r_small],\n", + " ['`ec_window_add_r_small`'])" ] }, { @@ -555,9 +578,9 @@ "outputs": [], "source": [ "from qualtran.resource_counting.generalizers import ignore_split_join\n", - "ec_window_add_g, ec_window_add_sigma = ec_window_add.call_graph(max_depth=1, generalizer=ignore_split_join)\n", - "show_call_graph(ec_window_add_g)\n", - "show_counts_sigma(ec_window_add_sigma)" + "ec_window_add_r_small_g, ec_window_add_r_small_sigma = ec_window_add_r_small.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(ec_window_add_r_small_g)\n", + "show_counts_sigma(ec_window_add_r_small_sigma)" ] } ], diff --git a/qualtran/bloqs/factoring/ecc/find_ecc_private_key.py b/qualtran/bloqs/factoring/ecc/find_ecc_private_key.py index efaa42ed3..05785939f 100644 --- a/qualtran/bloqs/factoring/ecc/find_ecc_private_key.py +++ b/qualtran/bloqs/factoring/ecc/find_ecc_private_key.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import functools from functools import cached_property from typing import Dict @@ -66,6 +67,8 @@ class FindECCPrivateKey(Bloq): n: The bitsize of the elliptic curve points' x and y registers. base_point: The base point $P$ with unknown order $r$ such that $P = [r] P$. public_key: The public key $Q$ such that $Q = [k] P$ for private key $k$. + add_window_size: The number of bits in the ECAdd window. + mul_window_size: The number of bits in the modular multiplication window. References: [How to compute a 256-bit elliptic curve private key with only 50 million Toffoli gates](https://arxiv.org/abs/2306.08585). @@ -75,6 +78,8 @@ class FindECCPrivateKey(Bloq): n: int base_point: ECPoint public_key: ECPoint + add_window_size: int = 1 + mul_window_size: int = 1 @cached_property def signature(self) -> 'Signature': @@ -92,15 +97,24 @@ def curve_a(self) -> SymbolicInt: raise ValueError("Inconsistent curve parameters in the two points.") return self.base_point.curve_a + @property + def ec_pe_r(self) -> functools.partial[ECPhaseEstimateR]: + return functools.partial( + ECPhaseEstimateR, + n=self.n, + add_window_size=self.add_window_size, + mul_window_size=self.mul_window_size, + ) + def build_composite_bloq(self, bb: 'BloqBuilder') -> Dict[str, 'SoquetT']: x = bb.add(IntState(bitsize=self.n, val=self.base_point.x)) y = bb.add(IntState(bitsize=self.n, val=self.base_point.y)) - x, y = bb.add(ECPhaseEstimateR(n=self.n, point=self.base_point), x=x, y=y) - x, y = bb.add(ECPhaseEstimateR(n=self.n, point=self.public_key), x=x, y=y) + x, y = bb.add(self.ec_pe_r(point=self.base_point), x=x, y=y) + x, y = bb.add(self.ec_pe_r(point=self.public_key), x=x, y=y) - bb.add(Free(QUInt(self.n)), reg=x) - bb.add(Free(QUInt(self.n)), reg=y) + bb.add(Free(QUInt(self.n), dirty=True), reg=x) + bb.add(Free(QUInt(self.n), dirty=True), reg=y) return {} def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': @@ -108,7 +122,7 @@ def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': Ry = ssa.new_symbol('Ry') generic_point = ECPoint(Rx, Ry, mod=self.mod, curve_a=self.curve_a) - return {ECPhaseEstimateR(n=self.n, point=generic_point): 2} + return {self.ec_pe_r(point=generic_point): 2} def cost_attrs(self): return [('n', self.n)] diff --git a/qualtran/bloqs/mod_arithmetic/mod_division.py b/qualtran/bloqs/mod_arithmetic/mod_division.py index d5bad9fc1..575a1aada 100644 --- a/qualtran/bloqs/mod_arithmetic/mod_division.py +++ b/qualtran/bloqs/mod_arithmetic/mod_division.py @@ -17,7 +17,7 @@ import numpy as np import sympy -from attrs import frozen +from attrs import evolve, frozen from qualtran import ( Bloq, @@ -632,6 +632,9 @@ def build_composite_bloq( bb.free(f) return {'x': x, 'm': m} + def adjoint(self) -> 'KaliskiModInverse': + return evolve(self, uncompute=not self.uncompute) + def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': return _KaliskiModInverseImpl(self.bitsize, self.mod).build_call_graph(ssa) diff --git a/qualtran/bloqs/mod_arithmetic/mod_division_test.py b/qualtran/bloqs/mod_arithmetic/mod_division_test.py index 31c56d394..02646ef82 100644 --- a/qualtran/bloqs/mod_arithmetic/mod_division_test.py +++ b/qualtran/bloqs/mod_arithmetic/mod_division_test.py @@ -40,6 +40,7 @@ def test_kaliski_mod_inverse_classical_action(bitsize, mod): assert len(res) == 2 assert res[0] == dtype.montgomery_inverse(x_montgomery, mod) assert dtype.montgomery_product(int(res[0]), x_montgomery, mod) == R + assert blq.adjoint().call_classically(x=res[0], m=res[1]) == (x_montgomery,) @pytest.mark.parametrize('bitsize', [5, 6]) diff --git a/qualtran/conftest.py b/qualtran/conftest.py index e229f0a2a..ede967863 100644 --- a/qualtran/conftest.py +++ b/qualtran/conftest.py @@ -116,6 +116,8 @@ def assert_bloq_example_serializes_for_pytest(bloq_ex: BloqExample): 'state_prep_alias_symb', # cannot serialize Shaped 'sparse_matrix_block_encoding', 'sparse_matrix_symb_block_encoding', + 'sparse_matrix_hermitian_block_encoding', + 'sparse_matrix_symb_hermitian_block_encoding', 'sparse_state_prep_alias_symb', # cannot serialize Shaped 'sparse_permutation', # contains nested tuple of inhomogeneous shape 'permutation_cycle_symb', # cannot serialize Shaped diff --git a/qualtran/serialization/resolver_dict.py b/qualtran/serialization/resolver_dict.py index c9287718a..cd01026fb 100644 --- a/qualtran/serialization/resolver_dict.py +++ b/qualtran/serialization/resolver_dict.py @@ -98,6 +98,7 @@ import qualtran.bloqs.data_loading.qrom import qualtran.bloqs.data_loading.select_swap_qrom import qualtran.bloqs.factoring._factoring_shims +import qualtran.bloqs.factoring.ecc import qualtran.bloqs.factoring.ecc.ec_add import qualtran.bloqs.factoring.rsa import qualtran.bloqs.for_testing.atom @@ -333,6 +334,7 @@ "qualtran.bloqs.data_loading.qrom.QROM": qualtran.bloqs.data_loading.qrom.QROM, "qualtran.bloqs.data_loading.qroam_clean.QROAMClean": qualtran.bloqs.data_loading.qroam_clean.QROAMClean, "qualtran.bloqs.data_loading.qroam_clean.QROAMCleanAdjoint": qualtran.bloqs.data_loading.qroam_clean.QROAMCleanAdjoint, + "qualtran.bloqs.data_loading.qroam_clean.QROAMCleanAdjointWrapper": qualtran.bloqs.data_loading.qroam_clean.QROAMCleanAdjointWrapper, "qualtran.bloqs.data_loading.select_swap_qrom.SelectSwapQROM": qualtran.bloqs.data_loading.select_swap_qrom.SelectSwapQROM, "qualtran.bloqs.mod_arithmetic.CModAddK": qualtran.bloqs.mod_arithmetic.CModAddK, "qualtran.bloqs.mod_arithmetic.mod_addition.ModAdd": qualtran.bloqs.mod_arithmetic.mod_addition.ModAdd, @@ -352,6 +354,7 @@ "qualtran.bloqs.mod_arithmetic.mod_division.KaliskiModInverse": qualtran.bloqs.mod_arithmetic.mod_division.KaliskiModInverse, "qualtran.bloqs.mod_arithmetic.mod_division._KaliskiIteration": qualtran.bloqs.mod_arithmetic.mod_division._KaliskiIteration, "qualtran.bloqs.factoring._factoring_shims.MeasureQFT": qualtran.bloqs.factoring._factoring_shims.MeasureQFT, + "qualtran.bloqs.factoring.ecc.ec_add_r.ECWindowAddR": qualtran.bloqs.factoring.ecc.ec_add_r.ECWindowAddR, "qualtran.bloqs.factoring.ecc.ec_add._ECAddStepOne": qualtran.bloqs.factoring.ecc.ec_add._ECAddStepOne, "qualtran.bloqs.factoring.ecc.ec_add._ECAddStepTwo": qualtran.bloqs.factoring.ecc.ec_add._ECAddStepTwo, "qualtran.bloqs.factoring.ecc.ec_add._ECAddStepThree": qualtran.bloqs.factoring.ecc.ec_add._ECAddStepThree,