diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py index 542bcfe41d..f8dd19eeaa 100644 --- a/dev_tools/qualtran_dev_tools/notebook_specs.py +++ b/dev_tools/qualtran_dev_tools/notebook_specs.py @@ -92,6 +92,8 @@ import qualtran.bloqs.gf_arithmetic.gf2_inverse import qualtran.bloqs.gf_arithmetic.gf2_multiplication import qualtran.bloqs.gf_arithmetic.gf2_square +import qualtran.bloqs.gf_poly_arithmetic.gf2_poly_add_k +import qualtran.bloqs.gf_poly_arithmetic.gf_poly_split_and_join import qualtran.bloqs.hamiltonian_simulation.hamiltonian_simulation_by_gqsp import qualtran.bloqs.mcmt.and_bloq import qualtran.bloqs.mcmt.controlled_via_and @@ -604,6 +606,24 @@ ), ] +GF_POLY_ARITHMETIC = [ + # -------------------------------------------------------------------------- + # ----- Polynomials defined over Galois Fields (GF) Arithmetic -------- + # -------------------------------------------------------------------------- + NotebookSpecV2( + title='Polynomials over GF($p^m$) - Split and Join', + module=qualtran.bloqs.gf_poly_arithmetic.gf_poly_split_and_join, + bloq_specs=[ + qualtran.bloqs.gf_poly_arithmetic.gf_poly_split_and_join._GF_POLY_SPLIT_DOC, + qualtran.bloqs.gf_poly_arithmetic.gf_poly_split_and_join._GF_POLY_JOIN_DOC, + ], + ), + NotebookSpecV2( + title='Polynomials over GF($2^m$) - Add Constant', + module=qualtran.bloqs.gf_poly_arithmetic.gf2_poly_add_k, + bloq_specs=[qualtran.bloqs.gf_poly_arithmetic.gf2_poly_add_k._GF2_POLY_ADD_K_DOC], + ), +] ROT_QFT_PE = [ # -------------------------------------------------------------------------- @@ -935,6 +955,7 @@ ('Arithmetic', ARITHMETIC), ('Modular Arithmetic', MOD_ARITHMETIC), ('GF Arithmetic', GF_ARITHMETIC), + ('Polynomials over Galois Fields', GF_POLY_ARITHMETIC), ('Rotations', ROT_QFT_PE), ('Block Encoding', BLOCK_ENCODING), ('Optimization', OPTIMIZATION), diff --git a/docs/bloqs/index.rst b/docs/bloqs/index.rst index 3f0dbbac0a..1723ffcf23 100644 --- a/docs/bloqs/index.rst +++ b/docs/bloqs/index.rst @@ -105,6 +105,13 @@ Bloqs Library gf_arithmetic/gf2_square.ipynb gf_arithmetic/gf2_inverse.ipynb +.. toctree:: + :maxdepth: 2 + :caption: Polynomials over Galois Fields: + + gf_poly_arithmetic/gf_poly_split_and_join.ipynb + gf_poly_arithmetic/gf2_poly_add_k.ipynb + .. toctree:: :maxdepth: 2 :caption: Rotations: diff --git a/qualtran/bloqs/gf_poly_arithmetic/__init__.py b/qualtran/bloqs/gf_poly_arithmetic/__init__.py new file mode 100644 index 0000000000..12c048d0cf --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2025 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.bloqs.gf_poly_arithmetic.gf2_poly_add_k import GF2PolyAddK +from qualtran.bloqs.gf_poly_arithmetic.gf_poly_split_and_join import GFPolyJoin, GFPolySplit diff --git a/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.ipynb b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.ipynb new file mode 100644 index 0000000000..a13cc1343a --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.ipynb @@ -0,0 +1,179 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "71e0dbd8", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# Polynomials over GF($2^m$) - Add Constant" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c02448cd", + "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": "a5f23a08", + "metadata": { + "cq.autogen": "GF2PolyAddK.bloq_doc.md" + }, + "source": [ + "## `GF2PolyAddK`\n", + "In place addition of a constant polynomial defined over GF($2^m$).\n", + "\n", + "The bloq implements in place addition of a classical constant polynomial $g(x)$ and\n", + "a quantum register $|f(x)\\rangle$ storing coefficients of a degree-n polynomial defined\n", + "over GF($2^m$). Addition in GF($2^m$) simply reduces to a component wise XOR, which can\n", + "be implemented via X gates.\n", + "\n", + "$$\n", + " |f(x)\\rangle \\rightarrow |f(x) + g(x)\\rangle\n", + "$$\n", + "\n", + "#### Parameters\n", + " - `qgf_poly`: An instance of `QGFPoly` type that defines the data type for quantum register $|f(x)\\rangle$ storing coefficients of a degree-n polynomial defined over GF($2^m$).\n", + " - `g_x`: An instance of `galois.Poly` that specifies that constant polynomial g(x) defined over GF($2^m$) that should be added to the input register f(x). \n", + "\n", + "#### Registers\n", + " - `f_x`: Input THRU register that stores coefficients of polynomial defined over $GF(2^m)$.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d40e23be", + "metadata": { + "cq.autogen": "GF2PolyAddK.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.gf_poly_arithmetic import GF2PolyAddK" + ] + }, + { + "cell_type": "markdown", + "id": "101615d7", + "metadata": { + "cq.autogen": "GF2PolyAddK.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2cc9165f", + "metadata": { + "cq.autogen": "GF2PolyAddK.gf2_poly_4_8_add_k" + }, + "outputs": [], + "source": [ + "from galois import Poly\n", + "\n", + "from qualtran import QGF, QGFPoly\n", + "\n", + "qgf_poly = QGFPoly(4, QGF(2, 3))\n", + "g_x = Poly(qgf_poly.qgf.gf_type([1, 2, 3, 4, 5]))\n", + "gf2_poly_4_8_add_k = GF2PolyAddK(qgf_poly, g_x)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9081edee", + "metadata": { + "cq.autogen": "GF2PolyAddK.gf2_poly_add_k_symbolic" + }, + "outputs": [], + "source": [ + "import sympy\n", + "from galois import Poly\n", + "\n", + "from qualtran import QGF, QGFPoly\n", + "\n", + "n, m = sympy.symbols('n, m', positive=True, integers=True)\n", + "qgf_poly = QGFPoly(n, QGF(2, m))\n", + "gf2_poly_add_k_symbolic = GF2PolyAddK(qgf_poly, Poly([0, 0, 0, 0]))" + ] + }, + { + "cell_type": "markdown", + "id": "11a4d71f", + "metadata": { + "cq.autogen": "GF2PolyAddK.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66b4a211", + "metadata": { + "cq.autogen": "GF2PolyAddK.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([gf2_poly_4_8_add_k, gf2_poly_add_k_symbolic],\n", + " ['`gf2_poly_4_8_add_k`', '`gf2_poly_add_k_symbolic`'])" + ] + }, + { + "cell_type": "markdown", + "id": "5940114d", + "metadata": { + "cq.autogen": "GF2PolyAddK.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dffd364c", + "metadata": { + "cq.autogen": "GF2PolyAddK.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "gf2_poly_4_8_add_k_g, gf2_poly_4_8_add_k_sigma = gf2_poly_4_8_add_k.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(gf2_poly_4_8_add_k_g)\n", + "show_counts_sigma(gf2_poly_4_8_add_k_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_poly_arithmetic/gf2_poly_add_k.py b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py new file mode 100644 index 0000000000..b8d6f8c52c --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py @@ -0,0 +1,134 @@ +# Copyright 2025 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 galois + +from qualtran import ( + Bloq, + bloq_example, + BloqDocSpec, + DecomposeTypeError, + QGFPoly, + Register, + Signature, +) +from qualtran.bloqs.gf_arithmetic import GF2AddK +from qualtran.bloqs.gf_poly_arithmetic.gf_poly_split_and_join import GFPolyJoin, GFPolySplit +from qualtran.symbolics import is_symbolic + +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 GF2PolyAddK(Bloq): + r"""In place addition of a constant polynomial defined over GF($2^m$). + + The bloq implements in place addition of a classical constant polynomial $g(x)$ and + a quantum register $|f(x)\rangle$ storing coefficients of a degree-n polynomial defined + over GF($2^m$). Addition in GF($2^m$) simply reduces to a component wise XOR, which can + be implemented via X gates. + + $$ + |f(x)\rangle \rightarrow |f(x) + g(x)\rangle + $$ + + Args: + qgf_poly: An instance of `QGFPoly` type that defines the data type for quantum + register $|f(x)\rangle$ storing coefficients of a degree-n polynomial defined + over GF($2^m$). + g_x: An instance of `galois.Poly` that specifies that constant polynomial g(x) + defined over GF($2^m$) that should be added to the input register f(x). + + Registers: + f_x: Input THRU register that stores coefficients of polynomial defined over $GF(2^m)$. + """ + + qgf_poly: QGFPoly + g_x: galois.Poly = attrs.field() + + @cached_property + def signature(self) -> 'Signature': + return Signature([Register('f_x', dtype=self.qgf_poly)]) + + @g_x.validator + def _validate_g_x(self, attribute, value): + if not is_symbolic(self.qgf_poly.degree): + if value.degree > self.qgf_poly.degree: + raise ValueError(f"Degree of constant polynomial must be <= {self.qgf_poly.degree}") + if not is_symbolic(self.qgf_poly.degree, self.qgf_poly.qgf): + if not value.field is self.qgf_poly.qgf.gf_type: + raise ValueError( + f"Constant polynomial must be defined over galois field {self.qgf_poly.qgf.gf_type}" + ) + + def is_symbolic(self): + return is_symbolic(self.qgf_poly.degree) + + def build_composite_bloq(self, bb: 'BloqBuilder', *, f_x: 'Soquet') -> Dict[str, 'Soquet']: + if self.is_symbolic(): + raise DecomposeTypeError(f"Cannot decompose symbolic {self}") + f_x = bb.add(GFPolySplit(self.qgf_poly), reg=f_x) + g_x = self.qgf_poly.to_gf_coefficients(self.g_x) + for i in range(self.qgf_poly.degree + 1): + f_x[i] = bb.add(GF2AddK(self.qgf_poly.qgf.bitsize, int(g_x[i])), x=f_x[i]) + + f_x = bb.add(GFPolyJoin(self.qgf_poly), reg=f_x) + return {'f_x': f_x} + + def build_call_graph( + self, ssa: 'SympySymbolAllocator' + ) -> Union['BloqCountDictT', Set['BloqCountT']]: + if self.is_symbolic(): + k = ssa.new_symbol('g_x') + return {GF2AddK(self.qgf_poly.qgf.bitsize, k): self.qgf_poly.degree + 1} + return super().build_call_graph(ssa) + + def on_classical_vals(self, *, f_x) -> Dict[str, 'ClassicalValT']: + return {'f_x': f_x + self.g_x} + + +@bloq_example +def _gf2_poly_4_8_add_k() -> GF2PolyAddK: + from galois import Poly + + from qualtran import QGF, QGFPoly + + qgf_poly = QGFPoly(4, QGF(2, 3)) + g_x = Poly(qgf_poly.qgf.gf_type([1, 2, 3, 4, 5])) + gf2_poly_4_8_add_k = GF2PolyAddK(qgf_poly, g_x) + return gf2_poly_4_8_add_k + + +@bloq_example +def _gf2_poly_add_k_symbolic() -> GF2PolyAddK: + import sympy + from galois import Poly + + from qualtran import QGF, QGFPoly + + n, m = sympy.symbols('n, m', positive=True, integers=True) + qgf_poly = QGFPoly(n, QGF(2, m)) + gf2_poly_add_k_symbolic = GF2PolyAddK(qgf_poly, Poly([0, 0, 0, 0])) + return gf2_poly_add_k_symbolic + + +_GF2_POLY_ADD_K_DOC = BloqDocSpec( + bloq_cls=GF2PolyAddK, examples=(_gf2_poly_4_8_add_k, _gf2_poly_add_k_symbolic) +) diff --git a/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k_test.py b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k_test.py new file mode 100644 index 0000000000..8b6ab0b6eb --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k_test.py @@ -0,0 +1,58 @@ +# Copyright 2025 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 +from galois import Poly + +from qualtran.bloqs.gf_poly_arithmetic.gf2_poly_add_k import ( + _gf2_poly_4_8_add_k, + _gf2_poly_add_k_symbolic, +) +from qualtran.resource_counting import get_cost_value, QECGatesCost +from qualtran.testing import assert_consistent_classical_action + + +def test_gf2_poly_4_8_add_k(bloq_autotester): + bloq_autotester(_gf2_poly_4_8_add_k) + + +def test_gf2_poly_symbolic_add_k(bloq_autotester): + bloq_autotester(_gf2_poly_add_k_symbolic) + + +def test_gf2_poly_add_k_resource(): + bloq = _gf2_poly_4_8_add_k.make() + assert get_cost_value(bloq, QECGatesCost()).total_t_count() == 0 + assert get_cost_value(bloq, QECGatesCost()).clifford == sum( + np.sum(bloq.qgf_poly.qgf.to_bits(x)) for x in bloq.g_x.coeffs + ) + + bloq = _gf2_poly_add_k_symbolic.make() + assert get_cost_value(bloq, QECGatesCost()).total_t_count() == 0 + assert get_cost_value(bloq, QECGatesCost()).clifford == bloq.qgf_poly.bitsize + + +def test_gf2_poly_add_k_classical_sim(): + bloq = _gf2_poly_4_8_add_k.make() + f_x = Poly(bloq.qgf_poly.qgf.gf_type([0, 1, 2, 3, 4])) + assert bloq.call_classically(f_x=f_x)[0] == f_x + bloq.g_x # type: ignore[arg-type] + + f_x_range = np.asarray( + [ + Poly(bloq.qgf_poly.qgf.gf_type([0, 0, 0, 0, 0])), + Poly(bloq.qgf_poly.qgf.gf_type([7, 7, 7, 7, 7])), + Poly(bloq.qgf_poly.qgf.gf_type([0, 0, 3, 5, 7])), + Poly(bloq.qgf_poly.qgf.gf_type([2, 3, 5, 0, 0])), + ] + ) + assert_consistent_classical_action(bloq, f_x=f_x_range) diff --git a/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.ipynb b/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.ipynb new file mode 100644 index 0000000000..49c689f2e6 --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.ipynb @@ -0,0 +1,233 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5c112e3c", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# Polynomials over GF($p^m$) - Split and Join" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa73d48d", + "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": "bc9d0e45", + "metadata": { + "cq.autogen": "GFPolySplit.bloq_doc.md" + }, + "source": [ + "## `GFPolySplit`\n", + "Split a register representing coefficients of a polynomial into an array of `QGF` types.\n", + "\n", + "A register of type `QGFPoly` represents a univariate polynomial $f(x)$ with coefficients in a\n", + "galois field GF($p^m$). Given an input quantum register representing a degree $n$ polynomial\n", + "$f(x)$, this bloq splits it into $n + 1$ registers of type $QGF(p, m)$.\n", + "\n", + "Give a polynomial\n", + "$$\n", + " f(x) = \\sum_{i = 0}^{n} a_{i} x^{i} \\\\ \\forall a_{i} \\in GF(p^m)\n", + "$$\n", + "\n", + "the bloq splits it into a big-endian representation such that\n", + "$$\n", + " \\ket{f(x)} \\xrightarrow{\\text{split}} \\ket{a_{n}}\\ket{a_{n - 1}} \\cdots \\ket{a_0}\n", + "$$\n", + "\n", + "See `GFPolyJoin` for the inverse operation.\n", + "\n", + "#### Parameters\n", + " - `qgf_poly`: An instance of `QGFPoly` type that represents a degree $n$ polynomial defined over a galois field GF($p^m$). \n", + "\n", + "#### Registers\n", + " - `reg`: The register to be split. On its left, it is of the type `qgf_poly`. On the right, it is an array of `QGF`s of shape `(qgf_poly.degree + 1,)`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f4ec23cc", + "metadata": { + "cq.autogen": "GFPolySplit.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.gf_poly_arithmetic import GFPolySplit" + ] + }, + { + "cell_type": "markdown", + "id": "99406fdd", + "metadata": { + "cq.autogen": "GFPolySplit.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b1e7f86", + "metadata": { + "cq.autogen": "GFPolySplit.gf_poly_split" + }, + "outputs": [], + "source": [ + "from qualtran import QGF, QGFPoly\n", + "\n", + "gf_poly_split = GFPolySplit(QGFPoly(4, QGF(2, 3)))" + ] + }, + { + "cell_type": "markdown", + "id": "4067682b", + "metadata": { + "cq.autogen": "GFPolySplit.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f323719", + "metadata": { + "cq.autogen": "GFPolySplit.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([gf_poly_split],\n", + " ['`gf_poly_split`'])" + ] + }, + { + "cell_type": "markdown", + "id": "988becd9", + "metadata": { + "cq.autogen": "GFPolyJoin.bloq_doc.md" + }, + "source": [ + "## `GFPolyJoin`\n", + "Join $n+1$ registers representing coefficients of a polynomial into a `QGFPoly` type.\n", + "\n", + "A register of type `QGFPoly` represents a univariate polynomial $f(x)$ with coefficients in a\n", + "galois field GF($p^m$). Given an input quantum register of shape (n + 1,) and type `QGF`\n", + "representing coefficients of a degree $n$ polynomial $f(x)$, this bloq joins it into\n", + "a register of type `QGFPoly`.\n", + "\n", + "Give a polynomial\n", + "$$\n", + " f(x) = \\sum_{i = 0}^{n} a_{i} x^{i} \\\\ \\forall a_{i} \\in GF(p^m)\n", + "$$\n", + "\n", + "the bloq joins register representing coefficients of the polynomial in big-endian representation\n", + "such that\n", + "$$\n", + " \\ket{a_{n}}\\ket{a_{n - 1}} \\cdots \\ket{a_0} \\xrightarrow{\\text{join}} \\ket{f(x)}\n", + "$$\n", + "\n", + "See `GFPolySplit` for the inverse operation.\n", + "\n", + "#### Parameters\n", + " - `qgf_poly`: An instance of `QGFPoly` type that represents a degree $n$ polynomial defined over a galois field GF($p^m$). \n", + "\n", + "#### Registers\n", + " - `reg`: The register to be joined. On its left, it is an array of `QGF`s of shape\n", + " - ```: \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97ecef9a", + "metadata": { + "cq.autogen": "GFPolyJoin.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.gf_poly_arithmetic import GFPolyJoin" + ] + }, + { + "cell_type": "markdown", + "id": "dbeba5fc", + "metadata": { + "cq.autogen": "GFPolyJoin.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99cf9a48", + "metadata": { + "cq.autogen": "GFPolyJoin.gf_poly_join" + }, + "outputs": [], + "source": [ + "from qualtran import QGF, QGFPoly\n", + "\n", + "gf_poly_join = GFPolyJoin(QGFPoly(4, QGF(2, 3)))" + ] + }, + { + "cell_type": "markdown", + "id": "9dcb3e99", + "metadata": { + "cq.autogen": "GFPolyJoin.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be92a58c", + "metadata": { + "cq.autogen": "GFPolyJoin.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([gf_poly_join],\n", + " ['`gf_poly_join`'])" + ] + } + ], + "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_poly_arithmetic/gf_poly_split_and_join.py b/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.py new file mode 100644 index 0000000000..02b4e1de0d --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.py @@ -0,0 +1,255 @@ +# Copyright 2025 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 cast, Dict, List, Optional, Tuple, TYPE_CHECKING + +import galois +import numpy as np +from attrs import field, frozen +from numpy.typing import NDArray + +from qualtran import ( + Bloq, + bloq_example, + BloqDocSpec, + CompositeBloq, + ConnectionT, + DecomposeTypeError, + QGFPoly, + Register, + Side, + Signature, +) +from qualtran.bloqs.bookkeeping._bookkeeping_bloq import _BookkeepingBloq +from qualtran.drawing import directional_text_box, Text, WireSymbol +from qualtran.symbolics import is_symbolic + +if TYPE_CHECKING: + import quimb.tensor as qtn + from pennylane.operation import Operation + from pennylane.wires import Wires + + from qualtran.cirq_interop import CirqQuregT + from qualtran.simulation.classical_sim import ClassicalValT + + +@frozen +class GFPolySplit(_BookkeepingBloq): + r"""Split a register representing coefficients of a polynomial into an array of `QGF` types. + + A register of type `QGFPoly` represents a univariate polynomial $f(x)$ with coefficients in a + galois field GF($p^m$). Given an input quantum register representing a degree $n$ polynomial + $f(x)$, this bloq splits it into $n + 1$ registers of type $QGF(p, m)$. + + Give a polynomial + $$ + f(x) = \sum_{i = 0}^{n} a_{i} x^{i} \\ \forall a_{i} \in GF(p^m) + $$ + + the bloq splits it into a big-endian representation such that + $$ + \ket{f(x)} \xrightarrow{\text{split}} \ket{a_{n}}\ket{a_{n - 1}} \cdots \ket{a_0} + $$ + + See `GFPolyJoin` for the inverse operation. + + Args: + qgf_poly: An instance of `QGFPoly` type that represents a degree $n$ polynomial defined + over a galois field GF($p^m$). + + Registers: + reg: The register to be split. On its left, it is of the type `qgf_poly`. On the right, + it is an array of `QGF`s of shape `(qgf_poly.degree + 1,)`. + """ + + dtype: QGFPoly = field() + + @cached_property + def signature(self) -> Signature: + return Signature( + [ + Register('reg', self.dtype, shape=tuple(), side=Side.LEFT), + Register('reg', self.dtype.qgf, shape=(self.dtype.degree + 1,), side=Side.RIGHT), + ] + ) + + @dtype.validator + def _validate_dtype(self, attribute, value): + if is_symbolic(value.degree): + raise ValueError(f"{self} cannot have a symbolic data type.") + + def decompose_bloq(self) -> 'CompositeBloq': + raise DecomposeTypeError(f'{self} is atomic') + + def adjoint(self) -> 'Bloq': + return GFPolyJoin(dtype=self.dtype) + + def as_cirq_op(self, qubit_manager, reg: 'CirqQuregT') -> Tuple[None, Dict[str, 'CirqQuregT']]: + return None, { + 'reg': reg.reshape((int(self.dtype.degree) + 1, int(self.dtype.qgf.num_qubits))) + } + + def as_pl_op(self, wires: 'Wires') -> 'Operation': + return None + + def on_classical_vals(self, reg: galois.Poly) -> Dict[str, 'ClassicalValT']: + return {'reg': self.dtype.to_gf_coefficients(reg)} + + def my_tensors( + self, incoming: Dict[str, 'ConnectionT'], outgoing: Dict[str, 'ConnectionT'] + ) -> List['qtn.Tensor']: + import quimb.tensor as qtn + + incoming = incoming['reg'] + outgoing = cast(NDArray, outgoing['reg']) + inp_inds = [(incoming, i) for i in range(int(self.dtype.num_qubits))] + out_inds = [ + (outgoing[i], j) + for i in range(int(self.dtype.degree) + 1) + for j in range(int(self.dtype.qgf.num_qubits)) + ] + assert len(inp_inds) == len(out_inds) + + return [ + qtn.Tensor(data=np.eye(2), inds=[out_inds[i], inp_inds[i]], tags=[str(self)]) + for i in range(int(self.dtype.num_qubits)) + ] + + def wire_symbol(self, reg: Optional[Register], idx: Tuple[int, ...] = tuple()) -> 'WireSymbol': + if reg is None: + return Text('') + if reg.shape: + text = f'[{", ".join(str(i) for i in idx)}]' + return directional_text_box(text, side=reg.side) + return directional_text_box(' ', side=reg.side) + + +@bloq_example +def _gf_poly_split() -> GFPolySplit: + from qualtran import QGF, QGFPoly + + gf_poly_split = GFPolySplit(QGFPoly(4, QGF(2, 3))) + return gf_poly_split + + +_GF_POLY_SPLIT_DOC = BloqDocSpec( + bloq_cls=GFPolySplit, examples=[_gf_poly_split], call_graph_example=None +) + + +@frozen +class GFPolyJoin(_BookkeepingBloq): + r"""Join $n+1$ registers representing coefficients of a polynomial into a `QGFPoly` type. + + A register of type `QGFPoly` represents a univariate polynomial $f(x)$ with coefficients in a + galois field GF($p^m$). Given an input quantum register of shape (n + 1,) and type `QGF` + representing coefficients of a degree $n$ polynomial $f(x)$, this bloq joins it into + a register of type `QGFPoly`. + + Give a polynomial + $$ + f(x) = \sum_{i = 0}^{n} a_{i} x^{i} \\ \forall a_{i} \in GF(p^m) + $$ + + the bloq joins register representing coefficients of the polynomial in big-endian representation + such that + $$ + \ket{a_{n}}\ket{a_{n - 1}} \cdots \ket{a_0} \xrightarrow{\text{join}} \ket{f(x)} + $$ + + See `GFPolySplit` for the inverse operation. + + Args: + qgf_poly: An instance of `QGFPoly` type that represents a degree $n$ polynomial defined + over a galois field GF($p^m$). + + Registers: + reg: The register to be joined. On its left, it is an array of `QGF`s of shape + `(qgf_poly.degree + 1,)`. On the right, it is of the type `qgf_poly`. + + """ + + dtype: QGFPoly = field() + + @cached_property + def signature(self) -> Signature: + return Signature( + [ + Register('reg', self.dtype.qgf, shape=(self.dtype.degree + 1,), side=Side.LEFT), + Register('reg', self.dtype, shape=tuple(), side=Side.RIGHT), + ] + ) + + @dtype.validator + def _validate_dtype(self, attribute, value): + if is_symbolic(value.degree): + raise ValueError(f"{self} cannot have a symbolic data type.") + + def decompose_bloq(self) -> 'CompositeBloq': + raise DecomposeTypeError(f'{self} is atomic') + + def adjoint(self) -> 'Bloq': + return GFPolySplit(dtype=self.dtype) + + def as_cirq_op(self, qubit_manager, reg: 'CirqQuregT') -> Tuple[None, Dict[str, 'CirqQuregT']]: + return None, {'reg': reg.reshape(int(self.dtype.num_qubits))} + + def as_pl_op(self, wires: 'Wires') -> 'Operation': + return None + + def my_tensors( + self, incoming: Dict[str, 'ConnectionT'], outgoing: Dict[str, 'ConnectionT'] + ) -> List['qtn.Tensor']: + import quimb.tensor as qtn + + incoming = cast(NDArray, incoming['reg']) + outgoing = outgoing['reg'] + + inp_inds = [ + (incoming[i], j) + for i in range(int(self.dtype.degree) + 1) + for j in range(int(self.dtype.qgf.num_qubits)) + ] + out_inds = [(outgoing, i) for i in range(int(self.dtype.num_qubits))] + assert len(inp_inds) == len(out_inds) + + return [ + qtn.Tensor(data=np.eye(2), inds=[out_inds[i], inp_inds[i]], tags=[str(self)]) + for i in range(int(self.dtype.num_qubits)) + ] + + def on_classical_vals(self, reg: 'galois.Array') -> Dict[str, galois.Poly]: + return {'reg': self.dtype.from_gf_coefficients(reg)} + + def wire_symbol(self, reg: Optional[Register], idx: Tuple[int, ...] = tuple()) -> 'WireSymbol': + if reg is None: + return Text('') + if reg.shape: + text = f'[{", ".join(str(i) for i in idx)}]' + return directional_text_box(text, side=reg.side) + return directional_text_box(' ', side=reg.side) + + +@bloq_example +def _gf_poly_join() -> GFPolyJoin: + from qualtran import QGF, QGFPoly + + gf_poly_join = GFPolyJoin(QGFPoly(4, QGF(2, 3))) + return gf_poly_join + + +_GF_POLY_JOIN_DOC = BloqDocSpec( + bloq_cls=GFPolyJoin, examples=[_gf_poly_join], call_graph_example=None +) diff --git a/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join_test.py b/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join_test.py new file mode 100644 index 0000000000..998c06c353 --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join_test.py @@ -0,0 +1,57 @@ +# Copyright 2025 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 galois import Poly + +from qualtran import QGF, QGFPoly +from qualtran.bloqs.gf_poly_arithmetic.gf_poly_split_and_join import ( + _gf_poly_join, + _gf_poly_split, + GFPolyJoin, + GFPolySplit, +) + + +def test_gf_poly_split(bloq_autotester): + bloq_autotester(_gf_poly_split) + + +def test_gf_poly_join(bloq_autotester): + bloq_autotester(_gf_poly_join) + + +def test_no_symbolic_degree(): + n = sympy.Symbol('n') + with pytest.raises(ValueError, match=r'.*cannot have a symbolic data type\.'): + GFPolySplit(QGFPoly(n, QGF(2, 3))) + + with pytest.raises(ValueError, match=r'.*cannot have a symbolic data type\.'): + GFPolyJoin(QGFPoly(n, QGF(2, 3))) + + +def test_classical_sim(): + bloq = _gf_poly_split.make() + p = Poly(bloq.dtype.qgf.gf_type([1, 2, 3, 4])) + coeffs = bloq.call_classically(reg=p)[0] # type: ignore[arg-type] + assert np.all(coeffs == [0, 1, 2, 3, 4]) + assert bloq.adjoint().call_classically(reg=coeffs)[0] == p # type: ignore[arg-type] + + +def test_tensor_sim(): + bloq = GFPolySplit(QGFPoly(2, QGF(2, 2))) + assert np.all(bloq.tensor_contract() == np.eye(2 ** (3 * 2))) + assert np.all(bloq.adjoint().tensor_contract() == np.eye(2 ** (3 * 2))) diff --git a/qualtran/conftest.py b/qualtran/conftest.py index 439ae61455..5a7b8daa7c 100644 --- a/qualtran/conftest.py +++ b/qualtran/conftest.py @@ -101,6 +101,10 @@ def assert_bloq_example_serializes_for_pytest(bloq_ex: BloqExample): 'gf2_square_symbolic', # cannot serialize QGF 'gf16_inverse', # cannot serialize QGF 'gf2_inverse_symbolic', # cannot serialize QGF + 'gf_poly_split', # cannot serialize QGF and QGFPoly + 'gf_poly_join', # cannot serialize QGF and QGFPoly + 'gf2_poly_4_8_add_k', # cannot serialize QGF and QGFPoly + 'gf2_poly_add_k_symbolic', # cannot serialize QGF and QGFPoly 'gqsp_1d_ising', 'auto_partition', 'unitary_block_encoding',