Skip to content

Commit 0dbe20f

Browse files
committed
Implement logarithm.
1 parent 4b034f5 commit 0dbe20f

9 files changed

Lines changed: 353 additions & 33 deletions

File tree

notebooks/accuracy/log_fbe.ipynb

Lines changed: 75 additions & 0 deletions
Large diffs are not rendered by default.

notebooks/classic/log2_qfbe.ipynb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": 5,
6+
"id": "87da278b",
7+
"metadata": {},
8+
"outputs": [
9+
{
10+
"name": "stdout",
11+
"output_type": "stream",
12+
"text": [
13+
"OK\n"
14+
]
15+
}
16+
],
17+
"source": [
18+
"# Evaluating log2(x), x in [1,2). \n",
19+
"# Reference: https://arxiv.org/pdf/2001.00807 (section 3.1.1)\n",
20+
"\n",
21+
"import numpy as np\n",
22+
"\n",
23+
"# r - precision\n",
24+
"def log2_fbe(x, r=40):\n",
25+
" w=[0]*r\n",
26+
" assert 1<=x<2\n",
27+
" a=x**2\n",
28+
" for i in range(r):\n",
29+
" assert 1<=a<4\n",
30+
" if a>=2:\n",
31+
" w[i]=1\n",
32+
" a=a/2\n",
33+
" a=a**2\n",
34+
" return sum(w[i]*(2**(-(i+1))) for i in range(r))\n",
35+
"\n",
36+
"for x in np.linspace(1, 1.99, 100):\n",
37+
" assert np.isclose(np.log2(x), log2_fbe(x))\n",
38+
"\n",
39+
"print(\"OK\")"
40+
]
41+
}
42+
],
43+
"metadata": {
44+
"kernelspec": {
45+
"display_name": "Python 3",
46+
"language": "python",
47+
"name": "python3"
48+
},
49+
"language_info": {
50+
"codemirror_mode": {
51+
"name": "ipython",
52+
"version": 3
53+
},
54+
"file_extension": ".py",
55+
"mimetype": "text/x-python",
56+
"name": "python",
57+
"nbconvert_exporter": "python",
58+
"pygments_lexer": "ipython3",
59+
"version": "3.12.3"
60+
}
61+
},
62+
"nbformat": 4,
63+
"nbformat_minor": 5
64+
}

qmath/func/bits.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Bitwise functions."""
2+
3+
from psiqworkbench import QFixed, QInt, QUInt, Qubits
4+
from psiqworkbench.qubricks import Qubrick
5+
6+
7+
class HighestSetBit(Qubrick):
8+
"""Finds most significant set bit in a and sets it in ans."""
9+
10+
def _compute(self, a: Qubits, ans: Qubits):
11+
flag: Qubits = self.alloc_temp_qreg(1, "flag")
12+
13+
# For each input qubit i compute which output qubit must be set if i is MSB.
14+
for i in range(a.num_qubits - 1, -1, -1):
15+
# Copy a[i] to ans[j], but only if flag is unset.
16+
flag.x()
17+
ans[i].lelbow(a[i] | flag)
18+
flag.x()
19+
20+
# If ans[i]=1 (which implies flag was unset), set the flag.
21+
# All less significant qubits will be ignored.
22+
flag.x(ans[i])

qmath/func/fbe.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,21 @@
88
https://arxiv.org/abs/2001.00807
99
"""
1010

11+
import math
12+
1113
import psiqworkbench.qubricks as qbk
12-
from psiqworkbench import QFixed, QInt, Qubits, QUInt
14+
from psiqworkbench import QFixed, QInt, Qubits, QUFixed, QUInt
1315
from psiqworkbench.qubits.base_qubits import BaseQubits
1416
from psiqworkbench.qubricks import Qubrick
1517
from psiqworkbench.symbolics.qubrick_costs import QubrickCosts
1618

