From af5e022bf4aacd35c878da1a5f1ea0e9b1e05359 Mon Sep 17 00:00:00 2001 From: Nour Yosri Date: Mon, 6 Jan 2025 15:38:00 -0800 Subject: [PATCH] Add bloq for constant polynomial multiplication modulu in GF(2) --- .../qualtran_dev_tools/notebook_specs.py | 5 +- .../gf_arithmetic/gf2_multiplication.ipynb | 129 ++++++++++++++++-- .../bloqs/gf_arithmetic/gf2_multiplication.py | 103 ++++++++++++++ .../gf_arithmetic/gf2_multiplication_test.py | 62 ++++++++- 4 files changed, 286 insertions(+), 13 deletions(-) diff --git a/dev_tools/qualtran_dev_tools/notebook_specs.py b/dev_tools/qualtran_dev_tools/notebook_specs.py index a35c468ce..4124941fc 100644 --- a/dev_tools/qualtran_dev_tools/notebook_specs.py +++ b/dev_tools/qualtran_dev_tools/notebook_specs.py @@ -558,7 +558,10 @@ NotebookSpecV2( title='GF($2^m$) Multiplication', module=qualtran.bloqs.gf_arithmetic.gf2_multiplication, - bloq_specs=[qualtran.bloqs.gf_arithmetic.gf2_multiplication._GF2_MULTIPLICATION_DOC], + bloq_specs=[ + qualtran.bloqs.gf_arithmetic.gf2_multiplication._GF2_MULTIPLICATION_DOC, + qualtran.bloqs.gf_arithmetic.gf2_multiplication._MULTIPLY_BY_CONSTANT_MOD_DOC, + ], ), NotebookSpecV2( title='GF($2^m$) Addition', diff --git a/qualtran/bloqs/gf_arithmetic/gf2_multiplication.ipynb b/qualtran/bloqs/gf_arithmetic/gf2_multiplication.ipynb index b001a27dd..367323315 100644 --- a/qualtran/bloqs/gf_arithmetic/gf2_multiplication.ipynb +++ b/qualtran/bloqs/gf_arithmetic/gf2_multiplication.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "87c95c4a", + "id": "acbb10a4", "metadata": { "cq.autogen": "title_cell" }, @@ -13,7 +13,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31c1f087", + "id": "fd0e976a", "metadata": { "cq.autogen": "top_imports" }, @@ -30,7 +30,7 @@ }, { "cell_type": "markdown", - "id": "307679ec", + "id": "8bfc1e7d", "metadata": { "cq.autogen": "GF2Multiplication.bloq_doc.md" }, @@ -72,7 +72,7 @@ { "cell_type": "code", "execution_count": null, - "id": "872a44d1", + "id": "d1193813", "metadata": { "cq.autogen": "GF2Multiplication.bloq_doc.py" }, @@ -83,7 +83,7 @@ }, { "cell_type": "markdown", - "id": "d0f0db7d", + "id": "4b13e0a3", "metadata": { "cq.autogen": "GF2Multiplication.example_instances.md" }, @@ -94,7 +94,7 @@ { "cell_type": "code", "execution_count": null, - "id": "131bc962", + "id": "20cab892", "metadata": { "cq.autogen": "GF2Multiplication.gf16_multiplication" }, @@ -106,7 +106,7 @@ { "cell_type": "code", "execution_count": null, - "id": "69f564d8", + "id": "06f44b6e", "metadata": { "cq.autogen": "GF2Multiplication.gf2_multiplication_symbolic" }, @@ -120,7 +120,7 @@ }, { "cell_type": "markdown", - "id": "2a62c2b8", + "id": "9b96d200", "metadata": { "cq.autogen": "GF2Multiplication.graphical_signature.md" }, @@ -131,7 +131,7 @@ { "cell_type": "code", "execution_count": null, - "id": "cf003e98", + "id": "3c5ef2f6", "metadata": { "cq.autogen": "GF2Multiplication.graphical_signature.py" }, @@ -144,7 +144,7 @@ }, { "cell_type": "markdown", - "id": "f14ef0c5", + "id": "e6a3dc52", "metadata": { "cq.autogen": "GF2Multiplication.call_graph.md" }, @@ -155,7 +155,7 @@ { "cell_type": "code", "execution_count": null, - "id": "f4b7bf2c", + "id": "e3b95e04", "metadata": { "cq.autogen": "GF2Multiplication.call_graph.py" }, @@ -166,6 +166,113 @@ "show_call_graph(gf16_multiplication_g)\n", "show_counts_sigma(gf16_multiplication_sigma)" ] + }, + { + "cell_type": "markdown", + "id": "8e1b2599", + "metadata": { + "cq.autogen": "MultiplyPolyByConstantMod.bloq_doc.md" + }, + "source": [ + "## `MultiplyPolyByConstantMod`\n", + "Multiply a polynomial by $f(x)$ modulu $m(x)$. Both $f(x)$ and $m(x)$ are constants.\n", + "\n", + "#### Parameters\n", + " - `f_x`: The polynomial to mulitply with, given either a galois.Poly or as a sequence degrees.\n", + " - `m_x`: The modulus polynomial, given either a galois.Poly or as a sequence degrees. \n", + "\n", + "#### Registers\n", + " - `g`: The polynomial coefficients (in GF(2)). \n", + "\n", + "Regerences:\n", + " - [Space-efficient quantum multiplication of polynomials for binary finite fields with\n", + " sub-quadratic Toffoli gate count](https://arxiv.org/abs/1910.02849v2) Algorithm 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f48b16b4", + "metadata": { + "cq.autogen": "MultiplyPolyByConstantMod.bloq_doc.py" + }, + "outputs": [], + "source": [ + "from qualtran.bloqs.gf_arithmetic import MultiplyPolyByConstantMod" + ] + }, + { + "cell_type": "markdown", + "id": "f71cbb55", + "metadata": { + "cq.autogen": "MultiplyPolyByConstantMod.example_instances.md" + }, + "source": [ + "### Example Instances" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb34d39c", + "metadata": { + "cq.autogen": "MultiplyPolyByConstantMod.gf2_multiply_by_constant_modulu" + }, + "outputs": [], + "source": [ + "fx = [2, 0] # x^2 + 1\n", + "mx = [0, 1, 3] # x^3 + x + 1\n", + "gf2_multiply_by_constant_modulu = MultiplyPolyByConstantMod(fx, mx)" + ] + }, + { + "cell_type": "markdown", + "id": "413907e4", + "metadata": { + "cq.autogen": "MultiplyPolyByConstantMod.graphical_signature.md" + }, + "source": [ + "#### Graphical Signature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffce6a92", + "metadata": { + "cq.autogen": "MultiplyPolyByConstantMod.graphical_signature.py" + }, + "outputs": [], + "source": [ + "from qualtran.drawing import show_bloqs\n", + "show_bloqs([gf2_multiply_by_constant_modulu],\n", + " ['`gf2_multiply_by_constant_modulu`'])" + ] + }, + { + "cell_type": "markdown", + "id": "703fa7bf", + "metadata": { + "cq.autogen": "MultiplyPolyByConstantMod.call_graph.md" + }, + "source": [ + "### Call Graph" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d8b870f", + "metadata": { + "cq.autogen": "MultiplyPolyByConstantMod.call_graph.py" + }, + "outputs": [], + "source": [ + "from qualtran.resource_counting.generalizers import ignore_split_join\n", + "gf2_multiply_by_constant_modulu_g, gf2_multiply_by_constant_modulu_sigma = gf2_multiply_by_constant_modulu.call_graph(max_depth=1, generalizer=ignore_split_join)\n", + "show_call_graph(gf2_multiply_by_constant_modulu_g)\n", + "show_counts_sigma(gf2_multiply_by_constant_modulu_sigma)" + ] } ], "metadata": { diff --git a/qualtran/bloqs/gf_arithmetic/gf2_multiplication.py b/qualtran/bloqs/gf_arithmetic/gf2_multiplication.py index 48e3a7f1e..d1d787302 100644 --- a/qualtran/bloqs/gf_arithmetic/gf2_multiplication.py +++ b/qualtran/bloqs/gf_arithmetic/gf2_multiplication.py @@ -247,3 +247,106 @@ def _gf2_multiplication_symbolic() -> GF2Multiplication: _GF2_MULTIPLICATION_DOC = BloqDocSpec( bloq_cls=GF2Multiplication, examples=(_gf16_multiplication, _gf2_multiplication_symbolic) ) + + +@attrs.frozen +class MultiplyPolyByConstantMod(Bloq): + r"""Multiply a polynomial by $f(x)$ modulu $m(x)$. Both $f(x)$ and $m(x)$ are constants. + + Args: + f_x: The polynomial to mulitply with, given either a galois.Poly or as + a sequence degrees. + m_x: The modulus polynomial, given either a galois.Poly or as + a sequence degrees. + + Registers: + g: The polynomial coefficients (in GF(2)). + + Regerences: + - [Space-efficient quantum multiplication of polynomials for binary finite fields with + sub-quadratic Toffoli gate count](https://arxiv.org/abs/1910.02849v2) Algorithm 1 + """ + + f_x: Poly = attrs.field(converter=lambda x: x if isinstance(x, Poly) else Poly.Degrees(x)) + m_x: Poly = attrs.field(converter=lambda x: x if isinstance(x, Poly) else Poly.Degrees(x)) + + def __attrs_post_init__(self): + assert self.m_x.is_irreducible() + assert self.f_x.degrees.max() < self.m_x.degrees.max() + + @cached_property + def n(self): + return self.m_x.degrees.max() + + @cached_property + def lup(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: + """Returns the LUP decomposition of the matrix representing the operation. + + If m_x is irreducible, then the operation y := (y*f_x)%m_x can be represented + by a full rank matrix that can be decomposed into PLU where L and U are lower + and upper traingular matricies and P is a permutation matrix. + """ + n = self.n + matrix = np.zeros((n, n), dtype=int) + for i in range(n): + p = (self.f_x * Poly.Degrees([i])) % self.m_x + for j in p.nonzero_degrees: + matrix[j, i] = 1 + P, L, U = GF(2)(matrix).plu_decompose() + return np.asarray(L, dtype=int), np.asarray(U, dtype=int), np.asarray(P, dtype=int) + + @cached_property + def signature(self) -> 'Signature': + return Signature([Register('g', QBit(), shape=(self.n,))]) + + def on_classical_vals(self, g) -> Dict[str, 'ClassicalValT']: + p = (Poly(g[::-1], GF(2)) * self.f_x) % self.m_x + res = p.coefficients().tolist() + res = [0 for _ in range(self.n - len(res))] + res + res = res[::-1] + return {'g': res} + + def build_composite_bloq(self, bb: 'BloqBuilder', g: 'Soquet') -> Dict[str, 'Soquet']: + L, U, P = self.lup + if is_symbolic(self.n): + raise DecomposeTypeError(f"Symbolic decomposition isn't supported for {self}") + for i in range(self.n): + for j in range(i + 1, self.n): + if U[i, j]: + g[j], g[i] = bb.add(CNOT(), ctrl=g[j], target=g[i]) + + for i in reversed(range(self.n)): + for j in reversed(range(i)): + if L[i, j]: + g[j], g[i] = bb.add(CNOT(), ctrl=g[j], target=g[i]) + + column = [*range(self.n)] + for i in range(self.n): + for j in range(i + 1, self.n): + if P[i, column[j]]: + g[i], g[j] = g[j], g[i] + column[i], column[j] = column[j], column[i] + return {'g': g} + + def build_call_graph( + self, ssa: 'SympySymbolAllocator' + ) -> Union['BloqCountDictT', Set['BloqCountT']]: + L, U, _ = self.lup + # The number of cnots is the number of non zero off-diagnoal entries in L and U. + cnots = np.sum(L) + np.sum(U) - 2 * self.n + if cnots: + return {CNOT(): cnots} + return {} + + +@bloq_example +def _gf2_multiply_by_constant_modulu() -> MultiplyPolyByConstantMod: + fx = [2, 0] # x^2 + 1 + mx = [0, 1, 3] # x^3 + x + 1 + gf2_multiply_by_constant_modulu = MultiplyPolyByConstantMod(fx, mx) + return gf2_multiply_by_constant_modulu + + +_MULTIPLY_BY_CONSTANT_MOD_DOC = BloqDocSpec( + bloq_cls=MultiplyPolyByConstantMod, examples=(_gf2_multiply_by_constant_modulu,) +) diff --git a/qualtran/bloqs/gf_arithmetic/gf2_multiplication_test.py b/qualtran/bloqs/gf_arithmetic/gf2_multiplication_test.py index a37b0cddf..00dc24cb2 100644 --- a/qualtran/bloqs/gf_arithmetic/gf2_multiplication_test.py +++ b/qualtran/bloqs/gf_arithmetic/gf2_multiplication_test.py @@ -14,15 +14,19 @@ import numpy as np import pytest -from galois import GF +from galois import GF, Poly +import qualtran.testing as qlt_testing from qualtran import QGF from qualtran.bloqs.gf_arithmetic.gf2_multiplication import ( _gf2_multiplication_symbolic, + _gf2_multiply_by_constant_modulu, _gf16_multiplication, GF2Multiplication, + MultiplyPolyByConstantMod, SynthesizeLRCircuit, ) +from qualtran.resource_counting import get_cost_value, QECGatesCost from qualtran.testing import assert_consistent_classical_action @@ -34,6 +38,10 @@ def test_gf2_multiplication_symbolic(bloq_autotester): bloq_autotester(_gf2_multiplication_symbolic) +def test_gf2_multiply_by_constant_modulu(bloq_autotester): + bloq_autotester(_gf2_multiply_by_constant_modulu) + + def test_synthesize_lr_circuit(): m = 2 matrix = GF2Multiplication(m).reduction_matrix_q @@ -89,3 +97,55 @@ def test_gf2_multiplication_classical_sim(m): bloq = GF2Multiplication(m, plus_equal_prod=False) GFM = GF(2**m) assert_consistent_classical_action(bloq, x=GFM.elements, y=GFM.elements) + + +@pytest.mark.parametrize('m_x', [Poly.Degrees([2, 1, 0]), Poly.Degrees([3, 1, 0])]) +def test_multiply_by_constant_mod_classical_action(m_x): + n = len(m_x.coeffs) - 1 + gf = GF(2, n, irreducible_poly=m_x) + QGFM = QGF(2, n) + elements = [Poly(tuple(QGFM.to_bits(i))) for i in gf.elements[1:]] + for f_x in elements: + blq = MultiplyPolyByConstantMod(f_x, m_x) + cblq = blq.decompose_bloq() + for g_x in elements: + g = [0] * n + g[-len(g_x.coeffs) :] = [int(x) for x in g_x.coeffs] + np.testing.assert_allclose(blq.call_classically(g=g), cblq.call_classically(g=g)) + + +@pytest.mark.parametrize( + ['m_x', 'cnot_count'], [[Poly.Degrees([2, 1, 0]), 0], [Poly.Degrees([3, 1, 0]), 0]] +) +def test_multiply_by_constant_mod_cost(m_x, cnot_count): + n = len(m_x.coeffs) - 1 + gf = GF(2, n, irreducible_poly=m_x) + QGFM = QGF(2, n) + elements = [Poly(tuple(QGFM.to_bits(i))) for i in gf.elements[1:]] + for f_x in elements: + blq = MultiplyPolyByConstantMod(f_x, m_x) + cost = get_cost_value(blq, QECGatesCost()) + assert cost.total_t_count() == 0 + assert cost.clifford < n**2 + + +@pytest.mark.parametrize('m_x', [Poly.Degrees([2, 1, 0]), Poly.Degrees([3, 1, 0])]) +def test_multiply_by_constant_mod_decomposition(m_x): + n = len(m_x.coeffs) - 1 + gf = GF(2, n, irreducible_poly=m_x) + QGFM = QGF(2, n) + elements = [Poly(tuple(QGFM.to_bits(i))) for i in gf.elements[1:]] + for f_x in elements: + blq = MultiplyPolyByConstantMod(f_x, m_x) + qlt_testing.assert_valid_bloq_decomposition(blq) + + +@pytest.mark.parametrize('m_x', [Poly.Degrees([2, 1, 0]), Poly.Degrees([3, 1, 0])]) +def test_multiply_by_constant_mod_counts(m_x): + n = len(m_x.coeffs) - 1 + gf = GF(2, n, irreducible_poly=m_x) + QGFM = QGF(2, n) + elements = [Poly(tuple(QGFM.to_bits(i))) for i in gf.elements[1:]] + for f_x in elements: + blq = MultiplyPolyByConstantMod(f_x, m_x) + qlt_testing.assert_equivalent_bloq_counts(blq)