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/runtime.py ADDED
@@ -0,0 +1,274 @@
1
+ """SPATIAL Q runtime + orchestrator.
2
+
3
+ Executes `main` (classical control), dispatches:
4
+ * classical tasks -> local CPU or a compute worker (SWP/0), per `on` clause / load;
5
+ * quantum kernels -> compiled to SQIR, optimised, sent to a backend chosen from the
6
+ circuit's ResourceEstimate (or the explicit `on <backend>`).
7
+ Every dispatch is appended to `trace` (what ran where, why, how long).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from concurrent.futures import ThreadPoolExecutor
13
+
14
+ import psutil
15
+
16
+ from ..compute import workloads
17
+ from . import backends
18
+ from .compiler import CONSTS, FUNCS, SQCompileError, compile_kernel, div
19
+ from .parser import Program, parse
20
+
21
+
22
+ class SQRuntimeError(Exception):
23
+ pass
24
+
25
+
26
+ class _Return(Exception):
27
+ def __init__(self, v):
28
+ self.v = v
29
+
30
+
31
+ class Runtime:
32
+ def __init__(self, program: Program, worker_client=None, seed: int | None = None,
33
+ ram_fraction: float = 0.5, out=print, default_backend: str = "statevector",
34
+ noise: float = 0.0):
35
+ self.noise = noise
36
+ self.p = program
37
+ self.worker = worker_client
38
+ self.seed = seed
39
+ self.ram_fraction = ram_fraction
40
+ self.out = out
41
+ self.default_backend = default_backend
42
+ self.trace: list[dict] = []
43
+ self._cache: dict = {}
44
+ self._backends: dict[str, backends.Backend] = {}
45
+
46
+ # ---- quantum dispatch
47
+ def _circuit(self, name: str, args: list):
48
+ if name not in self.p.kernels:
49
+ raise SQRuntimeError(f"unknown quantum kernel '{name}'")
50
+ key = (name, repr(args))
51
+ if key not in self._cache:
52
+ t0 = time.perf_counter()
53
+ c, info = compile_kernel(self.p.kernels[name], args, kernels=self.p.kernels)
54
+ info["compile_s"] = time.perf_counter() - t0
55
+ self._cache[key] = (c, info)
56
+ return self._cache[key]
57
+
58
+ def _backend(self, requested: str, est) -> backends.Backend:
59
+ name = requested
60
+ reason = "explicit"
61
+ if requested == "auto":
62
+ avail = psutil.virtual_memory().available
63
+ if est.statevector_bytes > self.ram_fraction * avail:
64
+ raise SQRuntimeError(
65
+ f"{est.qubits} qubits need {est.statevector_bytes / 2**30:.1f} GiB for an exact "
66
+ f"state vector; only {avail / 2**30:.1f} GiB available. Not simulable here: needs "
67
+ "a QPU, a larger machine or an approximate simulator.")
68
+ name = self.default_backend
69
+ reason = f"auto: state vector {est.statevector_bytes} B fits in RAM"
70
+ if name not in self._backends:
71
+ self._backends[name] = backends.get(name)
72
+ self._last_reason = reason
73
+ return self._backends[name]
74
+
75
+ def _sample(self, name, args, shots, backend):
76
+ c, info = self._circuit(name, args)
77
+ est = c.estimate()
78
+ b = self._backend(backend, est)
79
+ t0 = time.perf_counter()
80
+ if self.noise and b.name != "statevector":
81
+ raise SQRuntimeError(f"the noise option is only implemented for the built-in statevector backend, not '{b.name}'")
82
+ counts = b.sample(c, shots, self.seed, noise=self.noise) if self.noise else b.sample(c, shots, self.seed)
83
+ self.trace.append({"op": "sample", "kernel": name, "backend": b.name, "why": self._last_reason,
84
+ "shots": shots, "wall_s": time.perf_counter() - t0, "resources": est.to_dict(),
85
+ "optimizer": info, **({"noise": self.noise} if self.noise else {})})
86
+ return counts
87
+
88
+ def _expect(self, pauli, name, args, backend):
89
+ c, info = self._circuit(name, args)
90
+ est = c.estimate()
91
+ b = self._backend(backend, est)
92
+ t0 = time.perf_counter()
93
+ v = b.expectation(c, pauli)
94
+ self.trace.append({"op": "expect", "kernel": name, "backend": b.name, "pauli": pauli,
95
+ "wall_s": time.perf_counter() - t0, "qubits": est.qubits})
96
+ return v
97
+
98
+ # ---- classical dispatch
99
+ def _workload(self, wname: str, kwargs: dict, target: str):
100
+ where, why = "cpu", "task target"
101
+ if target == "worker" or (target == "any" and self.worker is not None
102
+ and psutil.cpu_percent(interval=0.05) > 75):
103
+ if self.worker is None:
104
+ raise SQRuntimeError("task targets a worker but none is attached to the runtime")
105
+ where = "worker"
106
+ why = "task target" if target == "worker" else "local CPU busy > 75%"
107
+ elif target == "any":
108
+ why = "local CPU has headroom" if self.worker is not None else "no worker attached"
109
+ t0 = time.perf_counter()
110
+ if where == "worker":
111
+ rh, _ = self.worker.call({"op": "run", "workload": wname, "params": kwargs})
112
+ r = rh["result"]
113
+ else:
114
+ r = workloads.run(wname, **kwargs)
115
+ self.trace.append({"op": "workload", "workload": wname, "where": where, "why": why,
116
+ "wall_s": time.perf_counter() - t0})
117
+ return r
118
+
119
+ def _call_task(self, name: str, args: list):
120
+ t = self.p.tasks[name]
121
+ if len(args) != len(t.params):
122
+ raise SQRuntimeError(f"task {name} expects {len(t.params)} argument(s)")
123
+ env = dict(zip(t.params, args))
124
+ try:
125
+ self._block(t.body, env, t.target)
126
+ except _Return as r:
127
+ return r.v
128
+ return None
129
+
130
+ # ---- evaluation
131
+ def ev(self, e, env: dict, target: str = "cpu"):
132
+ k = e[0]
133
+ if k in ("num", "str"):
134
+ return e[1]
135
+ if k == "var":
136
+ if e[1] in env:
137
+ return env[e[1]]
138
+ if e[1] in CONSTS:
139
+ return CONSTS[e[1]]
140
+ raise SQRuntimeError(f"line {e[2]}: unknown name '{e[1]}'")
141
+ if k == "neg":
142
+ return -self.ev(e[1], env, target)
143
+ if k == "not":
144
+ return not self.ev(e[1], env, target)
145
+ if k == "list":
146
+ return [self.ev(x, env, target) for x in e[1]]
147
+ if k == "index":
148
+ return self.ev(e[1], env, target)[self.ev(e[2], env, target)]
149
+ if k == "bin":
150
+ op = e[1]
151
+ a = self.ev(e[2], env, target)
152
+ if op == "and":
153
+ return a and self.ev(e[3], env, target)
154
+ if op == "or":
155
+ return a or self.ev(e[3], env, target)
156
+ b = self.ev(e[3], env, target)
157
+ return {"+": lambda: a + b, "-": lambda: a - b, "*": lambda: a * b, "/": lambda: div(a, b),
158
+ "%": lambda: a % b, "==": lambda: a == b, "!=": lambda: a != b, "<": lambda: a < b,
159
+ "<=": lambda: a <= b, ">": lambda: a > b, ">=": lambda: a >= b}[op]()
160
+ if k == "call":
161
+ args = [self.ev(a, env, target) for a in e[2]]
162
+ if e[1] in self.p.tasks:
163
+ return self._call_task(e[1], args)
164
+ if e[1] in FUNCS:
165
+ return FUNCS[e[1]](*args)
166
+ if e[1] in self.p.kernels:
167
+ raise SQRuntimeError(f"line {e[3]}: quantum kernel '{e[1]}' must be used with 'sample' or 'expect'")
168
+ raise SQRuntimeError(f"line {e[3]}: unknown function '{e[1]}'")
169
+ if k == "sample":
170
+ return self._sample(e[1], [self.ev(a, env, target) for a in e[2]],
171
+ int(self.ev(e[3], env, target)), e[4])
172
+ if k == "expect":
173
+ return self._expect(e[1], e[2], [self.ev(a, env, target) for a in e[3]], e[4])
174
+ if k == "workload":
175
+ return self._workload(e[1], {kk: self.ev(v, env, target) for kk, v in e[2].items()}, target)
176
+ if k == "measure":
177
+ raise SQRuntimeError(f"line {e[2]}: 'measure' is only valid inside a quantum kernel")
178
+ raise SQRuntimeError(f"cannot evaluate {k}")
179
+
180
+ def _block(self, stmts, env: dict, target: str = "cpu"):
181
+ for s in stmts:
182
+ k = s[0]
183
+ if k == "let" or k == "assign":
184
+ if k == "assign" and s[1] not in env:
185
+ raise SQRuntimeError(f"line {s[3]}: assignment to undeclared variable '{s[1]}'")
186
+ env[s[1]] = self.ev(s[2], env, target)
187
+ elif k == "print":
188
+ self.out(*[self.ev(x, env, target) for x in s[1]])
189
+ elif k == "for":
190
+ for i in range(self.ev(s[2], env, target), self.ev(s[3], env, target)):
191
+ env[s[1]] = i
192
+ self._block(s[4], env, target)
193
+ elif k == "while":
194
+ while self.ev(s[1], env, target):
195
+ self._block(s[2], env, target)
196
+ elif k == "if":
197
+ self._block(s[2] if self.ev(s[1], env, target) else s[3], env, target)
198
+ elif k == "return":
199
+ raise _Return(self.ev(s[1], env, target))
200
+ elif k == "expr":
201
+ self.ev(s[1], env, target)
202
+ elif k == "parallel":
203
+ lets = [x for x in s[1] if x[0] in ("let", "assign")]
204
+ if len(lets) != len(s[1]):
205
+ raise SQRuntimeError(f"line {s[2]}: a parallel block may only contain let/assignment statements")
206
+ t0 = time.perf_counter()
207
+ with ThreadPoolExecutor(len(lets)) as ex:
208
+ futs = [ex.submit(self.ev, x[2], dict(env), target) for x in lets]
209
+ for x, f in zip(lets, futs):
210
+ env[x[1]] = f.result()
211
+ self.trace.append({"op": "parallel", "branches": len(lets), "wall_s": time.perf_counter() - t0})
212
+ else:
213
+ raise SQRuntimeError(f"line {s[-1]}: statement '{k}' not allowed here")
214
+
215
+ def run(self, compiled: bool = False) -> dict:
216
+ """compiled=True runs the classical part through generated Python (see codegen.py)
217
+ instead of the tree-walking interpreter; results are identical."""
218
+ if compiled:
219
+ from .codegen import run_compiled
220
+ return run_compiled(self)
221
+ env: dict = {}
222
+ try:
223
+ self._block(self.p.main, env)
224
+ except _Return:
225
+ pass
226
+ return env
227
+
228
+
229
+ def run_source(src: str, compiled: bool = False, **kw) -> tuple[dict, Runtime]:
230
+ rt = Runtime(parse(src), **kw)
231
+ return rt.run(compiled=compiled), rt
232
+
233
+
234
+ def main():
235
+ import argparse
236
+ import json
237
+ ap = argparse.ArgumentParser(prog="spatialq", description="SPATIAL Q compiler/runtime")
238
+ ap.add_argument("cmd", choices=["run", "check", "qasm", "qasm2", "ir", "qir"])
239
+ ap.add_argument("file")
240
+ ap.add_argument("--kernel", help="kernel name for qasm/ir")
241
+ ap.add_argument("--args", default="[]", help="JSON list of kernel arguments")
242
+ ap.add_argument("--seed", type=int)
243
+ ap.add_argument("--trace", action="store_true")
244
+ ap.add_argument("--compiled", action="store_true", help="compile the classical part to Python bytecode")
245
+ ap.add_argument("--noise", type=float, default=0.0, help="depolarising error probability per gate and qubit")
246
+ a = ap.parse_args()
247
+ src = open(a.file).read()
248
+ prog = parse(src)
249
+ if a.cmd == "check":
250
+ print(f"ok: {len(prog.kernels)} kernel(s), {len(prog.tasks)} task(s)")
251
+ return
252
+ if a.cmd in ("qasm", "qasm2", "ir", "qir"):
253
+ name = a.kernel or next(k for k, v in prog.kernels.items() if "qubit" not in v.ptypes)
254
+ c, info = compile_kernel(prog.kernels[name], json.loads(a.args), kernels=prog.kernels)
255
+ if a.cmd == "qir":
256
+ from .qir import to_qir
257
+ print(to_qir(c))
258
+ elif a.cmd == "qasm2":
259
+ print(c.to_qasm2())
260
+ else:
261
+ print(c.to_qasm3() if a.cmd == "qasm" else c.to_json())
262
+ return
263
+ rt = Runtime(prog, seed=a.seed, noise=a.noise)
264
+ rt.run(compiled=a.compiled)
265
+ if a.trace:
266
+ for t in rt.trace:
267
+ print("TRACE", json.dumps(t, default=str))
268
+
269
+
270
+ if __name__ == "__main__":
271
+ try:
272
+ main()
273
+ except (SyntaxError, SQCompileError, SQRuntimeError, backends.BackendUnavailable) as e:
274
+ raise SystemExit(f"spatialq: {e}")
spatial/q/sim.py ADDED
@@ -0,0 +1,218 @@
1
+ """Built-in exact state-vector simulator (NumPy).
2
+
3
+ Conventions (same as Qiskit, to make cross-checks direct):
4
+ * qubit 0 is the least-significant bit of the basis-state index;
5
+ * printed bitstrings show qubit n-1 on the left, qubit 0 on the right;
6
+ * multi-qubit gate matrices list their first qubit argument as the most significant.
7
+ Memory: 16 bytes x 2^n. This is a classical simulation: it shows correctness, not speed-up.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import math
12
+
13
+ import numpy as np
14
+
15
+ from .ir import Circuit
16
+
17
+ _S2 = 1 / math.sqrt(2)
18
+ I2 = np.eye(2, dtype=complex)
19
+ FIXED = {
20
+ "h": np.array([[_S2, _S2], [_S2, -_S2]], dtype=complex),
21
+ "x": np.array([[0, 1], [1, 0]], dtype=complex),
22
+ "y": np.array([[0, -1j], [1j, 0]], dtype=complex),
23
+ "z": np.array([[1, 0], [0, -1]], dtype=complex),
24
+ "s": np.array([[1, 0], [0, 1j]], dtype=complex),
25
+ "sdg": np.array([[1, 0], [0, -1j]], dtype=complex),
26
+ "t": np.array([[1, 0], [0, np.exp(1j * math.pi / 4)]], dtype=complex),
27
+ "tdg": np.array([[1, 0], [0, np.exp(-1j * math.pi / 4)]], dtype=complex),
28
+ "sx": 0.5 * np.array([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]], dtype=complex),
29
+ "cx": np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], dtype=complex),
30
+ "cz": np.diag([1, 1, 1, -1]).astype(complex),
31
+ "swap": np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dtype=complex),
32
+ }
33
+ _ccx = np.eye(8, dtype=complex)
34
+ _ccx[6:, 6:] = [[0, 1], [1, 0]]
35
+ FIXED["ccx"] = _ccx
36
+
37
+
38
+ def matrix(name: str, params: tuple) -> np.ndarray:
39
+ if name in FIXED:
40
+ return FIXED[name]
41
+ t = params[0]
42
+ c, s = math.cos(t / 2), math.sin(t / 2)
43
+ if name == "rx":
44
+ return np.array([[c, -1j * s], [-1j * s, c]], dtype=complex)
45
+ if name == "ry":
46
+ return np.array([[c, -s], [s, c]], dtype=complex)
47
+ if name == "rz":
48
+ return np.array([[np.exp(-1j * t / 2), 0], [0, np.exp(1j * t / 2)]], dtype=complex)
49
+ if name == "p":
50
+ return np.array([[1, 0], [0, np.exp(1j * t)]], dtype=complex)
51
+ if name == "cp":
52
+ return np.diag([1, 1, 1, np.exp(1j * t)]).astype(complex)
53
+ if name == "crz":
54
+ return np.diag([1, 1, np.exp(-1j * t / 2), np.exp(1j * t / 2)]).astype(complex)
55
+ raise KeyError(f"unknown gate {name}")
56
+
57
+
58
+ class State:
59
+ def __init__(self, n: int):
60
+ self.n = n
61
+ self.psi = np.zeros((2,) * n, dtype=complex)
62
+ self.psi[(0,) * n] = 1.0
63
+
64
+ def apply(self, U: np.ndarray, qubits: tuple[int, ...]):
65
+ k = len(qubits)
66
+ axes = [self.n - 1 - q for q in qubits]
67
+ self.psi = np.moveaxis(
68
+ np.tensordot(U.reshape((2,) * (2 * k)), self.psi, axes=(list(range(k, 2 * k)), axes)),
69
+ list(range(k)), axes)
70
+
71
+ def prob1(self, q: int) -> float:
72
+ ax = self.n - 1 - q
73
+ return float(np.sum(np.abs(np.take(self.psi, 1, axis=ax)) ** 2))
74
+
75
+ def measure(self, q: int, rng) -> int:
76
+ p1 = self.prob1(q)
77
+ r = int(rng.random() < p1)
78
+ ax = self.n - 1 - q
79
+ sl = [slice(None)] * self.n
80
+ sl[ax] = 1 - r
81
+ self.psi[tuple(sl)] = 0
82
+ self.psi /= math.sqrt(p1 if r else 1 - p1)
83
+ return r
84
+
85
+ def reset(self, q: int, rng):
86
+ if self.measure(q, rng):
87
+ self.apply(FIXED["x"], (q,))
88
+
89
+ def probs(self) -> np.ndarray:
90
+ return np.abs(self.psi.reshape(-1)) ** 2
91
+
92
+ def vector(self) -> np.ndarray:
93
+ return self.psi.reshape(-1)
94
+
95
+
96
+ def run_static(c: Circuit) -> State:
97
+ """Circuit without mid-circuit measurement / feed-forward."""
98
+ s = State(c.n_qubits)
99
+ for o in c.ops:
100
+ s.apply(matrix(o.name, o.params), o.qubits)
101
+ return s
102
+
103
+
104
+ def is_static(c: Circuit) -> bool:
105
+ return all(o.name not in ("measure", "reset") and not o.cond for o in c.ops)
106
+
107
+
108
+ _NOISE_PAULIS = ("x", "y", "z")
109
+
110
+
111
+ def run_shot(c: Circuit, rng, noise: float = 0.0) -> State:
112
+ """One trajectory. noise = probability, after each gate and for each qubit it touches,
113
+ of a uniformly random Pauli error (single-qubit depolarising channel, Monte-Carlo)."""
114
+ s = State(c.n_qubits)
115
+ bits = [0] * c.n_cbits
116
+ for o in c.ops:
117
+ if o.cond and any(bits[b] != v for b, v in o.cond):
118
+ continue
119
+ if o.name == "measure":
120
+ bits[o.cbit] = s.measure(o.qubits[0], rng)
121
+ elif o.name == "reset":
122
+ s.reset(o.qubits[0], rng)
123
+ else:
124
+ s.apply(matrix(o.name, o.params), o.qubits)
125
+ if noise:
126
+ for q in o.qubits:
127
+ if rng.random() < noise:
128
+ s.apply(FIXED[_NOISE_PAULIS[rng.integers(3)]], (q,))
129
+ return s
130
+
131
+
132
+ def branch_distribution(c: Circuit, max_branches: int = 4096) -> np.ndarray | None:
133
+ """Exact output distribution of a dynamic circuit by enumerating measurement branches
134
+ (each mid-circuit measurement splits the state in two, weighted by its probability).
135
+ Returns None when the branch count would exceed `max_branches`."""
136
+ n = c.n_qubits
137
+ total = np.zeros(1 << n)
138
+ stack = [(1.0, State(n), [0] * c.n_cbits, 0)]
139
+ leaves = 0
140
+ while stack:
141
+ w, s, bits, pc = stack.pop()
142
+ while pc < len(c.ops):
143
+ o = c.ops[pc]
144
+ pc += 1
145
+ if o.cond and any(bits[b] != v for b, v in o.cond):
146
+ continue
147
+ if o.name in ("measure", "reset"):
148
+ q = o.qubits[0]
149
+ p1 = s.prob1(q)
150
+ ax = n - 1 - q
151
+ for r, pr in ((0, 1 - p1), (1, p1)):
152
+ if pr < 1e-15:
153
+ continue
154
+ s2 = State(n)
155
+ s2.psi = s.psi.copy()
156
+ sl = [slice(None)] * n
157
+ sl[ax] = 1 - r
158
+ s2.psi[tuple(sl)] = 0
159
+ s2.psi /= math.sqrt(pr)
160
+ b2 = list(bits)
161
+ if o.name == "measure":
162
+ b2[o.cbit] = r
163
+ elif r:
164
+ s2.apply(FIXED["x"], (q,))
165
+ stack.append((w * pr, s2, b2, pc))
166
+ if len(stack) + leaves > max_branches:
167
+ return None
168
+ break
169
+ s.apply(matrix(o.name, o.params), o.qubits)
170
+ else:
171
+ total += w * s.probs()
172
+ leaves += 1
173
+ return total
174
+
175
+
176
+ def sample(c: Circuit, shots: int, seed: int | None = None, dynamic: str = "branch",
177
+ noise: float = 0.0) -> dict[str, int]:
178
+ """dynamic="branch": exact branch enumeration then one multinomial draw (fast);
179
+ dynamic="shots" : one full simulation per shot (reference implementation)."""
180
+ rng = np.random.default_rng(seed)
181
+ n = c.n_qubits
182
+ p = None
183
+ if noise:
184
+ if not 0 <= noise <= 1:
185
+ raise ValueError("noise must be a probability")
186
+ dynamic = "shots" # noisy runs are simulated trajectory by trajectory
187
+ elif is_static(c):
188
+ p = run_static(c).probs()
189
+ elif dynamic == "branch":
190
+ p = branch_distribution(c)
191
+ if p is not None:
192
+ p = p / p.sum()
193
+ draws = rng.multinomial(shots, p)
194
+ return {format(i, f"0{n}b"): int(k) for i, k in enumerate(draws) if k}
195
+ counts: dict[str, int] = {}
196
+ for _ in range(shots):
197
+ p = run_shot(c, rng, noise).probs()
198
+ i = int(rng.choice(len(p), p=p / p.sum()))
199
+ key = format(i, f"0{n}b")
200
+ counts[key] = counts.get(key, 0) + 1
201
+ return counts
202
+
203
+
204
+ _PAULI = {"I": I2, "X": FIXED["x"], "Y": FIXED["y"], "Z": FIXED["z"]}
205
+
206
+
207
+ def expectation(c: Circuit, pauli: str) -> float:
208
+ """Exact <psi|P|psi>. `pauli` is written like a bitstring: leftmost char acts on qubit n-1."""
209
+ if not is_static(c):
210
+ raise ValueError("expect needs a kernel without mid-circuit measurement")
211
+ if len(pauli) != c.n_qubits or any(ch not in _PAULI for ch in pauli):
212
+ raise ValueError(f"Pauli string must have {c.n_qubits} characters from IXYZ")
213
+ s = run_static(c)
214
+ psi0 = s.psi.copy()
215
+ for pos, ch in enumerate(pauli):
216
+ if ch != "I":
217
+ s.apply(_PAULI[ch], (c.n_qubits - 1 - pos,))
218
+ return float(np.real(np.vdot(psi0.reshape(-1), s.psi.reshape(-1))))
@@ -0,0 +1,3 @@
1
+ """Spatial Runtime Engine: inspect artifacts, plan an execution route, run it."""
2
+ from .inspect import inspect, Artifact # noqa: F401
3
+ from .planner import plan, execute, host_caps # noqa: F401
@@ -0,0 +1,74 @@
1
+ """Environment detection and isolated Python environments (dependency management).
2
+
3
+ Isolation level: separate interpreter + site-packages (venv). It protects the host's
4
+ packages; it is NOT a security sandbox (same user, same filesystem)."""
5
+ from __future__ import annotations
6
+
7
+ import shutil
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+
12
+ def detect_interpreters() -> list[dict]:
13
+ """Language runtimes actually present on the host."""
14
+ out = []
15
+ probes = {"python3": ["--version"], "python3.14": ["--version"], "node": ["--version"],
16
+ "swift": ["--version"], "clang": ["--version"], "java": ["-version"], "go": ["version"],
17
+ "rustc": ["--version"], "ruby": ["--version"], "perl": ["-e", "print $^V"]}
18
+ for exe, flag in probes.items():
19
+ p = shutil.which(exe) or (f"/opt/local/bin/{exe}" if Path(f"/opt/local/bin/{exe}").exists() else None)
20
+ if not p:
21
+ out.append({"runtime": exe, "present": False})
22
+ continue
23
+ try:
24
+ r = subprocess.run([p, *flag], capture_output=True, text=True, timeout=15)
25
+ ver = (r.stdout or r.stderr).strip().splitlines()[0] if (r.stdout or r.stderr).strip() else ""
26
+ out.append({"runtime": exe, "present": r.returncode == 0, "path": p, "version": ver})
27
+ except Exception as e:
28
+ out.append({"runtime": exe, "present": False, "error": str(e)})
29
+ return out
30
+
31
+
32
+ def check_requirements(python: str, requirements: list[str]) -> dict[str, str | None]:
33
+ """Which requirement names are missing/incompatible in `python` (installed version or None)."""
34
+ code = ("import importlib.metadata as m,json,sys;"
35
+ "print(json.dumps({n:(m.version(n) if any(d.metadata['Name'].lower()==n.lower() "
36
+ "for d in m.distributions()) else None) for n in sys.argv[1:]}))")
37
+ import json
38
+ names = [r.split("==")[0].split(">=")[0].strip() for r in requirements]
39
+ r = subprocess.run([python, "-c", code, *names], capture_output=True, text=True, timeout=60)
40
+ return json.loads(r.stdout)
41
+
42
+
43
+ class IsolatedEnv:
44
+ def __init__(self, path: str, python: str | None = None):
45
+ self.path = Path(path)
46
+ self.python_src = python
47
+
48
+ @property
49
+ def python(self) -> str:
50
+ return str(self.path / "bin" / "python")
51
+
52
+ def create(self, requirements: list[str] | None = None) -> float:
53
+ import time
54
+ t0 = time.perf_counter()
55
+ uv = shutil.which("uv")
56
+ if uv:
57
+ cmd = [uv, "venv", str(self.path)] + (["--python", self.python_src] if self.python_src else [])
58
+ subprocess.run(cmd, check=True, capture_output=True, timeout=300)
59
+ if requirements:
60
+ subprocess.run([uv, "pip", "install", "--python", self.python, *requirements],
61
+ check=True, capture_output=True, timeout=600)
62
+ else:
63
+ import sys
64
+ subprocess.run([self.python_src or sys.executable, "-m", "venv", str(self.path)], check=True, timeout=300)
65
+ if requirements:
66
+ subprocess.run([self.python, "-m", "pip", "install", *requirements], check=True,
67
+ capture_output=True, timeout=600)
68
+ return time.perf_counter() - t0
69
+
70
+ def run(self, code: str, timeout: float = 120) -> subprocess.CompletedProcess:
71
+ return subprocess.run([self.python, "-c", code], capture_output=True, text=True, timeout=timeout)
72
+
73
+ def destroy(self):
74
+ shutil.rmtree(self.path, ignore_errors=True)
@@ -0,0 +1,48 @@
1
+ """Artifact inspection: identify executable format, target OS and CPU from file headers."""
2
+ from __future__ import annotations
3
+
4
+ import struct
5
+ from dataclasses import dataclass
6
+
7
+ _MACHO_CPU = {0x01000007: "x86_64", 0x0100000C: "arm64", 7: "i386", 12: "arm"}
8
+ _ELF_MACHINE = {0x3E: "x86_64", 0xB7: "aarch64", 0x03: "i386", 0x28: "arm", 0xF3: "riscv"}
9
+ _PE_MACHINE = {0x8664: "x86_64", 0xAA64: "aarch64", 0x014C: "i386"}
10
+
11
+
12
+ @dataclass
13
+ class Artifact:
14
+ path: str
15
+ format: str # macho | elf | pe | wasm | script | unknown
16
+ os: str | None # macos | linux | windows | any
17
+ arch: list[str]
18
+ detail: str = ""
19
+
20
+
21
+ def inspect(path: str) -> Artifact:
22
+ with open(path, "rb") as f:
23
+ head = f.read(4096)
24
+ if len(head) >= 8 and head[:4] in (b"\xcf\xfa\xed\xfe", b"\xce\xfa\xed\xfe"):
25
+ cpu = struct.unpack("<I", head[4:8])[0]
26
+ return Artifact(path, "macho", "macos", [_MACHO_CPU.get(cpu, hex(cpu))])
27
+ if head[:4] in (b"\xca\xfe\xba\xbe", b"\xca\xfe\xba\xbf"):
28
+ n = struct.unpack(">I", head[4:8])[0]
29
+ if n < 20: # fat Mach-O (a Java class file has a large "count" here)
30
+ archs = [_MACHO_CPU.get(struct.unpack(">I", head[8 + 20 * i:12 + 20 * i])[0], "?") for i in range(n)]
31
+ return Artifact(path, "macho", "macos", archs, "universal binary")
32
+ if head[:4] == b"\x7fELF":
33
+ little = head[5] == 1
34
+ mach = struct.unpack("<H" if little else ">H", head[18:20])[0]
35
+ osabi = head[7]
36
+ return Artifact(path, "elf", "linux" if osabi in (0, 3) else f"elf-osabi-{osabi}",
37
+ [_ELF_MACHINE.get(mach, hex(mach))], "64-bit" if head[4] == 2 else "32-bit")
38
+ if head[:2] == b"MZ" and len(head) >= 0x40:
39
+ off = struct.unpack("<I", head[0x3C:0x40])[0]
40
+ if off + 6 <= len(head) and head[off:off + 4] == b"PE\0\0":
41
+ mach = struct.unpack("<H", head[off + 4:off + 6])[0]
42
+ return Artifact(path, "pe", "windows", [_PE_MACHINE.get(mach, hex(mach))])
43
+ return Artifact(path, "pe", "windows", ["?"], "DOS/unknown PE")
44
+ if head[:4] == b"\0asm":
45
+ return Artifact(path, "wasm", "any", ["wasm32"], f"version {struct.unpack('<I', head[4:8])[0]}")
46
+ if head[:2] == b"#!":
47
+ return Artifact(path, "script", "any", ["any"], head.split(b"\n", 1)[0][2:].decode(errors="replace").strip())
48
+ return Artifact(path, "unknown", None, [])