17-
from .common import AddConst, Negate
18-
from .sqrt import Sqrt
19+
from ..utils.gates import ParallelCnot, ParallelCnotCtrl, write_int
20+
from ..utils.rotate import Div2, rotate_left, rotate_right
1921
from ..utils.symbolic import alloc_temp_qreg_like
22+
from .bits import HighestSetBit
23+
from .common import AddConst, Negate, MultiplyConstAdd
24+
from .sqrt import Sqrt
25+
from .square import Square, SquareOptimized
2026

2127

2228
def _sqrt_half(x: QFixed) -> QFixed:
@@ -119,3 +125,97 @@ def _compute(self, x: QFixed):
119125
cos_op = CosFbe(result_radix=self.result_radix)
120126
cos_op.compute(x)
121127
self.set_result_qreg(cos_op.get_result_qreg())
128+
129+
130+
class Log2FbeSegment(Qubrick):
131+
"""Computes log2(x) where 1<=x<2.
132+
133+
Reference: https://arxiv.org/abs/2001.00807, section 3.1.1.
134+
"""
135+
136+
def _square(self, x: QUFixed) -> QUFixed:
137+
result = QUFixed(self.alloc_temp_qreg(x.num_qubits, name="a"), radix=x.radix)
138+
SquareOptimized(signed=False).compute(x, result)
139+
return result
140+
141+
def _compute(self, x: QUFixed, result: QUFixed):
142+
assert x.num_qubits == 2 + x.radix
143+
assert result.num_qubits == result.radix
144+
a = self._square(x)
145+
146+
for i in range(result.radix):
147+
result_bit = result[result.radix - 1 - i]
148+
result_bit.x(a[-1])
149+
Div2().compute(a, ctrl=result_bit)
150+
a = self._square(a)
151+
152+
153+
class Log2Fbe(Qubrick):
154+
"""Computes log2(x) where x>0."""
155+
156+
def __init__(
157+
self,
158+
*,
159+
result_radix: None | int = None,
160+
**kwargs,
161+
):
162+
super().__init__(**kwargs)
163+
self.result_radix = result_radix
164+
165+
def _compute(self, x: QUFixed):
166+
# Find most significant bit of `x`, set it it `msb`.
167+
xn = x.num_qubits
168+
msb = self.alloc_temp_qreg(xn, "msb")
169+
HighestSetBit().compute(x, msb)
170+
171+
# Make shifted copy of input, such that second most significnat bit in
172+
# the copy corresponds to highest set bit in input.
173+
# This way value in x_copy is in range [1, 2).
174+
x_copy_qubits = self.alloc_temp_qreg(xn, name="x_copy")
175+
x_copy = QUFixed(x_copy_qubits, radix=xn - 2)
176+
for i in range(xn):
177+
# Controlled shift-copy.
178+
if i == xn - 1:
179+
ParallelCnotCtrl().compute(msb[i], x[1:], x_copy_qubits[0 : xn - 1])
180+
else:
181+
shift_left = xn - 2 - i
182+
assert shift_left >= 0
183+
ParallelCnotCtrl().compute(msb[i], x[0 : xn - shift_left], x_copy_qubits[shift_left:])
184+
185+
# Compute logarithm for shifted copy.
186+
r = self.result_radix or x.radix
187+
result_fract_part = QUFixed(self.alloc_temp_qreg(r, "result_frac"), radix=r)
188+
Log2FbeSegment().compute(x_copy, result_fract_part)
189+
190+
# Add integer to result, corresponding to input's shift.
191+
int_part_size = math.ceil(math.log2(max(x.radix, xn - x.radix))) + 1
192+
result_int_part = QInt(self.alloc_temp_qreg(int_part_size, "result_int"))
193+
for i in range(xn):
194+
write_int(result_int_part, i - x.radix, ctrl=msb[i])
195+
196+
self.set_result_qreg(QFixed(result_fract_part | result_int_part, radix=r))
197+
198+
199+
class LogFbe(Qubrick):
200+
"""Computes logarithm in given base."""
201+
202+
def __init__(
203+
self,
204+
base: float,
205+
*,
206+
result_radix: None | int = None,
207+
**kwargs,
208+
):
209+
super().__init__(**kwargs)
210+
self.ans_multiplier = 1.0 / math.log2(base)
211+
self.result_radix = result_radix
212+
213+
def _compute(self, x: QUFixed):
214+
op = Log2Fbe(result_radix=self.result_radix)
215+
with op.computed(x):
216+
_, ans = alloc_temp_qreg_like(self, op.get_result_qreg())
217+
if self.ans_multiplier == 1.0:
218+
ParallelCnot().compute(op.get_result_qreg(), ans)
219+
else:
220+
MultiplyConstAdd(self.ans_multiplier).compute(ans, op.get_result_qreg())
221+
self.set_result_qreg(ans)

