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/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))))
|
spatial/runtime/envs.py
ADDED
|
@@ -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, [])
|