Skip to content

Commit 06bfba2

Browse files
authored
Quantum Compiler (#2)
Implement EvaluateExpression qubrick that builds a circuit to evaluate symbolic expression. Currently support only addition, subtraction and multiplication.
1 parent 56ebadb commit 06bfba2

13 files changed

Lines changed: 18224 additions & 6 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ build/
44
*.egg-info/
55
tmp*
66
notebooks/re/tmp*
7+
notebooks/tmp*
78
profiling/profile.json

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ fail_fast: true
22

33
repos:
44
- repo: https://github.com/psf/black
5-
rev: 25.11.0
5+
rev: 26.1.0
66
hooks:
77
- id: black
88
language_version: python3.12

notebooks/compiler_demo.ipynb

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

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ dependencies = [
2727
dev = [
2828
"pytest",
2929
"pre-commit>=3.0.0",
30-
"black>=23.7.0",
30+
"black==26.1.0",
3131
"py-spy",
3232
]
3333

qmath/compile/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .evaluate import EvaluateExpression

qmath/compile/evaluate.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import ast
2+
3+
from psiqworkbench import QPU, QUInt, QFixed, Qubrick
4+
from psiqworkbench.filter_presets import BIT_DEFAULT
5+
6+
from qmath.utils.symbolic import alloc_temp_qreg_like
7+
from qmath.func.common import MultiplyAdd, MultiplyConstAdd, Add, AddConst, Negate
8+
from qmath.func.square import Square
9+
10+
from qmath.utils.gates import ParallelCnot
11+
12+
# Type alias to represent quantum register or a literal number.
13+
QValue = QFixed | float
14+
15+
16+
# Ensures that x is of type QValue.
17+
def _make_qvalue(x) -> QValue:
18+
if isinstance(x, QFixed):
19+
return x
20+
if isinstance(x, int) or isinstance(x, float):
21+
return float(x)
22+
raise ValueError("Unsupported type", type(x))
23+
24+
25+
ops = []
26+
27+
28+
class EvaluateExpression(Qubrick):
29+
"""Evaluates arithmetic expression."""
30+
31+
def __init__(self, expr: str, mutable_vars: set[str] = None, **kwargs):
32+
super().__init__(**kwargs)
33+
self.expr = expr
34+
self.vars = dict()
35+
self.immutable_regs = set()
36+
self.mutable_vars = mutable_vars or set()
37+
38+
def _make_copy(self, x: QFixed) -> QFixed:
39+
_, ans = alloc_temp_qreg_like(self, x)
40+
ParallelCnot().compute(x, ans)
41+
return ans
42+
43+
def _implement_unary_op(self, op: ast.BinOp, arg: QValue) -> QValue:
44+
if isinstance(op, ast.USub):
45+
return self._negate(arg)
46+
raise ValueError(f"Unsupported unary op: {op}.")
47+
48+
def _implement_binary_op(self, op: ast.BinOp, arg1: QValue, arg2: QValue) -> QValue:
49+
if isinstance(op, ast.Add):
50+
return self._add(arg1, arg2)
51+
if isinstance(op, ast.Sub):
52+
return self._sub(arg1, arg2)
53+
if isinstance(op, ast.Mult):
54+
return self._mul(arg1, arg2)
55+
raise ValueError(f"Unsupported binary op: {op}.")
56+
57+
def _negate(self, arg: QValue) -> QValue:
58+
if isinstance(arg, float):
59+
return -arg
60+
assert isinstance(arg, QFixed)
61+
if arg.mask() in self.immutable_regs:
62+
return self._negate(self._make_copy(arg))
63+
Negate().compute(arg)
64+
return arg
65+
66+
def _add(self, arg1: QValue, arg2: QValue) -> QValue:
67+
if isinstance(arg1, float) and isinstance(arg2, float):
68+
return arg1 + arg2
69+
if isinstance(arg1, float):
70+
return self._add(arg2, arg1)
71+
72+
assert isinstance(arg1, QFixed)
73+
74+
if isinstance(arg2, QFixed):
75+
# Quantum-quantum addition.
76+
if arg1.mask() in self.immutable_regs and arg2.mask() in self.immutable_regs:
77+
return self._add(self._make_copy(arg1), arg2)
78+
if arg1.mask() in self.immutable_regs:
79+
return self._add(arg2, arg1)
80+
Add().compute(arg1, arg2)
81+
return arg1
82+
else:
83+
assert isinstance(arg2, float)
84+
if arg1.mask() in self.immutable_regs:
85+
return self._add(self._make_copy(arg1), arg2)
86+
AddConst(arg2).compute(arg1)
87+
return arg1
88+
89+
def _sub(self, arg1: QValue, arg2: QValue) -> QValue:
90+
if isinstance(arg1, float):
91+
return self._add(-arg1, arg2)
92+
if isinstance(arg2, float):
93+
return self._add(arg1, -arg2)
94+
95+
assert isinstance(arg1, QFixed)
96+
assert isinstance(arg2, QFixed)
97+
98+
if arg1.mask() not in self.immutable_regs:
99+
# arg1 -= arg2
100+
with Negate().computed(arg1):
101+
Add().compute(arg1, arg2)
102+
return arg1
103+
elif arg2.mask() not in self.immutable_regs:
104+
# arg2 := -arg2
105+
# arg2 += arg1
106+
Negate().compute(arg2)
107+
Add().compute(arg2, arg1)
108+
return arg2
109+
else:
110+
# Both immutable. Allocate answer.
111+
return self._negate(arg1, self._make_copy(arg2))
112+
113+
def _mul(self, arg1: QValue, arg2: QValue) -> QValue:
114+
if isinstance(arg1, float) and isinstance(arg2, float):
115+
return arg1 * arg2
116+
if isinstance(arg1, float):
117+
return self._mul(arg2, arg1)
118+
119+
assert isinstance(arg1, QFixed)
120+
_, ans = alloc_temp_qreg_like(self, arg1)
121+
122+
if isinstance(arg2, QFixed):
123+
if arg1.mask() == arg2.mask():
124+
Square().compute(arg1, ans)
125+
return ans
126+
MultiplyAdd().compute(ans, arg1, arg2)
127+
else:
128+
assert isinstance(arg2, float)
129+
MultiplyConstAdd(arg2).compute(ans, arg1)
130+
return ans
131+
132+
def _convert_ast_node(self, node) -> QFixed | float:
133+
if isinstance(node, ast.BinOp):
134+
arg1 = self._convert_ast_node(node.left)
135+
arg2 = self._convert_ast_node(node.right)
136+
return self._implement_binary_op(node.op, arg1, arg2)
137+
elif isinstance(node, ast.UnaryOp):
138+
arg = self._convert_ast_node(node.operand)
139+
return self._implement_unary_op(node.op, arg)
140+
elif isinstance(node, ast.Name):
141+
assert node.id in self.vars
142+
return self.vars[node.id]
143+
elif isinstance(node, ast.Constant):
144+
return _make_qvalue(node.value)
145+
else:
146+
raise ValueError(f"Cannot handle: {node}")
147+
148+
def _compute(self, args: dict):
149+
self.vars = dict()
150+
for key, value in args.items():
151+
value = _make_qvalue(value)
152+
self.vars[key] = value
153+
if key not in self.mutable_vars and isinstance(value, QFixed):
154+
self.immutable_regs.add(value.mask())
155+
156+
root = ast.parse(self.expr, mode="eval")
157+
ans = self._convert_ast_node(root.body)
158+
self.set_result_qreg(ans)

qmath/compile/evaluate_test.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import os
2+
from dataclasses import dataclass
3+
from typing import Callable
4+
import pytest
5+
6+
from psiqworkbench import QPU, QFixed
7+
from psiqworkbench.filter_presets import BIT_DEFAULT
8+
9+
from qmath.compile import EvaluateExpression
10+
from qmath.utils.test_utils import QPUTestHelper
11+
12+
RUN_SLOW_TESTS = os.getenv("RUN_SLOW_TESTS") == "1"
13+
14+
15+
@dataclass
16+
class EvaluateTestCase:
17+
expr: str
18+
args: list[str]
19+
func: Callable[..., float]
20+
inputs: list[list[float]]
21+
num_qubits: int
22+
qubits_per_reg: int = 8
23+
radix: int = 1
24+
25+
26+
def _test_evaluate(tc: EvaluateTestCase):
27+
qpu_helper = QPUTestHelper(
28+
num_inputs=len(tc.args),
29+
num_qubits=tc.num_qubits,
30+
qubits_per_reg=tc.qubits_per_reg,
31+
radix=tc.radix,
32+
)
33+
v = {tc.args[i]: qpu_helper.inputs[i] for i in range(len(tc.args))}
34+
op = EvaluateExpression(tc.expr, qc=qpu_helper.qpu)
35+
op.compute(v)
36+
qpu_helper.record_op(op.get_result_qreg())
37+
38+
for args in tc.inputs:
39+
assert qpu_helper.apply_op(args) == tc.func(*args)
40+
41+
42+
# Use this test case for debugging. It does not use any helpers.
43+
@pytest.mark.skipif(not RUN_SLOW_TESTS, reason="slow test")
44+
def test_debug():
45+
qpu = QPU(filters=BIT_DEFAULT)
46+
qpu.reset(1000)
47+
qs_x = QFixed(20, name="x", radix=5, qpu=qpu)
48+
qs_y = QFixed(20, name="y", radix=5, qpu=qpu)
49+
qs_z = QFixed(20, name="z", radix=5, qpu=qpu)
50+
x, y, z = -10, 0, 5
51+
qs_x.write(x)
52+
qs_y.write(y)
53+
qs_z.write(z)
54+
55+
expected = -x + 2 * (y + 3 * z - x * x) + x * y + x * y * z - z * x
56+
compiler = EvaluateExpression("-x + 2*(y + 3*z - x*x) + x*y + x*y*z - z*x", qc=qpu)
57+
compiler.compute({"x": qs_x, "y": qs_y, "z": qs_z})
58+
ans = compiler.get_result_qreg()
59+
60+
assert ans.read() == expected
61+
62+
63+
def test_add():
64+
_test_evaluate(
65+
EvaluateTestCase(
66+
expr="x+y+z",
67+
args=["x", "y", "z"],
68+
func=lambda x, y, z: x + y + z,
69+
inputs=[[1, 2, -1], [-3.5, 4, 0]],
70+
num_qubits=50,
71+
)
72+
)
73+
74+
75+
def test_multiply():
76+
_test_evaluate(
77+
EvaluateTestCase(
78+
expr="x*y",
79+
args=["x", "y"],
80+
func=lambda x, y: x * y,
81+
inputs=[[3, 2], [-3.5, 4]],
82+
num_qubits=100,
83+
)
84+
)
85+
86+
87+
def test_multiply_const():
88+
_test_evaluate(
89+
EvaluateTestCase(
90+
expr="x*2.5",
91+
args=["x"],
92+
func=lambda x: x * 2.5,
93+
inputs=[[-1], [0], [2], [4]],
94+
num_qubits=100,
95+
)
96+
)
97+
98+
99+
@pytest.mark.skipif(not RUN_SLOW_TESTS, reason="slow test")
100+
def test_complex_expression():
101+
_test_evaluate(
102+
EvaluateTestCase(
103+
expr="-x + 2*(y + 3*z - x*x) + x*y + x*y*z - z*x",
104+
args=["x", "y", "z"],
105+
func=lambda x, y, z: -x + 2 * (y + 3 * z - x * x) + x * y + x * y * z - z * x,
106+
inputs=[[1, 2.0, -3], [4.125, 5, 6.5], [-10, 0, 5]],
107+
num_qubits=300,
108+
qubits_per_reg=20,
109+
radix=10,
110+
)
111+
)

qmath/compile/optimizers.py

Whitespace-only changes.

qmath/func/common.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,16 +138,53 @@ def _estimate(self, dst: SymbolicQFixed, lhs: SymbolicQFixed, rhs: SymbolicQFixe
138138
self.get_qc().add_cost_event(cost)
139139

140140

141-
# TODO: implement.
141+
# Preparation for MultiplyConstAdd to handle negative inputs.
142+
class _MulConstPrep(Qubrick):
143+
144+
def _compute(self, x: QFixed, x_sign: Qubits, y: float, dst: QFixed):
145+
x_sign[0].lelbow(x[-1])
146+
Negate().compute(x, ctrl=x_sign)
147+
if y < 0:
148+
x_sign.x()
149+
dst.x(x_sign)
150+
151+
142152
class MultiplyConstAdd(Qubrick):
143153
"""Computes dst += lhs * rhs (rhs is a classical number)."""
144154

145155
def __init__(self, rhs: float, **kwargs):
146156
super().__init__(**kwargs)
147157
self.rhs = rhs
148158

159+
# z += y*x, assuming x>=0, y>0.
160+
def _compute_positive(self, x: QFixed, y: float, z: QFixed):
161+
assert y > 0
162+
x = QInt(x)
163+
z = QInt(z)
164+
min_i = x.radix - z.radix - (x.num_qubits - 1)
165+
max_i = x.radix - z.radix + (z.num_qubits - 1)
166+
167+
for i in range(min_i, max_i + 1):
168+
shift = z.radix - x.radix + i
169+
bit = int(y * (2 ** (-shift))) % 2
170+
if bit == 1:
171+
if shift < 0:
172+
qbk.GidneyAdd().compute(z, x[(-shift):])
173+
else:
174+
qbk.GidneyAdd().compute(z[shift:], x)
175+
149176
def _compute(self, dst: QFixed, lhs: QFixed):
150-
pass
177+
if self.rhs == 0:
178+
return
179+
180+
x_sign = self.alloc_temp_qreg(1, "x_sign")
181+
182+
# Preparation to handle negative inputs.
183+
with _MulConstPrep().computed(lhs, x_sign, self.rhs, dst):
184+
self._compute_positive(lhs, abs(self.rhs), dst)
185+
186+
x_sign.release()
151187

152188
def _estimate(self, dst: SymbolicQFixed, lhs: SymbolicQFixed):
189+
# TODO: implement.
153190
pass

qmath/func/common_test.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import random
22

3-
from qmath.func.common import AbsInPlace, Subtract
3+
from qmath.func.common import AbsInPlace, Subtract, MultiplyConstAdd
44
from qmath.utils.test_utils import QPUTestHelper
55

66

@@ -26,3 +26,16 @@ def test_subtract():
2626
result = qpu_helper.apply_op([x, y])
2727
expected = x - y
2828
assert abs(result - expected) < 1e-9
29+
30+
31+
def test_multiply_const_add():
32+
for y in [-11.25, 0, 1.5, 10.3]:
33+
qpu_helper = QPUTestHelper(num_inputs=2, num_qubits=200, qubits_per_reg=25, radix=15)
34+
qs_x, qs_z = qpu_helper.inputs
35+
MultiplyConstAdd(y).compute(qs_z, qs_x)
36+
qpu_helper.record_op(qs_z)
37+
38+
for x in [-10, 5.5, 0, 10.125]:
39+
result = qpu_helper.apply_op([x, 0])
40+
expected = x * y
41+
assert abs(result - expected) < 1e-4

0 commit comments

Comments
 (0)