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/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)
|
spatial/core/protocol.py
ADDED
|
@@ -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
|
spatial/core/registry.py
ADDED
|
@@ -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()
|