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/core/model.py ADDED
@@ -0,0 +1,82 @@
1
+ """SPATIAL CORE data model shared by every engine.
2
+
3
+ A Node exposes Resources. A Task declares what it needs. An engine produces a
4
+ Measurement for every execution; nothing is claimed without one.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import time
9
+ import uuid
10
+ from dataclasses import asdict, dataclass, field
11
+ from enum import Enum
12
+ from typing import Any
13
+
14
+
15
+ class ResourceKind(str, Enum):
16
+ CPU = "cpu"
17
+ GPU = "gpu"
18
+ NPU = "npu"
19
+ RAM = "ram"
20
+ FAR_MEMORY = "far_memory" # compressed / remote / storage-backed. Never reported as RAM.
21
+ STORAGE = "storage"
22
+ NETWORK = "network"
23
+ QPU = "qpu"
24
+ QSIM = "qsim" # classical simulator of a quantum device
25
+
26
+
27
+ @dataclass
28
+ class Resource:
29
+ kind: ResourceKind
30
+ name: str
31
+ capacity: float # cores, bytes, qubits, bits/s
32
+ unit: str
33
+ available: float | None = None
34
+ attrs: dict[str, Any] = field(default_factory=dict)
35
+
36
+
37
+ @dataclass
38
+ class Node:
39
+ node_id: str
40
+ hostname: str
41
+ os: str
42
+ arch: str
43
+ resources: list[Resource] = field(default_factory=list)
44
+ attrs: dict[str, Any] = field(default_factory=dict)
45
+
46
+ def by_kind(self, kind: ResourceKind) -> list[Resource]:
47
+ return [r for r in self.resources if r.kind == kind]
48
+
49
+ def to_dict(self) -> dict:
50
+ d = asdict(self)
51
+ for r in d["resources"]:
52
+ r["kind"] = ResourceKind(r["kind"]).value
53
+ return d
54
+
55
+
56
+ @dataclass
57
+ class Task:
58
+ """Unit of work. `workload` names a whitelisted callable; no code is shipped."""
59
+ workload: str
60
+ params: dict[str, Any] = field(default_factory=dict)
61
+ needs: dict[str, float] = field(default_factory=dict) # e.g. {"ram": 2e9, "cpu": 2}
62
+ task_id: str = field(default_factory=lambda: uuid.uuid4().hex[:12])
63
+
64
+
65
+ @dataclass
66
+ class Measurement:
67
+ """One real execution. `ok=False` rows are kept: failures are results."""
68
+ engine: str
69
+ experiment: str
70
+ strategy: str
71
+ ok: bool
72
+ wall_s: float
73
+ cpu_s: float | None = None
74
+ peak_rss_bytes: int | None = None
75
+ energy_j: float | None = None # None when not measurable (e.g. on AC power)
76
+ cost_usd: float | None = None
77
+ metrics: dict[str, Any] = field(default_factory=dict)
78
+ notes: str = ""
79
+ ts: float = field(default_factory=time.time)
80
+
81
+ def to_dict(self) -> dict:
82
+ return asdict(self)
@@ -0,0 +1,167 @@
1
+ """SPATIAL wire protocol v1 (SWP/1): authenticated AND encrypted RPC over TCP.
2
+
3
+ Handshake server -> client : nonce_s (16 bytes)
4
+ client -> server : HMAC-SHA256(token, nonce_s) (32 bytes) | nonce_c (16 bytes)
5
+ Keys HKDF-SHA256(token, salt = nonce_s | nonce_c) -> one AES-256-GCM key per direction
6
+ Record u32 length | AES-GCM(ciphertext | tag), nonce = 4 zero bytes | u64 record counter
7
+ Plaintext u32 header_len | u32 blob_len | header (JSON utf-8) | blob (raw bytes)
8
+
9
+ Properties: the token never crosses the wire; every record is confidential and
10
+ integrity-protected; a replayed, reordered or modified record fails authentication.
11
+ Not provided: forward secrecy (a stolen token decrypts recorded sessions) and peer
12
+ identity beyond "knows the token". Use TLS/WireGuard on top for stronger guarantees.
13
+
14
+ Safety: servers bind to 127.0.0.1 unless allow_remote=True is passed explicitly.
15
+ No code is ever transferred: requests name allow-listed operations.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import hashlib
20
+ import hmac
21
+ import json
22
+ import os
23
+ import socket
24
+ import socketserver
25
+ import struct
26
+ import threading
27
+ from typing import Callable
28
+
29
+ from cryptography.exceptions import InvalidTag
30
+ from cryptography.hazmat.primitives import hashes
31
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
32
+ from cryptography.hazmat.primitives.kdf.hkdf import HKDF
33
+
34
+ _HDR = struct.Struct(">II")
35
+ _LEN = struct.Struct(">I")
36
+ MAX_FRAME = 256 * 1024 * 1024
37
+
38
+
39
+ def _recv_exact(sock: socket.socket, n: int) -> bytes:
40
+ buf = bytearray(n)
41
+ view = memoryview(buf)
42
+ got = 0
43
+ while got < n:
44
+ r = sock.recv_into(view[got:], n - got)
45
+ if r == 0:
46
+ raise ConnectionError("peer closed")
47
+ got += r
48
+ return bytes(buf)
49
+
50
+
51
+ class SecureChannel:
52
+ """Encrypted, ordered record channel over a connected socket."""
53
+
54
+ def __init__(self, sock: socket.socket, token: bytes, nonce_s: bytes, nonce_c: bytes, is_server: bool):
55
+ okm = HKDF(algorithm=hashes.SHA256(), length=64, salt=nonce_s + nonce_c, info=b"SWP/1 keys").derive(token)
56
+ c2s, s2c = AESGCM(okm[:32]), AESGCM(okm[32:])
57
+ self._tx, self._rx = (s2c, c2s) if is_server else (c2s, s2c)
58
+ self._ntx = self._nrx = 0
59
+ self.sock = sock
60
+
61
+ def send(self, header: dict, blob: bytes = b"") -> None:
62
+ h = json.dumps(header, separators=(",", ":")).encode()
63
+ plain = _HDR.pack(len(h), len(blob)) + h + blob
64
+ ct = self._tx.encrypt(b"\0\0\0\0" + struct.pack(">Q", self._ntx), plain, None)
65
+ self._ntx += 1
66
+ self.sock.sendall(_LEN.pack(len(ct)) + ct)
67
+
68
+ def recv(self) -> tuple[dict, bytes]:
69
+ (n,) = _LEN.unpack(_recv_exact(self.sock, _LEN.size))
70
+ if n > MAX_FRAME + 64:
71
+ raise ValueError("record too large")
72
+ try:
73
+ plain = self._rx.decrypt(b"\0\0\0\0" + struct.pack(">Q", self._nrx), _recv_exact(self.sock, n), None)
74
+ except InvalidTag:
75
+ raise ConnectionError("record failed authentication (tampered, replayed or wrong key)")
76
+ self._nrx += 1
77
+ hl, bl = _HDR.unpack(plain[:_HDR.size])
78
+ if _HDR.size + hl + bl != len(plain):
79
+ raise ValueError("malformed record")
80
+ header = json.loads(plain[_HDR.size:_HDR.size + hl]) if hl else {}
81
+ return header, plain[_HDR.size + hl:]
82
+
83
+
84
+ Handler = Callable[[dict, bytes], tuple[dict, bytes]]
85
+
86
+
87
+ class RpcServer:
88
+ """Threaded RPC server. Loopback-only by default."""
89
+
90
+ def __init__(self, handler: Handler, token: bytes, host: str = "127.0.0.1",
91
+ port: int = 0, allow_remote: bool = False):
92
+ if host not in ("127.0.0.1", "localhost", "::1") and not allow_remote:
93
+ raise PermissionError(
94
+ "refusing to bind a non-loopback address without allow_remote=True "
95
+ "(explicit user consent required to expose this machine)")
96
+ outer = self
97
+
98
+ class _H(socketserver.BaseRequestHandler):
99
+ def handle(self):
100
+ s = self.request
101
+ s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
102
+ nonce_s = os.urandom(16)
103
+ s.sendall(nonce_s)
104
+ try:
105
+ hello = _recv_exact(s, 48)
106
+ except ConnectionError:
107
+ return
108
+ if not hmac.compare_digest(hello[:32], hmac.new(token, nonce_s, hashlib.sha256).digest()):
109
+ return # silent drop on bad auth
110
+ ch = SecureChannel(s, token, nonce_s, hello[32:], is_server=True)
111
+ while True:
112
+ try:
113
+ header, blob = ch.recv()
114
+ except (ConnectionError, OSError, ValueError):
115
+ return
116
+ try:
117
+ rh, rb = outer.handler(header, blob)
118
+ rh.setdefault("ok", True)
119
+ except Exception as e: # report, never crash the server
120
+ rh, rb = {"ok": False, "error": f"{type(e).__name__}: {e}"}, b""
121
+ ch.send(rh, rb)
122
+
123
+ class _S(socketserver.ThreadingTCPServer):
124
+ allow_reuse_address = True
125
+ daemon_threads = True
126
+
127
+ self.handler = handler
128
+ self._srv = _S((host, port), _H)
129
+ self.host, self.port = self._srv.server_address[:2]
130
+ self._thread: threading.Thread | None = None
131
+
132
+ def start(self) -> "RpcServer":
133
+ self._thread = threading.Thread(target=self._srv.serve_forever, daemon=True)
134
+ self._thread.start()
135
+ return self
136
+
137
+ def serve_forever(self) -> None:
138
+ self._srv.serve_forever()
139
+
140
+ def stop(self) -> None:
141
+ self._srv.shutdown()
142
+ self._srv.server_close()
143
+
144
+
145
+ class RpcClient:
146
+ def __init__(self, host: str, port: int, token: bytes, timeout: float = 120.0):
147
+ self.sock = socket.create_connection((host, port), timeout=timeout)
148
+ self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
149
+ nonce_s = _recv_exact(self.sock, 16)
150
+ nonce_c = os.urandom(16)
151
+ self.sock.sendall(hmac.new(token, nonce_s, hashlib.sha256).digest() + nonce_c)
152
+ self._ch = SecureChannel(self.sock, token, nonce_s, nonce_c, is_server=False)
153
+ self._lock = threading.Lock()
154
+
155
+ def call(self, header: dict, blob: bytes = b"") -> tuple[dict, bytes]:
156
+ with self._lock:
157
+ self._ch.send(header, blob)
158
+ rh, rb = self._ch.recv()
159
+ if not rh.get("ok", False):
160
+ raise RuntimeError(rh.get("error", "remote error"))
161
+ return rh, rb
162
+
163
+ def close(self) -> None:
164
+ try:
165
+ self.sock.close()
166
+ except OSError:
167
+ pass
@@ -0,0 +1,59 @@
1
+ """Engine registry. Status is a claim that must be backed by `evidence`."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+
7
+
8
+ class Status(str, Enum):
9
+ OPERATIONAL_PROTOTYPE = "operational-prototype" # runs, tested, benchmarked on this Mac
10
+ FIRST_COMPONENTS = "first-components" # some real tested code, not a product
11
+ INTERFACE_ONLY = "interface-only" # data model / API only
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Engine:
16
+ key: str
17
+ name: str
18
+ package: str
19
+ status: Status
20
+ evidence: str # test or benchmark that justifies the status
21
+ not_done: str # what is explicitly NOT implemented
22
+
23
+
24
+ ENGINES: list[Engine] = [
25
+ Engine("compute", "Spatial Compute Engine", "spatial.compute", Status.OPERATIONAL_PROTOTYPE,
26
+ "tests/test_compute.py, benchmarks/bench_cpu.py, bench_gpu.py",
27
+ "no real second machine tested (offload and task queue validated over loopback only); no GPU scheduling"),
28
+ Engine("resource", "Spatial Resource Engine (memory tiering)", "spatial.memory",
29
+ Status.OPERATIONAL_PROTOTYPE,
30
+ "tests/test_memory.py, benchmarks/bench_memory.py",
31
+ "user-space object store only; not kernel paging; single writer; no RDMA/CXL hardware"),
32
+ Engine("runtime", "Spatial Runtime Engine", "spatial.runtime", Status.OPERATIONAL_PROTOTYPE,
33
+ "tests/test_runtime.py, benchmarks/bench_runtime.py",
34
+ "Windows apps not demonstrated (no Wine / Windows image); no containers; no GUI streaming"),
35
+ Engine("q", "SPATIAL Q", "spatial.q", Status.OPERATIONAL_PROTOTYPE,
36
+ "tests/test_q.py, benchmarks/bench_q.py",
37
+ "no real QPU executed (OpenQASM 3 and QIR export only); simulator limited by RAM"),
38
+ Engine("ai", "Spatial AI Engine", "spatial.ai", Status.FIRST_COMPONENTS,
39
+ "tests/test_engines.py (toy transformer KV cache: restore equals recompute)",
40
+ "toy NumPy model only; no real LLM, no GPU; placement planner is in the private package"),
41
+ Engine("scientific", "Spatial Scientific & Industrial Engine", "spatial.scientific",
42
+ Status.FIRST_COMPONENTS, "tests/test_domains.py, tests/test_engines.py (validated jobs)",
43
+ "two reference jobs; no domain solvers"),
44
+ Engine("simulation", "Spatial Simulation Engine", "spatial.simulation", Status.FIRST_COMPONENTS,
45
+ "tests/test_domains.py, tests/test_engines.py (heat diffusion, N-body)", "two small solvers; no multiphysics"),
46
+ Engine("intelligence", "Spatial Intelligence Engine", "spatial.intelligence",
47
+ Status.FIRST_COMPONENTS, "tests/test_domains.py, tests/test_engines.py (predictor drives the scheduler)",
48
+ "least-squares runtime model only"),
49
+ Engine("holographic", "Spatial Holographic Engine", "spatial.holographic", Status.FIRST_COMPONENTS,
50
+ "tests/test_engines.py (hologram synthesis + numerical reconstruction)",
51
+ "numerical optics only; no display hardware, no real-time rendering"),
52
+ ]
53
+
54
+
55
+ def table() -> str: # noqa: D103
56
+ rows = ["| Engine | Status | Evidence | Not done |", "|---|---|---|---|"]
57
+ for e in ENGINES:
58
+ rows.append(f"| {e.name} | {e.status.value} | {e.evidence} | {e.not_done} |")
59
+ return "\n".join(rows)
@@ -0,0 +1,8 @@
1
+ """Spatial Holographic Engine — FIRST COMPONENTS (numerical only).
2
+
3
+ No display hardware exists in this project. cgh.py synthesises a point-cloud hologram and
4
+ reconstructs it numerically (verified by a test); the rest fixes the data model (voxel grid, light-field specification) and computes the data
5
+ rates such displays would demand from the compute engine, so the requirement is explicit.
6
+ """
7
+ from .model import VoxelGrid, LightFieldSpec, voxelize # noqa: F401
8
+ from .cgh import hologram, reconstruct, max_diffraction_angle # noqa: F401
@@ -0,0 +1,40 @@
1
+ """Computer-generated hologram (point-source method) and numerical reconstruction.
2
+
3
+ hologram(): complex field on the hologram plane from a cloud of point emitters,
4
+ H(x, y) = sum_j a_j * exp(i k r_j) / r_j (textbook Fresnel/Huygens model).
5
+ reconstruct(): angular-spectrum propagation of that field to a depth z; the intensity
6
+ must peak where a point emitter sits. This is numerical optics on the CPU:
7
+ it proves the synthesis maths, not a physical display (none exists here).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+
13
+
14
+ def hologram(points: np.ndarray, amplitudes: np.ndarray, n: int, pitch: float, wavelength: float) -> np.ndarray:
15
+ """points [N,3] in metres (z > 0 in front of the plane); returns complex [n, n]."""
16
+ k = 2 * np.pi / wavelength
17
+ ax = (np.arange(n) - n / 2) * pitch
18
+ X, Y = np.meshgrid(ax, ax, indexing="xy")
19
+ H = np.zeros((n, n), dtype=complex)
20
+ for (px, py, pz), a in zip(points, amplitudes):
21
+ r = np.sqrt((X - px) ** 2 + (Y - py) ** 2 + pz ** 2)
22
+ H += a * np.exp(1j * k * r) / r
23
+ return H
24
+
25
+
26
+ def reconstruct(H: np.ndarray, z: float, pitch: float, wavelength: float) -> np.ndarray:
27
+ """Back-propagate the hologram field by distance z (angular spectrum); returns intensity."""
28
+ n = H.shape[0]
29
+ fx = np.fft.fftfreq(n, d=pitch)
30
+ FX, FY = np.meshgrid(fx, fx, indexing="xy")
31
+ arg = 1 / wavelength ** 2 - FX ** 2 - FY ** 2
32
+ kz = 2 * np.pi * np.sqrt(np.maximum(arg, 0))
33
+ field = np.fft.ifft2(np.fft.fft2(H) * np.exp(-1j * kz * z) * (arg > 0))
34
+ return np.abs(field) ** 2
35
+
36
+
37
+ def max_diffraction_angle(pitch: float, wavelength: float) -> float:
38
+ """Half-angle (radians) a pixel pitch can diffract: sin(theta) = wavelength / (2 * pitch).
39
+ Shows why holographic displays need sub-micron pixels for a wide field of view."""
40
+ return float(np.arcsin(min(1.0, wavelength / (2 * pitch))))
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
6
+
7
+
8
+ @dataclass
9
+ class VoxelGrid:
10
+ occupancy: np.ndarray # bool [nx, ny, nz]
11
+ origin: tuple[float, float, float]
12
+ voxel_size: float
13
+
14
+ @property
15
+ def filled(self) -> int:
16
+ return int(self.occupancy.sum())
17
+
18
+
19
+ def voxelize(points: np.ndarray, voxel_size: float) -> VoxelGrid:
20
+ """Point cloud [N,3] -> occupancy grid."""
21
+ lo = points.min(axis=0)
22
+ idx = np.floor((points - lo) / voxel_size).astype(int)
23
+ shape = tuple(idx.max(axis=0) + 1)
24
+ occ = np.zeros(shape, dtype=bool)
25
+ occ[idx[:, 0], idx[:, 1], idx[:, 2]] = True
26
+ return VoxelGrid(occ, tuple(float(x) for x in lo), voxel_size)
27
+
28
+
29
+ @dataclass
30
+ class LightFieldSpec:
31
+ """A multi-view (light-field) display: `views` distinct images per frame."""
32
+ views_x: int
33
+ views_y: int
34
+ width: int
35
+ height: int
36
+ fps: float
37
+ bytes_per_pixel: int = 3
38
+
39
+ @property
40
+ def pixels_per_second(self) -> float:
41
+ return self.views_x * self.views_y * self.width * self.height * self.fps
42
+
43
+ @property
44
+ def raw_bytes_per_second(self) -> float:
45
+ return self.pixels_per_second * self.bytes_per_pixel
46
+
47
+ def vs_2d(self, width: int = 3840, height: int = 2160, fps: float = 60) -> float:
48
+ """How many 2-D displays of the given format this represents in pixel rate."""
49
+ return self.pixels_per_second / (width * height * fps)
@@ -0,0 +1,6 @@
1
+ """Spatial Intelligence Engine — FIRST COMPONENTS ONLY.
2
+
3
+ A power-law runtime predictor fitted on ledger measurements, so the scheduler can
4
+ estimate a strategy's time before running it. No learned world model is claimed.
5
+ """
6
+ from .predictor import PowerLawPredictor # noqa: F401
@@ -0,0 +1,51 @@
1
+ """time ~ a * work^b, fitted per strategy by least squares in log-log space."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ from collections import defaultdict
6
+
7
+
8
+ class PowerLawPredictor:
9
+ def __init__(self):
10
+ self._obs: dict[str, list[tuple[float, float]]] = defaultdict(list)
11
+ self._fit: dict[str, tuple[float, float]] = {}
12
+
13
+ def observe(self, strategy: str, work: float, seconds: float) -> None:
14
+ if work > 0 and seconds > 0:
15
+ self._obs[strategy].append((math.log(work), math.log(seconds)))
16
+ self._fit.pop(strategy, None)
17
+
18
+ def _params(self, strategy: str) -> tuple[float, float] | None:
19
+ if strategy in self._fit:
20
+ return self._fit[strategy]
21
+ pts = self._obs.get(strategy, [])
22
+ if len({x for x, _ in pts}) < 2:
23
+ return None
24
+ n = len(pts)
25
+ mx = sum(x for x, _ in pts) / n
26
+ my = sum(y for _, y in pts) / n
27
+ sxx = sum((x - mx) ** 2 for x, _ in pts)
28
+ b = sum((x - mx) * (y - my) for x, y in pts) / sxx
29
+ self._fit[strategy] = (my - b * mx, b)
30
+ return self._fit[strategy]
31
+
32
+ def predict(self, strategy: str, work: float) -> float | None:
33
+ p = self._params(strategy)
34
+ return None if p is None else math.exp(p[0] + p[1] * math.log(work))
35
+
36
+ def best(self, work: float) -> tuple[str, float] | None:
37
+ cands = [(s, self.predict(s, work)) for s in self._obs]
38
+ cands = [(s, t) for s, t in cands if t is not None]
39
+ return min(cands, key=lambda x: x[1]) if cands else None
40
+
41
+ @classmethod
42
+ def from_ledger(cls, rows: list[dict], work_of=lambda r: r["metrics"]["params"]["n"] ** 2
43
+ * r["metrics"]["params"]["iters"]) -> "PowerLawPredictor":
44
+ p = cls()
45
+ for r in rows:
46
+ if r.get("type") == "measurement" and r.get("ok") and "params" in r.get("metrics", {}):
47
+ try:
48
+ p.observe(r["strategy"], work_of(r), r["wall_s"])
49
+ except (KeyError, TypeError):
50
+ continue
51
+ return p
@@ -0,0 +1,5 @@
1
+ """Spatial Resource Engine — memory tiering (hot RAM / compressed RAM / far)."""
2
+ from .tiered import TieredStore, FileBackend, RemoteBackend # noqa: F401
3
+ from .fararray import FarArray # noqa: F401
4
+ from .secure import EncryptedBackend, ReplicatedBackend # noqa: F401
5
+ from .shared import SharedPages, VersionConflict # noqa: F401
@@ -0,0 +1,44 @@
1
+ """FarArray: a 1-D numeric array whose pages live in a TieredStore.
2
+
3
+ Lets a workload address more data than its local RAM budget. Access outside the
4
+ hot tier is slower by a measured amount; this is tiering, not extra RAM.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import numpy as np
9
+
10
+ from .tiered import TieredStore
11
+
12
+
13
+ class FarArray:
14
+ def __init__(self, store: TieredStore, length: int, dtype=np.float64, page_items: int = 8192):
15
+ self.store, self.length = store, length
16
+ self.dtype = np.dtype(dtype)
17
+ self.page_items = page_items
18
+ self.npages = (length + page_items - 1) // page_items
19
+
20
+ @classmethod
21
+ def from_chunks(cls, store: TieredStore, chunks, length: int, dtype=np.float64,
22
+ page_items: int = 8192) -> "FarArray":
23
+ """Build from an iterator of arrays of exactly `page_items` items (last may be shorter)."""
24
+ fa = cls(store, length, dtype, page_items)
25
+ for i, c in enumerate(chunks):
26
+ store.put(i, np.ascontiguousarray(c, dtype=fa.dtype).tobytes())
27
+ return fa
28
+
29
+ def page(self, i: int) -> np.ndarray:
30
+ return np.frombuffer(self.store.get(i), dtype=self.dtype)
31
+
32
+ def __getitem__(self, idx: int):
33
+ return self.page(idx // self.page_items)[idx % self.page_items]
34
+
35
+ def sum(self) -> float:
36
+ return float(sum(self.page(i).sum() for i in range(self.npages)))
37
+
38
+ def take(self, indices: np.ndarray) -> np.ndarray:
39
+ out = np.empty(len(indices), dtype=self.dtype)
40
+ pages = indices // self.page_items
41
+ offs = indices % self.page_items
42
+ for j in range(len(indices)):
43
+ out[j] = self.page(int(pages[j]))[offs[j]]
44
+ return out
@@ -0,0 +1,141 @@
1
+ """Remote page server: holds compressed pages in ITS OWN RAM, served over SWP/0.
2
+
3
+ Run: python -m spatial.memory.pageserver --token-file <path> [--delay-ms X] [--mbps Y]
4
+
5
+ --delay-ms / --mbps EMULATE a network (added round-trip delay, bandwidth cap) because the
6
+ test bench has a single machine. Results obtained with them are labelled "emulated link":
7
+ they bound what a real LAN/WAN would do, they are not measurements of one.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import os
13
+ import secrets
14
+ import subprocess
15
+ import sys
16
+ import threading
17
+ import time
18
+ from pathlib import Path
19
+
20
+ from ..core.protocol import RpcClient, RpcServer
21
+
22
+
23
+ class VersionConflict(Exception):
24
+ pass
25
+
26
+
27
+ class PageStore:
28
+ def __init__(self, delay_ms: float = 0.0, mbps: float = 0.0, max_bytes: int = 4 << 30):
29
+ self.pages: dict[int, bytes] = {}
30
+ self.versions: dict[int, int] = {}
31
+ self._vlock = threading.Lock()
32
+ self.bytes = 0
33
+ self.delay = delay_ms / 1000.0
34
+ self.bps = mbps * 1e6 / 8
35
+ self.max_bytes = max_bytes
36
+
37
+ def _link(self, nbytes: int):
38
+ d = self.delay + (nbytes / self.bps if self.bps else 0.0)
39
+ if d > 0:
40
+ time.sleep(d)
41
+
42
+ def handle(self, h: dict, blob: bytes):
43
+ op = h["op"]
44
+ if op == "put":
45
+ if self.bytes + len(blob) > self.max_bytes:
46
+ raise MemoryError("page server quota exceeded")
47
+ old = self.pages.get(h["key"])
48
+ if old is not None:
49
+ self.bytes -= len(old)
50
+ self.pages[h["key"]] = blob
51
+ self.bytes += len(blob)
52
+ self._link(len(blob))
53
+ return {}, b""
54
+ if op == "get":
55
+ b = self.pages[h["key"]]
56
+ self._link(len(b))
57
+ return {}, b
58
+ if op == "get_many":
59
+ bl = [self.pages.get(k) for k in h["keys"]]
60
+ out = b"".join(b for b in bl if b is not None)
61
+ self._link(len(out))
62
+ return {"lens": [len(b) if b is not None else -1 for b in bl]}, out
63
+ if op == "del":
64
+ b = self.pages.pop(h["key"], None)
65
+ if b is not None:
66
+ self.bytes -= len(b)
67
+ return {}, b""
68
+ if op == "vget":
69
+ with self._vlock:
70
+ b = self.pages.get(h["key"])
71
+ return {"exists": b is not None, "version": self.versions.get(h["key"], 0)}, b or b""
72
+ if op == "vput":
73
+ with self._vlock: # compare-and-swap on the page version
74
+ cur = self.versions.get(h["key"], 0)
75
+ if h["expected"] != cur:
76
+ raise VersionConflict(f"page {h['key']} is at version {cur}, writer expected {h['expected']}")
77
+ old = self.pages.get(h["key"])
78
+ if self.bytes - (len(old) if old else 0) + len(blob) > self.max_bytes:
79
+ raise MemoryError("page server quota exceeded")
80
+ self.bytes += len(blob) - (len(old) if old else 0)
81
+ self.pages[h["key"]] = blob
82
+ self.versions[h["key"]] = cur + 1
83
+ return {"version": cur + 1}, b""
84
+ if op == "stat":
85
+ return {"pages": len(self.pages), "bytes": self.bytes}, b""
86
+ raise ValueError(op)
87
+
88
+
89
+ class LocalPageServer:
90
+ """Page server in a separate OS process on loopback."""
91
+
92
+ def __init__(self, delay_ms: float = 0.0, mbps: float = 0.0):
93
+ self.token = secrets.token_bytes(32)
94
+ self._tf = Path(os.environ.get("TMPDIR", "/tmp")) / f"spatial-ps-{os.getpid()}-{secrets.token_hex(4)}"
95
+ self._tf.write_bytes(self.token)
96
+ os.chmod(self._tf, 0o600)
97
+ root = Path(__file__).resolve().parents[2]
98
+ self.proc = subprocess.Popen(
99
+ [sys.executable, "-m", "spatial.memory.pageserver", "--token-file", str(self._tf),
100
+ "--delay-ms", str(delay_ms), "--mbps", str(mbps)],
101
+ cwd=root, stdout=subprocess.PIPE, text=True)
102
+ line = self.proc.stdout.readline()
103
+ if not line.startswith("PORT "):
104
+ raise RuntimeError(f"page server failed: {line!r}")
105
+ self.port = int(line.split()[1])
106
+
107
+ def client(self) -> RpcClient:
108
+ return RpcClient("127.0.0.1", self.port, self.token)
109
+
110
+ def stop(self):
111
+ self.proc.terminate()
112
+ try:
113
+ self.proc.wait(timeout=5)
114
+ except subprocess.TimeoutExpired:
115
+ self.proc.kill()
116
+ self._tf.unlink(missing_ok=True)
117
+
118
+ def __enter__(self):
119
+ return self
120
+
121
+ def __exit__(self, *a):
122
+ self.stop()
123
+
124
+
125
+ def main():
126
+ ap = argparse.ArgumentParser()
127
+ ap.add_argument("--token-file", required=True)
128
+ ap.add_argument("--host", default="127.0.0.1")
129
+ ap.add_argument("--port", type=int, default=0)
130
+ ap.add_argument("--delay-ms", type=float, default=0.0)
131
+ ap.add_argument("--mbps", type=float, default=0.0)
132
+ ap.add_argument("--allow-remote", action="store_true")
133
+ a = ap.parse_args()
134
+ store = PageStore(a.delay_ms, a.mbps)
135
+ srv = RpcServer(store.handle, Path(a.token_file).read_bytes(), a.host, a.port, a.allow_remote)
136
+ print(f"PORT {srv.port}", flush=True)
137
+ srv.serve_forever()
138
+
139
+
140
+ if __name__ == "__main__":
141
+ main()