Files
digital-drosophila/models/male-cns-lif/mb-credit-transfer-audit-v1/mbon_accounting.py
T
2026-09-30 13:58:39 +08:00

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