Repository: quantumlib/Qualtran
Pull request #1624: [QGFPoly] Add `GF2PolyAdd` bloq for quantum-quantum addition of polynomials over `GF(2^m)`
Author: login: tanujkhattar
Created: 2025-04-08T01:42:13Z | Merged: 2025-04-09T00:33:15Z

Part of a larger effort to add bloqs that perform arithmetic over polynomials with coefficients in a galois field.

Currently builds on top of https://github.com/quantumlib/Qualtran/pull/1622



Diff:
diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py
index f8dd19eeaa..ed9e48413c 100644
--- a/dev_tools/qualtran_dev_tools/notebook_specs.py
+++ b/dev_tools/qualtran_dev_tools/notebook_specs.py
@@ -92,6 +92,7 @@
 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
 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
@@ -623,6 +624,11 @@
         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],
     ),
+    NotebookSpecV2(
+        title='Polynomials over GF($2^m$) - Addition',
+        module=qualtran.bloqs.gf_poly_arithmetic.gf2_poly_add,
+        bloq_specs=[qualtran.bloqs.gf_poly_arithmetic.gf2_poly_add._GF2_POLY_ADD_DOC],
+    ),
 ]
 
 ROT_QFT_PE = [
diff --git a/docs/bloqs/index.rst b/docs/bloqs/index.rst
index 1723ffcf23..98a6d4b576 100644
--- a/docs/bloqs/index.rst
+++ b/docs/bloqs/index.rst
@@ -111,6 +111,7 @@ Bloqs Library
 
     gf_poly_arithmetic/gf_poly_split_and_join.ipynb
     gf_poly_arithmetic/gf2_poly_add_k.ipynb
+    gf_poly_arithmetic/gf2_poly_add.ipynb
 
 .. toctree::
     :maxdepth: 2
diff --git a/qualtran/bloqs/gf_poly_arithmetic/__init__.py b/qualtran/bloqs/gf_poly_arithmetic/__init__.py
index 12c048d0cf..44c084de06 100644
--- a/qualtran/bloqs/gf_poly_arithmetic/__init__.py
+++ b/qualtran/bloqs/gf_poly_arithmetic/__init__.py
@@ -13,5 +13,6 @@
 #  limitations under the License.
 
 
+from qualtran.bloqs.gf_poly_arithmetic.gf2_poly_add import GF2PolyAdd
 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.ipynb b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add.ipynb
new file mode 100644
index 0000000000..5b5ba06c41
--- /dev/null
+++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add.ipynb
@@ -0,0 +1,175 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "846e63df",
+   "metadata": {
+    "cq.autogen": "title_cell"
+   },
+   "source": [
+    "# Polynomials over GF($2^m$) - Addition"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "5dd599ca",
+   "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": "458cf4fa",
+   "metadata": {
+    "cq.autogen": "GF2PolyAdd.bloq_doc.md"
+   },
+   "source": [
+    "## `GF2PolyAdd`\n",
+    "In place quantum-quantum addition of two polynomials defined over GF($2^m$).\n",
+    "\n",
+    "The bloq implements in place addition of quantum registers $|f(x)\\rangle$ and $|g(x)\\rangle$\n",
+    "storing coefficients of two degree-n polynomials defined over GF($2^m$).\n",
+    "Addition in GF($2^m$) simply reduces to a component wise XOR, which can be implemented via\n",
+    "CNOT gates.\n",
+    "\n",
+    "$$\n",
+    "    |f(x)\\rangle |g(x)\\rangle  \\rightarrow |f(x)\\rangle |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",
+    "\n",
+    "#### Registers\n",
+    " - `f_x`: THRU register that stores coefficients of first polynomial defined over $GF(2^m)$.\n",
+    " - `g_x`: THRU register that stores coefficients of second polynomial defined over $GF(2^m)$.\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d67b7e61",
+   "metadata": {
+    "cq.autogen": "GF2PolyAdd.bloq_doc.py"
+   },
+   "outputs": [],
+   "source": [
+    "from qualtran.bloqs.gf_poly_arithmetic import GF2PolyAdd"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "0a0a575b",
+   "metadata": {
+    "cq.autogen": "GF2PolyAdd.example_instances.md"
+   },
+   "source": [
+    "### Example Instances"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "f77964d0",
+   "metadata": {
+    "cq.autogen": "GF2PolyAdd.gf2_poly_4_8_add"
+   },
+   "outputs": [],
+   "source": [
+    "from qualtran import QGF, QGFPoly\n",
+    "\n",
+    "qgf_poly = QGFPoly(4, QGF(2, 3))\n",
+    "gf2_poly_4_8_add = GF2PolyAdd(qgf_poly)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "0b094507",
+   "metadata": {
+    "cq.autogen": "GF2PolyAdd.gf2_poly_add_symbolic"
+   },
+   "outputs": [],
+   "source": [
+    "import sympy\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_symbolic = GF2PolyAdd(qgf_poly)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "43b66756",
+   "metadata": {
+    "cq.autogen": "GF2PolyAdd.graphical_signature.md"
+   },
+   "source": [
+    "#### Graphical Signature"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ecf1a676",
+   "metadata": {
+    "cq.autogen": "GF2PolyAdd.graphical_signature.py"
+   },
+   "outputs": [],
+   "source": [
+    "from qualtran.drawing import show_bloqs\n",
+    "show_bloqs([gf2_poly_4_8_add, gf2_poly_add_symbolic],\n",
+    "           ['`gf2_poly_4_8_add`', '`gf2_poly_add_symbolic`'])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "6d7fe836",
+   "metadata": {
+    "cq.autogen": "GF2PolyAdd.call_graph.md"
+   },
+   "source": [
+    "### Call Graph"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "bc1596d3",
+   "metadata": {
+    "cq.autogen": "GF2PolyAdd.call_graph.py"
+   },
+   "outputs": [],
+   "source": [
+    "from qualtran.resource_counting.generalizers import ignore_split_join\n",
+    "gf2_poly_4_8_add_g, gf2_poly_4_8_add_sigma = gf2_poly_4_8_add.call_graph(max_depth=1, generalizer=ignore_split_join)\n",
+    "show_call_graph(gf2_poly_4_8_add_g)\n",
+    "show_counts_sigma(gf2_poly_4_8_add_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.py b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add.py
new file mode 100644
index 0000000000..7f8d8a351a
--- /dev/null
+++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add.py
@@ -0,0 +1,118 @@
+#  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
+
+from qualtran import (
+    Bloq,
+    bloq_example,
+    BloqDocSpec,
+    DecomposeTypeError,
+    QGFPoly,
+    Register,
+    Signature,
+)
+from qualtran.bloqs.gf_arithmetic import GF2Addition
+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 GF2PolyAdd(Bloq):
+    r"""In place quantum-quantum addition of two polynomials defined over GF($2^m$).
+
+    The bloq implements in place addition of quantum registers $|f(x)\rangle$ and $|g(x)\rangle$
+    storing coefficients of two degree-n polynomials defined over GF($2^m$).
+    Addition in GF($2^m$) simply reduces to a component wise XOR, which can be implemented via
+    CNOT gates.
+
+    $$
+        |f(x)\rangle |g(x)\rangle  \rightarrow |f(x)\rangle |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$).
+
+    Registers:
+        f_x: THRU register that stores coefficients of first polynomial defined over $GF(2^m)$.
+        g_x: THRU register that stores coefficients of second polynomial defined over $GF(2^m)$.
+    """
+
+    qgf_poly: QGFPoly
+
+    @cached_property
+    def signature(self) -> 'Signature':
+        return Signature(
+            [Register('f_x', dtype=self.qgf_poly), Register('g_x', dtype=self.qgf_poly)]
+        )
+
+    def is_symbolic(self):
+        return is_symbolic(self.qgf_poly.degree)
+
+    def build_composite_bloq(
+        self, bb: 'BloqBuilder', *, f_x: 'Soquet', g_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 = bb.add(GFPolySplit(self.qgf_poly), reg=g_x)
+        for i in range(self.qgf_poly.degree + 1):
+            f_x[i], g_x[i] = bb.add(GF2Addition(self.qgf_poly.qgf.bitsize), x=f_x[i], y=g_x[i])
+
+        f_x = bb.add(GFPolyJoin(self.qgf_poly), reg=f_x)
+        g_x = bb.add(GFPolyJoin(self.qgf_poly), reg=g_x)
+        return {'f_x': f_x, 'g_x': g_x}
+
+    def build_call_graph(
+        self, ssa: 'SympySymbolAllocator'
+    ) -> Union['BloqCountDictT', Set['BloqCountT']]:
+        return {GF2Addition(self.qgf_poly.qgf.bitsize): self.qgf_poly.degree + 1}
+
+    def on_classical_vals(self, *, f_x, g_x) -> Dict[str, 'ClassicalValT']:
+        return {'f_x': f_x, 'g_x': f_x + g_x}
+
+
+@bloq_example
+def _gf2_poly_4_8_add() -> GF2PolyAdd:
+    from qualtran import QGF, QGFPoly
+
+    qgf_poly = QGFPoly(4, QGF(2, 3))
+    gf2_poly_4_8_add = GF2PolyAdd(qgf_poly)
+    return gf2_poly_4_8_add
+
+
+@bloq_example
+def _gf2_poly_add_symbolic() -> GF2PolyAdd:
+    import sympy
+
+    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_symbolic = GF2PolyAdd(qgf_poly)
+    return gf2_poly_add_symbolic
+
+
+_GF2_POLY_ADD_DOC = BloqDocSpec(
+    bloq_cls=GF2PolyAdd, examples=(_gf2_poly_4_8_add, _gf2_poly_add_symbolic)
+)
diff --git a/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_test.py b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_test.py
new file mode 100644
index 0000000000..6d698c536f
--- /dev/null
+++ b/qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add_test.py
@@ -0,0 +1,54 @@
+#  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 import _gf2_poly_4_8_add, _gf2_poly_add_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(bloq_autotester):
+    bloq_autotester(_gf2_poly_4_8_add)
+
+
+def test_gf2_poly_symbolic_add_k(bloq_autotester):
+    bloq_autotester(_gf2_poly_add_symbolic)
+
+
+def test_gf2_poly_add_classical_sim():
+    bloq = _gf2_poly_4_8_add.make()
+    f_x = Poly(bloq.qgf_poly.qgf.gf_type([0, 1, 2, 3, 4]))
+    g_x = Poly(bloq.qgf_poly.qgf.gf_type([1, 2, 3, 4, 5]))
+    assert bloq.call_classically(f_x=f_x, g_x=g_x) == (f_x, f_x + 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, g_x=f_x_range)
+
+
+def test_gf2_poly_add_resource():
+    bloq = _gf2_poly_4_8_add.make()
+    assert get_cost_value(bloq, QECGatesCost()).total_t_count() == 0
+    assert get_cost_value(bloq, QECGatesCost()).clifford == bloq.qgf_poly.bitsize
+
+    bloq = _gf2_poly_add_symbolic.make()
+    assert get_cost_value(bloq, QECGatesCost()).total_t_count() == 0
+    assert get_cost_value(bloq, QECGatesCost()).clifford == bloq.qgf_poly.bitsize
diff --git a/qualtran/conftest.py b/qualtran/conftest.py
index 5a7b8daa7c..90b1a09ab2 100644
--- a/qualtran/conftest.py
+++ b/qualtran/conftest.py
@@ -105,6 +105,8 @@ def assert_bloq_example_serializes_for_pytest(bloq_ex: BloqExample):
         '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
+        'gf2_poly_4_8_add',  # cannot serialize QGF and QGFPoly
+        'gf2_poly_add_symbolic',  # cannot serialize QGF and QGFPoly
         'gqsp_1d_ising',
         'auto_partition',
         'unitary_block_encoding',


Reviews:
user: login: NoureldinYosri
body: can you add tests for the decompositions and toffoli cost
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T22:59:26Z
user: login: tanujkhattar
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T23:17:47Z
user: login: NoureldinYosri
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T23:33:03Z

Review comments:
diff_hunk: @@ -0,0 +1,118 @@
+#  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
+
+from qualtran import (
+    Bloq,
+    bloq_example,
+    BloqDocSpec,
+    DecomposeTypeError,
+    QGFPoly,
+    Register,
+    Signature,
+)
+from qualtran.bloqs.gf_arithmetic import GF2Addition
+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 GF2PolyAdd(Bloq):
+    r"""In place quantum-quantum addition of two polynomials defined over GF($2^m$).
+
+    The bloq implements in place addition of quantum registers $|f(x)\rangle$ and $|g(x)\rangle$
+    storing coefficients of two degree-n polynomials defined over GF($2^m$).
+    Addition in GF($2^m$) simply reduces to a component wise XOR, which can be implemented via
+    CNOT gates.
+
+    $$
+        |f(x)\rangle |g(x)\rangle  \rightarrow |f(x)\rangle |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$).
+
+    Registers:
+        f_x: THRU register that stores coefficients of first polynomial defined over $GF(2^m)$.
+        g_x: THRU register that stores coefficients of second polynomial defined over $GF(2^m)$.
+    """
+
+    qgf_poly: QGFPoly
+
+    @cached_property
+    def signature(self) -> 'Signature':
+        return Signature(
+            [Register('f_x', dtype=self.qgf_poly), Register('g_x', dtype=self.qgf_poly)]
+        )
+
+    def is_symbolic(self):
+        return is_symbolic(self.qgf_poly.degree)
+
+    def build_composite_bloq(
+        self, bb: 'BloqBuilder', *, f_x: 'Soquet', g_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 = bb.add(GFPolySplit(self.qgf_poly), reg=g_x)
+        for i in range(self.qgf_poly.degree + 1):
path: qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add.py
user: login: NoureldinYosri
body: would `OnEach` work here?
created_at: 2025-04-08T22:56:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,118 @@
+#  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
+
+from qualtran import (
+    Bloq,
+    bloq_example,
+    BloqDocSpec,
+    DecomposeTypeError,
+    QGFPoly,
+    Register,
+    Signature,
+)
+from qualtran.bloqs.gf_arithmetic import GF2Addition
+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 GF2PolyAdd(Bloq):
+    r"""In place quantum-quantum addition of two polynomials defined over GF($2^m$).
+
+    The bloq implements in place addition of quantum registers $|f(x)\rangle$ and $|g(x)\rangle$
+    storing coefficients of two degree-n polynomials defined over GF($2^m$).
+    Addition in GF($2^m$) simply reduces to a component wise XOR, which can be implemented via
+    CNOT gates.
+
+    $$
+        |f(x)\rangle |g(x)\rangle  \rightarrow |f(x)\rangle |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$).
+
+    Registers:
+        f_x: THRU register that stores coefficients of first polynomial defined over $GF(2^m)$.
+        g_x: THRU register that stores coefficients of second polynomial defined over $GF(2^m)$.
+    """
+
+    qgf_poly: QGFPoly
+
+    @cached_property
+    def signature(self) -> 'Signature':
+        return Signature(
+            [Register('f_x', dtype=self.qgf_poly), Register('g_x', dtype=self.qgf_poly)]
+        )
+
+    def is_symbolic(self):
+        return is_symbolic(self.qgf_poly.degree)
+
+    def build_composite_bloq(
+        self, bb: 'BloqBuilder', *, f_x: 'Soquet', g_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 = bb.add(GFPolySplit(self.qgf_poly), reg=g_x)
+        for i in range(self.qgf_poly.degree + 1):
path: qualtran/bloqs/gf_poly_arithmetic/gf2_poly_add.py
user: login: tanujkhattar
body: No, `OnEach` works on bloqs that act on single qubits. 
created_at: 2025-04-08T23:17:47Z
author_association: COLLABORATOR

PR comments:
user: login: tanujkhattar
created_at: 2025-04-08T23:18:24Z
body: Decomposition is already tested via `assert_consistent_classical_action`. I'll add tests for gate counts. 
author_association: COLLABORATOR
user: login: tanujkhattar
created_at: 2025-04-08T23:28:24Z
body: @NoureldinYosri I've added tests for gate counts. PTAL. 
author_association: COLLABORATOR