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.
Files changed (58) hide show
  1. spatial/__init__.py +6 -0
  2. spatial/ai/__init__.py +18 -0
  3. spatial/ai/kvcache.py +81 -0
  4. spatial/cli.py +135 -0
  5. spatial/compute/__init__.py +3 -0
  6. spatial/compute/cost.py +69 -0
  7. spatial/compute/detect.py +139 -0
  8. spatial/compute/monitor.py +111 -0
  9. spatial/compute/scheduler.py +168 -0
  10. spatial/compute/taskqueue.py +107 -0
  11. spatial/compute/worker.py +89 -0
  12. spatial/compute/workloads.py +148 -0
  13. spatial/core/__init__.py +1 -0
  14. spatial/core/energy.py +101 -0
  15. spatial/core/ledger.py +76 -0
  16. spatial/core/model.py +82 -0
  17. spatial/core/protocol.py +167 -0
  18. spatial/core/registry.py +59 -0
  19. spatial/holographic/__init__.py +8 -0
  20. spatial/holographic/cgh.py +40 -0
  21. spatial/holographic/model.py +49 -0
  22. spatial/intelligence/__init__.py +6 -0
  23. spatial/intelligence/predictor.py +51 -0
  24. spatial/memory/__init__.py +5 -0
  25. spatial/memory/fararray.py +44 -0
  26. spatial/memory/pageserver.py +141 -0
  27. spatial/memory/secure.py +100 -0
  28. spatial/memory/shared.py +43 -0
  29. spatial/memory/tiered.py +355 -0
  30. spatial/q/__init__.py +4 -0
  31. spatial/q/__main__.py +9 -0
  32. spatial/q/backends/__init__.py +122 -0
  33. spatial/q/codegen.py +153 -0
  34. spatial/q/compiler.py +276 -0
  35. spatial/q/ir.py +121 -0
  36. spatial/q/lexer.py +61 -0
  37. spatial/q/parser.py +330 -0
  38. spatial/q/qir.py +128 -0
  39. spatial/q/runtime.py +274 -0
  40. spatial/q/sim.py +218 -0
  41. spatial/runtime/__init__.py +3 -0
  42. spatial/runtime/envs.py +74 -0
  43. spatial/runtime/inspect.py +48 -0
  44. spatial/runtime/microvm.py +125 -0
  45. spatial/runtime/planner.py +155 -0
  46. spatial/runtime/runner.py +108 -0
  47. spatial/runtime/toolchain.py +35 -0
  48. spatial/scientific/__init__.py +8 -0
  49. spatial/scientific/jobs.py +80 -0
  50. spatial/simulation/__init__.py +7 -0
  51. spatial/simulation/heat.py +48 -0
  52. spatial/simulation/nbody.py +31 -0
  53. spatial_computing-0.5.0.dist-info/METADATA +112 -0
  54. spatial_computing-0.5.0.dist-info/RECORD +58 -0
  55. spatial_computing-0.5.0.dist-info/WHEEL +5 -0
  56. spatial_computing-0.5.0.dist-info/entry_points.txt +3 -0
  57. spatial_computing-0.5.0.dist-info/licenses/LICENSE +202 -0
  58. 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