Skip to content

Create a general polynomial multiplication via karatsuba bloq #1628

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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

Check warning on line 15 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / pylint

W0611: Unused Union imported from typing (unused-import)

Check warning on line 15 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / pylint

W0611: Unused Set imported from typing (unused-import)

Check warning on line 15 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / pylint

W0611: Unused Dict imported from typing (unused-import)
import numpy as np

import attrs

from qualtran import (

Check warning on line 20 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / pylint

W0611: Unused QGF imported from qualtran (unused-import)

Check warning on line 20 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / pylint

W0611: Unused Signature imported from qualtran (unused-import)

Check warning on line 20 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / pylint

W0611: Unused Register imported from qualtran (unused-import)

Check warning on line 20 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / pylint

W0611: Unused DecomposeTypeError imported from qualtran (unused-import)

Check warning on line 20 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / pylint

W0611: Unused BloqDocSpec imported from qualtran (unused-import)

Check warning on line 20 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / pylint

W0611: Unused bloq_example imported from qualtran (unused-import)

Check warning on line 20 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / pylint

W0611: Unused Bloq imported from qualtran (unused-import)
Bloq,
bloq_example,
BloqDocSpec,
DecomposeTypeError,
QGFPoly,
Register,
Signature,
QGF,
)
from qualtran.bloqs.gf_arithmetic import GF2MulViaKaratsuba, GF2Addition
from qualtran.bloqs.gf_poly_arithmetic.gf_poly_split_and_join import GFPolyJoin, GFPolySplit
from qualtran.symbolics import is_symbolic
import qualtran.bloqs.polynomials as qp
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 MultGFPolyByOnePlusXkViaKaratsuba(qp.MultiplyPolyByOnePlusXkViaKaratsuba):

Check failure on line 41 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / mypy

Name "qp.MultiplyPolyByOnePlusXkViaKaratsuba" is not defined [name-defined]
qgf_poly: QGFPoly

@cached_property
def n(self):
return self.qgf_poly.degree + 1

@cached_property
def coef_dtype(self):
return self.qgf_poly.qgf

def _add_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet'):
return bb.add(GF2Addition(self.coef_dtype.bitsize), x=x, y=y)

def _subtract_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet'):
return bb.add(GF2Addition(self.coef_dtype.bitsize), x=x, y=y)

def _mult_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet', z: 'Soquet'):
return bb.add(GF2MulViaKaratsuba(self.coef_dtype), x=x, y=y, z=z)

def _mult_poly(self, bb: 'BloqBuilder', f_x: np.ndarray['Soquet'], g_x: np.ndarray['Soquet'], h_x: np.ndarray['Soquet']):

Check failure on line 61 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / mypy

"ndarray" expects 2 type arguments, but 1 given [type-arg]
return bb.add(MultGFPolyViaKaratsuba(self.qgf_poly), f=f_x, g=g_x, h=h_x)


@attrs.frozen
class MultGFPolyViaKaratsuba(qp.PolynomialMultiplicationViaKaratsuba):

Check failure on line 66 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / mypy

Name "qp.PolynomialMultiplicationViaKaratsuba" is not defined [name-defined]
qgf_poly: QGFPoly

@cached_property
def n(self):
return self.qgf_poly.degree + 1

@cached_property
def coef_dtype(self):
return self.qgf_poly.qgf

def _add_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet'):
return bb.add(GF2Addition(self.coef_dtype.bitsize), x=x, y=y)

def _subtract_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet'):
return bb.add(GF2Addition(self.coef_dtype.bitsize), x=x, y=y)

def _mult_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet', z: 'Soquet'):
return bb.add(GF2MulViaKaratsuba(self.coef_dtype), x=x, y=y, z=z)

def _mult_poly(self, bb: 'BloqBuilder', m: int, f_x: np.ndarray['Soquet'], g_x: np.ndarray['Soquet'], h_x: np.ndarray['Soquet']):

Check failure on line 86 in qualtran/bloqs/gf_poly_arithmetic/gf2_poly_mul.py

View workflow job for this annotation

GitHub Actions / mypy

"ndarray" expects 2 type arguments, but 1 given [type-arg]
return bb.add(MultGFPolyByOnePlusXkViaKaratsuba(attrs.evolve(self.qgf_poly, degree=m-1)), f=f_x, g=g_x, h=h_x)

274 changes: 274 additions & 0 deletions qualtran/bloqs/polynomials/multiplication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# 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 abc
from functools import cached_property
from typing import Dict, Optional, Sequence, Set, TYPE_CHECKING, Union

import attrs
import galois
import numpy as np
import sympy
from galois import GF, Poly

