spatial-computing 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spatial/__init__.py +6 -0
- spatial/ai/__init__.py +18 -0
- spatial/ai/kvcache.py +81 -0
- spatial/cli.py +135 -0
- spatial/compute/__init__.py +3 -0
- spatial/compute/cost.py +69 -0
- spatial/compute/detect.py +139 -0
- spatial/compute/monitor.py +111 -0
- spatial/compute/scheduler.py +168 -0
- spatial/compute/taskqueue.py +107 -0
- spatial/compute/worker.py +89 -0
- spatial/compute/workloads.py +148 -0
- spatial/core/__init__.py +1 -0
- spatial/core/energy.py +101 -0
- spatial/core/ledger.py +76 -0
- spatial/core/model.py +82 -0
- spatial/core/protocol.py +167 -0
- spatial/core/registry.py +59 -0
- spatial/holographic/__init__.py +8 -0
- spatial/holographic/cgh.py +40 -0
- spatial/holographic/model.py +49 -0
- spatial/intelligence/__init__.py +6 -0
- spatial/intelligence/predictor.py +51 -0
- spatial/memory/__init__.py +5 -0
- spatial/memory/fararray.py +44 -0
- spatial/memory/pageserver.py +141 -0
- spatial/memory/secure.py +100 -0
- spatial/memory/shared.py +43 -0
- spatial/memory/tiered.py +355 -0
- spatial/q/__init__.py +4 -0
- spatial/q/__main__.py +9 -0
- spatial/q/backends/__init__.py +122 -0
- spatial/q/codegen.py +153 -0
- spatial/q/compiler.py +276 -0
- spatial/q/ir.py +121 -0
- spatial/q/lexer.py +61 -0
- spatial/q/parser.py +330 -0
- spatial/q/qir.py +128 -0
- spatial/q/runtime.py +274 -0
- spatial/q/sim.py +218 -0
- spatial/runtime/__init__.py +3 -0
- spatial/runtime/envs.py +74 -0
- spatial/runtime/inspect.py +48 -0
- spatial/runtime/microvm.py +125 -0
- spatial/runtime/planner.py +155 -0
- spatial/runtime/runner.py +108 -0
- spatial/runtime/toolchain.py +35 -0
- spatial/scientific/__init__.py +8 -0
- spatial/scientific/jobs.py +80 -0
- spatial/simulation/__init__.py +7 -0
- spatial/simulation/heat.py +48 -0
- spatial/simulation/nbody.py +31 -0
- spatial_computing-0.5.0.dist-info/METADATA +112 -0
- spatial_computing-0.5.0.dist-info/RECORD +58 -0
- spatial_computing-0.5.0.dist-info/WHEEL +5 -0
- spatial_computing-0.5.0.dist-info/entry_points.txt +3 -0
- spatial_computing-0.5.0.dist-info/licenses/LICENSE +202 -0
- spatial_computing-0.5.0.dist-info/top_level.txt +1 -0
spatial/q/codegen.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Compiles the CLASSICAL part of a SPATIAL Q program (main + tasks) to Python source, which
|
|
2
|
+
CPython then compiles to bytecode. Quantum kernels keep going through the SQIR compiler;
|
|
3
|
+
this replaces the tree-walking interpreter for control flow and arithmetic.
|
|
4
|
+
|
|
5
|
+
Semantics are the interpreter's: same dispatch (`sample`, `expect`, `workload`, tasks,
|
|
6
|
+
`parallel`), same integer-preserving division, same errors for undeclared assignment.
|
|
7
|
+
`compile_program()` returns the generated source so it can be inspected or cached.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .compiler import CONSTS, FUNCS, div
|
|
12
|
+
from .parser import Program
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SQCodegenError(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_BIN = {"and": "and", "or": "or", "==": "==", "!=": "!=", "<": "<", "<=": "<=", ">": ">", ">=": ">=",
|
|
20
|
+
"+": "+", "-": "-", "*": "*", "%": "%"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Gen:
|
|
24
|
+
def __init__(self, prog: Program):
|
|
25
|
+
self.p = prog
|
|
26
|
+
self.lines: list[str] = []
|
|
27
|
+
|
|
28
|
+
def emit(self, depth: int, s: str):
|
|
29
|
+
self.lines.append(" " * depth + s)
|
|
30
|
+
|
|
31
|
+
def expr(self, e, scope: set, target: str) -> str:
|
|
32
|
+
k = e[0]
|
|
33
|
+
if k == "num":
|
|
34
|
+
return repr(e[1])
|
|
35
|
+
if k == "str":
|
|
36
|
+
return repr(e[1])
|
|
37
|
+
if k == "var":
|
|
38
|
+
if e[1] in scope:
|
|
39
|
+
return "v_" + e[1]
|
|
40
|
+
if e[1] in CONSTS:
|
|
41
|
+
return repr(CONSTS[e[1]])
|
|
42
|
+
raise SQCodegenError(f"line {e[2]}: unknown name '{e[1]}'")
|
|
43
|
+
if k == "neg":
|
|
44
|
+
return f"(-{self.expr(e[1], scope, target)})"
|
|
45
|
+
if k == "not":
|
|
46
|
+
return f"(not {self.expr(e[1], scope, target)})"
|
|
47
|
+
if k == "list":
|
|
48
|
+
return "[" + ", ".join(self.expr(x, scope, target) for x in e[1]) + "]"
|
|
49
|
+
if k == "index":
|
|
50
|
+
return f"{self.expr(e[1], scope, target)}[{self.expr(e[2], scope, target)}]"
|
|
51
|
+
if k == "bin":
|
|
52
|
+
a, b = self.expr(e[2], scope, target), self.expr(e[3], scope, target)
|
|
53
|
+
return f"_div({a}, {b})" if e[1] == "/" else f"({a} {_BIN[e[1]]} {b})"
|
|
54
|
+
if k == "call":
|
|
55
|
+
args = ", ".join(self.expr(a, scope, target) for a in e[2])
|
|
56
|
+
if e[1] in self.p.tasks:
|
|
57
|
+
return f"t_{e[1]}({args})"
|
|
58
|
+
if e[1] in FUNCS:
|
|
59
|
+
return f"_F[{e[1]!r}]({args})"
|
|
60
|
+
if e[1] in self.p.kernels:
|
|
61
|
+
raise SQCodegenError(f"line {e[3]}: quantum kernel '{e[1]}' must be used with 'sample' or 'expect'")
|
|
62
|
+
raise SQCodegenError(f"line {e[3]}: unknown function '{e[1]}'")
|
|
63
|
+
if k == "sample":
|
|
64
|
+
args = ", ".join(self.expr(a, scope, target) for a in e[2])
|
|
65
|
+
return f"_rt._sample({e[1]!r}, [{args}], int({self.expr(e[3], scope, target)}), {e[4]!r})"
|
|
66
|
+
if k == "expect":
|
|
67
|
+
args = ", ".join(self.expr(a, scope, target) for a in e[3])
|
|
68
|
+
return f"_rt._expect({e[1]!r}, {e[2]!r}, [{args}], {e[4]!r})"
|
|
69
|
+
if k == "workload":
|
|
70
|
+
kw = ", ".join(f"{kk!r}: {self.expr(v, scope, target)}" for kk, v in e[2].items())
|
|
71
|
+
return f"_rt._workload({e[1]!r}, {{{kw}}}, {target!r})"
|
|
72
|
+
if k == "measure":
|
|
73
|
+
raise SQCodegenError(f"line {e[2]}: 'measure' is only valid inside a quantum kernel")
|
|
74
|
+
raise SQCodegenError(f"cannot compile expression {k}")
|
|
75
|
+
|
|
76
|
+
def block(self, stmts, scope: set, depth: int, target: str):
|
|
77
|
+
if not stmts:
|
|
78
|
+
self.emit(depth, "pass")
|
|
79
|
+
for s in stmts:
|
|
80
|
+
k = s[0]
|
|
81
|
+
if k == "let":
|
|
82
|
+
rhs = self.expr(s[2], scope, target)
|
|
83
|
+
scope.add(s[1])
|
|
84
|
+
self.emit(depth, f"v_{s[1]} = {rhs}")
|
|
85
|
+
elif k == "assign":
|
|
86
|
+
if s[1] not in scope:
|
|
87
|
+
raise SQCodegenError(f"line {s[3]}: assignment to undeclared variable '{s[1]}'")
|
|
88
|
+
self.emit(depth, f"v_{s[1]} = {self.expr(s[2], scope, target)}")
|
|
89
|
+
elif k == "print":
|
|
90
|
+
self.emit(depth, f"_rt.out({', '.join(self.expr(x, scope, target) for x in s[1])})")
|
|
91
|
+
elif k == "for":
|
|
92
|
+
lo, hi = self.expr(s[2], scope, target), self.expr(s[3], scope, target)
|
|
93
|
+
scope.add(s[1])
|
|
94
|
+
self.emit(depth, f"for v_{s[1]} in range({lo}, {hi}):")
|
|
95
|
+
self.block(s[4], scope, depth + 1, target)
|
|
96
|
+
elif k == "while":
|
|
97
|
+
self.emit(depth, f"while {self.expr(s[1], scope, target)}:")
|
|
98
|
+
self.block(s[2], scope, depth + 1, target)
|
|
99
|
+
elif k == "if":
|
|
100
|
+
self.emit(depth, f"if {self.expr(s[1], scope, target)}:")
|
|
101
|
+
self.block(s[2], scope, depth + 1, target)
|
|
102
|
+
if s[3]:
|
|
103
|
+
self.emit(depth, "else:")
|
|
104
|
+
self.block(s[3], scope, depth + 1, target)
|
|
105
|
+
elif k == "return":
|
|
106
|
+
self.emit(depth, f"return {self.expr(s[1], scope, target)}")
|
|
107
|
+
elif k == "expr":
|
|
108
|
+
self.emit(depth, self.expr(s[1], scope, target))
|
|
109
|
+
elif k == "parallel":
|
|
110
|
+
lets = [x for x in s[1] if x[0] in ("let", "assign")]
|
|
111
|
+
if len(lets) != len(s[1]):
|
|
112
|
+
raise SQCodegenError(f"line {s[2]}: a parallel block may only contain let/assignment statements")
|
|
113
|
+
thunks = ", ".join(f"lambda: {self.expr(x[2], scope, target)}" for x in lets)
|
|
114
|
+
names = ", ".join(f"v_{x[1]}" for x in lets)
|
|
115
|
+
for x in lets:
|
|
116
|
+
scope.add(x[1])
|
|
117
|
+
self.emit(depth, f"{names}{',' if len(lets) == 1 else ''} = _par([{thunks}])")
|
|
118
|
+
else:
|
|
119
|
+
raise SQCodegenError(f"line {s[-1]}: statement '{k}' not allowed here")
|
|
120
|
+
|
|
121
|
+
def program(self) -> str:
|
|
122
|
+
for t in self.p.tasks.values():
|
|
123
|
+
self.emit(0, f"def t_{t.name}({', '.join('v_' + a for a in t.params)}):")
|
|
124
|
+
self.block(t.body, set(t.params), 1, t.target)
|
|
125
|
+
self.emit(0, "")
|
|
126
|
+
self.emit(0, "def _main():")
|
|
127
|
+
scope: set = set()
|
|
128
|
+
self.block(self.p.main, scope, 1, "cpu")
|
|
129
|
+
self.emit(1, "return {k[2:]: v for k, v in locals().items() if k.startswith('v_')}")
|
|
130
|
+
return "\n".join(self.lines) + "\n"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def compile_program(prog: Program) -> str:
|
|
134
|
+
"""Python source for the classical part of `prog`."""
|
|
135
|
+
return _Gen(prog).program()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_compiled(rt) -> dict:
|
|
139
|
+
"""Execute a Runtime's program through the compiled classical path. Returns main's variables."""
|
|
140
|
+
import time
|
|
141
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
142
|
+
|
|
143
|
+
def _par(thunks):
|
|
144
|
+
t0 = time.perf_counter()
|
|
145
|
+
with ThreadPoolExecutor(len(thunks)) as ex:
|
|
146
|
+
out = [f.result() for f in [ex.submit(t) for t in thunks]]
|
|
147
|
+
rt.trace.append({"op": "parallel", "branches": len(thunks), "wall_s": time.perf_counter() - t0})
|
|
148
|
+
return out
|
|
149
|
+
|
|
150
|
+
src = compile_program(rt.p)
|
|
151
|
+
ns = {"_rt": rt, "_F": FUNCS, "_div": div, "_par": _par}
|
|
152
|
+
exec(compile(src, "<spatialq-compiled>", "exec"), ns) # source generated by this module, never user-supplied Python
|
|
153
|
+
return ns["_main"]()
|
spatial/q/compiler.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""SPATIAL Q compiler: kernel AST + classical arguments -> SQIR Circuit, then optimisation.
|
|
2
|
+
|
|
3
|
+
Kernels are specialised at call time (like a JIT): classical expressions and loops are
|
|
4
|
+
evaluated at compile time; measurement results are run-time values that may only be used
|
|
5
|
+
as `if` conditions (feed-forward).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from .ir import Circuit, Op
|
|
13
|
+
from .parser import GATES, Kernel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SQCompileError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class MeasRef:
|
|
22
|
+
"""Run-time measurement outcome (classical bit index, expected value)."""
|
|
23
|
+
cbit: int
|
|
24
|
+
value: int = 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
CONSTS = {"pi": math.pi, "tau": math.tau, "e": math.e}
|
|
28
|
+
FUNCS = {"sin": math.sin, "cos": math.cos, "sqrt": math.sqrt, "abs": abs, "min": min, "max": max,
|
|
29
|
+
"floor": math.floor, "acos": math.acos, "asin": math.asin, "exp": math.exp, "log": math.log,
|
|
30
|
+
"len": len, "float": float, "int": int, "pow": pow}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def div(a, b):
|
|
34
|
+
"""`/` keeps integers when the division is exact (so `n / 2` can index or size things)."""
|
|
35
|
+
if isinstance(a, int) and isinstance(b, int) and not isinstance(a, bool) and b != 0 and a % b == 0:
|
|
36
|
+
return a // b
|
|
37
|
+
return a / b
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def eval_const(e, env: dict):
|
|
41
|
+
"""Evaluate a classical expression (shared with the runtime for pure expressions)."""
|
|
42
|
+
k = e[0]
|
|
43
|
+
if k in ("num", "str"):
|
|
44
|
+
return e[1]
|
|
45
|
+
if k == "var":
|
|
46
|
+
if e[1] in env:
|
|
47
|
+
return env[e[1]]
|
|
48
|
+
if e[1] in CONSTS:
|
|
49
|
+
return CONSTS[e[1]]
|
|
50
|
+
raise SQCompileError(f"line {e[2]}: unknown name '{e[1]}'")
|
|
51
|
+
if k == "neg":
|
|
52
|
+
return -eval_const(e[1], env)
|
|
53
|
+
if k == "not":
|
|
54
|
+
v = eval_const(e[1], env)
|
|
55
|
+
return MeasRef(v.cbit, 1 - v.value) if isinstance(v, MeasRef) else (not v)
|
|
56
|
+
if k == "list":
|
|
57
|
+
return [eval_const(x, env) for x in e[1]]
|
|
58
|
+
if k == "index":
|
|
59
|
+
return eval_const(e[1], env)[eval_const(e[2], env)]
|
|
60
|
+
if k == "bin":
|
|
61
|
+
op = e[1]
|
|
62
|
+
a = eval_const(e[2], env)
|
|
63
|
+
if op == "and":
|
|
64
|
+
return a and eval_const(e[3], env)
|
|
65
|
+
if op == "or":
|
|
66
|
+
return a or eval_const(e[3], env)
|
|
67
|
+
b = eval_const(e[3], env)
|
|
68
|
+
return {"+": lambda: a + b, "-": lambda: a - b, "*": lambda: a * b, "/": lambda: div(a, b),
|
|
69
|
+
"%": lambda: a % b, "==": lambda: a == b, "!=": lambda: a != b, "<": lambda: a < b,
|
|
70
|
+
"<=": lambda: a <= b, ">": lambda: a > b, ">=": lambda: a >= b}[op]()
|
|
71
|
+
if k == "call" and e[1] in FUNCS:
|
|
72
|
+
return FUNCS[e[1]](*[eval_const(a, env) for a in e[2]])
|
|
73
|
+
raise SQCompileError(f"expression kind '{k}' is not allowed here")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
MAX_CALL_DEPTH = 32
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class KernelCompiler:
|
|
80
|
+
def __init__(self, kernel: Kernel, args: list, kernels: dict | None = None):
|
|
81
|
+
if any(t == "qubit" for t in kernel.ptypes):
|
|
82
|
+
raise SQCompileError(f"kernel {kernel.name} takes qubit parameters: it can only be called from another kernel")
|
|
83
|
+
if len(args) != len(kernel.params):
|
|
84
|
+
raise SQCompileError(f"kernel {kernel.name} expects {len(kernel.params)} argument(s), got {len(args)}")
|
|
85
|
+
self.k = kernel
|
|
86
|
+
self.kernels = kernels or {}
|
|
87
|
+
self.env = dict(zip(kernel.params, args))
|
|
88
|
+
self.regs: dict[str, list[int]] = {} # register name -> qubit indices
|
|
89
|
+
self.c = Circuit(kernel.name)
|
|
90
|
+
self.cond: tuple[tuple[int, int], ...] = ()
|
|
91
|
+
self.depth = 0
|
|
92
|
+
|
|
93
|
+
def _qubits_from_expr(self, e, line) -> list[int]:
|
|
94
|
+
if e[0] == "var":
|
|
95
|
+
return self.qubits((e[1], None), line)
|
|
96
|
+
if e[0] == "index" and e[1][0] == "var":
|
|
97
|
+
return self.qubits((e[1][1], e[2]), line)
|
|
98
|
+
raise SQCompileError(f"line {line}: a qubit argument must be a register or a register element")
|
|
99
|
+
|
|
100
|
+
def _call(self, name: str, args: list, line: int):
|
|
101
|
+
"""Inline a sub-kernel: its gates are appended to this circuit on the qubits passed in."""
|
|
102
|
+
callee = self.kernels.get(name)
|
|
103
|
+
if callee is None:
|
|
104
|
+
raise SQCompileError(f"line {line}: unknown kernel '{name}'")
|
|
105
|
+
if len(args) != len(callee.params):
|
|
106
|
+
raise SQCompileError(f"line {line}: kernel {name} expects {len(callee.params)} argument(s), got {len(args)}")
|
|
107
|
+
if self.depth >= MAX_CALL_DEPTH:
|
|
108
|
+
raise SQCompileError(f"line {line}: kernel calls nested deeper than {MAX_CALL_DEPTH} (recursion?)")
|
|
109
|
+
child = object.__new__(KernelCompiler)
|
|
110
|
+
child.k, child.kernels, child.c, child.cond, child.depth = callee, self.kernels, self.c, self.cond, self.depth + 1
|
|
111
|
+
child.env, child.regs = {}, {}
|
|
112
|
+
ptypes = callee.ptypes or [None] * len(callee.params)
|
|
113
|
+
for pname, ptype, a in zip(callee.params, ptypes, args):
|
|
114
|
+
if ptype == "qubit":
|
|
115
|
+
child.regs[pname] = self._qubits_from_expr(a, line)
|
|
116
|
+
else:
|
|
117
|
+
child.env[pname] = eval_const(a, self.env)
|
|
118
|
+
child.body(callee.body)
|
|
119
|
+
|
|
120
|
+
def qubits(self, qa, line) -> list[int]:
|
|
121
|
+
name, idx = qa
|
|
122
|
+
if name not in self.regs:
|
|
123
|
+
raise SQCompileError(f"line {line}: unknown qubit register '{name}'")
|
|
124
|
+
qs = self.regs[name]
|
|
125
|
+
size = len(qs)
|
|
126
|
+
if idx is None:
|
|
127
|
+
return list(qs)
|
|
128
|
+
i = eval_const(idx, self.env)
|
|
129
|
+
if not isinstance(i, int) or not 0 <= i < size:
|
|
130
|
+
raise SQCompileError(f"line {line}: index {i!r} out of range for {name}[{size}]")
|
|
131
|
+
return [qs[i]]
|
|
132
|
+
|
|
133
|
+
def one(self, qa, line) -> int:
|
|
134
|
+
qs = self.qubits(qa, line)
|
|
135
|
+
if len(qs) != 1:
|
|
136
|
+
raise SQCompileError(f"line {line}: a single qubit is required here (use {qa[0]}[i])")
|
|
137
|
+
return qs[0]
|
|
138
|
+
|
|
139
|
+
def body(self, stmts):
|
|
140
|
+
for s in stmts:
|
|
141
|
+
kind = s[0]
|
|
142
|
+
if kind == "qubit":
|
|
143
|
+
size = eval_const(s[2], self.env)
|
|
144
|
+
if not isinstance(size, int) or size < 1:
|
|
145
|
+
raise SQCompileError(f"line {s[3]}: register size must be a positive integer")
|
|
146
|
+
self.regs[s[1]] = list(range(self.c.n_qubits, self.c.n_qubits + size))
|
|
147
|
+
self.c.n_qubits += size
|
|
148
|
+
elif kind == "kcall":
|
|
149
|
+
self._call(s[1], s[2], s[3])
|
|
150
|
+
elif kind == "gate":
|
|
151
|
+
_, name, params, qargs, line = s
|
|
152
|
+
ps = tuple(float(eval_const(p, self.env)) for p in params)
|
|
153
|
+
if GATES[name][1] == 1 and qargs[0][1] is None: # broadcast: `h q;`
|
|
154
|
+
for q in self.qubits(qargs[0], line):
|
|
155
|
+
self.c.ops.append(Op(name, (q,), ps, cond=self.cond))
|
|
156
|
+
else:
|
|
157
|
+
qs = tuple(self.one(qa, line) for qa in qargs)
|
|
158
|
+
if len(set(qs)) != len(qs):
|
|
159
|
+
raise SQCompileError(f"line {line}: gate {name} applied twice to the same qubit")
|
|
160
|
+
self.c.ops.append(Op(name, qs, ps, cond=self.cond))
|
|
161
|
+
elif kind == "reset":
|
|
162
|
+
for q in self.qubits(s[1], s[2]):
|
|
163
|
+
self.c.ops.append(Op("reset", (q,), cond=self.cond))
|
|
164
|
+
elif kind in ("let", "assign"):
|
|
165
|
+
e = s[2]
|
|
166
|
+
if e[0] == "measure":
|
|
167
|
+
q = self.one(e[1], e[2])
|
|
168
|
+
self.c.ops.append(Op("measure", (q,), cbit=self.c.n_cbits, cond=self.cond))
|
|
169
|
+
self.env[s[1]] = MeasRef(self.c.n_cbits)
|
|
170
|
+
self.c.n_cbits += 1
|
|
171
|
+
else:
|
|
172
|
+
self.env[s[1]] = eval_const(e, self.env)
|
|
173
|
+
elif kind == "for":
|
|
174
|
+
lo, hi = eval_const(s[2], self.env), eval_const(s[3], self.env)
|
|
175
|
+
for i in range(lo, hi):
|
|
176
|
+
self.env[s[1]] = i
|
|
177
|
+
self.body(s[4])
|
|
178
|
+
elif kind == "if":
|
|
179
|
+
c = eval_const(s[1], self.env)
|
|
180
|
+
if isinstance(c, MeasRef):
|
|
181
|
+
saved = self.cond
|
|
182
|
+
self.cond = saved + ((c.cbit, c.value),)
|
|
183
|
+
self.body(s[2])
|
|
184
|
+
self.cond = saved + ((c.cbit, 1 - c.value),)
|
|
185
|
+
self.body(s[3])
|
|
186
|
+
self.cond = saved
|
|
187
|
+
else:
|
|
188
|
+
self.body(s[2] if c else s[3])
|
|
189
|
+
elif kind == "expr" and s[1][0] == "measure":
|
|
190
|
+
q = self.one(s[1][1], s[1][2])
|
|
191
|
+
self.c.ops.append(Op("measure", (q,), cbit=self.c.n_cbits, cond=self.cond))
|
|
192
|
+
self.c.n_cbits += 1
|
|
193
|
+
else:
|
|
194
|
+
raise SQCompileError(f"line {s[-1]}: statement '{kind}' is not allowed in a quantum kernel")
|
|
195
|
+
|
|
196
|
+
def compile(self) -> Circuit:
|
|
197
|
+
self.body(self.k.body)
|
|
198
|
+
if self.c.n_qubits == 0:
|
|
199
|
+
raise SQCompileError(f"kernel {self.k.name} declares no qubits")
|
|
200
|
+
return self.c
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ------------------------------------------------------------------ optimiser
|
|
204
|
+
_SELF_INVERSE = {"h", "x", "y", "z", "cx", "cz", "swap", "ccx"}
|
|
205
|
+
_INVERSE_PAIR = {("s", "sdg"), ("sdg", "s"), ("t", "tdg"), ("tdg", "t")}
|
|
206
|
+
_ROT = {"rx", "ry", "rz", "p", "cp", "crz"}
|
|
207
|
+
_SYMMETRIC = {"cz", "swap", "cp"}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _key(o: Op):
|
|
211
|
+
return tuple(sorted(o.qubits)) if o.name in _SYMMETRIC else o.qubits
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def optimize(c: Circuit) -> tuple[Circuit, dict]:
|
|
215
|
+
"""Peephole passes to a fixed point: cancel adjacent inverse pairs, merge same-axis
|
|
216
|
+
rotations, drop identity rotations. 'Adjacent' = no other op touches those qubits in
|
|
217
|
+
between. Measurements, resets and conditional ops are barriers."""
|
|
218
|
+
ops = list(c.ops)
|
|
219
|
+
before = len(ops)
|
|
220
|
+
passes = 0
|
|
221
|
+
changed = True
|
|
222
|
+
while changed:
|
|
223
|
+
changed = False
|
|
224
|
+
passes += 1
|
|
225
|
+
out: list[Op | None] = []
|
|
226
|
+
last: dict[int, int] = {} # qubit -> index in `out` of the last op touching it
|
|
227
|
+
for o in ops:
|
|
228
|
+
barrier = o.name in ("measure", "reset") or bool(o.cond)
|
|
229
|
+
prev_i = None
|
|
230
|
+
if not barrier:
|
|
231
|
+
idxs = {last.get(q) for q in o.qubits}
|
|
232
|
+
if len(idxs) == 1 and None not in idxs:
|
|
233
|
+
prev_i = idxs.pop()
|
|
234
|
+
prev = out[prev_i] if prev_i is not None else None
|
|
235
|
+
if prev is not None and not prev.cond and prev.name not in ("measure", "reset") \
|
|
236
|
+
and _key(prev) == _key(o) and len(prev.qubits) == len(o.qubits):
|
|
237
|
+
if (prev.name == o.name and o.name in _SELF_INVERSE) or (prev.name, o.name) in _INVERSE_PAIR:
|
|
238
|
+
out[prev_i] = None
|
|
239
|
+
for q in o.qubits:
|
|
240
|
+
last.pop(q, None)
|
|
241
|
+
# restore `last` for these qubits from earlier ops
|
|
242
|
+
for j in range(prev_i - 1, -1, -1):
|
|
243
|
+
if out[j] is not None:
|
|
244
|
+
for q in out[j].qubits:
|
|
245
|
+
if q in o.qubits and q not in last:
|
|
246
|
+
last[q] = j
|
|
247
|
+
changed = True
|
|
248
|
+
continue
|
|
249
|
+
if prev.name == o.name and o.name in _ROT:
|
|
250
|
+
out[prev_i] = Op(o.name, prev.qubits, (prev.params[0] + o.params[0],))
|
|
251
|
+
changed = True
|
|
252
|
+
continue
|
|
253
|
+
out.append(o)
|
|
254
|
+
for q in o.qubits:
|
|
255
|
+
last[q] = len(out) - 1
|
|
256
|
+
ops = []
|
|
257
|
+
for o in out:
|
|
258
|
+
if o is None:
|
|
259
|
+
continue
|
|
260
|
+
if o.name in _ROT and not o.cond:
|
|
261
|
+
period = 2 * math.pi if o.name in ("p", "cp") else 4 * math.pi
|
|
262
|
+
a = math.fmod(o.params[0], period)
|
|
263
|
+
if abs(a) < 1e-12 or abs(abs(a) - period) < 1e-12:
|
|
264
|
+
changed = True
|
|
265
|
+
continue
|
|
266
|
+
ops.append(o)
|
|
267
|
+
oc = Circuit(c.name, c.n_qubits, c.n_cbits, ops)
|
|
268
|
+
return oc, {"ops_before": before, "ops_after": len(ops), "passes": passes}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def compile_kernel(kernel: Kernel, args: list, opt: bool = True, kernels: dict | None = None) -> tuple[Circuit, dict]:
|
|
272
|
+
"""`kernels` is the program's kernel table, needed when the kernel calls sub-kernels."""
|
|
273
|
+
c = KernelCompiler(kernel, args, kernels).compile()
|
|
274
|
+
if opt:
|
|
275
|
+
return optimize(c)
|
|
276
|
+
return c, {"ops_before": len(c.ops), "ops_after": len(c.ops), "passes": 0}
|
spatial/q/ir.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""SQIR — SPATIAL Q Intermediate Representation (quantum part).
|
|
2
|
+
|
|
3
|
+
A Circuit is a flat list of Ops on numbered qubits and classical bits, plus a
|
|
4
|
+
ResourceEstimate that the orchestrator uses to pick a backend. Serialisable to
|
|
5
|
+
JSON (`to_json`) and to OpenQASM 3 (`to_qasm3`), the interoperability target.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Op:
|
|
15
|
+
name: str # gate name | "measure" | "reset"
|
|
16
|
+
qubits: tuple[int, ...]
|
|
17
|
+
params: tuple[float, ...] = ()
|
|
18
|
+
cbit: int | None = None # target classical bit for "measure"
|
|
19
|
+
cond: tuple[tuple[int, int], ...] = () # all (cbit, value) pairs must hold
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ResourceEstimate:
|
|
24
|
+
qubits: int
|
|
25
|
+
ops: int
|
|
26
|
+
two_qubit_ops: int
|
|
27
|
+
depth: int
|
|
28
|
+
mid_circuit_measurements: int
|
|
29
|
+
statevector_bytes: int # 16 * 2^n: RAM needed by an exact state-vector simulation
|
|
30
|
+
dynamic: bool # needs feed-forward (conditional ops)
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict:
|
|
33
|
+
return self.__dict__.copy()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Circuit:
|
|
38
|
+
name: str
|
|
39
|
+
n_qubits: int = 0
|
|
40
|
+
n_cbits: int = 0
|
|
41
|
+
ops: list[Op] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
def estimate(self) -> ResourceEstimate:
|
|
44
|
+
level = [0] * max(self.n_qubits, 1)
|
|
45
|
+
two = mid = 0
|
|
46
|
+
dyn = False
|
|
47
|
+
for o in self.ops:
|
|
48
|
+
d = max(level[q] for q in o.qubits) + 1
|
|
49
|
+
for q in o.qubits:
|
|
50
|
+
level[q] = d
|
|
51
|
+
if len(o.qubits) >= 2:
|
|
52
|
+
two += 1
|
|
53
|
+
if o.name == "measure":
|
|
54
|
+
mid += 1
|
|
55
|
+
if o.cond:
|
|
56
|
+
dyn = True
|
|
57
|
+
return ResourceEstimate(self.n_qubits, len(self.ops), two, max(level) if self.ops else 0,
|
|
58
|
+
mid, 16 * (1 << self.n_qubits), dyn)
|
|
59
|
+
|
|
60
|
+
def to_json(self) -> str:
|
|
61
|
+
return json.dumps({
|
|
62
|
+
"sqir": "0.1", "name": self.name, "qubits": self.n_qubits, "cbits": self.n_cbits,
|
|
63
|
+
"ops": [{"g": o.name, "q": list(o.qubits), **({"p": list(o.params)} if o.params else {}),
|
|
64
|
+
**({"c": o.cbit} if o.cbit is not None else {}),
|
|
65
|
+
**({"if": [list(c) for c in o.cond]} if o.cond else {})} for o in self.ops],
|
|
66
|
+
"resources": self.estimate().to_dict()}, indent=1)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def from_json(text: str) -> "Circuit":
|
|
70
|
+
d = json.loads(text)
|
|
71
|
+
c = Circuit(d["name"], d["qubits"], d["cbits"])
|
|
72
|
+
for o in d["ops"]:
|
|
73
|
+
c.ops.append(Op(o["g"], tuple(o["q"]), tuple(o.get("p", ())), o.get("c"),
|
|
74
|
+
tuple(tuple(x) for x in o.get("if", ()))))
|
|
75
|
+
return c
|
|
76
|
+
|
|
77
|
+
def to_qasm2(self) -> str:
|
|
78
|
+
"""OpenQASM 2.0 text for STATIC circuits (the dialect Cirq, PennyLane and older tools import).
|
|
79
|
+
Gates missing from qelib1.inc are rewritten: p -> u1, cp -> cu1."""
|
|
80
|
+
if any(o.name in ("measure", "reset") or o.cond for o in self.ops):
|
|
81
|
+
raise ValueError("OpenQASM 2 export supports static circuits only; use to_qasm3()")
|
|
82
|
+
ren = {"p": "u1", "cp": "cu1"}
|
|
83
|
+
L = ["OPENQASM 2.0;", 'include "qelib1.inc";', f"qreg q[{self.n_qubits}];", f"creg c[{self.n_qubits}];"]
|
|
84
|
+
for o in self.ops:
|
|
85
|
+
p = "(" + ",".join(repr(float(x)) for x in o.params) + ")" if o.params else ""
|
|
86
|
+
L.append(f"{ren.get(o.name, o.name)}{p} " + ",".join(f"q[{q}]" for q in o.qubits) + ";")
|
|
87
|
+
L.append("measure q -> c;")
|
|
88
|
+
return "\n".join(L) + "\n"
|
|
89
|
+
|
|
90
|
+
def shot_cost(self, shots: int, usd_per_shot: float = 0.0, usd_per_task: float = 0.0,
|
|
91
|
+
usd_per_second: float = 0.0, seconds_per_shot: float = 0.0) -> dict:
|
|
92
|
+
"""Cost of running this circuit on a provider, from PRICES THE CALLER SUPPLIES.
|
|
93
|
+
No provider price is built in: tariffs differ and change; an invented default would be a lie."""
|
|
94
|
+
if not (usd_per_shot or usd_per_task or usd_per_second):
|
|
95
|
+
raise ValueError("no price given: pass the provider's usd_per_shot, usd_per_task or usd_per_second")
|
|
96
|
+
usd = usd_per_task + shots * usd_per_shot + shots * seconds_per_shot * usd_per_second
|
|
97
|
+
return {"shots": shots, "usd": usd, "usd_per_shot_effective": usd / shots,
|
|
98
|
+
"inputs": {"usd_per_shot": usd_per_shot, "usd_per_task": usd_per_task,
|
|
99
|
+
"usd_per_second": usd_per_second, "seconds_per_shot": seconds_per_shot}}
|
|
100
|
+
|
|
101
|
+
def to_qasm3(self) -> str:
|
|
102
|
+
"""OpenQASM 3 text. A final `c = measure q;` reads out every qubit."""
|
|
103
|
+
L = ["OPENQASM 3.0;", 'include "stdgates.inc";', f"qubit[{self.n_qubits}] q;"]
|
|
104
|
+
if self.n_cbits:
|
|
105
|
+
L.append(f"bit[{self.n_cbits}] m;")
|
|
106
|
+
L.append(f"bit[{self.n_qubits}] c;")
|
|
107
|
+
for o in self.ops:
|
|
108
|
+
qs = ", ".join(f"q[{q}]" for q in o.qubits)
|
|
109
|
+
if o.name == "measure":
|
|
110
|
+
s = f"m[{o.cbit}] = measure {qs};"
|
|
111
|
+
elif o.name == "reset":
|
|
112
|
+
s = f"reset {qs};"
|
|
113
|
+
else:
|
|
114
|
+
p = "(" + ", ".join(repr(float(x)) for x in o.params) + ")" if o.params else ""
|
|
115
|
+
s = f"{o.name}{p} {qs};"
|
|
116
|
+
if o.cond:
|
|
117
|
+
cond = " && ".join(f"m[{b}] == {v}" for b, v in o.cond)
|
|
118
|
+
s = f"if ({cond}) {{ {s} }}"
|
|
119
|
+
L.append(s)
|
|
120
|
+
L.append("c = measure q;")
|
|
121
|
+
return "\n".join(L) + "\n"
|
spatial/q/lexer.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""SPATIAL Q lexer."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
KEYWORDS = {
|
|
8
|
+
"quantum", "task", "main", "on", "qubit", "let", "for", "in", "while", "if", "else",
|
|
9
|
+
"return", "print", "parallel", "measure", "reset", "sample", "shots", "expect", "of",
|
|
10
|
+
"workload", "true", "false", "and", "or", "not",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_TOKEN = re.compile(r"""
|
|
14
|
+
(?P<ws>[ \t\r]+|//[^\n]*)
|
|
15
|
+
| (?P<nl>\n)
|
|
16
|
+
| (?P<num>\d+\.(?!\.)\d*(?:[eE][-+]?\d+)?|\d+[eE][-+]?\d+|\.\d+|\d+)
|
|
17
|
+
| (?P<str>"[^"\n]*")
|
|
18
|
+
| (?P<id>[A-Za-z_][A-Za-z0-9_]*)
|
|
19
|
+
| (?P<op>->|\.\.|==|!=|<=|>=|[-+*/%<>=(){}\[\],;:!])
|
|
20
|
+
""", re.X)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Tok:
|
|
25
|
+
kind: str # num | str | id | kw | op | eof
|
|
26
|
+
val: object
|
|
27
|
+
line: int
|
|
28
|
+
col: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SQSyntaxError(SyntaxError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def lex(src: str) -> list[Tok]:
|
|
36
|
+
out, pos, line, line_start = [], 0, 1, 0
|
|
37
|
+
while pos < len(src):
|
|
38
|
+
m = _TOKEN.match(src, pos)
|
|
39
|
+
if not m:
|
|
40
|
+
raise SQSyntaxError(f"line {line}:{pos - line_start + 1}: unexpected character {src[pos]!r}")
|
|
41
|
+
kind = m.lastgroup
|
|
42
|
+
text = m.group()
|
|
43
|
+
col = pos - line_start + 1
|
|
44
|
+
pos = m.end()
|
|
45
|
+
if kind == "ws":
|
|
46
|
+
continue
|
|
47
|
+
if kind == "nl":
|
|
48
|
+
line += 1
|
|
49
|
+
line_start = pos
|
|
50
|
+
continue
|
|
51
|
+
if kind == "num":
|
|
52
|
+
is_float = any(c in text for c in ".eE")
|
|
53
|
+
out.append(Tok("num", float(text) if is_float else int(text), line, col))
|
|
54
|
+
elif kind == "str":
|
|
55
|
+
out.append(Tok("str", text[1:-1], line, col))
|
|
56
|
+
elif kind == "id":
|
|
57
|
+
out.append(Tok("kw" if text in KEYWORDS else "id", text, line, col))
|
|
58
|
+
else:
|
|
59
|
+
out.append(Tok("op", text, line, col))
|
|
60
|
+
out.append(Tok("eof", None, line, 0))
|
|
61
|
+
return out
|