Skip to content

Commit fac5d53

Browse files
Merge branch 'main' into comparison
2 parents ec7f5fc + 0ef9f1d commit fac5d53

6 files changed

+208
-61
lines changed

qualtran/bloqs/gf_arithmetic/gf2_inverse.ipynb

+9-2
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,22 @@
5858
" a^{-1} = a^{2^m - 2}\n",
5959
"$$\n",
6060
"\n",
61-
"Thus, the inverse can be obtained via $m - 1$ squaring and multiplication operations.\n",
61+
"The exponential $a^{2^m - 2}$ is computed using $\\mathcal{O}(m)$ squaring and\n",
62+
"$\\mathcal{O}(\\log_2(m))$ multiplications via Itoh-Tsujii inversion. The algorithm is described on\n",
63+
"page 4 and 5 of Ref[1] and resembles binary exponentiation. The inverse is computed as $B_{n-1}^2$,\n",
64+
"where $B_1 = x$ and $B_{i+j} = B_i B_j^{2^i}$.\n",
6265
"\n",
6366
"#### Parameters\n",
6467
" - `bitsize`: The degree $m$ of the galois field $GF(2^m)$. Also corresponds to the number of qubits in the input register whose inverse should be calculated. \n",
6568
"\n",
6669
"#### Registers\n",
6770
" - `x`: Input THRU register of size $m$ that stores elements from $GF(2^m)$.\n",
6871
" - `result`: Output RIGHT register of size $m$ that stores $x^{-1}$ from $GF(2^m)$.\n",
69-
" - `junk`: Output RIGHT register of size $m$ and shape ($m - 2$) that stores results from intermediate multiplications.\n"
72+
" - `junk`: Output RIGHT register of size $m$ and shape ($m - 2$) that stores results from intermediate multiplications. \n",
73+
"\n",
74+
"#### References\n",
75+
" - [Efficient quantum circuits for binary elliptic curve arithmetic: reducing T -gate complexity](https://arxiv.org/abs/1209.6348). Section 2.3\n",
76+
" - [Structure of parallel multipliers for a class of fields GF(2^m)](https://doi.org/10.1016/0890-5401(89)90045-X)\n"
7077
]
7178
},
7279
{

qualtran/bloqs/gf_arithmetic/gf2_inverse.py

+94-33
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@
3030
from qualtran.bloqs.gf_arithmetic.gf2_addition import GF2Addition
3131
from qualtran.bloqs.gf_arithmetic.gf2_multiplication import GF2Multiplication
3232
from qualtran.bloqs.gf_arithmetic.gf2_square import GF2Square
33-
from qualtran.symbolics import is_symbolic, SymbolicInt
33+
from qualtran.resource_counting.generalizers import ignore_alloc_free, ignore_split_join
34+
from qualtran.symbolics import bit_length, ceil, is_symbolic, log2, SymbolicInt
3435

3536
if TYPE_CHECKING:
36-
from qualtran import BloqBuilder, Soquet
37+
from qualtran import BloqBuilder, Soquet, SoquetT
3738
from qualtran.resource_counting import BloqCountDictT, BloqCountT, CostKey, SympySymbolAllocator
3839
from qualtran.simulation.classical_sim import ClassicalValT
3940

@@ -62,7 +63,10 @@ class GF2Inverse(Bloq):
6263
a^{-1} = a^{2^m - 2}
6364
$$
6465
65-
Thus, the inverse can be obtained via $m - 1$ squaring and multiplication operations.
66+
The exponential $a^{2^m - 2}$ is computed using $\mathcal{O}(m)$ squaring and
67+
$\mathcal{O}(\log_2(m))$ multiplications via Itoh-Tsujii inversion. The algorithm is described on
68+
page 4 and 5 of Ref[1] and resembles binary exponentiation. The inverse is computed as $B_{n-1}^2$,
69+
where $B_1 = x$ and $B_{i+j} = B_i B_j^{2^i}$.
6670
6771
Args:
6872
bitsize: The degree $m$ of the galois field $GF(2^m)$. Also corresponds to the number of
@@ -73,15 +77,21 @@ class GF2Inverse(Bloq):
7377
result: Output RIGHT register of size $m$ that stores $x^{-1}$ from $GF(2^m)$.
7478
junk: Output RIGHT register of size $m$ and shape ($m - 2$) that stores
7579
results from intermediate multiplications.
80+
81+
References:
82+
[Efficient quantum circuits for binary elliptic curve arithmetic: reducing T -gate complexity](https://arxiv.org/abs/1209.6348).
83+
Section 2.3
84+
85+
[Structure of parallel multipliers for a class of fields GF(2^m)](https://doi.org/10.1016/0890-5401(89)90045-X)
7686
"""
7787

7888
bitsize: SymbolicInt
7989

8090
@cached_property
8191
def signature(self) -> 'Signature':
8292
junk_reg = (
83-
[Register('junk', dtype=self.qgf, shape=(self.bitsize - 2,), side=Side.RIGHT)]
84-
if is_symbolic(self.bitsize) or self.bitsize > 2
93+
[Register('junk', dtype=self.qgf, shape=(self.n_junk_regs,), side=Side.RIGHT)]
94+
if is_symbolic(self.bitsize) or self.bitsize > 1
8595
else []
8696
)
8797
return Signature(
@@ -96,60 +106,111 @@ def signature(self) -> 'Signature':
96106
def qgf(self) -> QGF:
97107
return QGF(characteristic=2, degree=self.bitsize)
98108

109+
@cached_property
110+
def n_junk_regs(self) -> SymbolicInt:
111+
return 2 * bit_length(self.bitsize - 1) + self.bitsize_hamming_weight
112+
113+
@cached_property
114+
def bitsize_hamming_weight(self) -> SymbolicInt:
115+
"""Hamming weight of self.bitsize - 1"""
116+
return (
117+
bit_length(self.bitsize - 1)
118+
if is_symbolic(self.bitsize)
119+
else int(self.bitsize - 1).bit_count()
120+
)
121+
99122
def my_static_costs(self, cost_key: 'CostKey'):
123+
from qualtran._infra.gate_with_registers import total_bits
100124
from qualtran.resource_counting import QubitCount
101125

102126
if isinstance(cost_key, QubitCount):
103-
return self.signature.n_qubits()
127+
return total_bits(self.signature.rights())
104128

105129
return NotImplemented
106130

107-
def build_composite_bloq(self, bb: 'BloqBuilder', *, x: 'Soquet') -> Dict[str, 'Soquet']:
131+
def build_composite_bloq(self, bb: 'BloqBuilder', *, x: 'Soquet') -> Dict[str, 'SoquetT']:
108132
if is_symbolic(self.bitsize):
109133
raise DecomposeTypeError(f"Cannot decompose symbolic {self}")
134+
110135
result = bb.allocate(dtype=self.qgf)
111136
if self.bitsize == 1:
112137
x, result = bb.add(GF2Addition(self.bitsize), x=x, y=result)
113138
return {'x': x, 'result': result}
114139

115-
x = bb.add(GF2Square(self.bitsize), x=x)
116-
x, result = bb.add(GF2Addition(self.bitsize), x=x, y=result)
117-
118140
junk = []
119-
for i in range(2, self.bitsize):
120-
x = bb.add(GF2Square(self.bitsize), x=x)
121-
x, result, new_result = bb.add(GF2Multiplication(self.bitsize), x=x, y=result)
122-
junk.append(result)
123-
result = new_result
124-
x = bb.add(GF2Square(self.bitsize), x=x)
125-
return {'x': x, 'result': result} | ({'junk': np.array(junk)} if junk else {})
141+
beta = bb.allocate(dtype=self.qgf)
142+
x, beta = bb.add(GF2Addition(self.bitsize), x=x, y=beta)
143+
is_first = True
144+
bitsize_minus_one = int(self.bitsize - 1)
145+
for i in range(bitsize_minus_one.bit_length()):
146+
if (1 << i) & bitsize_minus_one:
147+
if is_first:
148+
beta, result = bb.add(GF2Addition(self.bitsize), x=beta, y=result)
149+
is_first = False
150+
else:
151+
for j in range(2**i):
152+
result = bb.add(GF2Square(self.bitsize), x=result)
153+
beta, result, new_result = bb.add(
154+
GF2Multiplication(self.bitsize), x=beta, y=result
155+
)
156+
junk.append(result)
157+
result = new_result
158+
beta_squared = bb.allocate(dtype=self.qgf)
159+
beta, beta_squared = bb.add(GF2Addition(self.bitsize), x=beta, y=beta_squared)
160+
for j in range(2**i):
161+
beta_squared = bb.add(GF2Square(self.bitsize), x=beta_squared)
162+
beta, beta_squared, beta_new = bb.add(
163+
GF2Multiplication(self.bitsize), x=beta, y=beta_squared
164+
)
165+
junk.extend([beta, beta_squared])
166+
beta = beta_new
167+
junk.append(beta)
168+
result = bb.add(GF2Square(self.bitsize), x=result)
169+
assert len(junk) == self.n_junk_regs, f'{len(junk)=}, {self.n_junk_regs=}'
170+
return {'x': x, 'result': result, 'junk': np.array(junk)}
126171

127172
def build_call_graph(
128173
self, ssa: 'SympySymbolAllocator'
129174
) -> Union['BloqCountDictT', Set['BloqCountT']]:
130-
if is_symbolic(self.bitsize) or self.bitsize > 2:
131-
return {
132-
GF2Addition(self.bitsize): 1,
133-
GF2Square(self.bitsize): self.bitsize - 1,
134-
GF2Multiplication(self.bitsize): self.bitsize - 2,
135-
}
136-
return {GF2Addition(self.bitsize): 1} | (
137-
{GF2Square(self.bitsize): 1} if self.bitsize == 2 else {}
138-
)
175+
if not is_symbolic(self.bitsize) and self.bitsize == 1:
176+
return {GF2Addition(self.bitsize): 1}
177+
square_count = self.bitsize + 2 ** ceil(log2(self.bitsize)) - 1
178+
if not is_symbolic(self.bitsize):
179+
n = self.bitsize - 1
180+
square_count -= n & (-n)
181+
return {
182+
GF2Addition(self.bitsize): 2 + ceil(log2(self.bitsize)),
183+
GF2Square(self.bitsize): square_count,
184+
GF2Multiplication(self.bitsize): ceil(log2(self.bitsize))
185+
+ self.bitsize_hamming_weight
186+
- 1,
187+
}
139188

140189
def on_classical_vals(self, *, x) -> Dict[str, 'ClassicalValT']:
141190
assert isinstance(x, self.qgf.gf_type)
142-
x_temp = x**2
143-
result = x_temp
144191
junk = []
145-
for i in range(2, int(self.bitsize)):
146-
junk.append(result)
147-
x_temp = x_temp * x_temp
148-
result = result * x_temp
192+
bitsize_minus_one = int(self.bitsize - 1)
193+
beta = x
194+
result = self.qgf.gf_type(0)
195+
is_first = True
196+
for i in range(bitsize_minus_one.bit_length()):
197+
if (1 << i) & bitsize_minus_one:
198+
if is_first:
199+
is_first = False
200+
result = beta
201+
else:
202+
for j in range(2**i):
203+
result = result**2
204+
junk.append(result)
205+
result = result * beta
206+
beta_squared = beta ** (2 ** (2**i))
207+
junk.extend([beta, beta_squared])
208+
beta = beta * beta_squared
209+
junk.append(beta)
149210
return {'x': x, 'result': x ** (-1), 'junk': np.array(junk)}
150211

151212

152-
@bloq_example
213+
@bloq_example(generalizer=[ignore_split_join, ignore_alloc_free])
153214
def _gf16_inverse() -> GF2Inverse:
154215
gf16_inverse = GF2Inverse(4)
155216
return gf16_inverse

qualtran/bloqs/gf_arithmetic/gf2_inverse_test.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
GF2Inverse,
2323
)
2424
from qualtran.resource_counting import get_cost_value, QECGatesCost, QubitCount
25-
from qualtran.testing import assert_consistent_classical_action
25+
from qualtran.resource_counting.generalizers import ignore_alloc_free, ignore_split_join
26+
from qualtran.symbolics import ceil, log2
27+
from qualtran.testing import assert_consistent_classical_action, assert_equivalent_bloq_counts
2628

2729

2830
def test_gf16_inverse(bloq_autotester):
@@ -36,8 +38,10 @@ def test_gf2_inverse_symbolic(bloq_autotester):
3638
def test_gf2_inverse_symbolic_toffoli_complexity():
3739
bloq = _gf2_inverse_symbolic.make()
3840
m = bloq.bitsize
39-
assert get_cost_value(bloq, QECGatesCost()).total_toffoli_only() - m**2 * (m - 2) == 0
40-
assert sympy.simplify(get_cost_value(bloq, QubitCount()) - m**2) == 0
41+
expected_expr = m**2 * (2 * ceil(log2(m)) - 1)
42+
assert get_cost_value(bloq, QECGatesCost()).total_toffoli_only() - expected_expr == 0
43+
expected_expr = m * (3 * ceil(log2(m)) + 2)
44+
assert sympy.simplify(get_cost_value(bloq, QubitCount()) - expected_expr) == 0
4145

4246

4347
def test_gf2_inverse_classical_sim_quick():
@@ -53,3 +57,9 @@ def test_gf2_inverse_classical_sim(m):
5357
bloq = GF2Inverse(m)
5458
GFM = GF(2**m)
5559
assert_consistent_classical_action(bloq, x=GFM.elements[1:])
60+
61+
62+
@pytest.mark.parametrize('m', [*range(1, 12)])
63+
def test_gf2_equivalent_bloq_counts(m):
64+
bloq = GF2Inverse(m)
65+
assert_equivalent_bloq_counts(bloq, generalizer=[ignore_split_join, ignore_alloc_free])

qualtran/bloqs/gf_arithmetic/gf2_multiplication.ipynb

+5-4
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,13 @@
5656
"gates.\n",
5757
"\n",
5858
"#### Parameters\n",
59-
" - `bitsize`: The degree $m$ of the galois field $GF(2^m)$. Also corresponds to the number of qubits in each of the two input registers $a$ and $b$ that should be multiplied. \n",
59+
" - `bitsize`: The degree $m$ of the galois field $GF(2^m)$. Also corresponds to the number of qubits in each of the two input registers $a$ and $b$ that should be multiplied.\n",
60+
" - `plus_equal_prod`: If True, implements the `PlusEqualProduct` version that applies the map $|x\\rangle |y\\rangle |z\\rangle \\rightarrow |x\\rangle |y\\rangle |x + z\\rangle$. \n",
6061
"\n",
6162
"#### Registers\n",
6263
" - `x`: Input THRU register of size $m$ that stores elements from $GF(2^m)$.\n",
6364
" - `y`: Input THRU register of size $m$ that stores elements from $GF(2^m)$.\n",
64-
" - `result`: Output RIGHT register of size $m$ that stores the product $x * y$ in $GF(2^m)$. \n",
65+
" - `result`: Register of size $m$ that stores the product $x * y$ in $GF(2^m)$. If plus_equal_prod is True - result is a THRU register and stores $result + x * y$. If plus_equal_prod is False - result is a RIGHT register and stores $x * y$. \n",
6566
"\n",
6667
"#### References\n",
6768
" - [On the Design and Optimization of a Quantum Polynomial-Time Attack on Elliptic Curve Cryptography](https://arxiv.org/abs/0710.1093). \n",
@@ -99,7 +100,7 @@
99100
},
100101
"outputs": [],
101102
"source": [
102-
"gf16_multiplication = GF2Multiplication(4)"
103+
"gf16_multiplication = GF2Multiplication(4, plus_equal_prod=True)"
103104
]
104105
},
105106
{
@@ -114,7 +115,7 @@
114115
"import sympy\n",
115116
"\n",
116117
"m = sympy.Symbol('m')\n",
117-
"gf2_multiplication_symbolic = GF2Multiplication(m)"
118+
"gf2_multiplication_symbolic = GF2Multiplication(m, plus_equal_prod=False)"
118119
]
119120
},
120121
{

0 commit comments

Comments
 (0)