diff --git a/dev_tools/autogenerate-bloqs-notebooks-v2.py b/dev_tools/autogenerate-bloqs-notebooks-v2.py index 0a0dbe453..8ae0b129e 100644 --- a/dev_tools/autogenerate-bloqs-notebooks-v2.py +++ b/dev_tools/autogenerate-bloqs-notebooks-v2.py @@ -106,6 +106,7 @@ import qualtran.bloqs.factoring.ecc import qualtran.bloqs.factoring.mod_exp import qualtran.bloqs.gf_arithmetic.gf2_addition +import qualtran.bloqs.gf_arithmetic.gf2_inverse import qualtran.bloqs.gf_arithmetic.gf2_multiplication import qualtran.bloqs.gf_arithmetic.gf2_square import qualtran.bloqs.hamiltonian_simulation.hamiltonian_simulation_by_gqsp @@ -573,6 +574,11 @@ module=qualtran.bloqs.gf_arithmetic.gf2_square, bloq_specs=[qualtran.bloqs.gf_arithmetic.gf2_square._GF2_SQUARE_DOC], ), + NotebookSpecV2( + title='GF($2^m$) Inverse', + module=qualtran.bloqs.gf_arithmetic.gf2_inverse, + bloq_specs=[qualtran.bloqs.gf_arithmetic.gf2_inverse._GF2_INVERSE_DOC], + ), ] diff --git a/docs/bloqs/index.rst b/docs/bloqs/index.rst index 6a543d88b..208f6968b 100644 --- a/docs/bloqs/index.rst +++ b/docs/bloqs/index.rst @@ -94,6 +94,7 @@ Bloqs Library gf_arithmetic/gf2_multiplication.ipynb gf_arithmetic/gf2_addition.ipynb gf_arithmetic/gf2_square.ipynb + gf_arithmetic/gf2_inverse.ipynb .. toctree:: :maxdepth: 2 diff --git a/qualtran/bloqs/gf_arithmetic/__init__.py b/qualtran/bloqs/gf_arithmetic/__init__.py index 56084da16..d53076128 100644 --- a/qualtran/bloqs/gf_arithmetic/__init__.py +++ b/qualtran/bloqs/gf_arithmetic/__init__.py @@ -13,5 +13,6 @@ # limitations under the License. from qualtran.bloqs.gf_arithmetic.gf2_addition import GF2Addition +from qualtran.bloqs.gf_arithmetic.gf2_inverse import GF2Inverse from qualtran.bloqs.gf_arithmetic.gf2_multiplication import GF2Multiplication from qualtran.bloqs.gf_arithmetic.gf2_square import GF2Square diff --git a/qualtran/bloqs/gf_arithmetic/gf2_inverse.ipynb b/qualtran/bloqs/gf_arithmetic/gf2_inverse.ipynb new file mode 100644 index 000000000..43c4ed58a --- /dev/null +++ b/qualtran/bloqs/gf_arithmetic/gf2_inverse.ipynb @@ -0,0 +1,182 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "25463ba5", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# GF($2^m$) Inverse" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38263a0a", + "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": "9054cbbb", + "metadata": { + "cq.autogen": "GF2Inverse.bloq_doc.md" + }, + "source": [ + "## `GF2Inverse`\n", + "Out of place inversion for elements in GF($2^m$)\n", + "\n", + "Given a quantum register storing elements from GF($2^m$), this bloq computes the inverse\n", + "of the given element in a new output register, out-of-place. Specifically,\n", + "it implements the transformation\n", + "\n", + "$$\n", + " |a\\rangle |0\\rangle \\rightarrow |a\\rangle |a^{-1}\\rangle\n", + "$$\n", + "\n", + "Inverse is computed by using Fermat's little theorem for Finite Fields, which states that\n", + "for a finite field $\\mathbb{F}$ with $m$ elements, $\\forall a \\in \\mathbb{F}$\n", + "$$\n", + " a^{m} = a\n", + "$$\n", + "\n", + "When the finite field is GF($2^m$), Fermat's little theorem can be used to obtain the relation\n", + "\n", + "$$\n", + " a^{-1} = a^{2^m - 2}\n", + "$$\n", + "\n", + "Thus, the inverse can be obtained via $m - 1$ squaring and multiplication operations.\n", + "\n", + "#### Parameters\n", + " - `bitsize`: The degree $m$ of the galois field $GF(2^m)$. Also corresponds to the number of qubits in the input register whose inverse should be calculated. \n", + "\n", + "#### Registers\n", + " - `x`: Input THRU register of size $m$ that stores elements from $GF(2^m)$.\n", + " - `result`: Output RIGHT register of size $m$ that stores $x^{-1}$ from $GF(2^m)$.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3a43c81", + "metadata": { + "cq.autogen": "GF2Inverse.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.gf_arithmetic import GF2Inverse" + ] + }, + { + "cell_type": "markdown", + "id": "b0bf14cc", + "metadata": { + "cq.autogen": "GF2Inverse.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6ce3222", + "metadata": { + "cq.autogen": "GF2Inverse.gf16_inverse" + }, + "outputs": [], + "source": [ + "gf16_inverse = GF2Inverse(4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6c26f44f", + "metadata": { + "cq.autogen": "GF2Inverse.gf2_inverse_symbolic" + }, + "outputs": [], + "source": [ + "import sympy\n", + "\n", + "m = sympy.Symbol('m')\n", + "gf2_inverse_symbolic = GF2Inverse(m)" + ] + }, + { + "cell_type": "markdown", + "id": "c813ddef", + "metadata": { + "cq.autogen": "GF2Inverse.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "591221ff", + "metadata": { + "cq.autogen": "GF2Inverse.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([gf16_inverse, gf2_inverse_symbolic],\n", + " ['`gf16_inverse`', '`gf2_inverse_symbolic`'])" + ] + }, + { + "cell_type": "markdown", + "id": "c96fb97d", + "metadata": { + "cq.autogen": "GF2Inverse.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67aeeb6c", + "metadata": { + "cq.autogen": "GF2Inverse.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "gf16_inverse_g, gf16_inverse_sigma = gf16_inverse.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(gf16_inverse_g)\n", + "show_counts_sigma(gf16_inverse_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/gf_arithmetic/gf2_inverse.py b/qualtran/bloqs/gf_arithmetic/gf2_inverse.py new file mode 100644 index 000000000..4ad07410e --- /dev/null +++ b/qualtran/bloqs/gf_arithmetic/gf2_inverse.py @@ -0,0 +1,157 @@ +# 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 Dict, Set, TYPE_CHECKING, Union + +import attrs +import numpy as np + +from qualtran import ( + Bloq, + bloq_example, + BloqDocSpec, + DecomposeTypeError, + QGF, + Register, + Side, + Signature, +) +from qualtran.bloqs.gf_arithmetic.gf2_addition import GF2Addition +from qualtran.bloqs.gf_arithmetic.gf2_multiplication import GF2Multiplication +from qualtran.bloqs.gf_arithmetic.gf2_square import GF2Square +from qualtran.symbolics import is_symbolic, SymbolicInt + +if TYPE_CHECKING: + from qualtran import BloqBuilder, Soquet + from qualtran.resource_counting import BloqCountDictT, BloqCountT, SympySymbolAllocator + from qualtran.simulation.classical_sim import ClassicalValT + + +@attrs.frozen +class GF2Inverse(Bloq): + r"""Out of place inversion for elements in GF($2^m$) + + Given a quantum register storing elements from GF($2^m$), this bloq computes the inverse + of the given element in a new output register, out-of-place. Specifically, + it implements the transformation + + $$ + |a\rangle |0\rangle \rightarrow |a\rangle |a^{-1}\rangle + $$ + + Inverse is computed by using Fermat's little theorem for Finite Fields, which states that + for a finite field $\mathbb{F}$ with $m$ elements, $\forall a \in \mathbb{F}$ + $$ + a^{m} = a + $$ + + When the finite field is GF($2^m$), Fermat's little theorem can be used to obtain the relation + + $$ + a^{-1} = a^{2^m - 2} + $$ + + Thus, the inverse can be obtained via $m - 1$ squaring and multiplication operations. + + Args: + bitsize: The degree $m$ of the galois field $GF(2^m)$. Also corresponds to the number of + qubits in the input register whose inverse should be calculated. + + Registers: + x: Input THRU register of size $m$ that stores elements from $GF(2^m)$. + result: Output RIGHT register of size $m$ that stores $x^{-1}$ from $GF(2^m)$. + """ + + bitsize: SymbolicInt + + @cached_property + def signature(self) -> 'Signature': + junk_reg = ( + [Register('junk', dtype=self.qgf, shape=(self.bitsize - 2,), side=Side.RIGHT)] + if is_symbolic(self.bitsize) or self.bitsize > 2 + else [] + ) + return Signature( + [ + Register('x', dtype=self.qgf), + Register('result', dtype=self.qgf, side=Side.RIGHT), + *junk_reg, + ] + ) + + @cached_property + def qgf(self) -> QGF: + return QGF(characteristic=2, degree=self.bitsize) + + def build_composite_bloq(self, bb: 'BloqBuilder', *, x: 'Soquet') -> Dict[str, 'Soquet']: + if is_symbolic(self.bitsize): + raise DecomposeTypeError(f"Cannot decompose symbolic {self}") + result = bb.allocate(dtype=self.qgf) + if self.bitsize == 1: + x, result = bb.add(GF2Addition(self.bitsize), x=x, y=result) + return {'x': x, 'result': result} + + x = bb.add(GF2Square(self.bitsize), x=x) + x, result = bb.add(GF2Addition(self.bitsize), x=x, y=result) + + junk = [] + for i in range(2, self.bitsize): + x = bb.add(GF2Square(self.bitsize), x=x) + x, result, new_result = bb.add(GF2Multiplication(self.bitsize), x=x, y=result) + junk.append(result) + result = new_result + x = bb.add(GF2Square(self.bitsize), x=x) + return {'x': x, 'result': result} | ({'junk': np.array(junk)} if junk else {}) + + def build_call_graph( + self, ssa: 'SympySymbolAllocator' + ) -> Union['BloqCountDictT', Set['BloqCountT']]: + if is_symbolic(self.bitsize) or self.bitsize > 2: + return { + GF2Addition(self.bitsize): 1, + GF2Square(self.bitsize): self.bitsize - 1, + GF2Multiplication(self.bitsize): self.bitsize - 2, + } + return {GF2Addition(self.bitsize): 1} | ( + {GF2Square(self.bitsize): 1} if self.bitsize == 2 else {} + ) + + def on_classical_vals(self, *, x) -> Dict[str, 'ClassicalValT']: + assert isinstance(x, self.qgf.gf_type) + x_temp = x**2 + result = x_temp + junk = [] + for i in range(2, int(self.bitsize)): + junk.append(result) + x_temp = x_temp * x_temp + result = result * x_temp + return {'x': x, 'result': x ** (-1), 'junk': np.array(junk)} + + +@bloq_example +def _gf16_inverse() -> GF2Inverse: + gf16_inverse = GF2Inverse(4) + return gf16_inverse + + +@bloq_example +def _gf2_inverse_symbolic() -> GF2Inverse: + import sympy + + m = sympy.Symbol('m') + gf2_inverse_symbolic = GF2Inverse(m) + return gf2_inverse_symbolic + + +_GF2_INVERSE_DOC = BloqDocSpec(bloq_cls=GF2Inverse, examples=(_gf16_inverse, _gf2_inverse_symbolic)) diff --git a/qualtran/bloqs/gf_arithmetic/gf2_inverse_test.py b/qualtran/bloqs/gf_arithmetic/gf2_inverse_test.py new file mode 100644 index 000000000..4fceffa8b --- /dev/null +++ b/qualtran/bloqs/gf_arithmetic/gf2_inverse_test.py @@ -0,0 +1,46 @@ +# 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 galois import GF + +from qualtran.bloqs.gf_arithmetic.gf2_inverse import ( + _gf2_inverse_symbolic, + _gf16_inverse, + GF2Inverse, +) +from qualtran.testing import assert_consistent_classical_action + + +def test_gf16_multiplication(bloq_autotester): + bloq_autotester(_gf16_inverse) + + +def test_gf2_multiplication_symbolic(bloq_autotester): + bloq_autotester(_gf2_inverse_symbolic) + + +def test_gf2_multiplication_classical_sim_quick(): + m = 1 + bloq = GF2Inverse(m) + GFM = GF(2**m) + assert_consistent_classical_action(bloq, x=GFM.elements[1:]) + + +@pytest.mark.slow +@pytest.mark.parametrize('m', [2, 3, 4, 5]) +def test_gf2_multiplication_classical_sim(m): + bloq = GF2Inverse(m) + GFM = GF(2**m) + assert_consistent_classical_action(bloq, x=GFM.elements[1:]) diff --git a/qualtran/conftest.py b/qualtran/conftest.py index 8a4e7a0c1..f3eeec1b3 100644 --- a/qualtran/conftest.py +++ b/qualtran/conftest.py @@ -96,6 +96,8 @@ def assert_bloq_example_serializes_for_pytest(bloq_ex: BloqExample): 'gf2_multiplication_symbolic', # cannot serialize QGF 'gf16_square', # cannot serialize QGF 'gf2_square_symbolic', # cannot serialize QGF + 'gf16_inverse', # cannot serialize QGF + 'gf2_inverse_symbolic', # cannot serialize QGF 'gqsp_1d_ising', 'auto_partition', 'unitary_block_encoding', diff --git a/qualtran/testing.py b/qualtran/testing.py index fbb268c69..8cc89bfde 100644 --- a/qualtran/testing.py +++ b/qualtran/testing.py @@ -708,4 +708,6 @@ def assert_consistent_classical_action( call_with = {p: v for p, v in zip(parameter_names, vals)} bloq_res = bloq.call_classically(**call_with) decomposed_res = cb.call_classically(**call_with) - assert bloq_res == decomposed_res, f'{bloq=} {call_with=} {bloq_res=} {decomposed_res=}' + np.testing.assert_equal( + bloq_res, decomposed_res, err_msg=f'{bloq=} {call_with=} {bloq_res=} {decomposed_res=}' + )