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
@@ -0,0 +1,100 @@
1
+ """Far-tier wrappers: encryption and replication.
2
+
3
+ EncryptedBackend : pages are sealed with AES-256-GCM before they leave the process; the
4
+ page key is bound as associated data, so a page served under another
5
+ key (swap attack) or modified at rest fails to open.
6
+ ReplicatedBackend : every page is written to all replicas; reads fall back to the next
7
+ replica when one fails or returns a corrupted page.
8
+ Neither adds capacity: they add confidentiality and availability to the far tier.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import struct
14
+
15
+ from cryptography.exceptions import InvalidTag
16
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
17
+
18
+
19
+ class PageIntegrityError(Exception):
20
+ pass
21
+
22
+
23
+ class EncryptedBackend:
24
+ def __init__(self, inner, key: bytes | None = None):
25
+ self.inner = inner
26
+ self.key = key or AESGCM.generate_key(bit_length=256) # lives only in this process
27
+ self._aead = AESGCM(self.key)
28
+ self.name = f"encrypted({inner.name})"
29
+
30
+ def put(self, key: int, blob: bytes) -> None:
31
+ nonce = os.urandom(12)
32
+ self.inner.put(key, nonce + self._aead.encrypt(nonce, blob, struct.pack(">q", key)))
33
+
34
+ def _open(self, key: int, sealed: bytes) -> bytes:
35
+ try:
36
+ return self._aead.decrypt(sealed[:12], sealed[12:], struct.pack(">q", key))
37
+ except InvalidTag:
38
+ raise PageIntegrityError(f"far page {key} failed authentication")
39
+
40
+ def get(self, key: int) -> bytes:
41
+ return self._open(key, self.inner.get(key))
42
+
43
+ def get_many(self, keys):
44
+ return [None if b is None else self._open(k, b) for k, b in zip(keys, self.inner.get_many(keys))]
45
+
46
+ def delete(self, key: int) -> None:
47
+ self.inner.delete(key)
48
+
49
+ def close(self) -> None:
50
+ self.inner.close()
51
+
52
+
53
+ class ReplicatedBackend:
54
+ def __init__(self, replicas: list):
55
+ if len(replicas) < 2:
56
+ raise ValueError("replication needs at least two backends")
57
+ self.replicas = replicas
58
+ self.name = "replicated(" + ",".join(r.name for r in replicas) + ")"
59
+ self.failovers = 0
60
+ self.write_errors = 0
61
+
62
+ def put(self, key: int, blob: bytes) -> None:
63
+ ok = 0
64
+ for r in self.replicas:
65
+ try:
66
+ r.put(key, blob)
67
+ ok += 1
68
+ except Exception:
69
+ self.write_errors += 1
70
+ if ok == 0:
71
+ raise IOError(f"far page {key}: no replica accepted the write")
72
+
73
+ def get(self, key: int) -> bytes:
74
+ last = None
75
+ for i, r in enumerate(self.replicas):
76
+ try:
77
+ b = r.get(key)
78
+ if i:
79
+ self.failovers += 1
80
+ return b
81
+ except Exception as e:
82
+ last = e
83
+ raise IOError(f"far page {key}: all replicas failed ({last})")
84
+
85
+ def get_many(self, keys):
86
+ return [self.get(k) for k in keys]
87
+
88
+ def delete(self, key: int) -> None:
89
+ for r in self.replicas:
90
+ try:
91
+ r.delete(key)
92
+ except Exception:
93
+ pass
94
+
95
+ def close(self) -> None:
96
+ for r in self.replicas:
97
+ try:
98
+ r.close()
99
+ except Exception:
100
+ pass
@@ -0,0 +1,43 @@
1
+ """Shared pages with several writers: optimistic concurrency on the page server.
2
+
3
+ Each page has a version. A write succeeds only if the writer saw the current version
4
+ (compare-and-swap); otherwise it re-reads and retries. This gives lost-update-free
5
+ read-modify-write for pages shared by several processes. It is NOT distributed shared
6
+ memory: every access is a network round trip, and there is one server (no consensus).
7
+ """
8
+ from __future__ import annotations
9
+
10
+
11
+ class VersionConflict(Exception):
12
+ pass
13
+
14
+
15
+ class SharedPages:
16
+ def __init__(self, client):
17
+ self.c = client
18
+ self.conflicts = 0
19
+
20
+ def read(self, key: int) -> tuple[bytes | None, int]:
21
+ rh, blob = self.c.call({"op": "vget", "key": key})
22
+ return (blob if rh["exists"] else None), rh["version"]
23
+
24
+ def write(self, key: int, data: bytes, expected_version: int) -> int:
25
+ try:
26
+ rh, _ = self.c.call({"op": "vput", "key": key, "expected": expected_version}, data)
27
+ except RuntimeError as e:
28
+ if "VersionConflict" in str(e):
29
+ raise VersionConflict(str(e)) from None
30
+ raise
31
+ return rh["version"]
32
+
33
+ def update(self, key: int, fn, max_retries: int = 1000) -> bytes:
34
+ """Atomically replace the page by fn(current bytes or None)."""
35
+ for _ in range(max_retries):
36
+ cur, ver = self.read(key)
37
+ new = fn(cur)
38
+ try:
39
+ self.write(key, new, ver)
40
+ return new
41
+ except VersionConflict:
42
+ self.conflicts += 1
43
+ raise VersionConflict(f"page {key}: still contended after {max_retries} retries")
@@ -0,0 +1,355 @@
1
+ """Spatial Resource Engine — user-space tiered memory (application-integrated far memory).
2
+
3
+ Tiers:
4
+ T0 hot : uncompressed pages in local RAM (LRU, byte budget)
5
+ T1 compressed : compressed + content-deduplicated pages in local RAM (LRU, byte budget)
6
+ T2 far : compressed pages on a FarBackend (SSD file or remote page server)
7
+
8
+ HONESTY CONTRACT: T1 and T2 are NOT RAM. T1 trades CPU for space; T2 trades latency.
9
+ `stats()` always separates logical bytes from physical local RAM bytes, and every
10
+ access is timed per tier so the cost of leaving T0 is measured, never hidden.
11
+ This is an object store used through an API, not transparent kernel paging.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import os
17
+ import time
18
+ import zlib
19
+ from collections import OrderedDict
20
+ from typing import Iterable, Protocol
21
+
22
+ try:
23
+ import lz4.frame as _lz4
24
+ except Exception: # pragma: no cover
25
+ _lz4 = None
26
+
27
+
28
+ # ---------------------------------------------------------------- codecs
29
+ class Codec:
30
+ name = "none"
31
+
32
+ def enc(self, b: bytes) -> bytes:
33
+ return b
34
+
35
+ def dec(self, b: bytes) -> bytes:
36
+ return b
37
+
38
+
39
+ class Lz4Codec(Codec):
40
+ name = "lz4"
41
+
42
+ def enc(self, b):
43
+ return _lz4.compress(b, compression_level=0, store_size=True)
44
+
45
+ def dec(self, b):
46
+ return _lz4.decompress(b)
47
+
48
+
49
+ class ZlibCodec(Codec):
50
+ name = "zlib1"
51
+
52
+ def enc(self, b):
53
+ return zlib.compress(b, 1)
54
+
55
+ def dec(self, b):
56
+ return zlib.decompress(b)
57
+
58
+
59
+ def codec(name: str) -> Codec:
60
+ if name == "lz4":
61
+ if _lz4 is None:
62
+ raise RuntimeError("lz4 not installed")
63
+ return Lz4Codec()
64
+ return {"none": Codec, "zlib1": ZlibCodec}[name]()
65
+
66
+
67
+ # ---------------------------------------------------------------- far backends
68
+ class FarBackend(Protocol):
69
+ name: str
70
+
71
+ def put(self, key: int, blob: bytes) -> None: ...
72
+ def get(self, key: int) -> bytes: ...
73
+ def get_many(self, keys: list[int]) -> list[bytes | None]: ...
74
+ def delete(self, key: int) -> None: ...
75
+ def close(self) -> None: ...
76
+
77
+
78
+ class FileBackend:
79
+ """Append-only log on local storage (SSD). Storage, not memory."""
80
+
81
+ def __init__(self, path: str):
82
+ self.name = "ssd-file"
83
+ self.path = path
84
+ self._f = open(path, "w+b", buffering=0)
85
+ self._idx: dict[int, tuple[int, int]] = {}
86
+ self._end = 0
87
+
88
+ def put(self, key, blob):
89
+ os.pwrite(self._f.fileno(), blob, self._end)
90
+ self._idx[key] = (self._end, len(blob))
91
+ self._end += len(blob)
92
+
93
+ def get(self, key):
94
+ off, ln = self._idx[key]
95
+ return os.pread(self._f.fileno(), ln, off)
96
+
97
+ def get_many(self, keys):
98
+ return [self.get(k) if k in self._idx else None for k in keys]
99
+
100
+ def delete(self, key):
101
+ self._idx.pop(key, None)
102
+
103
+ def close(self):
104
+ self._f.close()
105
+ try:
106
+ os.unlink(self.path)
107
+ except OSError:
108
+ pass
109
+
110
+
111
+ class RemoteBackend:
112
+ """Pages held in the RAM of a page server reached over SWP/0 (see pageserver.py)."""
113
+
114
+ def __init__(self, client, label: str = "remote"):
115
+ self.name = label
116
+ self.c = client
117
+
118
+ def put(self, key, blob):
119
+ self.c.call({"op": "put", "key": key}, blob)
120
+
121
+ def get(self, key):
122
+ return self.c.call({"op": "get", "key": key})[1]
123
+
124
+ def get_many(self, keys):
125
+ rh, blob = self.c.call({"op": "get_many", "keys": keys})
126
+ out, off = [], 0
127
+ for ln in rh["lens"]:
128
+ if ln < 0:
129
+ out.append(None)
130
+ else:
131
+ out.append(blob[off:off + ln])
132
+ off += ln
133
+ return out
134
+
135
+ def delete(self, key):
136
+ self.c.call({"op": "del", "key": key})
137
+
138
+ def close(self):
139
+ self.c.close()
140
+
141
+
142
+ # ---------------------------------------------------------------- store
143
+ class _TierStat:
144
+ __slots__ = ("hits", "ns")
145
+
146
+ def __init__(self):
147
+ self.hits = 0
148
+ self.ns = 0
149
+
150
+ def add(self, ns):
151
+ self.hits += 1
152
+ self.ns += ns
153
+
154
+
155
+ class TieredStore:
156
+ def __init__(self, hot_bytes: int, compressed_bytes: int, far: FarBackend | None = None,
157
+ codec_name: str = "lz4", dedup: bool = True, prefetch: int = 0,
158
+ keep_latencies: bool = False, keep_far_copy: bool = False):
159
+ self.keep_far_copy = keep_far_copy
160
+ self.clean_evictions = self.far_writes = 0
161
+ self.hot_cap, self.comp_cap = hot_bytes, compressed_bytes
162
+ self.far = far
163
+ self.codec = codec(codec_name)
164
+ self.dedup = dedup
165
+ self.prefetch = prefetch
166
+ self._hot: OrderedDict[int, bytes] = OrderedDict()
167
+ self._hot_bytes = 0
168
+ self._comp: OrderedDict[int, bytes] = OrderedDict() # key -> digest (dedup) or blob
169
+ self._blobs: dict[bytes, list] = {} # digest -> [blob, refcount]
170
+ self._comp_bytes = 0
171
+ self._far_keys: dict[int, int] = {} # key -> blob length
172
+ self._far_bytes = 0
173
+ self.logical_bytes = 0
174
+ self._sizes: dict[int, int] = {}
175
+ self.st = {"hot": _TierStat(), "compressed": _TierStat(), "far": _TierStat()}
176
+ self.compress_ns = self.decompress_ns = 0
177
+ self.raw_in = self.comp_out = 0
178
+ self.dedup_hits = 0
179
+ self.prefetched = self.prefetch_used = 0
180
+ self._prefetched_keys: set[int] = set()
181
+ self._last_far_key: int | None = None
182
+ self.lat = {"hot": [], "compressed": [], "far": []} if keep_latencies else None
183
+
184
+ # ---- internals
185
+ def _comp_put(self, key: int, blob: bytes, digest: bytes | None):
186
+ if self.dedup and digest is not None:
187
+ ent = self._blobs.get(digest)
188
+ if ent:
189
+ ent[1] += 1
190
+ self.dedup_hits += 1
191
+ else:
192
+ self._blobs[digest] = [blob, 1]
193
+ self._comp_bytes += len(blob)
194
+ self._comp[key] = digest
195
+ else:
196
+ self._comp[key] = blob
197
+ self._comp_bytes += len(blob)
198
+ self._evict_comp()
199
+
200
+ def _comp_take(self, key: int) -> bytes:
201
+ v = self._comp.pop(key)
202
+ if self.dedup:
203
+ ent = self._blobs[v]
204
+ ent[1] -= 1
205
+ blob = ent[0]
206
+ if ent[1] == 0:
207
+ del self._blobs[v]
208
+ self._comp_bytes -= len(blob)
209
+ return blob
210
+ self._comp_bytes -= len(v)
211
+ return v
212
+
213
+ def _evict_comp(self):
214
+ while self._comp_bytes > self.comp_cap and self._comp:
215
+ k = next(iter(self._comp))
216
+ blob = self._comp_take(k)
217
+ if self.far is None:
218
+ raise MemoryError("tiered store full and no far tier configured")
219
+ if k in self._far_keys: # clean copy already there: eviction is free
220
+ self.clean_evictions += 1
221
+ continue
222
+ self.far.put(k, blob)
223
+ self.far_writes += 1
224
+ self._far_keys[k] = len(blob)
225
+ self._far_bytes += len(blob)
226
+
227
+ def _evict_hot(self):
228
+ while self._hot_bytes > self.hot_cap and len(self._hot) > 1:
229
+ k, raw = self._hot.popitem(last=False)
230
+ self._hot_bytes -= len(raw)
231
+ t = time.perf_counter_ns()
232
+ blob = self.codec.enc(raw)
233
+ self.compress_ns += time.perf_counter_ns() - t
234
+ self.raw_in += len(raw)
235
+ self.comp_out += len(blob)
236
+ digest = hashlib.blake2b(raw, digest_size=16).digest() if self.dedup else None
237
+ self._comp_put(k, blob, digest)
238
+
239
+ def _admit(self, key: int, raw: bytes):
240
+ self._hot[key] = raw
241
+ self._hot_bytes += len(raw)
242
+ self._evict_hot()
243
+
244
+ def _drop(self, key: int):
245
+ if key in self._hot:
246
+ self._hot_bytes -= len(self._hot.pop(key))
247
+ if key in self._comp:
248
+ self._comp_take(key)
249
+ if key in self._far_keys: # also invalidates a retained clean copy
250
+ self._far_bytes -= self._far_keys.pop(key)
251
+ self.far.delete(key)
252
+
253
+ # ---- API
254
+ def put(self, key: int, raw: bytes) -> None:
255
+ if key in self._sizes:
256
+ self.logical_bytes -= self._sizes[key]
257
+ self._drop(key)
258
+ self._sizes[key] = len(raw)
259
+ self.logical_bytes += len(raw)
260
+ self._admit(key, raw)
261
+
262
+ def get(self, key: int) -> bytes:
263
+ t0 = time.perf_counter_ns()
264
+ if key in self._hot:
265
+ self._hot.move_to_end(key)
266
+ raw = self._hot[key]
267
+ tier = "hot"
268
+ elif key in self._comp:
269
+ blob = self._comp_take(key)
270
+ t = time.perf_counter_ns()
271
+ raw = self.codec.dec(blob)
272
+ self.decompress_ns += time.perf_counter_ns() - t
273
+ if key in self._prefetched_keys:
274
+ self._prefetched_keys.discard(key)
275
+ self.prefetch_used += 1
276
+ self._admit(key, raw)
277
+ tier = "compressed"
278
+ elif key in self._far_keys:
279
+ raw = self._far_fetch(key)
280
+ tier = "far"
281
+ else:
282
+ raise KeyError(key)
283
+ ns = time.perf_counter_ns() - t0
284
+ self.st[tier].add(ns)
285
+ if self.lat is not None:
286
+ self.lat[tier].append(ns)
287
+ return raw
288
+
289
+ def _far_fetch(self, key: int) -> bytes:
290
+ # Sequential detector: only read ahead when the previous far miss was key-1.
291
+ seq = self.prefetch and self._last_far_key is not None and key == self._last_far_key + 1
292
+ self._last_far_key = key
293
+ if seq:
294
+ extra = [k for k in range(key + 1, key + 1 + self.prefetch)
295
+ if k in self._far_keys and k not in self._hot and k not in self._comp]
296
+ blobs = self.far.get_many([key] + extra)
297
+ blob = blobs[0]
298
+ for k, b in zip(extra, blobs[1:]):
299
+ if b is None:
300
+ continue
301
+ # Prefetched pages stay compressed in T1: cheap if never used.
302
+ self._far_release(k)
303
+ self._comp_put(k, b, hashlib.blake2b(b, digest_size=16).digest() if self.dedup else None)
304
+ self._prefetched_keys.add(k)
305
+ self.prefetched += 1
306
+ self._last_far_key = k
307
+ else:
308
+ blob = self.far.get(key)
309
+ self._far_release(key)
310
+ t = time.perf_counter_ns()
311
+ raw = self.codec.dec(blob)
312
+ self.decompress_ns += time.perf_counter_ns() - t
313
+ self._admit(key, raw)
314
+ return raw
315
+
316
+ def _far_release(self, key: int):
317
+ """After a far read: drop the far copy, or keep it as a clean copy (keep_far_copy) so a
318
+ later eviction of this unmodified page costs no network write."""
319
+ if self.keep_far_copy:
320
+ return
321
+ self._far_bytes -= self._far_keys.pop(key)
322
+ self.far.delete(key)
323
+
324
+ def keys(self) -> Iterable[int]:
325
+ return self._sizes.keys()
326
+
327
+ def stats(self) -> dict:
328
+ n = sum(s.hits for s in self.st.values()) or 1
329
+ d = {
330
+ "logical_bytes": self.logical_bytes,
331
+ "local_ram_bytes": self._hot_bytes + self._comp_bytes, # physical RAM really used
332
+ "hot_bytes": self._hot_bytes, "compressed_tier_bytes": self._comp_bytes,
333
+ "far_bytes": self._far_bytes, "far_backend": self.far.name if self.far else None,
334
+ "codec": self.codec.name,
335
+ "compression_ratio": round(self.raw_in / self.comp_out, 3) if self.comp_out else None,
336
+ "dedup_hits": self.dedup_hits,
337
+ "compress_cpu_s": self.compress_ns / 1e9, "decompress_cpu_s": self.decompress_ns / 1e9,
338
+ "prefetched": self.prefetched, "prefetch_used": self.prefetch_used,
339
+ "far_writes": self.far_writes, "clean_evictions": self.clean_evictions,
340
+ }
341
+ for name, s in self.st.items():
342
+ d[f"{name}_hits"] = s.hits
343
+ d[f"{name}_hit_ratio"] = round(s.hits / n, 4)
344
+ d[f"{name}_mean_us"] = round(s.ns / s.hits / 1e3, 2) if s.hits else None
345
+ if self.lat is not None:
346
+ for name, xs in self.lat.items():
347
+ if xs:
348
+ xs = sorted(xs)
349
+ d[f"{name}_p50_us"] = round(xs[len(xs) // 2] / 1e3, 2)
350
+ d[f"{name}_p99_us"] = round(xs[min(len(xs) - 1, int(len(xs) * 0.99))] / 1e3, 2)
351
+ return d
352
+
353
+ def close(self):
354
+ if self.far:
355
+ self.far.close()
spatial/q/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """SPATIAL Q: hybrid classical / distributed / quantum language, compiler and runtime."""
2
+ from .parser import parse # noqa: F401
3
+ from .compiler import compile_kernel, optimize # noqa: F401
4
+ from .runtime import Runtime, run_source # noqa: F401
spatial/q/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ from .codegen import SQCodegenError
2
+ from .compiler import SQCompileError
3
+ from .runtime import SQRuntimeError, main
4
+ from . import backends
5
+
6
+ try:
7
+ main()
8
+ except (SyntaxError, SQCodegenError, SQCompileError, SQRuntimeError, backends.BackendUnavailable) as e:
9
+ raise SystemExit(f"spatialq: {e}")
@@ -0,0 +1,122 @@
1
+ """SPATIAL Q backends. A backend either really executes a circuit or raises
2
+ BackendUnavailable with the reason; it never returns invented results."""
3
+ from __future__ import annotations
4
+
5
+ from ..ir import Circuit
6
+ from .. import sim
7
+
8
+
9
+ class BackendUnavailable(RuntimeError):
10
+ pass
11
+
12
+
13
+ class Backend:
14
+ name = "?"
15
+ kind = "qsim" # qsim | qpu | export
16
+
17
+ def sample(self, c: Circuit, shots: int, seed: int | None = None) -> dict[str, int]:
18
+ raise NotImplementedError
19
+
20
+ def expectation(self, c: Circuit, pauli: str) -> float:
21
+ raise BackendUnavailable(f"{self.name} does not provide exact expectation values")
22
+
23
+
24
+ class StateVectorBackend(Backend):
25
+ """Built-in NumPy simulator."""
26
+ name = "statevector"
27
+
28
+ def sample(self, c, shots, seed=None, noise: float = 0.0):
29
+ return sim.sample(c, shots, seed, noise=noise)
30
+
31
+ def expectation(self, c, pauli):
32
+ return sim.expectation(c, pauli)
33
+
34
+
35
+ def to_qiskit(c: Circuit, measure_all: bool = True):
36
+ from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister
37
+ q = QuantumRegister(c.n_qubits, "q")
38
+ regs = [q]
39
+ m = None
40
+ if c.n_cbits:
41
+ m = ClassicalRegister(c.n_cbits, "m")
42
+ regs.append(m)
43
+ qc = QuantumCircuit(*regs)
44
+
45
+ def emit(o):
46
+ if o.name == "measure":
47
+ qc.measure(q[o.qubits[0]], m[o.cbit])
48
+ elif o.name == "reset":
49
+ qc.reset(q[o.qubits[0]])
50
+ else:
51
+ getattr(qc, o.name)(*o.params, *[q[i] for i in o.qubits])
52
+
53
+ for o in c.ops:
54
+ if o.cond:
55
+ def nest(conds):
56
+ if not conds:
57
+ emit(o)
58
+ return
59
+ b, v = conds[0]
60
+ with qc.if_test((m[b], v)):
61
+ nest(conds[1:])
62
+ nest(list(o.cond))
63
+ else:
64
+ emit(o)
65
+ if measure_all:
66
+ cr = ClassicalRegister(c.n_qubits, "c")
67
+ qc.add_register(cr)
68
+ qc.measure(q, cr)
69
+ return qc
70
+
71
+
72
+ class AerBackend(Backend):
73
+ """Qiskit Aer simulator (independent implementation used as the reference)."""
74
+ name = "aer"
75
+
76
+ def __init__(self):
77
+ try:
78
+ from qiskit_aer import AerSimulator
79
+ except Exception as e:
80
+ raise BackendUnavailable(f"qiskit-aer not importable: {e}")
81
+ self._sim = AerSimulator()
82
+
83
+ def sample(self, c, shots, seed=None):
84
+ qc = to_qiskit(c)
85
+ res = self._sim.run(qc, shots=shots, seed_simulator=seed).result()
86
+ counts: dict[str, int] = {}
87
+ for k, v in res.get_counts().items():
88
+ key = k.split()[0] # registers print as "c m"; keep the final read-out
89
+ counts[key] = counts.get(key, 0) + v
90
+ return counts
91
+
92
+ def expectation(self, c, pauli):
93
+ from qiskit.quantum_info import Pauli, Statevector
94
+ return float(Statevector(to_qiskit(c, measure_all=False)).expectation_value(Pauli(pauli)).real)
95
+
96
+
97
+ class Qasm3Export(Backend):
98
+ """Not an executor: produces the OpenQASM 3 program a provider would receive."""
99
+ name = "qasm3"
100
+ kind = "export"
101
+
102
+ def sample(self, c, shots, seed=None):
103
+ raise BackendUnavailable("qasm3 is an export target; use Circuit.to_qasm3()")
104
+
105
+
106
+ class QpuBackend(Backend):
107
+ """Real quantum hardware. No provider credentials are configured in this prototype, so
108
+ this backend refuses to run instead of silently falling back to a simulator."""
109
+ name = "qpu"
110
+ kind = "qpu"
111
+
112
+ def sample(self, c, shots, seed=None):
113
+ raise BackendUnavailable(
114
+ "no QPU provider configured (needs an account/API token with IBM Quantum, IonQ, "
115
+ "Quantinuum, AWS Braket...). The circuit can be exported with to_qasm3().")
116
+
117
+
118
+ def get(name: str) -> Backend:
119
+ table = {"statevector": StateVectorBackend, "aer": AerBackend, "qasm3": Qasm3Export, "qpu": QpuBackend}
120
+ if name not in table:
121
+ raise BackendUnavailable(f"unknown backend '{name}'")
122
+ return table[name]()