from qualtran import (
Bloq,
bloq_example,
BloqDocSpec,
DecomposeTypeError,
QBit,
QGF,
Register,
Side,
Signature,
QDType
)
from qualtran.bloqs.basic_gates import CNOT, Toffoli
from qualtran.symbolics import ceil, is_symbolic, log2, Shaped, SymbolicInt

if TYPE_CHECKING:
from qualtran import BloqBuilder, Soquet, SoquetT
from qualtran.resource_counting import BloqCountDictT, BloqCountT, SympySymbolAllocator
from qualtran.simulation.classical_sim import ClassicalValT


@attrs.frozen
class MultiplyPolyByOnePlusXkViaKaratsuba(abc.ABC, Bloq):
r"""Out of place multiplication of $(1 + x^k) fg$

Applies the transformation
$$
\ket{f}\ket{g}\ket{h} \rightarrow \ket{f}{\ket{g}}\ket{h \oplus (1+x^k)fg}
$$

Note: While this construction follows Algorithm2 of https://arxiv.org/abs/1910.02849v2,
it has a slight modification. Namely that the original construction doesn't work in
some cases where $k < n$. However reversing the order of the first set of CNOTs (line 2)
makes the construction work for all $k \leq n+1$.

This construction abstracts Algorithm2 to work with polynomials of any field.

Args:
n: The degree of the polynomial ($2^n$ is the size of the galois field).
k: An integer specifing the shift $1 + x^k$ (or $1 + 2^k$ for galois fields.)

Registers:
f: The first polynomial.
g: The second polyonmial.
h: The target polynomial.

References:
[Space-efficient quantum multiplication of polynomials for binary finite fields with
sub-quadratic Toffoli gate count](https://arxiv.org/abs/1910.02849v2) Algorithm 2
"""

k: SymbolicInt

def __attrs_post_init__(self):
if is_symbolic(self.k):
return
assert self.k > 0

@cached_property
@abc.abstractmethod
def n(self):
pass

@cached_property
@abc.abstractmethod
def coef_dtype(self):
pass


@cached_property
def l(self):
return max(0, 2 * self.n - self.k - 1)

@cached_property
def signature(self) -> 'Signature':
return Signature(
[
Register('f', dtype=self.coef_dtype, shape=(self.n,)),
Register('g', dtype=self.coef_dtype, shape=(self.n,)),
Register('h', dtype=self.coef_dtype, shape=(2 * self.k + self.l,)),
]
)

@abc.abstractmethod
def _add_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet'):
""""""

@abc.abstractmethod
def _subtract_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet'):
""""""

@abc.abstractmethod
def _mult_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet', z: 'Soquet'):
""""""

@abc.abstractmethod
def _mult_poly(self, bb: 'BloqBuilder', f_x: np.ndarray['Soquet'], g_x: np.ndarray['Soquet'], h_x: np.ndarray['Soquet']):
""""""

def build_composite_bloq(
self, bb: 'BloqBuilder', f: 'SoquetT', g: 'SoquetT', h: 'SoquetT'
) -> Dict[str, 'SoquetT']:
n = self.n
k = self.k
l = self.l
if is_symbolic(n) or is_symbolic(k) or is_symbolic(l):
raise DecomposeTypeError(f"symbolic decomposition is not supported for {self}")
assert isinstance(f, np.ndarray)
assert isinstance(g, np.ndarray)
assert isinstance(h, np.ndarray)
original = len(h)
if n > 1:
# Note: This is the reverse order of what https://arxiv.org/abs/1910.02849v2 has.
# The is because the reverse order to makes the construction work for k <= n+1.
for i in reversed(range(l)):
h[2 * k + i], h[k + i] = self._add_coefs(bb, h[2 * k + i], h[k + i])
for i in range(k):
h[k + i], h[i] = self._add_coefs(bb, h[k + i], h[i])

f, g, h[k : 2 * k + l] = self._mult_poly(bb,
f, g, h[k : 2 * k + l]
)
for i in range(k):
h[k + i], h[i] = self._subtract_coefs(bb, h[k + i], h[i])
for i in range(l):
h[2 * k + i], h[k + i] = self._subtract_coefs(bb, h[2 * k + i], h[k + i])
else:
h[k], h[0] = self._add_coefs(bb, h[k], h[0])
(f[0], g[0]), h[k] = self._mult_coefs(bb, f[0], g[0], h[k])
h[k], h[0] = self._subtract_coefs(h[k], h[0])

Check failure on line 154 in qualtran/bloqs/polynomials/multiplication.py

View workflow job for this annotation

GitHub Actions / mypy

Missing positional argument "y" in call to "_subtract_coefs" of "MultiplyPolyByOnePlusXkViaKaratsuba" [call-arg]

assert len(h) == original, f'{original=} {len(h)}'
return {'f': f, 'g': g, 'h': h}