qmath/func/fbe_test.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
from qmath.func.fbe import CosFbe, SinFbe
2-
from qmath.utils.test_utils import QPUTestHelper
1+
import os
32

43
import numpy as np
4+
import pytest
5+
6+
from qmath.func.fbe import CosFbe, Log2Fbe, SinFbe
7+
from qmath.utils.test_utils import QPUTestHelper
8+
9+
RUN_SLOW_TESTS = os.getenv("RUN_SLOW_TESTS") == "1"
510

611

712
def test_cos():
@@ -30,3 +35,19 @@ def test_sin():
3035
result = qpu_helper.apply_op([x])
3136
expected = np.sin(np.pi * x)
3237
assert abs(result - expected) < 1e-4
38+
39+
40+
@pytest.mark.skipif(not RUN_SLOW_TESTS, reason="slow test")
41+
def test_log2():
42+
qpu_helper = QPUTestHelper(num_inputs=1, num_qubits=1000, qubits_per_reg=30, radix=24)
43+
qs_x = qpu_helper.inputs[0]
44+
op = Log2Fbe(result_radix=11)
45+
op.compute(qs_x)
46+
result = op.get_result_qreg()
47+
qpu_helper.record_op(op.get_result_qreg())
48+
49+
x_range = list(np.linspace(1, 2, 21)) + [1e-5, 1e-3, 0.5, 3, 4, 10, 20]
50+
for x in x_range:
51+
result = qpu_helper.apply_op([x])
52+
expected = np.log2(x)
53+
assert abs(result - expected) < 2e-3

qmath/func/inv_sqrt.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,16 @@
77
from .square import Square
88
from .common import Subtract, MultiplyAdd
99
from ..utils.symbolic import alloc_temp_qreg_like
10+
from .bits import HighestSetBit
1011

1112

1213
class _InitialGuess(Qubrick):
13-
def _msb(self, a: Qubits, ans: Qubits):
14-
"""Finds most significant bit in a and sets it in ans."""
15-
flag: Qubits = self.alloc_temp_qreg(1, "flag")
16-
17-
# For each input qubit i compute which output qubit must be set if i is MSB.
18-
for i in range(a.num_qubits - 1, -1, -1):
19-
# Copy a[i] to ans[j], but only if flag is unset.
20-
flag.x()
21-
ans[i].lelbow(a[i] | flag)
22-
flag.x()
23-
24-
# If ans[i]=1 (which implies flag was unset), set the flag.
25-
# All less significant qubits will be ignored.
26-
flag.x(ans[i])
2714

2815
def _compute(self, a: QFixed, ans: QFixed):
2916
"""Computes ans := 2**(-(floor(log2(a)))//2)."""
3017
# TODO: can this be optimized to compute result directly into ans?
3118
r = self.alloc_temp_qreg(a.num_qubits, "r")
32-
self._msb(a, r)
19+
HighestSetBit().compute(a, r)
3320

3421
for i in range(a.num_qubits - 1, -1, -1):
3522
pos1 = i - a.radix

