core/core/array_codec.py
Shay 21e2ccb508 feat(persistence): Shape B+ Phase A — bit-exact array codec + FieldState (de)serialize
Foundation for L10 resume-as-same-life persistence. Adds:
- core/array_codec.py: a leaf (numpy+base64) codec encoding arrays as
  {dtype, shape, b64(raw bytes)} — BIT-EXACT, never decimal. Float round-trips
  lose zero precision, so a restored versor keeps versor_condition < 1e-6 and a
  replayed turn keeps its trace_hash. dtype carries byte order; float32 is never
  conflated with float64.
- field/state.py: FieldState.to_dict/from_dict. Multivector arrays (F, holonomy)
  go through the byte codec; energy/valence round-trip exactly via JSON-safe
  helpers (lazy physics imports keep field/ cycle-free).

Exit gate (the scope's #1 risk, de-risked first): bit-exact round-trip AND
closure preserved — versor_condition(restored.F) == versor_condition(fs.F)
exactly. 10 codec/FieldState tests + 55 architectural-invariant/runtime tests
pass. Purely additive; no existing behavior changed.

Part of docs/analysis/L10-shapeBplus-persistence-scope-2026-06-05.md (Phase A).
2026-06-05 11:38:58 -07:00

54 lines
2.1 KiB
Python

"""Bit-exact (de)serialization of numpy arrays for deterministic persistence.
A numpy array encodes to ``{"dtype", "shape", "b64"}`` where ``b64`` is base64 of
the array's raw bytes. This is **bit-exact**: every float round-trips with zero
precision loss, so a restored versor keeps ``versor_condition < 1e-6`` and a
replayed turn keeps its ``trace_hash``.
NEVER serialize field arrays as decimal/JSON floats. Decimal truncates the
mantissa and silently breaks both closure and deterministic replay — the Cl(4,1)
float-truncation pitfall. ``dtype`` carries byte order (``'<f4'``/``'<f8'``), so
the encoding is portable, and ``float32`` is never conflated with ``float64``.
This module is a leaf: it imports only numpy + base64, so every layer (field,
vault, session, engine_state) can use it without an import cycle.
"""
from __future__ import annotations
import base64
from typing import Any
import numpy as np
def encode_array(arr: np.ndarray) -> dict[str, Any]:
"""Encode a numpy array to a bit-exact, JSON-safe dict."""
contiguous = np.ascontiguousarray(arr)
return {
"dtype": contiguous.dtype.str, # byte-order-aware, e.g. '<f4', '<f8', '<i4'
"shape": list(contiguous.shape),
"b64": base64.b64encode(contiguous.tobytes()).decode("ascii"),
}
def decode_array(payload: dict[str, Any]) -> np.ndarray:
"""Decode a payload produced by ``encode_array`` back to an exact array.
Returns a writable copy (``np.frombuffer`` is read-only) so the restored
array can be composed and mutated like a freshly-built one.
"""
dtype = np.dtype(payload["dtype"])
raw = base64.b64decode(payload["b64"])
flat = np.frombuffer(raw, dtype=dtype)
return flat.reshape(payload["shape"]).copy()
def encode_optional_array(arr: np.ndarray | None) -> dict[str, Any] | None:
"""Encode an array, or return ``None`` for ``None`` (e.g. optional holonomy)."""
return None if arr is None else encode_array(arr)
def decode_optional_array(payload: dict[str, Any] | None) -> np.ndarray | None:
"""Decode an optional-array payload, or return ``None`` for ``None``."""
return None if payload is None else decode_array(payload)