Seal Phase 4AG author-APL audit and fixed CAL failure
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# Phase4AG: author LIF + frozen legacy graded APL
|
||||
|
||||
Authorized independent hypothesis branch. All old source, raw connectome, signs,
|
||||
W_syn, APL calibration, eligibility-v2 and 14,232-edge mask remain read-only.
|
||||
No learning, gain/threshold changes, new edges, top-K, heldout activity, game or RL.
|
||||
|
||||
Author code provenance is the already archived MIT-licensed
|
||||
https://github.com/philshiu/Drosophila_brain_model at
|
||||
91bdd1e7dcf193f3e7ca5a8933497fcef63b7960; original model.py SHA-256
|
||||
fc45837d7122c6ce2a7f3f2f23c515992e4b232aadb919efabb72337fac88e4e.
|
||||
Associated paper: https://www.nature.com/articles/s41586-024-07763-9.
|
||||
This new MaleCNS/APL combination is not a strict reproduction of that model.
|
||||
|
||||
The declared combination is author float64 LIF plus the exact old float32 graded
|
||||
APL operator after synapses/resets at end. All 5,845 original APL fast out-edges
|
||||
are zeroed only in the effective in-memory model, as in the legacy overlay.
|
||||
Only KC receives graded replacement; known 1,212 non-KC edges remain unreplaced.
|
||||
This existing scope limitation is not silently repaired in a compatibility test.
|
||||
|
||||
Use the original full normalized I[2,166700]/O[2,4064] float32 bytes and original
|
||||
normalizers, even for induced real subgraphs. APL reads actual binary spikes,
|
||||
never requests, and keeps independent float32 state. Preserve unfused operations:
|
||||
state.mul_(float(f32(exp(-.1/20)))).add_(torch.mv(I, spikes_f32));
|
||||
h32=torch.matmul(state,O)*f32(.01). Promote h32 to float64, multiply by float64
|
||||
.001, then separately subtract from SI g. Do not alter TF32/default CuBLAS flags;
|
||||
record them. No added APL delay, drive dt/tau scaling, truncation or clipping.
|
||||
|
||||
APL writes KC g unconditionally after author reset, including refractory KC.
|
||||
Author g integration is frozen during refractory; therefore negative overlay g
|
||||
may accumulate and differs from old dynamics. This is an explicit hypothesis,
|
||||
not an assertion of original author or measured physiology. Guarded writes or
|
||||
reset-before/after changes would be different hypotheses, not fixes to this one.
|
||||
|
||||
Eight fixed metadata/connectivity-selected fixtures cover zero/reset, real PN/KC
|
||||
drive, current actual event feedback, duplicate natural/request event, 18-step
|
||||
arrival vs22-step refractory boundary, full fast APL mask, non-KC scope and mixed
|
||||
precision/high fan-in. No choice is made from runtime responses. Each GPU case
|
||||
has3 independent fresh runs. A Brian2 author oracle starts from its own state,
|
||||
computes its own actual spikes, and uses an isolated standard-I/O legacy-APL
|
||||
worker with independent state. No candidate inhibition trace is fed into Brian.
|
||||
All tick states, events, requests, queue order/guard, APL drive/state/h must agree
|
||||
exactly. The APL component is shared, so this is NOT a fully independent CPU
|
||||
oracle. Independently reconstruct old matrices using frozen initializer AST,
|
||||
replay actual events through original update AST, require exact component bytes;
|
||||
also quantify an independent float64 positive-sum arithmetic error bound. It is
|
||||
not a replacement for exact legacy equivalence. No arithmetic bound is adjusted.
|
||||
|
||||
Only after all fixture gates pass, run two predeclared full-graph probes: first
|
||||
old calibration odor, first original seed, both encoder modes. Compare each with
|
||||
the independently running Brian + isolated APL worker. Save state hashes and sparse
|
||||
events, not a time-by-all-neurons dense spike matrix. If a numerical gate fails,
|
||||
stop CAL; keep diagnostic evidence and do not widen tolerance.
|
||||
|
||||
Then run the complete original6 CAL odors ×5 original seeds ×2 original modes,
|
||||
100ms per run. Retain the ORIGINAL12-odor median for matched-energy encoding,
|
||||
original pn_drive target/RNG column order, original evaluation seeds51101..51105.
|
||||
Save Poisson requests before any trajectory. Only calibration dynamic cases are
|
||||
used; formal A/B and heldout remain unrun. Repeat first seed for every odor/mode.
|
||||
No per-odor pulse, amplitude, timing or threshold search. No learning.
|
||||
|
||||
Recompute the full original4L representation criteria on CAL only: mean KC active
|
||||
fraction .03..09, min>0, max<.5, same-odor Jaccard>different, between Euclidean>
|
||||
within, >=3 pairs whose mean between exceeds THAT PAIR's pooled within distances,
|
||||
both modes. Add finiteness and exact repeat requirements. CAL PASS does not imply
|
||||
heldout generalization, timing/credit specificity, memory or learning usability.
|
||||
If CAL fails stop promotion and report it, not a least-bad candidate.
|
||||
|
||||
Before/after verify SHA-256 of all original230239 files and AA--AF seals; rerun original five
|
||||
deterministic reference trials to new output. Archive code/env/source SHA,
|
||||
matrices/norms/masks/identities, requests and all outputs. No published candidate.
|
||||
|
||||
Technical references (queried2026-09-30): Brian2 2.5.1 refractory documentation
|
||||
https://brian2.readthedocs.io/en/2.5.1/user/refractoriness.html (actual generated code
|
||||
remains authoritative); PyTorch numerical accuracy notes
|
||||
https://docs.pytorch.org/docs/2.14/notes/numerical_accuracy.html (cross-backend
|
||||
floating operations are not assumed bitwise equivalent).
|
||||
@@ -0,0 +1,396 @@
|
||||
"""Complete original Phase 4L representation metrics on fixed CAL inputs only.
|
||||
|
||||
No simulation, fitting, heldout trajectory access, or candidate generation is
|
||||
performed here. The original 4L pair-specific within-odor baseline is retained;
|
||||
the weaker Phase 4P r4 aggregate gate is deliberately not used.
|
||||
"""
|
||||
import csv
|
||||
import hashlib
|
||||
import itertools
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
STATE_DTYPES = {"step": np.int32, "neuron_idx": np.int32, "counts": np.int32,
|
||||
"v": np.float64, "g": np.float64, "ref": np.int32}
|
||||
REQUEST_FIELDS = ("tick", "neuron_idx", "attempted", "accepted", "rejected")
|
||||
APL_FILES = {"apl-drive.float32.npy": 2, "apl-state.float32.npy": 2,
|
||||
"apl-inhibition.float32.npy": 4064}
|
||||
ORIGINAL_CAL_IDS = {
|
||||
"OMSUIQOIVADKIM-RXMQYKEDSA-N", "LABTWGUMFABVFG-ARJAWSKDSA-N",
|
||||
"QNVRIHYSUZMSGM-ZCFIWIBFSA-N", "VLSVVMPLPMNWBH-UHFFFAOYSA-N",
|
||||
"XHIUFYZDQBSEMF-UHFFFAOYSA-N", "OMSUIQOIVADKIM-YFKPBYRVSA-N"}
|
||||
ORIGINAL_MODES = ("physiological", "matched_energy")
|
||||
ORIGINAL_SEEDS = (51101, 51102, 51103, 51104, 51105)
|
||||
HEX_SHA = re.compile(r"[0-9a-f]{64}\Z")
|
||||
_DEFAULT_OUTPUT = object()
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def read_json(path):
|
||||
with Path(path).open(encoding="utf-8") as stream:
|
||||
return json.load(stream)
|
||||
|
||||
|
||||
def file_hash(path):
|
||||
digest = hashlib.sha256()
|
||||
with Path(path).open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(8 << 20), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def raw_hash(value):
|
||||
return hashlib.sha256(np.ascontiguousarray(value).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def arrays_bitwise_equal(first, second):
|
||||
"""Unlike array_equal, also reject different dtypes and signed-zero bits."""
|
||||
return bool(first.dtype == second.dtype and first.shape == second.shape and
|
||||
np.array_equal(np.ascontiguousarray(first).view(np.uint8),
|
||||
np.ascontiguousarray(second).view(np.uint8)))
|
||||
|
||||
|
||||
def pair_metrics(a, b):
|
||||
"""Exact definitions copied from phase4i.pair_metrics/safe_corr/cosine."""
|
||||
x, y = a.astype(np.float64), b.astype(np.float64)
|
||||
aa, bb = a > 0, b > 0
|
||||
union = int(np.count_nonzero(aa | bb))
|
||||
intersection = int(np.count_nonzero(aa & bb))
|
||||
denominator = float(np.linalg.norm(x) * np.linalg.norm(y))
|
||||
cosine = 1.0 if denominator == 0 and np.array_equal(x, y) else (
|
||||
0.0 if denominator == 0 else float(np.dot(x, y) / denominator))
|
||||
correlation = (1.0 if np.array_equal(x, y) else 0.0) if np.std(x) == 0 or np.std(y) == 0 else float(np.corrcoef(x, y)[0, 1])
|
||||
return {"active_intersection": intersection, "active_union": union,
|
||||
"active_jaccard": 1.0 if union == 0 else float(intersection / union),
|
||||
"cosine_similarity": cosine, "correlation": correlation,
|
||||
"euclidean_distance": float(np.linalg.norm(x - y))}
|
||||
|
||||
|
||||
def evaluate_gates(summary, gates):
|
||||
"""Each conjunct in original phase4l.py:239, kept visible for audit."""
|
||||
return {
|
||||
"mean_sparsity_in_closed_interval": bool(float(gates["sparse_min_fraction"]) <=
|
||||
summary["KC_active_fraction_mean"] <= float(gates["sparse_max_fraction"])),
|
||||
"maximum_strictly_below_half": bool(summary["KC_active_fraction_max"] < 0.5),
|
||||
"minimum_strictly_positive": bool(summary["KC_active_fraction_min"] > 0),
|
||||
"same_jaccard_gt_different": bool(summary["same_odor_jaccard_mean"] > summary["different_odor_jaccard_mean"]),
|
||||
"different_euclidean_gt_same": bool(summary["different_odor_euclidean_mean"] > summary["same_odor_euclidean_mean"]),
|
||||
"at_least_original_pair_count": bool(summary["pairwise_separation_pass_count"] >= int(gates["minimum_separable_odor_pairs"]))}
|
||||
|
||||
|
||||
def sample_cv(values):
|
||||
"""Original pandas Series.std uses ddof=1. Undefined diagnostic -> null."""
|
||||
values = np.asarray(values, dtype=np.float64)
|
||||
mean = float(values.mean())
|
||||
return None if mean == 0 else float(values.std(ddof=1) / mean)
|
||||
|
||||
|
||||
def summarize_mode(mode, odor_ids, seeds, runs, gates):
|
||||
within, within_j, between, between_j, between_cos, between_corr = [], [], [], [], [], []
|
||||
pairs = []
|
||||
for odor in odor_ids:
|
||||
arrays = [runs[(mode, odor, seed)]["kc_counts"] for seed in seeds]
|
||||
for first, second in itertools.combinations(arrays, 2):
|
||||
metric = pair_metrics(first, second)
|
||||
within.append(metric["euclidean_distance"])
|
||||
within_j.append(metric["active_jaccard"])
|
||||
for first_odor, second_odor in itertools.combinations(odor_ids, 2):
|
||||
distances = []
|
||||
for seed in seeds:
|
||||
metric = pair_metrics(runs[(mode, first_odor, seed)]["kc_counts"],
|
||||
runs[(mode, second_odor, seed)]["kc_counts"])
|
||||
distances.append(metric["euclidean_distance"])
|
||||
between.append(metric["euclidean_distance"])
|
||||
between_j.append(metric["active_jaccard"])
|
||||
between_cos.append(metric["cosine_similarity"])
|
||||
between_corr.append(metric["correlation"])
|
||||
pair_within = []
|
||||
for odor in (first_odor, second_odor):
|
||||
arrays = [runs[(mode, odor, seed)]["kc_counts"] for seed in seeds]
|
||||
pair_within.extend(pair_metrics(first, second)["euclidean_distance"]
|
||||
for first, second in itertools.combinations(arrays, 2))
|
||||
cross_mean, within_mean = float(np.mean(distances)), float(np.mean(pair_within))
|
||||
pairs.append({"odor_id_a": first_odor, "odor_id_b": second_odor,
|
||||
"between_same_seed_mean": cross_mean, "within_this_pair_mean": within_mean,
|
||||
"between_distance_count": len(distances), "within_distance_count": len(pair_within),
|
||||
"gate": bool(cross_mean > within_mean)})
|
||||
selected = [runs[(mode, odor, seed)] for odor in odor_ids for seed in seeds]
|
||||
fractions = [int(np.count_nonzero(run["kc_counts"])) / len(run["kc_counts"]) for run in selected]
|
||||
summary = {"KC_active_fraction_mean": float(np.mean(fractions)),
|
||||
"KC_active_fraction_min": float(np.min(fractions)), "KC_active_fraction_max": float(np.max(fractions)),
|
||||
"same_odor_jaccard_mean": float(np.mean(within_j)),
|
||||
"different_odor_jaccard_mean": float(np.mean(between_j)),
|
||||
"same_odor_euclidean_mean": float(np.mean(within)),
|
||||
"different_odor_euclidean_mean": float(np.mean(between)),
|
||||
"different_odor_cosine_mean": float(np.mean(between_cos)),
|
||||
"different_odor_correlation_mean": float(np.mean(between_corr)),
|
||||
"within_variance": float(np.var(within)), "between_distance_mean": float(np.mean(between)),
|
||||
"pairwise_separation_pass_count": sum(row["gate"] for row in pairs), "pair_count": len(pairs),
|
||||
"expected_event_count_cv": sample_cv([run["expected_external_events"] for run in selected]),
|
||||
"actual_event_count_cv": sample_cv([run["external_events"] for run in selected]),
|
||||
"within_distance_count": len(within), "between_distance_count": len(between),
|
||||
"pairwise_baseline": "Each pair's two odors: 10+10 within-seed distances; never pooled across all odors",
|
||||
"pairs": pairs}
|
||||
summary["conditions"] = evaluate_gates(summary, gates)
|
||||
summary["gate"] = bool(all(summary["conditions"].values()))
|
||||
return summary
|
||||
|
||||
|
||||
def validate_state(state, neurons, steps):
|
||||
require(set(state) == set(STATE_DTYPES), "state.npz must have exactly the six declared arrays")
|
||||
for name, dtype in STATE_DTYPES.items():
|
||||
value = state[name]
|
||||
require(value.dtype == dtype and value.ndim == 1, "Invalid state dtype/rank: " + name)
|
||||
require(bool(np.isfinite(value).all()), "Nonfinite state: " + name)
|
||||
if name not in ("step", "neuron_idx"):
|
||||
require(value.shape == (neurons,), "Invalid state length: " + name)
|
||||
ticks, nodes = state["step"], state["neuron_idx"]
|
||||
require(ticks.shape == nodes.shape and bool(np.all((ticks >= 0) & (ticks < steps))) and
|
||||
bool(np.all((nodes >= 0) & (nodes < neurons))), "Invalid sparse event indices")
|
||||
keys = ticks.astype(np.int64) * neurons + nodes
|
||||
require(bool(np.all(np.diff(keys) > 0)), "Sparse events not sorted unique by tick/global neuron")
|
||||
require(np.array_equal(np.bincount(nodes, minlength=neurons), state["counts"]), "Counts disagree with sparse events")
|
||||
require(bool(np.all((state["ref"] >= 0) & (state["ref"] <= 22))), "Final refractory outside frozen range")
|
||||
return keys
|
||||
|
||||
|
||||
def read_jsonl(path, steps, kind):
|
||||
with Path(path).open(encoding="utf-8") as source:
|
||||
rows = [json.loads(line) for line in source if line.strip()]
|
||||
require(len(rows) == steps and [row.get("tick") for row in rows] == list(range(steps)),
|
||||
"Missing/duplicate/reordered ticks in " + kind)
|
||||
for row in rows:
|
||||
names = ("v_sha256", "g_sha256", "ref_sha256") if kind == "state hashes" else (
|
||||
"ordered_edge_sha256", "accepted_mask_sha256", "joint_sha256")
|
||||
for name in names:
|
||||
require(isinstance(row.get(name), str) and HEX_SHA.fullmatch(row[name]), "Invalid SHA256 in " + kind)
|
||||
if kind == "queue":
|
||||
require(type(row.get("count")) is int and type(row.get("accepted_count")) is int and
|
||||
0 <= row["accepted_count"] <= row["count"], "Invalid queue counts")
|
||||
return rows
|
||||
|
||||
|
||||
def read_trial(output, case, spec, kc_globals, neurons, edges):
|
||||
summary = read_json(output / "summary.json")
|
||||
steps = spec["steps"]
|
||||
require(summary.get("complete") is True and summary.get("pass_all") is True and
|
||||
summary.get("all_finite") is True and summary.get("steps") == steps and
|
||||
summary.get("completed_ticks") == steps and summary.get("nodes") == neurons and
|
||||
summary.get("edges") == edges and summary.get("precision") == "float64" and
|
||||
summary.get("apl_precision") == "float32" and summary.get("case") == spec["name"] and
|
||||
summary.get("record_history") is False and summary.get("learning") is False,
|
||||
"Incomplete/failed/wrong-scope GPU summary")
|
||||
with np.load(output / "state.npz", allow_pickle=False) as source:
|
||||
state = {name: source[name].copy() for name in source.files}
|
||||
event_keys = validate_state(state, neurons, steps)
|
||||
hashes = read_jsonl(output / "tick-state-hashes.jsonl", steps, "state hashes")
|
||||
for name in ("v", "g", "ref"):
|
||||
require(hashes[-1][name + "_sha256"] == raw_hash(state[name]), "Final state does not match last tick hash: " + name)
|
||||
queues = read_jsonl(output / "queue.jsonl", steps, "queue")
|
||||
apl = {}
|
||||
for name, width in APL_FILES.items():
|
||||
value = np.load(output / name, mmap_mode="r", allow_pickle=False)
|
||||
require(value.dtype == np.float32 and value.shape == (steps, width) and
|
||||
bool(np.isfinite(value).all()) and bool(np.all(value >= 0)), "Invalid full APL trace: " + name)
|
||||
apl[name] = value
|
||||
with (output / "requests.csv").open(encoding="utf-8", newline="") as source:
|
||||
reader = csv.DictReader(source)
|
||||
require(tuple(reader.fieldnames or ()) == REQUEST_FIELDS, "Request CSV schema mismatch")
|
||||
request_rows = [{name: int(row[name]) for name in REQUEST_FIELDS} for row in reader]
|
||||
with np.load(case / "requests.npz", allow_pickle=False) as source:
|
||||
require(set(source.files) == {"tick", "neuron_idx"}, "Input request schema mismatch")
|
||||
expected_ticks, expected_nodes = source["tick"], source["neuron_idx"]
|
||||
require(expected_ticks.dtype == np.int32 and expected_nodes.dtype == np.int32 and
|
||||
expected_ticks.ndim == 1 and expected_ticks.shape == expected_nodes.shape,
|
||||
"Input request dtype/shape mismatch")
|
||||
require(len(request_rows) == len(expected_ticks), "Requested event coverage mismatch")
|
||||
require([row["tick"] for row in request_rows] == expected_ticks.tolist() and
|
||||
[row["neuron_idx"] for row in request_rows] == expected_nodes.tolist(), "Requests differ from frozen input")
|
||||
require(all(row["attempted"] == 1 and row["accepted"] in (0, 1) and
|
||||
row["rejected"] == 1 - row["accepted"] for row in request_rows), "Invalid request acceptance bookkeeping")
|
||||
requested_keys = expected_ticks.astype(np.int64) * neurons + expected_nodes
|
||||
require(bool(np.all(np.diff(requested_keys) > 0)) and bool(np.all((expected_ticks >= 0) & (expected_ticks < steps))) and
|
||||
bool(np.all((expected_nodes >= 0) & (expected_nodes < neurons))), "Input requests not sorted unique/in range")
|
||||
actual_accepted = np.array([row["accepted"] for row in request_rows], dtype=np.uint8)
|
||||
require(np.array_equal(np.isin(requested_keys, event_keys, assume_unique=True), actual_accepted.astype(bool)),
|
||||
"Request accepted/rejected values disagree with actual spikes")
|
||||
accepted = int(actual_accepted.sum())
|
||||
require(summary.get("requested") == len(request_rows) and summary.get("accepted_requests") == accepted and
|
||||
summary.get("rejected_requests") == len(request_rows) - accepted and
|
||||
summary.get("total_spikes") == len(state["step"]), "Summary counts disagree with raw output")
|
||||
component = read_json(output / "apl-component.json")
|
||||
require(component.get("ok") is True and component.get("n_neurons") == neurons and
|
||||
component.get("n_kc") == len(kc_globals) and component.get("n_apl") == 2 and
|
||||
component.get("dt_ms") == .1 and summary.get("apl_component") == component,
|
||||
"APL component provenance mismatch")
|
||||
bundle = Path(spec["apl_bundle_dir"])
|
||||
require(component.get("files_sha256") == {name: file_hash(bundle / name) for name in
|
||||
("input_matrix.npy", "output_matrix.npy", "kc_globals.npy", "apl_globals.npy")}, "APL bundle hashes changed")
|
||||
inputs = summary.get("input_sha256", {})
|
||||
require(inputs.get("spec.json") == file_hash(case / "spec.json") and
|
||||
inputs.get("requests.npz") == file_hash(case / "requests.npz"), "Case input provenance mismatch")
|
||||
kc_counts = state["counts"][kc_globals].copy()
|
||||
kc_event_mask = np.isin(state["neuron_idx"], kc_globals)
|
||||
record = {"case": spec["name"], "mode": spec["mode"], "odor_id": spec["odor_id"],
|
||||
"odor_name": spec["odor_name"], "seed": spec["seed"], "split": "calibration",
|
||||
"KC_active": int(np.count_nonzero(kc_counts)), "KC_active_fraction": float(np.count_nonzero(kc_counts) / len(kc_globals)),
|
||||
"KC_spikes": int(kc_counts.sum()), "KC_first_latency_ms":
|
||||
float(state["step"][kc_event_mask][0] * .1) if bool(np.any(kc_event_mask)) else None,
|
||||
"external_events": accepted, "expected_external_events": float(spec["expected_external_events"]),
|
||||
"total_PN_rate_hz": float(spec["rate_sum_hz"]), "finite": True,
|
||||
"total_spikes": len(state["step"]), "requested": len(request_rows)}
|
||||
require(np.isfinite(record["expected_external_events"]) and record["expected_external_events"] > 0,
|
||||
"CAL expected external events must be finite and positive")
|
||||
return {"state": state, "hashes": hashes, "queues": queues, "apl": apl,
|
||||
"requests": request_rows, "component": component, "kc_counts": kc_counts,
|
||||
"expected_external_events": record["expected_external_events"], "external_events": accepted,
|
||||
"record": record, "output": output}
|
||||
|
||||
|
||||
def exact_repeat(first, repeat):
|
||||
checks = {"state/" + name: arrays_bitwise_equal(first["state"][name], repeat["state"][name]) for name in STATE_DTYPES}
|
||||
checks.update({name: arrays_bitwise_equal(first["apl"][name], repeat["apl"][name]) for name in APL_FILES})
|
||||
checks.update(tick_state_hashes=first["hashes"] == repeat["hashes"],
|
||||
queues=first["queues"] == repeat["queues"], requests=first["requests"] == repeat["requests"],
|
||||
apl_component=first["component"] == repeat["component"])
|
||||
return {"pass_all": bool(all(checks.values())), "checks": checks,
|
||||
"scope": "All six state arrays, every tick all-neuron hashes, APL raw float32 traces, queues, requests and APL environment"}
|
||||
|
||||
|
||||
def validate_inventory(records, cfg):
|
||||
require(tuple(cfg["calibration"]["modes"]) == ORIGINAL_MODES and
|
||||
tuple(cfg["calibration"]["seeds"]) == ORIGINAL_SEEDS and
|
||||
cfg["calibration"]["duration_ms"] == 100.0 and cfg["calibration"]["odor_count"] == 6 and
|
||||
cfg["calibration"]["split"] == "calibration" and
|
||||
cfg["calibration"]["repeat_first_seed_each_odor"] is True, "Frozen CAL design changed")
|
||||
gates = cfg["gates"]
|
||||
require(gates["sparse_min_fraction"] == .03 and gates["sparse_max_fraction"] == .09 and
|
||||
gates["minimum_separable_odor_pairs"] == 3 and gates["max_fraction_strict"] == .5 and
|
||||
gates["min_fraction_strict"] == 0 and all(gates[key] is True for key in
|
||||
("same_jaccard_gt_between", "between_euclidean_gt_within", "both_modes_required", "finite_required", "repeat_exact_required")),
|
||||
"Original representation gates or additional integrity gates changed")
|
||||
expected = {(mode, index, seed) for mode in ORIGINAL_MODES for index in range(6) for seed in ORIGINAL_SEEDS}
|
||||
require(len(records) == 60 and {(row["mode"], row["odor_index"], row["seed"]) for row in records} == expected,
|
||||
"CAL inventory must cover exactly 2 modes x 6 odors x 5 seeds")
|
||||
odors = []
|
||||
for index in range(6):
|
||||
ids = {row["odor_id"] for row in records if row["odor_index"] == index}
|
||||
require(len(ids) == 1, "Odor identity changes across seeds/modes")
|
||||
odors.append(next(iter(ids)))
|
||||
require(set(odors) == ORIGINAL_CAL_IDS, "CAL identities differ from the original frozen six")
|
||||
for row in records:
|
||||
require(row["name"] == f"cal-{row['mode']}-{row['odor_index']:02d}-s{row['seed']}", "Unexpected CAL case name")
|
||||
return odors
|
||||
|
||||
|
||||
def summarize(out_json=_DEFAULT_OUTPUT):
|
||||
"""Recompute; default writes exclusively, out_json=None is strictly read-only."""
|
||||
from context import EXP, ROOT, CFG, check
|
||||
target = EXP / "calibration/summary.json" if out_json is _DEFAULT_OUTPUT else (
|
||||
None if out_json is None else Path(out_json))
|
||||
require(target is None or not target.exists(), "Refusing existing calibration summary")
|
||||
failures, runs, run_records, repeats, provenance = [], {}, [], [], []
|
||||
modes, odors = {}, []
|
||||
prerequisites_ok = False
|
||||
encoder = None
|
||||
records = []
|
||||
try:
|
||||
check()
|
||||
records = read_json(EXP / "protocol/calibration-cases.json")
|
||||
odors = validate_inventory(records, CFG)
|
||||
encoder = read_json(EXP / "protocol/encoder-audit.json")
|
||||
require(encoder["panel_count"] == 12 and encoder["cal_count"] == 6 and
|
||||
encoder["seeds"] == list(ORIGINAL_SEEDS) and encoder["requested_before_any_dynamics"] is True and
|
||||
encoder["formal_AB_run"] is False and encoder["heldout_run"] is False, "Invalid frozen encoder scope")
|
||||
# Only static original-panel metadata is read, never heldout trajectories.
|
||||
import pandas as pd
|
||||
original = pd.read_parquet(ROOT / "experiments/phase4l-olfactory-grounding/odor-encoder/frozen-odor-panel.parquet")
|
||||
require(len(original) == 12 and set(original[original.split == "calibration"].odor_id) == ORIGINAL_CAL_IDS,
|
||||
"Original static panel identity mismatch")
|
||||
require(float(np.median(original.mapped_response_sum)) == encoder["median_mapped_response_sum"],
|
||||
"Matched-energy reference is not the original frozen 12-panel median")
|
||||
kc_globals = np.load(EXP / "apl-bundle/kc_globals.npy", allow_pickle=False)
|
||||
require(kc_globals.dtype == np.int32 and kc_globals.shape == (4064,) and
|
||||
bool(np.all(np.diff(kc_globals) > 0)) and bool(np.all((kc_globals >= 0) & (kc_globals < CFG["neurons"]))),
|
||||
"Invalid frozen KC identities")
|
||||
prerequisites_ok = True
|
||||
except Exception as error:
|
||||
failures.append({"stage": "prerequisites", "type": type(error).__name__, "message": str(error)})
|
||||
if prerequisites_ok:
|
||||
for row in records:
|
||||
name = row["name"]
|
||||
first = None
|
||||
try:
|
||||
case = EXP / "cases" / name
|
||||
spec = read_json(case / "spec.json")
|
||||
require(spec["name"] == name and spec["kind"] == "calibration" and spec["steps"] == 1000 and
|
||||
spec["duration_ms"] == 100.0 and spec["dt_ms"] == .1 and spec["training"] is False and
|
||||
all(spec[key] == row[key] for key in ("mode", "odor_id", "odor_index", "seed")),
|
||||
"Case spec differs from fixed CAL inventory")
|
||||
first = read_trial(EXP / "gpu" / (name + "-r0"), case, spec, kc_globals, CFG["neurons"], CFG["edges"])
|
||||
require(first["record"]["requested"] == row["requests"], "Frozen inventory request count mismatch")
|
||||
runs[(row["mode"], row["odor_id"], row["seed"])] = {key: first[key] for key in
|
||||
("kc_counts", "expected_external_events", "external_events")}
|
||||
run_records.append(first["record"])
|
||||
provenance.append({"case": name, "repeat": 0, "state_npz_sha256": file_hash(first["output"] / "state.npz"),
|
||||
"summary_sha256": file_hash(first["output"] / "summary.json")})
|
||||
except Exception as error:
|
||||
failures.append({"stage": "raw_trial", "case": name, "repeat": 0,
|
||||
"type": type(error).__name__, "message": str(error)})
|
||||
if row["seed"] == ORIGINAL_SEEDS[0]:
|
||||
try:
|
||||
require(first is not None, "Baseline unavailable for exact repeat")
|
||||
repeat = read_trial(EXP / "gpu" / (name + "-r1"), case, spec, kc_globals, CFG["neurons"], CFG["edges"])
|
||||
repeat_report = {"case": name, **exact_repeat(first, repeat)}
|
||||
repeats.append(repeat_report)
|
||||
provenance.append({"case": name, "repeat": 1, "state_npz_sha256": file_hash(repeat["output"] / "state.npz"),
|
||||
"summary_sha256": file_hash(repeat["output"] / "summary.json")})
|
||||
require(repeat_report["pass_all"], "Exact repeat differs: " + ", ".join(
|
||||
key for key, value in repeat_report["checks"].items() if not value))
|
||||
except Exception as error:
|
||||
failures.append({"stage": "exact_repeat", "case": name, "repeat": 1,
|
||||
"type": type(error).__name__, "message": str(error)})
|
||||
if len(runs) == 60:
|
||||
for mode in ORIGINAL_MODES:
|
||||
try:
|
||||
modes[mode] = summarize_mode(mode, odors, ORIGINAL_SEEDS, runs, CFG["gates"])
|
||||
except Exception as error:
|
||||
failures.append({"stage": "representation", "mode": mode,
|
||||
"type": type(error).__name__, "message": str(error)})
|
||||
integrity_pass = bool(prerequisites_ok and len(runs) == 60 and not any(row["stage"] in
|
||||
("prerequisites", "raw_trial") for row in failures))
|
||||
repeat_pass = bool(len(repeats) == 12 and all(row["pass_all"] for row in repeats) and
|
||||
not any(row["stage"] == "exact_repeat" for row in failures))
|
||||
representation_pass = bool(len(modes) == 2 and all(row["gate"] for row in modes.values()))
|
||||
result = {"phase": "4AG", "scope": "Original fixed six CAL odors only; not heldout generalization or learning",
|
||||
"pass_all": bool(integrity_pass and repeat_pass and representation_pass),
|
||||
"numerical_integrity_pass": integrity_pass, "exact_repeat_pass": repeat_pass,
|
||||
"original_representation_gate_pass": representation_pass,
|
||||
"expected_base_trials": 60, "valid_base_trials": len(runs), "expected_repeats": 12,
|
||||
"checked_repeats": len(repeats), "seeds": list(ORIGINAL_SEEDS), "duration_ms": 100.0,
|
||||
"odor_ids": odors, "modes": modes, "runs": run_records, "repeats": repeats,
|
||||
"failures": failures, "input_output_provenance": provenance, "encoder_audit": encoder,
|
||||
"metric_source": "phase4l.representation (full original gate), phase4i.pair_metrics",
|
||||
"finite_gate_note": "Added integrity prerequisite; original 4L recorded finite but omitted it from its gate",
|
||||
"repeat_gate_note": "Additional exact repeat requirement; original metrics and numerical thresholds unchanged",
|
||||
"shared_apl_component_oracle_limit": True, "independent_oracle_equivalence_claim": False,
|
||||
"candidate_generated": False, "training": False, "heldout_dynamic_access": False}
|
||||
if target is not None:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with target.open("x", encoding="utf-8") as output:
|
||||
json.dump(result, output, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
output.write("\n")
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps({key: value for key, value in summarize().items() if key in
|
||||
("pass_all", "numerical_integrity_pass", "exact_repeat_pass", "original_representation_gate_pass")}), flush=True)
|
||||
@@ -0,0 +1,17 @@
|
||||
c14c2d3c35e2b404c516befa66370723fba075516540214ae46e4104ff87e813 PROTOCOL.md
|
||||
edb9cb2d890471fd07afd88c959ef3540b0cd40af490fac3f26396b5e7f5bf9c calibration.py
|
||||
6e1616b086f667fa4e4b5087f500f8080281977ca9922e7b055acfddd2003bd5 compare.py
|
||||
2aa6b4d777ff698018e1a6eaf571215208b8c874a4c848a6d632e2bd778c90e3 config.json
|
||||
b8b1231d958a3ec935ec1d5a36e08cbd9bcd1127f4d4bb243a9051884a0e7a16 context.py
|
||||
b6e3c67e668a971b544df32d76435ab5e537061b9fc2e8d9dedb5d2a726f5782 finalize.py
|
||||
69f5d893f47dafff4ef7c3186c3df0b9014666b32c8f4e943b57016bec92b18a gpu_apl.py
|
||||
21ceec65fe42581dc55872010e0025d660e80358905f1cf4df1dadf341a3b291 legacy_apl_worker.py
|
||||
9b7f3e0b59f2117b94b41a6f542f37e6f7950138d324c905e8e0ac6cb7d17151 oracle_apl.py
|
||||
197ccc6aae70566c4db4d4de17fa032e853b9bd9a04262905d6c71e12a930b6f prepare.py
|
||||
afbe6d5a37c29bda66d38c470093584130d1852e67881c681662aec3c76b33c7 run_case.py
|
||||
84c681030b0032119b5392d0c3f3c136a9c6b0d133eb548c40791a10bbbb8b63 run_suite.py
|
||||
bab3136eecbc5bb09a985d715844ee845cb72b07930ce376bfb39be33bd1845a test_calibration.py
|
||||
1a233f47943e01bbd845ac1dacdb1eec3f88c270c65a5f75bce1cf679b5de097 test_protocol.py
|
||||
5cfc9a4505da6b8fb4e7d2617cdaedaa8475ed46a11ec9bdf0d4d25b1d22c2d3 validate_components.py
|
||||
0d0e72b2f1348f6648124c8c34f75094f76fc5e1a281fbcca130a5fad2f35293 verify_frozen.py
|
||||
6aa01a937f1f3c6504ee92e847cf7b3a884e88aa4ef37a68b0b7d4e7deed4bf2 manifest.json
|
||||
@@ -0,0 +1,508 @@
|
||||
"""Fail-closed byte-exact audit of the shared-APL closed-loop fixtures.
|
||||
|
||||
Comparison is independent of either backend's stored PASS. No tolerance is
|
||||
used, timing fields are never compared, and reports contain no wall timestamp.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
N_GLOBAL = 166700
|
||||
STATE_KEYS = ('step', 'neuron_idx', 'counts', 'v', 'g', 'ref')
|
||||
REQUEST_FIELDS = ('tick', 'neuron_idx', 'attempted', 'accepted', 'rejected')
|
||||
HASH_FIELDS = {'tick', 'v_sha256', 'g_sha256', 'ref_sha256'}
|
||||
QUEUE_FIELDS = {'tick', 'count', 'accepted_count', 'ordered_edge_sha256',
|
||||
'accepted_mask_sha256', 'joint_sha256'}
|
||||
BUNDLE_FILES = ('input_matrix.npy', 'output_matrix.npy', 'kc_globals.npy', 'apl_globals.npy')
|
||||
COEFFICIENTS = ('gd', 'c0', 'cg', 'cv', 'threshold', 'reset', 'seed_volt')
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def file_hash(path):
|
||||
h = hashlib.sha256()
|
||||
with Path(path).open('rb') as source:
|
||||
for block in iter(lambda: source.read(8 << 20), b''):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def raw_hash(array, dtype=None):
|
||||
return hashlib.sha256(np.ascontiguousarray(array, dtype=dtype).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def save(path, report):
|
||||
if path is None:
|
||||
return
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open('x', encoding='utf-8') as target:
|
||||
json.dump(report, target, indent=2, sort_keys=True, allow_nan=False)
|
||||
target.write('\n')
|
||||
|
||||
|
||||
def _read_json(path, used):
|
||||
used.add(Path(path))
|
||||
return json.loads(Path(path).read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def _read_jsonl(path, used):
|
||||
used.add(Path(path))
|
||||
with Path(path).open(encoding='utf-8') as source:
|
||||
return [json.loads(line) for line in source if line.strip()]
|
||||
|
||||
|
||||
def _read_array(path, used):
|
||||
used.add(Path(path))
|
||||
return np.load(path, mmap_mode='r', allow_pickle=False)
|
||||
|
||||
|
||||
def _check(checks, name, condition):
|
||||
checks[name] = bool(condition)
|
||||
|
||||
|
||||
def _bit_mask(a, b):
|
||||
if a.dtype.kind == 'f':
|
||||
unsigned = np.dtype('uint'+str(a.dtype.itemsize*8))
|
||||
return np.ascontiguousarray(a).view(unsigned) != np.ascontiguousarray(b).view(unsigned)
|
||||
return a != b
|
||||
|
||||
|
||||
def _compare_array(name, a, b, checks, differences, dtype=None, shape=None):
|
||||
a, b = np.asarray(a), np.asarray(b)
|
||||
schema = a.shape == b.shape and a.dtype == b.dtype and \
|
||||
(dtype is None or a.dtype == np.dtype(dtype)) and (shape is None or a.shape == shape)
|
||||
_check(checks, name+'.schema', schema)
|
||||
if not schema:
|
||||
differences[name] = {'left_shape': list(a.shape), 'right_shape': list(b.shape),
|
||||
'left_dtype': str(a.dtype), 'right_dtype': str(b.dtype), 'expected_shape': None if shape is None else list(shape),
|
||||
'expected_dtype': None if dtype is None else str(np.dtype(dtype))}
|
||||
_check(checks, name+'.bitwise_exact', False)
|
||||
return False
|
||||
finite = bool(np.isfinite(a).all() and np.isfinite(b).all())
|
||||
_check(checks, name+'.finite', finite)
|
||||
mask = _bit_mask(a, b)
|
||||
exact = not bool(np.any(mask))
|
||||
_check(checks, name+'.bitwise_exact', exact)
|
||||
if not exact or not finite:
|
||||
flat = np.flatnonzero(mask.ravel())
|
||||
detail = {'mismatching_elements': int(len(flat)),
|
||||
'first_positions': [list(map(int, np.unravel_index(int(index), a.shape))) for index in flat[:5]],
|
||||
'left_nonfinite': int(np.count_nonzero(~np.isfinite(a))),
|
||||
'right_nonfinite': int(np.count_nonzero(~np.isfinite(b)))}
|
||||
if finite and a.size:
|
||||
error = np.abs(a.astype(np.float64)-b.astype(np.float64))
|
||||
detail.update(max_abs_error=float(error.max()), mean_abs_error=float(error.mean()))
|
||||
if a.dtype.kind == 'f' and len(flat):
|
||||
unsigned = np.dtype('uint'+str(a.dtype.itemsize*8))
|
||||
av = np.ascontiguousarray(a).view(unsigned).ravel()
|
||||
bv = np.ascontiguousarray(b).view(unsigned).ravel()
|
||||
detail['first_bit_patterns'] = [{'left': hex(int(av[index])), 'right': hex(int(bv[index]))} for index in flat[:5]]
|
||||
differences[name] = detail
|
||||
return exact and finite
|
||||
|
||||
|
||||
def _sha(value):
|
||||
return isinstance(value, str) and re.fullmatch('[0-9a-f]{64}', value) is not None
|
||||
|
||||
|
||||
def _read_requests(path, steps, used):
|
||||
used.add(Path(path))
|
||||
with Path(path).open(encoding='utf-8', newline='') as source:
|
||||
reader = csv.DictReader(source)
|
||||
require(tuple(reader.fieldnames or ()) == REQUEST_FIELDS, 'Request CSV schema mismatch: '+str(path))
|
||||
rows = [{key: int(row[key]) for key in REQUEST_FIELDS} for row in reader]
|
||||
previous = (-1, -1)
|
||||
for row in rows:
|
||||
key = row['tick'], row['neuron_idx']
|
||||
require(previous < key and 0 <= key[0] < steps and 0 <= key[1] < N_GLOBAL,
|
||||
'Request rows must be sorted, unique, and in range: '+str(path))
|
||||
require(row['attempted'] == 1 and row['accepted'] in (0, 1) and row['rejected'] == 1-row['accepted'],
|
||||
'Request accounting is incomplete: '+str(path))
|
||||
previous = key
|
||||
return rows
|
||||
|
||||
|
||||
def _load_run(directory, role, n, edges, steps, record_history, name, used, checks):
|
||||
directory = Path(directory)
|
||||
summary = _read_json(directory/'summary.json', used)
|
||||
_check(checks, role+'.summary_complete', summary.get('complete') is True and summary.get('pass_all') is True and
|
||||
summary.get('steps') == steps and summary.get('completed_ticks') == steps and
|
||||
summary.get('nodes') == n and summary.get('edges') == edges and summary.get('precision') == 'float64')
|
||||
is_cpu = role == 'cpu'
|
||||
_check(checks, role+'.case_name', summary.get('name' if is_cpu else 'case') == name)
|
||||
_check(checks, role+'.history_mode', summary.get('history_recorded' if is_cpu else 'record_history') is record_history)
|
||||
_check(checks, role+'.apl_precision', summary.get('APL_precision' if is_cpu else 'apl_precision') == 'float32')
|
||||
_check(checks, role+'.no_error_marker', not (directory/'error.json').exists())
|
||||
used.add(directory/'state.npz')
|
||||
with np.load(directory/'state.npz', allow_pickle=False) as source:
|
||||
require(set(source.files) == set(STATE_KEYS), 'State NPZ keys changed: '+str(directory))
|
||||
state = {key: source[key].copy() for key in STATE_KEYS}
|
||||
for key, dtype in (('v', 'float64'), ('g', 'float64'), ('ref', 'int32'), ('counts', 'int32')):
|
||||
require(state[key].shape == (n,) and state[key].dtype == np.dtype(dtype), 'Invalid final '+key+': '+str(directory))
|
||||
require(state['step'].dtype == state['neuron_idx'].dtype == np.int32 and state['step'].ndim == 1 and
|
||||
state['step'].shape == state['neuron_idx'].shape, 'Invalid sparse event schema: '+str(directory))
|
||||
require(bool(np.all((state['step'] >= 0) & (state['step'] < steps))) and
|
||||
bool(np.all((state['neuron_idx'] >= 0) & (state['neuron_idx'] < N_GLOBAL))), 'Sparse event range error')
|
||||
event_keys = state['step'].astype(np.int64)*N_GLOBAL+state['neuron_idx']
|
||||
_check(checks, role+'.events_sorted_unique', len(event_keys) < 2 or np.all(np.diff(event_keys) > 0))
|
||||
_check(checks, role+'.count_nonnegative_total', np.all(state['counts'] >= 0) and int(state['counts'].sum()) == len(event_keys))
|
||||
_check(checks, role+'.ref_range', np.all((state['ref'] >= 0) & (state['ref'] <= 22)))
|
||||
_check(checks, role+'.reported_events', summary.get('spikes' if is_cpu else 'total_spikes') == len(event_keys))
|
||||
hashes = _read_jsonl(directory/'tick-state-hashes.jsonl', used)
|
||||
require(len(hashes) == steps and [row.get('tick') for row in hashes] == list(range(steps)), 'Incomplete tick hash coverage')
|
||||
require(all(set(row) == HASH_FIELDS and all(_sha(row[key]) for key in HASH_FIELDS-{'tick'}) for row in hashes),
|
||||
'Invalid tick state hash schema')
|
||||
queues = _read_jsonl(directory/'queue.jsonl', used)
|
||||
require(len(queues) == steps and [row.get('tick') for row in queues] == list(range(steps)), 'Incomplete queue coverage')
|
||||
require(all(set(row) == QUEUE_FIELDS and type(row['count']) is int and type(row['accepted_count']) is int and
|
||||
0 <= row['accepted_count'] <= row['count'] <= edges and
|
||||
all(_sha(row[key]) for key in QUEUE_FIELDS-{'tick', 'count', 'accepted_count'}) for row in queues),
|
||||
'Invalid queue row schema/count/hash')
|
||||
for key in ('v', 'g', 'ref'):
|
||||
_check(checks, role+'.last_hash_'+key, hashes[-1][key+'_sha256'] == raw_hash(state[key], '<i4' if key == 'ref' else '<f8'))
|
||||
requests = _read_requests(directory/'requests.csv', steps, used)
|
||||
requested, accepted = len(requests), sum(row['accepted'] for row in requests)
|
||||
_check(checks, role+'.request_summary', summary.get('requests' if is_cpu else 'requested') == requested and
|
||||
summary.get('applicable_requests' if is_cpu else 'accepted_requests') == accepted and
|
||||
summary.get('ref_rejected_requests' if is_cpu else 'rejected_requests') == requested-accepted)
|
||||
apl = {field: _read_array(directory/('apl-'+field+'.float32.npy'), used) for field in ('drive', 'state', 'inhibition')}
|
||||
for field, width in (('drive', 2), ('state', 2), ('inhibition', 4064)):
|
||||
require(apl[field].dtype == np.float32 and apl[field].shape == (steps, width), 'Invalid APL '+field+' history schema')
|
||||
_check(checks, role+'.apl_positive_'+field, np.all(apl[field] >= 0))
|
||||
histories, pre = {}, {}
|
||||
for field, dtype in (('v', 'float64'), ('g', 'float64'), ('ref', 'int32')):
|
||||
files = (directory/(field+'-history.'+dtype+'.npy'), directory/('preAPL-'+field+'.'+dtype+'.npy'))
|
||||
if record_history:
|
||||
histories[field], pre[field] = (_read_array(path, used) for path in files)
|
||||
require(all(array.dtype == np.dtype(dtype) and array.shape == (steps, n) for array in (histories[field], pre[field])),
|
||||
'Small pre/post history schema mismatch')
|
||||
_check(checks, role+'.history_hashes_'+field, all(row[field+'_sha256'] == raw_hash(histories[field][tick])
|
||||
for tick, row in enumerate(hashes)))
|
||||
_check(checks, role+'.history_final_'+field, not np.any(_bit_mask(histories[field][-1], state[field])))
|
||||
else:
|
||||
_check(checks, role+'.no_dense_'+field, not any(path.exists() for path in files))
|
||||
if is_cpu:
|
||||
component = _read_json(directory/'apl-worker-handshake.json', used)['handshake']
|
||||
else:
|
||||
component = _read_json(directory/'apl-component.json', used)
|
||||
_check(checks, role+'.component_summary_consistent', summary.get('apl_component') == component)
|
||||
return dict(directory=directory, summary=summary, state=state, hashes=hashes, queues=queues,
|
||||
requests=requests, apl=apl, histories=histories, pre=pre, component=component)
|
||||
|
||||
|
||||
def _pair_outputs(left, right, checks, differences, n, steps, record_history):
|
||||
for field in STATE_KEYS:
|
||||
shape = None if field in ('step', 'neuron_idx') else (n,)
|
||||
dtype = 'float64' if field in ('v', 'g') else 'int32'
|
||||
_compare_array('state.'+field, left['state'][field], right['state'][field], checks, differences, dtype, shape)
|
||||
for field, width in (('drive', 2), ('state', 2), ('inhibition', 4064)):
|
||||
_compare_array('apl.'+field, left['apl'][field], right['apl'][field], checks, differences, 'float32', (steps, width))
|
||||
for field in ('hashes', 'queues', 'requests', 'component'):
|
||||
same = left[field] == right[field]
|
||||
_check(checks, field+'.parsed_exact', same)
|
||||
if not same:
|
||||
if isinstance(left[field], list):
|
||||
first = next((i for i, (a, b) in enumerate(zip(left[field], right[field])) if a != b), None)
|
||||
differences[field] = {'left_count': len(left[field]), 'right_count': len(right[field]),
|
||||
'first_differing_row': first,
|
||||
'left_row': None if first is None else left[field][first],
|
||||
'right_row': None if first is None else right[field][first]}
|
||||
else:
|
||||
differences[field] = {'left': left[field], 'right': right[field]}
|
||||
if record_history:
|
||||
for stage in ('histories', 'pre'):
|
||||
for field, dtype in (('v', 'float64'), ('g', 'float64'), ('ref', 'int32')):
|
||||
_compare_array(stage+'.'+field, left[stage][field], right[stage][field], checks, differences, dtype, (steps, n))
|
||||
|
||||
|
||||
def _queue_digest(tick, ids, accepted):
|
||||
ids = np.ascontiguousarray(ids, dtype='<i8').tobytes()
|
||||
mask = np.ascontiguousarray(accepted, dtype=np.uint8).tobytes()
|
||||
return {'tick': tick, 'count': len(mask), 'accepted_count': int(np.count_nonzero(accepted)),
|
||||
'ordered_edge_sha256': hashlib.sha256(ids).hexdigest(),
|
||||
'accepted_mask_sha256': hashlib.sha256(mask).hexdigest(),
|
||||
'joint_sha256': hashlib.sha256(b'phase4af-queue-v1\0'+struct.pack('<qq', tick, len(mask))+ids+mask).hexdigest()}
|
||||
|
||||
|
||||
def _independent_event_accounting(run, role, graph, requested, steps, checks, differences):
|
||||
nodes, ptr = graph['nodes'], graph['ptr']
|
||||
n = len(nodes)
|
||||
state = run['state']
|
||||
local = np.searchsorted(nodes, state['neuron_idx'])
|
||||
require(np.all(local < n) and np.array_equal(nodes[local], state['neuron_idx']), 'Actual spike outside graph identity')
|
||||
_check(checks, role+'.events_per_neuron_counts', np.array_equal(np.bincount(local, minlength=n), state['counts']))
|
||||
actual_request_pairs = [(row['tick'], row['neuron_idx']) for row in run['requests']]
|
||||
_check(checks, role+'.requests_match_frozen_input', actual_request_pairs == requested)
|
||||
require(actual_request_pairs == requested, 'Actual request population/time differs from frozen input')
|
||||
bounds = np.searchsorted(state['step'], np.arange(steps+1))
|
||||
request_ticks = np.asarray([row['tick'] for row in run['requests']], np.int32)
|
||||
rbounds = np.searchsorted(request_ticks, np.arange(steps+1))
|
||||
request_nodes = np.asarray([row['neuron_idx'] for row in run['requests']], np.int32)
|
||||
request_local = np.searchsorted(nodes, request_nodes)
|
||||
require(np.all(request_local < n) and np.array_equal(nodes[request_local], request_nodes), 'Request outside graph')
|
||||
last = np.full(n, -1_000_000_000, np.int64)
|
||||
degrees = np.diff(ptr)
|
||||
expected_counts = np.zeros(steps, np.int64)
|
||||
arrival_tick = state['step'].astype(np.int64)+18
|
||||
valid = arrival_tick < steps
|
||||
np.add.at(expected_counts, arrival_tick[valid], degrees[local[valid]])
|
||||
_check(checks, role+'.delayed_all_edges_count', np.array_equal(expected_counts, [row['count'] for row in run['queues']]))
|
||||
request_ok, delivered_ok, ref_ok, queue_ok, reset_ok = True, True, True, True, True
|
||||
first = {}
|
||||
for tick in range(steps):
|
||||
fired = local[bounds[tick]:bounds[tick+1]]
|
||||
lo, hi = rbounds[tick:tick+2]
|
||||
targets = request_local[lo:hi]
|
||||
accepted = (tick-last[targets]) >= 22
|
||||
for index, ok in zip(range(lo, hi), accepted):
|
||||
row = run['requests'][index]
|
||||
if row['accepted'] != int(ok) or row['rejected'] != int(not ok):
|
||||
request_ok = False
|
||||
first.setdefault('request_ref_mismatch', {'tick': tick, 'neuron_idx': row['neuron_idx']})
|
||||
if not np.isin(targets[accepted], fired).all() or np.isin(targets[~accepted], fired).any():
|
||||
delivered_ok = False
|
||||
first.setdefault('request_delivery_mismatch', tick)
|
||||
last[fired] = tick
|
||||
ref = np.maximum(22-(tick-last), 0).astype(np.int32)
|
||||
if raw_hash(ref, '<i4') != run['hashes'][tick]['ref_sha256']:
|
||||
ref_ok = False
|
||||
first.setdefault('ref_hash_mismatch', tick)
|
||||
if run['pre']:
|
||||
if len(fired) and not np.array_equal(np.ascontiguousarray(run['pre']['g'][tick, fired]).view(np.uint64), np.zeros(len(fired), np.uint64)):
|
||||
reset_ok = False
|
||||
first.setdefault('pre_apl_spike_reset_mismatch', tick)
|
||||
due = local[bounds[tick-18]:bounds[tick-17]] if tick >= 18 else np.empty(0, np.int64)
|
||||
chunks = [np.arange(ptr[source], ptr[source+1], dtype=np.int64) for source in due]
|
||||
indices = np.concatenate(chunks) if chunks else np.empty(0, np.int64)
|
||||
expected_queue = _queue_digest(tick, graph['edge_id'][indices], (ref[graph['dst'][indices]] == 0).astype(np.uint8))
|
||||
if expected_queue != run['queues'][tick]:
|
||||
queue_ok = False
|
||||
first.setdefault('queue_reconstruction_mismatch', tick)
|
||||
_check(checks, role+'.request_refractory_gate', request_ok)
|
||||
_check(checks, role+'.accepted_spikes_rejected_absent', delivered_ok)
|
||||
_check(checks, role+'.all_ref_hashes_from_actual_events', ref_ok)
|
||||
if run['pre']:
|
||||
_check(checks, role+'.small_queue_independent_reconstruction', queue_ok)
|
||||
_check(checks, role+'.spike_g_reset_before_apl', reset_ok)
|
||||
if first:
|
||||
differences[role+'.independent_accounting'] = first
|
||||
|
||||
|
||||
def _check_apl_application(run, role, nodes, kc, checks, differences):
|
||||
if not run['pre']:
|
||||
return
|
||||
common, local_kc, bundle_kc = np.intersect1d(nodes, kc, assume_unique=True, return_indices=True)
|
||||
_compare_array(role+'.apl_keeps_voltage', run['pre']['v'], run['histories']['v'], checks, differences, 'float64')
|
||||
_compare_array(role+'.apl_keeps_ref', run['pre']['ref'], run['histories']['ref'], checks, differences, 'int32')
|
||||
expected = np.array(run['pre']['g'], copy=True)
|
||||
delta = run['apl']['inhibition'][:, bundle_kc].astype(np.float64)*np.float64(.001)
|
||||
expected[:, local_kc] = expected[:, local_kc]-delta
|
||||
_compare_array(role+'.apl_unconditional_g_write', expected, run['histories']['g'], checks, differences, 'float64')
|
||||
|
||||
|
||||
def _finish(report, checks, differences, used, out_json):
|
||||
report['checks'] = {name: bool(value) for name, value in checks.items()}
|
||||
report['differences'] = differences
|
||||
report['pass_all'] = bool(checks) and all(checks.values()) and 'error' not in report
|
||||
report['failed_checks'] = sorted(name for name, value in checks.items() if not value)
|
||||
report['evidence_sha256'] = {str(path): file_hash(path) for path in sorted(used) if path.is_file()}
|
||||
save(out_json, report)
|
||||
return report
|
||||
|
||||
|
||||
def compare(case_dir, gpu_dir, cpu_dir, out_json=None):
|
||||
"""Strict independent closed-loop comparison; out_json=None is read-only."""
|
||||
case_dir, gpu_dir, cpu_dir = Path(case_dir), Path(gpu_dir), Path(cpu_dir)
|
||||
if out_json is not None:
|
||||
require(not Path(out_json).exists(), 'Refusing existing comparison report')
|
||||
checks, differences, used = {}, {}, set()
|
||||
report = {'kind': 'shared-APL-component closed-loop byte-exact comparison', 'pass_all': False,
|
||||
'case_dir': str(case_dir), 'gpu_dir': str(gpu_dir), 'cpu_dir': str(cpu_dir),
|
||||
'tolerance': None, 'signed_zero_included': True, 'timing_fields_compared': False,
|
||||
'independent_CPU_all_components_claim': False}
|
||||
try:
|
||||
spec = _read_json(case_dir/'spec.json', used)
|
||||
steps, history = spec['steps'], spec['record_history']
|
||||
require(type(steps) is int and steps > 0 and type(history) is bool and spec['dt_ms'] == .1,
|
||||
'Invalid fixed case clock/history schema')
|
||||
graph_dir, bundle_dir = Path(spec['graph_dir']), Path(spec['apl_bundle_dir'])
|
||||
graph = {field: _read_array(graph_dir/(field+'.npy'), used) for field in
|
||||
('nodes', 'edge_id', 'src', 'dst', 'ptr', 'signed_raw', 'weights')}
|
||||
nodes = graph['nodes']
|
||||
n, edges = len(nodes), len(graph['edge_id'])
|
||||
require(n > 0 and edges > 0 and nodes.dtype == np.int32 and nodes.ndim == 1 and np.all((nodes >= 0) & (nodes < N_GLOBAL)) and
|
||||
np.all(np.diff(nodes) > 0), 'Invalid canonical node identities')
|
||||
require(graph['ptr'].dtype == np.int64 and graph['ptr'].shape == (n+1,) and graph['ptr'][0] == 0 and
|
||||
graph['ptr'][-1] == edges and np.all(np.diff(graph['ptr']) >= 0), 'Invalid CSR')
|
||||
for key, dtype in (('edge_id', 'int64'), ('src', 'int32'), ('dst', 'int32'), ('signed_raw', 'int64'), ('weights', 'float64')):
|
||||
require(graph[key].dtype == np.dtype(dtype) and graph[key].shape == (edges,), 'Invalid graph '+key)
|
||||
require(np.array_equal(graph['src'], np.repeat(np.arange(n, dtype=np.int32), np.diff(graph['ptr']))) and
|
||||
np.all((graph['dst'] >= 0) & (graph['dst'] < n)), 'Invalid source/destination row mapping')
|
||||
require(history == (n < 512) and (n < 512 or n == N_GLOBAL), 'Invalid small/full history declaration')
|
||||
require(np.all(graph['edge_id'] >= 0) and np.all(np.diff(graph['edge_id']) > 0),
|
||||
'Original canonical edge IDs must be unique and increasing')
|
||||
if n == N_GLOBAL:
|
||||
require(edges == 25582938 and np.array_equal(nodes, np.arange(n, dtype=np.int32)) and
|
||||
np.array_equal(graph['edge_id'], np.arange(edges, dtype=np.int64)), 'Full canonical graph identity changed')
|
||||
bundle = {name: _read_array(bundle_dir/name, used) for name in BUNDLE_FILES}
|
||||
kc, apl_ids = bundle['kc_globals.npy'], bundle['apl_globals.npy']
|
||||
require(kc.dtype == apl_ids.dtype == np.int32 and kc.shape == (4064,) and apl_ids.shape == (2,), 'Invalid APL/KC identities')
|
||||
require(np.all((kc >= 0) & (kc < N_GLOBAL)) and np.all(np.diff(kc) > 0) and
|
||||
np.all((apl_ids >= 0) & (apl_ids < N_GLOBAL)) and np.all(np.diff(apl_ids) > 0) and
|
||||
not np.intersect1d(kc, apl_ids).size, 'APL/KC identities overlap or are invalid')
|
||||
for key, shape in (('input_matrix.npy', (2, N_GLOBAL)), ('output_matrix.npy', (2, 4064))):
|
||||
require(bundle[key].dtype == np.float32 and bundle[key].shape == shape and
|
||||
np.isfinite(bundle[key]).all() and np.all(bundle[key] >= 0), 'Invalid normalized APL matrix')
|
||||
gpu = _load_run(gpu_dir, 'gpu', n, edges, steps, history, spec['name'], used, checks)
|
||||
cpu = _load_run(cpu_dir, 'cpu', n, edges, steps, history, spec['name'], used, checks)
|
||||
_pair_outputs(gpu, cpu, checks, differences, n, steps, history)
|
||||
used.add(case_dir/'requests.npz')
|
||||
with np.load(case_dir/'requests.npz', allow_pickle=False) as source:
|
||||
require(set(source.files) == {'tick', 'neuron_idx'} and source['tick'].dtype == source['neuron_idx'].dtype == np.int32,
|
||||
'Fixed request NPZ schema changed')
|
||||
require(source['tick'].shape == source['neuron_idx'].shape and source['tick'].ndim == 1, 'Invalid sparse request shape')
|
||||
requested = list(zip(map(int, source['tick']), map(int, source['neuron_idx'])))
|
||||
for role, run in (('gpu', gpu), ('cpu', cpu)):
|
||||
_independent_event_accounting(run, role, graph, requested, steps, checks, differences)
|
||||
_check_apl_application(run, role, nodes, kc, checks, differences)
|
||||
|
||||
# Independently derive the effective clone. Original graph weights stay read-only.
|
||||
cpu_weights = _read_array(cpu_dir/'weights.npy', used)
|
||||
require(cpu_weights.dtype == np.float64 and cpu_weights.shape == (edges,), 'CPU effective weight schema mismatch')
|
||||
expected_hash, base_hash, mask_hash = hashlib.sha256(), hashlib.sha256(), hashlib.sha256()
|
||||
weight_ok, mask_count = True, 0
|
||||
for start in range(0, edges, 1 << 20):
|
||||
stop = min(edges, start+(1 << 20))
|
||||
original = np.asarray(graph['weights'][start:stop])
|
||||
require(np.isfinite(original).all(), 'Nonfinite original weights')
|
||||
raw_expected = graph['signed_raw'][start:stop]*(.275*.001)
|
||||
require(not np.any(_bit_mask(original, raw_expected)), 'Original SI weights differ from unaltered signed contacts')
|
||||
mask = np.isin(nodes[graph['src'][start:stop]], apl_ids)
|
||||
expected = original.copy()
|
||||
expected[mask] = np.float64(0)
|
||||
mask_count += int(mask.sum())
|
||||
expected_hash.update(np.ascontiguousarray(expected, dtype='<f8').tobytes())
|
||||
base_hash.update(np.ascontiguousarray(original, dtype='<f8').tobytes())
|
||||
mask_hash.update(np.ascontiguousarray(graph['edge_id'][start:stop][mask], dtype='<i8').tobytes())
|
||||
weight_ok &= not np.any(_bit_mask(expected, cpu_weights[start:stop]))
|
||||
expected_sha = expected_hash.hexdigest()
|
||||
_check(checks, 'cpu.effective_weights_independent_bytes', weight_ok)
|
||||
for role, run in (('gpu', gpu), ('cpu', cpu)):
|
||||
_check(checks, role+'.effective_weights_raw_sha', run['summary'].get('effective_weights_raw_sha256') == expected_sha)
|
||||
_check(checks, role+'.apl_fast_edge_count', run['summary'].get('apl_fast_edges_zeroed') == mask_count)
|
||||
_check(checks, role+'.kc_target_count', run['summary'].get('kc_targets_present') == len(np.intersect1d(nodes, kc)))
|
||||
mask_audit = _read_json(cpu_dir/'apl-mask-audit.json', used)
|
||||
_check(checks, 'cpu.mask_provenance', mask_audit['count'] == mask_count and
|
||||
mask_audit['ordered_original_edge_id_sha256'] == mask_hash.hexdigest() and
|
||||
mask_audit['base_weight_raw_bytes_sha256'] == base_hash.hexdigest() and
|
||||
mask_audit['effective_weights_raw_sha256'] == expected_sha and mask_audit['base_graph_files_modified'] is False)
|
||||
|
||||
case_hashes = {name: file_hash(case_dir/name) for name in ('spec.json', 'requests.npz')}
|
||||
if (case_dir/'arrays.npz').exists():
|
||||
case_hashes['arrays.npz'] = file_hash(case_dir/'arrays.npz')
|
||||
used.add(case_dir/'arrays.npz')
|
||||
baseline_coeff = _read_json(Path(spec['coefficients_path']), used)
|
||||
actual_coeff = _read_json(cpu_dir/'coefficients.json', used)
|
||||
_check(checks, 'cpu.actual_author_coefficients_exact', all(float(actual_coeff[key]).hex() == float(baseline_coeff[key]).hex()
|
||||
for key in COEFFICIENTS))
|
||||
audit = _read_json(cpu_dir/'input-audit.json', used)
|
||||
expected_graph_hashes = {name: file_hash(graph_dir/name) for name in
|
||||
tuple(field+'.npy' for field in graph)+('completeness.csv', 'connectivity.parquet')}
|
||||
bundle_hashes = {name: file_hash(bundle_dir/name) for name in BUNDLE_FILES}
|
||||
_check(checks, 'cpu.read_actual_case', audit['spec'] == spec and audit['case_hashes'] == case_hashes)
|
||||
_check(checks, 'cpu.graph_hashes', audit['graph_hashes'] == expected_graph_hashes)
|
||||
_check(checks, 'cpu.bundle_hashes', audit['bundle_hashes'] == bundle_hashes)
|
||||
_check(checks, 'cpu.no_candidate_trace_input', audit.get('candidate_trace_used_as_input') is False and
|
||||
cpu['summary'].get('candidate_trace_used_as_input') is False)
|
||||
_check(checks, 'cpu.worker_source_hash', audit['legacy_apl_worker_sha256'] == file_hash(spec['legacy_apl_worker']))
|
||||
_check(checks, 'cpu.coefficient_input_hash', audit['coefficients_sha256'] == file_hash(spec['coefficients_path']))
|
||||
gpu_hashes = dict(case_hashes, **{'base_weights.npy': file_hash(graph_dir/'weights.npy'),
|
||||
'coefficients.json': file_hash(spec['coefficients_path'])})
|
||||
_check(checks, 'gpu.input_hashes', gpu['summary']['input_sha256'] == gpu_hashes)
|
||||
for role, run in (('gpu', gpu), ('cpu', cpu)):
|
||||
component = run['component']
|
||||
_check(checks, role+'.actual_component_bundle_bytes', component['files_sha256'] == bundle_hashes and
|
||||
component['array_sha256'] == {name: raw_hash(value) for name, value in bundle.items()})
|
||||
_check(checks, role+'.component_dimensions_clock', component.get('ok') is True and
|
||||
component['protocol'] == 'phase4ag-apl-v1' and component['n_neurons'] == N_GLOBAL and
|
||||
component['n_apl'] == 2 and component['n_kc'] == 4064 and component['dt_ms'] == .1)
|
||||
decay = np.float32(math.exp(-.1/20.0))
|
||||
gain = np.float32(.01)
|
||||
_check(checks, role+'.frozen_apl_constants', component['decay_f32_bits'] == f'{int(decay.view(np.uint32)):08x}' and
|
||||
component['gain_f32_bits'] == f'{int(gain.view(np.uint32)):08x}')
|
||||
env = component['environment']
|
||||
_check(checks, role+'.component_precision_environment', env['cuda_matmul_allow_tf32'] is False and
|
||||
env['float32_matmul_precision'] == 'highest' and env['precision_configuration_changed_by_component'] is False and
|
||||
'4090' in env['device_name'])
|
||||
environment = _read_json(cpu_dir/'environment.json', used)
|
||||
_check(checks, 'cpu.pinned_environment', environment['packages']['brian2'] == '2.5.1' and
|
||||
environment['packages']['numpy'] == '1.22.3' and environment['packages']['sympy'] == '1.12' and
|
||||
environment['Torch_imported_into_Brian'] is False and environment['author_precision'] == 'float64')
|
||||
queue_audit = _read_json(cpu_dir/'queue-audit.json', used)
|
||||
_check(checks, 'cpu.queue_observer_coverage', queue_audit['peek_calls'] == queue_audit['advance_calls'] == steps and
|
||||
queue_audit['same_peek_object_returned'] is True and queue_audit['zero_weight_and_refractory_rejected_edges_retained'] is True)
|
||||
_check(checks, 'cpu.sidecar_complete', cpu['summary']['APL_worker_exit_status'] == 0 and cpu['summary']['APL_worker_ticks'] == steps)
|
||||
report.update(case=spec['name'], steps=steps, nodes=n, edges=edges, record_history=history,
|
||||
total_spikes=len(gpu['state']['step']), active_neurons=int(np.count_nonzero(gpu['state']['counts'])),
|
||||
requested=len(requested), accepted_requests=sum(row['accepted'] for row in gpu['requests']),
|
||||
rejected_requests=sum(row['rejected'] for row in gpu['requests']),
|
||||
arriving_edges=sum(row['count'] for row in gpu['queues']),
|
||||
accepted_arriving_edges=sum(row['accepted_count'] for row in gpu['queues']),
|
||||
apl_fast_edges_zeroed=mask_count, effective_weights_raw_sha256=expected_sha,
|
||||
component_environment=gpu['component']['environment'],
|
||||
limits=['APL Torch primitives are shared; original AST component validation is a separate required gate.',
|
||||
'Small cases preserve and independently hash all pre/post states; full cases compare all-tick instrumented state hashes without saving dense whole-brain histories.',
|
||||
'For full cases intermediate finite checks are performed by the instrumented backends; this file independently inspects final states and all APL traces.',
|
||||
'This is exact agreement for the declared trajectory, not physiological validity or learning.'])
|
||||
except Exception as error:
|
||||
report['error'] = {'type': type(error).__name__, 'message': str(error)}
|
||||
return _finish(report, checks, differences, used, out_json)
|
||||
|
||||
|
||||
def compare_gpu_repeats(first, second, out_json=None):
|
||||
"""Repeat equality only; the first run's independent oracle gate is separate."""
|
||||
first, second = Path(first), Path(second)
|
||||
if out_json is not None:
|
||||
require(not Path(out_json).exists(), 'Refusing existing repeat report')
|
||||
checks, differences, used = {}, {}, set()
|
||||
report = {'kind': 'fresh-GPU-run exact repeat comparison', 'first': str(first), 'second': str(second),
|
||||
'pass_all': False, 'tolerance': None, 'signed_zero_included': True, 'timing_fields_compared': False,
|
||||
'scope': 'Scientific outputs, recording integrity and input hashes; independent case/graph audit belongs to compare()'}
|
||||
try:
|
||||
summary = _read_json(first/'summary.json', used)
|
||||
n, edges, steps, name, history = (summary[key] for key in ('nodes', 'edges', 'steps', 'case', 'record_history'))
|
||||
require(type(n) is int and type(edges) is int and type(steps) is int and n > 0 and edges > 0 and steps > 0 and
|
||||
type(history) is bool and history == (n < 512), 'Invalid repeat dimensions')
|
||||
left = _load_run(first, 'first_gpu', n, edges, steps, history, name, used, checks)
|
||||
right = _load_run(second, 'second_gpu', n, edges, steps, history, name, used, checks)
|
||||
_pair_outputs(left, right, checks, differences, n, steps, history)
|
||||
stable = ('case', 'complete', 'pass_all', 'steps', 'completed_ticks', 'nodes', 'edges', 'precision',
|
||||
'apl_precision', 'state_units', 'total_spikes', 'requested', 'accepted_requests', 'rejected_requests',
|
||||
'arriving_edges', 'accepted_arriving_edges', 'counts_match_sparse_events', 'all_finite', 'record_history',
|
||||
'apl_fast_edges_zeroed', 'kc_targets_present', 'effective_weights_raw_sha256', 'apl_schedule',
|
||||
'apl_si_conversion', 'build', 'sealed_ae_backend_sha256', 'learning', 'input_sha256')
|
||||
for key in stable:
|
||||
_check(checks, 'summary.'+key, key in left['summary'] and key in right['summary'] and
|
||||
left['summary'][key] == right['summary'][key])
|
||||
if not checks['summary.'+key]:
|
||||
differences['summary.'+key] = {'first': left['summary'].get(key), 'second': right['summary'].get(key)}
|
||||
report.update(case=name, steps=steps, nodes=n, edges=edges, record_history=history,
|
||||
total_spikes=len(left['state']['step']),
|
||||
explicit_exclusions=['wall_seconds', 'diagnostic_simulation_seconds', 'build_or_cache_seconds',
|
||||
'peak_allocated_vram_bytes', 'peak_reserved_vram_bytes'])
|
||||
except Exception as error:
|
||||
report['error'] = {'type': type(error).__name__, 'message': str(error)}
|
||||
return _finish(report, checks, differences, used, out_json)
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"phase": "4AG",
|
||||
"name": "author-apl-lab-v1",
|
||||
"human_authorization": "允许独立新模型分支测试动力学假设",
|
||||
"gpu_uuid": "GPU-79f3a85d-adce-7833-7003-6488140ca596",
|
||||
"neurons": 166700,
|
||||
"edges": 25582938,
|
||||
"raw_weight_sum": 124177617,
|
||||
"precision": "float64",
|
||||
"apl_precision": "float32",
|
||||
"apl": {"tau_ms": 20.0, "gain": 0.01, "normalization": 1.0},
|
||||
"hypothesis": "Author LIF plus unchanged legacy graded APL operator at end, after reset, unconditional KC g write. Mixed precision: h32 promoted to float64, then multiplied by float64 .001 SI, then subtracted. Not original author's APL model.",
|
||||
"fixtures": ["zero", "pn_pulse", "kc_ref_boundary", "two_sources", "arrival_collision", "natural_request", "apl_only", "high_fanin"],
|
||||
"fixture_repeats": 3,
|
||||
"calibration": {"split": "calibration", "odor_count": 6, "modes": ["physiological", "matched_energy"], "seeds": [51101,51102,51103,51104,51105], "duration_ms": 100.0, "median_scope": "original frozen 12-odor panel", "full_oracle_probes": ["cal-physiological-00-s51101", "cal-matched_energy-00-s51101"], "repeat_first_seed_each_odor": true},
|
||||
"gates": {"sparse_min_fraction": 0.03, "sparse_max_fraction": 0.09, "max_fraction_strict": 0.5, "min_fraction_strict": 0.0, "minimum_separable_odor_pairs": 3, "same_jaccard_gt_between": true, "between_euclidean_gt_within": true, "both_modes_required": true, "finite_required": true, "repeat_exact_required": true},
|
||||
"training": false,
|
||||
"heldout_dynamic_access": false,
|
||||
"candidate_generation": false,
|
||||
"output_budget_bytes": 16106127360,
|
||||
"scope": "Semantic compatibility and fixed old CAL only. Shared-APL-component closed-loop Brian oracle plus independent arithmetic audit; never claim all-component independent CPU equivalence or learning success. No parameter/input/site/window selection from results."
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT=Path('/data/digital-drosophila')
|
||||
MODEL=Path(__file__).resolve().parent
|
||||
EXP=ROOT/'experiments/phase4ag-author-apl'
|
||||
AF_MODEL=ROOT/'models/male-cns-lif/author-fullgraph-lab-v1'
|
||||
AF_EXP=ROOT/'experiments/phase4af-author-fullgraph'
|
||||
AD_EXP=ROOT/'experiments/phase4ad-author-semantics'
|
||||
CFG=json.loads((MODEL/'config.json').read_text(encoding='utf-8'))
|
||||
|
||||
def digest(path):
|
||||
h=hashlib.sha256()
|
||||
with Path(path).open('rb') as f:
|
||||
for b in iter(lambda:f.read(8<<20),b''):h.update(b)
|
||||
return h.hexdigest()
|
||||
|
||||
def save(path,value):
|
||||
path=Path(path);path.parent.mkdir(parents=True,exist_ok=True)
|
||||
with path.open('x',encoding='utf-8') as f:json.dump(value,f,indent=2,ensure_ascii=False,allow_nan=False);f.write('\n')
|
||||
|
||||
def sources():
|
||||
return {p.name:digest(p) for p in sorted(MODEL.iterdir()) if p.is_file() and p.name not in ('manifest.json','checksums.sha256')}
|
||||
|
||||
def integrity():
|
||||
spec=importlib.util.spec_from_file_location('frozen_af_context',AF_MODEL/'context.py')
|
||||
mod=importlib.util.module_from_spec(spec);spec.loader.exec_module(mod)
|
||||
result=mod.integrity()
|
||||
import sys
|
||||
sys.path.insert(0,str(ROOT/'models/male-cns-lif/terminal-scope-diagnostic-v1'))
|
||||
from diagnostic import manifest_check
|
||||
for p in (AF_MODEL,AF_EXP):result[str(p.relative_to(ROOT))]=manifest_check(p,digest)
|
||||
assert all(r['pass'] for r in result.values())
|
||||
return result
|
||||
|
||||
def check():
|
||||
p=json.loads((EXP/'protocol/preregistered.json').read_text())
|
||||
assert p['sources']==sources(),'Source changed after preregistration'
|
||||
for row in p['inputs']:assert digest(ROOT/row['path'])==row['sha256'],row['path']
|
||||
for row in json.loads((EXP/'protocol/prepared-hashes.json').read_text()):
|
||||
assert digest(EXP/row['path'])==row['sha256'],row['path']
|
||||
return p
|
||||
|
||||
def budget():
|
||||
size=sum(p.stat().st_size for p in EXP.rglob('*') if p.is_file())
|
||||
assert size<CFG['output_budget_bytes'],'New output resource budget exceeded'
|
||||
return size
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Seal a completed gate or preserved failure; never claim learning."""
|
||||
import json
|
||||
from context import ROOT,MODEL,EXP,CFG,check,save,digest,integrity,budget
|
||||
|
||||
|
||||
def seal(root):
|
||||
assert not (root/'manifest.json').exists() and not (root/'checksums.sha256').exists()
|
||||
rows=[dict(path=str(p.relative_to(root)),bytes=p.stat().st_size,sha256=digest(p))
|
||||
for p in sorted(root.rglob('*')) if p.is_file() and '__pycache__' not in p.parts]
|
||||
save(root/'manifest.json',dict(root=str(root),files=rows))
|
||||
with (root/'checksums.sha256').open('x') as f:
|
||||
for row in rows:f.write(row['sha256']+' '+row['path']+'\n')
|
||||
f.write(digest(root/'manifest.json')+' manifest.json\n')
|
||||
|
||||
|
||||
def main():
|
||||
from compare import compare,compare_gpu_repeats
|
||||
check();budget();comparisons=[];repeats={};components={}
|
||||
for p in sorted((EXP/'audit').glob('compare-*.json')):
|
||||
name=p.stem[len('compare-'):]
|
||||
actual=compare(EXP/'cases'/name,EXP/'gpu'/(name+'-r0'),EXP/'oracle'/name,None)
|
||||
assert actual==json.loads(p.read_text()),'Changed raw closed-loop evidence: '+name
|
||||
comparisons.append(dict(case=name,pass_all=actual['pass_all']))
|
||||
for p in sorted((EXP/'audit').glob('repeat-*.json')):
|
||||
name,repeat=p.stem[len('repeat-'):].rsplit('-r',1)
|
||||
actual=compare_gpu_repeats(EXP/'gpu'/(name+'-r0'),EXP/'gpu'/f'{name}-r{repeat}',None)
|
||||
assert actual==json.loads(p.read_text()),'Changed raw repeat evidence: '+name
|
||||
repeats[f'{name}-r{repeat}']=actual['pass_all']
|
||||
for p in (EXP/'component-audit').glob('*.json'):
|
||||
result=json.loads(p.read_text())
|
||||
components[p.stem]=result['pass_all']
|
||||
for filename,sha in result.get('input_sha256',{}).items():
|
||||
assert digest(EXP/'gpu'/(p.stem+'-r0')/filename)==sha
|
||||
gates={}
|
||||
for name in ('fixtures','probes','calibration'):
|
||||
path=EXP/'audit'/(name+'-gate.json')
|
||||
gates[name]=json.loads(path.read_text()) if path.exists() else None
|
||||
calpath=EXP/'calibration/summary.json'
|
||||
cal=json.loads(calpath.read_text()) if calpath.exists() else None
|
||||
if cal is not None:
|
||||
from calibration import summarize
|
||||
assert summarize(out_json=None)==cal,'CAL raw evidence changed'
|
||||
frozen=json.loads((EXP/'regression/frozen-five.json').read_text())
|
||||
after=integrity();save(EXP/'regression/old-integrity-after.json',after)
|
||||
stops=[dict(path=str(p.relative_to(EXP)),report=json.loads(p.read_text())) for p in sorted((EXP/'audit').glob('stopped-*.json'))]
|
||||
expected_cases=CFG['fixtures']+CFG['calibration']['full_oracle_probes']
|
||||
expected_repeats=[f'{name}-r{r}' for name in CFG['fixtures'] for r in range(1,CFG['fixture_repeats'])]
|
||||
compared={x['case']:x['pass_all'] for x in comparisons}
|
||||
missing=dict(comparisons=sorted(set(expected_cases)-set(compared)),
|
||||
components=sorted(set(expected_cases)-set(components)),repeats=sorted(set(expected_repeats)-set(repeats)))
|
||||
evidence_ok=(set(compared)==set(components)==set(expected_cases) and set(repeats)==set(expected_repeats) and
|
||||
all(compared.values()) and all(components.values()) and all(repeats.values()))
|
||||
numerical=evidence_ok and all(gates[x] is not None and gates[x]['pass_all'] for x in ('fixtures','probes'))
|
||||
representation=bool(cal is not None and cal['pass_all'])
|
||||
if numerical:status='PASS-FIXED-CAL-REPRESENTATION' if representation else 'FAIL-FIXED-CAL-REPRESENTATION' if cal is not None else 'INCOMPLETE-CAL-EXECUTION'
|
||||
else:status='FAIL-NUMERICAL-OR-AUDIT-GATE' if any(not p for group in (compared,components,repeats) for p in group.values()) else 'INCOMPLETE-NUMERICAL-EXECUTION'
|
||||
oldpass=bool(frozen['pass_all'] and all(r['pass'] for r in after.values()))
|
||||
if not oldpass:status='FAIL-FROZEN-INTEGRITY-OR-REGRESSION'
|
||||
measurements=[]
|
||||
for p in sorted((EXP/'gpu').glob('*/summary.json')):
|
||||
s=json.loads(p.read_text())
|
||||
measurements.append({k:s[k] for k in ('case','total_spikes','accepted_requests','nodes','edges','diagnostic_simulation_seconds','peak_allocated_vram_bytes')})
|
||||
result=dict(status=status,numerical_gate_pass=bool(numerical),fixed_CAL_representation_pass=representation,
|
||||
gates=gates,closed_loop_comparisons=comparisons,component_results=components,repeat_results=repeats,
|
||||
missing_numerical_evidence=missing,calibration=cal,stopped=stops,
|
||||
old_frozen_integrity_and_five_reference_pass=oldpass,measurements=measurements,
|
||||
unchanged_APL_parameters=CFG['apl'],author_LIF_precision='float64',legacy_APL_precision='float32',
|
||||
explicit_hypothesis=CFG['hypothesis'],APL_nonKC_fast_edges_unreplaced=1212,
|
||||
shared_APL_component_oracle_limitation=True,whole_brain_universal_equivalence_claim=False,
|
||||
heldout_generalization_tested=False,formal_AB_tested=False,learning_achieved=False,candidate_generated=False,
|
||||
payload_bytes_before_manifest=budget(),
|
||||
next_gate='Independent seeds/novel-odor representation, then credit specificity; no learning promotion from CAL' if numerical and representation else
|
||||
'Audit first failure in this explicitly declared hypothesis; no gain search, no heldout tuning, no conditioning')
|
||||
save(EXP/'audit/final-status.json',result)
|
||||
lines=['# Phase 4AG: author LIF + unchanged legacy graded APL','',f'Status: `{status}`.','',
|
||||
f'Numerical closed-loop gate: {numerical}; original complete fixed six-CAL gate: {representation}.',
|
||||
'The independent Brian LIF and candidate use independent state but share the APL arithmetic component. Original frozen APL AST replay and a float64 error audit are separately recorded.',
|
||||
'Author LIF uses float64; APL retains float32, tau20ms/gain0.01/normalization1, full original matrices and order. No learning/parameter search.',
|
||||
'The explicitly hypothesized unconditional end-APL write includes refractory KC. Original fast APL mask leaves1,212 non-KC edges unreplaced; this scope limitation was not silently fixed.',
|
||||
f'Old checksums and five frozen reference trials: {oldpass}.',
|
||||
f'Successfully compared cases: {comparisons}.',
|
||||
'Detailed CAL, exact comparisons, component errors, event/request traces and execution exceptions are linked from audit/final-status.json.',
|
||||
'Wall/VRAM here include per-tick CPU synchronization, IPC and hashes. They are diagnostic costs, not runtime-performance benchmarks.',
|
||||
'No heldout dynamic outputs, formal A/B, learning, game, reward, decoder or new candidate.',
|
||||
'Next gate: '+result['next_gate'],'']
|
||||
with (EXP/'REPORT.md').open('x',encoding='utf-8') as f:f.write('\n'.join(lines))
|
||||
check();seal(MODEL);seal(EXP);print(status,flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,346 @@
|
||||
"""Diagnostic author-float64 LIF plus frozen legacy-float32 APL overlay.
|
||||
|
||||
The sealed Phase 4AE CUDA operators are reused without modification. APL is
|
||||
an explicitly mixed-precision engineering overlay, not an author-model claim.
|
||||
All CUDA/CPU synchronization here is diagnostic; no performance claim is made.
|
||||
"""
|
||||
from functools import lru_cache
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import time
|
||||
import traceback
|
||||
import types
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from legacy_apl_worker import LegacyAPL, N_NEURONS, file_sha256, array_sha256
|
||||
|
||||
|
||||
SOURCE_ROOT = Path(__file__).resolve().parent
|
||||
AE_MODEL = SOURCE_ROOT.parent / "author-runtime-lab-v1"
|
||||
AF_MODEL = SOURCE_ROOT.parent / "author-fullgraph-lab-v1"
|
||||
COEFFICIENT_NAMES = ("gd", "c0", "cg", "cv", "threshold", "reset", "seed_volt")
|
||||
REQUEST_FIELDS = ("tick", "neuron_idx", "attempted", "accepted", "rejected")
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def write_json(path, value):
|
||||
with Path(path).open("x", encoding="utf-8") as output:
|
||||
json.dump(value, output, indent=2, allow_nan=False)
|
||||
output.write("\n")
|
||||
|
||||
|
||||
def raw_hash(value, dtype):
|
||||
return hashlib.sha256(np.ascontiguousarray(value, dtype=dtype).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def load_source(path, name):
|
||||
module = types.ModuleType(name)
|
||||
module.__file__ = str(path)
|
||||
exec(compile(Path(path).read_bytes(), str(path), "exec"), module.__dict__)
|
||||
return module
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def sealed_backend():
|
||||
return load_source(AE_MODEL / "gpu_backend.py", "phase4ag_sealed_ae_backend")
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def queue_function():
|
||||
return load_source(AF_MODEL / "oracle_full.py", "phase4ag_sealed_af_queue").queue_summary
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def load_graph(graph_directory):
|
||||
root = Path(graph_directory)
|
||||
require(root.is_absolute(), "graph_dir must be absolute")
|
||||
graph = {}
|
||||
for name, dtype in (("nodes", np.int32), ("edge_id", np.int64), ("src", np.int32),
|
||||
("dst", np.int32), ("signed_raw", np.int64), ("ptr", np.int64),
|
||||
("weights", np.float64)):
|
||||
value = np.load(root / (name + ".npy"), mmap_mode="r", allow_pickle=False)
|
||||
require(value.dtype == dtype and value.ndim == 1, "Graph dtype/rank mismatch: " + name)
|
||||
graph[name] = value
|
||||
nodes, edges = graph["nodes"], graph["edge_id"]
|
||||
n, e = len(nodes), len(edges)
|
||||
require(0 < n <= N_NEURONS and bool(np.all((nodes >= 0) & (nodes < N_NEURONS))) and
|
||||
bool(np.all(np.diff(nodes) > 0)), "Global node IDs must be sorted unique")
|
||||
require(n < 512 or (n == N_NEURONS and np.array_equal(nodes, np.arange(N_NEURONS, dtype=np.int32))),
|
||||
"Only small fixtures or the intact frozen full graph are permitted")
|
||||
require(n != N_NEURONS or e == 25582938, "Full graph edge count changed")
|
||||
for name in ("src", "dst", "signed_raw", "weights"):
|
||||
require(graph[name].shape == (e,), "Graph edge array length mismatch: " + name)
|
||||
ptr = graph["ptr"]
|
||||
require(ptr.shape == (n + 1,) and ptr[0] == 0 and ptr[-1] == e and
|
||||
bool(np.all(np.diff(ptr) >= 0)), "Invalid graph CSR")
|
||||
require(np.array_equal(graph["src"], np.repeat(np.arange(n, dtype=np.int32), np.diff(ptr))),
|
||||
"Source order does not match CSR")
|
||||
# Global edge IDs need not equal local row indices in a fixture.
|
||||
for start in range(0, e, 1 << 20):
|
||||
stop = min(start + (1 << 20), e)
|
||||
require(bool(np.all((graph["dst"][start:stop] >= 0) & (graph["dst"][start:stop] < n))),
|
||||
"Local destination out of range")
|
||||
require(bool(np.isfinite(graph["weights"][start:stop]).all()), "Nonfinite base SI weights")
|
||||
require(np.array_equal(np.sign(graph["weights"][start:stop]), np.sign(graph["signed_raw"][start:stop])),
|
||||
"Base weight sign/zero pattern differs from frozen raw contacts")
|
||||
lo = max(0, start - 1)
|
||||
require(bool(np.all(edges[start:stop] >= 0)) and bool(np.all(np.diff(edges[lo:stop]) > 0)),
|
||||
"Global edge IDs must retain canonical increasing source/edge order")
|
||||
return graph
|
||||
|
||||
|
||||
def load_inputs(case_dir):
|
||||
spec = json.loads((case_dir / "spec.json").read_text(encoding="utf-8"))
|
||||
for name in ("name", "steps", "dt_ms", "duration_ms", "model", "graph_dir", "apl",
|
||||
"apl_bundle_dir", "coefficients_path", "record_history"):
|
||||
require(name in spec, "Missing case field: " + name)
|
||||
steps = spec["steps"]
|
||||
require(type(steps) is int and 0 < steps <= 1000 and float(spec["dt_ms"]) == .1 and
|
||||
float(spec["duration_ms"]) == steps * .1, "Unexpected steps/dt/duration")
|
||||
require(type(spec["record_history"]) is bool, "record_history must be Boolean")
|
||||
require(spec["apl"] == {"tau_ms": 20, "gain": .01, "normalization": 1}, "Frozen APL parameters changed")
|
||||
for name in ("graph_dir", "apl_bundle_dir", "coefficients_path"):
|
||||
require(Path(spec[name]).is_absolute(), "Input path must be absolute: " + name)
|
||||
model = spec["model"]
|
||||
for name in ("resting_potential_mv", "threshold_mv", "reset_potential_mv", "synaptic_delay_ms",
|
||||
"refractory_ms", "w_syn_mv_per_anatomical_synapse"):
|
||||
require(name in model and math.isfinite(float(model[name])), "Invalid model field: " + name)
|
||||
require(float(model["synaptic_delay_ms"]) == 1.8 and float(model["refractory_ms"]) == 2.2 and
|
||||
float(model["w_syn_mv_per_anatomical_synapse"]) == .275, "Frozen delay/ref/W_syn changed")
|
||||
coeff = json.loads(Path(spec["coefficients_path"]).read_text(encoding="utf-8"))
|
||||
for name in COEFFICIENT_NAMES:
|
||||
require(name in coeff and math.isfinite(float(coeff[name])), "Invalid author coefficient: " + name)
|
||||
require(coeff["threshold"] == float(model["threshold_mv"]) * .001 and
|
||||
coeff["reset"] == float(model["reset_potential_mv"]) * .001, "Coefficient threshold/reset mismatch")
|
||||
graph = load_graph(str(Path(spec["graph_dir"]).resolve()))
|
||||
n = len(graph["nodes"])
|
||||
require(spec["record_history"] == (n < 512), "Fixtures need dense history; full graph must not duplicate dense histories")
|
||||
with np.load(case_dir / "requests.npz", allow_pickle=False) as source:
|
||||
require(set(source.files) == {"tick", "neuron_idx"}, "Request sparse schema changed")
|
||||
ticks, requested = source["tick"].copy(), source["neuron_idx"].copy()
|
||||
require(ticks.dtype == np.int32 and requested.dtype == np.int32 and ticks.ndim == 1 and
|
||||
ticks.shape == requested.shape, "Invalid sparse request dtype/shape")
|
||||
require(bool(np.all((ticks >= 0) & (ticks < steps))), "Request tick outside trial")
|
||||
if len(ticks) > 1:
|
||||
require(bool(np.all((ticks[1:] > ticks[:-1]) |
|
||||
((ticks[1:] == ticks[:-1]) & (requested[1:] > requested[:-1])))),
|
||||
"Requests must be sorted unique by tick/global neuron")
|
||||
local = np.searchsorted(graph["nodes"], requested)
|
||||
require(bool(np.all(local < n)) and np.array_equal(graph["nodes"][local], requested),
|
||||
"Requested neuron absent from graph")
|
||||
initial = {"initial_v_mv": np.full(n, float(model["resting_potential_mv"]), dtype=np.float64),
|
||||
"initial_g_mv": np.zeros(n, dtype=np.float64)}
|
||||
initial_path = case_dir / "arrays.npz"
|
||||
if initial_path.exists():
|
||||
with np.load(initial_path, allow_pickle=False) as source:
|
||||
require(set(source.files).issubset(initial), "Unexpected initial-state array")
|
||||
for name in source.files:
|
||||
value = source[name]
|
||||
require(value.dtype == np.float64 and value.shape == (n,) and bool(np.isfinite(value).all()),
|
||||
"Invalid initial state: " + name)
|
||||
initial[name] = value.copy()
|
||||
return spec, graph, coeff, ticks, requested, local, initial
|
||||
|
||||
|
||||
def new_array(path, dtype, shape):
|
||||
require(not Path(path).exists(), "Refusing existing output: " + str(path))
|
||||
return np.lib.format.open_memmap(path, mode="w+", dtype=dtype, shape=shape)
|
||||
|
||||
|
||||
def run_gpu(case_dir, out_dir, build_root):
|
||||
"""Run fresh candidate state; never consume oracle/candidate APL traces.
|
||||
|
||||
pass_all denotes internal recording/finite integrity only. Independent
|
||||
comparison and the original complete representation gate are separate.
|
||||
"""
|
||||
case_dir, out_dir = Path(case_dir), Path(out_dir)
|
||||
require(not out_dir.exists(), "Refusing existing output directory: " + str(out_dir))
|
||||
out_dir.mkdir(parents=True, exist_ok=False)
|
||||
start, tick, completed = time.perf_counter(), -1, 0
|
||||
maps = {}
|
||||
try:
|
||||
spec, graph, coeff, request_ticks, request_nodes, request_local, initial = load_inputs(case_dir)
|
||||
require(torch.cuda.is_available() and torch.cuda.device_count() == 1 and
|
||||
"4090" in torch.cuda.get_device_name(), "Only the declared single visible RTX4090 is permitted")
|
||||
ext, build = sealed_backend().build_extension(build_root)
|
||||
build_done = time.perf_counter()
|
||||
apl = LegacyAPL(spec["apl_bundle_dir"], spec["dt_ms"])
|
||||
write_json(out_dir / "apl-component.json", apl.metadata)
|
||||
device = apl.device
|
||||
torch.cuda.reset_peak_memory_stats(device)
|
||||
n, e, steps = len(graph["nodes"]), len(graph["edge_id"]), spec["steps"]
|
||||
|
||||
def upload(value, dtype):
|
||||
return torch.from_numpy(np.array(value, copy=True, order="C")).to(device=device, dtype=dtype).contiguous()
|
||||
|
||||
indptr, dst = upload(graph["ptr"], torch.int64), upload(graph["dst"], torch.int32)
|
||||
global_nodes_gpu = upload(graph["nodes"], torch.int64)
|
||||
base_weights = upload(graph["weights"], torch.float64)
|
||||
weights = base_weights.clone()
|
||||
del base_weights
|
||||
local_apl = np.flatnonzero(np.isin(graph["nodes"], apl.apl_globals)).astype(np.int64)
|
||||
apl_out_mask = np.isin(graph["src"], local_apl)
|
||||
if bool(np.any(apl_out_mask)):
|
||||
weights[upload(apl_out_mask, torch.bool)] = 0.0
|
||||
effective_hash = hashlib.sha256()
|
||||
for offset in range(0, e, 1 << 20):
|
||||
effective_hash.update(weights[offset:offset + (1 << 20)].cpu().numpy().astype("<f8", copy=False).tobytes())
|
||||
common_kc, local_kc, full_kc = np.intersect1d(graph["nodes"], apl.kc_globals, assume_unique=True, return_indices=True)
|
||||
kc_local_gpu, kc_full_gpu = upload(local_kc, torch.int64), upload(full_kc, torch.int64)
|
||||
v = upload(initial["initial_v_mv"] * .001, torch.float64)
|
||||
g = upload(initial["initial_g_mv"] * .001, torch.float64)
|
||||
ref = torch.zeros(n, device=device, dtype=torch.int32)
|
||||
counts = torch.zeros(n, device=device, dtype=torch.int32)
|
||||
requests = torch.zeros(n, device=device, dtype=torch.uint8)
|
||||
ring = torch.zeros((19, n), device=device, dtype=torch.uint8)
|
||||
for name, width in (("drive", 2), ("state", 2), ("inhibition", 4064)):
|
||||
maps["apl_" + name] = new_array(out_dir / ("apl-" + name + ".float32.npy"), np.float32, (steps, width))
|
||||
if spec["record_history"]:
|
||||
for name, dtype in (("v", np.float64), ("g", np.float64), ("ref", np.int32)):
|
||||
maps[name] = new_array(out_dir / (name + "-history." + np.dtype(dtype).name + ".npy"), dtype, (steps, n))
|
||||
maps["pre_" + name] = new_array(out_dir / ("preAPL-" + name + "." + np.dtype(dtype).name + ".npy"), dtype, (steps, n))
|
||||
event_steps, event_nodes = [], []
|
||||
accepted_requests_total, due_total, accepted_due_total = 0, 0, 0
|
||||
queue_summary = queue_function()
|
||||
simulation_start = time.perf_counter()
|
||||
with (out_dir / "queue.jsonl").open("x", encoding="utf-8", buffering=1) as queue_output, \
|
||||
(out_dir / "tick-state-hashes.jsonl").open("x", encoding="utf-8", buffering=1) as hash_output, \
|
||||
(out_dir / "requests.csv").open("x", encoding="utf-8", newline="", buffering=1) as request_output:
|
||||
writer = csv.DictWriter(request_output, fieldnames=REQUEST_FIELDS)
|
||||
writer.writeheader()
|
||||
for tick in range(steps):
|
||||
requests.zero_()
|
||||
begin, end = np.searchsorted(request_ticks, (tick, tick + 1))
|
||||
if end > begin:
|
||||
requested_local_gpu = upload(request_local[begin:end], torch.int64)
|
||||
requests[requested_local_gpu] = 1
|
||||
accepted = (ref[requested_local_gpu] <= 1).to(torch.uint8).cpu().numpy()
|
||||
accepted_requests_total += int(accepted.sum())
|
||||
writer.writerows({"tick": tick, "neuron_idx": int(node), "attempted": 1,
|
||||
"accepted": int(ok), "rejected": 1 - int(ok)}
|
||||
for node, ok in zip(request_nodes[begin:end], accepted))
|
||||
spikes = ring[tick % 19]
|
||||
ext.state_step(v, g, ref, requests, spikes, counts,
|
||||
*[float(coeff[name]) for name in COEFFICIENT_NAMES], 22)
|
||||
expanded_global = np.empty(0, dtype=np.int64)
|
||||
accepted_cpu = np.empty(0, dtype=np.uint8)
|
||||
if tick >= 18:
|
||||
due = ring[(tick - 18) % 19]
|
||||
sources = torch.nonzero(due, as_tuple=False).flatten().contiguous()
|
||||
if sources.numel():
|
||||
degrees = indptr[sources + 1] - indptr[sources]
|
||||
offsets = (torch.cumsum(degrees, dim=0) - degrees).contiguous()
|
||||
edge_count = int(degrees.sum().item())
|
||||
expanded = torch.empty(edge_count, device=device, dtype=torch.int64)
|
||||
ext.fill_edges(indptr, sources, offsets, expanded)
|
||||
if edge_count:
|
||||
destination = dst[expanded]
|
||||
# Actual active fanout order, BEFORE stable target grouping.
|
||||
expanded_global = graph["edge_id"][expanded.cpu().numpy()].copy()
|
||||
accepted_cpu = (ref[destination.long()] == 0).to(torch.uint8).cpu().numpy().copy()
|
||||
order = torch.argsort(destination, stable=True)
|
||||
ordered_edges, ordered_targets = expanded[order].contiguous(), destination[order]
|
||||
boundary = torch.ones(edge_count, device=device, dtype=torch.bool)
|
||||
boundary[1:] = ordered_targets[1:] != ordered_targets[:-1]
|
||||
starts = torch.nonzero(boundary, as_tuple=False).flatten().contiguous()
|
||||
targets = ordered_targets[starts].contiguous()
|
||||
ext.apply_arrivals(ordered_edges, starts, targets, weights, g, ref)
|
||||
queue = queue_summary(tick, expanded_global, accepted_cpu)
|
||||
queue_output.write(json.dumps(queue, allow_nan=False) + "\n")
|
||||
due_total += queue["count"]
|
||||
accepted_due_total += queue["accepted_count"]
|
||||
if spec["record_history"]:
|
||||
for name, value in (("v", v), ("g", g), ("ref", ref)):
|
||||
maps["pre_" + name][tick] = value.cpu().numpy()
|
||||
if n == N_NEURONS:
|
||||
full_spikes = spikes
|
||||
else:
|
||||
apl.spikes.zero_()
|
||||
apl.spikes[global_nodes_gpu] = spikes
|
||||
full_spikes = apl.spikes
|
||||
drive, apl_state, h = apl.advance(full_spikes)
|
||||
# Mixed precision contract: widen f32 h EXACTLY, THEN multiply
|
||||
# in f64, THEN subtract in f64. Do not guard refractory KCs.
|
||||
delta_si = h[kc_full_gpu].to(torch.float64) * 0.001
|
||||
g[kc_local_gpu] = g[kc_local_gpu] - delta_si
|
||||
apl_cpu = {"drive": drive.cpu().numpy().copy(), "state": apl_state.cpu().numpy().copy(),
|
||||
"inhibition": h.cpu().numpy().copy()}
|
||||
for name, value in apl_cpu.items():
|
||||
maps["apl_" + name][tick] = value
|
||||
actual = {"v": v.cpu().numpy().copy(), "g": g.cpu().numpy().copy(),
|
||||
"ref": ref.cpu().numpy().copy()}
|
||||
finite = all(bool(np.isfinite(value).all()) for value in list(actual.values()) + list(apl_cpu.values()))
|
||||
row = {"tick": tick, **{name + "_sha256": raw_hash(value, "<i4" if name == "ref" else "<f8")
|
||||
for name, value in actual.items()}}
|
||||
hash_output.write(json.dumps(row, allow_nan=False) + "\n")
|
||||
if spec["record_history"]:
|
||||
for name, value in actual.items():
|
||||
maps[name][tick] = value
|
||||
fired_local = torch.nonzero(spikes, as_tuple=False).flatten().cpu().numpy()
|
||||
if len(fired_local):
|
||||
event_steps.append(np.full(len(fired_local), tick, dtype=np.int32))
|
||||
event_nodes.append(graph["nodes"][fired_local].copy())
|
||||
completed += 1
|
||||
if not finite:
|
||||
with (out_dir / "first-failure-state.npz").open("xb") as target:
|
||||
np.savez_compressed(target, tick=np.array(tick, dtype=np.int32), **actual,
|
||||
**{"apl_" + name: value for name, value in apl_cpu.items()})
|
||||
require(False, "Nonfinite state; first failure saved")
|
||||
torch.cuda.synchronize(device)
|
||||
|
||||
def joined(values):
|
||||
return np.concatenate(values).astype(np.int32, copy=False) if values else np.empty(0, dtype=np.int32)
|
||||
|
||||
state = {"step": joined(event_steps), "neuron_idx": joined(event_nodes), "counts": counts.cpu().numpy(),
|
||||
"v": v.cpu().numpy(), "g": g.cpu().numpy(), "ref": ref.cpu().numpy()}
|
||||
with (out_dir / "state.npz").open("xb") as target:
|
||||
np.savez_compressed(target, **state)
|
||||
event_locals = np.searchsorted(graph["nodes"], state["neuron_idx"])
|
||||
counts_exact = np.array_equal(np.bincount(event_locals, minlength=n), state["counts"])
|
||||
require(counts_exact and completed == steps, "Sparse events/counts/completed ticks mismatch")
|
||||
for value in maps.values():
|
||||
value.flush()
|
||||
summary = {"case": spec["name"], "complete": True, "pass_all": True,
|
||||
"pass_all_scope": "internal finite/recording/event-count integrity only; independent equivalence not claimed",
|
||||
"steps": steps, "completed_ticks": completed, "nodes": n, "edges": e, "precision": "float64",
|
||||
"apl_precision": "float32", "state_units": "SI volts", "total_spikes": len(state["step"]),
|
||||
"requested": len(request_nodes), "accepted_requests": accepted_requests_total,
|
||||
"rejected_requests": len(request_nodes) - accepted_requests_total,
|
||||
"arriving_edges": due_total, "accepted_arriving_edges": accepted_due_total,
|
||||
"counts_match_sparse_events": bool(counts_exact), "all_finite": True,
|
||||
"record_history": spec["record_history"], "apl_fast_edges_zeroed": int(apl_out_mask.sum()),
|
||||
"kc_targets_present": len(common_kc), "effective_weights_raw_sha256": effective_hash.hexdigest(),
|
||||
"apl_component": apl.metadata, "apl_schedule": "end, after threshold/reset and guarded arrivals, unconditional all KC",
|
||||
"apl_si_conversion": "h32 -> float64 -> multiply float64 0.001 -> separate float64 subtraction",
|
||||
"build": build, "sealed_ae_backend_sha256": file_sha256(AE_MODEL / "gpu_backend.py"),
|
||||
"shared_apl_component_oracle_limitation": True, "oracle_validation_claim": False,
|
||||
"learning": False, "performance_runtime_claim": False, "cpu_sync_each_tick": True,
|
||||
"build_or_cache_seconds": build_done - start,
|
||||
"diagnostic_simulation_seconds": time.perf_counter() - simulation_start,
|
||||
"wall_seconds": time.perf_counter() - start,
|
||||
"peak_allocated_vram_bytes": int(torch.cuda.max_memory_allocated(device)),
|
||||
"peak_reserved_vram_bytes": int(torch.cuda.max_memory_reserved(device)),
|
||||
"input_sha256": {"spec.json": file_sha256(case_dir / "spec.json"),
|
||||
"requests.npz": file_sha256(case_dir / "requests.npz"),
|
||||
"base_weights.npy": file_sha256(Path(spec["graph_dir"]) / "weights.npy"),
|
||||
"coefficients.json": file_sha256(spec["coefficients_path"])}}
|
||||
if (case_dir / "arrays.npz").exists():
|
||||
summary["input_sha256"]["arrays.npz"] = file_sha256(case_dir / "arrays.npz")
|
||||
write_json(out_dir / "summary.json", summary)
|
||||
return summary
|
||||
except Exception as error:
|
||||
for value in maps.values():
|
||||
value.flush()
|
||||
write_json(out_dir / "error.json", {"complete": False, "pass_all": False,
|
||||
"tick_in_progress": tick, "completed_ticks": completed,
|
||||
"type": type(error).__name__, "message": str(error), "traceback": traceback.format_exc()})
|
||||
raise
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Frozen eager Torch float32 APL component and binary oracle sidecar.
|
||||
|
||||
This is the Phase 4P operator sequence, not an author-neuron implementation.
|
||||
No precision/determinism flags are changed here. The full matrix dimensions
|
||||
are retained even when the caller simulates a small neuronal fixture.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
PROTOCOL = "phase4ag-apl-v1"
|
||||
N_NEURONS, N_APL, N_KC = 166700, 2, 4064
|
||||
BUNDLE_FILES = ("input_matrix.npy", "output_matrix.npy", "kc_globals.npy", "apl_globals.npy")
|
||||
FRAME = struct.Struct("<qQ")
|
||||
PAYLOAD_BYTES = (N_APL + N_APL + N_KC) * 4
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def file_sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with Path(path).open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(8 << 20), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def array_sha256(value):
|
||||
return hashlib.sha256(np.ascontiguousarray(value).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def environment_metadata():
|
||||
device = torch.cuda.current_device()
|
||||
return {"torch": str(torch.__version__), "numpy": str(np.__version__),
|
||||
"cuda": torch.version.cuda, "cudnn": torch.backends.cudnn.version(),
|
||||
"device_name": torch.cuda.get_device_name(device), "device_index": device,
|
||||
"device_capability": list(torch.cuda.get_device_capability(device)),
|
||||
"deterministic_algorithms": torch.are_deterministic_algorithms_enabled(),
|
||||
"cudnn_deterministic": torch.backends.cudnn.deterministic,
|
||||
"cudnn_benchmark": torch.backends.cudnn.benchmark,
|
||||
"cuda_matmul_allow_tf32": torch.backends.cuda.matmul.allow_tf32,
|
||||
"cudnn_allow_tf32": torch.backends.cudnn.allow_tf32,
|
||||
"float32_matmul_precision": torch.get_float32_matmul_precision(),
|
||||
"CUBLAS_WORKSPACE_CONFIG": os.environ.get("CUBLAS_WORKSPACE_CONFIG"),
|
||||
"precision_configuration_changed_by_component": False}
|
||||
|
||||
|
||||
class LegacyAPL:
|
||||
"""Independent zero-initialized APL state with original full-width matrices."""
|
||||
|
||||
def __init__(self, bundle_dir, dt_ms):
|
||||
require(float(dt_ms) == 0.1, "Only frozen dt_ms=0.1 is allowed")
|
||||
require(torch.cuda.is_available(), "CUDA is required for the legacy Torch component")
|
||||
self.bundle_dir = Path(bundle_dir)
|
||||
expected = {"input_matrix.npy": (np.float32, (N_APL, N_NEURONS)),
|
||||
"output_matrix.npy": (np.float32, (N_APL, N_KC)),
|
||||
"kc_globals.npy": (np.int32, (N_KC,)),
|
||||
"apl_globals.npy": (np.int32, (N_APL,))}
|
||||
arrays = {}
|
||||
for name, (dtype, shape) in expected.items():
|
||||
value = np.load(self.bundle_dir / name, allow_pickle=False)
|
||||
require(value.dtype == dtype and value.shape == shape and value.flags.c_contiguous,
|
||||
"APL bundle dtype/shape/layout mismatch: " + name)
|
||||
require(bool(np.isfinite(value).all()), "Nonfinite APL bundle: " + name)
|
||||
arrays[name] = value
|
||||
for name in ("kc_globals.npy", "apl_globals.npy"):
|
||||
ids = arrays[name]
|
||||
require(bool(np.all((ids >= 0) & (ids < N_NEURONS))) and
|
||||
bool(np.all(np.diff(ids) > 0)), "APL/KC globals must be sorted unique: " + name)
|
||||
require(not np.intersect1d(arrays["kc_globals.npy"], arrays["apl_globals.npy"]).size,
|
||||
"APL and KC identities overlap")
|
||||
require(bool(np.all(arrays["input_matrix.npy"] >= 0)) and
|
||||
bool(np.all(arrays["output_matrix.npy"] >= 0)), "APL raw-contact matrices must be nonnegative")
|
||||
self.device = torch.device("cuda", torch.cuda.current_device())
|
||||
self.input_gpu = torch.from_numpy(arrays["input_matrix.npy"].copy()).to(self.device)
|
||||
self.output_gpu = torch.from_numpy(arrays["output_matrix.npy"].copy()).to(self.device)
|
||||
self.kc_globals = arrays["kc_globals.npy"].copy()
|
||||
self.apl_globals = arrays["apl_globals.npy"].copy()
|
||||
self.state = torch.zeros(N_APL, dtype=torch.float32, device=self.device)
|
||||
self.spikes = torch.zeros(N_NEURONS, dtype=torch.uint8, device=self.device)
|
||||
self.decay = np.float32(math.exp(-float(dt_ms) / 20.0))
|
||||
self.gain = np.float32(0.01)
|
||||
self.metadata = {"ok": True, "protocol": PROTOCOL, "n_neurons": N_NEURONS,
|
||||
"n_apl": N_APL, "n_kc": N_KC, "dt_ms": float(dt_ms),
|
||||
"files_sha256": {name: file_sha256(self.bundle_dir / name) for name in BUNDLE_FILES},
|
||||
"array_sha256": {name: array_sha256(value) for name, value in arrays.items()},
|
||||
"decay_f32_bits": f"{int(self.decay.view(np.uint32)):08x}",
|
||||
"gain_f32_bits": f"{int(self.gain.view(np.uint32)):08x}",
|
||||
"environment": environment_metadata(),
|
||||
"operator_sequence": "state.mul_(decay32); drive=mv(I32,spikes32); state.add_(drive); h=matmul(state,O32)*gain32",
|
||||
"h_units": "legacy mV", "initial_state": "float32 zeros"}
|
||||
|
||||
@torch.no_grad()
|
||||
def advance(self, spikes_u8):
|
||||
require(spikes_u8.device == self.device and spikes_u8.dtype == torch.uint8 and
|
||||
tuple(spikes_u8.shape) == (N_NEURONS,) and spikes_u8.is_contiguous(),
|
||||
"APL expects full-width contiguous CUDA uint8 spikes")
|
||||
# Preserve separate eager operations, including Python evaluation order
|
||||
# in the frozen state.mul_(...).add_(torch.mv(...)) expression.
|
||||
self.state.mul_(float(self.decay))
|
||||
drive = torch.mv(self.input_gpu, spikes_u8.to(torch.float32))
|
||||
self.state.add_(drive)
|
||||
h = torch.matmul(self.state, self.output_gpu) * self.gain
|
||||
return drive, self.state, h
|
||||
|
||||
def advance_global_fired(self, fired):
|
||||
require(fired.dtype == np.int32 and fired.ndim == 1, "Global spikes must be int32 vector")
|
||||
require(bool(np.all((fired >= 0) & (fired < N_NEURONS))) and
|
||||
bool(np.all(np.diff(fired) > 0)), "Global spikes must be sorted unique and in range")
|
||||
self.spikes.zero_()
|
||||
if fired.size:
|
||||
indices = torch.from_numpy(fired.copy()).to(device=self.device, dtype=torch.int64)
|
||||
self.spikes[indices] = 1
|
||||
return self.advance(self.spikes)
|
||||
|
||||
|
||||
def read_exact(stream, count, boundary_eof=False):
|
||||
result = bytearray()
|
||||
while len(result) < count:
|
||||
block = stream.read(count - len(result))
|
||||
if not block:
|
||||
if boundary_eof and not result:
|
||||
return None
|
||||
raise EOFError("Truncated APL request frame")
|
||||
result.extend(block)
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--bundle-dir", required=True)
|
||||
parser.add_argument("--dt-ms", required=True, type=float)
|
||||
args = parser.parse_args()
|
||||
# stdout is exclusively the handshake line and binary response frames.
|
||||
output, source = sys.stdout.buffer, sys.stdin.buffer
|
||||
component = LegacyAPL(args.bundle_dir, args.dt_ms)
|
||||
output.write((json.dumps(component.metadata, allow_nan=False) + "\n").encode("utf-8"))
|
||||
output.flush()
|
||||
expected_tick = 0
|
||||
while True:
|
||||
header = read_exact(source, FRAME.size, boundary_eof=True)
|
||||
if header is None:
|
||||
return
|
||||
tick, count = FRAME.unpack(header)
|
||||
require(tick == expected_tick and count <= N_NEURONS, "Invalid APL request tick/count")
|
||||
fired = np.frombuffer(read_exact(source, count * 4), dtype="<i4").copy()
|
||||
drive, state, h = component.advance_global_fired(fired)
|
||||
arrays = [value.detach().cpu().numpy().astype("<f4", copy=False) for value in (drive, state, h)]
|
||||
require(all(bool(np.isfinite(value).all()) for value in arrays), "Nonfinite APL output")
|
||||
output.write(FRAME.pack(tick, PAYLOAD_BYTES))
|
||||
for value in arrays:
|
||||
output.write(value.tobytes())
|
||||
output.flush()
|
||||
expected_tick += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/author-apl-lab-v1",
|
||||
"files": [
|
||||
{
|
||||
"path": "PROTOCOL.md",
|
||||
"bytes": 5033,
|
||||
"sha256": "c14c2d3c35e2b404c516befa66370723fba075516540214ae46e4104ff87e813"
|
||||
},
|
||||
{
|
||||
"path": "calibration.py",
|
||||
"bytes": 25414,
|
||||
"sha256": "edb9cb2d890471fd07afd88c959ef3540b0cd40af490fac3f26396b5e7f5bf9c"
|
||||
},
|
||||
{
|
||||
"path": "compare.py",
|
||||
"bytes": 33632,
|
||||
"sha256": "6e1616b086f667fa4e4b5087f500f8080281977ca9922e7b055acfddd2003bd5"
|
||||
},
|
||||
{
|
||||
"path": "config.json",
|
||||
"bytes": 1845,
|
||||
"sha256": "2aa6b4d777ff698018e1a6eaf571215208b8c874a4c848a6d632e2bd778c90e3"
|
||||
},
|
||||
{
|
||||
"path": "context.py",
|
||||
"bytes": 2038,
|
||||
"sha256": "b8b1231d958a3ec935ec1d5a36e08cbd9bcd1127f4d4bb243a9051884a0e7a16"
|
||||
},
|
||||
{
|
||||
"path": "finalize.py",
|
||||
"bytes": 6747,
|
||||
"sha256": "b6e3c67e668a971b544df32d76435ab5e537061b9fc2e8d9dedb5d2a726f5782"
|
||||
},
|
||||
{
|
||||
"path": "gpu_apl.py",
|
||||
"bytes": 21206,
|
||||
"sha256": "69f5d893f47dafff4ef7c3186c3df0b9014666b32c8f4e943b57016bec92b18a"
|
||||
},
|
||||
{
|
||||
"path": "legacy_apl_worker.py",
|
||||
"bytes": 8158,
|
||||
"sha256": "21ceec65fe42581dc55872010e0025d660e80358905f1cf4df1dadf341a3b291"
|
||||
},
|
||||
{
|
||||
"path": "oracle_apl.py",
|
||||
"bytes": 46017,
|
||||
"sha256": "9b7f3e0b59f2117b94b41a6f542f37e6f7950138d324c905e8e0ac6cb7d17151"
|
||||
},
|
||||
{
|
||||
"path": "prepare.py",
|
||||
"bytes": 13633,
|
||||
"sha256": "197ccc6aae70566c4db4d4de17fa032e853b9bd9a04262905d6c71e12a930b6f"
|
||||
},
|
||||
{
|
||||
"path": "run_case.py",
|
||||
"bytes": 2116,
|
||||
"sha256": "afbe6d5a37c29bda66d38c470093584130d1852e67881c681662aec3c76b33c7"
|
||||
},
|
||||
{
|
||||
"path": "run_suite.py",
|
||||
"bytes": 3700,
|
||||
"sha256": "84c681030b0032119b5392d0c3f3c136a9c6b0d133eb548c40791a10bbbb8b63"
|
||||
},
|
||||
{
|
||||
"path": "test_calibration.py",
|
||||
"bytes": 8127,
|
||||
"sha256": "bab3136eecbc5bb09a985d715844ee845cb72b07930ce376bfb39be33bd1845a"
|
||||
},
|
||||
{
|
||||
"path": "test_protocol.py",
|
||||
"bytes": 11764,
|
||||
"sha256": "1a233f47943e01bbd845ac1dacdb1eec3f88c270c65a5f75bce1cf679b5de097"
|
||||
},
|
||||
{
|
||||
"path": "validate_components.py",
|
||||
"bytes": 24824,
|
||||
"sha256": "5cfc9a4505da6b8fb4e7d2617cdaedaa8475ed46a11ec9bdf0d4d25b1d22c2d3"
|
||||
},
|
||||
{
|
||||
"path": "verify_frozen.py",
|
||||
"bytes": 2160,
|
||||
"sha256": "0d0e72b2f1348f6648124c8c34f75094f76fc5e1a281fbcca130a5fad2f35293"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,740 @@
|
||||
"""Phase 4AG shared-APL-component closed-loop Brian oracle.
|
||||
|
||||
Brian has no Torch dependency. A separate, pinned science-Python process
|
||||
executes the unchanged legacy float32 APL operations from Brian's own spikes.
|
||||
This is independent LIF/scheduling validation with a shared APL numerical
|
||||
component, NOT an all-components-independent CPU oracle.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import queue
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import types
|
||||
from contextlib import ExitStack
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
BLOCK_BYTES = 8 << 20
|
||||
PROTOCOL = "phase4ag-apl-v1"
|
||||
AUTHOR_PATH = Path("/data/digital-drosophila/experiments/phase4ad-author-semantics/external-resources/model.py")
|
||||
AUTHOR_SHA256 = "fc45837d7122c6ce2a7f3f2f23c515992e4b232aadb919efabb72337fac88e4e"
|
||||
AE_PATH = Path("/data/digital-drosophila/models/male-cns-lif/author-runtime-lab-v1/oracle_stress.py")
|
||||
AE_SHA256 = "32eb1c42383068ce52701d2139a376ce155ad7072cde019949f8ca2d55492409"
|
||||
AF_PATH = Path("/data/digital-drosophila/models/male-cns-lif/author-fullgraph-lab-v1/oracle_full.py")
|
||||
AF_SHA256 = "d72003d98f0f1624b12126eff50bd361c43d888f4a64d90ba9247a58976f1961"
|
||||
EVENT_DTYPE = np.dtype([("step", "<i4"), ("neuron_idx", "<i4")])
|
||||
BUNDLE_FILES = ("input_matrix.npy", "output_matrix.npy", "kc_globals.npy", "apl_globals.npy")
|
||||
_PROCESS_USED = False
|
||||
|
||||
|
||||
def _check(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def _save(path, value):
|
||||
with Path(path).open("x", encoding="utf-8") as handle:
|
||||
json.dump(value, handle, indent=2, sort_keys=True, allow_nan=False)
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def _digest(path):
|
||||
result = hashlib.sha256()
|
||||
with Path(path).open("rb") as handle:
|
||||
for block in iter(lambda: handle.read(BLOCK_BYTES), b""):
|
||||
result.update(block)
|
||||
return result.hexdigest()
|
||||
|
||||
|
||||
def _array_hash(array, dtype):
|
||||
return hashlib.sha256(np.ascontiguousarray(array, dtype=dtype).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def _load_checked(path, expected, name):
|
||||
data = Path(path).read_bytes()
|
||||
_check(hashlib.sha256(data).hexdigest() == expected, f"Pinned source hash mismatch: {path}")
|
||||
module = types.ModuleType(name)
|
||||
module.__file__ = str(path)
|
||||
exec(compile(data, str(path), "exec"), module.__dict__)
|
||||
return module
|
||||
|
||||
|
||||
def _new_memmap(path, dtype, shape):
|
||||
_check(not Path(path).exists(), f"Refusing to overwrite output: {path}")
|
||||
return np.lib.format.open_memmap(path, mode="w+", dtype=dtype, shape=shape)
|
||||
|
||||
|
||||
def _budget(root, limit, reserve=0):
|
||||
total = 0
|
||||
for directory, _, filenames in os.walk(root, followlinks=False):
|
||||
for filename in filenames:
|
||||
total += (Path(directory) / filename).stat(follow_symlinks=False).st_size
|
||||
_check(total + int(reserve) <= limit,
|
||||
f"Output budget exceeded: {total}+{reserve}>{limit}; inputs and duration were not adjusted")
|
||||
return total
|
||||
|
||||
|
||||
class _PipeReader:
|
||||
"""One bounded reader thread, with timeout/EOF checks for every frame."""
|
||||
|
||||
def __init__(self, pipe, timeout):
|
||||
self.pipe, self.timeout = pipe, float(timeout)
|
||||
self.chunks = queue.Queue(maxsize=8)
|
||||
self.buffer = bytearray()
|
||||
self.thread = threading.Thread(target=self._pump, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def _pump(self):
|
||||
try:
|
||||
while True:
|
||||
chunk = os.read(self.pipe.fileno(), 65536)
|
||||
self.chunks.put(chunk)
|
||||
if not chunk:
|
||||
return
|
||||
except BaseException as error:
|
||||
self.chunks.put(error)
|
||||
|
||||
def _fill(self, deadline):
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError("APL sidecar response timed out")
|
||||
try:
|
||||
chunk = self.chunks.get(timeout=remaining)
|
||||
except queue.Empty as error:
|
||||
raise TimeoutError("APL sidecar response timed out") from error
|
||||
if isinstance(chunk, BaseException):
|
||||
raise RuntimeError("APL sidecar stdout reader failed") from chunk
|
||||
if not chunk:
|
||||
raise EOFError("APL sidecar ended before a complete response")
|
||||
self.buffer.extend(chunk)
|
||||
|
||||
def read(self, size):
|
||||
deadline = time.monotonic() + self.timeout
|
||||
while len(self.buffer) < size:
|
||||
self._fill(deadline)
|
||||
result = bytes(self.buffer[:size])
|
||||
del self.buffer[:size]
|
||||
return result
|
||||
|
||||
def line(self):
|
||||
deadline = time.monotonic() + self.timeout
|
||||
while b"\n" not in self.buffer:
|
||||
_check(len(self.buffer) < 1 << 20, "APL sidecar handshake exceeds 1 MiB")
|
||||
self._fill(deadline)
|
||||
end = self.buffer.index(b"\n") + 1
|
||||
result = bytes(self.buffer[:end])
|
||||
del self.buffer[:end]
|
||||
return result
|
||||
|
||||
|
||||
class _APLWorker:
|
||||
def __init__(self, python, script, bundle_dir, stderr, timeout=180):
|
||||
self.command = [str(python), "-B", str(script), "--bundle-dir", str(bundle_dir), "--dt-ms", "0.1"]
|
||||
self.process = subprocess.Popen(self.command, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=stderr, bufsize=0, shell=False)
|
||||
self.reader = _PipeReader(self.process.stdout, timeout)
|
||||
self.tick = 0
|
||||
try:
|
||||
self.hello = json.loads(self.reader.line().decode("utf-8"))
|
||||
_check(self.hello.get("ok") is True, "APL sidecar did not acknowledge initialization")
|
||||
_check(self.hello.get("protocol") == PROTOCOL, "Unexpected APL sidecar protocol")
|
||||
for key, value in (("n_neurons", 166700), ("n_apl", 2), ("n_kc", 4064)):
|
||||
_check(self.hello.get(key) == value, f"APL sidecar changed {key}")
|
||||
_check(self.hello.get("dt_ms") == .1, "APL sidecar changed dt")
|
||||
expected_decay = f"{int(np.float32(math.exp(-.1 / 20.)).view(np.uint32)):08x}"
|
||||
expected_gain = f"{int(np.float32(.01).view(np.uint32)):08x}"
|
||||
_check(self.hello.get("decay_f32_bits") == expected_decay and self.hello.get("gain_f32_bits") == expected_gain,
|
||||
"APL sidecar decay/gain differs from the frozen float32 scalars")
|
||||
_check(self.hello.get("files_sha256") == {name: _digest(bundle_dir / name) for name in BUNDLE_FILES},
|
||||
"APL sidecar bundle hashes differ from the independently read bundle")
|
||||
except BaseException:
|
||||
self.abort()
|
||||
raise
|
||||
|
||||
def step(self, tick, global_spikes):
|
||||
_check(tick == self.tick, "APL sidecar request ticks must be consecutive")
|
||||
nodes = np.asarray(global_spikes)
|
||||
_check(nodes.ndim == 1 and nodes.dtype == np.dtype("int32"), "APL input spikes must be int32 globals")
|
||||
_check(not len(nodes) or (nodes[0] >= 0 and nodes[-1] < 166700 and np.all(np.diff(nodes) > 0)),
|
||||
"APL input spikes must be sorted, unique, and in the frozen global range")
|
||||
packet = struct.pack("<qQ", int(tick), len(nodes)) + np.ascontiguousarray(nodes, dtype="<i4").tobytes()
|
||||
view = memoryview(packet)
|
||||
while view:
|
||||
written = self.process.stdin.write(view)
|
||||
if not written:
|
||||
raise BrokenPipeError("APL sidecar stdin failed")
|
||||
view = view[written:]
|
||||
self.process.stdin.flush()
|
||||
response_tick, byte_count = struct.unpack("<qQ", self.reader.read(16))
|
||||
_check(response_tick == tick and byte_count == (2 + 2 + 4064) * 4,
|
||||
"APL sidecar frame tick/length mismatch")
|
||||
values = np.frombuffer(self.reader.read(byte_count), dtype="<f4")
|
||||
_check(np.isfinite(values).all() and np.all(values >= 0), "APL sidecar returned invalid nonnegative f32 states")
|
||||
self.tick += 1
|
||||
return values[:2], values[2:4], values[4:]
|
||||
|
||||
def finish(self, steps):
|
||||
_check(self.tick == steps, "APL sidecar did not process every tick")
|
||||
self.process.stdin.close()
|
||||
status = self.process.wait(timeout=30)
|
||||
_check(status == 0, f"APL sidecar exited unsuccessfully: {status}")
|
||||
self.reader.thread.join(timeout=5)
|
||||
_check(not self.reader.thread.is_alive(), "APL stdout did not reach EOF")
|
||||
extra = bytes(self.reader.buffer)
|
||||
while not self.reader.chunks.empty():
|
||||
chunk = self.reader.chunks.get_nowait()
|
||||
_check(not isinstance(chunk, BaseException), "APL stdout reader failed after the last frame")
|
||||
extra += chunk
|
||||
_check(not extra, "APL sidecar emitted unrequested trailing stdout bytes")
|
||||
self.process.stdout.close()
|
||||
return status
|
||||
|
||||
def abort(self):
|
||||
if self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait(timeout=5)
|
||||
for handle in (self.process.stdin, self.process.stdout):
|
||||
if handle is not None and not handle.closed:
|
||||
handle.close()
|
||||
|
||||
|
||||
def _graph_inputs(directory):
|
||||
arrays = {}
|
||||
definitions = {"nodes": "int32", "edge_id": "int64", "src": "int32",
|
||||
"dst": "int32", "signed_raw": "int64", "ptr": "int64", "weights": "float64"}
|
||||
for name, dtype in definitions.items():
|
||||
value = np.load(directory / (name + ".npy"), mmap_mode="r", allow_pickle=False)
|
||||
_check(value.ndim == 1 and value.dtype == np.dtype(dtype), f"Invalid graph array {name}")
|
||||
arrays[name] = value
|
||||
nodes, edge_ids = arrays["nodes"], arrays["edge_id"]
|
||||
n, edges = len(nodes), len(edge_ids)
|
||||
_check(0 < n <= 166700 and edges > 0, "A case needs real global nodes and explicit nonempty connectivity")
|
||||
_check(nodes[0] >= 0 and nodes[-1] < 166700 and np.all(np.diff(nodes) > 0), "Invalid global node order")
|
||||
for name in ("src", "dst", "signed_raw", "weights"):
|
||||
_check(arrays[name].shape == (edges,), f"Graph array {name} has the wrong shape")
|
||||
pointer = arrays["ptr"]
|
||||
_check(pointer.shape == (n + 1,) and pointer[0] == 0 and pointer[-1] == edges and
|
||||
np.all(np.diff(pointer) >= 0), "Invalid graph CSR pointer")
|
||||
source_counts = np.zeros(n, np.int64)
|
||||
identity_edges = True
|
||||
previous_source, previous_edge = -1, -1
|
||||
width = BLOCK_BYTES // 8
|
||||
for begin in range(0, edges, width):
|
||||
stop = min(edges, begin + width)
|
||||
source, target, ids = arrays["src"][begin:stop], arrays["dst"][begin:stop], edge_ids[begin:stop]
|
||||
_check(np.all((source >= 0) & (source < n)) and np.all((target >= 0) & (target < n)),
|
||||
"Graph edge endpoint is outside the local node array")
|
||||
_check(np.all(ids >= 0), "Graph edge IDs must be nonnegative")
|
||||
_check(source[0] > previous_source or (source[0] == previous_source and ids[0] > previous_edge),
|
||||
"Graph edge blocks are not in canonical source/edge order")
|
||||
_check(np.all((source[1:] > source[:-1]) |
|
||||
((source[1:] == source[:-1]) & (ids[1:] > ids[:-1]))),
|
||||
"Graph edges are not in canonical source/edge order")
|
||||
previous_source, previous_edge = int(source[-1]), int(ids[-1])
|
||||
identity_edges &= np.array_equal(ids, np.arange(begin, stop, dtype=np.int64))
|
||||
source_counts += np.bincount(source, minlength=n)
|
||||
_check(np.isfinite(arrays["weights"][begin:stop]).all(), "Nonfinite graph weights")
|
||||
_check(np.array_equal(source_counts, np.diff(pointer)), "CSR pointer and source rows disagree")
|
||||
if not identity_edges:
|
||||
_check(len(np.unique(edge_ids)) == edges, "Original edge IDs must be unique")
|
||||
for name in ("completeness.csv", "connectivity.parquet"):
|
||||
_check((directory / name).is_file(), f"Missing unchanged author input: {name}")
|
||||
if n == 166700:
|
||||
_check(edges == 25582938 and identity_edges and np.array_equal(nodes, np.arange(n, dtype=np.int32)),
|
||||
"A full probe must use the unchanged complete canonical graph")
|
||||
return arrays, identity_edges
|
||||
|
||||
|
||||
def _requests(case_dir, nodes, steps):
|
||||
with np.load(case_dir / "requests.npz", allow_pickle=False) as archive:
|
||||
_check(set(archive.files) == {"tick", "neuron_idx"}, "Unexpected sparse request schema")
|
||||
ticks, requested = archive["tick"].copy(), archive["neuron_idx"].copy()
|
||||
_check(ticks.dtype == requested.dtype == np.dtype("int32") and ticks.ndim == requested.ndim == 1 and
|
||||
ticks.shape == requested.shape, "Requests must be equal-length int32 vectors")
|
||||
_check(np.all((ticks >= 0) & (ticks < steps)), "Request time outside the case")
|
||||
_check(not len(ticks) or np.all((ticks[1:] > ticks[:-1]) |
|
||||
((ticks[1:] == ticks[:-1]) & (requested[1:] > requested[:-1]))),
|
||||
"Requests must be sorted and unique by tick/global node")
|
||||
local = np.searchsorted(nodes, requested)
|
||||
_check(np.all(local < len(nodes)) and np.array_equal(nodes[local], requested),
|
||||
"Request references a global neuron absent from this case")
|
||||
counts = np.bincount(ticks, minlength=steps).astype(np.int32)
|
||||
pointer = np.concatenate((np.zeros(1, np.int64), np.cumsum(counts, dtype=np.int64)))
|
||||
return requested, local.astype(np.int32), pointer, counts
|
||||
|
||||
|
||||
class _QueueProxy:
|
||||
def __init__(self, original, get_tick, available, arrays, identity_edges,
|
||||
due_counts, spike_ring, delay_steps, handle, records, summarize):
|
||||
self._original_queue, self._get_tick = original, get_tick
|
||||
self._available, self._arrays, self._identity_edges = available, arrays, identity_edges
|
||||
self._due_counts, self._spike_ring, self._delay_steps = due_counts, spike_ring, delay_steps
|
||||
self._handle, self._records, self._summarize = handle, records, summarize
|
||||
|
||||
def peek(self, *args, **kwargs):
|
||||
original = self._original_queue.peek(*args, **kwargs)
|
||||
local_edges = np.asarray(original)
|
||||
tick = self._get_tick()
|
||||
_check(tick == len(self._records["rows"]), "Queue peek ticks must be consecutive")
|
||||
_check(local_edges.ndim == 1 and np.all((local_edges >= 0) & (local_edges < len(self._arrays["edge_id"]))),
|
||||
"Original queue contains an invalid local edge index")
|
||||
_check(len(local_edges) == self._due_counts[tick % len(self._due_counts)],
|
||||
"Queue arrivals differ from the fixed-delay outgoing degree of actual spikes")
|
||||
fired_then = np.zeros(len(self._arrays["nodes"]), dtype=bool)
|
||||
if tick >= self._delay_steps:
|
||||
fired_then[self._spike_ring[(tick - self._delay_steps) % len(self._spike_ring)]] = True
|
||||
|
||||
def accepted(begin, stop):
|
||||
part = local_edges[begin:stop]
|
||||
_check(np.all(fired_then[self._arrays["src"][part]]), "Arrival lacks a presynaptic spike at author delay")
|
||||
return self._available[self._arrays["dst"][part]]
|
||||
|
||||
# Only the observed ID view changes; the queue is returned verbatim.
|
||||
edge_ids = local_edges if self._identity_edges else self._arrays["edge_id"][local_edges]
|
||||
summary = self._summarize(tick, edge_ids, accepted)
|
||||
self._handle.write(json.dumps(summary, sort_keys=True) + "\n")
|
||||
self._records["rows"].append(summary)
|
||||
self._records["peek_calls"] += 1
|
||||
return original
|
||||
|
||||
def advance(self, *args, **kwargs):
|
||||
self._records["advance_calls"] += 1
|
||||
return self._original_queue.advance(*args, **kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._original_queue, name)
|
||||
|
||||
|
||||
def run_case(case_dir: Path, out_dir: Path) -> dict:
|
||||
"""Run one continuous case, with exclusive outputs and no candidate trace input.
|
||||
|
||||
Graph inputs are the AF array/author-table schema plus original weights.npy.
|
||||
requests.npz contains sparse tick/neuron_idx int32 vectors. Optional
|
||||
arrays.npz contains initial_v_mv/initial_g_mv float64 vectors only.
|
||||
Full probes write state hashes, sparse events and APL traces, not dense
|
||||
whole-brain histories. Each case requires a fresh process.
|
||||
"""
|
||||
global _PROCESS_USED
|
||||
_check(not _PROCESS_USED, "Every oracle case requires a fresh process")
|
||||
_PROCESS_USED = True
|
||||
case_dir, out_dir = Path(case_dir).resolve(strict=True), Path(out_dir).resolve()
|
||||
spec = json.loads((case_dir / "spec.json").read_text(encoding="utf-8"))
|
||||
graph_dir = Path(spec["graph_dir"]).resolve(strict=True)
|
||||
bundle_dir = Path(spec["apl_bundle_dir"]).resolve(strict=True)
|
||||
worker_path = Path(spec["legacy_apl_worker"]).resolve(strict=True)
|
||||
science_python = Path(spec["science_python"]).resolve(strict=True)
|
||||
author_path = Path(spec.get("author_path", AUTHOR_PATH)).resolve(strict=True)
|
||||
ae_path = Path(spec.get("ae_oracle_path", AE_PATH)).resolve(strict=True)
|
||||
af_path = Path(spec.get("af_oracle_path", AF_PATH)).resolve(strict=True)
|
||||
coefficients_path = Path(spec["coefficients_path"]).resolve(strict=True)
|
||||
steps = int(spec["steps"])
|
||||
_check(steps == spec["steps"] and 0 < steps <= np.iinfo(np.int32).max, "Invalid number of steps")
|
||||
_check(float(spec["dt_ms"]) == .1 and np.isclose(float(spec["duration_ms"]), steps * .1, rtol=0, atol=1e-12),
|
||||
"The frozen APL protocol requires dt_ms=0.1 and a matching duration")
|
||||
_check(spec.get("precision", "float64") == "float64", "Author state must be float64")
|
||||
_check(spec.get("training", False) is False and spec.get("poi_used", False) is False,
|
||||
"Training and original poi are outside this gate")
|
||||
_check(spec["apl"] == {"tau_ms": 20, "gain": .01, "normalization": 1}, "Frozen APL parameters changed")
|
||||
record_history = spec["record_history"]
|
||||
_check(isinstance(record_history, bool), "record_history must be a boolean")
|
||||
budget_root = Path(spec.get("budget_root", out_dir.parent)).resolve()
|
||||
budget_limit = int(spec.get("output_budget_bytes", 15 * 1024 ** 3))
|
||||
_check(budget_limit > 0 and out_dir.is_relative_to(budget_root), "Invalid output budget/root")
|
||||
out_dir.mkdir(parents=True, exist_ok=False)
|
||||
mapped_files = []
|
||||
saved_preferences, old_dt, worker = None, None, None
|
||||
last_complete_tick = -1
|
||||
try:
|
||||
arrays, identity_edges = _graph_inputs(graph_dir)
|
||||
nodes, n, edges = arrays["nodes"], len(arrays["nodes"]), len(arrays["edge_id"])
|
||||
_check(n < 166700 or not record_history, "Full probes must not save dense V/g/ref histories")
|
||||
bundle = {name: np.load(bundle_dir / (name + ".npy"), mmap_mode="r", allow_pickle=False)
|
||||
for name in ("input_matrix", "output_matrix", "kc_globals", "apl_globals")}
|
||||
for name, shape in (("input_matrix", (2, 166700)), ("output_matrix", (2, 4064))):
|
||||
value = bundle[name]
|
||||
_check(value.dtype == np.dtype("float32") and value.shape == shape and value.flags.c_contiguous,
|
||||
f"Frozen APL {name} shape/dtype/layout changed")
|
||||
_check(np.isfinite(value).all() and np.all(value >= 0), f"Invalid positive raw-normalized APL {name}")
|
||||
for name, count in (("kc_globals", 4064), ("apl_globals", 2)):
|
||||
value = bundle[name]
|
||||
_check(value.dtype == np.dtype("int32") and value.shape == (count,) and value[0] >= 0 and
|
||||
value[-1] < 166700 and np.all(np.diff(value) > 0), f"Frozen APL identity {name} changed")
|
||||
_check(not np.intersect1d(bundle["kc_globals"], bundle["apl_globals"]).size, "APL and KC identities overlap")
|
||||
kc_positions = np.searchsorted(nodes, bundle["kc_globals"])
|
||||
present = kc_positions < n
|
||||
present[present] &= nodes[kc_positions[present]] == bundle["kc_globals"][present]
|
||||
kc_local, kc_in_bundle = kc_positions[present], np.flatnonzero(present)
|
||||
requested_globals, request_local, request_ptr, request_count = _requests(case_dir, nodes, steps)
|
||||
initial = None
|
||||
if (case_dir / "arrays.npz").exists():
|
||||
with np.load(case_dir / "arrays.npz", allow_pickle=False) as archive:
|
||||
_check(set(archive.files) == {"initial_v_mv", "initial_g_mv"}, "Unexpected initial-state schema")
|
||||
initial = {name: archive[name].copy() for name in archive.files}
|
||||
for name, value in initial.items():
|
||||
_check(value.dtype == np.dtype("float64") and value.shape == (n,) and np.isfinite(value).all(),
|
||||
f"Invalid diagnostic initial state: {name}")
|
||||
history_bytes = steps * n * 40 if record_history else 0
|
||||
_budget(budget_root, budget_limit, history_bytes + steps * (2 + 2 + 4064) * 4 + edges * 8 + (64 << 20))
|
||||
versions = {name: importlib.metadata.version(name) for name in
|
||||
("brian2", "numpy", "pandas", "pyarrow", "sympy", "scipy", "cython")}
|
||||
_save(out_dir / "environment.json", {"python": sys.version, "platform": platform.platform(),
|
||||
"packages": versions, "codegen": "numpy", "author_precision": "float64",
|
||||
"APL_precision": "float32 in separate science-Python process", "Torch_imported_into_Brian": False})
|
||||
for name, required in (("brian2", "2.5.1"), ("numpy", "1.22.3"), ("sympy", "1.12")):
|
||||
_check(versions[name] == required, f"{name} must remain pinned to {required}")
|
||||
import brian2 as b2
|
||||
import pandas as pd
|
||||
from brian2.core.base import BrianObject
|
||||
|
||||
ae = _load_checked(ae_path, AE_SHA256, "phase4ag_sealed_ae_helpers")
|
||||
af = _load_checked(af_path, AF_SHA256, "phase4ag_sealed_af_helpers")
|
||||
author = _load_checked(author_path, AUTHOR_SHA256, "phase4ag_unchanged_author")
|
||||
_check("torch" not in sys.modules, "Torch must not be imported into the pinned Brian process")
|
||||
comp = pd.read_csv(graph_dir / "completeness.csv", index_col=0)
|
||||
_check(np.array_equal(comp.index.to_numpy(), nodes) and
|
||||
np.array_equal(comp["local_index"].to_numpy(), np.arange(n)), "Author CSV neuron identity differs")
|
||||
del comp
|
||||
graph_hashes = {name: _digest(graph_dir / name) for name in
|
||||
tuple(key + ".npy" for key in arrays) + ("completeness.csv", "connectivity.parquet")}
|
||||
bundle_hashes = {name: _digest(bundle_dir / name) for name in BUNDLE_FILES}
|
||||
case_hashes = {name: _digest(case_dir / name) for name in ("spec.json", "requests.npz")}
|
||||
if initial is not None:
|
||||
case_hashes["arrays.npz"] = _digest(case_dir / "arrays.npz")
|
||||
worker_hash = _digest(worker_path)
|
||||
coefficient_hash = _digest(coefficients_path)
|
||||
baseline_coefficients = json.loads(coefficients_path.read_text(encoding="utf-8"))
|
||||
if "legacy_apl_worker_sha256" in spec:
|
||||
_check(worker_hash == spec["legacy_apl_worker_sha256"], "APL worker differs from preregistered source")
|
||||
_save(out_dir / "input-audit.json", {"spec": spec, "case_hashes": case_hashes,
|
||||
"graph_hashes": graph_hashes, "bundle_hashes": bundle_hashes,
|
||||
"author_sha256": AUTHOR_SHA256, "ae_sha256": AE_SHA256, "af_sha256": AF_SHA256,
|
||||
"legacy_apl_worker_sha256": worker_hash, "coefficients_sha256": coefficient_hash,
|
||||
"candidate_trace_used_as_input": False,
|
||||
"scope": "shared-APL-component closed-loop oracle, not all-components-independent CPU equivalence"})
|
||||
saved_preferences = {key: b2.prefs[key] for key in
|
||||
("codegen.target", "core.default_float_dtype", "legacy.refractory_timing")}
|
||||
old_dt = b2.defaultclock.dt
|
||||
b2.start_scope()
|
||||
b2.prefs["codegen.target"] = "numpy"
|
||||
b2.prefs["core.default_float_dtype"] = np.float64
|
||||
b2.prefs["legacy.refractory_timing"] = False
|
||||
b2.defaultclock.dt = .1 * b2.ms
|
||||
parameters = dict(author.default_params)
|
||||
_save(out_dir / "parameter-checks.json", ae._parameters_match(parameters, spec["model"], b2))
|
||||
neu, syn, spikes = author.create_model(graph_dir / "completeness.csv", graph_dir / "connectivity.parquet", parameters)
|
||||
_check(len(neu) == n and len(syn) == edges, "Author loaded an unexpected graph size")
|
||||
_check(all(np.dtype(neu.variables[key].dtype) == np.float64 for key in ("v", "g")) and
|
||||
np.dtype(syn.variables["w"].dtype) == np.float64, "Author states/weights are not float64")
|
||||
effective = _new_memmap(out_dir / "weights.npy", "<f8", (edges,))
|
||||
mapped_files.append(effective)
|
||||
width, w_unit, mask_count = BLOCK_BYTES // 8, float(parameters["w_syn"] / b2.volt), 0
|
||||
mask_hasher = hashlib.sha256()
|
||||
base_hasher = hashlib.sha256()
|
||||
effective_hasher = hashlib.sha256()
|
||||
for begin in range(0, edges, width):
|
||||
stop = min(edges, begin + width)
|
||||
np.testing.assert_array_equal(np.asarray(syn.i[begin:stop]), arrays["src"][begin:stop])
|
||||
np.testing.assert_array_equal(np.asarray(syn.j[begin:stop]), arrays["dst"][begin:stop])
|
||||
original = np.asarray(syn.w_[begin:stop], dtype=np.float64).copy()
|
||||
raw_expected = arrays["signed_raw"][begin:stop] * w_unit
|
||||
_check(np.array_equal(original.view(np.uint64), raw_expected.view(np.uint64)) and
|
||||
np.array_equal(original.view(np.uint64), arrays["weights"][begin:stop].view(np.uint64)),
|
||||
"Actual original Brian weights differ from unchanged signed raw or AF base weights")
|
||||
base_hasher.update(np.ascontiguousarray(original, dtype="<f8").tobytes())
|
||||
mask = np.isin(nodes[arrays["src"][begin:stop]], bundle["apl_globals"])
|
||||
mask_count += int(mask.sum())
|
||||
mask_hasher.update(np.ascontiguousarray(arrays["edge_id"][begin:stop][mask], dtype="<i8").tobytes())
|
||||
original[mask] = np.float64(0.0)
|
||||
effective_hasher.update(np.ascontiguousarray(original, dtype="<f8").tobytes())
|
||||
effective[begin:stop] = original
|
||||
syn.w_[begin:stop] = original
|
||||
if n == 166700:
|
||||
_check(mask_count == 5845, "Full legacy APL fast-edge mask must contain exactly 5845 existing edges")
|
||||
effective.flush()
|
||||
_save(out_dir / "apl-mask-audit.json", {"count": mask_count, "ordered_original_edge_id_sha256": mask_hasher.hexdigest(),
|
||||
"base_weight_raw_bytes_sha256": base_hasher.hexdigest(), "effective_weights_file_sha256": _digest(out_dir / "weights.npy"),
|
||||
"effective_weights_raw_sha256": effective_hasher.hexdigest(),
|
||||
"rule": "zero +0 all existing fast edges whose source GLOBAL neuron is in apl_globals; no graph edge removal",
|
||||
"graded_targets": "only KC identities present in this LIF case", "KC_targets_in_case": len(kc_local),
|
||||
"base_graph_files_modified": False, "fullgraph_required_mask_count": 5845})
|
||||
if initial is not None:
|
||||
neu.v = initial["initial_v_mv"] * b2.mV
|
||||
neu.g = initial["initial_g_mv"] * b2.mV
|
||||
v_raw = neu.variables["v"].get_value()
|
||||
g_raw = neu.variables["g"].get_value()
|
||||
last_raw = neu.variables["lastspike"].get_value()
|
||||
available_raw = neu.variables["not_refractory"].get_value()
|
||||
histories = {}
|
||||
pre_apl_histories = {}
|
||||
if record_history:
|
||||
for name, dtype in (("v", "float64"), ("g", "float64"), ("ref", "int32")):
|
||||
histories[name] = _new_memmap(out_dir / f"{name}-history.{dtype}.npy", np.dtype(dtype), (steps, n))
|
||||
mapped_files.append(histories[name])
|
||||
pre_apl_histories[name] = _new_memmap(out_dir / f"preAPL-{name}.{dtype}.npy", np.dtype(dtype), (steps, n))
|
||||
mapped_files.append(pre_apl_histories[name])
|
||||
apl_history = {}
|
||||
for name, count in (("drive", 2), ("state", 2), ("inhibition", 4064)):
|
||||
apl_history[name] = _new_memmap(out_dir / f"apl-{name}.float32.npy", "<f4", (steps, count))
|
||||
mapped_files.append(apl_history[name])
|
||||
_save(out_dir / "output-schema.json", {"history_recorded": record_history, "history_shape": [steps, n],
|
||||
"state_hash_file": "tick-state-hashes.jsonl", "state_hash_encoding": "SHA256 of contiguous little-endian raw f64 V/g and i32 ref",
|
||||
"APL_dtype": "float32", "APL_h_units": "mV; gain already applied in float32",
|
||||
"APL_SI_conversion": "h.astype(float64) * float64(0.001), then separate float64 g subtraction",
|
||||
"APL_write_schedule": "end, order=0, after original reset, unconditional raw g write; recorder end/order=1",
|
||||
"APL_input": "Brian's own actual global spikes, never candidate spikes or candidate inhibition",
|
||||
"APL_shapes": {"drive": [steps, 2], "state": [steps, 2], "inhibition": [steps, 4064]},
|
||||
"request_columns": ["tick", "neuron_idx", "attempted", "accepted", "rejected"],
|
||||
"queue_protocol": "sealed Phase4AF queue_summary; original global edge IDs, including zero-weight/ref-rejected arrivals",
|
||||
"state_npz": ["step", "neuron_idx", "counts", "v", "g", "ref"],
|
||||
"complete_marker": "summary.json complete=true only after worker exit and all checks"})
|
||||
delay_steps = int(round(float(parameters["t_dly"] / b2.ms) / .1))
|
||||
ref_steps = int(round(float(parameters["t_rfc"] / b2.ms) / .1))
|
||||
_check(delay_steps == 18 and ref_steps == 22, "Author delay/refractory changed")
|
||||
queue_len = delay_steps + 1
|
||||
due_counts = np.zeros(queue_len, np.int64)
|
||||
spike_ring = [np.empty(0, np.int32) for _ in range(queue_len)]
|
||||
outdegree = np.diff(arrays["ptr"])
|
||||
request_seen = np.zeros(steps, np.uint8)
|
||||
apl_seen = np.zeros(steps, np.uint8)
|
||||
applicable_count = np.zeros(steps, np.int32)
|
||||
rejected_count = np.zeros(steps, np.int32)
|
||||
spike_count = np.zeros(steps, np.int32)
|
||||
total_counts = np.zeros(n, np.int32)
|
||||
records = {"rows": [], "peek_calls": 0, "advance_calls": 0, "installations": []}
|
||||
last_ticks = np.zeros(n, np.int64)
|
||||
final_ref = np.zeros(n, np.int32)
|
||||
permitted = np.empty(0, np.int32)
|
||||
total_events = 0
|
||||
|
||||
def tick_now():
|
||||
return int(round(float(neu.clock.t / b2.second) / .0001))
|
||||
|
||||
def current_ref(tick):
|
||||
seen = np.isfinite(last_raw) & (last_raw >= 0)
|
||||
last_ticks.fill(0)
|
||||
last_ticks[seen] = np.rint(last_raw[seen] / .0001).astype(np.int64)
|
||||
result = np.where(seen, np.maximum(ref_steps - (tick - last_ticks), 0), 0).astype(np.int32)
|
||||
_check(np.array_equal(result == 0, available_raw), "Ref history disagrees with author not_refractory")
|
||||
return result
|
||||
|
||||
with ExitStack() as stack:
|
||||
stderr = stack.enter_context((out_dir / "apl-worker.stderr.log").open("xb"))
|
||||
worker = _APLWorker(science_python, worker_path, bundle_dir, stderr, float(spec.get("worker_timeout_seconds", 180)))
|
||||
_save(out_dir / "apl-worker-handshake.json", {"command": worker.command, "handshake": worker.hello})
|
||||
queue_file = stack.enter_context((out_dir / "queue.jsonl").open("x", encoding="utf-8"))
|
||||
event_file = stack.enter_context((out_dir / "events.bin").open("xb"))
|
||||
hash_file = stack.enter_context((out_dir / "tick-state-hashes.jsonl").open("x", encoding="utf-8"))
|
||||
tick_file = stack.enter_context((out_dir / "ticks.jsonl").open("x", encoding="utf-8"))
|
||||
requests_file = stack.enter_context((out_dir / "requests.csv").open("x", encoding="utf-8", newline=""))
|
||||
request_writer = csv.DictWriter(requests_file, fieldnames=["tick", "neuron_idx", "attempted", "accepted", "rejected"])
|
||||
request_writer.writeheader()
|
||||
|
||||
def request_operation():
|
||||
nonlocal permitted
|
||||
tick = tick_now()
|
||||
_check(0 <= tick < steps and not request_seen[tick], "Invalid/duplicate common request tick")
|
||||
request_seen[tick] = 1
|
||||
begin, stop = request_ptr[tick:tick + 2]
|
||||
local = request_local[begin:stop]
|
||||
available = available_raw[local].copy()
|
||||
permitted = local[available]
|
||||
applicable_count[tick] = len(permitted)
|
||||
rejected_count[tick] = len(local) - len(permitted)
|
||||
for node, accepted in zip(requested_globals[begin:stop], available):
|
||||
request_writer.writerow({"tick": tick, "neuron_idx": int(node), "attempted": 1,
|
||||
"accepted": int(accepted), "rejected": int(not accepted)})
|
||||
if len(permitted):
|
||||
neu.v[permitted] = parameters["v_th"] + 1 * b2.mV
|
||||
|
||||
def apl_operation():
|
||||
tick = tick_now()
|
||||
_check(0 <= tick < steps and not apl_seen[tick] and request_seen[tick], "Invalid/duplicate APL tick")
|
||||
active = np.asarray(neu.spikes, dtype=np.int32)
|
||||
_check(not len(active) or np.all(np.diff(active) > 0), "Brian actual spikes are not canonical/unique")
|
||||
_check(np.all(g_raw[active] == 0.0), "APL ran before the original author g reset")
|
||||
if record_history:
|
||||
pre_apl_histories["v"][tick] = v_raw
|
||||
pre_apl_histories["g"][tick] = g_raw
|
||||
pre_apl_histories["ref"][tick] = current_ref(tick)
|
||||
drive, state, inhibition = worker.step(tick, np.asarray(nodes[active], dtype=np.int32))
|
||||
apl_history["drive"][tick] = drive
|
||||
apl_history["state"][tick] = state
|
||||
apl_history["inhibition"][tick] = inhibition
|
||||
# These are separate array operations. Do not move the unit
|
||||
# conversion into a fused subtract; do not guard by refractory.
|
||||
delta_si = inhibition[kc_in_bundle].astype(np.float64) * np.float64(.001)
|
||||
g_raw[kc_local] = g_raw[kc_local] - delta_si
|
||||
apl_seen[tick] = 1
|
||||
|
||||
def record_end():
|
||||
nonlocal last_complete_tick, total_events, final_ref
|
||||
tick = tick_now()
|
||||
_check(tick == last_complete_tick + 1 and tick < steps and apl_seen[tick], "End tick is incomplete/out of order")
|
||||
_check(len(records["rows"]) == tick + 1, "End state lacks queue observation")
|
||||
_check(np.isfinite(v_raw).all() and np.isfinite(g_raw).all(), "Nonfinite author/APL state")
|
||||
final_ref = current_ref(tick)
|
||||
if record_history:
|
||||
histories["v"][tick], histories["g"][tick], histories["ref"][tick] = v_raw, g_raw, final_ref
|
||||
active = np.asarray(neu.spikes, dtype=np.int32)
|
||||
_check(np.isin(permitted, active).all(), "An applicable request did not produce a real spike")
|
||||
spike_count[tick] = len(active)
|
||||
total_counts[active] += 1
|
||||
if len(active):
|
||||
events = np.empty(len(active), EVENT_DTYPE)
|
||||
events["step"], events["neuron_idx"] = tick, nodes[active]
|
||||
event_file.write(memoryview(events).cast("B"))
|
||||
total_events += len(active)
|
||||
spike_ring[tick % queue_len] = active.copy()
|
||||
due_counts[(tick + delay_steps) % queue_len] = int(outdegree[active].sum(dtype=np.int64))
|
||||
hash_file.write(json.dumps({"tick": tick, "v_sha256": _array_hash(v_raw, "<f8"),
|
||||
"g_sha256": _array_hash(g_raw, "<f8"), "ref_sha256": _array_hash(final_ref, "<i4")}, sort_keys=True) + "\n")
|
||||
tick_file.write(json.dumps({"tick": tick, "spikes": len(active), "requests": int(request_count[tick]),
|
||||
"applicable_requests": int(applicable_count[tick]), "ref_rejected_requests": int(rejected_count[tick]),
|
||||
"event_sha256": _array_hash(nodes[active], "<i4"),
|
||||
"due_arrivals": records["rows"][tick]["count"], "accepted_arrivals": records["rows"][tick]["accepted_count"],
|
||||
"APL_drive_sha256": _array_hash(apl_history["drive"][tick], "<f4"),
|
||||
"APL_state_sha256": _array_hash(apl_history["state"][tick], "<f4"),
|
||||
"APL_inhibition_sha256": _array_hash(apl_history["inhibition"][tick], "<f4")}, sort_keys=True) + "\n")
|
||||
_budget(budget_root, budget_limit, reserve=1 << 20)
|
||||
last_complete_tick = tick
|
||||
if (tick + 1) % 100 == 0 or tick + 1 == steps:
|
||||
for mapped in mapped_files:
|
||||
mapped.flush()
|
||||
for handle in (queue_file, event_file, hash_file, tick_file, requests_file):
|
||||
handle.flush()
|
||||
print(json.dumps({"oracle": spec["name"], "completed_ticks": tick + 1,
|
||||
"spikes": total_events, "APL_worker_ticks": worker.tick}), flush=True)
|
||||
|
||||
class QueueInstaller(BrianObject):
|
||||
def __init__(self):
|
||||
super().__init__(clock=neu.clock, when="end", order=1_000_000, name="zz_apl_queue_installer")
|
||||
|
||||
def before_run(self, run_namespace):
|
||||
installed = []
|
||||
for code in syn.pre.code_objects:
|
||||
if "_queue" not in code.namespace or "_queue.peek()" not in str(code.code):
|
||||
continue
|
||||
original = code.namespace["_queue"]
|
||||
if isinstance(original, _QueueProxy):
|
||||
original = original._original_queue
|
||||
code.namespace["_queue"] = _QueueProxy(original, tick_now, available_raw, arrays, identity_edges,
|
||||
due_counts, spike_ring, delay_steps, queue_file, records, af.queue_summary)
|
||||
installed.append(code.name)
|
||||
_check(len(installed) == 1, "Expected one original anatomical queue consumer")
|
||||
records["installations"].append(installed[0])
|
||||
|
||||
objects = [neu, syn, spikes,
|
||||
b2.NetworkOperation(request_operation, clock=neu.clock, when="before_thresholds", order=0, name="oracle_common_requests"),
|
||||
b2.NetworkOperation(apl_operation, clock=neu.clock, when="end", order=0, name="oracle_legacy_apl_unconditional"),
|
||||
b2.NetworkOperation(record_end, clock=neu.clock, when="end", order=1, name="oracle_after_apl_record"), QueueInstaller()]
|
||||
network = b2.Network(*objects)
|
||||
_save(out_dir / "schedule.json", {"schedule": list(network.schedule), "summary": str(network.scheduling_summary())})
|
||||
network.run(0 * b2.ms, namespace={})
|
||||
_save(out_dir / "compiled-code.json", ae._code_records(network))
|
||||
compiled_coefficients = ae._coefficients(neu, parameters, b2, .0001)
|
||||
coefficient_keys = ("gd", "c0", "cg", "cv", "threshold", "reset", "seed_volt")
|
||||
_check(all(float(compiled_coefficients[key]).hex() == float(baseline_coefficients[key]).hex()
|
||||
for key in coefficient_keys), "Compiled Brian coefficients differ from sealed AF")
|
||||
_check(last_complete_tick == -1 and worker.tick == 0 and not records["rows"] and not request_seen.any(),
|
||||
"Zero-duration compilation unexpectedly advanced state")
|
||||
network.run(steps * .1 * b2.ms, namespace={})
|
||||
_save(out_dir / "generated-code.json", ae._code_records(network))
|
||||
worker_status = worker.finish(steps)
|
||||
worker = None
|
||||
|
||||
_check(last_complete_tick == steps - 1 and np.all(request_seen == 1) and np.all(apl_seen == 1), "Incomplete trial")
|
||||
_check(records["peek_calls"] == records["advance_calls"] == steps, "Incomplete queue audit")
|
||||
_check(total_events == int(spikes.num_spikes) and np.array_equal(total_counts, np.asarray(spikes.count[:])),
|
||||
"Sparse events disagree with original SpikeMonitor")
|
||||
_check(np.array_equal(request_count, applicable_count + rejected_count), "Incomplete request accounting")
|
||||
_check(np.all(np.asarray(neu.rfc[:] / b2.ms) == float(parameters["t_rfc"] / b2.ms)), "Author refractory duration changed")
|
||||
for begin in range(0, edges, width):
|
||||
stop = min(edges, begin + width)
|
||||
_check(np.array_equal(np.asarray(syn.w_[begin:stop]).view(np.uint64), effective[begin:stop].view(np.uint64)),
|
||||
"Effective synaptic weights changed during the trial")
|
||||
event_values = np.memmap(out_dir / "events.bin", dtype=EVENT_DTYPE, mode="r", shape=(total_events,)) if total_events else np.empty(0, EVENT_DTYPE)
|
||||
_check((out_dir / "events.bin").stat().st_size == total_events * 8, "Sparse event byte count mismatch")
|
||||
for begin in range(0, total_events, width):
|
||||
stop = min(total_events, begin + width)
|
||||
spike_local = np.asarray(spikes.i[begin:stop], dtype=np.int32)
|
||||
np.testing.assert_array_equal(event_values["neuron_idx"][begin:stop], nodes[spike_local])
|
||||
times = np.rint(np.asarray(spikes.t[begin:stop] / b2.second) / .0001).astype(np.int32)
|
||||
np.testing.assert_array_equal(event_values["step"][begin:stop], times)
|
||||
_budget(budget_root, budget_limit, total_events * 8 + n * 24 + (8 << 20))
|
||||
with (out_dir / "state.npz").open("xb") as handle:
|
||||
np.savez_compressed(handle, step=event_values["step"], neuron_idx=event_values["neuron_idx"],
|
||||
counts=total_counts, v=v_raw, g=g_raw, ref=final_ref)
|
||||
with (out_dir / "tick-summary.npz").open("xb") as handle:
|
||||
np.savez_compressed(handle, request_count=request_count, applicable_count=applicable_count,
|
||||
rejected_count=rejected_count, spike_count=spike_count,
|
||||
arrival_count=np.asarray([row["count"] for row in records["rows"]], np.int64),
|
||||
accepted_arrival_count=np.asarray([row["accepted_count"] for row in records["rows"]], np.int64))
|
||||
coefficients = ae._coefficients(neu, parameters, b2, .0001)
|
||||
keys = ("gd", "c0", "cg", "cv", "threshold", "reset", "seed_volt")
|
||||
_check(all(float(coefficients[key]).hex() == float(baseline_coefficients[key]).hex() for key in keys),
|
||||
"Actual Brian coefficients differ from sealed AF coefficients")
|
||||
_save(out_dir / "coefficients.json", coefficients)
|
||||
_save(out_dir / "queue-audit.json", {"peek_calls": records["peek_calls"], "advance_calls": records["advance_calls"],
|
||||
"installations": records["installations"], "same_peek_object_returned": True,
|
||||
"original_edge_ids_not_sorted_or_filtered": True, "zero_weight_and_refractory_rejected_edges_retained": True,
|
||||
"post_threshold_pre_synaptic_guard_observed": True, "forwarding_contract": "sealed AE/AF design",
|
||||
"new_unwrapped_repeat_performed": False, "limit": "Current wrapper was not independently rerun without the queue proxy"})
|
||||
for path, expected in ((author_path, AUTHOR_SHA256), (ae_path, AE_SHA256), (af_path, AF_SHA256), (worker_path, worker_hash)):
|
||||
_check(_digest(path) == expected, f"Source changed during the run: {path}")
|
||||
_check(_digest(coefficients_path) == coefficient_hash, "Sealed coefficient reference changed during the run")
|
||||
for directory, hashes in ((graph_dir, graph_hashes), (bundle_dir, bundle_hashes), (case_dir, case_hashes)):
|
||||
for name, expected in hashes.items():
|
||||
_check(_digest(directory / name) == expected, f"Read-only input changed during the run: {name}")
|
||||
for mapped in mapped_files:
|
||||
mapped.flush()
|
||||
_check("torch" not in sys.modules, "Pinned Brian process unexpectedly imported Torch")
|
||||
output_bytes = _budget(budget_root, budget_limit, 1 << 20)
|
||||
summary = {"name": spec["name"], "complete": True, "precision": "float64", "APL_precision": "float32",
|
||||
"backend": "shared-APL-component closed-loop Brian2 oracle", "steps": steps, "completed_ticks": last_complete_tick + 1,
|
||||
"nodes": n, "edges": edges, "spikes": total_events, "requests": int(request_count.sum()),
|
||||
"applicable_requests": int(applicable_count.sum()), "ref_rejected_requests": int(rejected_count.sum()),
|
||||
"APL_fast_edges_zeroed": mask_count, "APL_worker_exit_status": worker_status, "APL_worker_ticks": steps,
|
||||
"apl_fast_edges_zeroed": mask_count, "effective_weights_raw_sha256": effective_hasher.hexdigest(),
|
||||
"kc_targets_present": len(kc_local),
|
||||
"candidate_trace_used_as_input": False, "independent_CPU_all_components_claim": False,
|
||||
"history_recorded": record_history, "single_continuous_run": True, "no_training": True, "no_poi": True,
|
||||
"source_and_inputs_unchanged": True, "pass_all": True,
|
||||
"pass_all_scope": "Oracle construction, protocol and internal self-checks only; candidate comparison is a separate gate",
|
||||
"scope": "Only this fixed trajectory; legacy Torch APL is a shared numerical component, not an independent CPU APL implementation",
|
||||
"output_budget_bytes": budget_limit, "output_bytes_before_summary": output_bytes,
|
||||
"state_hashes_sha256": _digest(out_dir / "tick-state-hashes.jsonl"), "queue_sha256": _digest(out_dir / "queue.jsonl"),
|
||||
"events_sha256": _digest(out_dir / "events.bin"), "weights_sha256": _digest(out_dir / "weights.npy")}
|
||||
_save(out_dir / "summary.json", summary)
|
||||
return summary
|
||||
except Exception as error:
|
||||
for mapped in mapped_files:
|
||||
mapped.flush()
|
||||
_save(out_dir / "error.json", {"complete": False, "last_complete_tick": last_complete_tick,
|
||||
"type": type(error).__name__, "message": str(error), "partial_files_preserved": True,
|
||||
"inputs_or_duration_adjusted": False})
|
||||
raise
|
||||
finally:
|
||||
if worker is not None:
|
||||
worker.abort()
|
||||
if saved_preferences is not None:
|
||||
for key, value in saved_preferences.items():
|
||||
b2.prefs[key] = value
|
||||
b2.defaultclock.dt = old_dt
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Preregister exact legacy APL data and unmodified fixed CAL requests."""
|
||||
from datetime import datetime,timezone
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yaml
|
||||
from context import ROOT,MODEL,EXP,AF_MODEL,AF_EXP,AD_EXP,CFG,save,digest,sources,integrity,budget
|
||||
|
||||
def arr(path,value):
|
||||
with Path(path).open('xb') as f:np.save(f,value,allow_pickle=False)
|
||||
|
||||
def graph(folder,nodes,full):
|
||||
folder.mkdir();nodes=np.asarray(nodes,np.int32);ptr,dst,src,raw,base=full
|
||||
if len(nodes)==CFG['neurons']:ids=np.arange(len(dst),dtype=np.int64)
|
||||
else:
|
||||
ids=np.concatenate([np.arange(ptr[s],ptr[s+1],dtype=np.int64) for s in nodes])
|
||||
ids=ids[np.isin(dst[ids],nodes)]
|
||||
lookup=np.full(CFG['neurons'],-1,np.int32);lookup[nodes]=np.arange(len(nodes),dtype=np.int32)
|
||||
ss,dd=lookup[src[ids]],lookup[dst[ids]];pp=np.r_[np.int64(0),np.bincount(ss,minlength=len(nodes)).cumsum(dtype=np.int64)]
|
||||
for name,value in dict(nodes=nodes,edge_id=ids,src=ss,dst=dd,ptr=pp,signed_raw=raw[ids],weights=base[ids]).items():arr(folder/(name+'.npy'),value)
|
||||
pd.DataFrame({'local_index':np.arange(len(nodes),dtype=np.int32)},index=pd.Index(nodes,name='global_neuron_idx')).to_csv(folder/'completeness.csv')
|
||||
pd.DataFrame({'Presynaptic_Index':ss,'Postsynaptic_Index':dd,'Excitatory x Connectivity':raw[ids],'edge_id':ids},copy=False).to_parquet(folder/'connectivity.parquet',index=False)
|
||||
save(folder/'audit.json',dict(nodes=len(nodes),edges=len(ids),canonical_original_edge_ids=True,anatomical_weights_not_modified=True,
|
||||
effective_APL_zeroing='Only in new runtime clone, never these original weight files'))
|
||||
return nodes
|
||||
|
||||
def case(name,kind,steps,nodes,graphdir,requests,initial=None,extra=None):
|
||||
folder=EXP/'cases'/name;folder.mkdir()
|
||||
model=json.loads((AD_EXP/'protocol/preregistered.json').read_text())['config']['model']
|
||||
spec=dict(name=name,kind=kind,steps=steps,dt_ms=.1,duration_ms=steps*.1,model=model,
|
||||
graph_dir=str(graphdir),apl_bundle_dir=str(EXP/'apl-bundle'),apl=CFG['apl'],record_history=kind=='fixture',
|
||||
coefficients_path=str(AF_EXP/'oracle/no_input/coefficients.json'),
|
||||
science_python=str(ROOT/'models/male-cns-lif/.venv/bin/python'),legacy_apl_worker=str(MODEL/'legacy_apl_worker.py'),
|
||||
budget_root=str(EXP),output_budget_bytes=CFG['output_budget_bytes'],precision='float64',apl_precision='float32',training=False)
|
||||
if extra:spec.update(extra)
|
||||
requests=np.asarray(requests,dtype=np.int32).reshape(-1,2)
|
||||
if len(requests):
|
||||
requests=requests[np.lexsort((requests[:,1],requests[:,0]))]
|
||||
assert len(np.unique(requests.astype(np.int64)[:,0]*CFG['neurons']+requests[:,1]))==len(requests)
|
||||
assert requests[:,0].min()>=0 and requests[:,0].max()<steps and np.isin(requests[:,1],nodes).all()
|
||||
with (folder/'requests.npz').open('xb') as f:np.savez_compressed(f,tick=requests[:,0],neuron_idx=requests[:,1])
|
||||
if kind=='fixture':
|
||||
v=np.full(len(nodes),model['resting_potential_mv'],np.float64);g=np.zeros(len(nodes),np.float64)
|
||||
if initial:
|
||||
for node,value in initial.get('v',{}).items():v[np.flatnonzero(nodes==node)[0]]=value
|
||||
for node,value in initial.get('g',{}).items():g[np.flatnonzero(nodes==node)[0]]=value
|
||||
with (folder/'arrays.npz').open('xb') as f:np.savez_compressed(f,initial_v_mv=v,initial_g_mv=g)
|
||||
save(folder/'spec.json',spec)
|
||||
|
||||
def main():
|
||||
assert not (EXP/'protocol/preregistered.json').exists()
|
||||
assert all(not (EXP/d).exists() or not any((EXP/d).iterdir()) for d in ('protocol','graphs','apl-bundle','cases','gpu','oracle'))
|
||||
assert shutil.disk_usage(EXP.parent).free>30*1024**3
|
||||
for d in ('protocol','apl-bundle','graphs','cases','gpu','oracle','component-audit','regression','calibration','audit','extension-build','logs'):(EXP/d).mkdir(parents=True,exist_ok=True)
|
||||
can=ROOT/'datasets/male-cns-v1.0/processed/neuron-only';legacy=ROOT/'models/male-cns-lif';lp=legacy/'olfactory-grounding-v1/phase4l.yaml'
|
||||
lock=ROOT/'experiments/phase4j-mb-physiology-repair/calibration/locked-parameters.json'
|
||||
files=[AF_MODEL/'manifest.json',AF_EXP/'manifest.json',AF_EXP/'oracle/no_input/coefficients.json',
|
||||
legacy/'deterministic-runtime-v2/phase4p_revalidate.py',lp,lock,can/'neuron-index/unified-metadata.parquet',can/'gpu/csr-weights.int64.npy',
|
||||
legacy/'author-runtime-lab-v1/author_ext.cpp',legacy/'author-runtime-lab-v1/author_ext.cu',legacy/'author-runtime-lab-v1/gpu_backend.py',
|
||||
legacy/'author-runtime-lab-v1/oracle_stress.py',AD_EXP/'external-resources/model.py',AF_MODEL/'oracle_full.py']
|
||||
lroot=ROOT/'experiments/phase4l-olfactory-grounding'
|
||||
files += [lroot/'odor-encoder'/x for x in ('frozen-odor-panel.parquet','mapped-glomerular-vectors.npz','selection.json')]
|
||||
files += [lroot/'receptor-mapping/olfactory-mapping.parquet']
|
||||
save(EXP/'protocol/preregistered.json',dict(timestamp_utc=datetime.now(timezone.utc).isoformat(),sources=sources(),config=CFG,
|
||||
inputs=[dict(path=str(p.relative_to(ROOT)),sha256=digest(p)) for p in files],before_any_trajectory=True,
|
||||
parent_commit=subprocess.check_output(['git','-C',str(ROOT),'rev-parse','HEAD'],text=True).strip()))
|
||||
save(EXP/'regression/old-integrity-before.json',integrity())
|
||||
locked=json.loads(lock.read_text());assert locked['parameters_locked'] and locked['calibration_only']
|
||||
assert {k:locked['selected'][k] for k in ('gain','normalization','tau_ms')}==CFG['apl']
|
||||
meta=pd.read_parquet(can/'neuron-index/unified-metadata.parquet').sort_values('neuron_idx').reset_index(drop=True)
|
||||
assert len(meta)==CFG['neurons'] and np.array_equal(meta.neuron_idx,np.arange(CFG['neurons']))
|
||||
cls=meta.annotation_class.fillna('').astype(str).to_numpy();typ=meta.annotation_type.fillna('').astype(str).to_numpy()
|
||||
kc=np.flatnonzero(cls=='Kenyon_Cell').astype(np.int32);pn=np.flatnonzero(cls=='ALPN').astype(np.int32);apl=np.flatnonzero(typ=='APL').astype(np.int32)
|
||||
assert len(kc)==4064 and len(apl)==2
|
||||
fg=AF_EXP/'graph';ptr=np.load(fg/'ptr.npy',mmap_mode='r');dst=np.load(fg/'dst.npy',mmap_mode='r');src=np.load(fg/'src.npy',mmap_mode='r')
|
||||
signed=np.load(fg/'signed_raw.npy',mmap_mode='r');base=np.load(AF_EXP/'oracle/no_input/weights.npy',mmap_mode='r');raw=np.load(can/'gpu/csr-weights.int64.npy',mmap_mode='r')
|
||||
assert len(dst)==CFG['edges'] and int(raw.sum())==CFG['raw_weight_sum']
|
||||
aplout=np.isin(src,apl);aplids=np.flatnonzero(aplout).astype(np.int64);assert len(aplids)==5845
|
||||
I=np.zeros((2,len(meta)),np.float32);O=np.zeros((2,len(kc)),np.float32)
|
||||
alook={int(v):i for i,v in enumerate(apl)};kl=np.full(len(meta),-1,np.int32);kl[kc]=np.arange(len(kc),dtype=np.int32)
|
||||
allowed=np.isin(src,np.r_[kc,pn])&np.isin(dst,apl)
|
||||
for eid in np.flatnonzero(allowed):I[alook[int(dst[eid])],int(src[eid])]+=float(raw[eid])
|
||||
for eid in np.flatnonzero(aplout&np.isin(dst,kc)):O[alook[int(src[eid])],kl[dst[eid]]]+=float(raw[eid])
|
||||
iscale=float(I[I>0].sum()/np.count_nonzero(np.any(I>0,axis=0)));oscale=float(O[O>0].mean());bundle=EXP/'apl-bundle'
|
||||
for name,value in dict(input_matrix=I/iscale,output_matrix=O/oscale,input_raw=I,output_raw=O,kc_globals=kc,pn_globals=pn,apl_globals=apl,apl_out_ids=aplids).items():arr(bundle/(name+'.npy'),value)
|
||||
save(bundle/'scales.json',dict(input_scale=iscale,output_scale=oscale))
|
||||
save(bundle/'provenance.json',dict(canonical_root=str(can),source=str(legacy/'deterministic-runtime-v2/phase4p_revalidate.py'),source_sha256=digest(legacy/'deterministic-runtime-v2/phase4p_revalidate.py'),
|
||||
parameters=CFG['apl'],lock_file=str(lock),lock_sha256=digest(lock),full_dimensions=[list(I.shape),list(O.shape)],
|
||||
APL=len(apl),KC=len(kc),ALPN=len(pn),APL_fast_edges=len(aplids),APL_nonKC_edges=int(np.count_nonzero(aplout&~np.isin(dst,kc))),
|
||||
APL_nonKC_contacts=int(raw[aplout&~np.isin(dst,kc)].sum()),normalization_recomputed_in_original_order=True,
|
||||
raw_files_modified=False))
|
||||
full=(ptr,dst,src,signed,base);fullnodes=graph(EXP/'graphs/full',np.arange(len(meta),dtype=np.int32),full)
|
||||
pscore=I.sum(axis=0);pnrank=pn[np.lexsort((pn,-pscore[pn]))];kscore=O.sum(axis=0);kcrank=kc[np.lexsort((kc,-kscore))]
|
||||
candidates=np.flatnonzero(np.isin(src,pn)&np.isin(dst,kc));best=int(candidates[np.argmax(raw[candidates])]);pairpn=int(src[best])
|
||||
pe=np.arange(ptr[pairpn],ptr[pairpn+1],dtype=np.int64);pe=pe[np.isin(dst[pe],kc)];pe=pe[np.lexsort((dst[pe],-raw[pe]))]
|
||||
collision_kc=np.unique(dst[pe[:3]]);assert len(collision_kc)==3
|
||||
nonkc=aplids[~np.isin(dst[aplids],kc)];nonkc_target=int(dst[nonkc[np.argmax(raw[nonkc])]])
|
||||
nodes=np.unique(np.r_[apl,pnrank[:16],kcrank[:16],pairpn,collision_kc,nonkc_target,[1197,1381,1511,82312,133666]]).astype(np.int32)
|
||||
fixturegraph=EXP/'graphs/fixtures';graph(fixturegraph,nodes,full)
|
||||
p=int(pnrank[0]);k=int(kcrank[0]);kc1,kc2,kc3=map(int,collision_kc)
|
||||
designs=[('zero',100,[],None),('pn_pulse',300,[(0,p)],None),
|
||||
('kc_ref_boundary',100,[(t,k) for t in (0,1,21,22,23,44)],None),
|
||||
('two_sources',300,[(t,n) for t in (0,22,44,66,88) for n in (p,k)],None),
|
||||
('arrival_collision',120,[(0,pairpn),(0,kc1),(18,kc2),(21,kc1)],None),
|
||||
('natural_request',100,[(0,k)],dict(v={k:-44.0})),
|
||||
('apl_only',100,[(0,int(a)) for a in apl],None),
|
||||
('high_fanin',400,[(t,int(n)) for t in (0,22,44) for n in nodes if pscore[n]>0],dict(g={k:.123}))]
|
||||
assert [v[0] for v in designs]==CFG['fixtures']
|
||||
for name,steps,requests,initial in designs:case(name,'fixture',steps,nodes,fixturegraph,requests,initial)
|
||||
meta.iloc[nodes][['neuron_idx','body_id','annotation_class','annotation_type','neurotransmitter']].to_csv(EXP/'protocol/fixture-identities.csv',index=False)
|
||||
save(EXP/'protocol/fixture-selection.json',dict(pn=p,kc=k,pair_PN=pairpn,arrival_KCs=collision_kc.tolist(),real_PN_KC_edge=best,
|
||||
nonKC_APL_target=nonkc_target,method='Static raw-count ranks and exact metadata; retain ALL existing induced edges and full APL matrices/norms',runtime_selection=False))
|
||||
os.environ['CUDA_VISIBLE_DEVICES']=CFG['gpu_uuid'];sys.path.insert(0,str(legacy));sys.path.insert(0,str(legacy/'deterministic-runtime-v2'))
|
||||
sys.path.insert(0,str(legacy/'terminal-scope-diagnostic-v1'));from diagnostic import queues
|
||||
queues()
|
||||
import torch
|
||||
from phase4p_revalidate import door_inputs,make_drive
|
||||
oc,panel,vectors,selection,mapping=door_inputs();cal=panel[panel.split=='calibration'].copy();assert len(panel)==12 and len(cal)==6
|
||||
assert oc['odor_encoder']['evaluation_seeds']==CFG['calibration']['seeds']
|
||||
assert float(oc['odor_encoder']['duration_ms'])==CFG['calibration']['duration_ms']
|
||||
cal.to_parquet(EXP/'protocol/calibration-panel.parquet',index=False);calrecords=[]
|
||||
for mode in CFG['calibration']['modes']:
|
||||
for oi,(_,odor) in enumerate(cal.iterrows()):
|
||||
targets,rates=make_drive(oc,panel,vectors,selection,mapping,odor.odor_id,mode)
|
||||
assert len(np.unique(targets))==len(targets),'Original encoder emits duplicate PN targets; audit instead of silently merging'
|
||||
rates=np.asarray(rates,np.float32);steps=round(CFG['calibration']['duration_ms']/.1)
|
||||
for seed in CFG['calibration']['seeds']:
|
||||
gen=torch.Generator(device='cuda');gen.manual_seed(seed)
|
||||
original_probability=rates*.1/1000.0
|
||||
explicit_probability=rates*np.float32(.1)/np.float32(1000.)
|
||||
assert original_probability.dtype==np.float32 and np.array_equal(original_probability.view(np.uint32),explicit_probability.view(np.uint32))
|
||||
probs=torch.as_tensor(original_probability,dtype=torch.float32,device='cuda')
|
||||
forced=(torch.rand((steps,len(targets)),generator=gen,device='cuda')<probs[None,:]).cpu().numpy()
|
||||
tt,cc=np.nonzero(forced);req=np.column_stack([tt,targets[cc]]).astype(np.int32)
|
||||
name=f'cal-{mode}-{oi:02d}-s{seed}'
|
||||
case(name,'calibration',steps,fullnodes,EXP/'graphs/full',req,extra=dict(odor_id=str(odor.odor_id),odor_name=str(odor.odor_name),
|
||||
mode=mode,seed=seed,odor_index=oi,expected_external_events=float(rates.sum())*steps*.1/1000.,target_count=len(targets),rate_sum_hz=float(rates.sum())))
|
||||
with (EXP/'cases'/name/'pn-drive.npz').open('xb') as f:np.savez_compressed(f,targets=targets,rates_hz=rates)
|
||||
calrecords.append(dict(name=name,mode=mode,odor_index=oi,odor_id=str(odor.odor_id),seed=seed,requests=len(req),rates_sum=float(rates.sum())))
|
||||
save(EXP/'protocol/calibration-cases.json',calrecords)
|
||||
save(EXP/'protocol/encoder-audit.json',dict(panel_count=12,cal_count=6,median_mapped_response_sum=float(np.median(panel.mapped_response_sum)),
|
||||
target_rng_column_order='Original pn_drive, no sorting before torch.rand',seeds=CFG['calibration']['seeds'],
|
||||
requested_before_any_dynamics=True,formal_AB_run=False,heldout_run=False,torch=torch.__version__,numpy=np.__version__,
|
||||
generation='Original rates*.1/1000 float32 GPU Bernoulli, independent per-case original seed'))
|
||||
save(EXP/'protocol/prepared-hashes.json',[dict(path=str(p.relative_to(EXP)),bytes=p.stat().st_size,sha256=digest(p))
|
||||
for d in ('graphs','apl-bundle','cases','protocol') for p in sorted((EXP/d).rglob('*')) if p.is_file() and p.name!='prepared-hashes.json'])
|
||||
budget();print('PREPARED8 fixtures and60 fixed CAL inputs; no trajectories; frozen integrity PASS',flush=True)
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,48 @@
|
||||
"""A fresh process per declared case; no concurrent scientific GPU jobs."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
from context import ROOT,MODEL,EXP,CFG,check,budget
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument('backend',choices=['gpu','oracle','component'])
|
||||
parser.add_argument('case')
|
||||
parser.add_argument('--repeat',type=int,default=0)
|
||||
args=parser.parse_args()
|
||||
assert '/' not in args.case and '..' not in args.case and 0<=args.repeat<3
|
||||
check();budget()
|
||||
os.environ['CUDA_VISIBLE_DEVICES']=CFG['gpu_uuid']
|
||||
os.environ['CUDA_HOME']='/usr/local/cuda'
|
||||
os.environ['PATH']=str(ROOT/'models/male-cns-lif/.venv/bin')+':/usr/local/cuda/bin:'+os.environ['PATH']
|
||||
os.environ['MAX_JOBS']='2';os.environ['TORCH_CUDA_ARCH_LIST']='8.9'
|
||||
# The pinned Brian environment intentionally has neither Torch nor PyYAML.
|
||||
# Keep this preflight standard-library-only; never install into that env.
|
||||
for port in (8188,8189,8190):
|
||||
with urllib.request.urlopen(f'http://192.168.10.51:{port}/queue',timeout=5) as response:
|
||||
queue=json.load(response)
|
||||
assert not queue['queue_running'] and not queue['queue_pending'],'ComfyUI busy: defer, never evict'
|
||||
case=EXP/'cases'/args.case
|
||||
assert case.is_dir()
|
||||
out=EXP/'gpu'/f'{args.case}-r{args.repeat}'
|
||||
if args.backend=='gpu':
|
||||
from gpu_apl import run_gpu
|
||||
result=run_gpu(case,out,EXP/'extension-build')
|
||||
elif args.backend=='oracle':
|
||||
assert args.repeat==0
|
||||
from oracle_apl import run_case
|
||||
result=run_case(case,EXP/'oracle'/args.case)
|
||||
else:
|
||||
assert args.repeat==0
|
||||
from validate_components import validate
|
||||
result=validate(out,EXP/'apl-bundle',EXP/'component-audit'/(args.case+'.json'))
|
||||
budget()
|
||||
print(json.dumps(dict(backend=args.backend,case=args.case,repeat=args.repeat,
|
||||
complete=result.get('complete'),pass_all=result.get('pass_all')),allow_nan=False),flush=True)
|
||||
if not result.get('pass_all',False):raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Ordered numerical gates before any full CAL; never tune from outcomes."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
import traceback
|
||||
from context import ROOT,MODEL,EXP,CFG,check,save,budget
|
||||
|
||||
SCIENCE=ROOT/'models/male-cns-lif/.venv/bin/python'
|
||||
ORACLE=ROOT/'experiments/phase4ad-author-semantics/environment/.venv/bin/python'
|
||||
|
||||
|
||||
def child(backend,name,repeat=0):
|
||||
log=EXP/'logs'/f'{backend}-{name}-r{repeat}.log'
|
||||
cmd=[str(ORACLE if backend=='oracle' else SCIENCE),'-B',str(MODEL/'run_case.py'),backend,name,'--repeat',str(repeat)]
|
||||
print('START',backend,name,repeat,flush=True);t0=time.perf_counter()
|
||||
with log.open('x',encoding='utf-8') as f:
|
||||
result=subprocess.run(cmd,stdout=f,stderr=subprocess.STDOUT,timeout=1800,check=False)
|
||||
print('FINISH',backend,name,repeat,'exit',result.returncode,'wall',round(time.perf_counter()-t0,2),flush=True)
|
||||
if result.returncode:raise RuntimeError(f'{backend} {name} failed; inspect {log}')
|
||||
|
||||
|
||||
def gate_case(name,repeats):
|
||||
from compare import compare,compare_gpu_repeats
|
||||
child('gpu',name)
|
||||
for r in range(1,repeats):child('gpu',name,r)
|
||||
child('oracle',name)
|
||||
result=compare(EXP/'cases'/name,EXP/'gpu'/(name+'-r0'),EXP/'oracle'/name,EXP/'audit'/('compare-'+name+'.json'))
|
||||
assert result['pass_all'],f'Closed-loop exact comparison failed: {name}; no later CAL permitted'
|
||||
for r in range(1,repeats):
|
||||
report=compare_gpu_repeats(EXP/'gpu'/(name+'-r0'),EXP/'gpu'/f'{name}-r{r}',EXP/'audit'/f'repeat-{name}-r{r}.json')
|
||||
assert report['pass_all'],f'Exact GPU repeat failed: {name}'
|
||||
child('component',name)
|
||||
return dict(case=name,pass_all=True,gpu_repeats=repeats)
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser();parser.add_argument('stage',choices=['fixtures','probes','calibration']);args=parser.parse_args()
|
||||
check();budget();receipt=EXP/'audit'/(args.stage+'-gate.json');assert not receipt.exists()
|
||||
try:
|
||||
if args.stage=='fixtures':
|
||||
rows=[gate_case(n,CFG['fixture_repeats']) for n in CFG['fixtures']]
|
||||
save(receipt,dict(pass_all=True,cases=rows,scope='Fixed real-edge induced fixtures only'))
|
||||
elif args.stage=='probes':
|
||||
assert json.loads((EXP/'audit/fixtures-gate.json').read_text())['pass_all']
|
||||
rows=[gate_case(n,1) for n in CFG['calibration']['full_oracle_probes']]
|
||||
save(receipt,dict(pass_all=True,cases=rows,scope='Only two original first-CAL fixed fullgraph trajectories'))
|
||||
else:
|
||||
assert json.loads((EXP/'audit/fixtures-gate.json').read_text())['pass_all']
|
||||
assert json.loads((EXP/'audit/probes-gate.json').read_text())['pass_all']
|
||||
records=json.loads((EXP/'protocol/calibration-cases.json').read_text());assert len(records)==60
|
||||
probes=CFG['calibration']['full_oracle_probes']
|
||||
for rec in records:
|
||||
if rec['name'] not in probes:child('gpu',rec['name'])
|
||||
if rec['seed']==CFG['calibration']['seeds'][0]:child('gpu',rec['name'],1)
|
||||
from calibration import summarize
|
||||
result=summarize()
|
||||
save(receipt,dict(pass_all=result['pass_all'],scope='Complete original six-CAL representation gate, not heldout or learning',summary='calibration/summary.json'))
|
||||
if not result['pass_all']:raise SystemExit(2)
|
||||
except Exception as e:
|
||||
save(EXP/'audit'/('stopped-'+args.stage+'.json'),dict(stage=args.stage,type=type(e).__name__,message=str(e),traceback=traceback.format_exc(),
|
||||
later_stages_not_permitted=True,partial_outputs_preserved=True,parameters_changed=False))
|
||||
raise
|
||||
budget()
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Local, synthetic tests only: no CUDA, scientific trajectories, or heldout data."""
|
||||
import copy
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from calibration import (ORIGINAL_MODES, ORIGINAL_SEEDS, ORIGINAL_CAL_IDS,
|
||||
APL_FILES, STATE_DTYPES, arrays_bitwise_equal, evaluate_gates, exact_repeat, pair_metrics,
|
||||
sample_cv, summarize_mode, validate_inventory, validate_state)
|
||||
|
||||
|
||||
GATES = {"sparse_min_fraction": .03, "sparse_max_fraction": .09,
|
||||
"minimum_separable_odor_pairs": 3, "max_fraction_strict": .5, "min_fraction_strict": 0.,
|
||||
"same_jaccard_gt_between": True, "between_euclidean_gt_within": True,
|
||||
"both_modes_required": True, "finite_required": True, "repeat_exact_required": True}
|
||||
|
||||
|
||||
def synthetic_runs(silent=False):
|
||||
result = {}
|
||||
for mode in ORIGINAL_MODES:
|
||||
for odor in range(6):
|
||||
for seed in ORIGINAL_SEEDS:
|
||||
counts = np.zeros(1000, dtype=np.int32)
|
||||
if not silent:
|
||||
counts[odor * 50:(odor + 1) * 50] = 1
|
||||
result[(mode, str(odor), seed)] = {"kc_counts": counts,
|
||||
"expected_external_events": 10., "external_events": 0 if silent else 10}
|
||||
return result
|
||||
|
||||
|
||||
class CalibrationTests(unittest.TestCase):
|
||||
def test_complete_gate_and_original_pair_counts(self):
|
||||
summary = summarize_mode(ORIGINAL_MODES[0], list(map(str, range(6))), ORIGINAL_SEEDS, synthetic_runs(), GATES)
|
||||
self.assertTrue(summary["gate"])
|
||||
self.assertEqual(summary["within_distance_count"], 60)
|
||||
self.assertEqual(summary["between_distance_count"], 75)
|
||||
self.assertEqual(summary["pair_count"], 15)
|
||||
self.assertEqual(summary["pairwise_separation_pass_count"], 15)
|
||||
self.assertTrue(all(row["within_distance_count"] == 20 and row["between_distance_count"] == 5 for row in summary["pairs"]))
|
||||
|
||||
def test_silent_is_not_pass(self):
|
||||
summary = summarize_mode(ORIGINAL_MODES[0], list(map(str, range(6))), ORIGINAL_SEEDS, synthetic_runs(True), GATES)
|
||||
self.assertFalse(summary["gate"])
|
||||
self.assertFalse(summary["conditions"]["minimum_strictly_positive"])
|
||||
self.assertFalse(summary["conditions"]["same_jaccard_gt_different"])
|
||||
self.assertFalse(summary["conditions"]["different_euclidean_gt_same"])
|
||||
self.assertEqual(summary["pairwise_separation_pass_count"], 0)
|
||||
self.assertIsNone(summary["actual_event_count_cv"])
|
||||
|
||||
def test_every_original_conjunct_is_necessary(self):
|
||||
baseline = summarize_mode(ORIGINAL_MODES[0], list(map(str, range(6))), ORIGINAL_SEEDS, synthetic_runs(), GATES)
|
||||
changes = (("KC_active_fraction_mean", .029), ("KC_active_fraction_mean", .091),
|
||||
("KC_active_fraction_max", .5), ("KC_active_fraction_min", 0.),
|
||||
("same_odor_jaccard_mean", baseline["different_odor_jaccard_mean"]),
|
||||
("different_odor_euclidean_mean", baseline["same_odor_euclidean_mean"]),
|
||||
("pairwise_separation_pass_count", 2))
|
||||
for key, value in changes:
|
||||
with self.subTest(key=key, value=value):
|
||||
modified = dict(baseline, **{key: value})
|
||||
self.assertFalse(all(evaluate_gates(modified, GATES).values()))
|
||||
for value in (.03, .09):
|
||||
self.assertTrue(evaluate_gates(dict(baseline, KC_active_fraction_mean=value), GATES)["mean_sparsity_in_closed_interval"])
|
||||
|
||||
def test_pair_specific_not_global_baseline(self):
|
||||
runs = synthetic_runs()
|
||||
mode = ORIGINAL_MODES[0]
|
||||
for odor in range(6):
|
||||
for index, seed in enumerate(ORIGINAL_SEEDS):
|
||||
counts = np.array([index + odor * 100] if odor < 2 else [10000 * odor + index * 1000], dtype=np.int32)
|
||||
runs[(mode, str(odor), seed)]["kc_counts"] = counts
|
||||
summary = summarize_mode(mode, list(map(str, range(6))), ORIGINAL_SEEDS, runs, GATES)
|
||||
first_pair = summary["pairs"][0]
|
||||
self.assertEqual(first_pair["within_this_pair_mean"], 2.)
|
||||
self.assertEqual(first_pair["between_same_seed_mean"], 100.)
|
||||
self.assertTrue(first_pair["gate"])
|
||||
self.assertGreater(summary["same_odor_euclidean_mean"], 100.)
|
||||
|
||||
def test_original_metric_zero_and_count_semantics(self):
|
||||
zero = np.zeros(3, dtype=np.int32)
|
||||
self.assertEqual(pair_metrics(zero, zero)["active_jaccard"], 1.)
|
||||
self.assertEqual(pair_metrics(zero, zero)["correlation"], 1.)
|
||||
result = pair_metrics(np.array([2, 0, 0]), np.array([1, 0, 0]))
|
||||
self.assertEqual(result["euclidean_distance"], 1.)
|
||||
self.assertEqual(result["active_jaccard"], 1.)
|
||||
self.assertAlmostEqual(sample_cv([1., 2., 3.]), .5)
|
||||
|
||||
def test_repeat_comparison_is_bits_and_dtype(self):
|
||||
self.assertFalse(arrays_bitwise_equal(np.array([0.], np.float64), np.array([-0.], np.float64)))
|
||||
self.assertFalse(arrays_bitwise_equal(np.array([1], np.int32), np.array([1], np.int64)))
|
||||
self.assertTrue(arrays_bitwise_equal(np.array([1.], np.float32), np.array([1.], np.float32)))
|
||||
|
||||
def test_exact_repeat_covers_all_recorded_semantics(self):
|
||||
first = {"state": {name: np.zeros(1, dtype) for name, dtype in STATE_DTYPES.items()},
|
||||
"apl": {name: np.zeros((1, width), np.float32) for name, width in APL_FILES.items()},
|
||||
"hashes": [{"tick": 0}], "queues": [{"tick": 0}],
|
||||
"requests": [], "component": {"environment": {"tf32": False}}}
|
||||
self.assertTrue(exact_repeat(first, copy.deepcopy(first))["pass_all"])
|
||||
for category in ("state", "apl"):
|
||||
for name in first[category]:
|
||||
bad = copy.deepcopy(first)
|
||||
bad[category][name].flat[0] = 1
|
||||
with self.subTest(category=category, name=name):
|
||||
self.assertFalse(exact_repeat(first, bad)["pass_all"])
|
||||
for key in ("hashes", "queues", "requests", "component"):
|
||||
bad = copy.deepcopy(first)
|
||||
bad[key] = {} if isinstance(first[key], list) else []
|
||||
with self.subTest(key=key):
|
||||
self.assertFalse(exact_repeat(first, bad)["pass_all"])
|
||||
|
||||
def test_raw_state_rejects_nonfinite_duplicate_and_wrong_counts(self):
|
||||
state = {"step": np.array([0], np.int32), "neuron_idx": np.array([1], np.int32),
|
||||
"counts": np.array([0, 1], np.int32), "v": np.zeros(2, np.float64),
|
||||
"g": np.zeros(2, np.float64), "ref": np.zeros(2, np.int32)}
|
||||
validate_state(state, 2, 10)
|
||||
for mutation in ("nan", "counts", "duplicate", "wrong_dtype"):
|
||||
bad = copy.deepcopy(state)
|
||||
if mutation == "nan": bad["g"][0] = np.nan
|
||||
if mutation == "counts": bad["counts"][0] = 1
|
||||
if mutation == "duplicate":
|
||||
bad["step"] = np.array([0, 0], np.int32)
|
||||
bad["neuron_idx"] = np.array([1, 1], np.int32)
|
||||
bad["counts"][1] = 2
|
||||
if mutation == "wrong_dtype": bad["v"] = bad["v"].astype(np.float32)
|
||||
with self.subTest(mutation=mutation), self.assertRaises(ValueError):
|
||||
validate_state(bad, 2, 10)
|
||||
|
||||
def test_inventory_rejects_missing_duplicate_or_heldout_identity(self):
|
||||
odors = sorted(ORIGINAL_CAL_IDS)
|
||||
records = [{"name": f"cal-{mode}-{index:02d}-s{seed}", "mode": mode, "odor_index": index,
|
||||
"odor_id": odors[index], "seed": seed} for mode in ORIGINAL_MODES
|
||||
for index in range(6) for seed in ORIGINAL_SEEDS]
|
||||
cfg = {"calibration": {"modes": list(ORIGINAL_MODES), "seeds": list(ORIGINAL_SEEDS),
|
||||
"duration_ms": 100., "odor_count": 6, "split": "calibration", "repeat_first_seed_each_odor": True}, "gates": GATES}
|
||||
self.assertEqual(validate_inventory(records, cfg), odors)
|
||||
for malformed in (records[:-1], records[:-1] + [records[0]],
|
||||
[dict(row, odor_id="heldout") if row["odor_index"] == 0 else row for row in records]):
|
||||
with self.assertRaises(ValueError):
|
||||
validate_inventory(malformed, cfg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,266 @@
|
||||
"""Local/remote pure protocol tests; no Brian, Torch, GPU or network execution.
|
||||
|
||||
Run with the already-provisioned Python and ``-B test_protocol.py``. The
|
||||
sidecar process is mocked with in-memory stdin and an anonymous stdout pipe.
|
||||
These tests validate transport and declared arithmetic/input contracts, not
|
||||
the scientific trajectory or independent correctness of the Torch component.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
import types
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def load_no_cache(path, name):
|
||||
module = types.ModuleType(name)
|
||||
module.__file__ = str(path)
|
||||
exec(compile(path.read_bytes(), str(path), "exec"), module.__dict__)
|
||||
return module
|
||||
|
||||
|
||||
ORACLE = load_no_cache(HERE / "oracle_apl.py", "phase4ag_protocol_oracle")
|
||||
|
||||
|
||||
def handshake(**updates):
|
||||
result = {"ok": True, "protocol": "phase4ag-apl-v1", "n_neurons": 166700,
|
||||
"n_apl": 2, "n_kc": 4064, "dt_ms": .1,
|
||||
"files_sha256": {name: "frozen-test-hash" for name in ORACLE.BUNDLE_FILES},
|
||||
"decay_f32_bits": f"{int(np.float32(math.exp(-.1 / 20.)).view(np.uint32)):08x}",
|
||||
"gain_f32_bits": f"{int(np.float32(.01).view(np.uint32)):08x}"}
|
||||
result.update(updates)
|
||||
return (json.dumps(result) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def frame(tick=0, values=None, byte_count=None):
|
||||
values = np.arange(4068, dtype="<f4") if values is None else np.asarray(values, dtype="<f4")
|
||||
count = values.nbytes if byte_count is None else byte_count
|
||||
return struct.pack("<qQ", tick, count) + values.tobytes()
|
||||
|
||||
|
||||
class MemoryInput(io.BytesIO):
|
||||
def close(self):
|
||||
if not self.closed:
|
||||
self.closed_data = self.getvalue()
|
||||
super().close()
|
||||
|
||||
|
||||
class FakeProcess:
|
||||
"""The only use of OS descriptors is an anonymous read-only response pipe."""
|
||||
def __init__(self, response, status=0, chunk_size=97):
|
||||
read_fd, write_fd = os.pipe()
|
||||
self.stdin = MemoryInput()
|
||||
self.stdout = os.fdopen(read_fd, "rb", buffering=0)
|
||||
self.status = status
|
||||
self.terminated = False
|
||||
|
||||
def write_response():
|
||||
try:
|
||||
for begin in range(0, len(response), chunk_size):
|
||||
view = memoryview(response[begin:begin + chunk_size])
|
||||
while view:
|
||||
view = view[os.write(write_fd, view):]
|
||||
except (BrokenPipeError, OSError):
|
||||
# Invalid-response tests deliberately close the reader early.
|
||||
pass
|
||||
finally:
|
||||
os.close(write_fd)
|
||||
|
||||
self.thread = threading.Thread(target=write_response, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def poll(self):
|
||||
return self.status
|
||||
|
||||
def wait(self, timeout=None):
|
||||
return self.status
|
||||
|
||||
def terminate(self):
|
||||
self.terminated = True
|
||||
|
||||
def kill(self):
|
||||
self.terminated = True
|
||||
|
||||
def close(self):
|
||||
if not self.stdin.closed:
|
||||
self.stdin.close()
|
||||
if not self.stdout.closed:
|
||||
self.stdout.close()
|
||||
self.thread.join(timeout=2)
|
||||
|
||||
|
||||
class ProtocolTests(unittest.TestCase):
|
||||
def create_worker(self, response, status=0):
|
||||
process = FakeProcess(response, status=status)
|
||||
self.addCleanup(process.close)
|
||||
source_patch = mock.patch.object(ORACLE, "_digest", return_value="frozen-test-hash")
|
||||
process_patch = mock.patch.object(ORACLE.subprocess, "Popen", return_value=process)
|
||||
source_patch.start()
|
||||
process_mock = process_patch.start()
|
||||
self.addCleanup(source_patch.stop)
|
||||
self.addCleanup(process_patch.stop)
|
||||
worker = ORACLE._APLWorker(Path("science-python"), Path("legacy_apl_worker.py"), Path("bundle"), None, timeout=2)
|
||||
self.addCleanup(worker.abort)
|
||||
return worker, process, process_mock
|
||||
|
||||
def test_exact_handshake_frame_and_clean_eof(self):
|
||||
worker, process, process_mock = self.create_worker(handshake() + frame())
|
||||
globals_ = np.array([1, 40, 166699], dtype=np.int32)
|
||||
drive, state, inhibition = worker.step(0, globals_)
|
||||
values = np.arange(4068, dtype=np.float32)
|
||||
np.testing.assert_array_equal(drive, values[:2])
|
||||
np.testing.assert_array_equal(state, values[2:4])
|
||||
np.testing.assert_array_equal(inhibition, values[4:])
|
||||
request = process.stdin.getvalue()
|
||||
self.assertEqual(struct.unpack("<qQ", request[:16]), (0, 3))
|
||||
np.testing.assert_array_equal(np.frombuffer(request[16:], dtype="<i4"), globals_)
|
||||
self.assertEqual(worker.finish(1), 0)
|
||||
command = process_mock.call_args.args[0]
|
||||
self.assertEqual(command, ["science-python", "-B", "legacy_apl_worker.py", "--bundle-dir", "bundle", "--dt-ms", "0.1"])
|
||||
self.assertFalse(process_mock.call_args.kwargs["shell"])
|
||||
|
||||
def test_empty_spike_frame_still_advances_one_tick(self):
|
||||
worker, process, _ = self.create_worker(handshake() + frame(values=np.zeros(4068, np.float32)))
|
||||
arrays = worker.step(0, np.empty(0, np.int32))
|
||||
self.assertTrue(all(np.all(value == 0) for value in arrays))
|
||||
self.assertEqual(process.stdin.getvalue(), struct.pack("<qQ", 0, 0))
|
||||
self.assertEqual(worker.finish(1), 0)
|
||||
|
||||
def test_two_consecutive_ticks(self):
|
||||
worker, _, _ = self.create_worker(handshake() + frame(0) + frame(1))
|
||||
worker.step(0, np.array([4], np.int32))
|
||||
worker.step(1, np.array([8, 10], np.int32))
|
||||
self.assertEqual(worker.finish(2), 0)
|
||||
|
||||
def test_rejects_wrong_protocol_or_dimensions(self):
|
||||
for update in ({"protocol": "other"}, {"ok": False}, {"n_neurons": 42},
|
||||
{"n_kc": 3}, {"dt_ms": .2}, {"gain_f32_bits": "00000000"},
|
||||
{"files_sha256": {}}):
|
||||
with self.subTest(update=update):
|
||||
with self.assertRaises(ValueError):
|
||||
self.create_worker(handshake(**update))
|
||||
|
||||
def test_rejects_wrong_tick_or_payload_length(self):
|
||||
for response in (frame(tick=1), frame(byte_count=8)):
|
||||
with self.subTest(response_header=response[:16]):
|
||||
worker, _, _ = self.create_worker(handshake() + response)
|
||||
with self.assertRaises(ValueError):
|
||||
worker.step(0, np.empty(0, np.int32))
|
||||
|
||||
def test_rejects_truncated_response(self):
|
||||
worker, _, _ = self.create_worker(handshake() + frame()[:-1])
|
||||
with self.assertRaises(EOFError):
|
||||
worker.step(0, np.empty(0, np.int32))
|
||||
|
||||
def test_rejects_nonfinite_or_negative_apl_payload(self):
|
||||
for bad_value in (np.nan, np.inf, -1):
|
||||
with self.subTest(value=bad_value):
|
||||
values = np.zeros(4068, np.float32)
|
||||
values[-1] = bad_value
|
||||
worker, _, _ = self.create_worker(handshake() + frame(values=values))
|
||||
with self.assertRaises(ValueError):
|
||||
worker.step(0, np.empty(0, np.int32))
|
||||
|
||||
def test_rejects_invalid_spike_dtype_order_and_range(self):
|
||||
for values in (np.array([1], np.int64), np.array([2, 1], np.int32),
|
||||
np.array([1, 1], np.int32), np.array([-1], np.int32),
|
||||
np.array([166700], np.int32)):
|
||||
with self.subTest(values=values):
|
||||
worker, process, _ = self.create_worker(handshake() + frame())
|
||||
with self.assertRaises(ValueError):
|
||||
worker.step(0, values)
|
||||
self.assertEqual(process.stdin.getvalue(), b"")
|
||||
|
||||
def test_rejects_skipped_request_tick(self):
|
||||
worker, process, _ = self.create_worker(handshake() + frame())
|
||||
with self.assertRaises(ValueError):
|
||||
worker.step(1, np.empty(0, np.int32))
|
||||
self.assertEqual(process.stdin.getvalue(), b"")
|
||||
|
||||
def test_rejects_failed_exit_and_trailing_stdout(self):
|
||||
for suffix, status in ((b"", 7), (b"unexpected", 0)):
|
||||
with self.subTest(status=status, suffix=suffix):
|
||||
worker, _, _ = self.create_worker(handshake() + frame() + suffix, status=status)
|
||||
worker.step(0, np.empty(0, np.int32))
|
||||
with self.assertRaises(ValueError):
|
||||
worker.finish(1)
|
||||
|
||||
def test_pipe_reader_timeout(self):
|
||||
read_fd, write_fd = os.pipe()
|
||||
stream = os.fdopen(read_fd, "rb", buffering=0)
|
||||
reader = ORACLE._PipeReader(stream, .05)
|
||||
try:
|
||||
with self.assertRaises(TimeoutError):
|
||||
reader.read(1)
|
||||
finally:
|
||||
os.close(write_fd)
|
||||
reader.thread.join(timeout=2)
|
||||
stream.close()
|
||||
self.assertFalse(reader.thread.is_alive())
|
||||
|
||||
|
||||
class ArithmeticAndIdentityContractTests(unittest.TestCase):
|
||||
def test_si_conversion_must_follow_float64_promotion(self):
|
||||
h32 = np.array([.01, 1 / 7, 123.456], dtype=np.float32)
|
||||
correct = h32.astype(np.float64) * np.float64(.001)
|
||||
early_conversion = (h32 * np.float32(.001)).astype(np.float64)
|
||||
self.assertFalse(np.array_equal(correct.view(np.uint64), early_conversion.view(np.uint64)))
|
||||
for actual, original in zip(correct, h32):
|
||||
self.assertEqual(actual.hex(), (float(original) * float(.001)).hex())
|
||||
|
||||
def test_declared_overlay_does_not_guard_refractory_targets(self):
|
||||
before = np.array([0, -.002, .001], dtype=np.float64)
|
||||
refractory = np.array([22, 21, 0], dtype=np.int32)
|
||||
h32 = np.array([1, 2, 3], dtype=np.float32)
|
||||
delta = h32.astype(np.float64) * np.float64(.001)
|
||||
after = before - delta
|
||||
self.assertTrue(np.all(after != before))
|
||||
np.testing.assert_array_equal(refractory, [22, 21, 0])
|
||||
self.assertEqual(after[0], -.001)
|
||||
|
||||
def test_af_queue_hash_keeps_original_noncontiguous_ids_and_rejections(self):
|
||||
path = HERE.parent / "author-fullgraph-lab-v1" / "oracle_full.py"
|
||||
self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), ORACLE.AF_SHA256)
|
||||
af = load_no_cache(path, "phase4ag_protocol_af")
|
||||
edges = np.array([81, 2, 500], dtype=np.int64)
|
||||
accepted = np.array([1, 0, 1], dtype=np.uint8)
|
||||
original_edges, original_mask = edges.copy(), accepted.copy()
|
||||
result = af.queue_summary(18, edges, accepted, chunk_bytes=8)
|
||||
packet = b"phase4af-queue-v1\0" + struct.pack("<qq", 18, 3) + edges.astype("<i8").tobytes() + accepted.tobytes()
|
||||
self.assertEqual(result["joint_sha256"], hashlib.sha256(packet).hexdigest())
|
||||
self.assertEqual((result["count"], result["accepted_count"]), (3, 2))
|
||||
self.assertEqual(result, af.queue_summary(18, edges, accepted, chunk_bytes=8192))
|
||||
np.testing.assert_array_equal(edges, original_edges)
|
||||
np.testing.assert_array_equal(accepted, original_mask)
|
||||
|
||||
def test_empty_queue_hash_includes_tick(self):
|
||||
af = load_no_cache(HERE.parent / "author-fullgraph-lab-v1" / "oracle_full.py", "phase4ag_empty_af")
|
||||
first = af.queue_summary(0, np.empty(0, np.int64), np.empty(0, np.uint8))
|
||||
second = af.queue_summary(1, np.empty(0, np.int64), np.empty(0, np.uint8))
|
||||
self.assertEqual(first["ordered_edge_sha256"], hashlib.sha256(b"").hexdigest())
|
||||
self.assertNotEqual(first["joint_sha256"], second["joint_sha256"])
|
||||
|
||||
def test_oracle_import_has_no_brian_or_torch_dependency(self):
|
||||
before = set(sys.modules)
|
||||
load_no_cache(HERE / "oracle_apl.py", "phase4ag_second_import")
|
||||
added = set(sys.modules) - before
|
||||
self.assertFalse(any(name == "brian2" or name.startswith("brian2.") or
|
||||
name == "torch" or name.startswith("torch.") for name in added))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,431 @@
|
||||
"""Independent extraction/replay of the frozen legacy APL component.
|
||||
|
||||
Only the initializer prefix before ``self.base`` and two original APL update
|
||||
statements are executed. No old simulator is instantiated, no learning code is
|
||||
executed, and no frozen artifact is written. The Torch replay is a component
|
||||
oracle, not an independent CPU whole-brain closed-loop oracle.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import copy
|
||||
from functools import lru_cache
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import types
|
||||
|
||||
import numpy as np
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
|
||||
|
||||
ROOT = Path('/data/digital-drosophila')
|
||||
CANONICAL = ROOT/'datasets/male-cns-v1.0/processed/neuron-only'
|
||||
SOURCE = ROOT/'models/male-cns-lif/deterministic-runtime-v2/phase4p_revalidate.py'
|
||||
SOURCE_SHA256 = 'c7e693a2da70acdce4f53af5c2853e52e8333acc93f88a46a5e8083eac639346'
|
||||
N, A, K = 166700, 2, 4064
|
||||
BUNDLE_ARRAYS = {
|
||||
'input_matrix': (np.float32, (A, N)), 'output_matrix': (np.float32, (A, K)),
|
||||
'input_raw': (np.float32, (A, N)), 'output_raw': (np.float32, (A, K)),
|
||||
'kc_globals': (np.int32, (K,)), 'apl_globals': (np.int32, (A,)),
|
||||
'pn_globals': (np.int32, None), 'apl_out_ids': (np.int64, (5845,)),
|
||||
}
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def digest(path):
|
||||
h = hashlib.sha256()
|
||||
with Path(path).open('rb') as source:
|
||||
for block in iter(lambda: source.read(8 << 20), b''):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def array_hash(value):
|
||||
return hashlib.sha256(np.ascontiguousarray(value).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def bit_equal(a, b):
|
||||
a, b = np.asarray(a), np.asarray(b)
|
||||
return bool(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)))
|
||||
|
||||
|
||||
def save(path, value):
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open('x', encoding='utf-8') as target:
|
||||
json.dump(value, target, indent=2, sort_keys=True, allow_nan=False)
|
||||
target.write('\n')
|
||||
|
||||
|
||||
class ReadOnlyNamespace:
|
||||
"""The extracted prefix can read rt, but cannot mutate or add rt fields."""
|
||||
def __init__(self, **values):
|
||||
object.__setattr__(self, '_values', types.MappingProxyType(values))
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self._values[name]
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
raise AttributeError('Read-only canonical runtime view')
|
||||
|
||||
|
||||
def _code(nodes, filename):
|
||||
module = ast.Module(body=copy.deepcopy(nodes), type_ignores=[])
|
||||
return compile(ast.fix_missing_locations(module), str(filename), 'exec')
|
||||
|
||||
|
||||
def _self_attribute(target, field):
|
||||
return isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and \
|
||||
target.value.id == 'self' and target.attr == field
|
||||
|
||||
|
||||
def extract_statements():
|
||||
data = SOURCE.read_bytes()
|
||||
require(hashlib.sha256(data).hexdigest() == SOURCE_SHA256, 'Frozen legacy source SHA-256 mismatch')
|
||||
text = data.decode('utf-8')
|
||||
tree = ast.parse(text, filename=str(SOURCE))
|
||||
classes = [node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == 'DeterministicAPLSim']
|
||||
require(len(classes) == 1, 'Expected exactly one frozen DeterministicAPLSim')
|
||||
methods = {node.name: node for node in classes[0].body if isinstance(node, ast.FunctionDef)}
|
||||
init, run = methods['__init__'], methods['run']
|
||||
cuts = [i for i, node in enumerate(init.body) if isinstance(node, ast.Assign) and
|
||||
any(_self_attribute(target, 'base') for target in node.targets)]
|
||||
require(len(cuts) == 1 and cuts[0] > 0, 'Missing/ambiguous self.base cutoff')
|
||||
prefix = init.body[:cuts[0]]
|
||||
# Executing no import or file-writing call is allowed in the extracted prefix.
|
||||
require(not any(isinstance(node, (ast.Import, ast.ImportFrom, ast.With, ast.AsyncWith))
|
||||
for stmt in prefix for node in ast.walk(stmt)), 'Unexpected side-effect construct in initializer prefix')
|
||||
expressions = [node for node in ast.walk(run) if isinstance(node, ast.Expr) and
|
||||
isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Attribute) and
|
||||
node.value.func.attr == 'add_' and isinstance(node.value.func.value, ast.Call) and
|
||||
isinstance(node.value.func.value.func, ast.Attribute) and
|
||||
isinstance(node.value.func.value.func.value, ast.Name) and
|
||||
node.value.func.value.func.value.id == 'apl_state' and node.value.func.value.func.attr == 'mul_']
|
||||
inhibition = [node for node in ast.walk(run) if isinstance(node, ast.Assign) and
|
||||
any(isinstance(target, ast.Name) and target.id == 'inhibition' for target in node.targets)]
|
||||
decay = [node for node in ast.walk(run) if isinstance(node, ast.Assign) and
|
||||
any(isinstance(target, ast.Name) and target.id == 'apl_decay' for target in node.targets)]
|
||||
require(len(expressions) == len(inhibition) == len(decay) == 1, 'Ambiguous frozen APL operator extraction')
|
||||
require(expressions[0].lineno < inhibition[0].lineno, 'Unexpected frozen APL update order')
|
||||
rhs = inhibition[0].value
|
||||
require(isinstance(rhs, ast.BinOp) and isinstance(rhs.op, ast.Mult), 'Unexpected inhibition gain expression')
|
||||
gain_expression = ast.Expression(copy.deepcopy(rhs.right))
|
||||
provenance = {'source': str(SOURCE), 'sha256': SOURCE_SHA256,
|
||||
'initializer_line_start': prefix[0].lineno, 'initializer_line_end': prefix[-1].end_lineno,
|
||||
'excluded_self_base_line': init.body[cuts[0]].lineno,
|
||||
'decay_line': decay[0].lineno, 'state_update_line': expressions[0].lineno,
|
||||
'inhibition_line': inhibition[0].lineno,
|
||||
'initializer_source': '\n'.join(ast.get_source_segment(text, node) for node in prefix),
|
||||
'decay_source': ast.get_source_segment(text, decay[0]),
|
||||
'state_update_source': ast.get_source_segment(text, expressions[0]),
|
||||
'inhibition_source': ast.get_source_segment(text, inhibition[0]),
|
||||
'learning_initializer_suffix_executed': False, 'original_run_loop_executed': False}
|
||||
return (_code(prefix, SOURCE), _code(decay, SOURCE),
|
||||
_code([expressions[0], inhibition[0]], SOURCE),
|
||||
compile(ast.fix_missing_locations(gain_expression), str(SOURCE), 'eval'), provenance)
|
||||
|
||||
|
||||
def _canonical_files():
|
||||
return {name: CANONICAL/path for name, path in {
|
||||
'metadata': 'neuron-index/unified-metadata.parquet',
|
||||
'indptr': 'gpu/csr-indptr.int64.npy', 'indices': 'gpu/csr-indices.int32.npy',
|
||||
'raw': 'gpu/csr-weights.int64.npy'}.items()}
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def reconstruct(canonical_hashes, device_name):
|
||||
"""Cache only reconstructions tied to exact canonical bytes and source hash."""
|
||||
files = _canonical_files()
|
||||
require(dict(canonical_hashes) == {name: digest(path) for name, path in files.items()},
|
||||
'Canonical bytes changed before matrix reconstruction')
|
||||
arrays = {name: np.load(files[name], mmap_mode='r', allow_pickle=False)
|
||||
for name in ('indptr', 'indices', 'raw')}
|
||||
require(arrays['indptr'].dtype == np.int64 and arrays['indices'].dtype == np.int32 and
|
||||
arrays['raw'].dtype == np.int64, 'Canonical CSR dtypes changed')
|
||||
require(arrays['indptr'].shape == (N + 1,) and arrays['indptr'][-1] == 25582938 and
|
||||
arrays['indices'].shape == arrays['raw'].shape == (25582938,), 'Canonical graph shape changed')
|
||||
require(int(arrays['raw'].sum()) == 124177617 and bool(np.all(arrays['raw'] > 0)), 'Canonical raw weights changed')
|
||||
table = pq.read_table(files['metadata'])
|
||||
require(table.num_rows == N, 'Canonical metadata count changed')
|
||||
rt = ReadOnlyNamespace(n=N, dt=.1, device=torch.device(device_name), table=table,
|
||||
indptr_np=arrays['indptr'], indices_np=arrays['indices'], raw_np=arrays['raw'])
|
||||
view = types.SimpleNamespace()
|
||||
init_code, decay_code, update_code, gain_code, provenance = extract_statements()
|
||||
scope = {'self': view, 'rt': rt, 'np': np, 'torch': torch}
|
||||
exec(init_code, scope)
|
||||
require(not hasattr(view, 'base') and not hasattr(view, 'identity'), 'Initializer crossed the learning cutoff')
|
||||
expected = {'input_matrix': view.input_gpu.detach().cpu().numpy().copy(),
|
||||
'output_matrix': view.output_gpu.detach().cpu().numpy().copy(),
|
||||
'input_raw': scope['input_matrix'], 'output_raw': scope['output_matrix'],
|
||||
'kc_globals': view.kc, 'apl_globals': view.apl, 'pn_globals': view.pn,
|
||||
'apl_out_ids': view.apl_out_ids}
|
||||
for value in expected.values():
|
||||
value.flags.writeable = False
|
||||
return view, expected, provenance, (decay_code, update_code, gain_code)
|
||||
|
||||
|
||||
class TorchObservationProxy:
|
||||
"""Observe the result of the actual extracted torch.mv call unchanged."""
|
||||
def __init__(self):
|
||||
self.drive = None
|
||||
self.calls = 0
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(torch, name)
|
||||
|
||||
def mv(self, *args, **kwargs):
|
||||
value = torch.mv(*args, **kwargs)
|
||||
self.drive = value
|
||||
self.calls += 1
|
||||
return value
|
||||
|
||||
|
||||
def _up(value):
|
||||
return np.nextafter(np.asarray(value, dtype=np.float64), np.inf)
|
||||
|
||||
|
||||
def _add(a, b):
|
||||
return _up(np.asarray(a, dtype=np.float64) + np.asarray(b, dtype=np.float64))
|
||||
|
||||
|
||||
def _mul(a, b):
|
||||
return _up(np.asarray(a, dtype=np.float64) * np.asarray(b, dtype=np.float64))
|
||||
|
||||
|
||||
def _gamma(count, unit_roundoff):
|
||||
require(count * unit_roundoff < 1, 'Gamma bound is undefined')
|
||||
# Outward rounding on the division; the denominator is rounded downward.
|
||||
numerator = _mul(count, unit_roundoff)
|
||||
denominator = np.nextafter(1.0 - numerator, -np.inf)
|
||||
return _up(numerator / denominator)
|
||||
|
||||
|
||||
def _positive_dot_upper(state, matrix):
|
||||
result = np.zeros(matrix.shape[1], np.float64)
|
||||
for index in range(len(state)):
|
||||
result = _add(result, _mul(state[index], matrix[index]))
|
||||
return result
|
||||
|
||||
|
||||
def _normal_or_zero(value, tiny):
|
||||
value = np.asarray(value)
|
||||
return bool(np.all((value == 0) | (value >= tiny)))
|
||||
|
||||
|
||||
def _new_metric():
|
||||
return {'max_abs_error': 0.0, 'sum_abs_error': 0.0, 'elements': 0,
|
||||
'max_relative_error_nonzero_reference': 0.0, 'zero_reference_nonzero_actual': 0,
|
||||
'max_error_bound': 0.0, 'max_bound_utilization': 0.0, 'bound_violations': 0}
|
||||
|
||||
|
||||
def _accumulate(metric, actual, reference, bound):
|
||||
error = np.abs(np.asarray(actual, np.float64) - reference)
|
||||
error_upper = np.where(error == 0, 0.0, _up(error))
|
||||
nonzero = reference != 0
|
||||
ratio = np.divide(error, np.abs(reference), out=np.zeros_like(error), where=nonzero)
|
||||
usage = np.divide(error_upper, bound, out=np.zeros_like(error), where=bound > 0)
|
||||
metric['max_abs_error'] = max(metric['max_abs_error'], float(error.max(initial=0)))
|
||||
metric['sum_abs_error'] += float(error.sum(dtype=np.float64))
|
||||
metric['elements'] += error.size
|
||||
metric['max_relative_error_nonzero_reference'] = max(metric['max_relative_error_nonzero_reference'], float(ratio.max(initial=0)))
|
||||
metric['zero_reference_nonzero_actual'] += int(np.count_nonzero((~nonzero) & (error != 0)))
|
||||
metric['max_error_bound'] = max(metric['max_error_bound'], float(np.max(bound, initial=0)))
|
||||
metric['max_bound_utilization'] = max(metric['max_bound_utilization'], float(usage.max(initial=0)))
|
||||
metric['bound_violations'] += int(np.count_nonzero(error_upper > bound))
|
||||
|
||||
|
||||
def validate(gpu_case_dir, bundle_dir, out_json):
|
||||
"""Validate one completed fixture; write one exclusive pure-JSON audit."""
|
||||
gpu_case_dir, bundle_dir, out_json = Path(gpu_case_dir), Path(bundle_dir), Path(out_json)
|
||||
require(not out_json.exists(), 'Refusing to overwrite an existing component audit')
|
||||
report = {'pass_all': False, 'scope': 'Frozen APL component extraction and recorded-spike replay only',
|
||||
'independent_cpu_closed_loop_oracle': False, 'brain_or_learning_parameters_changed': False,
|
||||
'candidate_function_called': False, 'gpu_case_dir': str(gpu_case_dir), 'bundle_dir': str(bundle_dir)}
|
||||
try:
|
||||
require(digest(SOURCE) == SOURCE_SHA256, 'Frozen source changed before component validation')
|
||||
require(torch.cuda.is_available() and torch.cuda.device_count() == 1 and
|
||||
'4090' in torch.cuda.get_device_name(), 'Require the one declared RTX4090')
|
||||
environment = {'torch': str(torch.__version__), 'numpy': np.__version__,
|
||||
'gpu': torch.cuda.get_device_name(), 'cuda': torch.version.cuda,
|
||||
'cuda_matmul_allow_tf32': torch.backends.cuda.matmul.allow_tf32,
|
||||
'float32_matmul_precision': torch.get_float32_matmul_precision(),
|
||||
'deterministic_algorithms': torch.are_deterministic_algorithms_enabled(),
|
||||
'precision_flags_changed': False}
|
||||
report['environment'] = environment
|
||||
canonical_hashes = tuple(sorted((name, digest(path)) for name, path in _canonical_files().items()))
|
||||
view, reconstructed, provenance, code = reconstruct(canonical_hashes, 'cuda:0')
|
||||
bundle = {}
|
||||
matrix_checks = {}
|
||||
for name, (dtype, shape) in BUNDLE_ARRAYS.items():
|
||||
value = np.load(bundle_dir/(name+'.npy'), mmap_mode='r', allow_pickle=False)
|
||||
require(value.dtype == dtype and value.ndim == len(shape or (0,)) and
|
||||
(shape is None or value.shape == shape), 'Bundle schema mismatch: '+name)
|
||||
bundle[name] = value
|
||||
matrix_checks[name] = bit_equal(value, reconstructed[name])
|
||||
scales = json.loads((bundle_dir/'scales.json').read_text())
|
||||
for name in ('input_scale', 'output_scale'):
|
||||
matrix_checks[name] = struct.pack('<d', float(scales[name])) == struct.pack('<d', float(getattr(view, name)))
|
||||
report['matrix_checks'] = matrix_checks
|
||||
report['extracted_source'] = provenance
|
||||
report['canonical_sha256'] = dict(canonical_hashes)
|
||||
report['bundle_sha256'] = {name+'.npy': digest(bundle_dir/(name+'.npy')) for name in BUNDLE_ARRAYS}
|
||||
report['bundle_sha256']['scales.json'] = digest(bundle_dir/'scales.json')
|
||||
report['reconstructed_array_sha256'] = {name: array_hash(value) for name, value in reconstructed.items()}
|
||||
bundle_provenance = json.loads((bundle_dir/'provenance.json').read_text())
|
||||
require(Path(bundle_provenance['canonical_root']).resolve() == CANONICAL.resolve(),
|
||||
'Bundle provenance names another canonical dataset')
|
||||
report['bundle_sha256']['provenance.json'] = digest(bundle_dir/'provenance.json')
|
||||
require(all(matrix_checks.values()), 'Bundle differs from the actual frozen initializer prefix')
|
||||
|
||||
traces = {name: np.load(gpu_case_dir/('apl-'+name+'.float32.npy'), mmap_mode='r', allow_pickle=False)
|
||||
for name in ('drive', 'state', 'inhibition')}
|
||||
steps = len(traces['state'])
|
||||
require(steps > 0, 'Empty APL trajectory cannot pass')
|
||||
for name, width in (('drive', A), ('state', A), ('inhibition', K)):
|
||||
require(traces[name].dtype == np.float32 and traces[name].shape == (steps, width) and
|
||||
bool(np.isfinite(traces[name]).all()) and bool(np.all(traces[name] >= 0)), 'Invalid APL trace: '+name)
|
||||
summary = json.loads((gpu_case_dir/'summary.json').read_text())
|
||||
require(summary.get('complete') is True and summary.get('pass_all') is True and
|
||||
summary.get('steps') == steps and summary.get('completed_ticks') == steps,
|
||||
'GPU fixture did not complete its internal execution checks')
|
||||
with np.load(gpu_case_dir/'state.npz', allow_pickle=False) as archive:
|
||||
event_step, event_node = archive['step'].copy(), archive['neuron_idx'].copy()
|
||||
require(event_step.dtype == event_node.dtype == np.int32 and event_step.ndim == 1 and
|
||||
event_step.shape == event_node.shape, 'Invalid sparse actual spike event schema')
|
||||
require(bool(np.all((event_step >= 0) & (event_step < steps))) and
|
||||
bool(np.all((event_node >= 0) & (event_node < N))), 'Sparse event outside its clock/identity range')
|
||||
require(np.array_equal(np.lexsort((event_node, event_step)), np.arange(len(event_step))), 'Actual events not sorted')
|
||||
keys = event_step.astype(np.int64)*N + event_node
|
||||
require(len(keys) < 2 or bool(np.all(np.diff(keys) > 0)), 'Duplicate actual spikes')
|
||||
boundaries = np.searchsorted(event_step, np.arange(steps + 1))
|
||||
|
||||
proxy = TorchObservationProxy()
|
||||
# Reconstructed original matrices are already on the original GPU/dtype.
|
||||
replay_rt = types.SimpleNamespace(spike_u8=torch.zeros(N, dtype=torch.uint8, device=view.device))
|
||||
shadow = types.SimpleNamespace(input_gpu=view.input_gpu, output_gpu=view.output_gpu, rt=replay_rt, dt=.1)
|
||||
scope = {'self': shadow, 'np': np, 'math': math, 'torch': proxy,
|
||||
'apl_state': torch.zeros(A, dtype=torch.float32, device=view.device)}
|
||||
decay_code, update_code, gain_code = code
|
||||
exec(decay_code, scope)
|
||||
decay32 = np.float32(scope['apl_decay'])
|
||||
gain32 = np.float32(eval(gain_code, {'np': np}))
|
||||
require(0 < decay32 < 1 and gain32 > 0, 'Invalid extracted positive APL constants')
|
||||
exact = {name: True for name in traces}
|
||||
first_difference = None
|
||||
I64 = np.asarray(bundle['input_matrix'], np.float64)
|
||||
O64 = np.asarray(bundle['output_matrix'], np.float64)
|
||||
require(bool(np.all(I64 >= 0)) and bool(np.all(O64 >= 0)), 'Positive-sum theory requires nonnegative matrices')
|
||||
d, q = float(decay32), float(gain32)
|
||||
u32, u64 = 2.0**-24, 2.0**-53
|
||||
gd32, gd64 = _gamma(N-1, u32), _gamma(N-1, u64)
|
||||
g232, g264 = _gamma(2, u32), _gamma(2, u64)
|
||||
gh32, gh64 = _gamma(2*A+1, u32), _gamma(2*A+1, u64)
|
||||
state64 = np.zeros(A, np.float64)
|
||||
e32 = np.zeros(A, np.float64)
|
||||
e64 = np.zeros(A, np.float64)
|
||||
previous32 = np.zeros(A, np.float64)
|
||||
statistics = {name: _new_metric() for name in traces}
|
||||
normal_arithmetic = _normal_or_zero(I64, np.finfo(np.float32).tiny) and _normal_or_zero(O64, np.finfo(np.float32).tiny)
|
||||
max_active_terms = 0
|
||||
with torch.no_grad():
|
||||
for tick in range(steps):
|
||||
fired = event_node[boundaries[tick]:boundaries[tick+1]]
|
||||
replay_rt.spike_u8.zero_()
|
||||
if len(fired):
|
||||
replay_rt.spike_u8[torch.as_tensor(fired.astype(np.int64), device=view.device)] = 1
|
||||
before_calls = proxy.calls
|
||||
exec(update_code, scope)
|
||||
require(proxy.calls == before_calls + 1, 'Original update did not issue exactly one mv')
|
||||
actual = {'drive': proxy.drive.detach().cpu().numpy(),
|
||||
'state': scope['apl_state'].detach().cpu().numpy(),
|
||||
'inhibition': scope['inhibition'].detach().cpu().numpy()}
|
||||
for name in traces:
|
||||
same = bit_equal(actual[name], traces[name][tick])
|
||||
exact[name] &= same
|
||||
if not same and first_difference is None:
|
||||
first_difference = {'tick': tick, 'field': name,
|
||||
'max_abs_error': float(np.max(np.abs(actual[name].astype(np.float64)-traces[name][tick])))}
|
||||
# Float64 reference is measurement only and never feeds either brain.
|
||||
selected = I64[:, fired]
|
||||
drive64 = selected.sum(axis=1, dtype=np.float64)
|
||||
max_active_terms = max(max_active_terms, int(np.count_nonzero(selected, axis=1).max(initial=0)))
|
||||
Dupper = _up(drive64 / np.nextafter(1.0-gd64, -np.inf))
|
||||
drive_bound = _add(_mul(gd32, Dupper), _mul(gd64, Dupper))
|
||||
state32 = traces['state'][tick].astype(np.float64)
|
||||
normal_arithmetic &= _normal_or_zero(d*previous32, np.finfo(np.float32).tiny)
|
||||
normal_arithmetic &= _normal_or_zero(O64*state32[:, None], np.finfo(np.float32).tiny)
|
||||
e32 = _add(_mul(d, e32), _add(_mul(g232, _add(_mul(d, previous32), Dupper)),
|
||||
_mul(1.0+u32, _mul(gd32, Dupper))))
|
||||
e64 = _add(_mul(d, e64), _add(_mul(g264, _add(_mul(d, state64), Dupper)),
|
||||
_mul(1.0+u64, _mul(gd64, Dupper))))
|
||||
state64 = d*state64 + drive64
|
||||
inhibition64 = np.matmul(state64, O64)*q
|
||||
h32bound = _add(_mul(q, _positive_dot_upper(e32, O64)),
|
||||
_mul(gh32, _mul(q, _positive_dot_upper(state32, O64))))
|
||||
h64bound = _add(_mul(q, _positive_dot_upper(e64, O64)),
|
||||
_mul(gh64, _mul(q, _positive_dot_upper(state64, O64))))
|
||||
normal_arithmetic &= _normal_or_zero(q*np.matmul(state32, O64), np.finfo(np.float32).tiny)
|
||||
_accumulate(statistics['drive'], traces['drive'][tick], drive64, drive_bound)
|
||||
_accumulate(statistics['state'], traces['state'][tick], state64, _add(e32, e64))
|
||||
_accumulate(statistics['inhibition'], traces['inhibition'][tick], inhibition64, _add(h32bound, h64bound))
|
||||
previous32 = state32
|
||||
for metric in statistics.values():
|
||||
metric['mean_abs_error'] = metric.pop('sum_abs_error') / metric['elements']
|
||||
theory_applicable = bool(normal_arithmetic and not environment['cuda_matmul_allow_tf32'] and
|
||||
environment['float32_matmul_precision'] == 'highest')
|
||||
report.update(steps=steps, actual_spike_events=len(event_step),
|
||||
ast_replay_bitwise_exact=exact, first_ast_difference=first_difference,
|
||||
value_units={'drive': 'legacy normalized-contact spike drive',
|
||||
'state': 'legacy normalized-drive accumulator', 'inhibition': 'legacy mV'},
|
||||
decay_f32_bits=f'{int(decay32.view(np.uint32)):08x}', gain_f32_bits=f'{int(gain32.view(np.uint32)):08x}',
|
||||
float64_measurement=statistics,
|
||||
theoretical_bound={'applicable': theory_applicable, 'normal_or_zero_arithmetic': bool(normal_arithmetic),
|
||||
'gamma_definition': 'gamma_n=(n*u)/(1-n*u), upward-rounded; u32=2^-24,u64=2^-53',
|
||||
'drive_n': N-1, 'max_observed_nonzero_drive_terms': max_active_terms,
|
||||
'gamma_drive_f32': float(gd32), 'gamma_drive_f64': float(gd64),
|
||||
'state_recurrence': 'E_p[t] <= d*E_p[t-1] + gamma_2,p*(d*abs(s_p[t-1])+D_upper) + (1+u_p)*gamma_(N-1),p*D_upper',
|
||||
'output_bound': 'q*sum(B*E_p) + gamma_(2*A+1),p*q*sum(B*abs(s_p)); compare with E32+E64',
|
||||
'assumptions': 'nonnegative f32 matrices, spikes exactly 0/1, fixed stored f32 decay/gain, normal finite RN binary32/64 operations; no reduced-precision matmul',
|
||||
'bound_arithmetic': 'Each positive bound operation is rounded toward +infinity via nextafter; denominator toward -infinity',
|
||||
'tolerance_fitted_to_observed_error': False,
|
||||
'f64_values_used_to_drive_brain': False,
|
||||
'no_cpu_gpu_bitwise_expectation_for_f64_measurement': True},
|
||||
input_sha256={name: digest(gpu_case_dir/name) for name in
|
||||
('state.npz', 'summary.json', 'apl-drive.float32.npy', 'apl-state.float32.npy', 'apl-inhibition.float32.npy')})
|
||||
report['pass_all'] = bool(all(matrix_checks.values()) and all(exact.values()) and theory_applicable and
|
||||
all(value['bound_violations'] == 0 for value in statistics.values()))
|
||||
report['limitations'] = [
|
||||
'AST shadow and candidate use the same original Torch/CUDA primitives; this validates preserved component semantics, not an independent CPU whole-brain oracle.',
|
||||
'Float64 is a non-feedback mathematical measurement with explicit gamma error bounds, not replacement APL dynamics.',
|
||||
'Replay uses the recorded actual spike train; it does not establish equivalence for untested closed-loop inputs.',
|
||||
'The N-1 positive-sum bound is deliberately conservative; passing it does not prove physiological validity or learning.']
|
||||
save(out_json, report)
|
||||
return report
|
||||
except Exception as error:
|
||||
report['error'] = {'type': type(error).__name__, 'message': str(error)}
|
||||
if not out_json.exists():
|
||||
save(out_json, report)
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('gpu_case_dir', type=Path)
|
||||
parser.add_argument('bundle_dir', type=Path)
|
||||
parser.add_argument('out_json', type=Path)
|
||||
args = parser.parse_args()
|
||||
result = validate(args.gpu_case_dir, args.bundle_dir, args.out_json)
|
||||
print(json.dumps({'pass_all': result['pass_all'], 'steps': result['steps']}), flush=True)
|
||||
if not result['pass_all']:
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Run old reference unchanged, writing only new experiment output."""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yaml
|
||||
from context import ROOT,EXP,CFG,save,check
|
||||
|
||||
|
||||
def main():
|
||||
check();assert not (EXP/'regression/frozen-five.json').exists()
|
||||
rtpath=ROOT/'models/male-cns-lif/deterministic-runtime-v2';pc=yaml.safe_load((rtpath/'phase4p.yaml').read_text())
|
||||
os.environ['CUDA_VISIBLE_DEVICES']=CFG['gpu_uuid'];os.environ['CUDA_HOME']='/usr/local/cuda'
|
||||
os.environ['PATH']=str(ROOT/'models/male-cns-lif/.venv/bin')+':/usr/local/cuda/bin:'+os.environ['PATH']
|
||||
os.environ['MAX_JOBS']='2';os.environ['TORCH_CUDA_ARCH_LIST']='8.9'
|
||||
sys.path.insert(0,str(ROOT/'models/male-cns-lif/terminal-scope-diagnostic-v1'))
|
||||
from diagnostic import queues
|
||||
queues();sys.path.insert(0,str(ROOT/'models/male-cns-lif'));sys.path.insert(0,str(rtpath))
|
||||
import torch,phase4p
|
||||
assert torch.cuda.mem_get_info()[0]>2*1024**3
|
||||
torch.cuda.reset_peak_memory_stats();t0=time.perf_counter();rt=phase4p.CanonicalRuntime(pc,rtpath,EXP);rows=[]
|
||||
for spec in pc['reference_trials']:
|
||||
queues();base=ROOT/'experiments/phase4p-deterministic-runtime/phase4c-v2/reference'/spec['name']
|
||||
ev=pd.read_parquet(base/'spike-events.parquet')
|
||||
ref=dict(step=ev.step.to_numpy(),neuron_idx=ev.neuron_idx.to_numpy(),counts=np.load(base/'per-neuron-spike-counts.int32.npy'),v=np.load(base/'final-voltage.float32.npy'),g=np.load(base/'final-g.float32.npy'))
|
||||
result=rt.run(np.flatnonzero(rt.pops[spec['population']]),spec['rate_hz'],spec['seed'],spec['duration_ms'])
|
||||
row=dict(trial=spec['name'],exact={k:bool(np.array_equal(v,result[k])) for k,v in ref.items()},wall_seconds=result['metrics']['wall_seconds'])
|
||||
rows.append(row);print(row,flush=True)
|
||||
save(EXP/'regression/frozen-five.json',dict(pass_all=all(all(r['exact'].values()) for r in rows),trials=rows,
|
||||
wall_seconds=time.perf_counter()-t0,peak_allocated_vram=int(torch.cuda.max_memory_allocated()),oracle='old frozen deterministic-v2 only'))
|
||||
assert all(all(r['exact'].values()) for r in rows)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,84 @@
|
||||
# Phase4AG: author LIF + frozen legacy graded APL
|
||||
|
||||
Execution revision r2: the first registered v1 attempt stopped before the first
|
||||
Brian tick because resolving the science-venv Python symlink launched system
|
||||
Python without NumPy. All v1 files and partial outputs are preserved separately.
|
||||
This revision retains the declared venv invocation path. It does not change any
|
||||
equation, arithmetic, mask, stimulus, gate, parameter or choice of trial.
|
||||
The complete original protocol is rerun from fresh state; no failed trial is
|
||||
overwritten or omitted, and the first version cannot be claimed numerical PASS.
|
||||
|
||||
Authorized independent hypothesis branch. All old source, raw connectome, signs,
|
||||
W_syn, APL calibration, eligibility-v2 and 14,232-edge mask remain read-only.
|
||||
No learning, gain/threshold changes, new edges, top-K, heldout activity, game or RL.
|
||||
|
||||
Author code provenance is the already archived MIT-licensed
|
||||
https://github.com/philshiu/Drosophila_brain_model at
|
||||
91bdd1e7dcf193f3e7ca5a8933497fcef63b7960; original model.py SHA-256
|
||||
fc45837d7122c6ce2a7f3f2f23c515992e4b232aadb919efabb72337fac88e4e.
|
||||
Associated paper: https://www.nature.com/articles/s41586-024-07763-9.
|
||||
This new MaleCNS/APL combination is not a strict reproduction of that model.
|
||||
|
||||
The declared combination is author float64 LIF plus the exact old float32 graded
|
||||
APL operator after synapses/resets at end. All 5,845 original APL fast out-edges
|
||||
are zeroed only in the effective in-memory model, as in the legacy overlay.
|
||||
Only KC receives graded replacement; known 1,212 non-KC edges remain unreplaced.
|
||||
This existing scope limitation is not silently repaired in a compatibility test.
|
||||
|
||||
Use the original full normalized I[2,166700]/O[2,4064] float32 bytes and original
|
||||
normalizers, even for induced real subgraphs. APL reads actual binary spikes,
|
||||
never requests, and keeps independent float32 state. Preserve unfused operations:
|
||||
state.mul_(float(f32(exp(-.1/20)))).add_(torch.mv(I, spikes_f32));
|
||||
h32=torch.matmul(state,O)*f32(.01). Promote h32 to float64, multiply by float64
|
||||
.001, then separately subtract from SI g. Do not alter TF32/default CuBLAS flags;
|
||||
record them. No added APL delay, drive dt/tau scaling, truncation or clipping.
|
||||
|
||||
APL writes KC g unconditionally after author reset, including refractory KC.
|
||||
Author g integration is frozen during refractory; therefore negative overlay g
|
||||
may accumulate and differs from old dynamics. This is an explicit hypothesis,
|
||||
not an assertion of original author or measured physiology. Guarded writes or
|
||||
reset-before/after changes would be different hypotheses, not fixes to this one.
|
||||
|
||||
Eight fixed metadata/connectivity-selected fixtures cover zero/reset, real PN/KC
|
||||
drive, current actual event feedback, duplicate natural/request event, 18-step
|
||||
arrival vs22-step refractory boundary, full fast APL mask, non-KC scope and mixed
|
||||
precision/high fan-in. No choice is made from runtime responses. Each GPU case
|
||||
has3 independent fresh runs. A Brian2 author oracle starts from its own state,
|
||||
computes its own actual spikes, and uses an isolated standard-I/O legacy-APL
|
||||
worker with independent state. No candidate inhibition trace is fed into Brian.
|
||||
All tick states, events, requests, queue order/guard, APL drive/state/h must agree
|
||||
exactly. The APL component is shared, so this is NOT a fully independent CPU
|
||||
oracle. Independently reconstruct old matrices using frozen initializer AST,
|
||||
replay actual events through original update AST, require exact component bytes;
|
||||
also quantify an independent float64 positive-sum arithmetic error bound. It is
|
||||
not a replacement for exact legacy equivalence. No arithmetic bound is adjusted.
|
||||
|
||||
Only after all fixture gates pass, run two predeclared full-graph probes: first
|
||||
old calibration odor, first original seed, both encoder modes. Compare each with
|
||||
the independently running Brian + isolated APL worker. Save state hashes and sparse
|
||||
events, not a time-by-all-neurons dense spike matrix. If a numerical gate fails,
|
||||
stop CAL; keep diagnostic evidence and do not widen tolerance.
|
||||
|
||||
Then run the complete original6 CAL odors ×5 original seeds ×2 original modes,
|
||||
100ms per run. Retain the ORIGINAL12-odor median for matched-energy encoding,
|
||||
original pn_drive target/RNG column order, original evaluation seeds51101..51105.
|
||||
Save Poisson requests before any trajectory. Only calibration dynamic cases are
|
||||
used; formal A/B and heldout remain unrun. Repeat first seed for every odor/mode.
|
||||
No per-odor pulse, amplitude, timing or threshold search. No learning.
|
||||
|
||||
Recompute the full original4L representation criteria on CAL only: mean KC active
|
||||
fraction .03..09, min>0, max<.5, same-odor Jaccard>different, between Euclidean>
|
||||
within, >=3 pairs whose mean between exceeds THAT PAIR's pooled within distances,
|
||||
both modes. Add finiteness and exact repeat requirements. CAL PASS does not imply
|
||||
heldout generalization, timing/credit specificity, memory or learning usability.
|
||||
If CAL fails stop promotion and report it, not a least-bad candidate.
|
||||
|
||||
Before/after verify SHA-256 of all original230239 files and AA--AF seals; rerun original five
|
||||
deterministic reference trials to new output. Archive code/env/source SHA,
|
||||
matrices/norms/masks/identities, requests and all outputs. No published candidate.
|
||||
|
||||
Technical references (queried2026-09-30): Brian2 2.5.1 refractory documentation
|
||||
https://brian2.readthedocs.io/en/2.5.1/user/refractoriness.html (actual generated code
|
||||
remains authoritative); PyTorch numerical accuracy notes
|
||||
https://docs.pytorch.org/docs/2.14/notes/numerical_accuracy.html (cross-backend
|
||||
floating operations are not assumed bitwise equivalent).
|
||||
@@ -0,0 +1,396 @@
|
||||
"""Complete original Phase 4L representation metrics on fixed CAL inputs only.
|
||||
|
||||
No simulation, fitting, heldout trajectory access, or candidate generation is
|
||||
performed here. The original 4L pair-specific within-odor baseline is retained;
|
||||
the weaker Phase 4P r4 aggregate gate is deliberately not used.
|
||||
"""
|
||||
import csv
|
||||
import hashlib
|
||||
import itertools
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
STATE_DTYPES = {"step": np.int32, "neuron_idx": np.int32, "counts": np.int32,
|
||||
"v": np.float64, "g": np.float64, "ref": np.int32}
|
||||
REQUEST_FIELDS = ("tick", "neuron_idx", "attempted", "accepted", "rejected")
|
||||
APL_FILES = {"apl-drive.float32.npy": 2, "apl-state.float32.npy": 2,
|
||||
"apl-inhibition.float32.npy": 4064}
|
||||
ORIGINAL_CAL_IDS = {
|
||||
"OMSUIQOIVADKIM-RXMQYKEDSA-N", "LABTWGUMFABVFG-ARJAWSKDSA-N",
|
||||
"QNVRIHYSUZMSGM-ZCFIWIBFSA-N", "VLSVVMPLPMNWBH-UHFFFAOYSA-N",
|
||||
"XHIUFYZDQBSEMF-UHFFFAOYSA-N", "OMSUIQOIVADKIM-YFKPBYRVSA-N"}
|
||||
ORIGINAL_MODES = ("physiological", "matched_energy")
|
||||
ORIGINAL_SEEDS = (51101, 51102, 51103, 51104, 51105)
|
||||
HEX_SHA = re.compile(r"[0-9a-f]{64}\Z")
|
||||
_DEFAULT_OUTPUT = object()
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def read_json(path):
|
||||
with Path(path).open(encoding="utf-8") as stream:
|
||||
return json.load(stream)
|
||||
|
||||
|
||||
def file_hash(path):
|
||||
digest = hashlib.sha256()
|
||||
with Path(path).open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(8 << 20), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def raw_hash(value):
|
||||
return hashlib.sha256(np.ascontiguousarray(value).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def arrays_bitwise_equal(first, second):
|
||||
"""Unlike array_equal, also reject different dtypes and signed-zero bits."""
|
||||
return bool(first.dtype == second.dtype and first.shape == second.shape and
|
||||
np.array_equal(np.ascontiguousarray(first).view(np.uint8),
|
||||
np.ascontiguousarray(second).view(np.uint8)))
|
||||
|
||||
|
||||
def pair_metrics(a, b):
|
||||
"""Exact definitions copied from phase4i.pair_metrics/safe_corr/cosine."""
|
||||
x, y = a.astype(np.float64), b.astype(np.float64)
|
||||
aa, bb = a > 0, b > 0
|
||||
union = int(np.count_nonzero(aa | bb))
|
||||
intersection = int(np.count_nonzero(aa & bb))
|
||||
denominator = float(np.linalg.norm(x) * np.linalg.norm(y))
|
||||
cosine = 1.0 if denominator == 0 and np.array_equal(x, y) else (
|
||||
0.0 if denominator == 0 else float(np.dot(x, y) / denominator))
|
||||
correlation = (1.0 if np.array_equal(x, y) else 0.0) if np.std(x) == 0 or np.std(y) == 0 else float(np.corrcoef(x, y)[0, 1])
|
||||
return {"active_intersection": intersection, "active_union": union,
|
||||
"active_jaccard": 1.0 if union == 0 else float(intersection / union),
|
||||
"cosine_similarity": cosine, "correlation": correlation,
|
||||
"euclidean_distance": float(np.linalg.norm(x - y))}
|
||||
|
||||
|
||||
def evaluate_gates(summary, gates):
|
||||
"""Each conjunct in original phase4l.py:239, kept visible for audit."""
|
||||
return {
|
||||
"mean_sparsity_in_closed_interval": bool(float(gates["sparse_min_fraction"]) <=
|
||||
summary["KC_active_fraction_mean"] <= float(gates["sparse_max_fraction"])),
|
||||
"maximum_strictly_below_half": bool(summary["KC_active_fraction_max"] < 0.5),
|
||||
"minimum_strictly_positive": bool(summary["KC_active_fraction_min"] > 0),
|
||||
"same_jaccard_gt_different": bool(summary["same_odor_jaccard_mean"] > summary["different_odor_jaccard_mean"]),
|
||||
"different_euclidean_gt_same": bool(summary["different_odor_euclidean_mean"] > summary["same_odor_euclidean_mean"]),
|
||||
"at_least_original_pair_count": bool(summary["pairwise_separation_pass_count"] >= int(gates["minimum_separable_odor_pairs"]))}
|
||||
|
||||
|
||||
def sample_cv(values):
|
||||
"""Original pandas Series.std uses ddof=1. Undefined diagnostic -> null."""
|
||||
values = np.asarray(values, dtype=np.float64)
|
||||
mean = float(values.mean())
|
||||
return None if mean == 0 else float(values.std(ddof=1) / mean)
|
||||
|
||||
|
||||
def summarize_mode(mode, odor_ids, seeds, runs, gates):
|
||||
within, within_j, between, between_j, between_cos, between_corr = [], [], [], [], [], []
|
||||
pairs = []
|
||||
for odor in odor_ids:
|
||||
arrays = [runs[(mode, odor, seed)]["kc_counts"] for seed in seeds]
|
||||
for first, second in itertools.combinations(arrays, 2):
|
||||
metric = pair_metrics(first, second)
|
||||
within.append(metric["euclidean_distance"])
|
||||
within_j.append(metric["active_jaccard"])
|
||||
for first_odor, second_odor in itertools.combinations(odor_ids, 2):
|
||||
distances = []
|
||||
for seed in seeds:
|
||||
metric = pair_metrics(runs[(mode, first_odor, seed)]["kc_counts"],
|
||||
runs[(mode, second_odor, seed)]["kc_counts"])
|
||||
distances.append(metric["euclidean_distance"])
|
||||
between.append(metric["euclidean_distance"])
|
||||
between_j.append(metric["active_jaccard"])
|
||||
between_cos.append(metric["cosine_similarity"])
|
||||
between_corr.append(metric["correlation"])
|
||||
pair_within = []
|
||||
for odor in (first_odor, second_odor):
|
||||
arrays = [runs[(mode, odor, seed)]["kc_counts"] for seed in seeds]
|
||||
pair_within.extend(pair_metrics(first, second)["euclidean_distance"]
|
||||
for first, second in itertools.combinations(arrays, 2))
|
||||
cross_mean, within_mean = float(np.mean(distances)), float(np.mean(pair_within))
|
||||
pairs.append({"odor_id_a": first_odor, "odor_id_b": second_odor,
|
||||
"between_same_seed_mean": cross_mean, "within_this_pair_mean": within_mean,
|
||||
"between_distance_count": len(distances), "within_distance_count": len(pair_within),
|
||||
"gate": bool(cross_mean > within_mean)})
|
||||
selected = [runs[(mode, odor, seed)] for odor in odor_ids for seed in seeds]
|
||||
fractions = [int(np.count_nonzero(run["kc_counts"])) / len(run["kc_counts"]) for run in selected]
|
||||
summary = {"KC_active_fraction_mean": float(np.mean(fractions)),
|
||||
"KC_active_fraction_min": float(np.min(fractions)), "KC_active_fraction_max": float(np.max(fractions)),
|
||||
"same_odor_jaccard_mean": float(np.mean(within_j)),
|
||||
"different_odor_jaccard_mean": float(np.mean(between_j)),
|
||||
"same_odor_euclidean_mean": float(np.mean(within)),
|
||||
"different_odor_euclidean_mean": float(np.mean(between)),
|
||||
"different_odor_cosine_mean": float(np.mean(between_cos)),
|
||||
"different_odor_correlation_mean": float(np.mean(between_corr)),
|
||||
"within_variance": float(np.var(within)), "between_distance_mean": float(np.mean(between)),
|
||||
"pairwise_separation_pass_count": sum(row["gate"] for row in pairs), "pair_count": len(pairs),
|
||||
"expected_event_count_cv": sample_cv([run["expected_external_events"] for run in selected]),
|
||||
"actual_event_count_cv": sample_cv([run["external_events"] for run in selected]),
|
||||
"within_distance_count": len(within), "between_distance_count": len(between),
|
||||
"pairwise_baseline": "Each pair's two odors: 10+10 within-seed distances; never pooled across all odors",
|
||||
"pairs": pairs}
|
||||
summary["conditions"] = evaluate_gates(summary, gates)
|
||||
summary["gate"] = bool(all(summary["conditions"].values()))
|
||||
return summary
|
||||
|
||||
|
||||
def validate_state(state, neurons, steps):
|
||||
require(set(state) == set(STATE_DTYPES), "state.npz must have exactly the six declared arrays")
|
||||
for name, dtype in STATE_DTYPES.items():
|
||||
value = state[name]
|
||||
require(value.dtype == dtype and value.ndim == 1, "Invalid state dtype/rank: " + name)
|
||||
require(bool(np.isfinite(value).all()), "Nonfinite state: " + name)
|
||||
if name not in ("step", "neuron_idx"):
|
||||
require(value.shape == (neurons,), "Invalid state length: " + name)
|
||||
ticks, nodes = state["step"], state["neuron_idx"]
|
||||
require(ticks.shape == nodes.shape and bool(np.all((ticks >= 0) & (ticks < steps))) and
|
||||
bool(np.all((nodes >= 0) & (nodes < neurons))), "Invalid sparse event indices")
|
||||
keys = ticks.astype(np.int64) * neurons + nodes
|
||||
require(bool(np.all(np.diff(keys) > 0)), "Sparse events not sorted unique by tick/global neuron")
|
||||
require(np.array_equal(np.bincount(nodes, minlength=neurons), state["counts"]), "Counts disagree with sparse events")
|
||||
require(bool(np.all((state["ref"] >= 0) & (state["ref"] <= 22))), "Final refractory outside frozen range")
|
||||
return keys
|
||||
|
||||
|
||||
def read_jsonl(path, steps, kind):
|
||||
with Path(path).open(encoding="utf-8") as source:
|
||||
rows = [json.loads(line) for line in source if line.strip()]
|
||||
require(len(rows) == steps and [row.get("tick") for row in rows] == list(range(steps)),
|
||||
"Missing/duplicate/reordered ticks in " + kind)
|
||||
for row in rows:
|
||||
names = ("v_sha256", "g_sha256", "ref_sha256") if kind == "state hashes" else (
|
||||
"ordered_edge_sha256", "accepted_mask_sha256", "joint_sha256")
|
||||
for name in names:
|
||||
require(isinstance(row.get(name), str) and HEX_SHA.fullmatch(row[name]), "Invalid SHA256 in " + kind)
|
||||
if kind == "queue":
|
||||
require(type(row.get("count")) is int and type(row.get("accepted_count")) is int and
|
||||
0 <= row["accepted_count"] <= row["count"], "Invalid queue counts")
|
||||
return rows
|
||||
|
||||
|
||||
def read_trial(output, case, spec, kc_globals, neurons, edges):
|
||||
summary = read_json(output / "summary.json")
|
||||
steps = spec["steps"]
|
||||
require(summary.get("complete") is True and summary.get("pass_all") is True and
|
||||
summary.get("all_finite") is True and summary.get("steps") == steps and
|
||||
summary.get("completed_ticks") == steps and summary.get("nodes") == neurons and
|
||||
summary.get("edges") == edges and summary.get("precision") == "float64" and
|
||||
summary.get("apl_precision") == "float32" and summary.get("case") == spec["name"] and
|
||||
summary.get("record_history") is False and summary.get("learning") is False,
|
||||
"Incomplete/failed/wrong-scope GPU summary")
|
||||
with np.load(output / "state.npz", allow_pickle=False) as source:
|
||||
state = {name: source[name].copy() for name in source.files}
|
||||
event_keys = validate_state(state, neurons, steps)
|
||||
hashes = read_jsonl(output / "tick-state-hashes.jsonl", steps, "state hashes")
|
||||
for name in ("v", "g", "ref"):
|
||||
require(hashes[-1][name + "_sha256"] == raw_hash(state[name]), "Final state does not match last tick hash: " + name)
|
||||
queues = read_jsonl(output / "queue.jsonl", steps, "queue")
|
||||
apl = {}
|
||||
for name, width in APL_FILES.items():
|
||||
value = np.load(output / name, mmap_mode="r", allow_pickle=False)
|
||||
require(value.dtype == np.float32 and value.shape == (steps, width) and
|
||||
bool(np.isfinite(value).all()) and bool(np.all(value >= 0)), "Invalid full APL trace: " + name)
|
||||
apl[name] = value
|
||||
with (output / "requests.csv").open(encoding="utf-8", newline="") as source:
|
||||
reader = csv.DictReader(source)
|
||||
require(tuple(reader.fieldnames or ()) == REQUEST_FIELDS, "Request CSV schema mismatch")
|
||||
request_rows = [{name: int(row[name]) for name in REQUEST_FIELDS} for row in reader]
|
||||
with np.load(case / "requests.npz", allow_pickle=False) as source:
|
||||
require(set(source.files) == {"tick", "neuron_idx"}, "Input request schema mismatch")
|
||||
expected_ticks, expected_nodes = source["tick"], source["neuron_idx"]
|
||||
require(expected_ticks.dtype == np.int32 and expected_nodes.dtype == np.int32 and
|
||||
expected_ticks.ndim == 1 and expected_ticks.shape == expected_nodes.shape,
|
||||
"Input request dtype/shape mismatch")
|
||||
require(len(request_rows) == len(expected_ticks), "Requested event coverage mismatch")
|
||||
require([row["tick"] for row in request_rows] == expected_ticks.tolist() and
|
||||
[row["neuron_idx"] for row in request_rows] == expected_nodes.tolist(), "Requests differ from frozen input")
|
||||
require(all(row["attempted"] == 1 and row["accepted"] in (0, 1) and
|
||||
row["rejected"] == 1 - row["accepted"] for row in request_rows), "Invalid request acceptance bookkeeping")
|
||||
requested_keys = expected_ticks.astype(np.int64) * neurons + expected_nodes
|
||||
require(bool(np.all(np.diff(requested_keys) > 0)) and bool(np.all((expected_ticks >= 0) & (expected_ticks < steps))) and
|
||||
bool(np.all((expected_nodes >= 0) & (expected_nodes < neurons))), "Input requests not sorted unique/in range")
|
||||
actual_accepted = np.array([row["accepted"] for row in request_rows], dtype=np.uint8)
|
||||
require(np.array_equal(np.isin(requested_keys, event_keys, assume_unique=True), actual_accepted.astype(bool)),
|
||||
"Request accepted/rejected values disagree with actual spikes")
|
||||
accepted = int(actual_accepted.sum())
|
||||
require(summary.get("requested") == len(request_rows) and summary.get("accepted_requests") == accepted and
|
||||
summary.get("rejected_requests") == len(request_rows) - accepted and
|
||||
summary.get("total_spikes") == len(state["step"]), "Summary counts disagree with raw output")
|
||||
component = read_json(output / "apl-component.json")
|
||||
require(component.get("ok") is True and component.get("n_neurons") == neurons and
|
||||
component.get("n_kc") == len(kc_globals) and component.get("n_apl") == 2 and
|
||||
component.get("dt_ms") == .1 and summary.get("apl_component") == component,
|
||||
"APL component provenance mismatch")
|
||||
bundle = Path(spec["apl_bundle_dir"])
|
||||
require(component.get("files_sha256") == {name: file_hash(bundle / name) for name in
|
||||
("input_matrix.npy", "output_matrix.npy", "kc_globals.npy", "apl_globals.npy")}, "APL bundle hashes changed")
|
||||
inputs = summary.get("input_sha256", {})
|
||||
require(inputs.get("spec.json") == file_hash(case / "spec.json") and
|
||||
inputs.get("requests.npz") == file_hash(case / "requests.npz"), "Case input provenance mismatch")
|
||||
kc_counts = state["counts"][kc_globals].copy()
|
||||
kc_event_mask = np.isin(state["neuron_idx"], kc_globals)
|
||||
record = {"case": spec["name"], "mode": spec["mode"], "odor_id": spec["odor_id"],
|
||||
"odor_name": spec["odor_name"], "seed": spec["seed"], "split": "calibration",
|
||||
"KC_active": int(np.count_nonzero(kc_counts)), "KC_active_fraction": float(np.count_nonzero(kc_counts) / len(kc_globals)),
|
||||
"KC_spikes": int(kc_counts.sum()), "KC_first_latency_ms":
|
||||
float(state["step"][kc_event_mask][0] * .1) if bool(np.any(kc_event_mask)) else None,
|
||||
"external_events": accepted, "expected_external_events": float(spec["expected_external_events"]),
|
||||
"total_PN_rate_hz": float(spec["rate_sum_hz"]), "finite": True,
|
||||
"total_spikes": len(state["step"]), "requested": len(request_rows)}
|
||||
require(np.isfinite(record["expected_external_events"]) and record["expected_external_events"] > 0,
|
||||
"CAL expected external events must be finite and positive")
|
||||
return {"state": state, "hashes": hashes, "queues": queues, "apl": apl,
|
||||
"requests": request_rows, "component": component, "kc_counts": kc_counts,
|
||||
"expected_external_events": record["expected_external_events"], "external_events": accepted,
|
||||
"record": record, "output": output}
|
||||
|
||||
|
||||
def exact_repeat(first, repeat):
|
||||
checks = {"state/" + name: arrays_bitwise_equal(first["state"][name], repeat["state"][name]) for name in STATE_DTYPES}
|
||||
checks.update({name: arrays_bitwise_equal(first["apl"][name], repeat["apl"][name]) for name in APL_FILES})
|
||||
checks.update(tick_state_hashes=first["hashes"] == repeat["hashes"],
|
||||
queues=first["queues"] == repeat["queues"], requests=first["requests"] == repeat["requests"],
|
||||
apl_component=first["component"] == repeat["component"])
|
||||
return {"pass_all": bool(all(checks.values())), "checks": checks,
|
||||
"scope": "All six state arrays, every tick all-neuron hashes, APL raw float32 traces, queues, requests and APL environment"}
|
||||
|
||||
|
||||
def validate_inventory(records, cfg):
|
||||
require(tuple(cfg["calibration"]["modes"]) == ORIGINAL_MODES and
|
||||
tuple(cfg["calibration"]["seeds"]) == ORIGINAL_SEEDS and
|
||||
cfg["calibration"]["duration_ms"] == 100.0 and cfg["calibration"]["odor_count"] == 6 and
|
||||
cfg["calibration"]["split"] == "calibration" and
|
||||
cfg["calibration"]["repeat_first_seed_each_odor"] is True, "Frozen CAL design changed")
|
||||
gates = cfg["gates"]
|
||||
require(gates["sparse_min_fraction"] == .03 and gates["sparse_max_fraction"] == .09 and
|
||||
gates["minimum_separable_odor_pairs"] == 3 and gates["max_fraction_strict"] == .5 and
|
||||
gates["min_fraction_strict"] == 0 and all(gates[key] is True for key in
|
||||
("same_jaccard_gt_between", "between_euclidean_gt_within", "both_modes_required", "finite_required", "repeat_exact_required")),
|
||||
"Original representation gates or additional integrity gates changed")
|
||||
expected = {(mode, index, seed) for mode in ORIGINAL_MODES for index in range(6) for seed in ORIGINAL_SEEDS}
|
||||
require(len(records) == 60 and {(row["mode"], row["odor_index"], row["seed"]) for row in records} == expected,
|
||||
"CAL inventory must cover exactly 2 modes x 6 odors x 5 seeds")
|
||||
odors = []
|
||||
for index in range(6):
|
||||
ids = {row["odor_id"] for row in records if row["odor_index"] == index}
|
||||
require(len(ids) == 1, "Odor identity changes across seeds/modes")
|
||||
odors.append(next(iter(ids)))
|
||||
require(set(odors) == ORIGINAL_CAL_IDS, "CAL identities differ from the original frozen six")
|
||||
for row in records:
|
||||
require(row["name"] == f"cal-{row['mode']}-{row['odor_index']:02d}-s{row['seed']}", "Unexpected CAL case name")
|
||||
return odors
|
||||
|
||||
|
||||
def summarize(out_json=_DEFAULT_OUTPUT):
|
||||
"""Recompute; default writes exclusively, out_json=None is strictly read-only."""
|
||||
from context import EXP, ROOT, CFG, check
|
||||
target = EXP / "calibration/summary.json" if out_json is _DEFAULT_OUTPUT else (
|
||||
None if out_json is None else Path(out_json))
|
||||
require(target is None or not target.exists(), "Refusing existing calibration summary")
|
||||
failures, runs, run_records, repeats, provenance = [], {}, [], [], []
|
||||
modes, odors = {}, []
|
||||
prerequisites_ok = False
|
||||
encoder = None
|
||||
records = []
|
||||
try:
|
||||
check()
|
||||
records = read_json(EXP / "protocol/calibration-cases.json")
|
||||
odors = validate_inventory(records, CFG)
|
||||
encoder = read_json(EXP / "protocol/encoder-audit.json")
|
||||
require(encoder["panel_count"] == 12 and encoder["cal_count"] == 6 and
|
||||
encoder["seeds"] == list(ORIGINAL_SEEDS) and encoder["requested_before_any_dynamics"] is True and
|
||||
encoder["formal_AB_run"] is False and encoder["heldout_run"] is False, "Invalid frozen encoder scope")
|
||||
# Only static original-panel metadata is read, never heldout trajectories.
|
||||
import pandas as pd
|
||||
original = pd.read_parquet(ROOT / "experiments/phase4l-olfactory-grounding/odor-encoder/frozen-odor-panel.parquet")
|
||||
require(len(original) == 12 and set(original[original.split == "calibration"].odor_id) == ORIGINAL_CAL_IDS,
|
||||
"Original static panel identity mismatch")
|
||||
require(float(np.median(original.mapped_response_sum)) == encoder["median_mapped_response_sum"],
|
||||
"Matched-energy reference is not the original frozen 12-panel median")
|
||||
kc_globals = np.load(EXP / "apl-bundle/kc_globals.npy", allow_pickle=False)
|
||||
require(kc_globals.dtype == np.int32 and kc_globals.shape == (4064,) and
|
||||
bool(np.all(np.diff(kc_globals) > 0)) and bool(np.all((kc_globals >= 0) & (kc_globals < CFG["neurons"]))),
|
||||
"Invalid frozen KC identities")
|
||||
prerequisites_ok = True
|
||||
except Exception as error:
|
||||
failures.append({"stage": "prerequisites", "type": type(error).__name__, "message": str(error)})
|
||||
if prerequisites_ok:
|
||||
for row in records:
|
||||
name = row["name"]
|
||||
first = None
|
||||
try:
|
||||
case = EXP / "cases" / name
|
||||
spec = read_json(case / "spec.json")
|
||||
require(spec["name"] == name and spec["kind"] == "calibration" and spec["steps"] == 1000 and
|
||||
spec["duration_ms"] == 100.0 and spec["dt_ms"] == .1 and spec["training"] is False and
|
||||
all(spec[key] == row[key] for key in ("mode", "odor_id", "odor_index", "seed")),
|
||||
"Case spec differs from fixed CAL inventory")
|
||||
first = read_trial(EXP / "gpu" / (name + "-r0"), case, spec, kc_globals, CFG["neurons"], CFG["edges"])
|
||||
require(first["record"]["requested"] == row["requests"], "Frozen inventory request count mismatch")
|
||||
runs[(row["mode"], row["odor_id"], row["seed"])] = {key: first[key] for key in
|
||||
("kc_counts", "expected_external_events", "external_events")}
|
||||
run_records.append(first["record"])
|
||||
provenance.append({"case": name, "repeat": 0, "state_npz_sha256": file_hash(first["output"] / "state.npz"),
|
||||
"summary_sha256": file_hash(first["output"] / "summary.json")})
|
||||
except Exception as error:
|
||||
failures.append({"stage": "raw_trial", "case": name, "repeat": 0,
|
||||
"type": type(error).__name__, "message": str(error)})
|
||||
if row["seed"] == ORIGINAL_SEEDS[0]:
|
||||
try:
|
||||
require(first is not None, "Baseline unavailable for exact repeat")
|
||||
repeat = read_trial(EXP / "gpu" / (name + "-r1"), case, spec, kc_globals, CFG["neurons"], CFG["edges"])
|
||||
repeat_report = {"case": name, **exact_repeat(first, repeat)}
|
||||
repeats.append(repeat_report)
|
||||
provenance.append({"case": name, "repeat": 1, "state_npz_sha256": file_hash(repeat["output"] / "state.npz"),
|
||||
"summary_sha256": file_hash(repeat["output"] / "summary.json")})
|
||||
require(repeat_report["pass_all"], "Exact repeat differs: " + ", ".join(
|
||||
key for key, value in repeat_report["checks"].items() if not value))
|
||||
except Exception as error:
|
||||
failures.append({"stage": "exact_repeat", "case": name, "repeat": 1,
|
||||
"type": type(error).__name__, "message": str(error)})
|
||||
if len(runs) == 60:
|
||||
for mode in ORIGINAL_MODES:
|
||||
try:
|
||||
modes[mode] = summarize_mode(mode, odors, ORIGINAL_SEEDS, runs, CFG["gates"])
|
||||
except Exception as error:
|
||||
failures.append({"stage": "representation", "mode": mode,
|
||||
"type": type(error).__name__, "message": str(error)})
|
||||
integrity_pass = bool(prerequisites_ok and len(runs) == 60 and not any(row["stage"] in
|
||||
("prerequisites", "raw_trial") for row in failures))
|
||||
repeat_pass = bool(len(repeats) == 12 and all(row["pass_all"] for row in repeats) and
|
||||
not any(row["stage"] == "exact_repeat" for row in failures))
|
||||
representation_pass = bool(len(modes) == 2 and all(row["gate"] for row in modes.values()))
|
||||
result = {"phase": "4AG", "scope": "Original fixed six CAL odors only; not heldout generalization or learning",
|
||||
"pass_all": bool(integrity_pass and repeat_pass and representation_pass),
|
||||
"numerical_integrity_pass": integrity_pass, "exact_repeat_pass": repeat_pass,
|
||||
"original_representation_gate_pass": representation_pass,
|
||||
"expected_base_trials": 60, "valid_base_trials": len(runs), "expected_repeats": 12,
|
||||
"checked_repeats": len(repeats), "seeds": list(ORIGINAL_SEEDS), "duration_ms": 100.0,
|
||||
"odor_ids": odors, "modes": modes, "runs": run_records, "repeats": repeats,
|
||||
"failures": failures, "input_output_provenance": provenance, "encoder_audit": encoder,
|
||||
"metric_source": "phase4l.representation (full original gate), phase4i.pair_metrics",
|
||||
"finite_gate_note": "Added integrity prerequisite; original 4L recorded finite but omitted it from its gate",
|
||||
"repeat_gate_note": "Additional exact repeat requirement; original metrics and numerical thresholds unchanged",
|
||||
"shared_apl_component_oracle_limit": True, "independent_oracle_equivalence_claim": False,
|
||||
"candidate_generated": False, "training": False, "heldout_dynamic_access": False}
|
||||
if target is not None:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with target.open("x", encoding="utf-8") as output:
|
||||
json.dump(result, output, ensure_ascii=False, indent=2, allow_nan=False)
|
||||
output.write("\n")
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(json.dumps({key: value for key, value in summarize().items() if key in
|
||||
("pass_all", "numerical_integrity_pass", "exact_repeat_pass", "original_representation_gate_pass")}), flush=True)
|
||||
@@ -0,0 +1,18 @@
|
||||
49f53924f758317bfcbd2b53cab9b8b785121eb99f3234a8343c9b483c5701d8 PROTOCOL.md
|
||||
edb9cb2d890471fd07afd88c959ef3540b0cd40af490fac3f26396b5e7f5bf9c calibration.py
|
||||
6e1616b086f667fa4e4b5087f500f8080281977ca9922e7b055acfddd2003bd5 compare.py
|
||||
a8bafb9deec49d918976f2836d977ef9f13c26a6fb7f70e015879e29cf986c9a config.json
|
||||
274eb13ccf38dbbc5c7f14d9d642130eb0f69072d91a090f208c30c27f87dd2b context.py
|
||||
b6e3c67e668a971b544df32d76435ab5e537061b9fc2e8d9dedb5d2a726f5782 finalize.py
|
||||
69f5d893f47dafff4ef7c3186c3df0b9014666b32c8f4e943b57016bec92b18a gpu_apl.py
|
||||
21ceec65fe42581dc55872010e0025d660e80358905f1cf4df1dadf341a3b291 legacy_apl_worker.py
|
||||
357a7d3debb5abc34a8714c0233b456547a154d9d5855d28661b4f01019a80ea oracle_apl.py
|
||||
e9328567753e144fc315491970f7e02af13725b93f35e4d4b97f7c4c48ce3338 prepare.py
|
||||
afbe6d5a37c29bda66d38c470093584130d1852e67881c681662aec3c76b33c7 run_case.py
|
||||
84c681030b0032119b5392d0c3f3c136a9c6b0d133eb548c40791a10bbbb8b63 run_suite.py
|
||||
bab3136eecbc5bb09a985d715844ee845cb72b07930ce376bfb39be33bd1845a test_calibration.py
|
||||
1a233f47943e01bbd845ac1dacdb1eec3f88c270c65a5f75bce1cf679b5de097 test_protocol.py
|
||||
0ed665f686e3499f8543e48f1712ccb4523946971cb7a0e096cf4f6d10d3bcba test_python_entry.py
|
||||
5cfc9a4505da6b8fb4e7d2617cdaedaa8475ed46a11ec9bdf0d4d25b1d22c2d3 validate_components.py
|
||||
0d0e72b2f1348f6648124c8c34f75094f76fc5e1a281fbcca130a5fad2f35293 verify_frozen.py
|
||||
208c00961b9d895d14ac050bc23fbc18db49d0df1fcf5c20d4c3eaa8e15ff7ff manifest.json
|
||||
@@ -0,0 +1,508 @@
|
||||
"""Fail-closed byte-exact audit of the shared-APL closed-loop fixtures.
|
||||
|
||||
Comparison is independent of either backend's stored PASS. No tolerance is
|
||||
used, timing fields are never compared, and reports contain no wall timestamp.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
N_GLOBAL = 166700
|
||||
STATE_KEYS = ('step', 'neuron_idx', 'counts', 'v', 'g', 'ref')
|
||||
REQUEST_FIELDS = ('tick', 'neuron_idx', 'attempted', 'accepted', 'rejected')
|
||||
HASH_FIELDS = {'tick', 'v_sha256', 'g_sha256', 'ref_sha256'}
|
||||
QUEUE_FIELDS = {'tick', 'count', 'accepted_count', 'ordered_edge_sha256',
|
||||
'accepted_mask_sha256', 'joint_sha256'}
|
||||
BUNDLE_FILES = ('input_matrix.npy', 'output_matrix.npy', 'kc_globals.npy', 'apl_globals.npy')
|
||||
COEFFICIENTS = ('gd', 'c0', 'cg', 'cv', 'threshold', 'reset', 'seed_volt')
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def file_hash(path):
|
||||
h = hashlib.sha256()
|
||||
with Path(path).open('rb') as source:
|
||||
for block in iter(lambda: source.read(8 << 20), b''):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def raw_hash(array, dtype=None):
|
||||
return hashlib.sha256(np.ascontiguousarray(array, dtype=dtype).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def save(path, report):
|
||||
if path is None:
|
||||
return
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open('x', encoding='utf-8') as target:
|
||||
json.dump(report, target, indent=2, sort_keys=True, allow_nan=False)
|
||||
target.write('\n')
|
||||
|
||||
|
||||
def _read_json(path, used):
|
||||
used.add(Path(path))
|
||||
return json.loads(Path(path).read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def _read_jsonl(path, used):
|
||||
used.add(Path(path))
|
||||
with Path(path).open(encoding='utf-8') as source:
|
||||
return [json.loads(line) for line in source if line.strip()]
|
||||
|
||||
|
||||
def _read_array(path, used):
|
||||
used.add(Path(path))
|
||||
return np.load(path, mmap_mode='r', allow_pickle=False)
|
||||
|
||||
|
||||
def _check(checks, name, condition):
|
||||
checks[name] = bool(condition)
|
||||
|
||||
|
||||
def _bit_mask(a, b):
|
||||
if a.dtype.kind == 'f':
|
||||
unsigned = np.dtype('uint'+str(a.dtype.itemsize*8))
|
||||
return np.ascontiguousarray(a).view(unsigned) != np.ascontiguousarray(b).view(unsigned)
|
||||
return a != b
|
||||
|
||||
|
||||
def _compare_array(name, a, b, checks, differences, dtype=None, shape=None):
|
||||
a, b = np.asarray(a), np.asarray(b)
|
||||
schema = a.shape == b.shape and a.dtype == b.dtype and \
|
||||
(dtype is None or a.dtype == np.dtype(dtype)) and (shape is None or a.shape == shape)
|
||||
_check(checks, name+'.schema', schema)
|
||||
if not schema:
|
||||
differences[name] = {'left_shape': list(a.shape), 'right_shape': list(b.shape),
|
||||
'left_dtype': str(a.dtype), 'right_dtype': str(b.dtype), 'expected_shape': None if shape is None else list(shape),
|
||||
'expected_dtype': None if dtype is None else str(np.dtype(dtype))}
|
||||
_check(checks, name+'.bitwise_exact', False)
|
||||
return False
|
||||
finite = bool(np.isfinite(a).all() and np.isfinite(b).all())
|
||||
_check(checks, name+'.finite', finite)
|
||||
mask = _bit_mask(a, b)
|
||||
exact = not bool(np.any(mask))
|
||||
_check(checks, name+'.bitwise_exact', exact)
|
||||
if not exact or not finite:
|
||||
flat = np.flatnonzero(mask.ravel())
|
||||
detail = {'mismatching_elements': int(len(flat)),
|
||||
'first_positions': [list(map(int, np.unravel_index(int(index), a.shape))) for index in flat[:5]],
|
||||
'left_nonfinite': int(np.count_nonzero(~np.isfinite(a))),
|
||||
'right_nonfinite': int(np.count_nonzero(~np.isfinite(b)))}
|
||||
if finite and a.size:
|
||||
error = np.abs(a.astype(np.float64)-b.astype(np.float64))
|
||||
detail.update(max_abs_error=float(error.max()), mean_abs_error=float(error.mean()))
|
||||
if a.dtype.kind == 'f' and len(flat):
|
||||
unsigned = np.dtype('uint'+str(a.dtype.itemsize*8))
|
||||
av = np.ascontiguousarray(a).view(unsigned).ravel()
|
||||
bv = np.ascontiguousarray(b).view(unsigned).ravel()
|
||||
detail['first_bit_patterns'] = [{'left': hex(int(av[index])), 'right': hex(int(bv[index]))} for index in flat[:5]]
|
||||
differences[name] = detail
|
||||
return exact and finite
|
||||
|
||||
|
||||
def _sha(value):
|
||||
return isinstance(value, str) and re.fullmatch('[0-9a-f]{64}', value) is not None
|
||||
|
||||
|
||||
def _read_requests(path, steps, used):
|
||||
used.add(Path(path))
|
||||
with Path(path).open(encoding='utf-8', newline='') as source:
|
||||
reader = csv.DictReader(source)
|
||||
require(tuple(reader.fieldnames or ()) == REQUEST_FIELDS, 'Request CSV schema mismatch: '+str(path))
|
||||
rows = [{key: int(row[key]) for key in REQUEST_FIELDS} for row in reader]
|
||||
previous = (-1, -1)
|
||||
for row in rows:
|
||||
key = row['tick'], row['neuron_idx']
|
||||
require(previous < key and 0 <= key[0] < steps and 0 <= key[1] < N_GLOBAL,
|
||||
'Request rows must be sorted, unique, and in range: '+str(path))
|
||||
require(row['attempted'] == 1 and row['accepted'] in (0, 1) and row['rejected'] == 1-row['accepted'],
|
||||
'Request accounting is incomplete: '+str(path))
|
||||
previous = key
|
||||
return rows
|
||||
|
||||
|
||||
def _load_run(directory, role, n, edges, steps, record_history, name, used, checks):
|
||||
directory = Path(directory)
|
||||
summary = _read_json(directory/'summary.json', used)
|
||||
_check(checks, role+'.summary_complete', summary.get('complete') is True and summary.get('pass_all') is True and
|
||||
summary.get('steps') == steps and summary.get('completed_ticks') == steps and
|
||||
summary.get('nodes') == n and summary.get('edges') == edges and summary.get('precision') == 'float64')
|
||||
is_cpu = role == 'cpu'
|
||||
_check(checks, role+'.case_name', summary.get('name' if is_cpu else 'case') == name)
|
||||
_check(checks, role+'.history_mode', summary.get('history_recorded' if is_cpu else 'record_history') is record_history)
|
||||
_check(checks, role+'.apl_precision', summary.get('APL_precision' if is_cpu else 'apl_precision') == 'float32')
|
||||
_check(checks, role+'.no_error_marker', not (directory/'error.json').exists())
|
||||
used.add(directory/'state.npz')
|
||||
with np.load(directory/'state.npz', allow_pickle=False) as source:
|
||||
require(set(source.files) == set(STATE_KEYS), 'State NPZ keys changed: '+str(directory))
|
||||
state = {key: source[key].copy() for key in STATE_KEYS}
|
||||
for key, dtype in (('v', 'float64'), ('g', 'float64'), ('ref', 'int32'), ('counts', 'int32')):
|
||||
require(state[key].shape == (n,) and state[key].dtype == np.dtype(dtype), 'Invalid final '+key+': '+str(directory))
|
||||
require(state['step'].dtype == state['neuron_idx'].dtype == np.int32 and state['step'].ndim == 1 and
|
||||
state['step'].shape == state['neuron_idx'].shape, 'Invalid sparse event schema: '+str(directory))
|
||||
require(bool(np.all((state['step'] >= 0) & (state['step'] < steps))) and
|
||||
bool(np.all((state['neuron_idx'] >= 0) & (state['neuron_idx'] < N_GLOBAL))), 'Sparse event range error')
|
||||
event_keys = state['step'].astype(np.int64)*N_GLOBAL+state['neuron_idx']
|
||||
_check(checks, role+'.events_sorted_unique', len(event_keys) < 2 or np.all(np.diff(event_keys) > 0))
|
||||
_check(checks, role+'.count_nonnegative_total', np.all(state['counts'] >= 0) and int(state['counts'].sum()) == len(event_keys))
|
||||
_check(checks, role+'.ref_range', np.all((state['ref'] >= 0) & (state['ref'] <= 22)))
|
||||
_check(checks, role+'.reported_events', summary.get('spikes' if is_cpu else 'total_spikes') == len(event_keys))
|
||||
hashes = _read_jsonl(directory/'tick-state-hashes.jsonl', used)
|
||||
require(len(hashes) == steps and [row.get('tick') for row in hashes] == list(range(steps)), 'Incomplete tick hash coverage')
|
||||
require(all(set(row) == HASH_FIELDS and all(_sha(row[key]) for key in HASH_FIELDS-{'tick'}) for row in hashes),
|
||||
'Invalid tick state hash schema')
|
||||
queues = _read_jsonl(directory/'queue.jsonl', used)
|
||||
require(len(queues) == steps and [row.get('tick') for row in queues] == list(range(steps)), 'Incomplete queue coverage')
|
||||
require(all(set(row) == QUEUE_FIELDS and type(row['count']) is int and type(row['accepted_count']) is int and
|
||||
0 <= row['accepted_count'] <= row['count'] <= edges and
|
||||
all(_sha(row[key]) for key in QUEUE_FIELDS-{'tick', 'count', 'accepted_count'}) for row in queues),
|
||||
'Invalid queue row schema/count/hash')
|
||||
for key in ('v', 'g', 'ref'):
|
||||
_check(checks, role+'.last_hash_'+key, hashes[-1][key+'_sha256'] == raw_hash(state[key], '<i4' if key == 'ref' else '<f8'))
|
||||
requests = _read_requests(directory/'requests.csv', steps, used)
|
||||
requested, accepted = len(requests), sum(row['accepted'] for row in requests)
|
||||
_check(checks, role+'.request_summary', summary.get('requests' if is_cpu else 'requested') == requested and
|
||||
summary.get('applicable_requests' if is_cpu else 'accepted_requests') == accepted and
|
||||
summary.get('ref_rejected_requests' if is_cpu else 'rejected_requests') == requested-accepted)
|
||||
apl = {field: _read_array(directory/('apl-'+field+'.float32.npy'), used) for field in ('drive', 'state', 'inhibition')}
|
||||
for field, width in (('drive', 2), ('state', 2), ('inhibition', 4064)):
|
||||
require(apl[field].dtype == np.float32 and apl[field].shape == (steps, width), 'Invalid APL '+field+' history schema')
|
||||
_check(checks, role+'.apl_positive_'+field, np.all(apl[field] >= 0))
|
||||
histories, pre = {}, {}
|
||||
for field, dtype in (('v', 'float64'), ('g', 'float64'), ('ref', 'int32')):
|
||||
files = (directory/(field+'-history.'+dtype+'.npy'), directory/('preAPL-'+field+'.'+dtype+'.npy'))
|
||||
if record_history:
|
||||
histories[field], pre[field] = (_read_array(path, used) for path in files)
|
||||
require(all(array.dtype == np.dtype(dtype) and array.shape == (steps, n) for array in (histories[field], pre[field])),
|
||||
'Small pre/post history schema mismatch')
|
||||
_check(checks, role+'.history_hashes_'+field, all(row[field+'_sha256'] == raw_hash(histories[field][tick])
|
||||
for tick, row in enumerate(hashes)))
|
||||
_check(checks, role+'.history_final_'+field, not np.any(_bit_mask(histories[field][-1], state[field])))
|
||||
else:
|
||||
_check(checks, role+'.no_dense_'+field, not any(path.exists() for path in files))
|
||||
if is_cpu:
|
||||
component = _read_json(directory/'apl-worker-handshake.json', used)['handshake']
|
||||
else:
|
||||
component = _read_json(directory/'apl-component.json', used)
|
||||
_check(checks, role+'.component_summary_consistent', summary.get('apl_component') == component)
|
||||
return dict(directory=directory, summary=summary, state=state, hashes=hashes, queues=queues,
|
||||
requests=requests, apl=apl, histories=histories, pre=pre, component=component)
|
||||
|
||||
|
||||
def _pair_outputs(left, right, checks, differences, n, steps, record_history):
|
||||
for field in STATE_KEYS:
|
||||
shape = None if field in ('step', 'neuron_idx') else (n,)
|
||||
dtype = 'float64' if field in ('v', 'g') else 'int32'
|
||||
_compare_array('state.'+field, left['state'][field], right['state'][field], checks, differences, dtype, shape)
|
||||
for field, width in (('drive', 2), ('state', 2), ('inhibition', 4064)):
|
||||
_compare_array('apl.'+field, left['apl'][field], right['apl'][field], checks, differences, 'float32', (steps, width))
|
||||
for field in ('hashes', 'queues', 'requests', 'component'):
|
||||
same = left[field] == right[field]
|
||||
_check(checks, field+'.parsed_exact', same)
|
||||
if not same:
|
||||
if isinstance(left[field], list):
|
||||
first = next((i for i, (a, b) in enumerate(zip(left[field], right[field])) if a != b), None)
|
||||
differences[field] = {'left_count': len(left[field]), 'right_count': len(right[field]),
|
||||
'first_differing_row': first,
|
||||
'left_row': None if first is None else left[field][first],
|
||||
'right_row': None if first is None else right[field][first]}
|
||||
else:
|
||||
differences[field] = {'left': left[field], 'right': right[field]}
|
||||
if record_history:
|
||||
for stage in ('histories', 'pre'):
|
||||
for field, dtype in (('v', 'float64'), ('g', 'float64'), ('ref', 'int32')):
|
||||
_compare_array(stage+'.'+field, left[stage][field], right[stage][field], checks, differences, dtype, (steps, n))
|
||||
|
||||
|
||||
def _queue_digest(tick, ids, accepted):
|
||||
ids = np.ascontiguousarray(ids, dtype='<i8').tobytes()
|
||||
mask = np.ascontiguousarray(accepted, dtype=np.uint8).tobytes()
|
||||
return {'tick': tick, 'count': len(mask), 'accepted_count': int(np.count_nonzero(accepted)),
|
||||
'ordered_edge_sha256': hashlib.sha256(ids).hexdigest(),
|
||||
'accepted_mask_sha256': hashlib.sha256(mask).hexdigest(),
|
||||
'joint_sha256': hashlib.sha256(b'phase4af-queue-v1\0'+struct.pack('<qq', tick, len(mask))+ids+mask).hexdigest()}
|
||||
|
||||
|
||||
def _independent_event_accounting(run, role, graph, requested, steps, checks, differences):
|
||||
nodes, ptr = graph['nodes'], graph['ptr']
|
||||
n = len(nodes)
|
||||
state = run['state']
|
||||
local = np.searchsorted(nodes, state['neuron_idx'])
|
||||
require(np.all(local < n) and np.array_equal(nodes[local], state['neuron_idx']), 'Actual spike outside graph identity')
|
||||
_check(checks, role+'.events_per_neuron_counts', np.array_equal(np.bincount(local, minlength=n), state['counts']))
|
||||
actual_request_pairs = [(row['tick'], row['neuron_idx']) for row in run['requests']]
|
||||
_check(checks, role+'.requests_match_frozen_input', actual_request_pairs == requested)
|
||||
require(actual_request_pairs == requested, 'Actual request population/time differs from frozen input')
|
||||
bounds = np.searchsorted(state['step'], np.arange(steps+1))
|
||||
request_ticks = np.asarray([row['tick'] for row in run['requests']], np.int32)
|
||||
rbounds = np.searchsorted(request_ticks, np.arange(steps+1))
|
||||
request_nodes = np.asarray([row['neuron_idx'] for row in run['requests']], np.int32)
|
||||
request_local = np.searchsorted(nodes, request_nodes)
|
||||
require(np.all(request_local < n) and np.array_equal(nodes[request_local], request_nodes), 'Request outside graph')
|
||||
last = np.full(n, -1_000_000_000, np.int64)
|
||||
degrees = np.diff(ptr)
|
||||
expected_counts = np.zeros(steps, np.int64)
|
||||
arrival_tick = state['step'].astype(np.int64)+18
|
||||
valid = arrival_tick < steps
|
||||
np.add.at(expected_counts, arrival_tick[valid], degrees[local[valid]])
|
||||
_check(checks, role+'.delayed_all_edges_count', np.array_equal(expected_counts, [row['count'] for row in run['queues']]))
|
||||
request_ok, delivered_ok, ref_ok, queue_ok, reset_ok = True, True, True, True, True
|
||||
first = {}
|
||||
for tick in range(steps):
|
||||
fired = local[bounds[tick]:bounds[tick+1]]
|
||||
lo, hi = rbounds[tick:tick+2]
|
||||
targets = request_local[lo:hi]
|
||||
accepted = (tick-last[targets]) >= 22
|
||||
for index, ok in zip(range(lo, hi), accepted):
|
||||
row = run['requests'][index]
|
||||
if row['accepted'] != int(ok) or row['rejected'] != int(not ok):
|
||||
request_ok = False
|
||||
first.setdefault('request_ref_mismatch', {'tick': tick, 'neuron_idx': row['neuron_idx']})
|
||||
if not np.isin(targets[accepted], fired).all() or np.isin(targets[~accepted], fired).any():
|
||||
delivered_ok = False
|
||||
first.setdefault('request_delivery_mismatch', tick)
|
||||
last[fired] = tick
|
||||
ref = np.maximum(22-(tick-last), 0).astype(np.int32)
|
||||
if raw_hash(ref, '<i4') != run['hashes'][tick]['ref_sha256']:
|
||||
ref_ok = False
|
||||
first.setdefault('ref_hash_mismatch', tick)
|
||||
if run['pre']:
|
||||
if len(fired) and not np.array_equal(np.ascontiguousarray(run['pre']['g'][tick, fired]).view(np.uint64), np.zeros(len(fired), np.uint64)):
|
||||
reset_ok = False
|
||||
first.setdefault('pre_apl_spike_reset_mismatch', tick)
|
||||
due = local[bounds[tick-18]:bounds[tick-17]] if tick >= 18 else np.empty(0, np.int64)
|
||||
chunks = [np.arange(ptr[source], ptr[source+1], dtype=np.int64) for source in due]
|
||||
indices = np.concatenate(chunks) if chunks else np.empty(0, np.int64)
|
||||
expected_queue = _queue_digest(tick, graph['edge_id'][indices], (ref[graph['dst'][indices]] == 0).astype(np.uint8))
|
||||
if expected_queue != run['queues'][tick]:
|
||||
queue_ok = False
|
||||
first.setdefault('queue_reconstruction_mismatch', tick)
|
||||
_check(checks, role+'.request_refractory_gate', request_ok)
|
||||
_check(checks, role+'.accepted_spikes_rejected_absent', delivered_ok)
|
||||
_check(checks, role+'.all_ref_hashes_from_actual_events', ref_ok)
|
||||
if run['pre']:
|
||||
_check(checks, role+'.small_queue_independent_reconstruction', queue_ok)
|
||||
_check(checks, role+'.spike_g_reset_before_apl', reset_ok)
|
||||
if first:
|
||||
differences[role+'.independent_accounting'] = first
|
||||
|
||||
|
||||
def _check_apl_application(run, role, nodes, kc, checks, differences):
|
||||
if not run['pre']:
|
||||
return
|
||||
common, local_kc, bundle_kc = np.intersect1d(nodes, kc, assume_unique=True, return_indices=True)
|
||||
_compare_array(role+'.apl_keeps_voltage', run['pre']['v'], run['histories']['v'], checks, differences, 'float64')
|
||||
_compare_array(role+'.apl_keeps_ref', run['pre']['ref'], run['histories']['ref'], checks, differences, 'int32')
|
||||
expected = np.array(run['pre']['g'], copy=True)
|
||||
delta = run['apl']['inhibition'][:, bundle_kc].astype(np.float64)*np.float64(.001)
|
||||
expected[:, local_kc] = expected[:, local_kc]-delta
|
||||
_compare_array(role+'.apl_unconditional_g_write', expected, run['histories']['g'], checks, differences, 'float64')
|
||||
|
||||
|
||||
def _finish(report, checks, differences, used, out_json):
|
||||
report['checks'] = {name: bool(value) for name, value in checks.items()}
|
||||
report['differences'] = differences
|
||||
report['pass_all'] = bool(checks) and all(checks.values()) and 'error' not in report
|
||||
report['failed_checks'] = sorted(name for name, value in checks.items() if not value)
|
||||
report['evidence_sha256'] = {str(path): file_hash(path) for path in sorted(used) if path.is_file()}
|
||||
save(out_json, report)
|
||||
return report
|
||||
|
||||
|
||||
def compare(case_dir, gpu_dir, cpu_dir, out_json=None):
|
||||
"""Strict independent closed-loop comparison; out_json=None is read-only."""
|
||||
case_dir, gpu_dir, cpu_dir = Path(case_dir), Path(gpu_dir), Path(cpu_dir)
|
||||
if out_json is not None:
|
||||
require(not Path(out_json).exists(), 'Refusing existing comparison report')
|
||||
checks, differences, used = {}, {}, set()
|
||||
report = {'kind': 'shared-APL-component closed-loop byte-exact comparison', 'pass_all': False,
|
||||
'case_dir': str(case_dir), 'gpu_dir': str(gpu_dir), 'cpu_dir': str(cpu_dir),
|
||||
'tolerance': None, 'signed_zero_included': True, 'timing_fields_compared': False,
|
||||
'independent_CPU_all_components_claim': False}
|
||||
try:
|
||||
spec = _read_json(case_dir/'spec.json', used)
|
||||
steps, history = spec['steps'], spec['record_history']
|
||||
require(type(steps) is int and steps > 0 and type(history) is bool and spec['dt_ms'] == .1,
|
||||
'Invalid fixed case clock/history schema')
|
||||
graph_dir, bundle_dir = Path(spec['graph_dir']), Path(spec['apl_bundle_dir'])
|
||||
graph = {field: _read_array(graph_dir/(field+'.npy'), used) for field in
|
||||
('nodes', 'edge_id', 'src', 'dst', 'ptr', 'signed_raw', 'weights')}
|
||||
nodes = graph['nodes']
|
||||
n, edges = len(nodes), len(graph['edge_id'])
|
||||
require(n > 0 and edges > 0 and nodes.dtype == np.int32 and nodes.ndim == 1 and np.all((nodes >= 0) & (nodes < N_GLOBAL)) and
|
||||
np.all(np.diff(nodes) > 0), 'Invalid canonical node identities')
|
||||
require(graph['ptr'].dtype == np.int64 and graph['ptr'].shape == (n+1,) and graph['ptr'][0] == 0 and
|
||||
graph['ptr'][-1] == edges and np.all(np.diff(graph['ptr']) >= 0), 'Invalid CSR')
|
||||
for key, dtype in (('edge_id', 'int64'), ('src', 'int32'), ('dst', 'int32'), ('signed_raw', 'int64'), ('weights', 'float64')):
|
||||
require(graph[key].dtype == np.dtype(dtype) and graph[key].shape == (edges,), 'Invalid graph '+key)
|
||||
require(np.array_equal(graph['src'], np.repeat(np.arange(n, dtype=np.int32), np.diff(graph['ptr']))) and
|
||||
np.all((graph['dst'] >= 0) & (graph['dst'] < n)), 'Invalid source/destination row mapping')
|
||||
require(history == (n < 512) and (n < 512 or n == N_GLOBAL), 'Invalid small/full history declaration')
|
||||
require(np.all(graph['edge_id'] >= 0) and np.all(np.diff(graph['edge_id']) > 0),
|
||||
'Original canonical edge IDs must be unique and increasing')
|
||||
if n == N_GLOBAL:
|
||||
require(edges == 25582938 and np.array_equal(nodes, np.arange(n, dtype=np.int32)) and
|
||||
np.array_equal(graph['edge_id'], np.arange(edges, dtype=np.int64)), 'Full canonical graph identity changed')
|
||||
bundle = {name: _read_array(bundle_dir/name, used) for name in BUNDLE_FILES}
|
||||
kc, apl_ids = bundle['kc_globals.npy'], bundle['apl_globals.npy']
|
||||
require(kc.dtype == apl_ids.dtype == np.int32 and kc.shape == (4064,) and apl_ids.shape == (2,), 'Invalid APL/KC identities')
|
||||
require(np.all((kc >= 0) & (kc < N_GLOBAL)) and np.all(np.diff(kc) > 0) and
|
||||
np.all((apl_ids >= 0) & (apl_ids < N_GLOBAL)) and np.all(np.diff(apl_ids) > 0) and
|
||||
not np.intersect1d(kc, apl_ids).size, 'APL/KC identities overlap or are invalid')
|
||||
for key, shape in (('input_matrix.npy', (2, N_GLOBAL)), ('output_matrix.npy', (2, 4064))):
|
||||
require(bundle[key].dtype == np.float32 and bundle[key].shape == shape and
|
||||
np.isfinite(bundle[key]).all() and np.all(bundle[key] >= 0), 'Invalid normalized APL matrix')
|
||||
gpu = _load_run(gpu_dir, 'gpu', n, edges, steps, history, spec['name'], used, checks)
|
||||
cpu = _load_run(cpu_dir, 'cpu', n, edges, steps, history, spec['name'], used, checks)
|
||||
_pair_outputs(gpu, cpu, checks, differences, n, steps, history)
|
||||
used.add(case_dir/'requests.npz')
|
||||
with np.load(case_dir/'requests.npz', allow_pickle=False) as source:
|
||||
require(set(source.files) == {'tick', 'neuron_idx'} and source['tick'].dtype == source['neuron_idx'].dtype == np.int32,
|
||||
'Fixed request NPZ schema changed')
|
||||
require(source['tick'].shape == source['neuron_idx'].shape and source['tick'].ndim == 1, 'Invalid sparse request shape')
|
||||
requested = list(zip(map(int, source['tick']), map(int, source['neuron_idx'])))
|
||||
for role, run in (('gpu', gpu), ('cpu', cpu)):
|
||||
_independent_event_accounting(run, role, graph, requested, steps, checks, differences)
|
||||
_check_apl_application(run, role, nodes, kc, checks, differences)
|
||||
|
||||
# Independently derive the effective clone. Original graph weights stay read-only.
|
||||
cpu_weights = _read_array(cpu_dir/'weights.npy', used)
|
||||
require(cpu_weights.dtype == np.float64 and cpu_weights.shape == (edges,), 'CPU effective weight schema mismatch')
|
||||
expected_hash, base_hash, mask_hash = hashlib.sha256(), hashlib.sha256(), hashlib.sha256()
|
||||
weight_ok, mask_count = True, 0
|
||||
for start in range(0, edges, 1 << 20):
|
||||
stop = min(edges, start+(1 << 20))
|
||||
original = np.asarray(graph['weights'][start:stop])
|
||||
require(np.isfinite(original).all(), 'Nonfinite original weights')
|
||||
raw_expected = graph['signed_raw'][start:stop]*(.275*.001)
|
||||
require(not np.any(_bit_mask(original, raw_expected)), 'Original SI weights differ from unaltered signed contacts')
|
||||
mask = np.isin(nodes[graph['src'][start:stop]], apl_ids)
|
||||
expected = original.copy()
|
||||
expected[mask] = np.float64(0)
|
||||
mask_count += int(mask.sum())
|
||||
expected_hash.update(np.ascontiguousarray(expected, dtype='<f8').tobytes())
|
||||
base_hash.update(np.ascontiguousarray(original, dtype='<f8').tobytes())
|
||||
mask_hash.update(np.ascontiguousarray(graph['edge_id'][start:stop][mask], dtype='<i8').tobytes())
|
||||
weight_ok &= not np.any(_bit_mask(expected, cpu_weights[start:stop]))
|
||||
expected_sha = expected_hash.hexdigest()
|
||||
_check(checks, 'cpu.effective_weights_independent_bytes', weight_ok)
|
||||
for role, run in (('gpu', gpu), ('cpu', cpu)):
|
||||
_check(checks, role+'.effective_weights_raw_sha', run['summary'].get('effective_weights_raw_sha256') == expected_sha)
|
||||
_check(checks, role+'.apl_fast_edge_count', run['summary'].get('apl_fast_edges_zeroed') == mask_count)
|
||||
_check(checks, role+'.kc_target_count', run['summary'].get('kc_targets_present') == len(np.intersect1d(nodes, kc)))
|
||||
mask_audit = _read_json(cpu_dir/'apl-mask-audit.json', used)
|
||||
_check(checks, 'cpu.mask_provenance', mask_audit['count'] == mask_count and
|
||||
mask_audit['ordered_original_edge_id_sha256'] == mask_hash.hexdigest() and
|
||||
mask_audit['base_weight_raw_bytes_sha256'] == base_hash.hexdigest() and
|
||||
mask_audit['effective_weights_raw_sha256'] == expected_sha and mask_audit['base_graph_files_modified'] is False)
|
||||
|
||||
case_hashes = {name: file_hash(case_dir/name) for name in ('spec.json', 'requests.npz')}
|
||||
if (case_dir/'arrays.npz').exists():
|
||||
case_hashes['arrays.npz'] = file_hash(case_dir/'arrays.npz')
|
||||
used.add(case_dir/'arrays.npz')
|
||||
baseline_coeff = _read_json(Path(spec['coefficients_path']), used)
|
||||
actual_coeff = _read_json(cpu_dir/'coefficients.json', used)
|
||||
_check(checks, 'cpu.actual_author_coefficients_exact', all(float(actual_coeff[key]).hex() == float(baseline_coeff[key]).hex()
|
||||
for key in COEFFICIENTS))
|
||||
audit = _read_json(cpu_dir/'input-audit.json', used)
|
||||
expected_graph_hashes = {name: file_hash(graph_dir/name) for name in
|
||||
tuple(field+'.npy' for field in graph)+('completeness.csv', 'connectivity.parquet')}
|
||||
bundle_hashes = {name: file_hash(bundle_dir/name) for name in BUNDLE_FILES}
|
||||
_check(checks, 'cpu.read_actual_case', audit['spec'] == spec and audit['case_hashes'] == case_hashes)
|
||||
_check(checks, 'cpu.graph_hashes', audit['graph_hashes'] == expected_graph_hashes)
|
||||
_check(checks, 'cpu.bundle_hashes', audit['bundle_hashes'] == bundle_hashes)
|
||||
_check(checks, 'cpu.no_candidate_trace_input', audit.get('candidate_trace_used_as_input') is False and
|
||||
cpu['summary'].get('candidate_trace_used_as_input') is False)
|
||||
_check(checks, 'cpu.worker_source_hash', audit['legacy_apl_worker_sha256'] == file_hash(spec['legacy_apl_worker']))
|
||||
_check(checks, 'cpu.coefficient_input_hash', audit['coefficients_sha256'] == file_hash(spec['coefficients_path']))
|
||||
gpu_hashes = dict(case_hashes, **{'base_weights.npy': file_hash(graph_dir/'weights.npy'),
|
||||
'coefficients.json': file_hash(spec['coefficients_path'])})
|
||||
_check(checks, 'gpu.input_hashes', gpu['summary']['input_sha256'] == gpu_hashes)
|
||||
for role, run in (('gpu', gpu), ('cpu', cpu)):
|
||||
component = run['component']
|
||||
_check(checks, role+'.actual_component_bundle_bytes', component['files_sha256'] == bundle_hashes and
|
||||
component['array_sha256'] == {name: raw_hash(value) for name, value in bundle.items()})
|
||||
_check(checks, role+'.component_dimensions_clock', component.get('ok') is True and
|
||||
component['protocol'] == 'phase4ag-apl-v1' and component['n_neurons'] == N_GLOBAL and
|
||||
component['n_apl'] == 2 and component['n_kc'] == 4064 and component['dt_ms'] == .1)
|
||||
decay = np.float32(math.exp(-.1/20.0))
|
||||
gain = np.float32(.01)
|
||||
_check(checks, role+'.frozen_apl_constants', component['decay_f32_bits'] == f'{int(decay.view(np.uint32)):08x}' and
|
||||
component['gain_f32_bits'] == f'{int(gain.view(np.uint32)):08x}')
|
||||
env = component['environment']
|
||||
_check(checks, role+'.component_precision_environment', env['cuda_matmul_allow_tf32'] is False and
|
||||
env['float32_matmul_precision'] == 'highest' and env['precision_configuration_changed_by_component'] is False and
|
||||
'4090' in env['device_name'])
|
||||
environment = _read_json(cpu_dir/'environment.json', used)
|
||||
_check(checks, 'cpu.pinned_environment', environment['packages']['brian2'] == '2.5.1' and
|
||||
environment['packages']['numpy'] == '1.22.3' and environment['packages']['sympy'] == '1.12' and
|
||||
environment['Torch_imported_into_Brian'] is False and environment['author_precision'] == 'float64')
|
||||
queue_audit = _read_json(cpu_dir/'queue-audit.json', used)
|
||||
_check(checks, 'cpu.queue_observer_coverage', queue_audit['peek_calls'] == queue_audit['advance_calls'] == steps and
|
||||
queue_audit['same_peek_object_returned'] is True and queue_audit['zero_weight_and_refractory_rejected_edges_retained'] is True)
|
||||
_check(checks, 'cpu.sidecar_complete', cpu['summary']['APL_worker_exit_status'] == 0 and cpu['summary']['APL_worker_ticks'] == steps)
|
||||
report.update(case=spec['name'], steps=steps, nodes=n, edges=edges, record_history=history,
|
||||
total_spikes=len(gpu['state']['step']), active_neurons=int(np.count_nonzero(gpu['state']['counts'])),
|
||||
requested=len(requested), accepted_requests=sum(row['accepted'] for row in gpu['requests']),
|
||||
rejected_requests=sum(row['rejected'] for row in gpu['requests']),
|
||||
arriving_edges=sum(row['count'] for row in gpu['queues']),
|
||||
accepted_arriving_edges=sum(row['accepted_count'] for row in gpu['queues']),
|
||||
apl_fast_edges_zeroed=mask_count, effective_weights_raw_sha256=expected_sha,
|
||||
component_environment=gpu['component']['environment'],
|
||||
limits=['APL Torch primitives are shared; original AST component validation is a separate required gate.',
|
||||
'Small cases preserve and independently hash all pre/post states; full cases compare all-tick instrumented state hashes without saving dense whole-brain histories.',
|
||||
'For full cases intermediate finite checks are performed by the instrumented backends; this file independently inspects final states and all APL traces.',
|
||||
'This is exact agreement for the declared trajectory, not physiological validity or learning.'])
|
||||
except Exception as error:
|
||||
report['error'] = {'type': type(error).__name__, 'message': str(error)}
|
||||
return _finish(report, checks, differences, used, out_json)
|
||||
|
||||
|
||||
def compare_gpu_repeats(first, second, out_json=None):
|
||||
"""Repeat equality only; the first run's independent oracle gate is separate."""
|
||||
first, second = Path(first), Path(second)
|
||||
if out_json is not None:
|
||||
require(not Path(out_json).exists(), 'Refusing existing repeat report')
|
||||
checks, differences, used = {}, {}, set()
|
||||
report = {'kind': 'fresh-GPU-run exact repeat comparison', 'first': str(first), 'second': str(second),
|
||||
'pass_all': False, 'tolerance': None, 'signed_zero_included': True, 'timing_fields_compared': False,
|
||||
'scope': 'Scientific outputs, recording integrity and input hashes; independent case/graph audit belongs to compare()'}
|
||||
try:
|
||||
summary = _read_json(first/'summary.json', used)
|
||||
n, edges, steps, name, history = (summary[key] for key in ('nodes', 'edges', 'steps', 'case', 'record_history'))
|
||||
require(type(n) is int and type(edges) is int and type(steps) is int and n > 0 and edges > 0 and steps > 0 and
|
||||
type(history) is bool and history == (n < 512), 'Invalid repeat dimensions')
|
||||
left = _load_run(first, 'first_gpu', n, edges, steps, history, name, used, checks)
|
||||
right = _load_run(second, 'second_gpu', n, edges, steps, history, name, used, checks)
|
||||
_pair_outputs(left, right, checks, differences, n, steps, history)
|
||||
stable = ('case', 'complete', 'pass_all', 'steps', 'completed_ticks', 'nodes', 'edges', 'precision',
|
||||
'apl_precision', 'state_units', 'total_spikes', 'requested', 'accepted_requests', 'rejected_requests',
|
||||
'arriving_edges', 'accepted_arriving_edges', 'counts_match_sparse_events', 'all_finite', 'record_history',
|
||||
'apl_fast_edges_zeroed', 'kc_targets_present', 'effective_weights_raw_sha256', 'apl_schedule',
|
||||
'apl_si_conversion', 'build', 'sealed_ae_backend_sha256', 'learning', 'input_sha256')
|
||||
for key in stable:
|
||||
_check(checks, 'summary.'+key, key in left['summary'] and key in right['summary'] and
|
||||
left['summary'][key] == right['summary'][key])
|
||||
if not checks['summary.'+key]:
|
||||
differences['summary.'+key] = {'first': left['summary'].get(key), 'second': right['summary'].get(key)}
|
||||
report.update(case=name, steps=steps, nodes=n, edges=edges, record_history=history,
|
||||
total_spikes=len(left['state']['step']),
|
||||
explicit_exclusions=['wall_seconds', 'diagnostic_simulation_seconds', 'build_or_cache_seconds',
|
||||
'peak_allocated_vram_bytes', 'peak_reserved_vram_bytes'])
|
||||
except Exception as error:
|
||||
report['error'] = {'type': type(error).__name__, 'message': str(error)}
|
||||
return _finish(report, checks, differences, used, out_json)
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"phase": "4AG-r2",
|
||||
"name": "author-apl-lab-v2",
|
||||
"execution_revision": "Preserve declared venv Python entry rather than resolving its symlink to system Python. No dynamics or inputs changed.",
|
||||
"human_authorization": "允许独立新模型分支测试动力学假设",
|
||||
"gpu_uuid": "GPU-79f3a85d-adce-7833-7003-6488140ca596",
|
||||
"neurons": 166700,
|
||||
"edges": 25582938,
|
||||
"raw_weight_sum": 124177617,
|
||||
"precision": "float64",
|
||||
"apl_precision": "float32",
|
||||
"apl": {"tau_ms": 20.0, "gain": 0.01, "normalization": 1.0},
|
||||
"hypothesis": "Author LIF plus unchanged legacy graded APL operator at end, after reset, unconditional KC g write. Mixed precision: h32 promoted to float64, then multiplied by float64 .001 SI, then subtracted. Not original author's APL model.",
|
||||
"fixtures": ["zero", "pn_pulse", "kc_ref_boundary", "two_sources", "arrival_collision", "natural_request", "apl_only", "high_fanin"],
|
||||
"fixture_repeats": 3,
|
||||
"calibration": {"split": "calibration", "odor_count": 6, "modes": ["physiological", "matched_energy"], "seeds": [51101,51102,51103,51104,51105], "duration_ms": 100.0, "median_scope": "original frozen 12-odor panel", "full_oracle_probes": ["cal-physiological-00-s51101", "cal-matched_energy-00-s51101"], "repeat_first_seed_each_odor": true},
|
||||
"gates": {"sparse_min_fraction": 0.03, "sparse_max_fraction": 0.09, "max_fraction_strict": 0.5, "min_fraction_strict": 0.0, "minimum_separable_odor_pairs": 3, "same_jaccard_gt_between": true, "between_euclidean_gt_within": true, "both_modes_required": true, "finite_required": true, "repeat_exact_required": true},
|
||||
"training": false,
|
||||
"heldout_dynamic_access": false,
|
||||
"candidate_generation": false,
|
||||
"output_budget_bytes": 16106127360,
|
||||
"scope": "Semantic compatibility and fixed old CAL only. Shared-APL-component closed-loop Brian oracle plus independent arithmetic audit; never claim all-component independent CPU equivalence or learning success. No parameter/input/site/window selection from results."
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ROOT=Path('/data/digital-drosophila')
|
||||
MODEL=Path(__file__).resolve().parent
|
||||
EXP=ROOT/'experiments/phase4ag-author-apl-r2'
|
||||
AF_MODEL=ROOT/'models/male-cns-lif/author-fullgraph-lab-v1'
|
||||
AF_EXP=ROOT/'experiments/phase4af-author-fullgraph'
|
||||
AD_EXP=ROOT/'experiments/phase4ad-author-semantics'
|
||||
CFG=json.loads((MODEL/'config.json').read_text(encoding='utf-8'))
|
||||
|
||||
def digest(path):
|
||||
h=hashlib.sha256()
|
||||
with Path(path).open('rb') as f:
|
||||
for b in iter(lambda:f.read(8<<20),b''):h.update(b)
|
||||
return h.hexdigest()
|
||||
|
||||
def save(path,value):
|
||||
path=Path(path);path.parent.mkdir(parents=True,exist_ok=True)
|
||||
with path.open('x',encoding='utf-8') as f:json.dump(value,f,indent=2,ensure_ascii=False,allow_nan=False);f.write('\n')
|
||||
|
||||
def sources():
|
||||
return {p.name:digest(p) for p in sorted(MODEL.iterdir()) if p.is_file() and p.name not in ('manifest.json','checksums.sha256')}
|
||||
|
||||
def integrity():
|
||||
spec=importlib.util.spec_from_file_location('frozen_af_context',AF_MODEL/'context.py')
|
||||
mod=importlib.util.module_from_spec(spec);spec.loader.exec_module(mod)
|
||||
result=mod.integrity()
|
||||
import sys
|
||||
sys.path.insert(0,str(ROOT/'models/male-cns-lif/terminal-scope-diagnostic-v1'))
|
||||
from diagnostic import manifest_check
|
||||
for p in (AF_MODEL,AF_EXP,ROOT/'models/male-cns-lif/author-apl-lab-v1',ROOT/'experiments/phase4ag-author-apl'):
|
||||
result[str(p.relative_to(ROOT))]=manifest_check(p,digest)
|
||||
assert all(r['pass'] for r in result.values())
|
||||
return result
|
||||
|
||||
def check():
|
||||
p=json.loads((EXP/'protocol/preregistered.json').read_text())
|
||||
assert p['sources']==sources(),'Source changed after preregistration'
|
||||
for row in p['inputs']:assert digest(ROOT/row['path'])==row['sha256'],row['path']
|
||||
for row in json.loads((EXP/'protocol/prepared-hashes.json').read_text()):
|
||||
assert digest(EXP/row['path'])==row['sha256'],row['path']
|
||||
return p
|
||||
|
||||
def budget():
|
||||
size=sum(p.stat().st_size for p in EXP.rglob('*') if p.is_file())
|
||||
assert size<CFG['output_budget_bytes'],'New output resource budget exceeded'
|
||||
return size
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Seal a completed gate or preserved failure; never claim learning."""
|
||||
import json
|
||||
from context import ROOT,MODEL,EXP,CFG,check,save,digest,integrity,budget
|
||||
|
||||
|
||||
def seal(root):
|
||||
assert not (root/'manifest.json').exists() and not (root/'checksums.sha256').exists()
|
||||
rows=[dict(path=str(p.relative_to(root)),bytes=p.stat().st_size,sha256=digest(p))
|
||||
for p in sorted(root.rglob('*')) if p.is_file() and '__pycache__' not in p.parts]
|
||||
save(root/'manifest.json',dict(root=str(root),files=rows))
|
||||
with (root/'checksums.sha256').open('x') as f:
|
||||
for row in rows:f.write(row['sha256']+' '+row['path']+'\n')
|
||||
f.write(digest(root/'manifest.json')+' manifest.json\n')
|
||||
|
||||
|
||||
def main():
|
||||
from compare import compare,compare_gpu_repeats
|
||||
check();budget();comparisons=[];repeats={};components={}
|
||||
for p in sorted((EXP/'audit').glob('compare-*.json')):
|
||||
name=p.stem[len('compare-'):]
|
||||
actual=compare(EXP/'cases'/name,EXP/'gpu'/(name+'-r0'),EXP/'oracle'/name,None)
|
||||
assert actual==json.loads(p.read_text()),'Changed raw closed-loop evidence: '+name
|
||||
comparisons.append(dict(case=name,pass_all=actual['pass_all']))
|
||||
for p in sorted((EXP/'audit').glob('repeat-*.json')):
|
||||
name,repeat=p.stem[len('repeat-'):].rsplit('-r',1)
|
||||
actual=compare_gpu_repeats(EXP/'gpu'/(name+'-r0'),EXP/'gpu'/f'{name}-r{repeat}',None)
|
||||
assert actual==json.loads(p.read_text()),'Changed raw repeat evidence: '+name
|
||||
repeats[f'{name}-r{repeat}']=actual['pass_all']
|
||||
for p in (EXP/'component-audit').glob('*.json'):
|
||||
result=json.loads(p.read_text())
|
||||
components[p.stem]=result['pass_all']
|
||||
for filename,sha in result.get('input_sha256',{}).items():
|
||||
assert digest(EXP/'gpu'/(p.stem+'-r0')/filename)==sha
|
||||
gates={}
|
||||
for name in ('fixtures','probes','calibration'):
|
||||
path=EXP/'audit'/(name+'-gate.json')
|
||||
gates[name]=json.loads(path.read_text()) if path.exists() else None
|
||||
calpath=EXP/'calibration/summary.json'
|
||||
cal=json.loads(calpath.read_text()) if calpath.exists() else None
|
||||
if cal is not None:
|
||||
from calibration import summarize
|
||||
assert summarize(out_json=None)==cal,'CAL raw evidence changed'
|
||||
frozen=json.loads((EXP/'regression/frozen-five.json').read_text())
|
||||
after=integrity();save(EXP/'regression/old-integrity-after.json',after)
|
||||
stops=[dict(path=str(p.relative_to(EXP)),report=json.loads(p.read_text())) for p in sorted((EXP/'audit').glob('stopped-*.json'))]
|
||||
expected_cases=CFG['fixtures']+CFG['calibration']['full_oracle_probes']
|
||||
expected_repeats=[f'{name}-r{r}' for name in CFG['fixtures'] for r in range(1,CFG['fixture_repeats'])]
|
||||
compared={x['case']:x['pass_all'] for x in comparisons}
|
||||
missing=dict(comparisons=sorted(set(expected_cases)-set(compared)),
|
||||
components=sorted(set(expected_cases)-set(components)),repeats=sorted(set(expected_repeats)-set(repeats)))
|
||||
evidence_ok=(set(compared)==set(components)==set(expected_cases) and set(repeats)==set(expected_repeats) and
|
||||
all(compared.values()) and all(components.values()) and all(repeats.values()))
|
||||
numerical=evidence_ok and all(gates[x] is not None and gates[x]['pass_all'] for x in ('fixtures','probes'))
|
||||
representation=bool(cal is not None and cal['pass_all'])
|
||||
if numerical:status='PASS-FIXED-CAL-REPRESENTATION' if representation else 'FAIL-FIXED-CAL-REPRESENTATION' if cal is not None else 'INCOMPLETE-CAL-EXECUTION'
|
||||
else:status='FAIL-NUMERICAL-OR-AUDIT-GATE' if any(not p for group in (compared,components,repeats) for p in group.values()) else 'INCOMPLETE-NUMERICAL-EXECUTION'
|
||||
oldpass=bool(frozen['pass_all'] and all(r['pass'] for r in after.values()))
|
||||
if not oldpass:status='FAIL-FROZEN-INTEGRITY-OR-REGRESSION'
|
||||
measurements=[]
|
||||
for p in sorted((EXP/'gpu').glob('*/summary.json')):
|
||||
s=json.loads(p.read_text())
|
||||
measurements.append({k:s[k] for k in ('case','total_spikes','accepted_requests','nodes','edges','diagnostic_simulation_seconds','peak_allocated_vram_bytes')})
|
||||
result=dict(status=status,numerical_gate_pass=bool(numerical),fixed_CAL_representation_pass=representation,
|
||||
gates=gates,closed_loop_comparisons=comparisons,component_results=components,repeat_results=repeats,
|
||||
missing_numerical_evidence=missing,calibration=cal,stopped=stops,
|
||||
old_frozen_integrity_and_five_reference_pass=oldpass,measurements=measurements,
|
||||
unchanged_APL_parameters=CFG['apl'],author_LIF_precision='float64',legacy_APL_precision='float32',
|
||||
explicit_hypothesis=CFG['hypothesis'],APL_nonKC_fast_edges_unreplaced=1212,
|
||||
shared_APL_component_oracle_limitation=True,whole_brain_universal_equivalence_claim=False,
|
||||
heldout_generalization_tested=False,formal_AB_tested=False,learning_achieved=False,candidate_generated=False,
|
||||
payload_bytes_before_manifest=budget(),
|
||||
next_gate='Independent seeds/novel-odor representation, then credit specificity; no learning promotion from CAL' if numerical and representation else
|
||||
'Audit first failure in this explicitly declared hypothesis; no gain search, no heldout tuning, no conditioning')
|
||||
save(EXP/'audit/final-status.json',result)
|
||||
lines=['# Phase 4AG: author LIF + unchanged legacy graded APL','',f'Status: `{status}`.','',
|
||||
f'Numerical closed-loop gate: {numerical}; original complete fixed six-CAL gate: {representation}.',
|
||||
'The independent Brian LIF and candidate use independent state but share the APL arithmetic component. Original frozen APL AST replay and a float64 error audit are separately recorded.',
|
||||
'Author LIF uses float64; APL retains float32, tau20ms/gain0.01/normalization1, full original matrices and order. No learning/parameter search.',
|
||||
'The explicitly hypothesized unconditional end-APL write includes refractory KC. Original fast APL mask leaves1,212 non-KC edges unreplaced; this scope limitation was not silently fixed.',
|
||||
f'Old checksums and five frozen reference trials: {oldpass}.',
|
||||
f'Successfully compared cases: {comparisons}.',
|
||||
'Detailed CAL, exact comparisons, component errors, event/request traces and execution exceptions are linked from audit/final-status.json.',
|
||||
'Wall/VRAM here include per-tick CPU synchronization, IPC and hashes. They are diagnostic costs, not runtime-performance benchmarks.',
|
||||
'No heldout dynamic outputs, formal A/B, learning, game, reward, decoder or new candidate.',
|
||||
'Next gate: '+result['next_gate'],'']
|
||||
with (EXP/'REPORT.md').open('x',encoding='utf-8') as f:f.write('\n'.join(lines))
|
||||
check();seal(MODEL);seal(EXP);print(status,flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,346 @@
|
||||
"""Diagnostic author-float64 LIF plus frozen legacy-float32 APL overlay.
|
||||
|
||||
The sealed Phase 4AE CUDA operators are reused without modification. APL is
|
||||
an explicitly mixed-precision engineering overlay, not an author-model claim.
|
||||
All CUDA/CPU synchronization here is diagnostic; no performance claim is made.
|
||||
"""
|
||||
from functools import lru_cache
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import time
|
||||
import traceback
|
||||
import types
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from legacy_apl_worker import LegacyAPL, N_NEURONS, file_sha256, array_sha256
|
||||
|
||||
|
||||
SOURCE_ROOT = Path(__file__).resolve().parent
|
||||
AE_MODEL = SOURCE_ROOT.parent / "author-runtime-lab-v1"
|
||||
AF_MODEL = SOURCE_ROOT.parent / "author-fullgraph-lab-v1"
|
||||
COEFFICIENT_NAMES = ("gd", "c0", "cg", "cv", "threshold", "reset", "seed_volt")
|
||||
REQUEST_FIELDS = ("tick", "neuron_idx", "attempted", "accepted", "rejected")
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def write_json(path, value):
|
||||
with Path(path).open("x", encoding="utf-8") as output:
|
||||
json.dump(value, output, indent=2, allow_nan=False)
|
||||
output.write("\n")
|
||||
|
||||
|
||||
def raw_hash(value, dtype):
|
||||
return hashlib.sha256(np.ascontiguousarray(value, dtype=dtype).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def load_source(path, name):
|
||||
module = types.ModuleType(name)
|
||||
module.__file__ = str(path)
|
||||
exec(compile(Path(path).read_bytes(), str(path), "exec"), module.__dict__)
|
||||
return module
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def sealed_backend():
|
||||
return load_source(AE_MODEL / "gpu_backend.py", "phase4ag_sealed_ae_backend")
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def queue_function():
|
||||
return load_source(AF_MODEL / "oracle_full.py", "phase4ag_sealed_af_queue").queue_summary
|
||||
|
||||
|
||||
@lru_cache(maxsize=4)
|
||||
def load_graph(graph_directory):
|
||||
root = Path(graph_directory)
|
||||
require(root.is_absolute(), "graph_dir must be absolute")
|
||||
graph = {}
|
||||
for name, dtype in (("nodes", np.int32), ("edge_id", np.int64), ("src", np.int32),
|
||||
("dst", np.int32), ("signed_raw", np.int64), ("ptr", np.int64),
|
||||
("weights", np.float64)):
|
||||
value = np.load(root / (name + ".npy"), mmap_mode="r", allow_pickle=False)
|
||||
require(value.dtype == dtype and value.ndim == 1, "Graph dtype/rank mismatch: " + name)
|
||||
graph[name] = value
|
||||
nodes, edges = graph["nodes"], graph["edge_id"]
|
||||
n, e = len(nodes), len(edges)
|
||||
require(0 < n <= N_NEURONS and bool(np.all((nodes >= 0) & (nodes < N_NEURONS))) and
|
||||
bool(np.all(np.diff(nodes) > 0)), "Global node IDs must be sorted unique")
|
||||
require(n < 512 or (n == N_NEURONS and np.array_equal(nodes, np.arange(N_NEURONS, dtype=np.int32))),
|
||||
"Only small fixtures or the intact frozen full graph are permitted")
|
||||
require(n != N_NEURONS or e == 25582938, "Full graph edge count changed")
|
||||
for name in ("src", "dst", "signed_raw", "weights"):
|
||||
require(graph[name].shape == (e,), "Graph edge array length mismatch: " + name)
|
||||
ptr = graph["ptr"]
|
||||
require(ptr.shape == (n + 1,) and ptr[0] == 0 and ptr[-1] == e and
|
||||
bool(np.all(np.diff(ptr) >= 0)), "Invalid graph CSR")
|
||||
require(np.array_equal(graph["src"], np.repeat(np.arange(n, dtype=np.int32), np.diff(ptr))),
|
||||
"Source order does not match CSR")
|
||||
# Global edge IDs need not equal local row indices in a fixture.
|
||||
for start in range(0, e, 1 << 20):
|
||||
stop = min(start + (1 << 20), e)
|
||||
require(bool(np.all((graph["dst"][start:stop] >= 0) & (graph["dst"][start:stop] < n))),
|
||||
"Local destination out of range")
|
||||
require(bool(np.isfinite(graph["weights"][start:stop]).all()), "Nonfinite base SI weights")
|
||||
require(np.array_equal(np.sign(graph["weights"][start:stop]), np.sign(graph["signed_raw"][start:stop])),
|
||||
"Base weight sign/zero pattern differs from frozen raw contacts")
|
||||
lo = max(0, start - 1)
|
||||
require(bool(np.all(edges[start:stop] >= 0)) and bool(np.all(np.diff(edges[lo:stop]) > 0)),
|
||||
"Global edge IDs must retain canonical increasing source/edge order")
|
||||
return graph
|
||||
|
||||
|
||||
def load_inputs(case_dir):
|
||||
spec = json.loads((case_dir / "spec.json").read_text(encoding="utf-8"))
|
||||
for name in ("name", "steps", "dt_ms", "duration_ms", "model", "graph_dir", "apl",
|
||||
"apl_bundle_dir", "coefficients_path", "record_history"):
|
||||
require(name in spec, "Missing case field: " + name)
|
||||
steps = spec["steps"]
|
||||
require(type(steps) is int and 0 < steps <= 1000 and float(spec["dt_ms"]) == .1 and
|
||||
float(spec["duration_ms"]) == steps * .1, "Unexpected steps/dt/duration")
|
||||
require(type(spec["record_history"]) is bool, "record_history must be Boolean")
|
||||
require(spec["apl"] == {"tau_ms": 20, "gain": .01, "normalization": 1}, "Frozen APL parameters changed")
|
||||
for name in ("graph_dir", "apl_bundle_dir", "coefficients_path"):
|
||||
require(Path(spec[name]).is_absolute(), "Input path must be absolute: " + name)
|
||||
model = spec["model"]
|
||||
for name in ("resting_potential_mv", "threshold_mv", "reset_potential_mv", "synaptic_delay_ms",
|
||||
"refractory_ms", "w_syn_mv_per_anatomical_synapse"):
|
||||
require(name in model and math.isfinite(float(model[name])), "Invalid model field: " + name)
|
||||
require(float(model["synaptic_delay_ms"]) == 1.8 and float(model["refractory_ms"]) == 2.2 and
|
||||
float(model["w_syn_mv_per_anatomical_synapse"]) == .275, "Frozen delay/ref/W_syn changed")
|
||||
coeff = json.loads(Path(spec["coefficients_path"]).read_text(encoding="utf-8"))
|
||||
for name in COEFFICIENT_NAMES:
|
||||
require(name in coeff and math.isfinite(float(coeff[name])), "Invalid author coefficient: " + name)
|
||||
require(coeff["threshold"] == float(model["threshold_mv"]) * .001 and
|
||||
coeff["reset"] == float(model["reset_potential_mv"]) * .001, "Coefficient threshold/reset mismatch")
|
||||
graph = load_graph(str(Path(spec["graph_dir"]).resolve()))
|
||||
n = len(graph["nodes"])
|
||||
require(spec["record_history"] == (n < 512), "Fixtures need dense history; full graph must not duplicate dense histories")
|
||||
with np.load(case_dir / "requests.npz", allow_pickle=False) as source:
|
||||
require(set(source.files) == {"tick", "neuron_idx"}, "Request sparse schema changed")
|
||||
ticks, requested = source["tick"].copy(), source["neuron_idx"].copy()
|
||||
require(ticks.dtype == np.int32 and requested.dtype == np.int32 and ticks.ndim == 1 and
|
||||
ticks.shape == requested.shape, "Invalid sparse request dtype/shape")
|
||||
require(bool(np.all((ticks >= 0) & (ticks < steps))), "Request tick outside trial")
|
||||
if len(ticks) > 1:
|
||||
require(bool(np.all((ticks[1:] > ticks[:-1]) |
|
||||
((ticks[1:] == ticks[:-1]) & (requested[1:] > requested[:-1])))),
|
||||
"Requests must be sorted unique by tick/global neuron")
|
||||
local = np.searchsorted(graph["nodes"], requested)
|
||||
require(bool(np.all(local < n)) and np.array_equal(graph["nodes"][local], requested),
|
||||
"Requested neuron absent from graph")
|
||||
initial = {"initial_v_mv": np.full(n, float(model["resting_potential_mv"]), dtype=np.float64),
|
||||
"initial_g_mv": np.zeros(n, dtype=np.float64)}
|
||||
initial_path = case_dir / "arrays.npz"
|
||||
if initial_path.exists():
|
||||
with np.load(initial_path, allow_pickle=False) as source:
|
||||
require(set(source.files).issubset(initial), "Unexpected initial-state array")
|
||||
for name in source.files:
|
||||
value = source[name]
|
||||
require(value.dtype == np.float64 and value.shape == (n,) and bool(np.isfinite(value).all()),
|
||||
"Invalid initial state: " + name)
|
||||
initial[name] = value.copy()
|
||||
return spec, graph, coeff, ticks, requested, local, initial
|
||||
|
||||
|
||||
def new_array(path, dtype, shape):
|
||||
require(not Path(path).exists(), "Refusing existing output: " + str(path))
|
||||
return np.lib.format.open_memmap(path, mode="w+", dtype=dtype, shape=shape)
|
||||
|
||||
|
||||
def run_gpu(case_dir, out_dir, build_root):
|
||||
"""Run fresh candidate state; never consume oracle/candidate APL traces.
|
||||
|
||||
pass_all denotes internal recording/finite integrity only. Independent
|
||||
comparison and the original complete representation gate are separate.
|
||||
"""
|
||||
case_dir, out_dir = Path(case_dir), Path(out_dir)
|
||||
require(not out_dir.exists(), "Refusing existing output directory: " + str(out_dir))
|
||||
out_dir.mkdir(parents=True, exist_ok=False)
|
||||
start, tick, completed = time.perf_counter(), -1, 0
|
||||
maps = {}
|
||||
try:
|
||||
spec, graph, coeff, request_ticks, request_nodes, request_local, initial = load_inputs(case_dir)
|
||||
require(torch.cuda.is_available() and torch.cuda.device_count() == 1 and
|
||||
"4090" in torch.cuda.get_device_name(), "Only the declared single visible RTX4090 is permitted")
|
||||
ext, build = sealed_backend().build_extension(build_root)
|
||||
build_done = time.perf_counter()
|
||||
apl = LegacyAPL(spec["apl_bundle_dir"], spec["dt_ms"])
|
||||
write_json(out_dir / "apl-component.json", apl.metadata)
|
||||
device = apl.device
|
||||
torch.cuda.reset_peak_memory_stats(device)
|
||||
n, e, steps = len(graph["nodes"]), len(graph["edge_id"]), spec["steps"]
|
||||
|
||||
def upload(value, dtype):
|
||||
return torch.from_numpy(np.array(value, copy=True, order="C")).to(device=device, dtype=dtype).contiguous()
|
||||
|
||||
indptr, dst = upload(graph["ptr"], torch.int64), upload(graph["dst"], torch.int32)
|
||||
global_nodes_gpu = upload(graph["nodes"], torch.int64)
|
||||
base_weights = upload(graph["weights"], torch.float64)
|
||||
weights = base_weights.clone()
|
||||
del base_weights
|
||||
local_apl = np.flatnonzero(np.isin(graph["nodes"], apl.apl_globals)).astype(np.int64)
|
||||
apl_out_mask = np.isin(graph["src"], local_apl)
|
||||
if bool(np.any(apl_out_mask)):
|
||||
weights[upload(apl_out_mask, torch.bool)] = 0.0
|
||||
effective_hash = hashlib.sha256()
|
||||
for offset in range(0, e, 1 << 20):
|
||||
effective_hash.update(weights[offset:offset + (1 << 20)].cpu().numpy().astype("<f8", copy=False).tobytes())
|
||||
common_kc, local_kc, full_kc = np.intersect1d(graph["nodes"], apl.kc_globals, assume_unique=True, return_indices=True)
|
||||
kc_local_gpu, kc_full_gpu = upload(local_kc, torch.int64), upload(full_kc, torch.int64)
|
||||
v = upload(initial["initial_v_mv"] * .001, torch.float64)
|
||||
g = upload(initial["initial_g_mv"] * .001, torch.float64)
|
||||
ref = torch.zeros(n, device=device, dtype=torch.int32)
|
||||
counts = torch.zeros(n, device=device, dtype=torch.int32)
|
||||
requests = torch.zeros(n, device=device, dtype=torch.uint8)
|
||||
ring = torch.zeros((19, n), device=device, dtype=torch.uint8)
|
||||
for name, width in (("drive", 2), ("state", 2), ("inhibition", 4064)):
|
||||
maps["apl_" + name] = new_array(out_dir / ("apl-" + name + ".float32.npy"), np.float32, (steps, width))
|
||||
if spec["record_history"]:
|
||||
for name, dtype in (("v", np.float64), ("g", np.float64), ("ref", np.int32)):
|
||||
maps[name] = new_array(out_dir / (name + "-history." + np.dtype(dtype).name + ".npy"), dtype, (steps, n))
|
||||
maps["pre_" + name] = new_array(out_dir / ("preAPL-" + name + "." + np.dtype(dtype).name + ".npy"), dtype, (steps, n))
|
||||
event_steps, event_nodes = [], []
|
||||
accepted_requests_total, due_total, accepted_due_total = 0, 0, 0
|
||||
queue_summary = queue_function()
|
||||
simulation_start = time.perf_counter()
|
||||
with (out_dir / "queue.jsonl").open("x", encoding="utf-8", buffering=1) as queue_output, \
|
||||
(out_dir / "tick-state-hashes.jsonl").open("x", encoding="utf-8", buffering=1) as hash_output, \
|
||||
(out_dir / "requests.csv").open("x", encoding="utf-8", newline="", buffering=1) as request_output:
|
||||
writer = csv.DictWriter(request_output, fieldnames=REQUEST_FIELDS)
|
||||
writer.writeheader()
|
||||
for tick in range(steps):
|
||||
requests.zero_()
|
||||
begin, end = np.searchsorted(request_ticks, (tick, tick + 1))
|
||||
if end > begin:
|
||||
requested_local_gpu = upload(request_local[begin:end], torch.int64)
|
||||
requests[requested_local_gpu] = 1
|
||||
accepted = (ref[requested_local_gpu] <= 1).to(torch.uint8).cpu().numpy()
|
||||
accepted_requests_total += int(accepted.sum())
|
||||
writer.writerows({"tick": tick, "neuron_idx": int(node), "attempted": 1,
|
||||
"accepted": int(ok), "rejected": 1 - int(ok)}
|
||||
for node, ok in zip(request_nodes[begin:end], accepted))
|
||||
spikes = ring[tick % 19]
|
||||
ext.state_step(v, g, ref, requests, spikes, counts,
|
||||
*[float(coeff[name]) for name in COEFFICIENT_NAMES], 22)
|
||||
expanded_global = np.empty(0, dtype=np.int64)
|
||||
accepted_cpu = np.empty(0, dtype=np.uint8)
|
||||
if tick >= 18:
|
||||
due = ring[(tick - 18) % 19]
|
||||
sources = torch.nonzero(due, as_tuple=False).flatten().contiguous()
|
||||
if sources.numel():
|
||||
degrees = indptr[sources + 1] - indptr[sources]
|
||||
offsets = (torch.cumsum(degrees, dim=0) - degrees).contiguous()
|
||||
edge_count = int(degrees.sum().item())
|
||||
expanded = torch.empty(edge_count, device=device, dtype=torch.int64)
|
||||
ext.fill_edges(indptr, sources, offsets, expanded)
|
||||
if edge_count:
|
||||
destination = dst[expanded]
|
||||
# Actual active fanout order, BEFORE stable target grouping.
|
||||
expanded_global = graph["edge_id"][expanded.cpu().numpy()].copy()
|
||||
accepted_cpu = (ref[destination.long()] == 0).to(torch.uint8).cpu().numpy().copy()
|
||||
order = torch.argsort(destination, stable=True)
|
||||
ordered_edges, ordered_targets = expanded[order].contiguous(), destination[order]
|
||||
boundary = torch.ones(edge_count, device=device, dtype=torch.bool)
|
||||
boundary[1:] = ordered_targets[1:] != ordered_targets[:-1]
|
||||
starts = torch.nonzero(boundary, as_tuple=False).flatten().contiguous()
|
||||
targets = ordered_targets[starts].contiguous()
|
||||
ext.apply_arrivals(ordered_edges, starts, targets, weights, g, ref)
|
||||
queue = queue_summary(tick, expanded_global, accepted_cpu)
|
||||
queue_output.write(json.dumps(queue, allow_nan=False) + "\n")
|
||||
due_total += queue["count"]
|
||||
accepted_due_total += queue["accepted_count"]
|
||||
if spec["record_history"]:
|
||||
for name, value in (("v", v), ("g", g), ("ref", ref)):
|
||||
maps["pre_" + name][tick] = value.cpu().numpy()
|
||||
if n == N_NEURONS:
|
||||
full_spikes = spikes
|
||||
else:
|
||||
apl.spikes.zero_()
|
||||
apl.spikes[global_nodes_gpu] = spikes
|
||||
full_spikes = apl.spikes
|
||||
drive, apl_state, h = apl.advance(full_spikes)
|
||||
# Mixed precision contract: widen f32 h EXACTLY, THEN multiply
|
||||
# in f64, THEN subtract in f64. Do not guard refractory KCs.
|
||||
delta_si = h[kc_full_gpu].to(torch.float64) * 0.001
|
||||
g[kc_local_gpu] = g[kc_local_gpu] - delta_si
|
||||
apl_cpu = {"drive": drive.cpu().numpy().copy(), "state": apl_state.cpu().numpy().copy(),
|
||||
"inhibition": h.cpu().numpy().copy()}
|
||||
for name, value in apl_cpu.items():
|
||||
maps["apl_" + name][tick] = value
|
||||
actual = {"v": v.cpu().numpy().copy(), "g": g.cpu().numpy().copy(),
|
||||
"ref": ref.cpu().numpy().copy()}
|
||||
finite = all(bool(np.isfinite(value).all()) for value in list(actual.values()) + list(apl_cpu.values()))
|
||||
row = {"tick": tick, **{name + "_sha256": raw_hash(value, "<i4" if name == "ref" else "<f8")
|
||||
for name, value in actual.items()}}
|
||||
hash_output.write(json.dumps(row, allow_nan=False) + "\n")
|
||||
if spec["record_history"]:
|
||||
for name, value in actual.items():
|
||||
maps[name][tick] = value
|
||||
fired_local = torch.nonzero(spikes, as_tuple=False).flatten().cpu().numpy()
|
||||
if len(fired_local):
|
||||
event_steps.append(np.full(len(fired_local), tick, dtype=np.int32))
|
||||
event_nodes.append(graph["nodes"][fired_local].copy())
|
||||
completed += 1
|
||||
if not finite:
|
||||
with (out_dir / "first-failure-state.npz").open("xb") as target:
|
||||
np.savez_compressed(target, tick=np.array(tick, dtype=np.int32), **actual,
|
||||
**{"apl_" + name: value for name, value in apl_cpu.items()})
|
||||
require(False, "Nonfinite state; first failure saved")
|
||||
torch.cuda.synchronize(device)
|
||||
|
||||
def joined(values):
|
||||
return np.concatenate(values).astype(np.int32, copy=False) if values else np.empty(0, dtype=np.int32)
|
||||
|
||||
state = {"step": joined(event_steps), "neuron_idx": joined(event_nodes), "counts": counts.cpu().numpy(),
|
||||
"v": v.cpu().numpy(), "g": g.cpu().numpy(), "ref": ref.cpu().numpy()}
|
||||
with (out_dir / "state.npz").open("xb") as target:
|
||||
np.savez_compressed(target, **state)
|
||||
event_locals = np.searchsorted(graph["nodes"], state["neuron_idx"])
|
||||
counts_exact = np.array_equal(np.bincount(event_locals, minlength=n), state["counts"])
|
||||
require(counts_exact and completed == steps, "Sparse events/counts/completed ticks mismatch")
|
||||
for value in maps.values():
|
||||
value.flush()
|
||||
summary = {"case": spec["name"], "complete": True, "pass_all": True,
|
||||
"pass_all_scope": "internal finite/recording/event-count integrity only; independent equivalence not claimed",
|
||||
"steps": steps, "completed_ticks": completed, "nodes": n, "edges": e, "precision": "float64",
|
||||
"apl_precision": "float32", "state_units": "SI volts", "total_spikes": len(state["step"]),
|
||||
"requested": len(request_nodes), "accepted_requests": accepted_requests_total,
|
||||
"rejected_requests": len(request_nodes) - accepted_requests_total,
|
||||
"arriving_edges": due_total, "accepted_arriving_edges": accepted_due_total,
|
||||
"counts_match_sparse_events": bool(counts_exact), "all_finite": True,
|
||||
"record_history": spec["record_history"], "apl_fast_edges_zeroed": int(apl_out_mask.sum()),
|
||||
"kc_targets_present": len(common_kc), "effective_weights_raw_sha256": effective_hash.hexdigest(),
|
||||
"apl_component": apl.metadata, "apl_schedule": "end, after threshold/reset and guarded arrivals, unconditional all KC",
|
||||
"apl_si_conversion": "h32 -> float64 -> multiply float64 0.001 -> separate float64 subtraction",
|
||||
"build": build, "sealed_ae_backend_sha256": file_sha256(AE_MODEL / "gpu_backend.py"),
|
||||
"shared_apl_component_oracle_limitation": True, "oracle_validation_claim": False,
|
||||
"learning": False, "performance_runtime_claim": False, "cpu_sync_each_tick": True,
|
||||
"build_or_cache_seconds": build_done - start,
|
||||
"diagnostic_simulation_seconds": time.perf_counter() - simulation_start,
|
||||
"wall_seconds": time.perf_counter() - start,
|
||||
"peak_allocated_vram_bytes": int(torch.cuda.max_memory_allocated(device)),
|
||||
"peak_reserved_vram_bytes": int(torch.cuda.max_memory_reserved(device)),
|
||||
"input_sha256": {"spec.json": file_sha256(case_dir / "spec.json"),
|
||||
"requests.npz": file_sha256(case_dir / "requests.npz"),
|
||||
"base_weights.npy": file_sha256(Path(spec["graph_dir"]) / "weights.npy"),
|
||||
"coefficients.json": file_sha256(spec["coefficients_path"])}}
|
||||
if (case_dir / "arrays.npz").exists():
|
||||
summary["input_sha256"]["arrays.npz"] = file_sha256(case_dir / "arrays.npz")
|
||||
write_json(out_dir / "summary.json", summary)
|
||||
return summary
|
||||
except Exception as error:
|
||||
for value in maps.values():
|
||||
value.flush()
|
||||
write_json(out_dir / "error.json", {"complete": False, "pass_all": False,
|
||||
"tick_in_progress": tick, "completed_ticks": completed,
|
||||
"type": type(error).__name__, "message": str(error), "traceback": traceback.format_exc()})
|
||||
raise
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Frozen eager Torch float32 APL component and binary oracle sidecar.
|
||||
|
||||
This is the Phase 4P operator sequence, not an author-neuron implementation.
|
||||
No precision/determinism flags are changed here. The full matrix dimensions
|
||||
are retained even when the caller simulates a small neuronal fixture.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
PROTOCOL = "phase4ag-apl-v1"
|
||||
N_NEURONS, N_APL, N_KC = 166700, 2, 4064
|
||||
BUNDLE_FILES = ("input_matrix.npy", "output_matrix.npy", "kc_globals.npy", "apl_globals.npy")
|
||||
FRAME = struct.Struct("<qQ")
|
||||
PAYLOAD_BYTES = (N_APL + N_APL + N_KC) * 4
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def file_sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with Path(path).open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(8 << 20), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def array_sha256(value):
|
||||
return hashlib.sha256(np.ascontiguousarray(value).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def environment_metadata():
|
||||
device = torch.cuda.current_device()
|
||||
return {"torch": str(torch.__version__), "numpy": str(np.__version__),
|
||||
"cuda": torch.version.cuda, "cudnn": torch.backends.cudnn.version(),
|
||||
"device_name": torch.cuda.get_device_name(device), "device_index": device,
|
||||
"device_capability": list(torch.cuda.get_device_capability(device)),
|
||||
"deterministic_algorithms": torch.are_deterministic_algorithms_enabled(),
|
||||
"cudnn_deterministic": torch.backends.cudnn.deterministic,
|
||||
"cudnn_benchmark": torch.backends.cudnn.benchmark,
|
||||
"cuda_matmul_allow_tf32": torch.backends.cuda.matmul.allow_tf32,
|
||||
"cudnn_allow_tf32": torch.backends.cudnn.allow_tf32,
|
||||
"float32_matmul_precision": torch.get_float32_matmul_precision(),
|
||||
"CUBLAS_WORKSPACE_CONFIG": os.environ.get("CUBLAS_WORKSPACE_CONFIG"),
|
||||
"precision_configuration_changed_by_component": False}
|
||||
|
||||
|
||||
class LegacyAPL:
|
||||
"""Independent zero-initialized APL state with original full-width matrices."""
|
||||
|
||||
def __init__(self, bundle_dir, dt_ms):
|
||||
require(float(dt_ms) == 0.1, "Only frozen dt_ms=0.1 is allowed")
|
||||
require(torch.cuda.is_available(), "CUDA is required for the legacy Torch component")
|
||||
self.bundle_dir = Path(bundle_dir)
|
||||
expected = {"input_matrix.npy": (np.float32, (N_APL, N_NEURONS)),
|
||||
"output_matrix.npy": (np.float32, (N_APL, N_KC)),
|
||||
"kc_globals.npy": (np.int32, (N_KC,)),
|
||||
"apl_globals.npy": (np.int32, (N_APL,))}
|
||||
arrays = {}
|
||||
for name, (dtype, shape) in expected.items():
|
||||
value = np.load(self.bundle_dir / name, allow_pickle=False)
|
||||
require(value.dtype == dtype and value.shape == shape and value.flags.c_contiguous,
|
||||
"APL bundle dtype/shape/layout mismatch: " + name)
|
||||
require(bool(np.isfinite(value).all()), "Nonfinite APL bundle: " + name)
|
||||
arrays[name] = value
|
||||
for name in ("kc_globals.npy", "apl_globals.npy"):
|
||||
ids = arrays[name]
|
||||
require(bool(np.all((ids >= 0) & (ids < N_NEURONS))) and
|
||||
bool(np.all(np.diff(ids) > 0)), "APL/KC globals must be sorted unique: " + name)
|
||||
require(not np.intersect1d(arrays["kc_globals.npy"], arrays["apl_globals.npy"]).size,
|
||||
"APL and KC identities overlap")
|
||||
require(bool(np.all(arrays["input_matrix.npy"] >= 0)) and
|
||||
bool(np.all(arrays["output_matrix.npy"] >= 0)), "APL raw-contact matrices must be nonnegative")
|
||||
self.device = torch.device("cuda", torch.cuda.current_device())
|
||||
self.input_gpu = torch.from_numpy(arrays["input_matrix.npy"].copy()).to(self.device)
|
||||
self.output_gpu = torch.from_numpy(arrays["output_matrix.npy"].copy()).to(self.device)
|
||||
self.kc_globals = arrays["kc_globals.npy"].copy()
|
||||
self.apl_globals = arrays["apl_globals.npy"].copy()
|
||||
self.state = torch.zeros(N_APL, dtype=torch.float32, device=self.device)
|
||||
self.spikes = torch.zeros(N_NEURONS, dtype=torch.uint8, device=self.device)
|
||||
self.decay = np.float32(math.exp(-float(dt_ms) / 20.0))
|
||||
self.gain = np.float32(0.01)
|
||||
self.metadata = {"ok": True, "protocol": PROTOCOL, "n_neurons": N_NEURONS,
|
||||
"n_apl": N_APL, "n_kc": N_KC, "dt_ms": float(dt_ms),
|
||||
"files_sha256": {name: file_sha256(self.bundle_dir / name) for name in BUNDLE_FILES},
|
||||
"array_sha256": {name: array_sha256(value) for name, value in arrays.items()},
|
||||
"decay_f32_bits": f"{int(self.decay.view(np.uint32)):08x}",
|
||||
"gain_f32_bits": f"{int(self.gain.view(np.uint32)):08x}",
|
||||
"environment": environment_metadata(),
|
||||
"operator_sequence": "state.mul_(decay32); drive=mv(I32,spikes32); state.add_(drive); h=matmul(state,O32)*gain32",
|
||||
"h_units": "legacy mV", "initial_state": "float32 zeros"}
|
||||
|
||||
@torch.no_grad()
|
||||
def advance(self, spikes_u8):
|
||||
require(spikes_u8.device == self.device and spikes_u8.dtype == torch.uint8 and
|
||||
tuple(spikes_u8.shape) == (N_NEURONS,) and spikes_u8.is_contiguous(),
|
||||
"APL expects full-width contiguous CUDA uint8 spikes")
|
||||
# Preserve separate eager operations, including Python evaluation order
|
||||
# in the frozen state.mul_(...).add_(torch.mv(...)) expression.
|
||||
self.state.mul_(float(self.decay))
|
||||
drive = torch.mv(self.input_gpu, spikes_u8.to(torch.float32))
|
||||
self.state.add_(drive)
|
||||
h = torch.matmul(self.state, self.output_gpu) * self.gain
|
||||
return drive, self.state, h
|
||||
|
||||
def advance_global_fired(self, fired):
|
||||
require(fired.dtype == np.int32 and fired.ndim == 1, "Global spikes must be int32 vector")
|
||||
require(bool(np.all((fired >= 0) & (fired < N_NEURONS))) and
|
||||
bool(np.all(np.diff(fired) > 0)), "Global spikes must be sorted unique and in range")
|
||||
self.spikes.zero_()
|
||||
if fired.size:
|
||||
indices = torch.from_numpy(fired.copy()).to(device=self.device, dtype=torch.int64)
|
||||
self.spikes[indices] = 1
|
||||
return self.advance(self.spikes)
|
||||
|
||||
|
||||
def read_exact(stream, count, boundary_eof=False):
|
||||
result = bytearray()
|
||||
while len(result) < count:
|
||||
block = stream.read(count - len(result))
|
||||
if not block:
|
||||
if boundary_eof and not result:
|
||||
return None
|
||||
raise EOFError("Truncated APL request frame")
|
||||
result.extend(block)
|
||||
return bytes(result)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--bundle-dir", required=True)
|
||||
parser.add_argument("--dt-ms", required=True, type=float)
|
||||
args = parser.parse_args()
|
||||
# stdout is exclusively the handshake line and binary response frames.
|
||||
output, source = sys.stdout.buffer, sys.stdin.buffer
|
||||
component = LegacyAPL(args.bundle_dir, args.dt_ms)
|
||||
output.write((json.dumps(component.metadata, allow_nan=False) + "\n").encode("utf-8"))
|
||||
output.flush()
|
||||
expected_tick = 0
|
||||
while True:
|
||||
header = read_exact(source, FRAME.size, boundary_eof=True)
|
||||
if header is None:
|
||||
return
|
||||
tick, count = FRAME.unpack(header)
|
||||
require(tick == expected_tick and count <= N_NEURONS, "Invalid APL request tick/count")
|
||||
fired = np.frombuffer(read_exact(source, count * 4), dtype="<i4").copy()
|
||||
drive, state, h = component.advance_global_fired(fired)
|
||||
arrays = [value.detach().cpu().numpy().astype("<f4", copy=False) for value in (drive, state, h)]
|
||||
require(all(bool(np.isfinite(value).all()) for value in arrays), "Nonfinite APL output")
|
||||
output.write(FRAME.pack(tick, PAYLOAD_BYTES))
|
||||
for value in arrays:
|
||||
output.write(value.tobytes())
|
||||
output.flush()
|
||||
expected_tick += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/author-apl-lab-v2",
|
||||
"files": [
|
||||
{
|
||||
"path": "PROTOCOL.md",
|
||||
"bytes": 5583,
|
||||
"sha256": "49f53924f758317bfcbd2b53cab9b8b785121eb99f3234a8343c9b483c5701d8"
|
||||
},
|
||||
{
|
||||
"path": "calibration.py",
|
||||
"bytes": 25414,
|
||||
"sha256": "edb9cb2d890471fd07afd88c959ef3540b0cd40af490fac3f26396b5e7f5bf9c"
|
||||
},
|
||||
{
|
||||
"path": "compare.py",
|
||||
"bytes": 33632,
|
||||
"sha256": "6e1616b086f667fa4e4b5087f500f8080281977ca9922e7b055acfddd2003bd5"
|
||||
},
|
||||
{
|
||||
"path": "config.json",
|
||||
"bytes": 1994,
|
||||
"sha256": "a8bafb9deec49d918976f2836d977ef9f13c26a6fb7f70e015879e29cf986c9a"
|
||||
},
|
||||
{
|
||||
"path": "context.py",
|
||||
"bytes": 2134,
|
||||
"sha256": "274eb13ccf38dbbc5c7f14d9d642130eb0f69072d91a090f208c30c27f87dd2b"
|
||||
},
|
||||
{
|
||||
"path": "finalize.py",
|
||||
"bytes": 6747,
|
||||
"sha256": "b6e3c67e668a971b544df32d76435ab5e537061b9fc2e8d9dedb5d2a726f5782"
|
||||
},
|
||||
{
|
||||
"path": "gpu_apl.py",
|
||||
"bytes": 21206,
|
||||
"sha256": "69f5d893f47dafff4ef7c3186c3df0b9014666b32c8f4e943b57016bec92b18a"
|
||||
},
|
||||
{
|
||||
"path": "legacy_apl_worker.py",
|
||||
"bytes": 8158,
|
||||
"sha256": "21ceec65fe42581dc55872010e0025d660e80358905f1cf4df1dadf341a3b291"
|
||||
},
|
||||
{
|
||||
"path": "oracle_apl.py",
|
||||
"bytes": 46236,
|
||||
"sha256": "357a7d3debb5abc34a8714c0233b456547a154d9d5855d28661b4f01019a80ea"
|
||||
},
|
||||
{
|
||||
"path": "prepare.py",
|
||||
"bytes": 15297,
|
||||
"sha256": "e9328567753e144fc315491970f7e02af13725b93f35e4d4b97f7c4c48ce3338"
|
||||
},
|
||||
{
|
||||
"path": "run_case.py",
|
||||
"bytes": 2116,
|
||||
"sha256": "afbe6d5a37c29bda66d38c470093584130d1852e67881c681662aec3c76b33c7"
|
||||
},
|
||||
{
|
||||
"path": "run_suite.py",
|
||||
"bytes": 3700,
|
||||
"sha256": "84c681030b0032119b5392d0c3f3c136a9c6b0d133eb548c40791a10bbbb8b63"
|
||||
},
|
||||
{
|
||||
"path": "test_calibration.py",
|
||||
"bytes": 8127,
|
||||
"sha256": "bab3136eecbc5bb09a985d715844ee845cb72b07930ce376bfb39be33bd1845a"
|
||||
},
|
||||
{
|
||||
"path": "test_protocol.py",
|
||||
"bytes": 11764,
|
||||
"sha256": "1a233f47943e01bbd845ac1dacdb1eec3f88c270c65a5f75bce1cf679b5de097"
|
||||
},
|
||||
{
|
||||
"path": "test_python_entry.py",
|
||||
"bytes": 1760,
|
||||
"sha256": "0ed665f686e3499f8543e48f1712ccb4523946971cb7a0e096cf4f6d10d3bcba"
|
||||
},
|
||||
{
|
||||
"path": "validate_components.py",
|
||||
"bytes": 24824,
|
||||
"sha256": "5cfc9a4505da6b8fb4e7d2617cdaedaa8475ed46a11ec9bdf0d4d25b1d22c2d3"
|
||||
},
|
||||
{
|
||||
"path": "verify_frozen.py",
|
||||
"bytes": 2160,
|
||||
"sha256": "0d0e72b2f1348f6648124c8c34f75094f76fc5e1a281fbcca130a5fad2f35293"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,747 @@
|
||||
"""Phase 4AG shared-APL-component closed-loop Brian oracle.
|
||||
|
||||
Brian has no Torch dependency. A separate, pinned science-Python process
|
||||
executes the unchanged legacy float32 APL operations from Brian's own spikes.
|
||||
This is independent LIF/scheduling validation with a shared APL numerical
|
||||
component, NOT an all-components-independent CPU oracle.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import hashlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import queue
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import types
|
||||
from contextlib import ExitStack
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
BLOCK_BYTES = 8 << 20
|
||||
PROTOCOL = "phase4ag-apl-v1"
|
||||
AUTHOR_PATH = Path("/data/digital-drosophila/experiments/phase4ad-author-semantics/external-resources/model.py")
|
||||
AUTHOR_SHA256 = "fc45837d7122c6ce2a7f3f2f23c515992e4b232aadb919efabb72337fac88e4e"
|
||||
AE_PATH = Path("/data/digital-drosophila/models/male-cns-lif/author-runtime-lab-v1/oracle_stress.py")
|
||||
AE_SHA256 = "32eb1c42383068ce52701d2139a376ce155ad7072cde019949f8ca2d55492409"
|
||||
AF_PATH = Path("/data/digital-drosophila/models/male-cns-lif/author-fullgraph-lab-v1/oracle_full.py")
|
||||
AF_SHA256 = "d72003d98f0f1624b12126eff50bd361c43d888f4a64d90ba9247a58976f1961"
|
||||
EVENT_DTYPE = np.dtype([("step", "<i4"), ("neuron_idx", "<i4")])
|
||||
BUNDLE_FILES = ("input_matrix.npy", "output_matrix.npy", "kc_globals.npy", "apl_globals.npy")
|
||||
_PROCESS_USED = False
|
||||
|
||||
|
||||
def _check(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def _save(path, value):
|
||||
with Path(path).open("x", encoding="utf-8") as handle:
|
||||
json.dump(value, handle, indent=2, sort_keys=True, allow_nan=False)
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def _digest(path):
|
||||
result = hashlib.sha256()
|
||||
with Path(path).open("rb") as handle:
|
||||
for block in iter(lambda: handle.read(BLOCK_BYTES), b""):
|
||||
result.update(block)
|
||||
return result.hexdigest()
|
||||
|
||||
|
||||
def _array_hash(array, dtype):
|
||||
return hashlib.sha256(np.ascontiguousarray(array, dtype=dtype).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def _load_checked(path, expected, name):
|
||||
data = Path(path).read_bytes()
|
||||
_check(hashlib.sha256(data).hexdigest() == expected, f"Pinned source hash mismatch: {path}")
|
||||
module = types.ModuleType(name)
|
||||
module.__file__ = str(path)
|
||||
exec(compile(data, str(path), "exec"), module.__dict__)
|
||||
return module
|
||||
|
||||
|
||||
def _python_entry(path):
|
||||
"""A venv is selected by its invocation path, not the resolved ELF target."""
|
||||
entry = Path(path).absolute()
|
||||
_check(entry.is_file(), f"Missing declared Python entry: {entry}")
|
||||
return entry
|
||||
|
||||
|
||||
def _new_memmap(path, dtype, shape):
|
||||
_check(not Path(path).exists(), f"Refusing to overwrite output: {path}")
|
||||
return np.lib.format.open_memmap(path, mode="w+", dtype=dtype, shape=shape)
|
||||
|
||||
|
||||
def _budget(root, limit, reserve=0):
|
||||
total = 0
|
||||
for directory, _, filenames in os.walk(root, followlinks=False):
|
||||
for filename in filenames:
|
||||
total += (Path(directory) / filename).stat(follow_symlinks=False).st_size
|
||||
_check(total + int(reserve) <= limit,
|
||||
f"Output budget exceeded: {total}+{reserve}>{limit}; inputs and duration were not adjusted")
|
||||
return total
|
||||
|
||||
|
||||
class _PipeReader:
|
||||
"""One bounded reader thread, with timeout/EOF checks for every frame."""
|
||||
|
||||
def __init__(self, pipe, timeout):
|
||||
self.pipe, self.timeout = pipe, float(timeout)
|
||||
self.chunks = queue.Queue(maxsize=8)
|
||||
self.buffer = bytearray()
|
||||
self.thread = threading.Thread(target=self._pump, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def _pump(self):
|
||||
try:
|
||||
while True:
|
||||
chunk = os.read(self.pipe.fileno(), 65536)
|
||||
self.chunks.put(chunk)
|
||||
if not chunk:
|
||||
return
|
||||
except BaseException as error:
|
||||
self.chunks.put(error)
|
||||
|
||||
def _fill(self, deadline):
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError("APL sidecar response timed out")
|
||||
try:
|
||||
chunk = self.chunks.get(timeout=remaining)
|
||||
except queue.Empty as error:
|
||||
raise TimeoutError("APL sidecar response timed out") from error
|
||||
if isinstance(chunk, BaseException):
|
||||
raise RuntimeError("APL sidecar stdout reader failed") from chunk
|
||||
if not chunk:
|
||||
raise EOFError("APL sidecar ended before a complete response")
|
||||
self.buffer.extend(chunk)
|
||||
|
||||
def read(self, size):
|
||||
deadline = time.monotonic() + self.timeout
|
||||
while len(self.buffer) < size:
|
||||
self._fill(deadline)
|
||||
result = bytes(self.buffer[:size])
|
||||
del self.buffer[:size]
|
||||
return result
|
||||
|
||||
def line(self):
|
||||
deadline = time.monotonic() + self.timeout
|
||||
while b"\n" not in self.buffer:
|
||||
_check(len(self.buffer) < 1 << 20, "APL sidecar handshake exceeds 1 MiB")
|
||||
self._fill(deadline)
|
||||
end = self.buffer.index(b"\n") + 1
|
||||
result = bytes(self.buffer[:end])
|
||||
del self.buffer[:end]
|
||||
return result
|
||||
|
||||
|
||||
class _APLWorker:
|
||||
def __init__(self, python, script, bundle_dir, stderr, timeout=180):
|
||||
self.command = [str(python), "-B", str(script), "--bundle-dir", str(bundle_dir), "--dt-ms", "0.1"]
|
||||
self.process = subprocess.Popen(self.command, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=stderr, bufsize=0, shell=False)
|
||||
self.reader = _PipeReader(self.process.stdout, timeout)
|
||||
self.tick = 0
|
||||
try:
|
||||
self.hello = json.loads(self.reader.line().decode("utf-8"))
|
||||
_check(self.hello.get("ok") is True, "APL sidecar did not acknowledge initialization")
|
||||
_check(self.hello.get("protocol") == PROTOCOL, "Unexpected APL sidecar protocol")
|
||||
for key, value in (("n_neurons", 166700), ("n_apl", 2), ("n_kc", 4064)):
|
||||
_check(self.hello.get(key) == value, f"APL sidecar changed {key}")
|
||||
_check(self.hello.get("dt_ms") == .1, "APL sidecar changed dt")
|
||||
expected_decay = f"{int(np.float32(math.exp(-.1 / 20.)).view(np.uint32)):08x}"
|
||||
expected_gain = f"{int(np.float32(.01).view(np.uint32)):08x}"
|
||||
_check(self.hello.get("decay_f32_bits") == expected_decay and self.hello.get("gain_f32_bits") == expected_gain,
|
||||
"APL sidecar decay/gain differs from the frozen float32 scalars")
|
||||
_check(self.hello.get("files_sha256") == {name: _digest(bundle_dir / name) for name in BUNDLE_FILES},
|
||||
"APL sidecar bundle hashes differ from the independently read bundle")
|
||||
except BaseException:
|
||||
self.abort()
|
||||
raise
|
||||
|
||||
def step(self, tick, global_spikes):
|
||||
_check(tick == self.tick, "APL sidecar request ticks must be consecutive")
|
||||
nodes = np.asarray(global_spikes)
|
||||
_check(nodes.ndim == 1 and nodes.dtype == np.dtype("int32"), "APL input spikes must be int32 globals")
|
||||
_check(not len(nodes) or (nodes[0] >= 0 and nodes[-1] < 166700 and np.all(np.diff(nodes) > 0)),
|
||||
"APL input spikes must be sorted, unique, and in the frozen global range")
|
||||
packet = struct.pack("<qQ", int(tick), len(nodes)) + np.ascontiguousarray(nodes, dtype="<i4").tobytes()
|
||||
view = memoryview(packet)
|
||||
while view:
|
||||
written = self.process.stdin.write(view)
|
||||
if not written:
|
||||
raise BrokenPipeError("APL sidecar stdin failed")
|
||||
view = view[written:]
|
||||
self.process.stdin.flush()
|
||||
response_tick, byte_count = struct.unpack("<qQ", self.reader.read(16))
|
||||
_check(response_tick == tick and byte_count == (2 + 2 + 4064) * 4,
|
||||
"APL sidecar frame tick/length mismatch")
|
||||
values = np.frombuffer(self.reader.read(byte_count), dtype="<f4")
|
||||
_check(np.isfinite(values).all() and np.all(values >= 0), "APL sidecar returned invalid nonnegative f32 states")
|
||||
self.tick += 1
|
||||
return values[:2], values[2:4], values[4:]
|
||||
|
||||
def finish(self, steps):
|
||||
_check(self.tick == steps, "APL sidecar did not process every tick")
|
||||
self.process.stdin.close()
|
||||
status = self.process.wait(timeout=30)
|
||||
_check(status == 0, f"APL sidecar exited unsuccessfully: {status}")
|
||||
self.reader.thread.join(timeout=5)
|
||||
_check(not self.reader.thread.is_alive(), "APL stdout did not reach EOF")
|
||||
extra = bytes(self.reader.buffer)
|
||||
while not self.reader.chunks.empty():
|
||||
chunk = self.reader.chunks.get_nowait()
|
||||
_check(not isinstance(chunk, BaseException), "APL stdout reader failed after the last frame")
|
||||
extra += chunk
|
||||
_check(not extra, "APL sidecar emitted unrequested trailing stdout bytes")
|
||||
self.process.stdout.close()
|
||||
return status
|
||||
|
||||
def abort(self):
|
||||
if self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait(timeout=5)
|
||||
for handle in (self.process.stdin, self.process.stdout):
|
||||
if handle is not None and not handle.closed:
|
||||
handle.close()
|
||||
|
||||
|
||||
def _graph_inputs(directory):
|
||||
arrays = {}
|
||||
definitions = {"nodes": "int32", "edge_id": "int64", "src": "int32",
|
||||
"dst": "int32", "signed_raw": "int64", "ptr": "int64", "weights": "float64"}
|
||||
for name, dtype in definitions.items():
|
||||
value = np.load(directory / (name + ".npy"), mmap_mode="r", allow_pickle=False)
|
||||
_check(value.ndim == 1 and value.dtype == np.dtype(dtype), f"Invalid graph array {name}")
|
||||
arrays[name] = value
|
||||
nodes, edge_ids = arrays["nodes"], arrays["edge_id"]
|
||||
n, edges = len(nodes), len(edge_ids)
|
||||
_check(0 < n <= 166700 and edges > 0, "A case needs real global nodes and explicit nonempty connectivity")
|
||||
_check(nodes[0] >= 0 and nodes[-1] < 166700 and np.all(np.diff(nodes) > 0), "Invalid global node order")
|
||||
for name in ("src", "dst", "signed_raw", "weights"):
|
||||
_check(arrays[name].shape == (edges,), f"Graph array {name} has the wrong shape")
|
||||
pointer = arrays["ptr"]
|
||||
_check(pointer.shape == (n + 1,) and pointer[0] == 0 and pointer[-1] == edges and
|
||||
np.all(np.diff(pointer) >= 0), "Invalid graph CSR pointer")
|
||||
source_counts = np.zeros(n, np.int64)
|
||||
identity_edges = True
|
||||
previous_source, previous_edge = -1, -1
|
||||
width = BLOCK_BYTES // 8
|
||||
for begin in range(0, edges, width):
|
||||
stop = min(edges, begin + width)
|
||||
source, target, ids = arrays["src"][begin:stop], arrays["dst"][begin:stop], edge_ids[begin:stop]
|
||||
_check(np.all((source >= 0) & (source < n)) and np.all((target >= 0) & (target < n)),
|
||||
"Graph edge endpoint is outside the local node array")
|
||||
_check(np.all(ids >= 0), "Graph edge IDs must be nonnegative")
|
||||
_check(source[0] > previous_source or (source[0] == previous_source and ids[0] > previous_edge),
|
||||
"Graph edge blocks are not in canonical source/edge order")
|
||||
_check(np.all((source[1:] > source[:-1]) |
|
||||
((source[1:] == source[:-1]) & (ids[1:] > ids[:-1]))),
|
||||
"Graph edges are not in canonical source/edge order")
|
||||
previous_source, previous_edge = int(source[-1]), int(ids[-1])
|
||||
identity_edges &= np.array_equal(ids, np.arange(begin, stop, dtype=np.int64))
|
||||
source_counts += np.bincount(source, minlength=n)
|
||||
_check(np.isfinite(arrays["weights"][begin:stop]).all(), "Nonfinite graph weights")
|
||||
_check(np.array_equal(source_counts, np.diff(pointer)), "CSR pointer and source rows disagree")
|
||||
if not identity_edges:
|
||||
_check(len(np.unique(edge_ids)) == edges, "Original edge IDs must be unique")
|
||||
for name in ("completeness.csv", "connectivity.parquet"):
|
||||
_check((directory / name).is_file(), f"Missing unchanged author input: {name}")
|
||||
if n == 166700:
|
||||
_check(edges == 25582938 and identity_edges and np.array_equal(nodes, np.arange(n, dtype=np.int32)),
|
||||
"A full probe must use the unchanged complete canonical graph")
|
||||
return arrays, identity_edges
|
||||
|
||||
|
||||
def _requests(case_dir, nodes, steps):
|
||||
with np.load(case_dir / "requests.npz", allow_pickle=False) as archive:
|
||||
_check(set(archive.files) == {"tick", "neuron_idx"}, "Unexpected sparse request schema")
|
||||
ticks, requested = archive["tick"].copy(), archive["neuron_idx"].copy()
|
||||
_check(ticks.dtype == requested.dtype == np.dtype("int32") and ticks.ndim == requested.ndim == 1 and
|
||||
ticks.shape == requested.shape, "Requests must be equal-length int32 vectors")
|
||||
_check(np.all((ticks >= 0) & (ticks < steps)), "Request time outside the case")
|
||||
_check(not len(ticks) or np.all((ticks[1:] > ticks[:-1]) |
|
||||
((ticks[1:] == ticks[:-1]) & (requested[1:] > requested[:-1]))),
|
||||
"Requests must be sorted and unique by tick/global node")
|
||||
local = np.searchsorted(nodes, requested)
|
||||
_check(np.all(local < len(nodes)) and np.array_equal(nodes[local], requested),
|
||||
"Request references a global neuron absent from this case")
|
||||
counts = np.bincount(ticks, minlength=steps).astype(np.int32)
|
||||
pointer = np.concatenate((np.zeros(1, np.int64), np.cumsum(counts, dtype=np.int64)))
|
||||
return requested, local.astype(np.int32), pointer, counts
|
||||
|
||||
|
||||
class _QueueProxy:
|
||||
def __init__(self, original, get_tick, available, arrays, identity_edges,
|
||||
due_counts, spike_ring, delay_steps, handle, records, summarize):
|
||||
self._original_queue, self._get_tick = original, get_tick
|
||||
self._available, self._arrays, self._identity_edges = available, arrays, identity_edges
|
||||
self._due_counts, self._spike_ring, self._delay_steps = due_counts, spike_ring, delay_steps
|
||||
self._handle, self._records, self._summarize = handle, records, summarize
|
||||
|
||||
def peek(self, *args, **kwargs):
|
||||
original = self._original_queue.peek(*args, **kwargs)
|
||||
local_edges = np.asarray(original)
|
||||
tick = self._get_tick()
|
||||
_check(tick == len(self._records["rows"]), "Queue peek ticks must be consecutive")
|
||||
_check(local_edges.ndim == 1 and np.all((local_edges >= 0) & (local_edges < len(self._arrays["edge_id"]))),
|
||||
"Original queue contains an invalid local edge index")
|
||||
_check(len(local_edges) == self._due_counts[tick % len(self._due_counts)],
|
||||
"Queue arrivals differ from the fixed-delay outgoing degree of actual spikes")
|
||||
fired_then = np.zeros(len(self._arrays["nodes"]), dtype=bool)
|
||||
if tick >= self._delay_steps:
|
||||
fired_then[self._spike_ring[(tick - self._delay_steps) % len(self._spike_ring)]] = True
|
||||
|
||||
def accepted(begin, stop):
|
||||
part = local_edges[begin:stop]
|
||||
_check(np.all(fired_then[self._arrays["src"][part]]), "Arrival lacks a presynaptic spike at author delay")
|
||||
return self._available[self._arrays["dst"][part]]
|
||||
|
||||
# Only the observed ID view changes; the queue is returned verbatim.
|
||||
edge_ids = local_edges if self._identity_edges else self._arrays["edge_id"][local_edges]
|
||||
summary = self._summarize(tick, edge_ids, accepted)
|
||||
self._handle.write(json.dumps(summary, sort_keys=True) + "\n")
|
||||
self._records["rows"].append(summary)
|
||||
self._records["peek_calls"] += 1
|
||||
return original
|
||||
|
||||
def advance(self, *args, **kwargs):
|
||||
self._records["advance_calls"] += 1
|
||||
return self._original_queue.advance(*args, **kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._original_queue, name)
|
||||
|
||||
|
||||
def run_case(case_dir: Path, out_dir: Path) -> dict:
|
||||
"""Run one continuous case, with exclusive outputs and no candidate trace input.
|
||||
|
||||
Graph inputs are the AF array/author-table schema plus original weights.npy.
|
||||
requests.npz contains sparse tick/neuron_idx int32 vectors. Optional
|
||||
arrays.npz contains initial_v_mv/initial_g_mv float64 vectors only.
|
||||
Full probes write state hashes, sparse events and APL traces, not dense
|
||||
whole-brain histories. Each case requires a fresh process.
|
||||
"""
|
||||
global _PROCESS_USED
|
||||
_check(not _PROCESS_USED, "Every oracle case requires a fresh process")
|
||||
_PROCESS_USED = True
|
||||
case_dir, out_dir = Path(case_dir).resolve(strict=True), Path(out_dir).resolve()
|
||||
spec = json.loads((case_dir / "spec.json").read_text(encoding="utf-8"))
|
||||
graph_dir = Path(spec["graph_dir"]).resolve(strict=True)
|
||||
bundle_dir = Path(spec["apl_bundle_dir"]).resolve(strict=True)
|
||||
worker_path = Path(spec["legacy_apl_worker"]).resolve(strict=True)
|
||||
science_python = _python_entry(spec["science_python"])
|
||||
author_path = Path(spec.get("author_path", AUTHOR_PATH)).resolve(strict=True)
|
||||
ae_path = Path(spec.get("ae_oracle_path", AE_PATH)).resolve(strict=True)
|
||||
af_path = Path(spec.get("af_oracle_path", AF_PATH)).resolve(strict=True)
|
||||
coefficients_path = Path(spec["coefficients_path"]).resolve(strict=True)
|
||||
steps = int(spec["steps"])
|
||||
_check(steps == spec["steps"] and 0 < steps <= np.iinfo(np.int32).max, "Invalid number of steps")
|
||||
_check(float(spec["dt_ms"]) == .1 and np.isclose(float(spec["duration_ms"]), steps * .1, rtol=0, atol=1e-12),
|
||||
"The frozen APL protocol requires dt_ms=0.1 and a matching duration")
|
||||
_check(spec.get("precision", "float64") == "float64", "Author state must be float64")
|
||||
_check(spec.get("training", False) is False and spec.get("poi_used", False) is False,
|
||||
"Training and original poi are outside this gate")
|
||||
_check(spec["apl"] == {"tau_ms": 20, "gain": .01, "normalization": 1}, "Frozen APL parameters changed")
|
||||
record_history = spec["record_history"]
|
||||
_check(isinstance(record_history, bool), "record_history must be a boolean")
|
||||
budget_root = Path(spec.get("budget_root", out_dir.parent)).resolve()
|
||||
budget_limit = int(spec.get("output_budget_bytes", 15 * 1024 ** 3))
|
||||
_check(budget_limit > 0 and out_dir.is_relative_to(budget_root), "Invalid output budget/root")
|
||||
out_dir.mkdir(parents=True, exist_ok=False)
|
||||
mapped_files = []
|
||||
saved_preferences, old_dt, worker = None, None, None
|
||||
last_complete_tick = -1
|
||||
try:
|
||||
arrays, identity_edges = _graph_inputs(graph_dir)
|
||||
nodes, n, edges = arrays["nodes"], len(arrays["nodes"]), len(arrays["edge_id"])
|
||||
_check(n < 166700 or not record_history, "Full probes must not save dense V/g/ref histories")
|
||||
bundle = {name: np.load(bundle_dir / (name + ".npy"), mmap_mode="r", allow_pickle=False)
|
||||
for name in ("input_matrix", "output_matrix", "kc_globals", "apl_globals")}
|
||||
for name, shape in (("input_matrix", (2, 166700)), ("output_matrix", (2, 4064))):
|
||||
value = bundle[name]
|
||||
_check(value.dtype == np.dtype("float32") and value.shape == shape and value.flags.c_contiguous,
|
||||
f"Frozen APL {name} shape/dtype/layout changed")
|
||||
_check(np.isfinite(value).all() and np.all(value >= 0), f"Invalid positive raw-normalized APL {name}")
|
||||
for name, count in (("kc_globals", 4064), ("apl_globals", 2)):
|
||||
value = bundle[name]
|
||||
_check(value.dtype == np.dtype("int32") and value.shape == (count,) and value[0] >= 0 and
|
||||
value[-1] < 166700 and np.all(np.diff(value) > 0), f"Frozen APL identity {name} changed")
|
||||
_check(not np.intersect1d(bundle["kc_globals"], bundle["apl_globals"]).size, "APL and KC identities overlap")
|
||||
kc_positions = np.searchsorted(nodes, bundle["kc_globals"])
|
||||
present = kc_positions < n
|
||||
present[present] &= nodes[kc_positions[present]] == bundle["kc_globals"][present]
|
||||
kc_local, kc_in_bundle = kc_positions[present], np.flatnonzero(present)
|
||||
requested_globals, request_local, request_ptr, request_count = _requests(case_dir, nodes, steps)
|
||||
initial = None
|
||||
if (case_dir / "arrays.npz").exists():
|
||||
with np.load(case_dir / "arrays.npz", allow_pickle=False) as archive:
|
||||
_check(set(archive.files) == {"initial_v_mv", "initial_g_mv"}, "Unexpected initial-state schema")
|
||||
initial = {name: archive[name].copy() for name in archive.files}
|
||||
for name, value in initial.items():
|
||||
_check(value.dtype == np.dtype("float64") and value.shape == (n,) and np.isfinite(value).all(),
|
||||
f"Invalid diagnostic initial state: {name}")
|
||||
history_bytes = steps * n * 40 if record_history else 0
|
||||
_budget(budget_root, budget_limit, history_bytes + steps * (2 + 2 + 4064) * 4 + edges * 8 + (64 << 20))
|
||||
versions = {name: importlib.metadata.version(name) for name in
|
||||
("brian2", "numpy", "pandas", "pyarrow", "sympy", "scipy", "cython")}
|
||||
_save(out_dir / "environment.json", {"python": sys.version, "platform": platform.platform(),
|
||||
"packages": versions, "codegen": "numpy", "author_precision": "float64",
|
||||
"APL_precision": "float32 in separate science-Python process", "Torch_imported_into_Brian": False})
|
||||
for name, required in (("brian2", "2.5.1"), ("numpy", "1.22.3"), ("sympy", "1.12")):
|
||||
_check(versions[name] == required, f"{name} must remain pinned to {required}")
|
||||
import brian2 as b2
|
||||
import pandas as pd
|
||||
from brian2.core.base import BrianObject
|
||||
|
||||
ae = _load_checked(ae_path, AE_SHA256, "phase4ag_sealed_ae_helpers")
|
||||
af = _load_checked(af_path, AF_SHA256, "phase4ag_sealed_af_helpers")
|
||||
author = _load_checked(author_path, AUTHOR_SHA256, "phase4ag_unchanged_author")
|
||||
_check("torch" not in sys.modules, "Torch must not be imported into the pinned Brian process")
|
||||
comp = pd.read_csv(graph_dir / "completeness.csv", index_col=0)
|
||||
_check(np.array_equal(comp.index.to_numpy(), nodes) and
|
||||
np.array_equal(comp["local_index"].to_numpy(), np.arange(n)), "Author CSV neuron identity differs")
|
||||
del comp
|
||||
graph_hashes = {name: _digest(graph_dir / name) for name in
|
||||
tuple(key + ".npy" for key in arrays) + ("completeness.csv", "connectivity.parquet")}
|
||||
bundle_hashes = {name: _digest(bundle_dir / name) for name in BUNDLE_FILES}
|
||||
case_hashes = {name: _digest(case_dir / name) for name in ("spec.json", "requests.npz")}
|
||||
if initial is not None:
|
||||
case_hashes["arrays.npz"] = _digest(case_dir / "arrays.npz")
|
||||
worker_hash = _digest(worker_path)
|
||||
coefficient_hash = _digest(coefficients_path)
|
||||
baseline_coefficients = json.loads(coefficients_path.read_text(encoding="utf-8"))
|
||||
if "legacy_apl_worker_sha256" in spec:
|
||||
_check(worker_hash == spec["legacy_apl_worker_sha256"], "APL worker differs from preregistered source")
|
||||
_save(out_dir / "input-audit.json", {"spec": spec, "case_hashes": case_hashes,
|
||||
"graph_hashes": graph_hashes, "bundle_hashes": bundle_hashes,
|
||||
"author_sha256": AUTHOR_SHA256, "ae_sha256": AE_SHA256, "af_sha256": AF_SHA256,
|
||||
"legacy_apl_worker_sha256": worker_hash, "coefficients_sha256": coefficient_hash,
|
||||
"candidate_trace_used_as_input": False,
|
||||
"scope": "shared-APL-component closed-loop oracle, not all-components-independent CPU equivalence"})
|
||||
saved_preferences = {key: b2.prefs[key] for key in
|
||||
("codegen.target", "core.default_float_dtype", "legacy.refractory_timing")}
|
||||
old_dt = b2.defaultclock.dt
|
||||
b2.start_scope()
|
||||
b2.prefs["codegen.target"] = "numpy"
|
||||
b2.prefs["core.default_float_dtype"] = np.float64
|
||||
b2.prefs["legacy.refractory_timing"] = False
|
||||
b2.defaultclock.dt = .1 * b2.ms
|
||||
parameters = dict(author.default_params)
|
||||
_save(out_dir / "parameter-checks.json", ae._parameters_match(parameters, spec["model"], b2))
|
||||
neu, syn, spikes = author.create_model(graph_dir / "completeness.csv", graph_dir / "connectivity.parquet", parameters)
|
||||
_check(len(neu) == n and len(syn) == edges, "Author loaded an unexpected graph size")
|
||||
_check(all(np.dtype(neu.variables[key].dtype) == np.float64 for key in ("v", "g")) and
|
||||
np.dtype(syn.variables["w"].dtype) == np.float64, "Author states/weights are not float64")
|
||||
effective = _new_memmap(out_dir / "weights.npy", "<f8", (edges,))
|
||||
mapped_files.append(effective)
|
||||
width, w_unit, mask_count = BLOCK_BYTES // 8, float(parameters["w_syn"] / b2.volt), 0
|
||||
mask_hasher = hashlib.sha256()
|
||||
base_hasher = hashlib.sha256()
|
||||
effective_hasher = hashlib.sha256()
|
||||
for begin in range(0, edges, width):
|
||||
stop = min(edges, begin + width)
|
||||
np.testing.assert_array_equal(np.asarray(syn.i[begin:stop]), arrays["src"][begin:stop])
|
||||
np.testing.assert_array_equal(np.asarray(syn.j[begin:stop]), arrays["dst"][begin:stop])
|
||||
original = np.asarray(syn.w_[begin:stop], dtype=np.float64).copy()
|
||||
raw_expected = arrays["signed_raw"][begin:stop] * w_unit
|
||||
_check(np.array_equal(original.view(np.uint64), raw_expected.view(np.uint64)) and
|
||||
np.array_equal(original.view(np.uint64), arrays["weights"][begin:stop].view(np.uint64)),
|
||||
"Actual original Brian weights differ from unchanged signed raw or AF base weights")
|
||||
base_hasher.update(np.ascontiguousarray(original, dtype="<f8").tobytes())
|
||||
mask = np.isin(nodes[arrays["src"][begin:stop]], bundle["apl_globals"])
|
||||
mask_count += int(mask.sum())
|
||||
mask_hasher.update(np.ascontiguousarray(arrays["edge_id"][begin:stop][mask], dtype="<i8").tobytes())
|
||||
original[mask] = np.float64(0.0)
|
||||
effective_hasher.update(np.ascontiguousarray(original, dtype="<f8").tobytes())
|
||||
effective[begin:stop] = original
|
||||
syn.w_[begin:stop] = original
|
||||
if n == 166700:
|
||||
_check(mask_count == 5845, "Full legacy APL fast-edge mask must contain exactly 5845 existing edges")
|
||||
effective.flush()
|
||||
_save(out_dir / "apl-mask-audit.json", {"count": mask_count, "ordered_original_edge_id_sha256": mask_hasher.hexdigest(),
|
||||
"base_weight_raw_bytes_sha256": base_hasher.hexdigest(), "effective_weights_file_sha256": _digest(out_dir / "weights.npy"),
|
||||
"effective_weights_raw_sha256": effective_hasher.hexdigest(),
|
||||
"rule": "zero +0 all existing fast edges whose source GLOBAL neuron is in apl_globals; no graph edge removal",
|
||||
"graded_targets": "only KC identities present in this LIF case", "KC_targets_in_case": len(kc_local),
|
||||
"base_graph_files_modified": False, "fullgraph_required_mask_count": 5845})
|
||||
if initial is not None:
|
||||
neu.v = initial["initial_v_mv"] * b2.mV
|
||||
neu.g = initial["initial_g_mv"] * b2.mV
|
||||
v_raw = neu.variables["v"].get_value()
|
||||
g_raw = neu.variables["g"].get_value()
|
||||
last_raw = neu.variables["lastspike"].get_value()
|
||||
available_raw = neu.variables["not_refractory"].get_value()
|
||||
histories = {}
|
||||
pre_apl_histories = {}
|
||||
if record_history:
|
||||
for name, dtype in (("v", "float64"), ("g", "float64"), ("ref", "int32")):
|
||||
histories[name] = _new_memmap(out_dir / f"{name}-history.{dtype}.npy", np.dtype(dtype), (steps, n))
|
||||
mapped_files.append(histories[name])
|
||||
pre_apl_histories[name] = _new_memmap(out_dir / f"preAPL-{name}.{dtype}.npy", np.dtype(dtype), (steps, n))
|
||||
mapped_files.append(pre_apl_histories[name])
|
||||
apl_history = {}
|
||||
for name, count in (("drive", 2), ("state", 2), ("inhibition", 4064)):
|
||||
apl_history[name] = _new_memmap(out_dir / f"apl-{name}.float32.npy", "<f4", (steps, count))
|
||||
mapped_files.append(apl_history[name])
|
||||
_save(out_dir / "output-schema.json", {"history_recorded": record_history, "history_shape": [steps, n],
|
||||
"state_hash_file": "tick-state-hashes.jsonl", "state_hash_encoding": "SHA256 of contiguous little-endian raw f64 V/g and i32 ref",
|
||||
"APL_dtype": "float32", "APL_h_units": "mV; gain already applied in float32",
|
||||
"APL_SI_conversion": "h.astype(float64) * float64(0.001), then separate float64 g subtraction",
|
||||
"APL_write_schedule": "end, order=0, after original reset, unconditional raw g write; recorder end/order=1",
|
||||
"APL_input": "Brian's own actual global spikes, never candidate spikes or candidate inhibition",
|
||||
"APL_shapes": {"drive": [steps, 2], "state": [steps, 2], "inhibition": [steps, 4064]},
|
||||
"request_columns": ["tick", "neuron_idx", "attempted", "accepted", "rejected"],
|
||||
"queue_protocol": "sealed Phase4AF queue_summary; original global edge IDs, including zero-weight/ref-rejected arrivals",
|
||||
"state_npz": ["step", "neuron_idx", "counts", "v", "g", "ref"],
|
||||
"complete_marker": "summary.json complete=true only after worker exit and all checks"})
|
||||
delay_steps = int(round(float(parameters["t_dly"] / b2.ms) / .1))
|
||||
ref_steps = int(round(float(parameters["t_rfc"] / b2.ms) / .1))
|
||||
_check(delay_steps == 18 and ref_steps == 22, "Author delay/refractory changed")
|
||||
queue_len = delay_steps + 1
|
||||
due_counts = np.zeros(queue_len, np.int64)
|
||||
spike_ring = [np.empty(0, np.int32) for _ in range(queue_len)]
|
||||
outdegree = np.diff(arrays["ptr"])
|
||||
request_seen = np.zeros(steps, np.uint8)
|
||||
apl_seen = np.zeros(steps, np.uint8)
|
||||
applicable_count = np.zeros(steps, np.int32)
|
||||
rejected_count = np.zeros(steps, np.int32)
|
||||
spike_count = np.zeros(steps, np.int32)
|
||||
total_counts = np.zeros(n, np.int32)
|
||||
records = {"rows": [], "peek_calls": 0, "advance_calls": 0, "installations": []}
|
||||
last_ticks = np.zeros(n, np.int64)
|
||||
final_ref = np.zeros(n, np.int32)
|
||||
permitted = np.empty(0, np.int32)
|
||||
total_events = 0
|
||||
|
||||
def tick_now():
|
||||
return int(round(float(neu.clock.t / b2.second) / .0001))
|
||||
|
||||
def current_ref(tick):
|
||||
seen = np.isfinite(last_raw) & (last_raw >= 0)
|
||||
last_ticks.fill(0)
|
||||
last_ticks[seen] = np.rint(last_raw[seen] / .0001).astype(np.int64)
|
||||
result = np.where(seen, np.maximum(ref_steps - (tick - last_ticks), 0), 0).astype(np.int32)
|
||||
_check(np.array_equal(result == 0, available_raw), "Ref history disagrees with author not_refractory")
|
||||
return result
|
||||
|
||||
with ExitStack() as stack:
|
||||
stderr = stack.enter_context((out_dir / "apl-worker.stderr.log").open("xb"))
|
||||
worker = _APLWorker(science_python, worker_path, bundle_dir, stderr, float(spec.get("worker_timeout_seconds", 180)))
|
||||
_save(out_dir / "apl-worker-handshake.json", {"command": worker.command, "handshake": worker.hello})
|
||||
queue_file = stack.enter_context((out_dir / "queue.jsonl").open("x", encoding="utf-8"))
|
||||
event_file = stack.enter_context((out_dir / "events.bin").open("xb"))
|
||||
hash_file = stack.enter_context((out_dir / "tick-state-hashes.jsonl").open("x", encoding="utf-8"))
|
||||
tick_file = stack.enter_context((out_dir / "ticks.jsonl").open("x", encoding="utf-8"))
|
||||
requests_file = stack.enter_context((out_dir / "requests.csv").open("x", encoding="utf-8", newline=""))
|
||||
request_writer = csv.DictWriter(requests_file, fieldnames=["tick", "neuron_idx", "attempted", "accepted", "rejected"])
|
||||
request_writer.writeheader()
|
||||
|
||||
def request_operation():
|
||||
nonlocal permitted
|
||||
tick = tick_now()
|
||||
_check(0 <= tick < steps and not request_seen[tick], "Invalid/duplicate common request tick")
|
||||
request_seen[tick] = 1
|
||||
begin, stop = request_ptr[tick:tick + 2]
|
||||
local = request_local[begin:stop]
|
||||
available = available_raw[local].copy()
|
||||
permitted = local[available]
|
||||
applicable_count[tick] = len(permitted)
|
||||
rejected_count[tick] = len(local) - len(permitted)
|
||||
for node, accepted in zip(requested_globals[begin:stop], available):
|
||||
request_writer.writerow({"tick": tick, "neuron_idx": int(node), "attempted": 1,
|
||||
"accepted": int(accepted), "rejected": int(not accepted)})
|
||||
if len(permitted):
|
||||
neu.v[permitted] = parameters["v_th"] + 1 * b2.mV
|
||||
|
||||
def apl_operation():
|
||||
tick = tick_now()
|
||||
_check(0 <= tick < steps and not apl_seen[tick] and request_seen[tick], "Invalid/duplicate APL tick")
|
||||
active = np.asarray(neu.spikes, dtype=np.int32)
|
||||
_check(not len(active) or np.all(np.diff(active) > 0), "Brian actual spikes are not canonical/unique")
|
||||
_check(np.all(g_raw[active] == 0.0), "APL ran before the original author g reset")
|
||||
if record_history:
|
||||
pre_apl_histories["v"][tick] = v_raw
|
||||
pre_apl_histories["g"][tick] = g_raw
|
||||
pre_apl_histories["ref"][tick] = current_ref(tick)
|
||||
drive, state, inhibition = worker.step(tick, np.asarray(nodes[active], dtype=np.int32))
|
||||
apl_history["drive"][tick] = drive
|
||||
apl_history["state"][tick] = state
|
||||
apl_history["inhibition"][tick] = inhibition
|
||||
# These are separate array operations. Do not move the unit
|
||||
# conversion into a fused subtract; do not guard by refractory.
|
||||
delta_si = inhibition[kc_in_bundle].astype(np.float64) * np.float64(.001)
|
||||
g_raw[kc_local] = g_raw[kc_local] - delta_si
|
||||
apl_seen[tick] = 1
|
||||
|
||||
def record_end():
|
||||
nonlocal last_complete_tick, total_events, final_ref
|
||||
tick = tick_now()
|
||||
_check(tick == last_complete_tick + 1 and tick < steps and apl_seen[tick], "End tick is incomplete/out of order")
|
||||
_check(len(records["rows"]) == tick + 1, "End state lacks queue observation")
|
||||
_check(np.isfinite(v_raw).all() and np.isfinite(g_raw).all(), "Nonfinite author/APL state")
|
||||
final_ref = current_ref(tick)
|
||||
if record_history:
|
||||
histories["v"][tick], histories["g"][tick], histories["ref"][tick] = v_raw, g_raw, final_ref
|
||||
active = np.asarray(neu.spikes, dtype=np.int32)
|
||||
_check(np.isin(permitted, active).all(), "An applicable request did not produce a real spike")
|
||||
spike_count[tick] = len(active)
|
||||
total_counts[active] += 1
|
||||
if len(active):
|
||||
events = np.empty(len(active), EVENT_DTYPE)
|
||||
events["step"], events["neuron_idx"] = tick, nodes[active]
|
||||
event_file.write(memoryview(events).cast("B"))
|
||||
total_events += len(active)
|
||||
spike_ring[tick % queue_len] = active.copy()
|
||||
due_counts[(tick + delay_steps) % queue_len] = int(outdegree[active].sum(dtype=np.int64))
|
||||
hash_file.write(json.dumps({"tick": tick, "v_sha256": _array_hash(v_raw, "<f8"),
|
||||
"g_sha256": _array_hash(g_raw, "<f8"), "ref_sha256": _array_hash(final_ref, "<i4")}, sort_keys=True) + "\n")
|
||||
tick_file.write(json.dumps({"tick": tick, "spikes": len(active), "requests": int(request_count[tick]),
|
||||
"applicable_requests": int(applicable_count[tick]), "ref_rejected_requests": int(rejected_count[tick]),
|
||||
"event_sha256": _array_hash(nodes[active], "<i4"),
|
||||
"due_arrivals": records["rows"][tick]["count"], "accepted_arrivals": records["rows"][tick]["accepted_count"],
|
||||
"APL_drive_sha256": _array_hash(apl_history["drive"][tick], "<f4"),
|
||||
"APL_state_sha256": _array_hash(apl_history["state"][tick], "<f4"),
|
||||
"APL_inhibition_sha256": _array_hash(apl_history["inhibition"][tick], "<f4")}, sort_keys=True) + "\n")
|
||||
_budget(budget_root, budget_limit, reserve=1 << 20)
|
||||
last_complete_tick = tick
|
||||
if (tick + 1) % 100 == 0 or tick + 1 == steps:
|
||||
for mapped in mapped_files:
|
||||
mapped.flush()
|
||||
for handle in (queue_file, event_file, hash_file, tick_file, requests_file):
|
||||
handle.flush()
|
||||
print(json.dumps({"oracle": spec["name"], "completed_ticks": tick + 1,
|
||||
"spikes": total_events, "APL_worker_ticks": worker.tick}), flush=True)
|
||||
|
||||
class QueueInstaller(BrianObject):
|
||||
def __init__(self):
|
||||
super().__init__(clock=neu.clock, when="end", order=1_000_000, name="zz_apl_queue_installer")
|
||||
|
||||
def before_run(self, run_namespace):
|
||||
installed = []
|
||||
for code in syn.pre.code_objects:
|
||||
if "_queue" not in code.namespace or "_queue.peek()" not in str(code.code):
|
||||
continue
|
||||
original = code.namespace["_queue"]
|
||||
if isinstance(original, _QueueProxy):
|
||||
original = original._original_queue
|
||||
code.namespace["_queue"] = _QueueProxy(original, tick_now, available_raw, arrays, identity_edges,
|
||||
due_counts, spike_ring, delay_steps, queue_file, records, af.queue_summary)
|
||||
installed.append(code.name)
|
||||
_check(len(installed) == 1, "Expected one original anatomical queue consumer")
|
||||
records["installations"].append(installed[0])
|
||||
|
||||
objects = [neu, syn, spikes,
|
||||
b2.NetworkOperation(request_operation, clock=neu.clock, when="before_thresholds", order=0, name="oracle_common_requests"),
|
||||
b2.NetworkOperation(apl_operation, clock=neu.clock, when="end", order=0, name="oracle_legacy_apl_unconditional"),
|
||||
b2.NetworkOperation(record_end, clock=neu.clock, when="end", order=1, name="oracle_after_apl_record"), QueueInstaller()]
|
||||
network = b2.Network(*objects)
|
||||
_save(out_dir / "schedule.json", {"schedule": list(network.schedule), "summary": str(network.scheduling_summary())})
|
||||
network.run(0 * b2.ms, namespace={})
|
||||
_save(out_dir / "compiled-code.json", ae._code_records(network))
|
||||
compiled_coefficients = ae._coefficients(neu, parameters, b2, .0001)
|
||||
coefficient_keys = ("gd", "c0", "cg", "cv", "threshold", "reset", "seed_volt")
|
||||
_check(all(float(compiled_coefficients[key]).hex() == float(baseline_coefficients[key]).hex()
|
||||
for key in coefficient_keys), "Compiled Brian coefficients differ from sealed AF")
|
||||
_check(last_complete_tick == -1 and worker.tick == 0 and not records["rows"] and not request_seen.any(),
|
||||
"Zero-duration compilation unexpectedly advanced state")
|
||||
network.run(steps * .1 * b2.ms, namespace={})
|
||||
_save(out_dir / "generated-code.json", ae._code_records(network))
|
||||
worker_status = worker.finish(steps)
|
||||
worker = None
|
||||
|
||||
_check(last_complete_tick == steps - 1 and np.all(request_seen == 1) and np.all(apl_seen == 1), "Incomplete trial")
|
||||
_check(records["peek_calls"] == records["advance_calls"] == steps, "Incomplete queue audit")
|
||||
_check(total_events == int(spikes.num_spikes) and np.array_equal(total_counts, np.asarray(spikes.count[:])),
|
||||
"Sparse events disagree with original SpikeMonitor")
|
||||
_check(np.array_equal(request_count, applicable_count + rejected_count), "Incomplete request accounting")
|
||||
_check(np.all(np.asarray(neu.rfc[:] / b2.ms) == float(parameters["t_rfc"] / b2.ms)), "Author refractory duration changed")
|
||||
for begin in range(0, edges, width):
|
||||
stop = min(edges, begin + width)
|
||||
_check(np.array_equal(np.asarray(syn.w_[begin:stop]).view(np.uint64), effective[begin:stop].view(np.uint64)),
|
||||
"Effective synaptic weights changed during the trial")
|
||||
event_values = np.memmap(out_dir / "events.bin", dtype=EVENT_DTYPE, mode="r", shape=(total_events,)) if total_events else np.empty(0, EVENT_DTYPE)
|
||||
_check((out_dir / "events.bin").stat().st_size == total_events * 8, "Sparse event byte count mismatch")
|
||||
for begin in range(0, total_events, width):
|
||||
stop = min(total_events, begin + width)
|
||||
spike_local = np.asarray(spikes.i[begin:stop], dtype=np.int32)
|
||||
np.testing.assert_array_equal(event_values["neuron_idx"][begin:stop], nodes[spike_local])
|
||||
times = np.rint(np.asarray(spikes.t[begin:stop] / b2.second) / .0001).astype(np.int32)
|
||||
np.testing.assert_array_equal(event_values["step"][begin:stop], times)
|
||||
_budget(budget_root, budget_limit, total_events * 8 + n * 24 + (8 << 20))
|
||||
with (out_dir / "state.npz").open("xb") as handle:
|
||||
np.savez_compressed(handle, step=event_values["step"], neuron_idx=event_values["neuron_idx"],
|
||||
counts=total_counts, v=v_raw, g=g_raw, ref=final_ref)
|
||||
with (out_dir / "tick-summary.npz").open("xb") as handle:
|
||||
np.savez_compressed(handle, request_count=request_count, applicable_count=applicable_count,
|
||||
rejected_count=rejected_count, spike_count=spike_count,
|
||||
arrival_count=np.asarray([row["count"] for row in records["rows"]], np.int64),
|
||||
accepted_arrival_count=np.asarray([row["accepted_count"] for row in records["rows"]], np.int64))
|
||||
coefficients = ae._coefficients(neu, parameters, b2, .0001)
|
||||
keys = ("gd", "c0", "cg", "cv", "threshold", "reset", "seed_volt")
|
||||
_check(all(float(coefficients[key]).hex() == float(baseline_coefficients[key]).hex() for key in keys),
|
||||
"Actual Brian coefficients differ from sealed AF coefficients")
|
||||
_save(out_dir / "coefficients.json", coefficients)
|
||||
_save(out_dir / "queue-audit.json", {"peek_calls": records["peek_calls"], "advance_calls": records["advance_calls"],
|
||||
"installations": records["installations"], "same_peek_object_returned": True,
|
||||
"original_edge_ids_not_sorted_or_filtered": True, "zero_weight_and_refractory_rejected_edges_retained": True,
|
||||
"post_threshold_pre_synaptic_guard_observed": True, "forwarding_contract": "sealed AE/AF design",
|
||||
"new_unwrapped_repeat_performed": False, "limit": "Current wrapper was not independently rerun without the queue proxy"})
|
||||
for path, expected in ((author_path, AUTHOR_SHA256), (ae_path, AE_SHA256), (af_path, AF_SHA256), (worker_path, worker_hash)):
|
||||
_check(_digest(path) == expected, f"Source changed during the run: {path}")
|
||||
_check(_digest(coefficients_path) == coefficient_hash, "Sealed coefficient reference changed during the run")
|
||||
for directory, hashes in ((graph_dir, graph_hashes), (bundle_dir, bundle_hashes), (case_dir, case_hashes)):
|
||||
for name, expected in hashes.items():
|
||||
_check(_digest(directory / name) == expected, f"Read-only input changed during the run: {name}")
|
||||
for mapped in mapped_files:
|
||||
mapped.flush()
|
||||
_check("torch" not in sys.modules, "Pinned Brian process unexpectedly imported Torch")
|
||||
output_bytes = _budget(budget_root, budget_limit, 1 << 20)
|
||||
summary = {"name": spec["name"], "complete": True, "precision": "float64", "APL_precision": "float32",
|
||||
"backend": "shared-APL-component closed-loop Brian2 oracle", "steps": steps, "completed_ticks": last_complete_tick + 1,
|
||||
"nodes": n, "edges": edges, "spikes": total_events, "requests": int(request_count.sum()),
|
||||
"applicable_requests": int(applicable_count.sum()), "ref_rejected_requests": int(rejected_count.sum()),
|
||||
"APL_fast_edges_zeroed": mask_count, "APL_worker_exit_status": worker_status, "APL_worker_ticks": steps,
|
||||
"apl_fast_edges_zeroed": mask_count, "effective_weights_raw_sha256": effective_hasher.hexdigest(),
|
||||
"kc_targets_present": len(kc_local),
|
||||
"candidate_trace_used_as_input": False, "independent_CPU_all_components_claim": False,
|
||||
"history_recorded": record_history, "single_continuous_run": True, "no_training": True, "no_poi": True,
|
||||
"source_and_inputs_unchanged": True, "pass_all": True,
|
||||
"pass_all_scope": "Oracle construction, protocol and internal self-checks only; candidate comparison is a separate gate",
|
||||
"scope": "Only this fixed trajectory; legacy Torch APL is a shared numerical component, not an independent CPU APL implementation",
|
||||
"output_budget_bytes": budget_limit, "output_bytes_before_summary": output_bytes,
|
||||
"state_hashes_sha256": _digest(out_dir / "tick-state-hashes.jsonl"), "queue_sha256": _digest(out_dir / "queue.jsonl"),
|
||||
"events_sha256": _digest(out_dir / "events.bin"), "weights_sha256": _digest(out_dir / "weights.npy")}
|
||||
_save(out_dir / "summary.json", summary)
|
||||
return summary
|
||||
except Exception as error:
|
||||
for mapped in mapped_files:
|
||||
mapped.flush()
|
||||
_save(out_dir / "error.json", {"complete": False, "last_complete_tick": last_complete_tick,
|
||||
"type": type(error).__name__, "message": str(error), "partial_files_preserved": True,
|
||||
"inputs_or_duration_adjusted": False})
|
||||
raise
|
||||
finally:
|
||||
if worker is not None:
|
||||
worker.abort()
|
||||
if saved_preferences is not None:
|
||||
for key, value in saved_preferences.items():
|
||||
b2.prefs[key] = value
|
||||
b2.defaultclock.dt = old_dt
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Preregister exact legacy APL data and unmodified fixed CAL requests."""
|
||||
from datetime import datetime,timezone
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yaml
|
||||
from context import ROOT,MODEL,EXP,AF_MODEL,AF_EXP,AD_EXP,CFG,save,digest,sources,integrity,budget
|
||||
|
||||
def arr(path,value):
|
||||
with Path(path).open('xb') as f:np.save(f,value,allow_pickle=False)
|
||||
|
||||
def graph(folder,nodes,full):
|
||||
folder.mkdir();nodes=np.asarray(nodes,np.int32);ptr,dst,src,raw,base=full
|
||||
if len(nodes)==CFG['neurons']:ids=np.arange(len(dst),dtype=np.int64)
|
||||
else:
|
||||
ids=np.concatenate([np.arange(ptr[s],ptr[s+1],dtype=np.int64) for s in nodes])
|
||||
ids=ids[np.isin(dst[ids],nodes)]
|
||||
lookup=np.full(CFG['neurons'],-1,np.int32);lookup[nodes]=np.arange(len(nodes),dtype=np.int32)
|
||||
ss,dd=lookup[src[ids]],lookup[dst[ids]];pp=np.r_[np.int64(0),np.bincount(ss,minlength=len(nodes)).cumsum(dtype=np.int64)]
|
||||
for name,value in dict(nodes=nodes,edge_id=ids,src=ss,dst=dd,ptr=pp,signed_raw=raw[ids],weights=base[ids]).items():arr(folder/(name+'.npy'),value)
|
||||
pd.DataFrame({'local_index':np.arange(len(nodes),dtype=np.int32)},index=pd.Index(nodes,name='global_neuron_idx')).to_csv(folder/'completeness.csv')
|
||||
pd.DataFrame({'Presynaptic_Index':ss,'Postsynaptic_Index':dd,'Excitatory x Connectivity':raw[ids],'edge_id':ids},copy=False).to_parquet(folder/'connectivity.parquet',index=False)
|
||||
save(folder/'audit.json',dict(nodes=len(nodes),edges=len(ids),canonical_original_edge_ids=True,anatomical_weights_not_modified=True,
|
||||
effective_APL_zeroing='Only in new runtime clone, never these original weight files'))
|
||||
return nodes
|
||||
|
||||
def case(name,kind,steps,nodes,graphdir,requests,initial=None,extra=None):
|
||||
folder=EXP/'cases'/name;folder.mkdir()
|
||||
model=json.loads((AD_EXP/'protocol/preregistered.json').read_text())['config']['model']
|
||||
spec=dict(name=name,kind=kind,steps=steps,dt_ms=.1,duration_ms=steps*.1,model=model,
|
||||
graph_dir=str(graphdir),apl_bundle_dir=str(EXP/'apl-bundle'),apl=CFG['apl'],record_history=kind=='fixture',
|
||||
coefficients_path=str(AF_EXP/'oracle/no_input/coefficients.json'),
|
||||
science_python=str(ROOT/'models/male-cns-lif/.venv/bin/python'),legacy_apl_worker=str(MODEL/'legacy_apl_worker.py'),
|
||||
budget_root=str(EXP),output_budget_bytes=CFG['output_budget_bytes'],precision='float64',apl_precision='float32',training=False)
|
||||
if extra:spec.update(extra)
|
||||
requests=np.asarray(requests,dtype=np.int32).reshape(-1,2)
|
||||
if len(requests):
|
||||
requests=requests[np.lexsort((requests[:,1],requests[:,0]))]
|
||||
assert len(np.unique(requests.astype(np.int64)[:,0]*CFG['neurons']+requests[:,1]))==len(requests)
|
||||
assert requests[:,0].min()>=0 and requests[:,0].max()<steps and np.isin(requests[:,1],nodes).all()
|
||||
with (folder/'requests.npz').open('xb') as f:np.savez_compressed(f,tick=requests[:,0],neuron_idx=requests[:,1])
|
||||
if kind=='fixture':
|
||||
v=np.full(len(nodes),model['resting_potential_mv'],np.float64);g=np.zeros(len(nodes),np.float64)
|
||||
if initial:
|
||||
for node,value in initial.get('v',{}).items():v[np.flatnonzero(nodes==node)[0]]=value
|
||||
for node,value in initial.get('g',{}).items():g[np.flatnonzero(nodes==node)[0]]=value
|
||||
with (folder/'arrays.npz').open('xb') as f:np.savez_compressed(f,initial_v_mv=v,initial_g_mv=g)
|
||||
save(folder/'spec.json',spec)
|
||||
|
||||
def main():
|
||||
assert not (EXP/'protocol/preregistered.json').exists()
|
||||
assert all(not (EXP/d).exists() or not any((EXP/d).iterdir()) for d in ('protocol','graphs','apl-bundle','cases','gpu','oracle'))
|
||||
assert shutil.disk_usage(EXP.parent).free>30*1024**3
|
||||
for d in ('protocol','apl-bundle','graphs','cases','gpu','oracle','component-audit','regression','calibration','audit','extension-build','logs'):(EXP/d).mkdir(parents=True,exist_ok=True)
|
||||
can=ROOT/'datasets/male-cns-v1.0/processed/neuron-only';legacy=ROOT/'models/male-cns-lif';lp=legacy/'olfactory-grounding-v1/phase4l.yaml'
|
||||
lock=ROOT/'experiments/phase4j-mb-physiology-repair/calibration/locked-parameters.json'
|
||||
files=[AF_MODEL/'manifest.json',AF_EXP/'manifest.json',AF_EXP/'oracle/no_input/coefficients.json',
|
||||
legacy/'deterministic-runtime-v2/phase4p_revalidate.py',lp,lock,can/'neuron-index/unified-metadata.parquet',can/'gpu/csr-weights.int64.npy',
|
||||
legacy/'author-runtime-lab-v1/author_ext.cpp',legacy/'author-runtime-lab-v1/author_ext.cu',legacy/'author-runtime-lab-v1/gpu_backend.py',
|
||||
legacy/'author-runtime-lab-v1/oracle_stress.py',AD_EXP/'external-resources/model.py',AF_MODEL/'oracle_full.py']
|
||||
lroot=ROOT/'experiments/phase4l-olfactory-grounding'
|
||||
files += [lroot/'odor-encoder'/x for x in ('frozen-odor-panel.parquet','mapped-glomerular-vectors.npz','selection.json')]
|
||||
files += [lroot/'receptor-mapping/olfactory-mapping.parquet']
|
||||
prior=ROOT/'experiments/phase4ag-author-apl'
|
||||
files += [prior/'manifest.json',ROOT/'models/male-cns-lif/author-apl-lab-v1/manifest.json']
|
||||
save(EXP/'protocol/preregistered.json',dict(timestamp_utc=datetime.now(timezone.utc).isoformat(),sources=sources(),config=CFG,
|
||||
inputs=[dict(path=str(p.relative_to(ROOT)),sha256=digest(p)) for p in files],before_any_trajectory=True,
|
||||
parent_commit=subprocess.check_output(['git','-C',str(ROOT),'rev-parse','HEAD'],text=True).strip()))
|
||||
save(EXP/'regression/old-integrity-before.json',integrity())
|
||||
science_entry=ROOT/'models/male-cns-lif/.venv/bin/python'
|
||||
command=[str(science_entry),'-B','-c',
|
||||
'import sys,json,importlib.util; print(json.dumps(dict(executable=sys.executable,prefix=sys.prefix,base_prefix=sys.base_prefix,numpy_visible=importlib.util.find_spec("numpy") is not None,torch_visible=importlib.util.find_spec("torch") is not None)))']
|
||||
environment=json.loads(subprocess.check_output(command,text=True))
|
||||
assert environment['executable']==str(science_entry) and environment['prefix']==str(science_entry.parent.parent)
|
||||
assert environment['prefix']!=environment['base_prefix'] and environment['numpy_visible'] and environment['torch_visible']
|
||||
save(EXP/'protocol/execution-preflight.json',dict(command=command,observed=environment,
|
||||
before_any_trajectory=True,torch_imported=False,CUDA_initialized=False))
|
||||
locked=json.loads(lock.read_text());assert locked['parameters_locked'] and locked['calibration_only']
|
||||
assert {k:locked['selected'][k] for k in ('gain','normalization','tau_ms')}==CFG['apl']
|
||||
meta=pd.read_parquet(can/'neuron-index/unified-metadata.parquet').sort_values('neuron_idx').reset_index(drop=True)
|
||||
assert len(meta)==CFG['neurons'] and np.array_equal(meta.neuron_idx,np.arange(CFG['neurons']))
|
||||
cls=meta.annotation_class.fillna('').astype(str).to_numpy();typ=meta.annotation_type.fillna('').astype(str).to_numpy()
|
||||
kc=np.flatnonzero(cls=='Kenyon_Cell').astype(np.int32);pn=np.flatnonzero(cls=='ALPN').astype(np.int32);apl=np.flatnonzero(typ=='APL').astype(np.int32)
|
||||
assert len(kc)==4064 and len(apl)==2
|
||||
fg=AF_EXP/'graph';ptr=np.load(fg/'ptr.npy',mmap_mode='r');dst=np.load(fg/'dst.npy',mmap_mode='r');src=np.load(fg/'src.npy',mmap_mode='r')
|
||||
signed=np.load(fg/'signed_raw.npy',mmap_mode='r');base=np.load(AF_EXP/'oracle/no_input/weights.npy',mmap_mode='r');raw=np.load(can/'gpu/csr-weights.int64.npy',mmap_mode='r')
|
||||
assert len(dst)==CFG['edges'] and int(raw.sum())==CFG['raw_weight_sum']
|
||||
aplout=np.isin(src,apl);aplids=np.flatnonzero(aplout).astype(np.int64);assert len(aplids)==5845
|
||||
I=np.zeros((2,len(meta)),np.float32);O=np.zeros((2,len(kc)),np.float32)
|
||||
alook={int(v):i for i,v in enumerate(apl)};kl=np.full(len(meta),-1,np.int32);kl[kc]=np.arange(len(kc),dtype=np.int32)
|
||||
allowed=np.isin(src,np.r_[kc,pn])&np.isin(dst,apl)
|
||||
for eid in np.flatnonzero(allowed):I[alook[int(dst[eid])],int(src[eid])]+=float(raw[eid])
|
||||
for eid in np.flatnonzero(aplout&np.isin(dst,kc)):O[alook[int(src[eid])],kl[dst[eid]]]+=float(raw[eid])
|
||||
iscale=float(I[I>0].sum()/np.count_nonzero(np.any(I>0,axis=0)));oscale=float(O[O>0].mean());bundle=EXP/'apl-bundle'
|
||||
for name,value in dict(input_matrix=I/iscale,output_matrix=O/oscale,input_raw=I,output_raw=O,kc_globals=kc,pn_globals=pn,apl_globals=apl,apl_out_ids=aplids).items():arr(bundle/(name+'.npy'),value)
|
||||
save(bundle/'scales.json',dict(input_scale=iscale,output_scale=oscale))
|
||||
save(bundle/'provenance.json',dict(canonical_root=str(can),source=str(legacy/'deterministic-runtime-v2/phase4p_revalidate.py'),source_sha256=digest(legacy/'deterministic-runtime-v2/phase4p_revalidate.py'),
|
||||
parameters=CFG['apl'],lock_file=str(lock),lock_sha256=digest(lock),full_dimensions=[list(I.shape),list(O.shape)],
|
||||
APL=len(apl),KC=len(kc),ALPN=len(pn),APL_fast_edges=len(aplids),APL_nonKC_edges=int(np.count_nonzero(aplout&~np.isin(dst,kc))),
|
||||
APL_nonKC_contacts=int(raw[aplout&~np.isin(dst,kc)].sum()),normalization_recomputed_in_original_order=True,
|
||||
raw_files_modified=False))
|
||||
full=(ptr,dst,src,signed,base);fullnodes=graph(EXP/'graphs/full',np.arange(len(meta),dtype=np.int32),full)
|
||||
pscore=I.sum(axis=0);pnrank=pn[np.lexsort((pn,-pscore[pn]))];kscore=O.sum(axis=0);kcrank=kc[np.lexsort((kc,-kscore))]
|
||||
candidates=np.flatnonzero(np.isin(src,pn)&np.isin(dst,kc));best=int(candidates[np.argmax(raw[candidates])]);pairpn=int(src[best])
|
||||
pe=np.arange(ptr[pairpn],ptr[pairpn+1],dtype=np.int64);pe=pe[np.isin(dst[pe],kc)];pe=pe[np.lexsort((dst[pe],-raw[pe]))]
|
||||
collision_kc=np.unique(dst[pe[:3]]);assert len(collision_kc)==3
|
||||
nonkc=aplids[~np.isin(dst[aplids],kc)];nonkc_target=int(dst[nonkc[np.argmax(raw[nonkc])]])
|
||||
nodes=np.unique(np.r_[apl,pnrank[:16],kcrank[:16],pairpn,collision_kc,nonkc_target,[1197,1381,1511,82312,133666]]).astype(np.int32)
|
||||
fixturegraph=EXP/'graphs/fixtures';graph(fixturegraph,nodes,full)
|
||||
p=int(pnrank[0]);k=int(kcrank[0]);kc1,kc2,kc3=map(int,collision_kc)
|
||||
designs=[('zero',100,[],None),('pn_pulse',300,[(0,p)],None),
|
||||
('kc_ref_boundary',100,[(t,k) for t in (0,1,21,22,23,44)],None),
|
||||
('two_sources',300,[(t,n) for t in (0,22,44,66,88) for n in (p,k)],None),
|
||||
('arrival_collision',120,[(0,pairpn),(0,kc1),(18,kc2),(21,kc1)],None),
|
||||
('natural_request',100,[(0,k)],dict(v={k:-44.0})),
|
||||
('apl_only',100,[(0,int(a)) for a in apl],None),
|
||||
('high_fanin',400,[(t,int(n)) for t in (0,22,44) for n in nodes if pscore[n]>0],dict(g={k:.123}))]
|
||||
assert [v[0] for v in designs]==CFG['fixtures']
|
||||
for name,steps,requests,initial in designs:case(name,'fixture',steps,nodes,fixturegraph,requests,initial)
|
||||
meta.iloc[nodes][['neuron_idx','body_id','annotation_class','annotation_type','neurotransmitter']].to_csv(EXP/'protocol/fixture-identities.csv',index=False)
|
||||
save(EXP/'protocol/fixture-selection.json',dict(pn=p,kc=k,pair_PN=pairpn,arrival_KCs=collision_kc.tolist(),real_PN_KC_edge=best,
|
||||
nonKC_APL_target=nonkc_target,method='Static raw-count ranks and exact metadata; retain ALL existing induced edges and full APL matrices/norms',runtime_selection=False))
|
||||
os.environ['CUDA_VISIBLE_DEVICES']=CFG['gpu_uuid'];sys.path.insert(0,str(legacy));sys.path.insert(0,str(legacy/'deterministic-runtime-v2'))
|
||||
sys.path.insert(0,str(legacy/'terminal-scope-diagnostic-v1'));from diagnostic import queues
|
||||
queues()
|
||||
import torch
|
||||
from phase4p_revalidate import door_inputs,make_drive
|
||||
oc,panel,vectors,selection,mapping=door_inputs();cal=panel[panel.split=='calibration'].copy();assert len(panel)==12 and len(cal)==6
|
||||
assert oc['odor_encoder']['evaluation_seeds']==CFG['calibration']['seeds']
|
||||
assert float(oc['odor_encoder']['duration_ms'])==CFG['calibration']['duration_ms']
|
||||
cal.to_parquet(EXP/'protocol/calibration-panel.parquet',index=False);calrecords=[]
|
||||
for mode in CFG['calibration']['modes']:
|
||||
for oi,(_,odor) in enumerate(cal.iterrows()):
|
||||
targets,rates=make_drive(oc,panel,vectors,selection,mapping,odor.odor_id,mode)
|
||||
assert len(np.unique(targets))==len(targets),'Original encoder emits duplicate PN targets; audit instead of silently merging'
|
||||
rates=np.asarray(rates,np.float32);steps=round(CFG['calibration']['duration_ms']/.1)
|
||||
for seed in CFG['calibration']['seeds']:
|
||||
gen=torch.Generator(device='cuda');gen.manual_seed(seed)
|
||||
original_probability=rates*.1/1000.0
|
||||
explicit_probability=rates*np.float32(.1)/np.float32(1000.)
|
||||
assert original_probability.dtype==np.float32 and np.array_equal(original_probability.view(np.uint32),explicit_probability.view(np.uint32))
|
||||
probs=torch.as_tensor(original_probability,dtype=torch.float32,device='cuda')
|
||||
forced=(torch.rand((steps,len(targets)),generator=gen,device='cuda')<probs[None,:]).cpu().numpy()
|
||||
tt,cc=np.nonzero(forced);req=np.column_stack([tt,targets[cc]]).astype(np.int32)
|
||||
name=f'cal-{mode}-{oi:02d}-s{seed}'
|
||||
case(name,'calibration',steps,fullnodes,EXP/'graphs/full',req,extra=dict(odor_id=str(odor.odor_id),odor_name=str(odor.odor_name),
|
||||
mode=mode,seed=seed,odor_index=oi,expected_external_events=float(rates.sum())*steps*.1/1000.,target_count=len(targets),rate_sum_hz=float(rates.sum())))
|
||||
with (EXP/'cases'/name/'pn-drive.npz').open('xb') as f:np.savez_compressed(f,targets=targets,rates_hz=rates)
|
||||
calrecords.append(dict(name=name,mode=mode,odor_index=oi,odor_id=str(odor.odor_id),seed=seed,requests=len(req),rates_sum=float(rates.sum())))
|
||||
save(EXP/'protocol/calibration-cases.json',calrecords)
|
||||
save(EXP/'protocol/encoder-audit.json',dict(panel_count=12,cal_count=6,median_mapped_response_sum=float(np.median(panel.mapped_response_sum)),
|
||||
target_rng_column_order='Original pn_drive, no sorting before torch.rand',seeds=CFG['calibration']['seeds'],
|
||||
requested_before_any_dynamics=True,formal_AB_run=False,heldout_run=False,torch=torch.__version__,numpy=np.__version__,
|
||||
generation='Original rates*.1/1000 float32 GPU Bernoulli, independent per-case original seed'))
|
||||
revision=[]
|
||||
for folder in sorted((EXP/'cases').iterdir()):
|
||||
with np.load(folder/'requests.npz',allow_pickle=False) as new,np.load(prior/'cases'/folder.name/'requests.npz',allow_pickle=False) as old:
|
||||
assert new.files==old.files and all(new[key].dtype==old[key].dtype and np.array_equal(new[key],old[key]) for key in old.files)
|
||||
revision.append(dict(case=folder.name,sparse_request_arrays_exact=True))
|
||||
save(EXP/'protocol/revision-input-equivalence.json',dict(prior_experiment=str(prior),cases=revision,
|
||||
input_arrays_unchanged=True,reason='Execution-only venv invocation fix; no trajectory outcome used for input choice'))
|
||||
save(EXP/'protocol/prepared-hashes.json',[dict(path=str(p.relative_to(EXP)),bytes=p.stat().st_size,sha256=digest(p))
|
||||
for d in ('graphs','apl-bundle','cases','protocol') for p in sorted((EXP/d).rglob('*')) if p.is_file() and p.name!='prepared-hashes.json'])
|
||||
budget();print('PREPARED8 fixtures and60 fixed CAL inputs; no trajectories; frozen integrity PASS',flush=True)
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,48 @@
|
||||
"""A fresh process per declared case; no concurrent scientific GPU jobs."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
from context import ROOT,MODEL,EXP,CFG,check,budget
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument('backend',choices=['gpu','oracle','component'])
|
||||
parser.add_argument('case')
|
||||
parser.add_argument('--repeat',type=int,default=0)
|
||||
args=parser.parse_args()
|
||||
assert '/' not in args.case and '..' not in args.case and 0<=args.repeat<3
|
||||
check();budget()
|
||||
os.environ['CUDA_VISIBLE_DEVICES']=CFG['gpu_uuid']
|
||||
os.environ['CUDA_HOME']='/usr/local/cuda'
|
||||
os.environ['PATH']=str(ROOT/'models/male-cns-lif/.venv/bin')+':/usr/local/cuda/bin:'+os.environ['PATH']
|
||||
os.environ['MAX_JOBS']='2';os.environ['TORCH_CUDA_ARCH_LIST']='8.9'
|
||||
# The pinned Brian environment intentionally has neither Torch nor PyYAML.
|
||||
# Keep this preflight standard-library-only; never install into that env.
|
||||
for port in (8188,8189,8190):
|
||||
with urllib.request.urlopen(f'http://192.168.10.51:{port}/queue',timeout=5) as response:
|
||||
queue=json.load(response)
|
||||
assert not queue['queue_running'] and not queue['queue_pending'],'ComfyUI busy: defer, never evict'
|
||||
case=EXP/'cases'/args.case
|
||||
assert case.is_dir()
|
||||
out=EXP/'gpu'/f'{args.case}-r{args.repeat}'
|
||||
if args.backend=='gpu':
|
||||
from gpu_apl import run_gpu
|
||||
result=run_gpu(case,out,EXP/'extension-build')
|
||||
elif args.backend=='oracle':
|
||||
assert args.repeat==0
|
||||
from oracle_apl import run_case
|
||||
result=run_case(case,EXP/'oracle'/args.case)
|
||||
else:
|
||||
assert args.repeat==0
|
||||
from validate_components import validate
|
||||
result=validate(out,EXP/'apl-bundle',EXP/'component-audit'/(args.case+'.json'))
|
||||
budget()
|
||||
print(json.dumps(dict(backend=args.backend,case=args.case,repeat=args.repeat,
|
||||
complete=result.get('complete'),pass_all=result.get('pass_all')),allow_nan=False),flush=True)
|
||||
if not result.get('pass_all',False):raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Ordered numerical gates before any full CAL; never tune from outcomes."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
import traceback
|
||||
from context import ROOT,MODEL,EXP,CFG,check,save,budget
|
||||
|
||||
SCIENCE=ROOT/'models/male-cns-lif/.venv/bin/python'
|
||||
ORACLE=ROOT/'experiments/phase4ad-author-semantics/environment/.venv/bin/python'
|
||||
|
||||
|
||||
def child(backend,name,repeat=0):
|
||||
log=EXP/'logs'/f'{backend}-{name}-r{repeat}.log'
|
||||
cmd=[str(ORACLE if backend=='oracle' else SCIENCE),'-B',str(MODEL/'run_case.py'),backend,name,'--repeat',str(repeat)]
|
||||
print('START',backend,name,repeat,flush=True);t0=time.perf_counter()
|
||||
with log.open('x',encoding='utf-8') as f:
|
||||
result=subprocess.run(cmd,stdout=f,stderr=subprocess.STDOUT,timeout=1800,check=False)
|
||||
print('FINISH',backend,name,repeat,'exit',result.returncode,'wall',round(time.perf_counter()-t0,2),flush=True)
|
||||
if result.returncode:raise RuntimeError(f'{backend} {name} failed; inspect {log}')
|
||||
|
||||
|
||||
def gate_case(name,repeats):
|
||||
from compare import compare,compare_gpu_repeats
|
||||
child('gpu',name)
|
||||
for r in range(1,repeats):child('gpu',name,r)
|
||||
child('oracle',name)
|
||||
result=compare(EXP/'cases'/name,EXP/'gpu'/(name+'-r0'),EXP/'oracle'/name,EXP/'audit'/('compare-'+name+'.json'))
|
||||
assert result['pass_all'],f'Closed-loop exact comparison failed: {name}; no later CAL permitted'
|
||||
for r in range(1,repeats):
|
||||
report=compare_gpu_repeats(EXP/'gpu'/(name+'-r0'),EXP/'gpu'/f'{name}-r{r}',EXP/'audit'/f'repeat-{name}-r{r}.json')
|
||||
assert report['pass_all'],f'Exact GPU repeat failed: {name}'
|
||||
child('component',name)
|
||||
return dict(case=name,pass_all=True,gpu_repeats=repeats)
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser();parser.add_argument('stage',choices=['fixtures','probes','calibration']);args=parser.parse_args()
|
||||
check();budget();receipt=EXP/'audit'/(args.stage+'-gate.json');assert not receipt.exists()
|
||||
try:
|
||||
if args.stage=='fixtures':
|
||||
rows=[gate_case(n,CFG['fixture_repeats']) for n in CFG['fixtures']]
|
||||
save(receipt,dict(pass_all=True,cases=rows,scope='Fixed real-edge induced fixtures only'))
|
||||
elif args.stage=='probes':
|
||||
assert json.loads((EXP/'audit/fixtures-gate.json').read_text())['pass_all']
|
||||
rows=[gate_case(n,1) for n in CFG['calibration']['full_oracle_probes']]
|
||||
save(receipt,dict(pass_all=True,cases=rows,scope='Only two original first-CAL fixed fullgraph trajectories'))
|
||||
else:
|
||||
assert json.loads((EXP/'audit/fixtures-gate.json').read_text())['pass_all']
|
||||
assert json.loads((EXP/'audit/probes-gate.json').read_text())['pass_all']
|
||||
records=json.loads((EXP/'protocol/calibration-cases.json').read_text());assert len(records)==60
|
||||
probes=CFG['calibration']['full_oracle_probes']
|
||||
for rec in records:
|
||||
if rec['name'] not in probes:child('gpu',rec['name'])
|
||||
if rec['seed']==CFG['calibration']['seeds'][0]:child('gpu',rec['name'],1)
|
||||
from calibration import summarize
|
||||
result=summarize()
|
||||
save(receipt,dict(pass_all=result['pass_all'],scope='Complete original six-CAL representation gate, not heldout or learning',summary='calibration/summary.json'))
|
||||
if not result['pass_all']:raise SystemExit(2)
|
||||
except Exception as e:
|
||||
save(EXP/'audit'/('stopped-'+args.stage+'.json'),dict(stage=args.stage,type=type(e).__name__,message=str(e),traceback=traceback.format_exc(),
|
||||
later_stages_not_permitted=True,partial_outputs_preserved=True,parameters_changed=False))
|
||||
raise
|
||||
budget()
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Local, synthetic tests only: no CUDA, scientific trajectories, or heldout data."""
|
||||
import copy
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from calibration import (ORIGINAL_MODES, ORIGINAL_SEEDS, ORIGINAL_CAL_IDS,
|
||||
APL_FILES, STATE_DTYPES, arrays_bitwise_equal, evaluate_gates, exact_repeat, pair_metrics,
|
||||
sample_cv, summarize_mode, validate_inventory, validate_state)
|
||||
|
||||
|
||||
GATES = {"sparse_min_fraction": .03, "sparse_max_fraction": .09,
|
||||
"minimum_separable_odor_pairs": 3, "max_fraction_strict": .5, "min_fraction_strict": 0.,
|
||||
"same_jaccard_gt_between": True, "between_euclidean_gt_within": True,
|
||||
"both_modes_required": True, "finite_required": True, "repeat_exact_required": True}
|
||||
|
||||
|
||||
def synthetic_runs(silent=False):
|
||||
result = {}
|
||||
for mode in ORIGINAL_MODES:
|
||||
for odor in range(6):
|
||||
for seed in ORIGINAL_SEEDS:
|
||||
counts = np.zeros(1000, dtype=np.int32)
|
||||
if not silent:
|
||||
counts[odor * 50:(odor + 1) * 50] = 1
|
||||
result[(mode, str(odor), seed)] = {"kc_counts": counts,
|
||||
"expected_external_events": 10., "external_events": 0 if silent else 10}
|
||||
return result
|
||||
|
||||
|
||||
class CalibrationTests(unittest.TestCase):
|
||||
def test_complete_gate_and_original_pair_counts(self):
|
||||
summary = summarize_mode(ORIGINAL_MODES[0], list(map(str, range(6))), ORIGINAL_SEEDS, synthetic_runs(), GATES)
|
||||
self.assertTrue(summary["gate"])
|
||||
self.assertEqual(summary["within_distance_count"], 60)
|
||||
self.assertEqual(summary["between_distance_count"], 75)
|
||||
self.assertEqual(summary["pair_count"], 15)
|
||||
self.assertEqual(summary["pairwise_separation_pass_count"], 15)
|
||||
self.assertTrue(all(row["within_distance_count"] == 20 and row["between_distance_count"] == 5 for row in summary["pairs"]))
|
||||
|
||||
def test_silent_is_not_pass(self):
|
||||
summary = summarize_mode(ORIGINAL_MODES[0], list(map(str, range(6))), ORIGINAL_SEEDS, synthetic_runs(True), GATES)
|
||||
self.assertFalse(summary["gate"])
|
||||
self.assertFalse(summary["conditions"]["minimum_strictly_positive"])
|
||||
self.assertFalse(summary["conditions"]["same_jaccard_gt_different"])
|
||||
self.assertFalse(summary["conditions"]["different_euclidean_gt_same"])
|
||||
self.assertEqual(summary["pairwise_separation_pass_count"], 0)
|
||||
self.assertIsNone(summary["actual_event_count_cv"])
|
||||
|
||||
def test_every_original_conjunct_is_necessary(self):
|
||||
baseline = summarize_mode(ORIGINAL_MODES[0], list(map(str, range(6))), ORIGINAL_SEEDS, synthetic_runs(), GATES)
|
||||
changes = (("KC_active_fraction_mean", .029), ("KC_active_fraction_mean", .091),
|
||||
("KC_active_fraction_max", .5), ("KC_active_fraction_min", 0.),
|
||||
("same_odor_jaccard_mean", baseline["different_odor_jaccard_mean"]),
|
||||
("different_odor_euclidean_mean", baseline["same_odor_euclidean_mean"]),
|
||||
("pairwise_separation_pass_count", 2))
|
||||
for key, value in changes:
|
||||
with self.subTest(key=key, value=value):
|
||||
modified = dict(baseline, **{key: value})
|
||||
self.assertFalse(all(evaluate_gates(modified, GATES).values()))
|
||||
for value in (.03, .09):
|
||||
self.assertTrue(evaluate_gates(dict(baseline, KC_active_fraction_mean=value), GATES)["mean_sparsity_in_closed_interval"])
|
||||
|
||||
def test_pair_specific_not_global_baseline(self):
|
||||
runs = synthetic_runs()
|
||||
mode = ORIGINAL_MODES[0]
|
||||
for odor in range(6):
|
||||
for index, seed in enumerate(ORIGINAL_SEEDS):
|
||||
counts = np.array([index + odor * 100] if odor < 2 else [10000 * odor + index * 1000], dtype=np.int32)
|
||||
runs[(mode, str(odor), seed)]["kc_counts"] = counts
|
||||
summary = summarize_mode(mode, list(map(str, range(6))), ORIGINAL_SEEDS, runs, GATES)
|
||||
first_pair = summary["pairs"][0]
|
||||
self.assertEqual(first_pair["within_this_pair_mean"], 2.)
|
||||
self.assertEqual(first_pair["between_same_seed_mean"], 100.)
|
||||
self.assertTrue(first_pair["gate"])
|
||||
self.assertGreater(summary["same_odor_euclidean_mean"], 100.)
|
||||
|
||||
def test_original_metric_zero_and_count_semantics(self):
|
||||
zero = np.zeros(3, dtype=np.int32)
|
||||
self.assertEqual(pair_metrics(zero, zero)["active_jaccard"], 1.)
|
||||
self.assertEqual(pair_metrics(zero, zero)["correlation"], 1.)
|
||||
result = pair_metrics(np.array([2, 0, 0]), np.array([1, 0, 0]))
|
||||
self.assertEqual(result["euclidean_distance"], 1.)
|
||||
self.assertEqual(result["active_jaccard"], 1.)
|
||||
self.assertAlmostEqual(sample_cv([1., 2., 3.]), .5)
|
||||
|
||||
def test_repeat_comparison_is_bits_and_dtype(self):
|
||||
self.assertFalse(arrays_bitwise_equal(np.array([0.], np.float64), np.array([-0.], np.float64)))
|
||||
self.assertFalse(arrays_bitwise_equal(np.array([1], np.int32), np.array([1], np.int64)))
|
||||
self.assertTrue(arrays_bitwise_equal(np.array([1.], np.float32), np.array([1.], np.float32)))
|
||||
|
||||
def test_exact_repeat_covers_all_recorded_semantics(self):
|
||||
first = {"state": {name: np.zeros(1, dtype) for name, dtype in STATE_DTYPES.items()},
|
||||
"apl": {name: np.zeros((1, width), np.float32) for name, width in APL_FILES.items()},
|
||||
"hashes": [{"tick": 0}], "queues": [{"tick": 0}],
|
||||
"requests": [], "component": {"environment": {"tf32": False}}}
|
||||
self.assertTrue(exact_repeat(first, copy.deepcopy(first))["pass_all"])
|
||||
for category in ("state", "apl"):
|
||||
for name in first[category]:
|
||||
bad = copy.deepcopy(first)
|
||||
bad[category][name].flat[0] = 1
|
||||
with self.subTest(category=category, name=name):
|
||||
self.assertFalse(exact_repeat(first, bad)["pass_all"])
|
||||
for key in ("hashes", "queues", "requests", "component"):
|
||||
bad = copy.deepcopy(first)
|
||||
bad[key] = {} if isinstance(first[key], list) else []
|
||||
with self.subTest(key=key):
|
||||
self.assertFalse(exact_repeat(first, bad)["pass_all"])
|
||||
|
||||
def test_raw_state_rejects_nonfinite_duplicate_and_wrong_counts(self):
|
||||
state = {"step": np.array([0], np.int32), "neuron_idx": np.array([1], np.int32),
|
||||
"counts": np.array([0, 1], np.int32), "v": np.zeros(2, np.float64),
|
||||
"g": np.zeros(2, np.float64), "ref": np.zeros(2, np.int32)}
|
||||
validate_state(state, 2, 10)
|
||||
for mutation in ("nan", "counts", "duplicate", "wrong_dtype"):
|
||||
bad = copy.deepcopy(state)
|
||||
if mutation == "nan": bad["g"][0] = np.nan
|
||||
if mutation == "counts": bad["counts"][0] = 1
|
||||
if mutation == "duplicate":
|
||||
bad["step"] = np.array([0, 0], np.int32)
|
||||
bad["neuron_idx"] = np.array([1, 1], np.int32)
|
||||
bad["counts"][1] = 2
|
||||
if mutation == "wrong_dtype": bad["v"] = bad["v"].astype(np.float32)
|
||||
with self.subTest(mutation=mutation), self.assertRaises(ValueError):
|
||||
validate_state(bad, 2, 10)
|
||||
|
||||
def test_inventory_rejects_missing_duplicate_or_heldout_identity(self):
|
||||
odors = sorted(ORIGINAL_CAL_IDS)
|
||||
records = [{"name": f"cal-{mode}-{index:02d}-s{seed}", "mode": mode, "odor_index": index,
|
||||
"odor_id": odors[index], "seed": seed} for mode in ORIGINAL_MODES
|
||||
for index in range(6) for seed in ORIGINAL_SEEDS]
|
||||
cfg = {"calibration": {"modes": list(ORIGINAL_MODES), "seeds": list(ORIGINAL_SEEDS),
|
||||
"duration_ms": 100., "odor_count": 6, "split": "calibration", "repeat_first_seed_each_odor": True}, "gates": GATES}
|
||||
self.assertEqual(validate_inventory(records, cfg), odors)
|
||||
for malformed in (records[:-1], records[:-1] + [records[0]],
|
||||
[dict(row, odor_id="heldout") if row["odor_index"] == 0 else row for row in records]):
|
||||
with self.assertRaises(ValueError):
|
||||
validate_inventory(malformed, cfg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,266 @@
|
||||
"""Local/remote pure protocol tests; no Brian, Torch, GPU or network execution.
|
||||
|
||||
Run with the already-provisioned Python and ``-B test_protocol.py``. The
|
||||
sidecar process is mocked with in-memory stdin and an anonymous stdout pipe.
|
||||
These tests validate transport and declared arithmetic/input contracts, not
|
||||
the scientific trajectory or independent correctness of the Torch component.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
import types
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def load_no_cache(path, name):
|
||||
module = types.ModuleType(name)
|
||||
module.__file__ = str(path)
|
||||
exec(compile(path.read_bytes(), str(path), "exec"), module.__dict__)
|
||||
return module
|
||||
|
||||
|
||||
ORACLE = load_no_cache(HERE / "oracle_apl.py", "phase4ag_protocol_oracle")
|
||||
|
||||
|
||||
def handshake(**updates):
|
||||
result = {"ok": True, "protocol": "phase4ag-apl-v1", "n_neurons": 166700,
|
||||
"n_apl": 2, "n_kc": 4064, "dt_ms": .1,
|
||||
"files_sha256": {name: "frozen-test-hash" for name in ORACLE.BUNDLE_FILES},
|
||||
"decay_f32_bits": f"{int(np.float32(math.exp(-.1 / 20.)).view(np.uint32)):08x}",
|
||||
"gain_f32_bits": f"{int(np.float32(.01).view(np.uint32)):08x}"}
|
||||
result.update(updates)
|
||||
return (json.dumps(result) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def frame(tick=0, values=None, byte_count=None):
|
||||
values = np.arange(4068, dtype="<f4") if values is None else np.asarray(values, dtype="<f4")
|
||||
count = values.nbytes if byte_count is None else byte_count
|
||||
return struct.pack("<qQ", tick, count) + values.tobytes()
|
||||
|
||||
|
||||
class MemoryInput(io.BytesIO):
|
||||
def close(self):
|
||||
if not self.closed:
|
||||
self.closed_data = self.getvalue()
|
||||
super().close()
|
||||
|
||||
|
||||
class FakeProcess:
|
||||
"""The only use of OS descriptors is an anonymous read-only response pipe."""
|
||||
def __init__(self, response, status=0, chunk_size=97):
|
||||
read_fd, write_fd = os.pipe()
|
||||
self.stdin = MemoryInput()
|
||||
self.stdout = os.fdopen(read_fd, "rb", buffering=0)
|
||||
self.status = status
|
||||
self.terminated = False
|
||||
|
||||
def write_response():
|
||||
try:
|
||||
for begin in range(0, len(response), chunk_size):
|
||||
view = memoryview(response[begin:begin + chunk_size])
|
||||
while view:
|
||||
view = view[os.write(write_fd, view):]
|
||||
except (BrokenPipeError, OSError):
|
||||
# Invalid-response tests deliberately close the reader early.
|
||||
pass
|
||||
finally:
|
||||
os.close(write_fd)
|
||||
|
||||
self.thread = threading.Thread(target=write_response, daemon=True)
|
||||
self.thread.start()
|
||||
|
||||
def poll(self):
|
||||
return self.status
|
||||
|
||||
def wait(self, timeout=None):
|
||||
return self.status
|
||||
|
||||
def terminate(self):
|
||||
self.terminated = True
|
||||
|
||||
def kill(self):
|
||||
self.terminated = True
|
||||
|
||||
def close(self):
|
||||
if not self.stdin.closed:
|
||||
self.stdin.close()
|
||||
if not self.stdout.closed:
|
||||
self.stdout.close()
|
||||
self.thread.join(timeout=2)
|
||||
|
||||
|
||||
class ProtocolTests(unittest.TestCase):
|
||||
def create_worker(self, response, status=0):
|
||||
process = FakeProcess(response, status=status)
|
||||
self.addCleanup(process.close)
|
||||
source_patch = mock.patch.object(ORACLE, "_digest", return_value="frozen-test-hash")
|
||||
process_patch = mock.patch.object(ORACLE.subprocess, "Popen", return_value=process)
|
||||
source_patch.start()
|
||||
process_mock = process_patch.start()
|
||||
self.addCleanup(source_patch.stop)
|
||||
self.addCleanup(process_patch.stop)
|
||||
worker = ORACLE._APLWorker(Path("science-python"), Path("legacy_apl_worker.py"), Path("bundle"), None, timeout=2)
|
||||
self.addCleanup(worker.abort)
|
||||
return worker, process, process_mock
|
||||
|
||||
def test_exact_handshake_frame_and_clean_eof(self):
|
||||
worker, process, process_mock = self.create_worker(handshake() + frame())
|
||||
globals_ = np.array([1, 40, 166699], dtype=np.int32)
|
||||
drive, state, inhibition = worker.step(0, globals_)
|
||||
values = np.arange(4068, dtype=np.float32)
|
||||
np.testing.assert_array_equal(drive, values[:2])
|
||||
np.testing.assert_array_equal(state, values[2:4])
|
||||
np.testing.assert_array_equal(inhibition, values[4:])
|
||||
request = process.stdin.getvalue()
|
||||
self.assertEqual(struct.unpack("<qQ", request[:16]), (0, 3))
|
||||
np.testing.assert_array_equal(np.frombuffer(request[16:], dtype="<i4"), globals_)
|
||||
self.assertEqual(worker.finish(1), 0)
|
||||
command = process_mock.call_args.args[0]
|
||||
self.assertEqual(command, ["science-python", "-B", "legacy_apl_worker.py", "--bundle-dir", "bundle", "--dt-ms", "0.1"])
|
||||
self.assertFalse(process_mock.call_args.kwargs["shell"])
|
||||
|
||||
def test_empty_spike_frame_still_advances_one_tick(self):
|
||||
worker, process, _ = self.create_worker(handshake() + frame(values=np.zeros(4068, np.float32)))
|
||||
arrays = worker.step(0, np.empty(0, np.int32))
|
||||
self.assertTrue(all(np.all(value == 0) for value in arrays))
|
||||
self.assertEqual(process.stdin.getvalue(), struct.pack("<qQ", 0, 0))
|
||||
self.assertEqual(worker.finish(1), 0)
|
||||
|
||||
def test_two_consecutive_ticks(self):
|
||||
worker, _, _ = self.create_worker(handshake() + frame(0) + frame(1))
|
||||
worker.step(0, np.array([4], np.int32))
|
||||
worker.step(1, np.array([8, 10], np.int32))
|
||||
self.assertEqual(worker.finish(2), 0)
|
||||
|
||||
def test_rejects_wrong_protocol_or_dimensions(self):
|
||||
for update in ({"protocol": "other"}, {"ok": False}, {"n_neurons": 42},
|
||||
{"n_kc": 3}, {"dt_ms": .2}, {"gain_f32_bits": "00000000"},
|
||||
{"files_sha256": {}}):
|
||||
with self.subTest(update=update):
|
||||
with self.assertRaises(ValueError):
|
||||
self.create_worker(handshake(**update))
|
||||
|
||||
def test_rejects_wrong_tick_or_payload_length(self):
|
||||
for response in (frame(tick=1), frame(byte_count=8)):
|
||||
with self.subTest(response_header=response[:16]):
|
||||
worker, _, _ = self.create_worker(handshake() + response)
|
||||
with self.assertRaises(ValueError):
|
||||
worker.step(0, np.empty(0, np.int32))
|
||||
|
||||
def test_rejects_truncated_response(self):
|
||||
worker, _, _ = self.create_worker(handshake() + frame()[:-1])
|
||||
with self.assertRaises(EOFError):
|
||||
worker.step(0, np.empty(0, np.int32))
|
||||
|
||||
def test_rejects_nonfinite_or_negative_apl_payload(self):
|
||||
for bad_value in (np.nan, np.inf, -1):
|
||||
with self.subTest(value=bad_value):
|
||||
values = np.zeros(4068, np.float32)
|
||||
values[-1] = bad_value
|
||||
worker, _, _ = self.create_worker(handshake() + frame(values=values))
|
||||
with self.assertRaises(ValueError):
|
||||
worker.step(0, np.empty(0, np.int32))
|
||||
|
||||
def test_rejects_invalid_spike_dtype_order_and_range(self):
|
||||
for values in (np.array([1], np.int64), np.array([2, 1], np.int32),
|
||||
np.array([1, 1], np.int32), np.array([-1], np.int32),
|
||||
np.array([166700], np.int32)):
|
||||
with self.subTest(values=values):
|
||||
worker, process, _ = self.create_worker(handshake() + frame())
|
||||
with self.assertRaises(ValueError):
|
||||
worker.step(0, values)
|
||||
self.assertEqual(process.stdin.getvalue(), b"")
|
||||
|
||||
def test_rejects_skipped_request_tick(self):
|
||||
worker, process, _ = self.create_worker(handshake() + frame())
|
||||
with self.assertRaises(ValueError):
|
||||
worker.step(1, np.empty(0, np.int32))
|
||||
self.assertEqual(process.stdin.getvalue(), b"")
|
||||
|
||||
def test_rejects_failed_exit_and_trailing_stdout(self):
|
||||
for suffix, status in ((b"", 7), (b"unexpected", 0)):
|
||||
with self.subTest(status=status, suffix=suffix):
|
||||
worker, _, _ = self.create_worker(handshake() + frame() + suffix, status=status)
|
||||
worker.step(0, np.empty(0, np.int32))
|
||||
with self.assertRaises(ValueError):
|
||||
worker.finish(1)
|
||||
|
||||
def test_pipe_reader_timeout(self):
|
||||
read_fd, write_fd = os.pipe()
|
||||
stream = os.fdopen(read_fd, "rb", buffering=0)
|
||||
reader = ORACLE._PipeReader(stream, .05)
|
||||
try:
|
||||
with self.assertRaises(TimeoutError):
|
||||
reader.read(1)
|
||||
finally:
|
||||
os.close(write_fd)
|
||||
reader.thread.join(timeout=2)
|
||||
stream.close()
|
||||
self.assertFalse(reader.thread.is_alive())
|
||||
|
||||
|
||||
class ArithmeticAndIdentityContractTests(unittest.TestCase):
|
||||
def test_si_conversion_must_follow_float64_promotion(self):
|
||||
h32 = np.array([.01, 1 / 7, 123.456], dtype=np.float32)
|
||||
correct = h32.astype(np.float64) * np.float64(.001)
|
||||
early_conversion = (h32 * np.float32(.001)).astype(np.float64)
|
||||
self.assertFalse(np.array_equal(correct.view(np.uint64), early_conversion.view(np.uint64)))
|
||||
for actual, original in zip(correct, h32):
|
||||
self.assertEqual(actual.hex(), (float(original) * float(.001)).hex())
|
||||
|
||||
def test_declared_overlay_does_not_guard_refractory_targets(self):
|
||||
before = np.array([0, -.002, .001], dtype=np.float64)
|
||||
refractory = np.array([22, 21, 0], dtype=np.int32)
|
||||
h32 = np.array([1, 2, 3], dtype=np.float32)
|
||||
delta = h32.astype(np.float64) * np.float64(.001)
|
||||
after = before - delta
|
||||
self.assertTrue(np.all(after != before))
|
||||
np.testing.assert_array_equal(refractory, [22, 21, 0])
|
||||
self.assertEqual(after[0], -.001)
|
||||
|
||||
def test_af_queue_hash_keeps_original_noncontiguous_ids_and_rejections(self):
|
||||
path = HERE.parent / "author-fullgraph-lab-v1" / "oracle_full.py"
|
||||
self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), ORACLE.AF_SHA256)
|
||||
af = load_no_cache(path, "phase4ag_protocol_af")
|
||||
edges = np.array([81, 2, 500], dtype=np.int64)
|
||||
accepted = np.array([1, 0, 1], dtype=np.uint8)
|
||||
original_edges, original_mask = edges.copy(), accepted.copy()
|
||||
result = af.queue_summary(18, edges, accepted, chunk_bytes=8)
|
||||
packet = b"phase4af-queue-v1\0" + struct.pack("<qq", 18, 3) + edges.astype("<i8").tobytes() + accepted.tobytes()
|
||||
self.assertEqual(result["joint_sha256"], hashlib.sha256(packet).hexdigest())
|
||||
self.assertEqual((result["count"], result["accepted_count"]), (3, 2))
|
||||
self.assertEqual(result, af.queue_summary(18, edges, accepted, chunk_bytes=8192))
|
||||
np.testing.assert_array_equal(edges, original_edges)
|
||||
np.testing.assert_array_equal(accepted, original_mask)
|
||||
|
||||
def test_empty_queue_hash_includes_tick(self):
|
||||
af = load_no_cache(HERE.parent / "author-fullgraph-lab-v1" / "oracle_full.py", "phase4ag_empty_af")
|
||||
first = af.queue_summary(0, np.empty(0, np.int64), np.empty(0, np.uint8))
|
||||
second = af.queue_summary(1, np.empty(0, np.int64), np.empty(0, np.uint8))
|
||||
self.assertEqual(first["ordered_edge_sha256"], hashlib.sha256(b"").hexdigest())
|
||||
self.assertNotEqual(first["joint_sha256"], second["joint_sha256"])
|
||||
|
||||
def test_oracle_import_has_no_brian_or_torch_dependency(self):
|
||||
before = set(sys.modules)
|
||||
load_no_cache(HERE / "oracle_apl.py", "phase4ag_second_import")
|
||||
added = set(sys.modules) - before
|
||||
self.assertFalse(any(name == "brian2" or name.startswith("brian2.") or
|
||||
name == "torch" or name.startswith("torch.") for name in added))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Regression for an execution-path error, unrelated to brain dynamics."""
|
||||
from pathlib import Path
|
||||
from unittest import TestCase,main,mock,skipIf
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from oracle_apl import _python_entry
|
||||
|
||||
|
||||
class PythonEntryTests(TestCase):
|
||||
@skipIf(os.name=='nt','Real POSIX interpreter symlink is validated on the task server')
|
||||
def test_real_symlink_remains_invocation_path(self):
|
||||
with tempfile.TemporaryDirectory(prefix='phase4ag-entry-') as folder:
|
||||
path=Path(folder)/'python'
|
||||
path.symlink_to(sys.executable)
|
||||
self.assertEqual(_python_entry(path),path)
|
||||
self.assertNotEqual(_python_entry(path),path.resolve())
|
||||
|
||||
@skipIf(os.name=='nt','POSIX dangling symlink test is validated on the task server')
|
||||
def test_real_dangling_link_and_directory_rejected(self):
|
||||
with tempfile.TemporaryDirectory(prefix='phase4ag-entry-') as folder:
|
||||
path=Path(folder)/'python'
|
||||
path.symlink_to(Path(folder)/'absent')
|
||||
with self.assertRaises(ValueError):_python_entry(path)
|
||||
with self.assertRaises(ValueError):_python_entry(Path(folder))
|
||||
|
||||
def test_preserves_declared_venv_path_without_resolving_symlink(self):
|
||||
entry=Path.cwd()/'.fixture-venv/bin/python'
|
||||
with mock.patch.object(Path,'resolve',side_effect=AssertionError('Must not resolve interpreter symlink')):
|
||||
with mock.patch.object(Path,'is_file',return_value=True):
|
||||
self.assertEqual(_python_entry(entry),entry)
|
||||
|
||||
def test_missing_python_entry_is_rejected(self):
|
||||
with mock.patch.object(Path,'is_file',return_value=False):
|
||||
with self.assertRaises(ValueError):_python_entry('/missing/python')
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,431 @@
|
||||
"""Independent extraction/replay of the frozen legacy APL component.
|
||||
|
||||
Only the initializer prefix before ``self.base`` and two original APL update
|
||||
statements are executed. No old simulator is instantiated, no learning code is
|
||||
executed, and no frozen artifact is written. The Torch replay is a component
|
||||
oracle, not an independent CPU whole-brain closed-loop oracle.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import copy
|
||||
from functools import lru_cache
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import types
|
||||
|
||||
import numpy as np
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
|
||||
|
||||
ROOT = Path('/data/digital-drosophila')
|
||||
CANONICAL = ROOT/'datasets/male-cns-v1.0/processed/neuron-only'
|
||||
SOURCE = ROOT/'models/male-cns-lif/deterministic-runtime-v2/phase4p_revalidate.py'
|
||||
SOURCE_SHA256 = 'c7e693a2da70acdce4f53af5c2853e52e8333acc93f88a46a5e8083eac639346'
|
||||
N, A, K = 166700, 2, 4064
|
||||
BUNDLE_ARRAYS = {
|
||||
'input_matrix': (np.float32, (A, N)), 'output_matrix': (np.float32, (A, K)),
|
||||
'input_raw': (np.float32, (A, N)), 'output_raw': (np.float32, (A, K)),
|
||||
'kc_globals': (np.int32, (K,)), 'apl_globals': (np.int32, (A,)),
|
||||
'pn_globals': (np.int32, None), 'apl_out_ids': (np.int64, (5845,)),
|
||||
}
|
||||
|
||||
|
||||
def require(condition, message):
|
||||
if not condition:
|
||||
raise ValueError(message)
|
||||
|
||||
|
||||
def digest(path):
|
||||
h = hashlib.sha256()
|
||||
with Path(path).open('rb') as source:
|
||||
for block in iter(lambda: source.read(8 << 20), b''):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def array_hash(value):
|
||||
return hashlib.sha256(np.ascontiguousarray(value).tobytes()).hexdigest()
|
||||
|
||||
|
||||
def bit_equal(a, b):
|
||||
a, b = np.asarray(a), np.asarray(b)
|
||||
return bool(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)))
|
||||
|
||||
|
||||
def save(path, value):
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open('x', encoding='utf-8') as target:
|
||||
json.dump(value, target, indent=2, sort_keys=True, allow_nan=False)
|
||||
target.write('\n')
|
||||
|
||||
|
||||
class ReadOnlyNamespace:
|
||||
"""The extracted prefix can read rt, but cannot mutate or add rt fields."""
|
||||
def __init__(self, **values):
|
||||
object.__setattr__(self, '_values', types.MappingProxyType(values))
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self._values[name]
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
raise AttributeError('Read-only canonical runtime view')
|
||||
|
||||
|
||||
def _code(nodes, filename):
|
||||
module = ast.Module(body=copy.deepcopy(nodes), type_ignores=[])
|
||||
return compile(ast.fix_missing_locations(module), str(filename), 'exec')
|
||||
|
||||
|
||||
def _self_attribute(target, field):
|
||||
return isinstance(target, ast.Attribute) and isinstance(target.value, ast.Name) and \
|
||||
target.value.id == 'self' and target.attr == field
|
||||
|
||||
|
||||
def extract_statements():
|
||||
data = SOURCE.read_bytes()
|
||||
require(hashlib.sha256(data).hexdigest() == SOURCE_SHA256, 'Frozen legacy source SHA-256 mismatch')
|
||||
text = data.decode('utf-8')
|
||||
tree = ast.parse(text, filename=str(SOURCE))
|
||||
classes = [node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == 'DeterministicAPLSim']
|
||||
require(len(classes) == 1, 'Expected exactly one frozen DeterministicAPLSim')
|
||||
methods = {node.name: node for node in classes[0].body if isinstance(node, ast.FunctionDef)}
|
||||
init, run = methods['__init__'], methods['run']
|
||||
cuts = [i for i, node in enumerate(init.body) if isinstance(node, ast.Assign) and
|
||||
any(_self_attribute(target, 'base') for target in node.targets)]
|
||||
require(len(cuts) == 1 and cuts[0] > 0, 'Missing/ambiguous self.base cutoff')
|
||||
prefix = init.body[:cuts[0]]
|
||||
# Executing no import or file-writing call is allowed in the extracted prefix.
|
||||
require(not any(isinstance(node, (ast.Import, ast.ImportFrom, ast.With, ast.AsyncWith))
|
||||
for stmt in prefix for node in ast.walk(stmt)), 'Unexpected side-effect construct in initializer prefix')
|
||||
expressions = [node for node in ast.walk(run) if isinstance(node, ast.Expr) and
|
||||
isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Attribute) and
|
||||
node.value.func.attr == 'add_' and isinstance(node.value.func.value, ast.Call) and
|
||||
isinstance(node.value.func.value.func, ast.Attribute) and
|
||||
isinstance(node.value.func.value.func.value, ast.Name) and
|
||||
node.value.func.value.func.value.id == 'apl_state' and node.value.func.value.func.attr == 'mul_']
|
||||
inhibition = [node for node in ast.walk(run) if isinstance(node, ast.Assign) and
|
||||
any(isinstance(target, ast.Name) and target.id == 'inhibition' for target in node.targets)]
|
||||
decay = [node for node in ast.walk(run) if isinstance(node, ast.Assign) and
|
||||
any(isinstance(target, ast.Name) and target.id == 'apl_decay' for target in node.targets)]
|
||||
require(len(expressions) == len(inhibition) == len(decay) == 1, 'Ambiguous frozen APL operator extraction')
|
||||
require(expressions[0].lineno < inhibition[0].lineno, 'Unexpected frozen APL update order')
|
||||
rhs = inhibition[0].value
|
||||
require(isinstance(rhs, ast.BinOp) and isinstance(rhs.op, ast.Mult), 'Unexpected inhibition gain expression')
|
||||
gain_expression = ast.Expression(copy.deepcopy(rhs.right))
|
||||
provenance = {'source': str(SOURCE), 'sha256': SOURCE_SHA256,
|
||||
'initializer_line_start': prefix[0].lineno, 'initializer_line_end': prefix[-1].end_lineno,
|
||||
'excluded_self_base_line': init.body[cuts[0]].lineno,
|
||||
'decay_line': decay[0].lineno, 'state_update_line': expressions[0].lineno,
|
||||
'inhibition_line': inhibition[0].lineno,
|
||||
'initializer_source': '\n'.join(ast.get_source_segment(text, node) for node in prefix),
|
||||
'decay_source': ast.get_source_segment(text, decay[0]),
|
||||
'state_update_source': ast.get_source_segment(text, expressions[0]),
|
||||
'inhibition_source': ast.get_source_segment(text, inhibition[0]),
|
||||
'learning_initializer_suffix_executed': False, 'original_run_loop_executed': False}
|
||||
return (_code(prefix, SOURCE), _code(decay, SOURCE),
|
||||
_code([expressions[0], inhibition[0]], SOURCE),
|
||||
compile(ast.fix_missing_locations(gain_expression), str(SOURCE), 'eval'), provenance)
|
||||
|
||||
|
||||
def _canonical_files():
|
||||
return {name: CANONICAL/path for name, path in {
|
||||
'metadata': 'neuron-index/unified-metadata.parquet',
|
||||
'indptr': 'gpu/csr-indptr.int64.npy', 'indices': 'gpu/csr-indices.int32.npy',
|
||||
'raw': 'gpu/csr-weights.int64.npy'}.items()}
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def reconstruct(canonical_hashes, device_name):
|
||||
"""Cache only reconstructions tied to exact canonical bytes and source hash."""
|
||||
files = _canonical_files()
|
||||
require(dict(canonical_hashes) == {name: digest(path) for name, path in files.items()},
|
||||
'Canonical bytes changed before matrix reconstruction')
|
||||
arrays = {name: np.load(files[name], mmap_mode='r', allow_pickle=False)
|
||||
for name in ('indptr', 'indices', 'raw')}
|
||||
require(arrays['indptr'].dtype == np.int64 and arrays['indices'].dtype == np.int32 and
|
||||
arrays['raw'].dtype == np.int64, 'Canonical CSR dtypes changed')
|
||||
require(arrays['indptr'].shape == (N + 1,) and arrays['indptr'][-1] == 25582938 and
|
||||
arrays['indices'].shape == arrays['raw'].shape == (25582938,), 'Canonical graph shape changed')
|
||||
require(int(arrays['raw'].sum()) == 124177617 and bool(np.all(arrays['raw'] > 0)), 'Canonical raw weights changed')
|
||||
table = pq.read_table(files['metadata'])
|
||||
require(table.num_rows == N, 'Canonical metadata count changed')
|
||||
rt = ReadOnlyNamespace(n=N, dt=.1, device=torch.device(device_name), table=table,
|
||||
indptr_np=arrays['indptr'], indices_np=arrays['indices'], raw_np=arrays['raw'])
|
||||
view = types.SimpleNamespace()
|
||||
init_code, decay_code, update_code, gain_code, provenance = extract_statements()
|
||||
scope = {'self': view, 'rt': rt, 'np': np, 'torch': torch}
|
||||
exec(init_code, scope)
|
||||
require(not hasattr(view, 'base') and not hasattr(view, 'identity'), 'Initializer crossed the learning cutoff')
|
||||
expected = {'input_matrix': view.input_gpu.detach().cpu().numpy().copy(),
|
||||
'output_matrix': view.output_gpu.detach().cpu().numpy().copy(),
|
||||
'input_raw': scope['input_matrix'], 'output_raw': scope['output_matrix'],
|
||||
'kc_globals': view.kc, 'apl_globals': view.apl, 'pn_globals': view.pn,
|
||||
'apl_out_ids': view.apl_out_ids}
|
||||
for value in expected.values():
|
||||
value.flags.writeable = False
|
||||
return view, expected, provenance, (decay_code, update_code, gain_code)
|
||||
|
||||
|
||||
class TorchObservationProxy:
|
||||
"""Observe the result of the actual extracted torch.mv call unchanged."""
|
||||
def __init__(self):
|
||||
self.drive = None
|
||||
self.calls = 0
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(torch, name)
|
||||
|
||||
def mv(self, *args, **kwargs):
|
||||
value = torch.mv(*args, **kwargs)
|
||||
self.drive = value
|
||||
self.calls += 1
|
||||
return value
|
||||
|
||||
|
||||
def _up(value):
|
||||
return np.nextafter(np.asarray(value, dtype=np.float64), np.inf)
|
||||
|
||||
|
||||
def _add(a, b):
|
||||
return _up(np.asarray(a, dtype=np.float64) + np.asarray(b, dtype=np.float64))
|
||||
|
||||
|
||||
def _mul(a, b):
|
||||
return _up(np.asarray(a, dtype=np.float64) * np.asarray(b, dtype=np.float64))
|
||||
|
||||
|
||||
def _gamma(count, unit_roundoff):
|
||||
require(count * unit_roundoff < 1, 'Gamma bound is undefined')
|
||||
# Outward rounding on the division; the denominator is rounded downward.
|
||||
numerator = _mul(count, unit_roundoff)
|
||||
denominator = np.nextafter(1.0 - numerator, -np.inf)
|
||||
return _up(numerator / denominator)
|
||||
|
||||
|
||||
def _positive_dot_upper(state, matrix):
|
||||
result = np.zeros(matrix.shape[1], np.float64)
|
||||
for index in range(len(state)):
|
||||
result = _add(result, _mul(state[index], matrix[index]))
|
||||
return result
|
||||
|
||||
|
||||
def _normal_or_zero(value, tiny):
|
||||
value = np.asarray(value)
|
||||
return bool(np.all((value == 0) | (value >= tiny)))
|
||||
|
||||
|
||||
def _new_metric():
|
||||
return {'max_abs_error': 0.0, 'sum_abs_error': 0.0, 'elements': 0,
|
||||
'max_relative_error_nonzero_reference': 0.0, 'zero_reference_nonzero_actual': 0,
|
||||
'max_error_bound': 0.0, 'max_bound_utilization': 0.0, 'bound_violations': 0}
|
||||
|
||||
|
||||
def _accumulate(metric, actual, reference, bound):
|
||||
error = np.abs(np.asarray(actual, np.float64) - reference)
|
||||
error_upper = np.where(error == 0, 0.0, _up(error))
|
||||
nonzero = reference != 0
|
||||
ratio = np.divide(error, np.abs(reference), out=np.zeros_like(error), where=nonzero)
|
||||
usage = np.divide(error_upper, bound, out=np.zeros_like(error), where=bound > 0)
|
||||
metric['max_abs_error'] = max(metric['max_abs_error'], float(error.max(initial=0)))
|
||||
metric['sum_abs_error'] += float(error.sum(dtype=np.float64))
|
||||
metric['elements'] += error.size
|
||||
metric['max_relative_error_nonzero_reference'] = max(metric['max_relative_error_nonzero_reference'], float(ratio.max(initial=0)))
|
||||
metric['zero_reference_nonzero_actual'] += int(np.count_nonzero((~nonzero) & (error != 0)))
|
||||
metric['max_error_bound'] = max(metric['max_error_bound'], float(np.max(bound, initial=0)))
|
||||
metric['max_bound_utilization'] = max(metric['max_bound_utilization'], float(usage.max(initial=0)))
|
||||
metric['bound_violations'] += int(np.count_nonzero(error_upper > bound))
|
||||
|
||||
|
||||
def validate(gpu_case_dir, bundle_dir, out_json):
|
||||
"""Validate one completed fixture; write one exclusive pure-JSON audit."""
|
||||
gpu_case_dir, bundle_dir, out_json = Path(gpu_case_dir), Path(bundle_dir), Path(out_json)
|
||||
require(not out_json.exists(), 'Refusing to overwrite an existing component audit')
|
||||
report = {'pass_all': False, 'scope': 'Frozen APL component extraction and recorded-spike replay only',
|
||||
'independent_cpu_closed_loop_oracle': False, 'brain_or_learning_parameters_changed': False,
|
||||
'candidate_function_called': False, 'gpu_case_dir': str(gpu_case_dir), 'bundle_dir': str(bundle_dir)}
|
||||
try:
|
||||
require(digest(SOURCE) == SOURCE_SHA256, 'Frozen source changed before component validation')
|
||||
require(torch.cuda.is_available() and torch.cuda.device_count() == 1 and
|
||||
'4090' in torch.cuda.get_device_name(), 'Require the one declared RTX4090')
|
||||
environment = {'torch': str(torch.__version__), 'numpy': np.__version__,
|
||||
'gpu': torch.cuda.get_device_name(), 'cuda': torch.version.cuda,
|
||||
'cuda_matmul_allow_tf32': torch.backends.cuda.matmul.allow_tf32,
|
||||
'float32_matmul_precision': torch.get_float32_matmul_precision(),
|
||||
'deterministic_algorithms': torch.are_deterministic_algorithms_enabled(),
|
||||
'precision_flags_changed': False}
|
||||
report['environment'] = environment
|
||||
canonical_hashes = tuple(sorted((name, digest(path)) for name, path in _canonical_files().items()))
|
||||
view, reconstructed, provenance, code = reconstruct(canonical_hashes, 'cuda:0')
|
||||
bundle = {}
|
||||
matrix_checks = {}
|
||||
for name, (dtype, shape) in BUNDLE_ARRAYS.items():
|
||||
value = np.load(bundle_dir/(name+'.npy'), mmap_mode='r', allow_pickle=False)
|
||||
require(value.dtype == dtype and value.ndim == len(shape or (0,)) and
|
||||
(shape is None or value.shape == shape), 'Bundle schema mismatch: '+name)
|
||||
bundle[name] = value
|
||||
matrix_checks[name] = bit_equal(value, reconstructed[name])
|
||||
scales = json.loads((bundle_dir/'scales.json').read_text())
|
||||
for name in ('input_scale', 'output_scale'):
|
||||
matrix_checks[name] = struct.pack('<d', float(scales[name])) == struct.pack('<d', float(getattr(view, name)))
|
||||
report['matrix_checks'] = matrix_checks
|
||||
report['extracted_source'] = provenance
|
||||
report['canonical_sha256'] = dict(canonical_hashes)
|
||||
report['bundle_sha256'] = {name+'.npy': digest(bundle_dir/(name+'.npy')) for name in BUNDLE_ARRAYS}
|
||||
report['bundle_sha256']['scales.json'] = digest(bundle_dir/'scales.json')
|
||||
report['reconstructed_array_sha256'] = {name: array_hash(value) for name, value in reconstructed.items()}
|
||||
bundle_provenance = json.loads((bundle_dir/'provenance.json').read_text())
|
||||
require(Path(bundle_provenance['canonical_root']).resolve() == CANONICAL.resolve(),
|
||||
'Bundle provenance names another canonical dataset')
|
||||
report['bundle_sha256']['provenance.json'] = digest(bundle_dir/'provenance.json')
|
||||
require(all(matrix_checks.values()), 'Bundle differs from the actual frozen initializer prefix')
|
||||
|
||||
traces = {name: np.load(gpu_case_dir/('apl-'+name+'.float32.npy'), mmap_mode='r', allow_pickle=False)
|
||||
for name in ('drive', 'state', 'inhibition')}
|
||||
steps = len(traces['state'])
|
||||
require(steps > 0, 'Empty APL trajectory cannot pass')
|
||||
for name, width in (('drive', A), ('state', A), ('inhibition', K)):
|
||||
require(traces[name].dtype == np.float32 and traces[name].shape == (steps, width) and
|
||||
bool(np.isfinite(traces[name]).all()) and bool(np.all(traces[name] >= 0)), 'Invalid APL trace: '+name)
|
||||
summary = json.loads((gpu_case_dir/'summary.json').read_text())
|
||||
require(summary.get('complete') is True and summary.get('pass_all') is True and
|
||||
summary.get('steps') == steps and summary.get('completed_ticks') == steps,
|
||||
'GPU fixture did not complete its internal execution checks')
|
||||
with np.load(gpu_case_dir/'state.npz', allow_pickle=False) as archive:
|
||||
event_step, event_node = archive['step'].copy(), archive['neuron_idx'].copy()
|
||||
require(event_step.dtype == event_node.dtype == np.int32 and event_step.ndim == 1 and
|
||||
event_step.shape == event_node.shape, 'Invalid sparse actual spike event schema')
|
||||
require(bool(np.all((event_step >= 0) & (event_step < steps))) and
|
||||
bool(np.all((event_node >= 0) & (event_node < N))), 'Sparse event outside its clock/identity range')
|
||||
require(np.array_equal(np.lexsort((event_node, event_step)), np.arange(len(event_step))), 'Actual events not sorted')
|
||||
keys = event_step.astype(np.int64)*N + event_node
|
||||
require(len(keys) < 2 or bool(np.all(np.diff(keys) > 0)), 'Duplicate actual spikes')
|
||||
boundaries = np.searchsorted(event_step, np.arange(steps + 1))
|
||||
|
||||
proxy = TorchObservationProxy()
|
||||
# Reconstructed original matrices are already on the original GPU/dtype.
|
||||
replay_rt = types.SimpleNamespace(spike_u8=torch.zeros(N, dtype=torch.uint8, device=view.device))
|
||||
shadow = types.SimpleNamespace(input_gpu=view.input_gpu, output_gpu=view.output_gpu, rt=replay_rt, dt=.1)
|
||||
scope = {'self': shadow, 'np': np, 'math': math, 'torch': proxy,
|
||||
'apl_state': torch.zeros(A, dtype=torch.float32, device=view.device)}
|
||||
decay_code, update_code, gain_code = code
|
||||
exec(decay_code, scope)
|
||||
decay32 = np.float32(scope['apl_decay'])
|
||||
gain32 = np.float32(eval(gain_code, {'np': np}))
|
||||
require(0 < decay32 < 1 and gain32 > 0, 'Invalid extracted positive APL constants')
|
||||
exact = {name: True for name in traces}
|
||||
first_difference = None
|
||||
I64 = np.asarray(bundle['input_matrix'], np.float64)
|
||||
O64 = np.asarray(bundle['output_matrix'], np.float64)
|
||||
require(bool(np.all(I64 >= 0)) and bool(np.all(O64 >= 0)), 'Positive-sum theory requires nonnegative matrices')
|
||||
d, q = float(decay32), float(gain32)
|
||||
u32, u64 = 2.0**-24, 2.0**-53
|
||||
gd32, gd64 = _gamma(N-1, u32), _gamma(N-1, u64)
|
||||
g232, g264 = _gamma(2, u32), _gamma(2, u64)
|
||||
gh32, gh64 = _gamma(2*A+1, u32), _gamma(2*A+1, u64)
|
||||
state64 = np.zeros(A, np.float64)
|
||||
e32 = np.zeros(A, np.float64)
|
||||
e64 = np.zeros(A, np.float64)
|
||||
previous32 = np.zeros(A, np.float64)
|
||||
statistics = {name: _new_metric() for name in traces}
|
||||
normal_arithmetic = _normal_or_zero(I64, np.finfo(np.float32).tiny) and _normal_or_zero(O64, np.finfo(np.float32).tiny)
|
||||
max_active_terms = 0
|
||||
with torch.no_grad():
|
||||
for tick in range(steps):
|
||||
fired = event_node[boundaries[tick]:boundaries[tick+1]]
|
||||
replay_rt.spike_u8.zero_()
|
||||
if len(fired):
|
||||
replay_rt.spike_u8[torch.as_tensor(fired.astype(np.int64), device=view.device)] = 1
|
||||
before_calls = proxy.calls
|
||||
exec(update_code, scope)
|
||||
require(proxy.calls == before_calls + 1, 'Original update did not issue exactly one mv')
|
||||
actual = {'drive': proxy.drive.detach().cpu().numpy(),
|
||||
'state': scope['apl_state'].detach().cpu().numpy(),
|
||||
'inhibition': scope['inhibition'].detach().cpu().numpy()}
|
||||
for name in traces:
|
||||
same = bit_equal(actual[name], traces[name][tick])
|
||||
exact[name] &= same
|
||||
if not same and first_difference is None:
|
||||
first_difference = {'tick': tick, 'field': name,
|
||||
'max_abs_error': float(np.max(np.abs(actual[name].astype(np.float64)-traces[name][tick])))}
|
||||
# Float64 reference is measurement only and never feeds either brain.
|
||||
selected = I64[:, fired]
|
||||
drive64 = selected.sum(axis=1, dtype=np.float64)
|
||||
max_active_terms = max(max_active_terms, int(np.count_nonzero(selected, axis=1).max(initial=0)))
|
||||
Dupper = _up(drive64 / np.nextafter(1.0-gd64, -np.inf))
|
||||
drive_bound = _add(_mul(gd32, Dupper), _mul(gd64, Dupper))
|
||||
state32 = traces['state'][tick].astype(np.float64)
|
||||
normal_arithmetic &= _normal_or_zero(d*previous32, np.finfo(np.float32).tiny)
|
||||
normal_arithmetic &= _normal_or_zero(O64*state32[:, None], np.finfo(np.float32).tiny)
|
||||
e32 = _add(_mul(d, e32), _add(_mul(g232, _add(_mul(d, previous32), Dupper)),
|
||||
_mul(1.0+u32, _mul(gd32, Dupper))))
|
||||
e64 = _add(_mul(d, e64), _add(_mul(g264, _add(_mul(d, state64), Dupper)),
|
||||
_mul(1.0+u64, _mul(gd64, Dupper))))
|
||||
state64 = d*state64 + drive64
|
||||
inhibition64 = np.matmul(state64, O64)*q
|
||||
h32bound = _add(_mul(q, _positive_dot_upper(e32, O64)),
|
||||
_mul(gh32, _mul(q, _positive_dot_upper(state32, O64))))
|
||||
h64bound = _add(_mul(q, _positive_dot_upper(e64, O64)),
|
||||
_mul(gh64, _mul(q, _positive_dot_upper(state64, O64))))
|
||||
normal_arithmetic &= _normal_or_zero(q*np.matmul(state32, O64), np.finfo(np.float32).tiny)
|
||||
_accumulate(statistics['drive'], traces['drive'][tick], drive64, drive_bound)
|
||||
_accumulate(statistics['state'], traces['state'][tick], state64, _add(e32, e64))
|
||||
_accumulate(statistics['inhibition'], traces['inhibition'][tick], inhibition64, _add(h32bound, h64bound))
|
||||
previous32 = state32
|
||||
for metric in statistics.values():
|
||||
metric['mean_abs_error'] = metric.pop('sum_abs_error') / metric['elements']
|
||||
theory_applicable = bool(normal_arithmetic and not environment['cuda_matmul_allow_tf32'] and
|
||||
environment['float32_matmul_precision'] == 'highest')
|
||||
report.update(steps=steps, actual_spike_events=len(event_step),
|
||||
ast_replay_bitwise_exact=exact, first_ast_difference=first_difference,
|
||||
value_units={'drive': 'legacy normalized-contact spike drive',
|
||||
'state': 'legacy normalized-drive accumulator', 'inhibition': 'legacy mV'},
|
||||
decay_f32_bits=f'{int(decay32.view(np.uint32)):08x}', gain_f32_bits=f'{int(gain32.view(np.uint32)):08x}',
|
||||
float64_measurement=statistics,
|
||||
theoretical_bound={'applicable': theory_applicable, 'normal_or_zero_arithmetic': bool(normal_arithmetic),
|
||||
'gamma_definition': 'gamma_n=(n*u)/(1-n*u), upward-rounded; u32=2^-24,u64=2^-53',
|
||||
'drive_n': N-1, 'max_observed_nonzero_drive_terms': max_active_terms,
|
||||
'gamma_drive_f32': float(gd32), 'gamma_drive_f64': float(gd64),
|
||||
'state_recurrence': 'E_p[t] <= d*E_p[t-1] + gamma_2,p*(d*abs(s_p[t-1])+D_upper) + (1+u_p)*gamma_(N-1),p*D_upper',
|
||||
'output_bound': 'q*sum(B*E_p) + gamma_(2*A+1),p*q*sum(B*abs(s_p)); compare with E32+E64',
|
||||
'assumptions': 'nonnegative f32 matrices, spikes exactly 0/1, fixed stored f32 decay/gain, normal finite RN binary32/64 operations; no reduced-precision matmul',
|
||||
'bound_arithmetic': 'Each positive bound operation is rounded toward +infinity via nextafter; denominator toward -infinity',
|
||||
'tolerance_fitted_to_observed_error': False,
|
||||
'f64_values_used_to_drive_brain': False,
|
||||
'no_cpu_gpu_bitwise_expectation_for_f64_measurement': True},
|
||||
input_sha256={name: digest(gpu_case_dir/name) for name in
|
||||
('state.npz', 'summary.json', 'apl-drive.float32.npy', 'apl-state.float32.npy', 'apl-inhibition.float32.npy')})
|
||||
report['pass_all'] = bool(all(matrix_checks.values()) and all(exact.values()) and theory_applicable and
|
||||
all(value['bound_violations'] == 0 for value in statistics.values()))
|
||||
report['limitations'] = [
|
||||
'AST shadow and candidate use the same original Torch/CUDA primitives; this validates preserved component semantics, not an independent CPU whole-brain oracle.',
|
||||
'Float64 is a non-feedback mathematical measurement with explicit gamma error bounds, not replacement APL dynamics.',
|
||||
'Replay uses the recorded actual spike train; it does not establish equivalence for untested closed-loop inputs.',
|
||||
'The N-1 positive-sum bound is deliberately conservative; passing it does not prove physiological validity or learning.']
|
||||
save(out_json, report)
|
||||
return report
|
||||
except Exception as error:
|
||||
report['error'] = {'type': type(error).__name__, 'message': str(error)}
|
||||
if not out_json.exists():
|
||||
save(out_json, report)
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('gpu_case_dir', type=Path)
|
||||
parser.add_argument('bundle_dir', type=Path)
|
||||
parser.add_argument('out_json', type=Path)
|
||||
args = parser.parse_args()
|
||||
result = validate(args.gpu_case_dir, args.bundle_dir, args.out_json)
|
||||
print(json.dumps({'pass_all': result['pass_all'], 'steps': result['steps']}), flush=True)
|
||||
if not result['pass_all']:
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Run old reference unchanged, writing only new experiment output."""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yaml
|
||||
from context import ROOT,EXP,CFG,save,check
|
||||
|
||||
|
||||
def main():
|
||||
check();assert not (EXP/'regression/frozen-five.json').exists()
|
||||
rtpath=ROOT/'models/male-cns-lif/deterministic-runtime-v2';pc=yaml.safe_load((rtpath/'phase4p.yaml').read_text())
|
||||
os.environ['CUDA_VISIBLE_DEVICES']=CFG['gpu_uuid'];os.environ['CUDA_HOME']='/usr/local/cuda'
|
||||
os.environ['PATH']=str(ROOT/'models/male-cns-lif/.venv/bin')+':/usr/local/cuda/bin:'+os.environ['PATH']
|
||||
os.environ['MAX_JOBS']='2';os.environ['TORCH_CUDA_ARCH_LIST']='8.9'
|
||||
sys.path.insert(0,str(ROOT/'models/male-cns-lif/terminal-scope-diagnostic-v1'))
|
||||
from diagnostic import queues
|
||||
queues();sys.path.insert(0,str(ROOT/'models/male-cns-lif'));sys.path.insert(0,str(rtpath))
|
||||
import torch,phase4p
|
||||
assert torch.cuda.mem_get_info()[0]>2*1024**3
|
||||
torch.cuda.reset_peak_memory_stats();t0=time.perf_counter();rt=phase4p.CanonicalRuntime(pc,rtpath,EXP);rows=[]
|
||||
for spec in pc['reference_trials']:
|
||||
queues();base=ROOT/'experiments/phase4p-deterministic-runtime/phase4c-v2/reference'/spec['name']
|
||||
ev=pd.read_parquet(base/'spike-events.parquet')
|
||||
ref=dict(step=ev.step.to_numpy(),neuron_idx=ev.neuron_idx.to_numpy(),counts=np.load(base/'per-neuron-spike-counts.int32.npy'),v=np.load(base/'final-voltage.float32.npy'),g=np.load(base/'final-g.float32.npy'))
|
||||
result=rt.run(np.flatnonzero(rt.pops[spec['population']]),spec['rate_hz'],spec['seed'],spec['duration_ms'])
|
||||
row=dict(trial=spec['name'],exact={k:bool(np.array_equal(v,result[k])) for k,v in ref.items()},wall_seconds=result['metrics']['wall_seconds'])
|
||||
rows.append(row);print(row,flush=True)
|
||||
save(EXP/'regression/frozen-five.json',dict(pass_all=all(all(r['exact'].values()) for r in rows),trials=rows,
|
||||
wall_seconds=time.perf_counter()-t0,peak_allocated_vram=int(torch.cuda.max_memory_allocated()),oracle='old frozen deterministic-v2 only'))
|
||||
assert all(all(r['exact'].values()) for r in rows)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
Reference in New Issue
Block a user