qmath/func/square.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ class SquareOptimized(Qubrick):
4444
While this precision issue is not fixed, it's recommedned to use Square instead.
4545
"""
4646

47+
def __init__(self, *, signed=True, **kwargs):
48+
super().__init__(**kwargs)
49+
self.signed = signed
50+
4751
def _compute_unsigned(self, x: QUFixed, target: QUFixed):
4852
"""Computes square assuming x is unsigned."""
4953
anc: Qubits = self.alloc_temp_qreg(target.num_qubits, "anc")
@@ -61,10 +65,13 @@ def _compute_unsigned(self, x: QUFixed, target: QUFixed):
6165
anc.release()
6266

6367
def _compute(self, x: QFixed, target: QFixed):
64-
with AbsInPlace().computed(x):
65-
x_unsigned = QUFixed(x[0 : x.num_qubits - 1], radix=x.radix)
66-
target_unsigned = QUFixed(target[0 : target.num_qubits - 1], radix=target.radix)
67-
self._compute_unsigned(x_unsigned, target_unsigned)
68+
if self.signed:
69+
with AbsInPlace().computed(x):
70+
x_unsigned = QUFixed(x[0 : x.num_qubits - 1], radix=x.radix)
71+
target_unsigned = QUFixed(target[0 : target.num_qubits - 1], radix=target.radix)
72+
self._compute_unsigned(x_unsigned, target_unsigned)
73+
else:
74+
self._compute_unsigned(x, target)
6875

6976
def _estimate(self, x: SymbolicQFixed, target: SymbolicQFixed):
7077
n = x.num_qubits

qmath/utils/gates.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from psiqworkbench import Qubits, QUInt, Qubrick, QFixed
1+
from psiqworkbench import Qubits, QUInt, Qubrick, QFixed, QInt
22
from psiqworkbench.symbolics.qubrick_costs import QubrickCosts
33

44
from typing import Optional
@@ -14,6 +14,13 @@ def ccnot(a: Qubits, b: Qubits, c: Qubits, ctrl: Optional[Qubits] = None):
1414
c.x(a | b | ctrl)
1515

1616

17+
def swap(x: Qubits, y: Qubits, ctrl: Optional[Qubits] = None):
18+
if ctrl is None:
19+
x.swap(y)
20+
else:
21+
x.swap(y, condition_mask=ctrl)
22+
23+
1724
def write_uint(target: QUInt, number: int, ctrl: Optional[Qubits] = None):
1825
"""Writes target ⊕= number*ctrl."""
1926
assert 0 <= number < 2**target.num_qubits
@@ -22,6 +29,12 @@ def write_uint(target: QUInt, number: int, ctrl: Optional[Qubits] = None):
2229
target[i].x(ctrl)
2330

2431

32+
def write_int(target: QInt, number: int, ctrl: Optional[Qubits] = None):
33+
n = target.num_qubits
34+
assert -(2 ** (n - 1)) <= number < 2 ** (n - 1)
35+
write_uint(target, number % (2**n), ctrl=ctrl)
36+
37+
2538
def write_qfixed(target: QFixed, number: float, ctrl: Optional[Qubits] = None):
2639
"""Writes target ⊕= number*ctrl."""
2740
assert 0 <= number < 2**target.num_qubits
@@ -41,3 +54,17 @@ def _estimate(self, a: Qubits, b: Qubits):
4154
n = a.num_qubits
4255
assert b.num_qubits == n
4356
self.get_qc().add_cost_event(QubrickCosts(active_volume=4 * n))
57+
58+
59+
class ParallelCnotCtrl(Qubrick):
60+
def _compute(
61+
self,
62+
ctrl: Qubits,
63+
src: Qubits,
64+
dst: Qubits,
65+
):
66+
assert len(ctrl) == 1
67+
n = src.num_qubits
68+
assert dst.num_qubits == n
69+
for i in range(n):
70+
dst[i].x(src[i] | ctrl)

0 commit comments

Comments
 (0)