@attrs.frozen
class PolynomialMultiplicationViaKaratsuba(Bloq):
r"""Out of place multiplication of binary polynomial multiplication.

Applies the transformation
$$
\ket{f}\ket{g}\ket{h} \rightarrow \ket{f}{\ket{g}}\ket{h \oplus fg}
$$

The multiplication cost of this construction is $n^{\log_2{3}}$.

This construction abstracts Algorithm3 to work with polynomials of any field.

Args:
n: The degree of the polynomial ($2^n$ is the size of the galois field).

Registers:
f: The first polynomial.
g: The second polyonmial.
h: The target polynomial.

References:
[Space-efficient quantum multiplication of polynomials for binary finite fields with
sub-quadratic Toffoli gate count](https://arxiv.org/abs/1910.02849v2) Algorithm 3
"""

@cached_property
@abc.abstractmethod
def n(self):
pass

@cached_property
@abc.abstractmethod
def coef_dtype(self):
pass

@cached_property
def signature(self) -> 'Signature':
return Signature(
[
Register('f', dtype=self.coef_dtype, shape=(self.n,)),
Register('g', dtype=self.coef_dtype, shape=(self.n,)),
Register('h', dtype=self.coef_dtype, shape=(2 * self.n - 1,)),
]
)

@property
def k(self) -> 'SymbolicInt':
if isinstance(self.n, int):
return (self.n + 1) >> 1
return sympy.ceiling(self.n / 2)


@abc.abstractmethod
def _add_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet'):
""""""

@abc.abstractmethod
def _subtract_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet'):
""""""

@abc.abstractmethod
def _mult_coefs(self, bb: 'BloqBuilder', x: 'Soquet', y: 'Soquet', z: 'Soquet'):
""""""

@abc.abstractmethod
def _mult_poly(self, bb: 'BloqBuilder', m: int, f_x: np.ndarray['Soquet'], g_x: np.ndarray['Soquet'], h_x: np.ndarray['Soquet']):

Check failure on line 226 in qualtran/bloqs/polynomials/multiplication.py

View workflow job for this annotation

GitHub Actions / mypy

"ndarray" expects 2 type arguments, but 1 given [type-arg]
""""""

def build_composite_bloq(
self, bb: 'BloqBuilder', f: 'SoquetT', g: 'SoquetT', h: 'SoquetT'
) -> Dict[str, 'SoquetT']:
k, n = self.k, self.n
if is_symbolic(n) or is_symbolic(k):
raise DecomposeTypeError(f"symbolic decomposition is not supported for {self}")
assert isinstance(f, np.ndarray)
assert isinstance(g, np.ndarray)
assert isinstance(h, np.ndarray)

if n == 1:
(f[0], g[0]), h[0] = self._mult_coefs(f[0], g[0], h[0])

Check failure on line 240 in qualtran/bloqs/polynomials/multiplication.py

View workflow job for this annotation

GitHub Actions / mypy

Missing positional argument "z" in call to "_mult_coefs" of "PolynomialMultiplicationViaKaratsuba" [call-arg]
return {'f': f, 'g': g, 'h': h}

f[:k], g[:k], h[: 3 * k - 1] = self._mult_poly(bb, k, f[:k], g[:k], h[: 3 * k - 1])
w = 2 * k + max(0, 2 * (n - k) - k - 1)
delta = k + w - len(h)
if delta > 0:
# This happens for some values (e.g. n=3) where we need to add extra qubits that will always endup in zero state.
aux = bb.split(bb.allocate(delta, self.coef_dtype))
h = np.concatenate([h, aux])
assert isinstance(h, np.ndarray)

f[k:n], g[k:n], h[k : k + w] = self._mult_poly(bb, n - k, f[k:n], g[k:n], h[k : k + w]
)
if delta > 0:
aux = h[-delta:]
h = h[:-delta]
bb.free(bb.join(aux))

for i in range(n - k):
f[k + i], f[i] = self._add_coefs(bb, f[k + i], f[i])
for i in range(n - k):
g[k + i], g[i] = self._add_coefs(bb, g[k + i], g[i])

f[:k], g[:k], h[k : 3 * k - 1] = bb.add( # type: ignore[index]
attrs.evolve(self, n=k), f=f[:k], g=g[:k], h=h[k : 3 * k - 1]

Check failure on line 265 in qualtran/bloqs/polynomials/multiplication.py

View workflow job for this annotation

GitHub Actions / mypy

Unexpected keyword argument "n" for "evolve" of "PolynomialMultiplicationViaKaratsuba" [call-arg]
)

for i in range(n - k):
g[k + i], g[i] = self._subtract_coefs(bb, g[k + i], g[i])

for i in range(n - k):
f[k + i], f[i] = self._subtract_coefs(bb, f[k + i], f[i])

return {'f': f, 'g': g, 'h': h}
Loading