From 54d8d0be0816a79f7fea7c5d7eed3b735c475bef Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Tue, 8 Apr 2025 00:24:35 +0000 Subject: [PATCH 1/4] Add GF2PolyAddK, GFPolySplit and GFPolyJoin bloqs --- .../qualtran_dev_tools/notebook_specs.py | 21 ++ docs/bloqs/index.rst | 7 + qualtran/bloqs/gf_poly_arithmetic/__init__.py | 0 .../gf_poly_arithmetic/gf2_poly_add_k.ipynb | 176 +++++++++++++ .../gf_poly_arithmetic/gf2_poly_add_k.py | 132 ++++++++++ .../gf_poly_arithmetic/gf2_poly_add_k_test.py | 42 +++ .../gf_poly_split_and_join.ipynb | 229 ++++++++++++++++ .../gf_poly_split_and_join.py | 244 ++++++++++++++++++ .../gf_poly_split_and_join_test.py | 57 ++++ qualtran/conftest.py | 4 + 10 files changed, 912 insertions(+) create mode 100644 qualtran/bloqs/gf_poly_arithmetic/__init__.py create mode 100644 qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.ipynb create mode 100644 qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py create mode 100644 qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k_test.py create mode 100644 qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.ipynb create mode 100644 qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.py create mode 100644 qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join_test.py diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py index 542bcfe41d..41b92cd7bb 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='GF Polynomials 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='GF($2^m$) Polynomials Add Constant', + module=qualtran.bloqs.gf_poly_arithmetic.gf2_poly_add_k, + bloq_specs=[qualtran.bloqs.gf_poly_arithmetic.gf2_poly_add_k._GF2_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..e69de29bb2 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..e543d1d4fe --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.ipynb @@ -0,0 +1,176 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3d480a1d", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# GF($2^m$) Polynomials Add Constant" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "218a8310", + "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": "203cfaf0", + "metadata": { + "cq.autogen": "GF2AddK.bloq_doc.md" + }, + "source": [ + "## `GF2AddK`\n", + "In place addition of a constant $k$ for elements in GF($2^m$).\n", + "\n", + "The bloq implements in place addition of a classical constant $k$ and a quantum register\n", + "$|x\\rangle$ storing elements from GF($2^m$). Addition in GF($2^m$) simply reduces to a component\n", + "wise XOR, which can be implemented via X gates.\n", + "\n", + "$$\n", + "|x\\rangle \\rightarrow |x + k\\rangle\n", + "$$\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 x.\n", + " - `k`: Integer representation of constant over GF($2^m$) that should be added to the input register x. \n", + "\n", + "#### Registers\n", + " - `x`: Input THRU register of size $m$ that stores elements from $GF(2^m)$.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d52372c6", + "metadata": { + "cq.autogen": "GF2AddK.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.gf_arithmetic import GF2AddK" + ] + }, + { + "cell_type": "markdown", + "id": "1b73fd35", + "metadata": { + "cq.autogen": "GF2AddK.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab076857", + "metadata": { + "cq.autogen": "GF2AddK.gf2_poly_4_8_add_k" + }, + "outputs": [], + "source": [ + "from galois import Poly\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": "991e90de", + "metadata": { + "cq.autogen": "GF2AddK.gf2_poly_add_k_symbolic" + }, + "outputs": [], + "source": [ + "import sympy\n", + "from galois import Poly\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_symbolic_add_k = GF2PolyAddK(qgf_poly, Poly([0, 0, 0, 0]))" + ] + }, + { + "cell_type": "markdown", + "id": "6a3ecb1e", + "metadata": { + "cq.autogen": "GF2AddK.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32a100a5", + "metadata": { + "cq.autogen": "GF2AddK.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": "3cd8496b", + "metadata": { + "cq.autogen": "GF2AddK.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "605f81e0", + "metadata": { + "cq.autogen": "GF2AddK.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..fc193f9f93 --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py @@ -0,0 +1,132 @@ +# 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, TYPE_CHECKING + +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, 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) + + 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') -> 'BloqCountDictT': + 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_symbolic_add_k = GF2PolyAddK(qgf_poly, Poly([0, 0, 0, 0])) + return gf2_poly_symbolic_add_k + + +_GF2_ADD_K_DOC = BloqDocSpec( + bloq_cls=GF2AddK, 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..9dd8cb8dbc --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k_test.py @@ -0,0 +1,42 @@ +# 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 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.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_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 + + f_x_range = [ + 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..49f0a7e7ab --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.ipynb @@ -0,0 +1,229 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3ec2a648", + "metadata": { + "cq.autogen": "title_cell" + }, + "source": [ + "# GF Polynomials Split and Join" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aaeff645", + "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": "5e3dc60c", + "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": "232378d8", + "metadata": { + "cq.autogen": "GFPolySplit.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.gf_poly_arithmetic import GFPolySplit" + ] + }, + { + "cell_type": "markdown", + "id": "e36d131e", + "metadata": { + "cq.autogen": "GFPolySplit.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21041084", + "metadata": { + "cq.autogen": "GFPolySplit.gf_poly_split" + }, + "outputs": [], + "source": [ + "gf_poly_split = GFPolySplit(QGFPoly(4, QGF(2, 3)))" + ] + }, + { + "cell_type": "markdown", + "id": "e15977b5", + "metadata": { + "cq.autogen": "GFPolySplit.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb490cc5", + "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": "9a81e346", + "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": "2fc7eb46", + "metadata": { + "cq.autogen": "GFPolyJoin.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.gf_poly_arithmetic import GFPolyJoin" + ] + }, + { + "cell_type": "markdown", + "id": "d21c9ac6", + "metadata": { + "cq.autogen": "GFPolyJoin.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76ee657e", + "metadata": { + "cq.autogen": "GFPolyJoin.gf_poly_join" + }, + "outputs": [], + "source": [ + "gf_poly_join = GFPolyJoin(QGFPoly(4, QGF(2, 3)))" + ] + }, + { + "cell_type": "markdown", + "id": "7a443fe6", + "metadata": { + "cq.autogen": "GFPolyJoin.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c733b11", + "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..850c5c7054 --- /dev/null +++ b/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.py @@ -0,0 +1,244 @@ +# 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, List, Optional, Tuple, TYPE_CHECKING + +import galois +import numpy as np +from attrs import field, frozen + +from qualtran import ( + Bloq, + bloq_example, + BloqDocSpec, + CompositeBloq, + ConnectionT, + DecomposeTypeError, + QGF, + 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((self.dtype.degree + 1, 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 + + inp_inds = [(incoming['reg'], i) for i in range(self.dtype.num_qubits)] + out_inds = [ + (outgoing['reg'][i], j) + for i in range(self.dtype.degree + 1) + for j in range(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(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: + 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(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 + + inp_inds = [ + (incoming['reg'][i], j) + for i in range(self.dtype.degree + 1) + for j in range(self.dtype.qgf.num_qubits) + ] + out_inds = [(outgoing['reg'], i) for i in range(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(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: + 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..e8ea3598f1 --- /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] + assert np.all(coeffs == [0, 1, 2, 3, 4]) + assert bloq.adjoint().call_classically(reg=coeffs)[0] == p + + +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', From 19d5e50cd2fac7c763bb05fb6a6390d870b27573 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Tue, 8 Apr 2025 00:53:25 +0000 Subject: [PATCH 2/4] Fix tests, mypy and pylint --- .../qualtran_dev_tools/notebook_specs.py | 6 +- qualtran/bloqs/gf_poly_arithmetic/__init__.py | 17 +++++ .../gf_poly_arithmetic/gf2_poly_add_k.ipynb | 67 ++++++++++--------- .../gf_poly_arithmetic/gf2_poly_add_k.py | 16 +++-- .../gf_poly_arithmetic/gf2_poly_add_k_test.py | 19 +++--- .../gf_poly_split_and_join.ipynb | 34 +++++----- .../gf_poly_split_and_join.py | 39 +++++++---- .../gf_poly_split_and_join_test.py | 4 +- 8 files changed, 121 insertions(+), 81 deletions(-) diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py index 41b92cd7bb..f8dd19eeaa 100644 --- a/dev_tools/qualtran_dev_tools/notebook_specs.py +++ b/dev_tools/qualtran_dev_tools/notebook_specs.py @@ -611,7 +611,7 @@ # ----- Polynomials defined over Galois Fields (GF) Arithmetic -------- # -------------------------------------------------------------------------- NotebookSpecV2( - title='GF Polynomials Split and Join', + 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, @@ -619,9 +619,9 @@ ], ), NotebookSpecV2( - title='GF($2^m$) Polynomials Add Constant', + 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_ADD_K_DOC], + bloq_specs=[qualtran.bloqs.gf_poly_arithmetic.gf2_poly_add_k._GF2_POLY_ADD_K_DOC], ), ] diff --git a/qualtran/bloqs/gf_poly_arithmetic/__init__.py b/qualtran/bloqs/gf_poly_arithmetic/__init__.py index e69de29bb2..12c048d0cf 100644 --- a/qualtran/bloqs/gf_poly_arithmetic/__init__.py +++ 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 index e543d1d4fe..a13cc1343a 100644 --- a/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.ipynb +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.ipynb @@ -2,18 +2,18 @@ "cells": [ { "cell_type": "markdown", - "id": "3d480a1d", + "id": "71e0dbd8", "metadata": { "cq.autogen": "title_cell" }, "source": [ - "# GF($2^m$) Polynomials Add Constant" + "# Polynomials over GF($2^m$) - Add Constant" ] }, { "cell_type": "code", "execution_count": null, - "id": "218a8310", + "id": "c02448cd", "metadata": { "cq.autogen": "top_imports" }, @@ -30,47 +30,48 @@ }, { "cell_type": "markdown", - "id": "203cfaf0", + "id": "a5f23a08", "metadata": { - "cq.autogen": "GF2AddK.bloq_doc.md" + "cq.autogen": "GF2PolyAddK.bloq_doc.md" }, "source": [ - "## `GF2AddK`\n", - "In place addition of a constant $k$ for elements in GF($2^m$).\n", + "## `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 $k$ and a quantum register\n", - "$|x\\rangle$ storing elements from GF($2^m$). Addition in GF($2^m$) simply reduces to a component\n", - "wise XOR, which can be implemented via X gates.\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", - "|x\\rangle \\rightarrow |x + k\\rangle\n", + " |f(x)\\rangle \\rightarrow |f(x) + g(x)\\rangle\n", "$$\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 x.\n", - " - `k`: Integer representation of constant over GF($2^m$) that should be added to the input register x. \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", - " - `x`: Input THRU register of size $m$ that stores elements from $GF(2^m)$.\n" + " - `f_x`: Input THRU register that stores coefficients of polynomial defined over $GF(2^m)$.\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "d52372c6", + "id": "d40e23be", "metadata": { - "cq.autogen": "GF2AddK.bloq_doc.py" + "cq.autogen": "GF2PolyAddK.bloq_doc.py" }, "outputs": [], "source": [ - "from qualtran.bloqs.gf_arithmetic import GF2AddK" + "from qualtran.bloqs.gf_poly_arithmetic import GF2PolyAddK" ] }, { "cell_type": "markdown", - "id": "1b73fd35", + "id": "101615d7", "metadata": { - "cq.autogen": "GF2AddK.example_instances.md" + "cq.autogen": "GF2PolyAddK.example_instances.md" }, "source": [ "### Example Instances" @@ -79,13 +80,14 @@ { "cell_type": "code", "execution_count": null, - "id": "ab076857", + "id": "2cc9165f", "metadata": { - "cq.autogen": "GF2AddK.gf2_poly_4_8_add_k" + "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", @@ -96,26 +98,27 @@ { "cell_type": "code", "execution_count": null, - "id": "991e90de", + "id": "9081edee", "metadata": { - "cq.autogen": "GF2AddK.gf2_poly_add_k_symbolic" + "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_symbolic_add_k = GF2PolyAddK(qgf_poly, Poly([0, 0, 0, 0]))" + "gf2_poly_add_k_symbolic = GF2PolyAddK(qgf_poly, Poly([0, 0, 0, 0]))" ] }, { "cell_type": "markdown", - "id": "6a3ecb1e", + "id": "11a4d71f", "metadata": { - "cq.autogen": "GF2AddK.graphical_signature.md" + "cq.autogen": "GF2PolyAddK.graphical_signature.md" }, "source": [ "#### Graphical Signature" @@ -124,9 +127,9 @@ { "cell_type": "code", "execution_count": null, - "id": "32a100a5", + "id": "66b4a211", "metadata": { - "cq.autogen": "GF2AddK.graphical_signature.py" + "cq.autogen": "GF2PolyAddK.graphical_signature.py" }, "outputs": [], "source": [ @@ -137,9 +140,9 @@ }, { "cell_type": "markdown", - "id": "3cd8496b", + "id": "5940114d", "metadata": { - "cq.autogen": "GF2AddK.call_graph.md" + "cq.autogen": "GF2PolyAddK.call_graph.md" }, "source": [ "### Call Graph" @@ -148,9 +151,9 @@ { "cell_type": "code", "execution_count": null, - "id": "605f81e0", + "id": "dffd364c", "metadata": { - "cq.autogen": "GF2AddK.call_graph.py" + "cq.autogen": "GF2PolyAddK.call_graph.py" }, "outputs": [], "source": [ diff --git a/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py index fc193f9f93..5837adee8c 100644 --- a/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from functools import cached_property -from typing import Dict, TYPE_CHECKING +from typing import Dict, Set, TYPE_CHECKING, Union import attrs import galois @@ -32,7 +32,7 @@ if TYPE_CHECKING: from qualtran import BloqBuilder, Soquet - from qualtran.resource_counting import BloqCountDictT, SympySymbolAllocator + from qualtran.resource_counting import BloqCountDictT, BloqCountT, SympySymbolAllocator from qualtran.simulation.classical_sim import ClassicalValT @@ -92,7 +92,9 @@ def build_composite_bloq(self, bb: 'BloqBuilder', *, f_x: 'Soquet') -> Dict[str, f_x = bb.add(GFPolyJoin(self.qgf_poly), reg=f_x) return {'f_x': f_x} - def build_call_graph(self, ssa: 'SympySymbolAllocator') -> 'BloqCountDictT': + 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} @@ -123,10 +125,10 @@ def _gf2_poly_add_k_symbolic() -> GF2PolyAddK: n, m = sympy.symbols('n, m', positive=True, integers=True) qgf_poly = QGFPoly(n, QGF(2, m)) - gf2_poly_symbolic_add_k = GF2PolyAddK(qgf_poly, Poly([0, 0, 0, 0])) - return gf2_poly_symbolic_add_k + gf2_poly_add_k_symbolic = GF2PolyAddK(qgf_poly, Poly([0, 0, 0, 0])) + return gf2_poly_add_k_symbolic -_GF2_ADD_K_DOC = BloqDocSpec( - bloq_cls=GF2AddK, examples=(_gf2_poly_4_8_add_k, _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 index 9dd8cb8dbc..5ff82403aa 100644 --- a/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k_test.py +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k_test.py @@ -11,6 +11,7 @@ # 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 ( @@ -31,12 +32,14 @@ def test_gf2_poly_symbolic_add_k(bloq_autotester): 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 - - f_x_range = [ - 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 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 index 49f0a7e7ab..49c689f2e6 100644 --- a/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.ipynb +++ b/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.ipynb @@ -2,18 +2,18 @@ "cells": [ { "cell_type": "markdown", - "id": "3ec2a648", + "id": "5c112e3c", "metadata": { "cq.autogen": "title_cell" }, "source": [ - "# GF Polynomials Split and Join" + "# Polynomials over GF($p^m$) - Split and Join" ] }, { "cell_type": "code", "execution_count": null, - "id": "aaeff645", + "id": "fa73d48d", "metadata": { "cq.autogen": "top_imports" }, @@ -30,7 +30,7 @@ }, { "cell_type": "markdown", - "id": "5e3dc60c", + "id": "bc9d0e45", "metadata": { "cq.autogen": "GFPolySplit.bloq_doc.md" }, @@ -64,7 +64,7 @@ { "cell_type": "code", "execution_count": null, - "id": "232378d8", + "id": "f4ec23cc", "metadata": { "cq.autogen": "GFPolySplit.bloq_doc.py" }, @@ -75,7 +75,7 @@ }, { "cell_type": "markdown", - "id": "e36d131e", + "id": "99406fdd", "metadata": { "cq.autogen": "GFPolySplit.example_instances.md" }, @@ -86,18 +86,20 @@ { "cell_type": "code", "execution_count": null, - "id": "21041084", + "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": "e15977b5", + "id": "4067682b", "metadata": { "cq.autogen": "GFPolySplit.graphical_signature.md" }, @@ -108,7 +110,7 @@ { "cell_type": "code", "execution_count": null, - "id": "eb490cc5", + "id": "0f323719", "metadata": { "cq.autogen": "GFPolySplit.graphical_signature.py" }, @@ -121,7 +123,7 @@ }, { "cell_type": "markdown", - "id": "9a81e346", + "id": "988becd9", "metadata": { "cq.autogen": "GFPolyJoin.bloq_doc.md" }, @@ -158,7 +160,7 @@ { "cell_type": "code", "execution_count": null, - "id": "2fc7eb46", + "id": "97ecef9a", "metadata": { "cq.autogen": "GFPolyJoin.bloq_doc.py" }, @@ -169,7 +171,7 @@ }, { "cell_type": "markdown", - "id": "d21c9ac6", + "id": "dbeba5fc", "metadata": { "cq.autogen": "GFPolyJoin.example_instances.md" }, @@ -180,18 +182,20 @@ { "cell_type": "code", "execution_count": null, - "id": "76ee657e", + "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": "7a443fe6", + "id": "9dcb3e99", "metadata": { "cq.autogen": "GFPolyJoin.graphical_signature.md" }, @@ -202,7 +206,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9c733b11", + "id": "be92a58c", "metadata": { "cq.autogen": "GFPolyJoin.graphical_signature.py" }, 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 index 850c5c7054..02b4e1de0d 100644 --- a/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.py +++ b/qualtran/bloqs/gf_poly_arithmetic/gf_poly_split_and_join.py @@ -13,11 +13,12 @@ # limitations under the License. from functools import cached_property -from typing import Dict, List, Optional, Tuple, TYPE_CHECKING +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, @@ -26,7 +27,6 @@ CompositeBloq, ConnectionT, DecomposeTypeError, - QGF, QGFPoly, Register, Side, @@ -97,7 +97,9 @@ 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((self.dtype.degree + 1, self.dtype.qgf.num_qubits))} + 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 @@ -110,17 +112,19 @@ def my_tensors( ) -> List['qtn.Tensor']: import quimb.tensor as qtn - inp_inds = [(incoming['reg'], i) for i in range(self.dtype.num_qubits)] + incoming = incoming['reg'] + outgoing = cast(NDArray, outgoing['reg']) + inp_inds = [(incoming, i) for i in range(int(self.dtype.num_qubits))] out_inds = [ - (outgoing['reg'][i], j) - for i in range(self.dtype.degree + 1) - for j in range(self.dtype.qgf.num_qubits) + (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(self.dtype.num_qubits) + for i in range(int(self.dtype.num_qubits)) ] def wire_symbol(self, reg: Optional[Register], idx: Tuple[int, ...] = tuple()) -> 'WireSymbol': @@ -134,6 +138,8 @@ def wire_symbol(self, reg: Optional[Register], idx: Tuple[int, ...] = tuple()) - @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 @@ -198,7 +204,7 @@ 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(self.dtype.num_qubits)} + return None, {'reg': reg.reshape(int(self.dtype.num_qubits))} def as_pl_op(self, wires: 'Wires') -> 'Operation': return None @@ -208,17 +214,20 @@ def my_tensors( ) -> List['qtn.Tensor']: import quimb.tensor as qtn + incoming = cast(NDArray, incoming['reg']) + outgoing = outgoing['reg'] + inp_inds = [ - (incoming['reg'][i], j) - for i in range(self.dtype.degree + 1) - for j in range(self.dtype.qgf.num_qubits) + (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['reg'], i) for i in range(self.dtype.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(self.dtype.num_qubits) + for i in range(int(self.dtype.num_qubits)) ] def on_classical_vals(self, reg: 'galois.Array') -> Dict[str, galois.Poly]: @@ -235,6 +244,8 @@ def wire_symbol(self, reg: Optional[Register], idx: Tuple[int, ...] = tuple()) - @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 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 index e8ea3598f1..998c06c353 100644 --- 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 @@ -46,9 +46,9 @@ def test_no_symbolic_degree(): 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] + 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 + assert bloq.adjoint().call_classically(reg=coeffs)[0] == p # type: ignore[arg-type] def test_tensor_sim(): From c371fc4846405cd82866b020c87160f4863a5398 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Tue, 8 Apr 2025 01:39:12 +0000 Subject: [PATCH 3/4] Mark symbolic only when degree is symbolic --- qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py index 5837adee8c..b8d6f8c52c 100644 --- a/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k.py @@ -79,7 +79,7 @@ def _validate_g_x(self, attribute, value): ) def is_symbolic(self): - return is_symbolic(self.qgf_poly) + return is_symbolic(self.qgf_poly.degree) def build_composite_bloq(self, bb: 'BloqBuilder', *, f_x: 'Soquet') -> Dict[str, 'Soquet']: if self.is_symbolic(): From 0ec446c0b4bc7c62b82d25811b79b0783a1611f2 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Tue, 8 Apr 2025 23:22:54 +0000 Subject: [PATCH 4/4] Add tests for gate counts --- .../bloqs/gf_poly_arithmetic/gf2_poly_add_k_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 index 5ff82403aa..8b6ab0b6eb 100644 --- a/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k_test.py +++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_k_test.py @@ -18,6 +18,7 @@ _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 @@ -29,6 +30,18 @@ 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]))