268 lines
18 KiB
Python
268 lines
18 KiB
Python
"""Read-only CPU accounting for the sealed AH replay callback.
|
|
|
|
Source/sign lanes are conditional, non-feedback bookkeeping, not additional
|
|
neurons, conductances, energies, or a whole-brain causal intervention. The
|
|
caller supplies anatomical groups and must independently verify the original
|
|
events/state and due/pending coverage. No files, Torch, or simulation entry
|
|
points are used here.
|
|
"""
|
|
from collections.abc import Mapping
|
|
import json
|
|
|
|
import numpy as np
|
|
|
|
|
|
DT_MS = .1
|
|
COEFF_KEYS = ("gd", "c0", "cg", "cv", "threshold", "reset", "seed_volt")
|
|
FLOAT_KEYS = ("old_v", "old_g", "v_integrated", "g_decayed", "v_threshold",
|
|
"v_after_reset", "g_after_reset", "v_pre_apl", "g_pre_apl", "delta_si", "v", "g")
|
|
INT_KEYS = ("ref_before", "ref_after_reset", "ref_pre_apl", "ref")
|
|
BOOL_KEYS = ("available", "request", "request_accepted", "request_rejected", "fired")
|
|
|
|
|
|
def require(value, message):
|
|
if not value:
|
|
raise ValueError(message)
|
|
|
|
|
|
def bit_equal(a, b):
|
|
return (a.dtype == b.dtype and a.shape == b.shape and
|
|
np.array_equal(np.ascontiguousarray(a).view(np.uint8), np.ascontiguousarray(b).view(np.uint8)))
|
|
|
|
|
|
class MBONAccounting:
|
|
"""callback(tick, stage); finish() -> (per_target, per_edge, JSON summary).
|
|
|
|
Group arrays have shape (N,G), sign lanes (N,G,2), with sign order positive,
|
|
negative. Negative increments/integrals retain their negative sign.
|
|
Histories use (T,N), or (T,N,G,2) for history_group_g. Target indices are
|
|
incoming.dst_local, not global IDs; the caller joins the verified target
|
|
list. Float64 lanes retain their own canonical additions; summing lanes
|
|
is not the runtime's interleaved accumulation, so residuals are reported
|
|
without correcting them or assigning them to an anatomical source.
|
|
"""
|
|
|
|
def __init__(self, incoming, groups, group_names, coeff, steps):
|
|
require(isinstance(steps, (int, np.integer)) and not isinstance(steps, (bool, np.bool_))
|
|
and 0 < steps <= np.iinfo(np.int32).max, "steps must be a positive int32-range integer")
|
|
require(isinstance(group_names, tuple) and group_names and
|
|
all(isinstance(x, str) and x for x in group_names) and len(set(group_names)) == len(group_names),
|
|
"group_names must be a nonempty tuple of unique names")
|
|
require(isinstance(coeff, Mapping), "coeff must be a mapping")
|
|
self.coeff = {}
|
|
for key in COEFF_KEYS:
|
|
value = coeff.get(key)
|
|
require(not isinstance(value, (bool, np.bool_)) and
|
|
isinstance(value, (int, float, np.integer, np.float64)) and np.isfinite(value),
|
|
"Missing/non-double/nonfinite coefficient: " + key)
|
|
self.coeff[key] = np.float64(value)
|
|
require(self.coeff["seed_volt"] > self.coeff["threshold"], "seed_volt must exceed threshold")
|
|
require("dt_ms" not in coeff or coeff["dt_ms"] == DT_MS, "AH time step is fixed at .1 ms")
|
|
require(isinstance(incoming.target_count, (int, np.integer)) and 0 < incoming.target_count <= 166700,
|
|
"Invalid incoming target count")
|
|
self.n, self.steps, self.names = int(incoming.target_count), int(steps), group_names
|
|
self.e = len(incoming.edge_id)
|
|
for name, dtype in (("src_global", "int32"), ("dst_local", "int32"),
|
|
("edge_id", "int64"), ("weights", "float64")):
|
|
value = np.asarray(getattr(incoming, name))
|
|
require(value.dtype == np.dtype(dtype) and value.shape == (self.e,), "Invalid incoming " + name)
|
|
setattr(self, name, value.copy())
|
|
require(np.all((self.src_global >= 0) & (self.src_global < 166700)) and
|
|
np.all(self.src_global[1:] >= self.src_global[:-1]), "Noncanonical incoming sources")
|
|
require(np.all(self.edge_id >= 0) and np.all(self.edge_id[1:] > self.edge_id[:-1]), "Noncanonical edge IDs")
|
|
require(np.all((self.dst_local >= 0) & (self.dst_local < self.n)) and
|
|
np.isfinite(self.weights).all(), "Invalid incoming destination/weight")
|
|
codes = np.asarray(groups)
|
|
require(codes.dtype == np.int32 and codes.shape == (self.e,) and
|
|
np.all((codes >= 0) & (codes < len(self.names))), "groups must be int32 per incoming row")
|
|
self.groups = codes.copy()
|
|
self.ticks, self.finished, self.previous = 0, False, None
|
|
shape = (self.n, len(self.names))
|
|
self.counts = {key: np.zeros(shape, np.int64) for key in
|
|
("due_count", "accepted_count", "rejected_count", "accepted_zero_count", "rejected_zero_count")}
|
|
self.increments = {key: np.zeros(shape, np.float64) for key in
|
|
("accepted_positive_V", "accepted_negative_V", "rejected_positive_V", "rejected_negative_V")}
|
|
self.edge_due = np.zeros(self.e, np.int64)
|
|
self.edge_accepted = np.zeros(self.e, np.int64)
|
|
self.spike_count = np.zeros(self.n, np.int32)
|
|
self.first = np.full(self.n, -1, np.int32)
|
|
self.requests = {key: np.zeros(self.n, np.int64) for key in
|
|
("request_count", "request_accepted_count", "request_rejected_count")}
|
|
self.sg = np.zeros(shape + (2,), np.float64)
|
|
self.sv = np.zeros_like(self.sg)
|
|
self.bg = np.zeros(self.n, np.float64)
|
|
self.bv = np.zeros(self.n, np.float64)
|
|
self.integral_g = np.zeros_like(self.sg)
|
|
self.integral_v = np.zeros_like(self.sg)
|
|
self.max_g_residual = np.zeros(self.n, np.float64)
|
|
self.max_v_residual = np.zeros(self.n, np.float64)
|
|
self.history_v = np.empty((self.steps, self.n), np.float64)
|
|
self.history_g = np.empty_like(self.history_v)
|
|
self.history_ref = np.empty((self.steps, self.n), np.int32)
|
|
self.history_group_g = np.empty((self.steps,) + self.sg.shape, np.float64)
|
|
self.history_baseline_g = np.empty_like(self.history_v)
|
|
self.history_g_residual = np.empty_like(self.history_v)
|
|
|
|
def _validate(self, tick, s):
|
|
require(not self.finished and isinstance(tick, (int, np.integer)) and
|
|
not isinstance(tick, (bool, np.bool_)) and tick == self.ticks and tick < self.steps,
|
|
"Missing, duplicate or out-of-order callback")
|
|
require(isinstance(s, Mapping), "stage must be a mapping")
|
|
for keys, dtype in ((FLOAT_KEYS, np.float64), (INT_KEYS, np.int32), (BOOL_KEYS, np.bool_)):
|
|
for key in keys:
|
|
value = s.get(key)
|
|
require(isinstance(value, np.ndarray) and value.shape == (self.n,) and value.dtype == dtype
|
|
and np.isfinite(value).all(), "Missing/invalid stage field: " + key)
|
|
h = s.get("h32")
|
|
require(isinstance(h, np.ndarray) and h.dtype == np.float32 and h.shape == (self.n,)
|
|
and np.all(h == 0) and np.all(s["delta_si"] == 0), "MBON requires zero graded APL h/delta")
|
|
require(np.all((s["ref_before"] >= 0) & (s["ref_before"] <= 22)), "Invalid refractory value")
|
|
if self.previous is not None:
|
|
require(all(bit_equal(s[a], self.previous[b]) for a, b in
|
|
(("old_v", "v"), ("old_g", "g"), ("ref_before", "ref"))), "Discontinuous observed state")
|
|
else:
|
|
require(np.all(s["ref_before"] == 0), "AH replay starts nonrefractory")
|
|
ref = np.maximum(s["ref_before"] - np.int32(1), np.int32(0))
|
|
available = ref == 0
|
|
require(np.array_equal(s["available"], available), "Availability mismatch")
|
|
require(np.array_equal(s["request_accepted"], s["request"] & available) and
|
|
np.array_equal(s["request_rejected"], s["request"] & ~available), "Request guard mismatch")
|
|
c = self.coeff
|
|
v, g = s["old_v"].copy(), s["old_g"].copy()
|
|
g[available] = np.multiply(c["gd"], s["old_g"][available])
|
|
v[available] = np.add(c["c0"], np.add(np.multiply(c["cg"], s["old_g"][available]),
|
|
np.multiply(c["cv"], s["old_v"][available])))
|
|
require(bit_equal(v, s["v_integrated"]) and bit_equal(g, s["g_decayed"]), "Integration mismatch")
|
|
v[s["request_accepted"]] = c["seed_volt"]
|
|
require(bit_equal(v, s["v_threshold"]), "Request override mismatch")
|
|
fired = available & (v > c["threshold"])
|
|
require(np.array_equal(s["fired"], fired), "Strict threshold mismatch")
|
|
v[fired], g[fired], ref[fired] = c["reset"], np.float64(0), np.int32(22)
|
|
for key, value in (("v_after_reset", v), ("g_after_reset", g), ("ref_after_reset", ref)):
|
|
require(bit_equal(s[key], value), "Reset mismatch: " + key)
|
|
due, accepted = s.get("due_edge_indices"), s.get("arrival_accepted")
|
|
require(isinstance(due, np.ndarray) and due.dtype == np.int64 and due.ndim == 1 and
|
|
np.all((due >= 0) & (due < self.e)) and np.all(due[1:] > due[:-1]), "Noncanonical due edge rows")
|
|
require(isinstance(s.get("due_edge_id"), np.ndarray) and
|
|
bit_equal(s["due_edge_id"], self.edge_id[due]), "Due edge-ID mismatch")
|
|
require(isinstance(accepted, np.ndarray) and accepted.dtype == np.bool_ and accepted.shape == due.shape
|
|
and np.array_equal(accepted, ref[self.dst_local[due]] == 0), "Arrival acceptance mismatch")
|
|
require(tick >= 18 or not len(due), "Arrival before delay 18")
|
|
rows = due[accepted]
|
|
np.add.at(g, self.dst_local[rows], self.weights[rows])
|
|
for key, value in (("g_pre_apl", g), ("v_pre_apl", v), ("ref_pre_apl", ref), ("v", v), ("ref", ref)):
|
|
require(bit_equal(s[key], value), "Arrival/final mismatch: " + key)
|
|
require(bit_equal(s["g"], np.subtract(g, s["delta_si"])), "Final g mismatch")
|
|
return due, accepted
|
|
|
|
def _residual(self, voltage, g):
|
|
# No repair: anatomical lanes are never contaminated with roundoff.
|
|
vr = voltage - np.add(self.bv, self.sv.sum(axis=(1, 2)))
|
|
gr = g - np.add(self.bg, self.sg.sum(axis=(1, 2)))
|
|
self.max_v_residual = np.maximum(self.max_v_residual, np.abs(vr))
|
|
self.max_g_residual = np.maximum(self.max_g_residual, np.abs(gr))
|
|
return gr
|
|
|
|
def __call__(self, tick, s):
|
|
due, accepted = self._validate(tick, s)
|
|
if self.previous is None:
|
|
self.bv, self.bg = s["old_v"].copy(), s["old_g"].copy()
|
|
c, available, fired = self.coeff, s["available"], s["fired"]
|
|
self.sv[available] = np.add(np.multiply(c["cg"], self.sg[available]),
|
|
np.multiply(c["cv"], self.sv[available]))
|
|
self.bv[available] = np.add(c["c0"], np.add(np.multiply(c["cg"], self.bg[available]),
|
|
np.multiply(c["cv"], self.bv[available])))
|
|
self.sg[available] = np.multiply(c["gd"], self.sg[available])
|
|
self.bg[available] = np.multiply(c["gd"], self.bg[available])
|
|
self._residual(s["v_integrated"], s["g_decayed"])
|
|
req = s["request_accepted"]
|
|
self.sv[req], self.bv[req] = 0., c["seed_volt"]
|
|
self._residual(s["v_threshold"], s["g_decayed"])
|
|
self.sv[fired], self.sg[fired] = 0., 0.
|
|
self.bv[fired], self.bg[fired] = c["reset"], 0.
|
|
self.first[(self.first < 0) & fired] = np.int32(tick)
|
|
self.spike_count += fired.astype(np.int32)
|
|
for key, stage_key in (("request_count", "request"), ("request_accepted_count", "request_accepted"),
|
|
("request_rejected_count", "request_rejected")):
|
|
self.requests[key] += s[stage_key].astype(np.int64)
|
|
dst, codes, weights = self.dst_local[due], self.groups[due], self.weights[due]
|
|
self.edge_due[due] += 1
|
|
self.edge_accepted[due[accepted]] += 1
|
|
for key, keep in (("due_count", np.ones(len(due), bool)), ("accepted_count", accepted),
|
|
("rejected_count", ~accepted), ("accepted_zero_count", accepted & (weights == 0)),
|
|
("rejected_zero_count", ~accepted & (weights == 0))):
|
|
np.add.at(self.counts[key], (dst[keep], codes[keep]), 1)
|
|
for key, keep in (("accepted_positive_V", accepted & (weights > 0)),
|
|
("accepted_negative_V", accepted & (weights < 0)),
|
|
("rejected_positive_V", ~accepted & (weights > 0)),
|
|
("rejected_negative_V", ~accepted & (weights < 0))):
|
|
np.add.at(self.increments[key], (dst[keep], codes[keep]), weights[keep])
|
|
nonzero = accepted & (weights != 0)
|
|
# add.at is sequential for repeated lane indices, preserving every
|
|
# lane's relative canonical edge order. Never pre-sum edge weights.
|
|
np.add.at(self.sg, (dst[nonzero], codes[nonzero], (weights[nonzero] < 0).astype(np.int32)), weights[nonzero])
|
|
gr = self._residual(s["v"], s["g"])
|
|
self.integral_g += np.multiply(self.sg, np.float64(DT_MS))
|
|
self.integral_v += np.multiply(self.sv, np.float64(DT_MS))
|
|
for value in (self.sg, self.sv, self.bg, self.bv, self.integral_g, self.integral_v,
|
|
self.max_v_residual, self.max_g_residual, *self.increments.values()):
|
|
require(np.isfinite(value).all(), "Nonfinite source accounting")
|
|
self.history_v[tick], self.history_g[tick], self.history_ref[tick] = s["v"], s["g"], s["ref"]
|
|
self.history_group_g[tick] = self.sg
|
|
self.history_baseline_g[tick], self.history_g_residual[tick] = self.bg, gr
|
|
self.previous = {key: s[key].copy() for key in ("v", "g", "ref")}
|
|
self.ticks += 1
|
|
|
|
def finish(self):
|
|
require(not self.finished and self.ticks == self.steps and self.previous is not None,
|
|
"Observer must cover exactly all ticks before finish")
|
|
require(np.array_equal(self.counts["due_count"], self.counts["accepted_count"] + self.counts["rejected_count"]),
|
|
"Group count conservation mismatch")
|
|
for key, edge in (("due_count", self.edge_due), ("accepted_count", self.edge_accepted)):
|
|
grouped = np.zeros_like(self.counts[key])
|
|
np.add.at(grouped, (self.dst_local, self.groups), edge)
|
|
require(np.array_equal(grouped, self.counts[key]), "Per-edge/group count mismatch")
|
|
data = {"target_local": np.arange(self.n, dtype=np.int32), "spike_count": self.spike_count.copy(),
|
|
"first_spike_tick": self.first.copy(), **{key: value.copy() for key, value in self.requests.items()},
|
|
**{key: value.copy() for key, value in self.counts.items()},
|
|
**{key: value.copy() for key, value in self.increments.items()},
|
|
**{"final_" + key: value.copy() for key, value in self.previous.items()},
|
|
"final_group_g": self.sg.copy(), "final_group_v": self.sv.copy(),
|
|
"g_integral_V_ms": self.integral_g.sum(axis=2), "g_sign_integral_V_ms": self.integral_g.copy(),
|
|
"v_sign_integral_V_ms": self.integral_v.copy(),
|
|
"max_g_lane_residual_V": self.max_g_residual.copy(), "max_v_lane_residual_V": self.max_v_residual.copy(),
|
|
"history_v": self.history_v.copy(), "history_g": self.history_g.copy(), "history_ref": self.history_ref.copy(),
|
|
"history_group_g": self.history_group_g.copy(), "history_baseline_g": self.history_baseline_g.copy(),
|
|
"history_g_residual": self.history_g_residual.copy()}
|
|
edges = {"edge_id": self.edge_id.copy(), "source_neuron_idx": self.src_global.copy(),
|
|
"target_local": self.dst_local.copy(), "group": self.groups.copy(), "weight_V": self.weights.copy(),
|
|
"due_count": self.edge_due.copy(), "accepted_count": self.edge_accepted.copy(),
|
|
"rejected_count": self.edge_due - self.edge_accepted}
|
|
groups = {name: {**{key: int(value[:, gi].sum()) for key, value in self.counts.items()},
|
|
**{key: float(value[:, gi].sum()) for key, value in self.increments.items()},
|
|
"g_integral_V_ms": float(self.integral_g[:, gi].sum()),
|
|
"g_positive_integral_V_ms": float(self.integral_g[:, gi, 0].sum()),
|
|
"g_negative_integral_V_ms": float(self.integral_g[:, gi, 1].sum())}
|
|
for gi, name in enumerate(self.names)}
|
|
summary = {"complete": True, "pass_all": True,
|
|
"pass_all_scope": "Observer callback coverage and internal count/state-accounting checks only",
|
|
"steps": self.steps, "callback_count": self.ticks, "targets": self.n, "incoming_edges": self.e,
|
|
"group_names": list(self.names), "sign_order": ["positive", "negative"], "dt_ms": DT_MS,
|
|
"due_count": int(self.edge_due.sum()), "accepted_count": int(self.edge_accepted.sum()),
|
|
"rejected_count": int((self.edge_due - self.edge_accepted).sum()), "groups": groups,
|
|
"max_g_lane_residual_V": float(self.max_g_residual.max(initial=0.)),
|
|
"max_v_lane_residual_V": float(self.max_v_residual.max(initial=0.)),
|
|
"residual_policy": "Observed state minus uncorrected float64 lane sum; no fitted tolerance or anatomical reassignment",
|
|
"integral_contract": "Sum of end-of-tick source g or v lanes times dt_ms; units V*ms, not conductance or energy",
|
|
"increment_contract": "Nominal accepted/rejected signed per-edge increments; negative fields retain negative sign",
|
|
"lane_contract": "Observed availability controls decay/integration; observed request/reset clears voltage/source history; fired clears g; canonical accepted adds only",
|
|
"pending_coverage_checked_by_observer": False, "original_events_verified_by_observer": False,
|
|
"caller_must_verify_original_events_final_and_queue_coverage": True,
|
|
"nonfeedback": True, "parameter_changes": False, "learning": False,
|
|
"functional_transfer_claim": False, "whole_brain_intervention_claim": False}
|
|
# Includes aggregate overflows: a finite per-target array alone must
|
|
# never permit an Infinity/NaN JSON report with pass_all=True.
|
|
json.dumps(summary, allow_nan=False)
|
|
self.finished = True
|
|
return data, edges, summary
|