Archive MaleCNS research sources, provenance, bibliography and immutable data inventory
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# MaleCNS LIF Phase 4A
|
||||
|
||||
Auditable, single-RTX-4090 whole-connectome LIF baseline. It consumes only the
|
||||
validated Phase 3B neuron-only products and never writes to the dataset tree.
|
||||
|
||||
Run with the exact GPU UUID to prevent accidental V100 or mixed-GPU use:
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=GPU-79f3a85d-adce-7833-7003-6488140ca596 \
|
||||
.venv/bin/python phase4a.py \
|
||||
--config /data/digital-drosophila/experiments/phase4a-baseline/config/baseline.yaml
|
||||
```
|
||||
|
||||
The raw `int64` synapse-count array remains read-only. The configured transform
|
||||
creates a separate signed `float32` effective-weight array. Spike output is a
|
||||
sparse Parquet event table, never a dense time-by-neuron matrix.
|
||||
|
||||
The baseline parameters are FlyWire-inspired modeling defaults, not MaleCNS
|
||||
single-cell measurements. Synthetic drive amplitudes are deterministic smoke-test
|
||||
inputs and are not a biological calibration.
|
||||
@@ -0,0 +1,110 @@
|
||||
phase: 4A
|
||||
paths:
|
||||
connectome_root: /data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4a-baseline
|
||||
|
||||
expected_graph:
|
||||
neurons: 166700
|
||||
directed_edges: 25582938
|
||||
synaptic_contact_weight_sum: 124177617
|
||||
isolated_neurons: 217
|
||||
self_loops: 101
|
||||
|
||||
device:
|
||||
required_name_substring: RTX 4090
|
||||
launcher_visible_gpu_uuid: GPU-79f3a85d-adce-7833-7003-6488140ca596
|
||||
mixed_multi_gpu: false
|
||||
|
||||
model:
|
||||
integrator: forward_euler
|
||||
dt_ms: 0.1
|
||||
membrane_tau_ms: 20.0
|
||||
synaptic_tau_ms: 5.0
|
||||
refractory_ms: 2.2
|
||||
resting_potential_mv: -52.0
|
||||
reset_potential_mv: -52.0
|
||||
threshold_mv: -45.0
|
||||
synaptic_delay_ms: 1.8
|
||||
parameter_provenance: FlyWire-inspired 2024 LIF baseline; not MaleCNS measured single-cell parameters
|
||||
|
||||
sign_policy:
|
||||
neurotransmitter_sign:
|
||||
acetylcholine: 1
|
||||
gaba: -1
|
||||
glutamate: -1
|
||||
dopamine: 1
|
||||
octopamine: 1
|
||||
serotonin: 1
|
||||
histamine: -1
|
||||
histamine_policy_note: explicitly configured inhibitory baseline assumption; not silently inferred
|
||||
unknown_sign: 0
|
||||
unclear_sign: 0
|
||||
unmapped_policy: error
|
||||
unmapped_sign: 0
|
||||
|
||||
weights:
|
||||
transform: sqrt
|
||||
supported_transforms: [linear, sqrt, log1p]
|
||||
scale: 0.05
|
||||
chunk_edges: 2000000
|
||||
units: model synaptic-state increment per presynaptic spike
|
||||
formula: effective_weight = presynaptic_sign * scale * f(raw_synapse_count)
|
||||
preserve_raw_int64: true
|
||||
|
||||
stimulations:
|
||||
single_neuron:
|
||||
kind: single_neuron_pulse
|
||||
neuron_idx: 0
|
||||
start_ms: 2.0
|
||||
duration_ms: 1.0
|
||||
amplitude_mv_equivalent: 400.0
|
||||
known_type_AMMC005:
|
||||
kind: known_type_pulse
|
||||
type_name: AMMC005
|
||||
max_neurons: 12
|
||||
start_ms: 5.0
|
||||
duration_ms: 1.0
|
||||
amplitude_mv_equivalent: 400.0
|
||||
sparse_poisson:
|
||||
kind: sparse_poisson
|
||||
target_fraction: 0.001
|
||||
rate_hz: 20.0
|
||||
event_amplitude_mv_equivalent: 1600.0
|
||||
|
||||
smoke_tests:
|
||||
- name: smoke-10ms-single-neuron
|
||||
duration_ms: 10.0
|
||||
stimulation: single_neuron
|
||||
seed: 41001
|
||||
- name: smoke-100ms-known-type
|
||||
duration_ms: 100.0
|
||||
stimulation: known_type_AMMC005
|
||||
seed: 41002
|
||||
- name: smoke-1s-sparse-poisson
|
||||
duration_ms: 1000.0
|
||||
stimulation: sparse_poisson
|
||||
seed: 41003
|
||||
|
||||
cpu_gpu_reference:
|
||||
seed: 41100
|
||||
subgraph_neurons: 512
|
||||
steps: 100
|
||||
pulse_start_step: 5
|
||||
pulse_steps: 10
|
||||
pulse_neurons: 4
|
||||
pulse_amplitude_mv_equivalent: 400.0
|
||||
absolute_tolerance: 1.0e-10
|
||||
|
||||
validation:
|
||||
finite_check_interval_steps: 100
|
||||
explosive_step_fraction: 0.1
|
||||
memory_growth_tolerance_bytes: 10485760
|
||||
|
||||
output:
|
||||
spike_representation: event_table_timestamp_ms_neuron_idx
|
||||
dense_time_by_neuron_forbidden: true
|
||||
rate_window_ms: 10.0
|
||||
|
||||
reproducibility:
|
||||
seed: 41000
|
||||
deterministic_synthetic_inputs: true
|
||||
@@ -0,0 +1,415 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stable game-agnostic Brain I/O API for MaleCNS-LIF-runtime-v1."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
import numpy as np
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
|
||||
API_VERSION = "1.0.0"
|
||||
INPUT_BATCH_VERSION = "1.0.0"
|
||||
ACTIVITY_STREAM_VERSION = "1.0.0"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InputEvent:
|
||||
target_neuron_idx: np.ndarray
|
||||
stimulation_mode: str
|
||||
value_kind: str
|
||||
value: float | np.ndarray
|
||||
start_ms: float
|
||||
duration_ms: float
|
||||
source_id: str
|
||||
channel_id: str
|
||||
amplitude_mv: float = 0.0
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def validate(self, neuron_count: int, dt_ms: float) -> None:
|
||||
target = np.asarray(self.target_neuron_idx)
|
||||
if target.ndim != 1 or target.dtype.kind not in "iu":
|
||||
raise ValueError("target_neuron_idx must be a one-dimensional integer array")
|
||||
if len(target) and (int(target.min()) < 0 or int(target.max()) >= neuron_count):
|
||||
raise ValueError("target neuron_idx outside canonical range")
|
||||
if len(np.unique(target)) != len(target):
|
||||
raise ValueError("target neuron_idx values must be unique")
|
||||
if self.stimulation_mode not in {"spike_injection", "external_synaptic_drive"}:
|
||||
raise ValueError(f"unsupported stimulation_mode {self.stimulation_mode!r}")
|
||||
if self.value_kind not in {"binary", "scalar_intensity", "poisson_rate_hz", "signed_modulation"}:
|
||||
raise ValueError(f"unsupported value_kind {self.value_kind!r}")
|
||||
if self.start_ms < 0 or self.duration_ms <= 0:
|
||||
raise ValueError("timestamps/duration must be non-negative/positive")
|
||||
for x in (self.start_ms, self.duration_ms):
|
||||
if not math.isclose(x / dt_ms, round(x / dt_ms), abs_tol=1e-8):
|
||||
raise ValueError(f"{x} ms is not aligned to dt={dt_ms} ms")
|
||||
value = np.asarray(self.value)
|
||||
if value.ndim > 1 or (value.ndim == 1 and len(value) not in (1, len(target))):
|
||||
raise ValueError("value must be scalar or match target count")
|
||||
if self.value_kind == "poisson_rate_hz" and np.any(value < 0):
|
||||
raise ValueError("Poisson rate cannot be negative")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InputBatch:
|
||||
events: tuple[InputEvent, ...]
|
||||
batch_id: str
|
||||
version: str = INPUT_BATCH_VERSION
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def validate(self, neuron_count: int, dt_ms: float) -> None:
|
||||
if self.version != INPUT_BATCH_VERSION:
|
||||
raise ValueError(f"InputBatch version must be {INPUT_BATCH_VERSION}")
|
||||
for event in self.events:
|
||||
event.validate(neuron_count, dt_ms)
|
||||
|
||||
|
||||
class VisualAdapterV1:
|
||||
"""Maps abstract channels to audited visual hierarchy populations."""
|
||||
|
||||
def __init__(self, config: dict[str, Any], phase4c_root: Path):
|
||||
self.config = config
|
||||
self.phase4c_root = phase4c_root
|
||||
self.modes: dict[str, np.ndarray] = {}
|
||||
for mode, spec in config["visual_adapter"]["modes"].items():
|
||||
population = spec["hierarchy_population"]
|
||||
path = phase4c_root / "visual-audit" / f"hierarchy-{population}.parquet"
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(path)
|
||||
self.modes[mode] = pq.read_table(path, columns=["neuron_idx"])["neuron_idx"].to_numpy().astype(np.int64)
|
||||
|
||||
def population(self, mode: str) -> np.ndarray:
|
||||
if mode not in self.modes:
|
||||
raise KeyError(mode)
|
||||
return self.modes[mode].copy()
|
||||
|
||||
def encode(
|
||||
self, mode: str, channel_values: np.ndarray, start_ms: float, duration_ms: float,
|
||||
source_id: str, channel_id: str, value_kind: str = "poisson_rate_hz",
|
||||
) -> InputBatch:
|
||||
if mode not in self.modes:
|
||||
raise KeyError(mode)
|
||||
spec = self.config["visual_adapter"]["modes"][mode]
|
||||
targets = self.modes[mode]
|
||||
values = np.asarray(channel_values, dtype=np.float32)
|
||||
if values.shape != (len(targets),):
|
||||
raise ValueError(f"visual channel must have shape {(len(targets),)}, got {values.shape}")
|
||||
selected = np.flatnonzero(values != 0).astype(np.int64)
|
||||
target = targets[selected]
|
||||
selected_values = values[selected]
|
||||
event = InputEvent(
|
||||
target_neuron_idx=target,
|
||||
stimulation_mode=spec["input_mode"],
|
||||
value_kind=value_kind,
|
||||
value=selected_values,
|
||||
amplitude_mv=float(self.config["visual_adapter"]["external_drive_mv"]),
|
||||
start_ms=start_ms,
|
||||
duration_ms=duration_ms,
|
||||
source_id=source_id,
|
||||
channel_id=channel_id,
|
||||
metadata={
|
||||
"adapter": self.config["names"]["visual_adapter"],
|
||||
"adapter_mode": mode,
|
||||
"biological_fidelity": spec["biological_fidelity"],
|
||||
"connectome_weights_modified": False,
|
||||
},
|
||||
)
|
||||
return InputBatch(events=(event,), batch_id=f"{source_id}:{channel_id}:{start_ms}", metadata={"adapter_mode": mode})
|
||||
|
||||
|
||||
class BrainRuntime:
|
||||
"""Persistent exact-state BrainRuntime API. It has no game-specific dependency."""
|
||||
|
||||
def __init__(self, config_path: Path, extension_build_dir: Path):
|
||||
self.config_path = config_path.resolve()
|
||||
self.cfg = yaml.safe_load(self.config_path.read_text(encoding="utf-8"))
|
||||
self.dt = float(self.cfg["model"]["dt_ms"])
|
||||
self.n = 166700
|
||||
self.model_root = Path(self.cfg["paths"]["model_root"])
|
||||
self.runtime_root = Path(self.cfg["paths"]["runtime_root"])
|
||||
self.input_root = Path(self.cfg["paths"]["connectome_root"])
|
||||
runtime_module_dir = str(self.runtime_root)
|
||||
if runtime_module_dir not in sys.path:
|
||||
sys.path.insert(0, runtime_module_dir)
|
||||
import phase4c as runtime_module
|
||||
self.runtime_module = runtime_module
|
||||
p4cfg = yaml.safe_load((self.runtime_root / "phase4c.yaml").read_text(encoding="utf-8"))
|
||||
self.paths = runtime_module.exact_paths(p4cfg)
|
||||
self.indptr_np, self.indices_np, self.raw_np = runtime_module.p4b.validate_graph(p4cfg, self.paths)
|
||||
self.table = runtime_module.full_metadata(self.paths["metadata"])
|
||||
self.population_masks = runtime_module.populations(self.table)
|
||||
if not torch.cuda.is_available() or torch.cuda.device_count() != 1:
|
||||
raise RuntimeError("BrainRuntime requires exactly one visible CUDA GPU")
|
||||
self.device = torch.device("cuda:0")
|
||||
props = torch.cuda.get_device_properties(self.device)
|
||||
if self.cfg["device"]["required_name_substring"] not in props.name:
|
||||
raise RuntimeError(f"required RTX 4090, got {props.name}")
|
||||
extension_build_dir.mkdir(parents=True, exist_ok=True)
|
||||
build_experiment_root = extension_build_dir.parent.parent
|
||||
(build_experiment_root / "compiler-reports/extension-build").mkdir(parents=True, exist_ok=True)
|
||||
self.extension, report = runtime_module.build_extension(self.runtime_root, build_experiment_root)
|
||||
if self.extension is None:
|
||||
raise RuntimeError(f"runtime-v1 extension unavailable: {report}")
|
||||
base = runtime_module.Runtime(p4cfg, self.table, self.indptr_np, self.indices_np, self.raw_np, self.device, self.extension)
|
||||
self.indptr, self.indices, self.weights = base.indptr, base.indices, base.weights
|
||||
self.signs, self.superclass = base.signs, base.superclass
|
||||
self.neuron_type = np.asarray(self.table["annotation_type"].to_pylist(), dtype=object)
|
||||
self.neuron_class = np.asarray(self.table["annotation_class"].to_pylist(), dtype=object)
|
||||
self.body_id = self.table["body_id"].to_numpy().astype(np.uint64)
|
||||
m = self.cfg["model"]
|
||||
self.delay_steps = int(round(float(m["synaptic_delay_ms"]) / self.dt))
|
||||
self.queue_len = self.delay_steps + 1
|
||||
self.refractory_steps = int(round(float(m["refractory_ms"]) / self.dt))
|
||||
self.alpha = self.dt / float(m["membrane_tau_ms"])
|
||||
self.g_decay = 1.0 - self.dt / float(m["synaptic_tau_ms"])
|
||||
self.rest = float(m["resting_potential_mv"])
|
||||
self.reset_value = float(m["reset_potential_mv"])
|
||||
self.threshold = float(m["threshold_mv"])
|
||||
self.output_populations = self._output_populations()
|
||||
self.reset(0)
|
||||
|
||||
def _output_populations(self) -> dict[str, np.ndarray]:
|
||||
s = self.superclass
|
||||
return {
|
||||
"descending": np.flatnonzero(np.isin(s, ["descending_neuron", "descending_neuron_tbc"])).astype(np.int32),
|
||||
"motor_related": np.flatnonzero(np.isin(s, ["vnc_motor", "cb_motor"])).astype(np.int32),
|
||||
"ascending": np.flatnonzero(np.isin(s, ["ascending_neuron", "sensory_ascending", "sensory_ascending_tbc", "efferent_ascending"])).astype(np.int32),
|
||||
}
|
||||
|
||||
def reset(self, seed: int, initial_state: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
self.seed = int(seed)
|
||||
self.generator = torch.Generator(device=self.device); self.generator.manual_seed(self.seed)
|
||||
self.v = torch.full((self.n,), self.rest, dtype=torch.float32, device=self.device)
|
||||
self.g = torch.zeros(self.n, dtype=torch.float32, device=self.device)
|
||||
self.refractory = torch.zeros(self.n, dtype=torch.int32, device=self.device)
|
||||
self.delay_queue = torch.zeros((self.queue_len, self.n), dtype=torch.float32, device=self.device)
|
||||
self.spike_counts = torch.zeros(self.n, dtype=torch.int32, device=self.device)
|
||||
self.simulation_step = 0; self.sequence_id = 0
|
||||
self.spike_steps: list[np.ndarray] = []; self.spike_nodes: list[np.ndarray] = []
|
||||
self.spike_origins: list[np.ndarray] = []
|
||||
self.drive_steps: list[np.ndarray] = []; self.drive_nodes: list[np.ndarray] = []
|
||||
self.drive_values: list[np.ndarray] = []; self.input_audit: list[dict[str, Any]] = []
|
||||
if initial_state is not None:
|
||||
self.restore_state(initial_state)
|
||||
return self.get_state_summary()
|
||||
|
||||
@property
|
||||
def simulation_time_ms(self) -> float:
|
||||
return self.simulation_step * self.dt
|
||||
|
||||
def _event_values(self, event: InputEvent, targets: torch.Tensor) -> torch.Tensor:
|
||||
value = np.asarray(event.value, dtype=np.float32)
|
||||
if value.ndim == 0:
|
||||
return torch.full((len(targets),), float(value), dtype=torch.float32, device=self.device)
|
||||
return torch.as_tensor(value, dtype=torch.float32, device=self.device)
|
||||
|
||||
def _active(self, event: InputEvent, time_ms: float) -> bool:
|
||||
return event.start_ms - 1e-9 <= time_ms < event.start_ms + event.duration_ms - 1e-9
|
||||
|
||||
def _hybrid_edges(self, sources: torch.Tensor) -> torch.Tensor:
|
||||
starts = self.indptr[sources]
|
||||
degrees = self.indptr[sources + 1] - starts
|
||||
cumulative = torch.cumsum(degrees, 0)
|
||||
total = int(cumulative[-1].item()) if cumulative.numel() else 0
|
||||
output = torch.empty(total, dtype=torch.int64, device=self.device)
|
||||
if total:
|
||||
self.extension.fill_edges(self.indptr, sources, cumulative - degrees, output)
|
||||
return output
|
||||
|
||||
def step(self, input_batch: InputBatch | None, biological_ms: float) -> dict[str, Any]:
|
||||
steps = int(round(float(biological_ms) / self.dt))
|
||||
if not math.isclose(steps * self.dt, float(biological_ms), abs_tol=1e-8) or steps <= 0:
|
||||
raise ValueError("biological_ms must be a positive multiple of dt")
|
||||
batch = input_batch or InputBatch(events=(), batch_id="zero-input")
|
||||
batch.validate(self.n, self.dt)
|
||||
batch_start = self.simulation_time_ms
|
||||
prepared = [(e, torch.as_tensor(e.target_neuron_idx, dtype=torch.int64, device=self.device)) for e in batch.events]
|
||||
for event in batch.events:
|
||||
self.input_audit.append({
|
||||
"batch_id": batch.batch_id, "source_id": event.source_id, "channel_id": event.channel_id,
|
||||
"mode": event.stimulation_mode, "value_kind": event.value_kind,
|
||||
"start_ms": event.start_ms, "duration_ms": event.duration_ms,
|
||||
"target_count": int(len(event.target_neuron_idx)), "metadata": event.metadata,
|
||||
"external_drive_is_connectome_transmission": False,
|
||||
})
|
||||
start_event_count = sum(len(x) for x in self.spike_nodes)
|
||||
start_drive_count = sum(len(x) for x in self.drive_nodes)
|
||||
for _ in range(steps):
|
||||
global_step = self.simulation_step; time_ms = self.simulation_time_ms
|
||||
slot = global_step % self.queue_len
|
||||
self.g.mul_(self.g_decay).add_(self.delay_queue[slot]); self.delay_queue[slot].zero_()
|
||||
spike_requests: list[tuple[InputEvent, torch.Tensor, torch.Tensor]] = []
|
||||
for event, targets in prepared:
|
||||
if not self._active(event, time_ms) or targets.numel() == 0:
|
||||
continue
|
||||
values = self._event_values(event, targets)
|
||||
if event.value_kind == "poisson_rate_hz":
|
||||
selected = torch.rand(len(targets), generator=self.generator, device=self.device) < (values * self.dt / 1000.0)
|
||||
elif event.value_kind == "binary":
|
||||
selected = values != 0
|
||||
elif event.value_kind == "scalar_intensity":
|
||||
selected = values != 0
|
||||
else: # signed_modulation
|
||||
selected = torch.rand(len(targets), generator=self.generator, device=self.device) < (torch.abs(values) * self.dt / 1000.0)
|
||||
if event.stimulation_mode == "external_synaptic_drive":
|
||||
chosen = targets[selected]
|
||||
if chosen.numel():
|
||||
signed_value = values[selected] * float(event.amplitude_mv)
|
||||
if event.value_kind == "poisson_rate_hz":
|
||||
signed_value = torch.full_like(signed_value, float(event.amplitude_mv))
|
||||
self.g.index_add_(0, chosen, signed_value)
|
||||
node_np = chosen.cpu().numpy().astype(np.int32, copy=True)
|
||||
val_np = signed_value.cpu().numpy().astype(np.float32, copy=True)
|
||||
self.drive_steps.append(np.full(len(node_np), global_step, dtype=np.int32))
|
||||
self.drive_nodes.append(node_np); self.drive_values.append(val_np)
|
||||
else:
|
||||
spike_requests.append((event, targets, selected))
|
||||
self.refractory.clamp_min_(1).sub_(1)
|
||||
available = self.refractory == 0
|
||||
self.v[available] += self.alpha * (self.g[available] - (self.v[available] - self.rest))
|
||||
self.v[~available] = self.reset_value
|
||||
natural = available & (self.v >= self.threshold)
|
||||
spikes = natural.clone()
|
||||
injected = torch.zeros(self.n, dtype=torch.bool, device=self.device)
|
||||
for _event, targets, selected in spike_requests:
|
||||
accepted = selected & available[targets]
|
||||
chosen = targets[accepted]
|
||||
if chosen.numel():
|
||||
spikes[chosen] = True; injected[chosen] = ~natural[chosen]
|
||||
sources = torch.nonzero(spikes, as_tuple=False).flatten()
|
||||
self.spike_counts.add_(spikes.to(torch.int32))
|
||||
if sources.numel():
|
||||
self.v[sources] = self.reset_value; self.refractory[sources] = self.refractory_steps
|
||||
edge_ids = self._hybrid_edges(sources)
|
||||
if edge_ids.numel():
|
||||
self.delay_queue[(global_step + self.delay_steps) % self.queue_len].scatter_add_(0, self.indices[edge_ids].long(), self.weights[edge_ids])
|
||||
src_np = sources.cpu().numpy().astype(np.int32, copy=True)
|
||||
origin_np = np.where(injected[sources].cpu().numpy(), "external_spike_injection", "network_spike")
|
||||
self.spike_steps.append(np.full(len(src_np), global_step, dtype=np.int32))
|
||||
self.spike_nodes.append(src_np); self.spike_origins.append(origin_np)
|
||||
self.simulation_step += 1
|
||||
torch.cuda.synchronize()
|
||||
self.sequence_id += 1
|
||||
return {
|
||||
"api_version": API_VERSION, "sequence_id": self.sequence_id,
|
||||
"batch_id": batch.batch_id, "start_time_ms": batch_start,
|
||||
"end_time_ms": self.simulation_time_ms, "biological_ms": biological_ms,
|
||||
"new_spikes": sum(len(x) for x in self.spike_nodes) - start_event_count,
|
||||
"new_external_drive_events": sum(len(x) for x in self.drive_nodes) - start_drive_count,
|
||||
}
|
||||
|
||||
def _events(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
steps = np.concatenate(self.spike_steps) if self.spike_steps else np.empty(0, np.int32)
|
||||
nodes = np.concatenate(self.spike_nodes) if self.spike_nodes else np.empty(0, np.int32)
|
||||
origins = np.concatenate(self.spike_origins) if self.spike_origins else np.empty(0, object)
|
||||
return steps, nodes, origins
|
||||
|
||||
def read_spikes(self, window_ms: float | None = None) -> dict[str, Any]:
|
||||
steps, nodes, origins = self._events()
|
||||
if window_ms is not None:
|
||||
threshold = self.simulation_step - int(round(window_ms / self.dt))
|
||||
mask = steps >= threshold; steps, nodes, origins = steps[mask], nodes[mask], origins[mask]
|
||||
return {
|
||||
"simulation_time_ms": self.simulation_time_ms,
|
||||
"step": steps, "timestamp_ms": steps.astype(np.float64) * self.dt,
|
||||
"neuron_idx": nodes, "origin": origins,
|
||||
}
|
||||
|
||||
def read_population(self, population_id: str, window_ms: float) -> dict[str, Any]:
|
||||
if population_id not in self.output_populations:
|
||||
raise KeyError(population_id)
|
||||
members = self.output_populations[population_id]
|
||||
events = self.read_spikes(window_ms)
|
||||
mask = np.isin(events["neuron_idx"], members)
|
||||
nodes = events["neuron_idx"][mask]
|
||||
counts = np.zeros(len(members), dtype=np.int32)
|
||||
if len(nodes):
|
||||
lookup = np.full(self.n, -1, dtype=np.int32); lookup[members] = np.arange(len(members), dtype=np.int32)
|
||||
np.add.at(counts, lookup[nodes], 1)
|
||||
seconds = window_ms / 1000.0
|
||||
return {
|
||||
"simulation_time_ms": self.simulation_time_ms, "population_id": population_id,
|
||||
"neuron_idx": members.copy(), "spike_counts": counts,
|
||||
"firing_rate_hz": counts.astype(np.float32) / seconds,
|
||||
"active_mask": counts > 0, "aggregate_rate_hz": float(counts.sum() / (len(members) * seconds)),
|
||||
"total_spikes": int(counts.sum()), "active_neurons": int(np.count_nonzero(counts)),
|
||||
}
|
||||
|
||||
def read_output(self, output_spec: dict[str, Any], window_ms: float) -> dict[str, Any]:
|
||||
population = output_spec.get("population", "descending")
|
||||
aggregation = output_spec.get("aggregation", "per-neuron")
|
||||
base = self.read_population(population, window_ms)
|
||||
members = base["neuron_idx"]; counts = base["spike_counts"]
|
||||
if aggregation == "per-neuron":
|
||||
return base
|
||||
if aggregation not in {"per-type", "per-class"}:
|
||||
raise ValueError("aggregation must be per-neuron, per-type, or per-class")
|
||||
labels = self.neuron_type if aggregation == "per-type" else self.neuron_class
|
||||
result: dict[str, dict[str, float | int]] = {}
|
||||
for label in sorted({"<null>" if labels[i] is None else str(labels[i]) for i in members}):
|
||||
mask = np.asarray([("<null>" if labels[i] is None else str(labels[i])) == label for i in members])
|
||||
result[label] = {"neuron_count": int(mask.sum()), "spike_count": int(counts[mask].sum()), "mean_rate_hz": float(base["firing_rate_hz"][mask].mean())}
|
||||
return {"simulation_time_ms": self.simulation_time_ms, "population_id": population, "aggregation": aggregation, "features": result}
|
||||
|
||||
def fixed_temporal_bins(self, population_id: str, bin_ms: float, window_ms: float) -> dict[str, Any]:
|
||||
if bin_ms not in self.cfg["readout"]["bins_ms"]:
|
||||
raise ValueError("unsupported fixed bin")
|
||||
ev = self.read_spikes(window_ms); members = self.output_populations[population_id]
|
||||
mask = np.isin(ev["neuron_idx"], members); times = ev["timestamp_ms"][mask]
|
||||
start = self.simulation_time_ms - window_ms
|
||||
edges = np.arange(start, self.simulation_time_ms + bin_ms + 1e-9, bin_ms)
|
||||
counts, _ = np.histogram(times, bins=edges)
|
||||
return {"simulation_time_ms": self.simulation_time_ms, "population_id": population_id, "bin_ms": bin_ms, "bin_start_ms": edges[:-1], "spike_counts": counts.astype(np.int32)}
|
||||
|
||||
def get_state_summary(self) -> dict[str, Any]:
|
||||
return {
|
||||
"api_version": API_VERSION, "runtime": self.cfg["names"]["runtime"],
|
||||
"simulation_step": self.simulation_step, "simulation_time_ms": self.simulation_time_ms,
|
||||
"sequence_id": self.sequence_id, "seed": self.seed,
|
||||
"neurons": self.n, "edges": int(self.indices.numel()),
|
||||
"active_neurons_total": int(torch.count_nonzero(self.spike_counts).item()),
|
||||
"spikes_total": int(self.spike_counts.sum().item()),
|
||||
"external_drive_events_total": sum(len(x) for x in self.drive_nodes),
|
||||
}
|
||||
|
||||
def checkpoint_state(self) -> dict[str, Any]:
|
||||
return {
|
||||
"api_version": API_VERSION, "seed": self.seed, "simulation_step": self.simulation_step,
|
||||
"sequence_id": self.sequence_id, "generator_state": self.generator.get_state().cpu(),
|
||||
"v": self.v.clone(), "g": self.g.clone(), "refractory": self.refractory.clone(),
|
||||
"delay_queue": self.delay_queue.clone(), "spike_counts": self.spike_counts.clone(),
|
||||
"spike_steps": copy.deepcopy(self.spike_steps), "spike_nodes": copy.deepcopy(self.spike_nodes),
|
||||
"spike_origins": copy.deepcopy(self.spike_origins), "drive_steps": copy.deepcopy(self.drive_steps),
|
||||
"drive_nodes": copy.deepcopy(self.drive_nodes), "drive_values": copy.deepcopy(self.drive_values),
|
||||
"input_audit": copy.deepcopy(self.input_audit),
|
||||
}
|
||||
|
||||
def restore_state(self, state: dict[str, Any]) -> dict[str, Any]:
|
||||
if state.get("api_version") != API_VERSION:
|
||||
raise ValueError("checkpoint API version mismatch")
|
||||
self.seed = int(state["seed"]); self.simulation_step = int(state["simulation_step"]); self.sequence_id = int(state["sequence_id"])
|
||||
self.generator.set_state(state["generator_state"].cpu())
|
||||
for name in ("v", "g", "refractory", "delay_queue", "spike_counts"):
|
||||
setattr(self, name, state[name].clone().to(self.device))
|
||||
for name in ("spike_steps", "spike_nodes", "spike_origins", "drive_steps", "drive_nodes", "drive_values", "input_audit"):
|
||||
setattr(self, name, copy.deepcopy(state[name]))
|
||||
return self.get_state_summary()
|
||||
|
||||
def activity_packet(self, window_ms: float, input_metadata: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ev = self.read_spikes(window_ms)
|
||||
aggregates = {name: {k: v for k, v in self.read_population(name, window_ms).items() if k in ("active_neurons", "total_spikes", "aggregate_rate_hz")} for name in self.output_populations}
|
||||
return {
|
||||
"contract_version": ACTIVITY_STREAM_VERSION, "simulation_time_ms": self.simulation_time_ms,
|
||||
"sequence_id": self.sequence_id, "window_ms": window_ms,
|
||||
"spike_neuron_idx": ev["neuron_idx"].tolist(), "spike_timestamp_ms": ev["timestamp_ms"].tolist(),
|
||||
"population_aggregates": aggregates, "active_neuron_count": int(len(np.unique(ev["neuron_idx"]))),
|
||||
"input_channel_metadata": input_metadata or {}, "representation": "event-only",
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"file_count": 11,
|
||||
"files": [
|
||||
{
|
||||
"bytes": 353,
|
||||
"path": "VERSION.json",
|
||||
"sha256": "6d5cf119bde4963fe78b1407ff1452e42d0cf70168661d60cc52bdf5a61b8667"
|
||||
},
|
||||
{
|
||||
"bytes": 13925,
|
||||
"path": "phase4n.py",
|
||||
"sha256": "dbbb49447aecbc862d3f54bba9de41f6b8ab90a6afb1ef43a686149fce832d13"
|
||||
},
|
||||
{
|
||||
"bytes": 1712,
|
||||
"path": "phase4n.yaml",
|
||||
"sha256": "6e1b85fc923c7c969df0a7002557256cb719106f8a028c2c506c5ccfe0edd2b2"
|
||||
},
|
||||
{
|
||||
"bytes": 6158,
|
||||
"path": "phase4n_alldan.py",
|
||||
"sha256": "df12477905d7da70e0e337a8d4408cc247135e7876ac7b197e1997f2c4611a54"
|
||||
},
|
||||
{
|
||||
"bytes": 3237,
|
||||
"path": "phase4n_control_whole.py",
|
||||
"sha256": "560ebffba3e8c3457827a0a682476d125a2bbef81958f5b621d32c1203aada1e"
|
||||
},
|
||||
{
|
||||
"bytes": 7198,
|
||||
"path": "phase4n_controls.py",
|
||||
"sha256": "229e48aa63a725e65202dfbabec58d9a092822482c6d0b461ab5a72ee323a901"
|
||||
},
|
||||
{
|
||||
"bytes": 17592,
|
||||
"path": "phase4n_events.py",
|
||||
"sha256": "1fd10326d9e9e4dd6e78b71a6a7c784058c51119a3a5523d4fe50550cd23bbda"
|
||||
},
|
||||
{
|
||||
"bytes": 20496,
|
||||
"path": "phase4n_finalize.py",
|
||||
"sha256": "2ed9b2a7282f0319238646d2c9be0533650a0146292c2de8201af27bb9cb2e5c"
|
||||
},
|
||||
{
|
||||
"bytes": 16993,
|
||||
"path": "phase4n_finish.py",
|
||||
"sha256": "690d33c18d6438d11050ba1fdee9a8015c6bc25b7c85f78cb56f4d06820e35a7"
|
||||
},
|
||||
{
|
||||
"bytes": 6839,
|
||||
"path": "phase4n_paths.py",
|
||||
"sha256": "41554ae9b712b1710a3e8559e86e01e2aa5d95ca0f88fcaf93a4a5baa8dc3877"
|
||||
},
|
||||
{
|
||||
"bytes": 757,
|
||||
"path": "phase4n_preflight.py",
|
||||
"sha256": "aa339208407f4dd00282201ce362ea06fa042f3115e57483670196c6b3c3d642"
|
||||
}
|
||||
],
|
||||
"generated_utc": "2026-09-21T10:57:20.327961+00:00",
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/credit-assignment-v1",
|
||||
"total_bytes": 95260
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"candidate_generated": false,
|
||||
"config_sha256": "6e1b85fc923c7c969df0a7002557256cb719106f8a028c2c506c5ccfe0edd2b2",
|
||||
"frozen_dynamic_regression": false,
|
||||
"frozen_static_integrity": true,
|
||||
"identity_sha256": "aa63eacdfe4f63751a3bdf1efe3071a69d07babcd64b479aa91c9f3b512cd45d",
|
||||
"phase": "4N",
|
||||
"status": "PARTIAL-MECHANISM-AUDIT-NO-CANDIDATE"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
8782baf95df0bc5d4a17e1ecca99fd7f3af55a25a7619b8ae5028d22c70b0417 MANIFEST.json
|
||||
6d5cf119bde4963fe78b1407ff1452e42d0cf70168661d60cc52bdf5a61b8667 VERSION.json
|
||||
dbbb49447aecbc862d3f54bba9de41f6b8ab90a6afb1ef43a686149fce832d13 phase4n.py
|
||||
6e1b85fc923c7c969df0a7002557256cb719106f8a028c2c506c5ccfe0edd2b2 phase4n.yaml
|
||||
df12477905d7da70e0e337a8d4408cc247135e7876ac7b197e1997f2c4611a54 phase4n_alldan.py
|
||||
560ebffba3e8c3457827a0a682476d125a2bbef81958f5b621d32c1203aada1e phase4n_control_whole.py
|
||||
229e48aa63a725e65202dfbabec58d9a092822482c6d0b461ab5a72ee323a901 phase4n_controls.py
|
||||
1fd10326d9e9e4dd6e78b71a6a7c784058c51119a3a5523d4fe50550cd23bbda phase4n_events.py
|
||||
2ed9b2a7282f0319238646d2c9be0533650a0146292c2de8201af27bb9cb2e5c phase4n_finalize.py
|
||||
690d33c18d6438d11050ba1fdee9a8015c6bc25b7c85f78cb56f4d06820e35a7 phase4n_finish.py
|
||||
41554ae9b712b1710a3e8559e86e01e2aa5d95ca0f88fcaf93a4a5baa8dc3877 phase4n_paths.py
|
||||
aa339208407f4dd00282201ce362ea06fa042f3115e57483670196c6b3c3d642 phase4n_preflight.py
|
||||
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4N read-only reconstruction of frozen Phase 4M acquisition and replay."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yaml
|
||||
|
||||
from phase4h_audit import sha256, verify_checksum, write_json
|
||||
from phase4i import Validator
|
||||
from phase4k import seal
|
||||
from phase4m import ReplayEngine, compare_A_B, frozen_checks, frozen_hashes, load_odor_inputs, replay_effect
|
||||
from phase4l import OdorSimulator, pn_drive
|
||||
from phase4j import effect
|
||||
|
||||
|
||||
GROUPS = ("A-specific", "shared", "B-specific", "inactive")
|
||||
|
||||
|
||||
def load_config(path: Path):
|
||||
ncfg = yaml.safe_load(path.read_text())
|
||||
mpath = Path(ncfg["frozen_phase4m_config"])
|
||||
mcfg = yaml.safe_load(mpath.read_text())
|
||||
lpath = Path(mcfg["frozen_phase4l_config"])
|
||||
cfg = yaml.safe_load(lpath.read_text())
|
||||
cfg["paths"]["phase4l_root"] = cfg["paths"]["experiment_root"]
|
||||
cfg["paths"]["phase4l_model_root"] = str(lpath.parent)
|
||||
return ncfg, mcfg, cfg, lpath
|
||||
|
||||
|
||||
def prepare(ncfg, mcfg, cfg, exp: Path):
|
||||
if exp.exists() and (exp / "edge-identity/definition.json").exists():
|
||||
raise RuntimeError("Phase 4N identity/definition already frozen; refusing to overwrite")
|
||||
previous = frozen_checks(cfg)
|
||||
mroot = Path(mcfg["experiment_root"])
|
||||
mmodel = Path(mcfg["model_root"])
|
||||
for root, rel in ((mroot, "manifests/checksums.sha256"), (mmodel, "checksums.sha256")):
|
||||
check = verify_checksum(root, root / rel)
|
||||
if not check["passed"]:
|
||||
raise RuntimeError(f"Phase 4M frozen checksum failed: {root}")
|
||||
for sub in ("edge-identity", "acquisition-trajectory", "specificity-curve", "dan-source", "endogenous-dan", "local-eligibility", "shared-edge", "b-contamination", "isolated", "whole-brain", "controls", "stopping-analysis", "performance", "regression", "audit", "manifests"):
|
||||
(exp / sub).mkdir(parents=True, exist_ok=True)
|
||||
src = Path(cfg["paths"]["phase4l_root"]) / "eligible-edges/edge-identity.parquet"
|
||||
identity = pd.read_parquet(src)
|
||||
required = {"edge_id", "source_neuron_idx", "source_body_id", "target_neuron_idx", "target_body_id", "raw_synapse_count", "target_compartment_verbatim", "odor_eligibility_category"}
|
||||
if not required.issubset(identity.columns) or len(identity) != 14232:
|
||||
raise RuntimeError("Phase 4L identity schema/size mismatch")
|
||||
if set(identity.odor_eligibility_category.astype(str)) != set(GROUPS):
|
||||
raise RuntimeError("identity groups differ from four frozen groups")
|
||||
identity["anatomical_weight_mv"] = identity.raw_synapse_count.astype(np.float32) * np.float32(cfg["model"]["w_syn_mv_per_anatomical_synapse"])
|
||||
identity["initial_E"] = np.float32(1.0)
|
||||
identity.rename(columns={"odor_eligibility_category": "identity_group", "target_compartment_verbatim": "compartment"}, inplace=True)
|
||||
out = exp / "edge-identity/frozen-edge-identity.parquet"
|
||||
identity.to_parquet(out, index=False)
|
||||
definition = {"source": str(src), "source_sha256": sha256(src), "source_semantics": ncfg["edge_identity_source"],
|
||||
"group_counts": identity.identity_group.value_counts().to_dict(), "edge_count": len(identity),
|
||||
"checkpoint_rule": ncfg["checkpoints"], "CSI": ncfg["credit_specificity_index"],
|
||||
"functional_separation": ncfg["functional_separation"], "peak_rule": ncfg["peak_rule"],
|
||||
"generalization_onset_rule": ncfg["generalization_onset_rule"], "frozen_before_learning": True,
|
||||
"phase4m_experiment_checksum_sha256": sha256(mroot / "manifests/checksums.sha256"),
|
||||
"phase4m_model_checksum_sha256": sha256(mmodel / "checksums.sha256"), "prior_checksum_passed": previous["all_passed"]}
|
||||
write_json(exp / "edge-identity/definition.json", definition)
|
||||
shutil.copy2(Path(__file__).resolve(), exp / "audit/phase4n.py")
|
||||
return identity, definition
|
||||
|
||||
|
||||
def reconstruct(ncfg, mcfg, cfg, exp: Path, identity: pd.DataFrame):
|
||||
hist = pd.read_parquet(Path(mcfg["experiment_root"]) / "efficacy-overlay/update-history.parquet")
|
||||
required = {"train_seed", "pairing", "edge_id", "E_bounded_before", "E_bounded_after", "E_multiplicative_before", "E_multiplicative_after", "eligibility"}
|
||||
if not required.issubset(hist.columns):
|
||||
raise RuntimeError("Phase 4M update history schema mismatch")
|
||||
edge_ids = identity.edge_id.to_numpy(np.int64)
|
||||
lookup = pd.Series(np.arange(len(edge_ids), dtype=np.int32), index=edge_ids)
|
||||
checkpoints = {}
|
||||
verify = []
|
||||
points = list(map(int, ncfg["checkpoints"]))
|
||||
all_rows = []
|
||||
for seed in ncfg["train_seeds"]:
|
||||
by = hist[hist.train_seed == seed]
|
||||
E = np.ones(len(edge_ids), np.float32)
|
||||
U = np.ones(len(edge_ids), np.float32)
|
||||
checkpoints[seed] = {0: {"bounded": E.copy(), "unbounded": U.copy()}}
|
||||
np.savez_compressed(exp / "acquisition-trajectory" / f"E-seed-{seed}-pairings-0.npz", E_bounded=E, E_unbounded=U, edge_id=edge_ids)
|
||||
for mechanism in ("bounded", "unbounded"):
|
||||
for group in GROUPS:
|
||||
mask = identity.identity_group.to_numpy() == group
|
||||
all_rows.append({"seed": seed, "pairings": 0, "mechanism": mechanism, "group": group,
|
||||
"total_edges": int(mask.sum()), "updated_edges": 0, "mean_delta_E": 0.0, "median_delta_E": 0.0,
|
||||
"weighted_transmission_reduction": 0.0, "E_change_total": 0.0})
|
||||
for pairing in range(1, max(points) + 1):
|
||||
item = by[by.pairing == pairing]
|
||||
local = lookup.loc[item.edge_id.to_numpy(np.int64)].to_numpy(np.int32)
|
||||
before_error = max(float(np.max(np.abs(E[local] - item.E_bounded_before.to_numpy(np.float32)))) if len(local) else 0.0,
|
||||
float(np.max(np.abs(U[local] - item.E_multiplicative_before.to_numpy(np.float32)))) if len(local) else 0.0)
|
||||
if before_error > 1e-6:
|
||||
raise RuntimeError(f"Phase 4M event history not reconstructible seed={seed} pairing={pairing}: {before_error}")
|
||||
E[local] = item.E_bounded_after.to_numpy(np.float32)
|
||||
U[local] = item.E_multiplicative_after.to_numpy(np.float32)
|
||||
if pairing not in points:
|
||||
continue
|
||||
checkpoints[seed][pairing] = {"bounded": E.copy(), "unbounded": U.copy()}
|
||||
np.savez_compressed(exp / "acquisition-trajectory" / f"E-seed-{seed}-pairings-{pairing}.npz", E_bounded=E, E_unbounded=U, edge_id=edge_ids)
|
||||
old = Path(mcfg["experiment_root"]) / "efficacy-overlay" / f"E-seed-{seed}-pairings-{pairing}.npz"
|
||||
if old.exists():
|
||||
with np.load(old) as f:
|
||||
err = max(float(np.max(np.abs(E - f["E_bounded_multiplicative"]))), float(np.max(np.abs(U - f["E_multiplicative"]))))
|
||||
verify.append({"seed": seed, "pairings": pairing, "max_abs_error": err})
|
||||
if err > 1e-6:
|
||||
raise RuntimeError("reconstructed E differs from sealed Phase 4M checkpoint")
|
||||
for mechanism, value in (("bounded", E), ("unbounded", U)):
|
||||
for group in GROUPS:
|
||||
mask = identity.identity_group.to_numpy() == group
|
||||
w = identity.anatomical_weight_mv.to_numpy(np.float32)[mask]
|
||||
reduction = 1 - value[mask]
|
||||
all_rows.append({"seed": seed, "pairings": pairing, "mechanism": mechanism, "group": group,
|
||||
"total_edges": int(mask.sum()), "updated_edges": int(np.count_nonzero(reduction > 1e-7)),
|
||||
"mean_delta_E": float((-reduction).mean()), "median_delta_E": float(np.median(-reduction)),
|
||||
"weighted_transmission_reduction": float(np.dot(w.astype(np.float64), reduction.astype(np.float64)) / w.sum()),
|
||||
"E_change_total": float(reduction.sum())})
|
||||
table = pd.DataFrame(all_rows)
|
||||
table.to_parquet(exp / "acquisition-trajectory/group-plasticity.parquet", index=False)
|
||||
wide = table.pivot_table(index=["seed", "pairings", "mechanism"], columns="group", values="weighted_transmission_reduction").reset_index()
|
||||
a, b, shared = (wide[x].to_numpy(np.float64) for x in ("A-specific", "B-specific", "shared"))
|
||||
wide["CSI"] = a - np.maximum(shared, b)
|
||||
wide["A_to_shared_ratio"] = np.divide(a, shared, out=np.full_like(a, np.nan), where=shared != 0)
|
||||
wide["A_to_B_ratio"] = np.divide(a, b, out=np.full_like(a, np.nan), where=b != 0)
|
||||
wide.to_parquet(exp / "specificity-curve/CSI.parquet", index=False)
|
||||
write_json(exp / "regression/phase4m-history-reconstruction.json", {"passed": True, "old_checkpoint_comparisons": verify, "max_error": max((v["max_abs_error"] for v in verify), default=0.0)})
|
||||
return checkpoints, table, wide, hist
|
||||
|
||||
|
||||
def isolated(ncfg, mcfg, cfg, exp: Path, identity: pd.DataFrame, checkpoints):
|
||||
v = Validator(cfg, Path(mcfg["frozen_phase4l_config"]), exp)
|
||||
if not np.array_equal(v.edge_ids, identity.edge_id.to_numpy(np.int64)):
|
||||
raise RuntimeError("plastic candidate ordering mismatch")
|
||||
sim = OdorSimulator(v, cfg, exp)
|
||||
comp = identity.compartment.astype(str).to_numpy() == str(ncfg["matched_compartment_verbatim"])
|
||||
targets = np.unique(identity.loc[comp, "target_neuron_idx"].to_numpy(np.int32))
|
||||
replay = ReplayEngine(sim, targets)
|
||||
anatomical = identity.anatomical_weight_mv.to_numpy(np.float32)
|
||||
old = Path(mcfg["experiment_root"])
|
||||
evaldata = {}
|
||||
zero = np.zeros(len(v.edge_ids), np.float32)
|
||||
for seed in ncfg["evaluation_seeds"]:
|
||||
for odor in ("A", "B"):
|
||||
spikes = pd.read_parquet(old / "isolated-replay" / f"KC-spikes-ODOR_{odor}-seed-{seed}.parquet")
|
||||
evaldata[(seed, odor)] = (spikes, replay.run(spikes, zero, float(mcfg["protocol"]["duration_ms"])))
|
||||
rows = []
|
||||
replay_wall = []
|
||||
for train_seed, eval_seed in zip(ncfg["train_seeds"], ncfg["evaluation_seeds"]):
|
||||
for pair in ncfg["checkpoints"]:
|
||||
E = checkpoints[train_seed][pair]["bounded"]
|
||||
delta = anatomical * (E - 1)
|
||||
rec = {"train_seed": train_seed, "eval_seed": eval_seed, "pairings": pair}
|
||||
effects = {}
|
||||
for odor in ("A", "B"):
|
||||
spikes, baseline = evaldata[(eval_seed, odor)]
|
||||
post = replay.run(spikes, delta, float(mcfg["protocol"]["duration_ms"]))
|
||||
replay_wall.append(post["wall_seconds"])
|
||||
effects[odor] = replay_effect(baseline, post, sim.dt)
|
||||
rec.update({f"{odor}_{k}": value for k, value in effects[odor].items() if k != "isolated_input_scope"})
|
||||
rec["A_specific_functional"] = compare_A_B(effects["A"], effects["B"])
|
||||
rec["functional_separation_spike_L1"] = float(effects["A"]["MBON_spike_L1"] - effects["B"]["MBON_spike_L1"])
|
||||
al = effects["A"]["first_latency_delta_ms"] or 0.0
|
||||
bl = effects["B"]["first_latency_delta_ms"] or 0.0
|
||||
rec["functional_separation_latency_ms"] = float(al - bl)
|
||||
rows.append(rec)
|
||||
frame = pd.DataFrame(rows)
|
||||
frame.to_parquet(exp / "isolated/response-by-checkpoint.parquet", index=False)
|
||||
curve = frame.groupby("pairings").agg(A_specific_functional_seeds=("A_specific_functional", "sum"),
|
||||
mean_spike_L1_separation=("functional_separation_spike_L1", "mean"),
|
||||
mean_latency_separation_ms=("functional_separation_latency_ms", "mean"),
|
||||
A_MBON_spike_L1=("A_MBON_spike_L1", "mean"), B_MBON_spike_L1=("B_MBON_spike_L1", "mean"),
|
||||
A_transmission_reduction_percent=("A_transmitted_reduction_percent", "mean"),
|
||||
B_transmission_reduction_percent=("B_transmitted_reduction_percent", "mean")).reset_index()
|
||||
curve.to_parquet(exp / "specificity-curve/memory-specificity.parquet", index=False)
|
||||
valid = curve[curve.A_specific_functional_seeds >= 3].copy()
|
||||
if valid.empty:
|
||||
peak = None
|
||||
else:
|
||||
best_spikes = float(valid.mean_spike_L1_separation.max())
|
||||
valid = valid[np.isclose(valid.mean_spike_L1_separation, best_spikes, atol=1e-9, rtol=0)]
|
||||
best_latency = float(valid.mean_latency_separation_ms.max())
|
||||
valid = valid[valid.mean_latency_separation_ms >= best_latency - sim.dt / 2]
|
||||
peak = int(valid.pairings.min())
|
||||
if peak is not None:
|
||||
# Equal score/latency checkpoints are resolved to earliest by the predeclared rule.
|
||||
peak_csi = float(np.mean(pd.read_parquet(exp / "specificity-curve/CSI.parquet").query("mechanism == 'bounded' and pairings == @peak").CSI))
|
||||
else:
|
||||
peak_csi = None
|
||||
write_json(exp / "specificity-curve/peak.json", {"status": "SPECIFICITY-PEAK-IDENTIFIED" if peak is not None else "UNIDENTIFIED", "pairings": peak,
|
||||
"peak_CSI": peak_csi, "selection_rule": ncfg["peak_rule"]})
|
||||
write_json(exp / "performance/isolated.json", {"mean_replay_wall_seconds": float(np.mean(replay_wall)), "total_replay_wall_seconds": float(np.sum(replay_wall)), "count": len(replay_wall)})
|
||||
return v, sim, frame, curve, peak
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--config", required=True, type=Path)
|
||||
args = ap.parse_args()
|
||||
ncfg, mcfg, cfg, base_path = load_config(args.config)
|
||||
exp = Path(ncfg["experiment_root"])
|
||||
identity, definition = prepare(ncfg, mcfg, cfg, exp)
|
||||
print(json.dumps({"stage": "identity_frozen", "group_counts": definition["group_counts"], "identity_sha256": sha256(exp / "edge-identity/frozen-edge-identity.parquet")}), flush=True)
|
||||
checkpoints, group, csi, history = reconstruct(ncfg, mcfg, cfg, exp, identity)
|
||||
print(json.dumps({"stage": "trajectory_reconstructed", "checkpoint_count": len(ncfg["checkpoints"]), "updates": len(history)}), flush=True)
|
||||
v, sim, frame, curve, peak = isolated(ncfg, mcfg, cfg, exp, identity, checkpoints)
|
||||
print(json.dumps({"stage": "isolated_complete", "peak": peak, "curve": curve.to_dict("records")}), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,25 @@
|
||||
phase: 4N
|
||||
name: MaleCNS-CreditAssignment-audit
|
||||
frozen_phase4m_config: /data/digital-drosophila/models/male-cns-lif/kcmbon-efficacy-v1/phase4m.yaml
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4n-credit-assignment
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif/credit-assignment-v1
|
||||
mechanism: bounded_multiplicative
|
||||
checkpoints: [0, 1, 2, 5, 10, 20, 30, 40, 50, 60, 75, 100]
|
||||
train_seeds: [51201, 51202, 51203, 51204, 51205]
|
||||
evaluation_seeds: [51301, 51302, 51303, 51304, 51305]
|
||||
edge_identity_source: phase4l matched_energy held-out ODOR_A/B majority-of-five KC representation
|
||||
credit_specificity_index: group mean anatomical-weighted E depression for A-specific minus maximum of shared and B-specific groups
|
||||
functional_separation: A MBON spike-count L1 change minus B MBON spike-count L1 change; first-spike latency used only as tie-breaker
|
||||
peak_rule: earliest checkpoint with at least 3 of 5 A-specific functional seeds and maximum five-seed mean spike-L1 separation; spike-L1 ties within 1e-9 use mean first-latency separation only if difference exceeds half a dt step (0.05 ms), otherwise choose earliest; no peak if no such checkpoint
|
||||
generalization_onset_rule: earliest checkpoint after peak with CSI at least 20 percent below peak and shared-or-B group depression at least 10 percent above its peak value; otherwise unidentified
|
||||
random_us_seed_offset: 700000
|
||||
random_us_window_ms: [30.0, 110.0]
|
||||
shadow_trace_tau_ms: 20.0
|
||||
event_coincidence_window_ms: 1.0
|
||||
first_activation_only: true
|
||||
matched_compartment_verbatim: "y5B'2a"
|
||||
unrelated_compartment_verbatim: "y2a'1"
|
||||
matched_dan_type: PAM15
|
||||
unrelated_dan_type: PPL103
|
||||
no_frozen_artifact_writes: true
|
||||
no_formal_associative_memory_claim: true
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare external-only E with open-loop all-confirmed-DAN gate shadow."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from phase4h_audit import write_json
|
||||
from phase4i import Validator
|
||||
from phase4j import effect
|
||||
from phase4l import OdorSimulator, pn_drive
|
||||
from phase4m import ReplayEngine, compare_A_B, load_odor_inputs, replay_effect
|
||||
from phase4n import load_config
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument("--config",type=Path,required=True);args=ap.parse_args()
|
||||
ncfg,mcfg,cfg,basepath=load_config(args.config);exp=Path(ncfg["experiment_root"])
|
||||
identity=pd.read_parquet(exp/"edge-identity/frozen-edge-identity.parquet")
|
||||
reproduction=json.loads((exp/"regression/event-trace-reproduction.json").read_text())
|
||||
bad={int(seed):min(int(x["pairing"]) for x in reproduction["mismatched_trials"] if int(x["seed"])==int(seed))
|
||||
for seed in ncfg["train_seeds"] if any(int(x["seed"])==int(seed) for x in reproduction["mismatched_trials"])}
|
||||
v=Validator(cfg,basepath,exp);sim=OdorSimulator(v,cfg,exp)
|
||||
comp=identity.compartment.astype(str).to_numpy()==str(ncfg["matched_compartment_verbatim"])
|
||||
engine=ReplayEngine(sim,np.unique(identity.loc[comp,"target_neuron_idx"].to_numpy(np.int32)))
|
||||
w=identity.anatomical_weight_mv.to_numpy(np.float32);old=Path(mcfg["experiment_root"])
|
||||
rows=[];calls=[]
|
||||
for seed,es in zip(ncfg["train_seeds"],ncfg["evaluation_seeds"]):
|
||||
for pair in [20,50,60,75,100]:
|
||||
with np.load(exp/"endogenous-dan"/f"E-all-DAN-shadow-seed-{seed}-pairings-{pair}.npz") as f:
|
||||
states={"external-only":f["E_external_only"].copy(),"all-confirmed-DAN-shadow":f["E_all_DAN"].copy()}
|
||||
for mechanism,E in states.items():
|
||||
delta=w*(E-1);results={}
|
||||
for odor in ("A","B"):
|
||||
spikes=pd.read_parquet(old/"isolated-replay"/f"KC-spikes-ODOR_{odor}-seed-{es}.parquet")
|
||||
zero=engine.run(spikes,np.zeros(len(E),np.float32),float(mcfg["protocol"]["duration_ms"]))
|
||||
result=engine.run(spikes,delta,float(mcfg["protocol"]["duration_ms"]))
|
||||
calls.append(result["wall_seconds"]);results[odor]=replay_effect(zero,result,sim.dt)
|
||||
rows.append({"seed":seed,"eval_seed":es,"pairings":pair,"mechanism":mechanism,
|
||||
"trace_exact_prefix":bool(pair < bad.get(int(seed),10**9)),
|
||||
"A_specific_functional":compare_A_B(results["A"],results["B"]),
|
||||
"A_MBON_spike_L1":results["A"]["MBON_spike_L1"],"B_MBON_spike_L1":results["B"]["MBON_spike_L1"],
|
||||
"A_transmission_reduction_percent":results["A"]["transmitted_reduction_percent"],
|
||||
"B_transmission_reduction_percent":results["B"]["transmitted_reduction_percent"]})
|
||||
table=pd.DataFrame(rows);table.to_parquet(exp/"endogenous-dan/isolated-gate-comparison.parquet",index=False)
|
||||
panel,vectors,selection,mapping=load_odor_inputs(cfg);gloms=selection["glomeruli_order"];ms=float(np.median(panel.mapped_response_sum));pn={}
|
||||
for odor in ("A","B"):
|
||||
oid=selection[f"ODOR_{odor}"]["odor_id"]
|
||||
pn[odor]=pn_drive(oid,vectors[oid],mapping,gloms,str(mcfg["protocol"]["input_mode"]),ms,cfg)[:2]
|
||||
whole=[];brain_calls=[]
|
||||
for seed,es in zip(ncfg["train_seeds"],ncfg["evaluation_seeds"]):
|
||||
baseline={odor:sim.run_rates(*pn[odor],es,float(mcfg["protocol"]["duration_ms"])) for odor in pn}
|
||||
for pair in (60,100):
|
||||
with np.load(exp/"endogenous-dan"/f"E-all-DAN-shadow-seed-{seed}-pairings-{pair}.npz") as f:
|
||||
states={"external-only":f["E_external_only"].copy(),"all-confirmed-DAN-shadow":f["E_all_DAN"].copy()}
|
||||
for mechanism,E in states.items():
|
||||
delta=w*(E-1)
|
||||
for odor in ("A","B"):
|
||||
post=sim.run_rates(*pn[odor],es,float(mcfg["protocol"]["duration_ms"]),delta=delta)
|
||||
brain_calls.append(post["wall_seconds"]);e=effect(baseline[odor],post)
|
||||
whole.append({"seed":seed,"eval_seed":es,"pairings":pair,"mechanism":mechanism,"odor":odor,
|
||||
"trace_exact_prefix":bool(pair < bad.get(int(seed),10**9)),
|
||||
"MBON_spike_L1":e["MBON_spike_l1"],"downstream_spike_L1":e["downstream_spike_l1"],
|
||||
"KC_spike_L1":int(np.abs(post["kc_counts"]-baseline[odor]["kc_counts"]).sum()),
|
||||
"finite":bool(post["finite"]),"wall_seconds":post["wall_seconds"]})
|
||||
wt=pd.DataFrame(whole);wt.to_parquet(exp/"endogenous-dan/wholebrain-gate-comparison.parquet",index=False)
|
||||
iso=table.groupby(["mechanism","pairings"]).agg(A_specific_seeds=("A_specific_functional","sum"),A_spike_L1=("A_MBON_spike_L1","mean"),B_spike_L1=("B_MBON_spike_L1","mean")).reset_index()
|
||||
pivot=wt.pivot(index=["mechanism","pairings","seed"],columns="odor",values="MBON_spike_L1").reset_index()
|
||||
pivot["A_minus_B"]=pivot["A"]-pivot["B"]
|
||||
wh=pivot.groupby(["mechanism","pairings"]).agg(A_minus_B_mean=("A_minus_B","mean"),A_specific_seeds=("A_minus_B",lambda x:int((x>0).sum()))).reset_index()
|
||||
clean=table[table.trace_exact_prefix].groupby(["mechanism","pairings"]).agg(clean_seed_count=("seed","nunique"),A_specific_seeds=("A_specific_functional","sum"),
|
||||
A_spike_L1=("A_MBON_spike_L1","mean"),B_spike_L1=("B_MBON_spike_L1","mean")).reset_index()
|
||||
comp_summary={"isolated":iso.to_dict("records"),"wholebrain":wh.to_dict("records"),
|
||||
"isolated_exact_prefix_only":clean.to_dict("records"),"nonexact_trials":reproduction["mismatched_trials"],
|
||||
"all_DAN_branch_status":"counterfactual open-loop shadow gate, not frozen Phase 4M dynamics or physiological dopamine",
|
||||
"isolated_replay_wall_seconds":float(sum(calls)),"wholebrain_wall_seconds":float(sum(brain_calls)),
|
||||
"external_US_only_is_frozen_Phase4M_rule":True}
|
||||
write_json(exp/"endogenous-dan/gate-specificity.json",comp_summary)
|
||||
print(json.dumps(comp_summary),flush=True)
|
||||
|
||||
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Held-out whole-brain readout for independent Phase 4N control E overlays."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from phase4h_audit import write_json
|
||||
from phase4i import Validator
|
||||
from phase4j import effect
|
||||
from phase4l import OdorSimulator, pn_drive
|
||||
from phase4m import load_odor_inputs
|
||||
from phase4n import load_config
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument("--config",type=Path,required=True);args=ap.parse_args()
|
||||
ncfg,mcfg,cfg,basepath=load_config(args.config);exp=Path(ncfg["experiment_root"])
|
||||
identity=pd.read_parquet(exp/"edge-identity/frozen-edge-identity.parquet")
|
||||
v=Validator(cfg,basepath,exp);sim=OdorSimulator(v,cfg,exp)
|
||||
w=identity.anatomical_weight_mv.to_numpy(np.float32)
|
||||
panel,vectors,selection,mapping=load_odor_inputs(cfg);gloms=selection["glomeruli_order"];ms=float(np.median(panel.mapped_response_sum))
|
||||
pn={}
|
||||
for odor in ("A","B"):
|
||||
oid=selection[f"ODOR_{odor}"]["odor_id"]
|
||||
pn[odor]=pn_drive(oid,vectors[oid],mapping,gloms,str(mcfg["protocol"]["input_mode"]),ms,cfg)[:2]
|
||||
rows=[];walls=[];conditions=("B+matched-US","A+random-US-timing","A+unrelated-compartment-US")
|
||||
for seed,es in zip(ncfg["train_seeds"],ncfg["evaluation_seeds"]):
|
||||
baseline={odor:sim.run_rates(*pn[odor],es,float(mcfg["protocol"]["duration_ms"])) for odor in pn}
|
||||
for pair in (50,100):
|
||||
for condition in conditions:
|
||||
with np.load(exp/"controls"/f"E-{condition}-seed-{seed}-pairings-{pair}.npz") as f:E=f["E"].copy()
|
||||
delta=w*(E-1)
|
||||
for odor in ("A","B"):
|
||||
post=sim.run_rates(*pn[odor],es,float(mcfg["protocol"]["duration_ms"]),delta=delta)
|
||||
e=effect(baseline[odor],post);walls.append(post["wall_seconds"])
|
||||
rows.append({"condition":condition,"train_seed":seed,"eval_seed":es,"pairings":pair,"odor":odor,
|
||||
"MBON_spike_L1":e["MBON_spike_l1"],"downstream_spike_L1":e["downstream_spike_l1"],
|
||||
"KC_spike_L1":int(np.abs(post["kc_counts"]-baseline[odor]["kc_counts"]).sum()),
|
||||
"finite":bool(post["finite"]),"wall_seconds":post["wall_seconds"]})
|
||||
print(json.dumps({"seed":seed,"completed_control_wholebrain":True}),flush=True)
|
||||
table=pd.DataFrame(rows);table.to_parquet(exp/"controls/wholebrain-heldout-control-replay.parquet",index=False)
|
||||
pivot=table.pivot(index=["condition","pairings","train_seed"],columns="odor",values="MBON_spike_L1").reset_index()
|
||||
pivot["A_minus_B"]=pivot["A"]-pivot["B"]
|
||||
summary=pivot.groupby(["condition","pairings"]).agg(A_minus_B_mean=("A_minus_B","mean"),A_specific_seeds=("A_minus_B",lambda x:int((x>0).sum()))).reset_index()
|
||||
write_json(exp/"controls/wholebrain-summary.json",{"rows":len(table),"conditions":summary.to_dict("records"),
|
||||
"total_brain_wall_seconds":float(sum(walls)),"frozen_E1_reference_reused_per_evaluation_seed":True,
|
||||
"A_D0_and_frozen_E1_effect_are_exactly_zero_by_E_identity":True})
|
||||
print(json.dumps(summary.to_dict("records")),flush=True)
|
||||
|
||||
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4N independent control trajectories; no frozen artifact writes."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import resource
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from phase4h_audit import write_json
|
||||
from phase4i import Validator
|
||||
from phase4j import apply_v2, effect
|
||||
from phase4l import OdorSimulator, pn_drive
|
||||
from phase4m import ReplayEngine, compare_A_B, load_odor_inputs, replay_effect
|
||||
from phase4n import load_config
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument("--config",type=Path,required=True);ap.add_argument("--max-pairings",type=int,default=100)
|
||||
args=ap.parse_args();ncfg,mcfg,cfg,basepath=load_config(args.config);exp=Path(ncfg["experiment_root"])
|
||||
identity=pd.read_parquet(exp/"edge-identity/frozen-edge-identity.parquet")
|
||||
v=Validator(cfg,basepath,exp);sim=OdorSimulator(v,cfg,exp)
|
||||
panel,vectors,selection,mapping=load_odor_inputs(cfg);gloms=selection["glomeruli_order"];ms=float(np.median(panel.mapped_response_sum))
|
||||
pn={}
|
||||
for label in ("A","B"):
|
||||
oid=selection[f"ODOR_{label}"]["odor_id"]
|
||||
pn[label]=pn_drive(oid,vectors[oid],mapping,gloms,str(mcfg["protocol"]["input_mode"]),ms,cfg)[:2]
|
||||
matched=v.dan_targets(str(ncfg["matched_dan_type"]),str(ncfg["matched_compartment_verbatim"]))
|
||||
unrelated=v.dan_targets(str(ncfg["unrelated_dan_type"]),str(ncfg["unrelated_compartment_verbatim"]))
|
||||
src=identity.source_neuron_idx.to_numpy(np.int32);loc=np.full(v.n,-1,np.int32);loc[v.kc]=np.arange(len(v.kc),dtype=np.int32);loc=loc[src]
|
||||
k=np.float32(cfg["eligibility_v2"]["eta_effective_mv"]/cfg["model"]["w_syn_mv_per_anatomical_synapse"])
|
||||
bound=np.float32(1-cfg["eligibility_v2"]["max_fractional_depression"])
|
||||
points=set(map(int,ncfg["checkpoints"]));rows=[];walls=[]
|
||||
conditions={"B+matched-US":("B",matched,str(ncfg["matched_compartment_verbatim"])),
|
||||
"A+random-US-timing":("A",matched,str(ncfg["matched_compartment_verbatim"])),
|
||||
"A+unrelated-compartment-US":("A",unrelated,str(ncfg["unrelated_compartment_verbatim"]))}
|
||||
for condition,(odor,dan,compartment) in conditions.items():
|
||||
group_mask=identity.compartment.astype(str).to_numpy()==compartment
|
||||
for seed in ncfg["train_seeds"]:
|
||||
add=np.zeros(len(v.edge_ids),np.float32);E=np.ones(len(v.edge_ids),np.float32)
|
||||
rng=np.random.default_rng(seed+int(ncfg["random_us_seed_offset"]))
|
||||
np.savez_compressed(exp/"controls"/f"E-{condition}-seed-{seed}-pairings-0.npz",E=E,edge_id=v.edge_ids)
|
||||
rows.append({"condition":condition,"seed":seed,"pairings":0,"odor":odor,"DAN_compartment":compartment,
|
||||
"US_time_ms":None,"updated_edges":0,"matched_compartment_E_reduction_sum":0.0,
|
||||
"all_mask_E_reduction_sum":0.0,"brain_wall_seconds":0.0})
|
||||
for pairing in range(1,args.max_pairings+1):
|
||||
pulse=float(rng.uniform(*ncfg["random_us_window_ms"])) if condition=="A+random-US-timing" else float(mcfg["protocol"]["dan_time_ms"])
|
||||
run=sim.run_rates(*pn[odor],seed+pairing,float(mcfg["protocol"]["duration_ms"]),delta=add,dan_targets=dan,dan_pulses=[pulse])
|
||||
walls.append(run["wall_seconds"]);trace=run["kc_trace_at_dan"];elig=trace[loc]
|
||||
active=group_mask&(elig>0)
|
||||
E[active]=np.maximum(bound,E[active]*(np.float32(1.0)-k*elig[active]))
|
||||
add,_=apply_v2(v,trace,compartment,1.0,add,cfg)
|
||||
if pairing in points:
|
||||
out=exp/"controls"/f"E-{condition}-seed-{seed}-pairings-{pairing}.npz"
|
||||
np.savez_compressed(out,E=E,edge_id=v.edge_ids)
|
||||
rows.append({"condition":condition,"seed":seed,"pairings":pairing,"odor":odor,"DAN_compartment":compartment,
|
||||
"US_time_ms":pulse,"updated_edges":int(np.count_nonzero(E<1-1e-7)),"matched_compartment_E_reduction_sum":float(np.sum((1-E)[identity.compartment.astype(str).to_numpy()==str(ncfg["matched_compartment_verbatim"])])),
|
||||
"all_mask_E_reduction_sum":float(np.sum(1-E)),"brain_wall_seconds":run["wall_seconds"]})
|
||||
if pairing % 20 == 0:
|
||||
print(json.dumps({"condition":condition,"seed":seed,"pairing":pairing,"E_reduction_sum":float(np.sum(1-E))}),flush=True)
|
||||
print(json.dumps({"condition":condition,"seed":seed,"pairings":args.max_pairings,"E_reduction_sum":float(np.sum(1-E))}),flush=True)
|
||||
frame=pd.DataFrame(rows);frame.to_parquet(exp/"controls/independent-training-trajectories.parquet",index=False)
|
||||
# Common frozen held-out A/B KC spike trains; changes below are diagnostic, not memory claims.
|
||||
target_comp=identity.compartment.astype(str).to_numpy()==str(ncfg["matched_compartment_verbatim"])
|
||||
engine=ReplayEngine(sim,np.unique(identity.loc[target_comp,"target_neuron_idx"].to_numpy(np.int32)))
|
||||
w=identity.anatomical_weight_mv.to_numpy(np.float32);replay_rows=[]
|
||||
for condition in conditions:
|
||||
for seed,es in zip(ncfg["train_seeds"],ncfg["evaluation_seeds"]):
|
||||
for pair in [20,50,60,75,100]:
|
||||
with np.load(exp/"controls"/f"E-{condition}-seed-{seed}-pairings-{pair}.npz") as f:E=f["E"].copy()
|
||||
delta=w*(E-1);effects={}
|
||||
for odor in ("A","B"):
|
||||
spikes=pd.read_parquet(Path(mcfg["experiment_root"])/"isolated-replay"/f"KC-spikes-ODOR_{odor}-seed-{es}.parquet")
|
||||
base=engine.run(spikes,np.zeros(len(E),np.float32),float(mcfg["protocol"]["duration_ms"]))
|
||||
post=engine.run(spikes,delta,float(mcfg["protocol"]["duration_ms"]))
|
||||
effects[odor]=replay_effect(base,post,sim.dt)
|
||||
replay_rows.append({"condition":condition,"train_seed":seed,"eval_seed":es,"pairings":pair,
|
||||
"A_specific_functional":compare_A_B(effects["A"],effects["B"]),
|
||||
"A_MBON_spike_L1":effects["A"]["MBON_spike_L1"],"B_MBON_spike_L1":effects["B"]["MBON_spike_L1"],
|
||||
"A_transmission_reduction_percent":effects["A"]["transmitted_reduction_percent"],
|
||||
"B_transmission_reduction_percent":effects["B"]["transmitted_reduction_percent"]})
|
||||
rframe=pd.DataFrame(replay_rows);rframe.to_parquet(exp/"controls/isolated-heldout-control-replay.parquet",index=False)
|
||||
write_json(exp/"controls/independent-summary.json",{"conditions":list(conditions),"seeds":list(ncfg["train_seeds"]),
|
||||
"pairings_per_seed":args.max_pairings,"training_calls":len(walls),"training_wall_seconds":float(sum(walls)),
|
||||
"mean_call_seconds":float(np.mean(walls)),"peak_RAM_KiB":int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss),
|
||||
"matched_compartment_target_not_modified_by_unrelated_US":bool(frame[frame.condition=="A+unrelated-compartment-US"].matched_compartment_E_reduction_sum.eq(0).all()),
|
||||
"isolated_replay":rframe.groupby(["condition","pairings"]).agg(A_specific_seeds=("A_specific_functional","sum"),A_spike_L1=("A_MBON_spike_L1","mean"),B_spike_L1=("B_MBON_spike_L1","mean")).reset_index().to_dict("records")})
|
||||
|
||||
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,227 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4N event-level KC/DAN side-channel on unmodified frozen LIF dynamics."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import resource
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
|
||||
from phase4h_audit import write_json
|
||||
from phase4i import Validator
|
||||
from phase4l import OdorSimulator, pn_drive
|
||||
from phase4m import load_odor_inputs
|
||||
from phase4n import load_config
|
||||
|
||||
|
||||
class EventSimulator(OdorSimulator):
|
||||
def run_events(self, targets, rates_hz, seed, duration_ms, delta, dan_targets, dan_pulse_ms, all_dan,
|
||||
save_all_events=False):
|
||||
targets = np.asarray(targets, np.int32); rates_hz = np.asarray(rates_hz, np.float32)
|
||||
self.v.install(delta); self.rt.weights[self.apl_edge_gpu] = 0.0; self.rt.reset(int(seed))
|
||||
steps = int(round(duration_ms / self.dt)); tg = torch.as_tensor(targets, dtype=torch.int64, device=self.device)
|
||||
rg = torch.as_tensor(rates_hz * self.dt / 1000.0, dtype=torch.float32, device=self.device)
|
||||
gen = torch.Generator(device=self.device); gen.manual_seed(int(seed))
|
||||
forced = torch.rand((steps, len(targets)), generator=gen, device=self.device) < rg[None, :]
|
||||
dan_targets = np.asarray(dan_targets, np.int32); dg = torch.as_tensor(dan_targets, dtype=torch.int64, device=self.device)
|
||||
dan_forced = self._forced(steps, dan_targets, seed + 991, None, [dan_pulse_ms])
|
||||
dan_all_gpu = torch.as_tensor(all_dan, dtype=torch.int64, device=self.device)
|
||||
apl = self.cfg["apl_overlay"]; apl_decay = math.exp(-self.dt / float(apl["tau_ms"]))
|
||||
trace_decay = math.exp(-self.dt / float(self.cfg["eligibility_v2"]["kc_trace_tau_ms"]))
|
||||
apl_state = torch.zeros(len(self.apl), dtype=torch.float32, device=self.device)
|
||||
trace = torch.zeros(len(self.kc), dtype=torch.float32, device=self.device)
|
||||
kc_history = torch.zeros((steps, len(self.kc)), dtype=torch.bool, device=self.device)
|
||||
dan_history = torch.zeros((steps, len(all_dan)), dtype=torch.bool, device=self.device)
|
||||
natural_dan_history = torch.zeros_like(dan_history)
|
||||
trace_at_gate = None; all_nodes = []; all_steps = []
|
||||
alpha, gd, rest, reset, threshold, refsteps = self.rt.alpha, self.rt.g_decay, self.rt.rest, self.rt.reset_value, self.rt.threshold, self.rt.refractory_steps
|
||||
t0 = time.perf_counter()
|
||||
for step in range(steps):
|
||||
slot = step % self.rt.queue_len; self.rt.g.mul_(gd).add_(self.rt.delay_queue[slot]); self.rt.delay_queue[slot].zero_()
|
||||
self.rt.refractory.clamp_min_(1).sub_(1); available = self.rt.refractory == 0
|
||||
self.rt.v[available] += alpha * (self.rt.g[available] - (self.rt.v[available] - rest)); self.rt.v[~available] = reset
|
||||
spikes = available & (self.rt.v >= threshold)
|
||||
accepted = forced[step] & available[tg]
|
||||
if accepted.any(): spikes[tg[accepted]] = True
|
||||
natural_dan_history[step] = spikes[dan_all_gpu]
|
||||
if len(dan_targets):
|
||||
da = dan_forced[step] & available[dg]
|
||||
if da.any(): spikes[dg[da]] = True
|
||||
sources = torch.nonzero(spikes, as_tuple=False).flatten()
|
||||
kc_history[step] = spikes[self.kc_gpu]
|
||||
dan_history[step] = spikes[dan_all_gpu]
|
||||
if save_all_events and sources.numel():
|
||||
all_nodes.append(sources.to(torch.int32)); all_steps.append(torch.full((sources.numel(),), step, dtype=torch.int32, device=self.device))
|
||||
if sources.numel():
|
||||
self.rt.v[sources] = reset; self.rt.refractory[sources] = refsteps; eids = self.rt._hybrid_edges(sources)
|
||||
if eids.numel(): self.rt.delay_queue[(step + self.rt.delay_steps) % self.rt.queue_len].scatter_add_(0, self.rt.indices[eids].long(), self.rt.weights[eids])
|
||||
trace.mul_(trace_decay).add_(spikes[self.kc_gpu].to(torch.float32)).clamp_max_(1.0)
|
||||
if step == int(round(dan_pulse_ms / self.dt)): trace_at_gate = trace.clone()
|
||||
apl_state.mul_(apl_decay).add_(torch.mv(self.input_gpu, spikes.to(torch.float32)) * float(apl["normalization"]))
|
||||
inhibition = torch.matmul(apl_state, self.output_gpu) * float(apl["gain"])
|
||||
self.rt.g.index_add_(0, self.kc_gpu, -inhibition)
|
||||
torch.cuda.synchronize()
|
||||
event_table = None
|
||||
if save_all_events:
|
||||
event_table = pd.DataFrame({"step": torch.cat(all_steps).cpu().numpy() if all_steps else np.empty(0,np.int32),
|
||||
"neuron_idx": torch.cat(all_nodes).cpu().numpy() if all_nodes else np.empty(0,np.int32)})
|
||||
return {"kc": kc_history.cpu().numpy(), "dan": dan_history.cpu().numpy(), "natural_dan": natural_dan_history.cpu().numpy(), "all_events": event_table,
|
||||
"trace_at_external": trace_at_gate.cpu().numpy(), "wall_seconds": time.perf_counter()-t0,
|
||||
"peak_vram_bytes": int(torch.cuda.max_memory_allocated())}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True); ap.add_argument("--max-pairings", type=int, default=100); ap.add_argument("--train-seed", type=int)
|
||||
args = ap.parse_args(); ncfg, mcfg, cfg, basepath = load_config(args.config)
|
||||
exp = Path(ncfg["experiment_root"])
|
||||
identity = pd.read_parquet(exp / "edge-identity/frozen-edge-identity.parquet")
|
||||
history = pd.read_parquet(Path(mcfg["experiment_root"]) / "efficacy-overlay/update-history.parquet")
|
||||
v = Validator(cfg, basepath, exp); sim = EventSimulator(v, cfg, exp)
|
||||
if not np.array_equal(v.edge_ids, identity.edge_id.to_numpy(np.int64)):
|
||||
raise RuntimeError("edge identity order mismatch")
|
||||
all_dan = np.unique(v.confirmed.source_neuron_idx.to_numpy(np.int32))
|
||||
matched = v.dan_targets(str(ncfg["matched_dan_type"]), str(ncfg["matched_compartment_verbatim"]))
|
||||
matched_set = set(map(int, matched)); all_dan_to_local = {int(n): i for i,n in enumerate(all_dan)}
|
||||
matched_loc = np.array([all_dan_to_local[int(n)] for n in matched], np.int32)
|
||||
panel, vectors, selection, mapping = load_odor_inputs(cfg)
|
||||
gloms = selection["glomeruli_order"]; matched_sum = float(np.median(panel.mapped_response_sum))
|
||||
oid = selection["ODOR_A"]["odor_id"]
|
||||
pn = pn_drive(oid, vectors[oid], mapping, gloms, str(mcfg["protocol"]["input_mode"]), matched_sum, cfg)[:2]
|
||||
src = identity.source_neuron_idx.to_numpy(np.int32)
|
||||
kclook = np.full(v.n, -1, np.int32); kclook[v.kc] = np.arange(len(v.kc), dtype=np.int32)
|
||||
loc = kclook[src]
|
||||
comp = identity.compartment.astype(str).to_numpy() == str(ncfg["matched_compartment_verbatim"])
|
||||
bound = np.float32(1.0 - cfg["eligibility_v2"]["max_fractional_depression"])
|
||||
k = np.float32(cfg["eligibility_v2"]["eta_effective_mv"] / cfg["model"]["w_syn_mv_per_anatomical_synapse"])
|
||||
tau = float(cfg["eligibility_v2"]["kc_trace_tau_ms"])
|
||||
short_tau = float(ncfg["shadow_trace_tau_ms"])
|
||||
pulse_step = int(round(float(mcfg["protocol"]["dan_time_ms"]) / sim.dt))
|
||||
hist_by = {(int(s),int(p)): frame for (s,p),frame in history.groupby(["train_seed","pairing"])}
|
||||
eid_to_local = pd.Series(np.arange(len(identity), dtype=np.int32), index=identity.edge_id.to_numpy(np.int64))
|
||||
audit_rows = []; gate_rows = []; shadow_rows = []; perf = []; kc_event_frames = []; dan_event_frames = []; mismatches = []
|
||||
key = {10,20,50,60,75,100}
|
||||
for seed in ([args.train_seed] if args.train_seed is not None else ncfg["train_seeds"]):
|
||||
additive = np.zeros(len(identity), np.float32)
|
||||
E_ext = np.ones(len(identity), np.float32)
|
||||
E_all = np.ones(len(identity), np.float32)
|
||||
rng = np.random.default_rng(seed + int(ncfg["random_us_seed_offset"]))
|
||||
for pairing in range(1, args.max_pairings+1):
|
||||
audit_start = len(audit_rows); gate_start = len(gate_rows)
|
||||
capture = pairing in key
|
||||
run = sim.run_events(*pn, seed+pairing, float(mcfg["protocol"]["duration_ms"]), additive, matched,
|
||||
float(mcfg["protocol"]["dan_time_ms"]), all_dan, save_all_events=capture)
|
||||
perf.append({"seed":seed,"pairing":pairing,"wall_seconds":run["wall_seconds"],"peak_vram_bytes":run["peak_vram_bytes"]})
|
||||
kc = run["kc"]; dan = run["natural_dan"]
|
||||
ks, kl = np.nonzero(kc)
|
||||
if len(ks):
|
||||
kc_event_frames.append(pd.DataFrame({"seed":seed,"pairing":pairing,"step":ks.astype(np.int32),
|
||||
"neuron_idx":v.kc[kl].astype(np.int32)}))
|
||||
ds, dl = np.nonzero(dan)
|
||||
if len(ds):
|
||||
dan_event_frames.append(pd.DataFrame({"seed":seed,"pairing":pairing,"step":ds.astype(np.int32),
|
||||
"neuron_idx":all_dan[dl].astype(np.int32),"source":"endogenous-spike"}))
|
||||
trace = np.zeros(len(v.kc), np.float32); short = np.zeros_like(trace)
|
||||
last = np.full(len(v.kc), -1, np.int32); first = np.full(len(v.kc), -1, np.int32)
|
||||
external_at_gate = None
|
||||
endogenous_steps = 0; endogenous_events = 0
|
||||
random_step = int(round(float(rng.uniform(*ncfg["random_us_window_ms"])) / sim.dt))
|
||||
random_trace = None
|
||||
for step in range(len(kc)):
|
||||
spike = kc[step]
|
||||
trace *= np.float32(math.exp(-sim.dt / tau)); trace += spike.astype(np.float32); np.minimum(trace, 1.0, out=trace)
|
||||
short *= np.float32(math.exp(-sim.dt / short_tau)); short += spike.astype(np.float32); np.minimum(short, 1.0, out=short)
|
||||
last[spike] = step; first[spike & (first < 0)] = step
|
||||
if step == random_step: random_trace = trace[loc].copy()
|
||||
endogenous = bool(np.any(dan[step, matched_loc]))
|
||||
external = step == pulse_step
|
||||
if endogenous:
|
||||
endogenous_steps += 1; endogenous_events += int(np.count_nonzero(dan[step, matched_loc]))
|
||||
if external:
|
||||
external_at_gate = trace[loc].copy()
|
||||
max_error = float(np.max(np.abs(trace - run["trace_at_external"])))
|
||||
if max_error > 2e-5: raise RuntimeError(f"KC trace reconstruction mismatch {max_error}")
|
||||
if external or endogenous:
|
||||
elig = trace[loc]; active = comp & (elig > 0)
|
||||
before = E_all[active].copy()
|
||||
E_all[active] = np.maximum(bound, E_all[active] * (np.float32(1.0)-k*elig[active]))
|
||||
gate_rows.append({"seed":seed,"pairing":pairing,"step":step,"time_ms":step*sim.dt,
|
||||
"source": "both" if external and endogenous else ("external-US-associated" if external else "endogenous-feedback-associated"),
|
||||
"matched_endogenous_DAN_spikes":int(np.count_nonzero(dan[step,matched_loc])),
|
||||
"eligible_edges":int(active.sum()),"E_change_sum":float(np.sum(before-E_all[active]))})
|
||||
if external:
|
||||
last_age_ms = np.where(last[loc] >= 0, (step-last[loc])*sim.dt, np.inf)
|
||||
a = comp & (trace[loc] > 0)
|
||||
for local in np.flatnonzero(a):
|
||||
source = int(src[local]); age = float(last_age_ms[local])
|
||||
audit_rows.append({"seed":seed,"pairing":pairing,"edge_id":int(identity.edge_id.iat[local]),
|
||||
"source_neuron_idx":source,"source_body_id":int(identity.source_body_id.iat[local]),
|
||||
"target_neuron_idx":int(identity.target_neuron_idx.iat[local]),"compartment":str(identity.compartment.iat[local]),
|
||||
"identity_group":str(identity.identity_group.iat[local]),"update_time_ms":step*sim.dt,
|
||||
"eligibility_v2":float(trace[loc[local]]),"eligibility_short_shadow":float(short[loc[local]]),
|
||||
"event_coincidence_shadow":bool(age <= float(ncfg["event_coincidence_window_ms"])),
|
||||
"first_activation_only_shadow":bool(first[loc[local]] == last[loc[local]]),
|
||||
"last_local_KC_spike_age_ms":age,"first_local_KC_spike_ms":float(first[loc[local]]*sim.dt),
|
||||
"matched_external_DAN_arrival_ms":step*sim.dt,
|
||||
"endogenous_matched_DAN_simultaneous":bool(endogenous),
|
||||
"source_class": "external-US-associated", "delta_E_bounded":float(-k*trace[loc[local]]*E_ext[local])})
|
||||
if external_at_gate is None: raise RuntimeError("external gate missing")
|
||||
rec = hist_by[(int(seed),int(pairing))]
|
||||
active_loc = eid_to_local.loc[rec.edge_id.to_numpy(np.int64)].to_numpy(np.int32)
|
||||
err = float(np.max(np.abs(external_at_gate[active_loc] - rec.eligibility.to_numpy(np.float32)))) if len(active_loc) else 0.0
|
||||
exact = err <= 2e-5
|
||||
if not exact:
|
||||
mismatches.append({"seed":seed,"pairing":pairing,"max_abs_eligibility_error":err})
|
||||
for row in audit_rows[audit_start:]: row["frozen_trace_match"] = exact
|
||||
for row in gate_rows[gate_start:]: row["frozen_trace_match"] = exact
|
||||
# Advance the next trial from the sealed Phase 4M overlay, never from a divergent audit replay.
|
||||
old_E = rec.E_bounded_before.to_numpy(np.float32); new_E = rec.E_bounded_after.to_numpy(np.float32)
|
||||
if len(active_loc) and float(np.max(np.abs(E_ext[active_loc]-old_E))) > 1e-6:
|
||||
raise RuntimeError(f"sealed E history before-state mismatch {seed}/{pairing}")
|
||||
E_ext[active_loc] = new_E
|
||||
true_elig = rec.eligibility.to_numpy(np.float32)
|
||||
additive[active_loc] = np.maximum(-np.float32(cfg["eligibility_v2"]["max_fractional_depression"])*v.base_candidate[active_loc],
|
||||
additive[active_loc] - np.float32(cfg["eligibility_v2"]["eta_effective_mv"])*true_elig)
|
||||
if pairing in ncfg["checkpoints"]:
|
||||
with np.load(exp / "acquisition-trajectory" / f"E-seed-{seed}-pairings-{pairing}.npz") as f:
|
||||
chk_err = float(np.max(np.abs(E_ext-f["E_bounded"])))
|
||||
if chk_err > 1e-6: raise RuntimeError(f"external E reconstruction mismatch {seed}/{pairing}: {chk_err}")
|
||||
np.savez_compressed(exp / "endogenous-dan" / f"E-all-DAN-shadow-seed-{seed}-pairings-{pairing}.npz", E_all_DAN=E_all,
|
||||
E_external_only=E_ext, edge_id=identity.edge_id.to_numpy(np.int64))
|
||||
shadow_rows.append({"seed":seed,"pairing":pairing,"random_US_step":random_step,
|
||||
"external_matched_E_reduction_sum":float(np.sum(1-E_ext)),"all_DAN_shadow_E_reduction_sum":float(np.sum(1-E_all)),
|
||||
"matched_endogenous_DAN_steps":endogenous_steps,"matched_endogenous_DAN_spikes":endogenous_events,
|
||||
"random_US_trace_positive_edges":int(np.count_nonzero(comp & (random_trace>0))) if random_trace is not None else 0,
|
||||
"frozen_trace_match":exact,"max_abs_eligibility_error":err})
|
||||
if capture and run["all_events"] is not None:
|
||||
run["all_events"].to_parquet(exp / "whole-brain" / f"spike-events-seed-{seed}-pairings-{pairing}.parquet", index=False)
|
||||
if pairing % 20 == 0:
|
||||
print(json.dumps({"seed":seed,"pairing":pairing,"endogenous_steps":endogenous_steps,"E_external_sum":float(np.sum(1-E_ext)),"E_all_sum":float(np.sum(1-E_all))}),flush=True)
|
||||
pd.DataFrame(audit_rows).to_parquet(exp / "local-eligibility/event-level-updates.parquet", index=False)
|
||||
pd.DataFrame(gate_rows).to_parquet(exp / "dan-source/event-level-gates.parquet", index=False)
|
||||
pd.concat(kc_event_frames,ignore_index=True).to_parquet(exp / "local-eligibility/KC-spike-history.parquet",index=False)
|
||||
if dan_event_frames:
|
||||
pd.concat(dan_event_frames,ignore_index=True).to_parquet(exp / "dan-source/endogenous-DAN-spike-history.parquet",index=False)
|
||||
pd.DataFrame(shadow_rows).to_parquet(exp / "endogenous-dan/shadow-trajectory.parquet", index=False)
|
||||
write_json(exp / "regression/event-trace-reproduction.json", {"all_passed":len(mismatches)==0,"mismatched_trials":mismatches,
|
||||
"mismatched_count":len(mismatches),"audited_trial_count":len(shadow_rows),
|
||||
"next_trial_overlay_restored_from_sealed_Phase4M_history":True})
|
||||
pd.DataFrame(perf).to_parquet(exp / "performance/event-audit-calls.parquet", index=False)
|
||||
write_json(exp / "endogenous-dan/summary.json", {"mode_A": "observational frozen Phase 4M LIF with endogenous DAN spikes recorded",
|
||||
"mode_B": "external US-only plasticity gate, exact Phase 4M E reproduction",
|
||||
"all_DAN_mode": "counterfactual open-loop shadow E; same frozen Phase 4M additive brain trajectory; not a new brain model",
|
||||
"all_DAN_scope": "only metadata-confirmed DAN mapped to matched compartment; other DAN unresolved",
|
||||
"frozen_brain_dynamics_changed": False, "training_seed_count":len(ncfg["train_seeds"]),"pairings_per_seed":args.max_pairings})
|
||||
write_json(exp / "performance/event-audit.json", {"calls":len(perf),"total_wall_seconds":float(sum(x["wall_seconds"] for x in perf)),
|
||||
"mean_wall_seconds":float(np.mean([x["wall_seconds"] for x in perf])),"peak_VRAM_bytes":max(x["peak_vram_bytes"] for x in perf),
|
||||
"peak_RAM_KiB":int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss),
|
||||
"KC_boolean_history_bytes_per_trial":int(len(run["kc"])*len(v.kc)),"DAN_boolean_history_bytes_per_trial":int(len(run["dan"])*len(all_dan)),
|
||||
"no_dense_time_by_edge_tensor":True})
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4N synthesis, independent checks, and final SHA-256 manifest."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import json
|
||||
import resource
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
|
||||
from phase4h_audit import sha256, verify_checksum, write_json
|
||||
from phase4i import phase4c_regression
|
||||
from phase4k import dataset_checks, seal
|
||||
from phase4m import frozen_checks, frozen_hashes
|
||||
from phase4n import load_config
|
||||
from phase4n_finish import curves, shared_decomposition
|
||||
from phase4i import Validator
|
||||
from phase4l import OdorSimulator
|
||||
|
||||
|
||||
def contamination(exp, history):
|
||||
event = pd.read_parquet(exp / "local-eligibility/event-level-updates.parquet")
|
||||
if event.duplicated(["seed", "pairing", "edge_id"]).any():
|
||||
raise RuntimeError("duplicate local eligibility row")
|
||||
h = history[["train_seed", "pairing", "edge_id", "E_bounded_before", "E_bounded_after"]].rename(columns={"train_seed":"seed"})
|
||||
x = event.merge(h,on=["seed","pairing","edge_id"],how="left",validate="one_to_one")
|
||||
unmatched_runtime_rows=int(x.E_bounded_after.isna().sum())
|
||||
nonmatching_trace_rows=int((~x.frozen_trace_match).sum())
|
||||
excluded_B_rows=int(((~x.frozen_trace_match)&(x.identity_group=="B-specific")).sum())
|
||||
x=x[x.frozen_trace_match & x.E_bounded_after.notna()].copy()
|
||||
x["actual_E_depression"] = x.E_bounded_before-x.E_bounded_after
|
||||
age = x.last_local_KC_spike_age_ms.to_numpy(np.float64)
|
||||
x["temporal_credit_class"] = np.where(age <= 1.0,"coincident_<=1ms",np.where(age <= 20.0,"recent_1to20ms","long_trace_>20ms"))
|
||||
x.to_parquet(exp / "b-contamination/updated-edges-with-temporal-source.parquet",index=False)
|
||||
b=x[(x.identity_group=="B-specific")&(x.actual_E_depression>1e-7)]
|
||||
total=float(b.actual_E_depression.sum())
|
||||
fractions={str(k):float(g.actual_E_depression.sum()/total) if total else None for k,g in b.groupby("temporal_credit_class")}
|
||||
event_fraction={str(k):int(len(g)) for k,g in b.groupby("temporal_credit_class")}
|
||||
relative_short=np.divide(b.eligibility_short_shadow.to_numpy(np.float64),b.eligibility_v2.to_numpy(np.float64),
|
||||
out=np.zeros(len(b),np.float64),where=b.eligibility_v2.to_numpy(np.float64)>0)
|
||||
weighted_short=float(np.dot(b.actual_E_depression.to_numpy(np.float64),relative_short)/total) if total else None
|
||||
report={"B_specific_update_rows":int(len(b)),"B_specific_unique_edges":int(b.edge_id.nunique()),
|
||||
"B_specific_unique_source_KC":int(b.source_neuron_idx.nunique()),
|
||||
"first_B_specific_update_pairing_by_seed":{str(seed):int(frame.pairing.min()) for seed,frame in b.groupby("seed")},
|
||||
"B_specific_E_depression_sum_with_exact_replay":total,"temporal_credit_weighted_fraction":fractions,"temporal_credit_event_counts":event_fraction,
|
||||
"excluded_nonmatching_trace_rows":nonmatching_trace_rows,"excluded_nonmatching_B_rows":excluded_B_rows,"unmatched_runtime_rows":unmatched_runtime_rows,
|
||||
"external_matched_US_gate_fraction":1.0,"endogenous_DAN_as_gate_fraction_in_frozen_model":0.0,
|
||||
"causal_route_fractions":{"direct_A_driven":None,"recurrent_KC":None,"MBON_DAN_feedback":None,"olfactory_feedback":None,"unresolved":1.0},
|
||||
"reason":"Whole-brain source spike timing plus a positive anatomical edge is not proof of membrane-spike causation. Frozen Phase 4M history has no causal predecessor trace. Do not assign invented path percentages.",
|
||||
"shadow_short_tau_20ms_excluded_B_update_fraction":float(b.loc[b.eligibility_short_shadow<.01,"actual_E_depression"].sum()/total) if total else None,
|
||||
"shadow_short_tau_20ms_weighted_eligibility_ratio_to_frozen":weighted_short,
|
||||
"event_coincidence_1ms_included_B_update_fraction":float(b.loc[b.event_coincidence_shadow,"actual_E_depression"].sum()/total) if total else None}
|
||||
report["first_activation_only_included_B_update_fraction"]=(float(b.loc[b.first_activation_only_shadow,"actual_E_depression"].sum()/total) if total else None)
|
||||
write_json(exp / "b-contamination/summary.json",report)
|
||||
return report
|
||||
|
||||
|
||||
def dan_summary(exp, identity, ncfg, cfg=None):
|
||||
gates=pd.read_parquet(exp / "dan-source/event-level-gates.parquet")
|
||||
trail=pd.read_parquet(exp / "endogenous-dan/shadow-trajectory.parquet")
|
||||
source=gates.groupby("source").agg(gates=("source","size"),E_depression_sum=("E_change_sum","sum")).reset_index()
|
||||
external_only=trail[trail.pairing==100].external_matched_E_reduction_sum.mean()
|
||||
all_dan=trail[trail.pairing==100].all_DAN_shadow_E_reduction_sum.mean()
|
||||
report={"source_breakdown":source.to_dict("records"),"trial_count":len(trail),
|
||||
"matched_endogenous_DAN_spikes_total":int(trail.matched_endogenous_DAN_spikes.sum()),
|
||||
"external_only_E_depression_sum_mean_100":float(external_only),"all_DAN_shadow_E_depression_sum_mean_100":float(all_dan),
|
||||
"additional_all_DAN_shadow_E_depression_sum_mean_100":float(all_dan-external_only),
|
||||
"frozen_Phase4M_endogenous_DAN_teaching_gate_count":0,
|
||||
"all_DAN_shadow_interpretation":"counterfactual open-loop teaching-gate interpretation; not frozen model and not measured dopamine concentration"}
|
||||
if cfg is not None:
|
||||
mapping=pd.read_parquet(Path(cfg["paths"]["phase4h_root"])/"mappings/dan-to-mbon-confirmed.parquet")
|
||||
mapping=mapping[["source_neuron_idx","source_annotation_type","source_compartment_verbatim"]].drop_duplicates()
|
||||
matched_set=set(map(int,mapping.loc[mapping.source_compartment_verbatim.astype(str)==str(ncfg["matched_compartment_verbatim"]),"source_neuron_idx"]))
|
||||
mapping=mapping[["source_neuron_idx","source_annotation_type"]].drop_duplicates(subset=["source_neuron_idx"])
|
||||
events_file=exp/"dan-source/endogenous-DAN-spike-history.parquet"
|
||||
if events_file.exists():
|
||||
events=pd.read_parquet(events_file)
|
||||
events=events.merge(mapping,left_on="neuron_idx",right_on="source_neuron_idx",how="left")
|
||||
typ=events.source_annotation_type.fillna("unknown").astype(str)
|
||||
events["cluster"] = np.where(typ.str.startswith("PAM"),"PAM",np.where(typ.str.startswith("PPL1"),"PPL1","other/unknown"))
|
||||
report["endogenous_spike_cluster_counts"]={str(k):int(v) for k,v in events.cluster.value_counts().items()}
|
||||
report["confirmed_type_counts"]={str(k):int(v) for k,v in typ.value_counts().items()}
|
||||
report["matched_compartment_spikes"] = int(events.neuron_idx.isin(matched_set).sum())
|
||||
events.to_parquet(exp/"dan-source/endogenous-DAN-with-type.parquet",index=False)
|
||||
write_json(exp / "dan-source/summary.json",report)
|
||||
return report
|
||||
|
||||
|
||||
def shared_summary(exp):
|
||||
p=exp / "shared-edge/isolated-decomposition.parquet"
|
||||
if not p.exists(): return {"status":"NOT-RUN-NO-PEAK"}
|
||||
table=pd.read_parquet(p)
|
||||
agg=table.groupby(["decomposition","odor"]).agg(spike_L1=("MBON_spike_L1","mean"),transmission_reduction=("transmitted_reduction_percent","mean"),
|
||||
latency_delta=("first_latency_delta_ms","mean")).reset_index()
|
||||
write_json(exp / "shared-edge/summary.json",{"scope":"isolated replay only; existing edges never deleted", "group_means":agg.to_dict("records")})
|
||||
return agg.to_dict("records")
|
||||
|
||||
|
||||
def separation(exp, peak):
|
||||
whole=pd.read_parquet(exp / "whole-brain/checkpoint-responses.parquet")
|
||||
iso=pd.read_parquet(exp / "isolated/response-by-checkpoint.parquet")
|
||||
rows=[]
|
||||
for pair in sorted(whole.pairings.unique()):
|
||||
w=whole[whole.pairings==pair].pivot(index="train_seed",columns="odor",values="MBON_spike_L1")
|
||||
i=iso[iso.pairings==pair]
|
||||
whole_sep=float((w["A"]-w["B"]).mean())
|
||||
iso_sep=float(i.functional_separation_spike_L1.mean())
|
||||
rows.append({"pairings":int(pair),"isolated_A_minus_B_spike_L1":iso_sep,"wholebrain_A_minus_B_spike_L1":whole_sep,
|
||||
"wholebrain_specificity_penalty":iso_sep-whole_sep,
|
||||
"wholebrain_A_specific_seeds":int(((w["A"]>w["B"])&(w["A"]>0)).sum())})
|
||||
frame=pd.DataFrame(rows);frame.to_parquet(exp / "whole-brain/specificity-penalty.parquet",index=False)
|
||||
return {"trajectory":rows,"at_peak":next((r for r in rows if r["pairings"]==peak),None)}
|
||||
|
||||
|
||||
def control_summary(exp):
|
||||
original=json.loads((exp / "controls/summary.json").read_text())
|
||||
independent=json.loads((exp / "controls/independent-summary.json").read_text())
|
||||
wholebrain=json.loads((exp / "controls/wholebrain-summary.json").read_text())
|
||||
original.update({"B_matched_US":"RUN-INDEPENDENT-100-PAIRING-TRAJECTORY",
|
||||
"random_US_timing":"RUN-INDEPENDENT-100-PAIRING-TRAJECTORY",
|
||||
"unrelated_compartment_US":"RUN-INDEPENDENT-100-PAIRING-TRAJECTORY",
|
||||
"all_DAN_gate":"RUN-OPEN-LOOP-SHADOW-100-PAIRING-TRAJECTORY",
|
||||
"full_control_matrix_pass":False,"independent_results":"controls/independent-summary.json",
|
||||
"all_DAN_closed_loop":False,"independent_wholebrain":"controls/wholebrain-summary.json"})
|
||||
write_json(exp / "controls/summary.json",original)
|
||||
return original, independent, wholebrain
|
||||
|
||||
|
||||
def integrity(exp, ncfg, mcfg, cfg, basepath):
|
||||
gc.collect();torch.cuda.empty_cache()
|
||||
previous=frozen_checks(cfg)
|
||||
mroot=Path(mcfg["experiment_root"]);mmodel=Path(mcfg["model_root"])
|
||||
phase4m={"experiment":verify_checksum(mroot,mroot/"manifests/checksums.sha256")["passed"],
|
||||
"model":verify_checksum(mmodel,mmodel/"checksums.sha256")["passed"]}
|
||||
dataset=dataset_checks(cfg)
|
||||
before=json.loads((exp / "audit/frozen-integrity.json").read_text())
|
||||
# A second replay detects intermittent GPU/event divergence; it never rewrites frozen references.
|
||||
replay=phase4c_regression(cfg,basepath,exp)
|
||||
phase4m_summary=json.loads((mroot/"audit/final-summary.json").read_text())
|
||||
reference_hashes=phase4m_summary["integrity"]["after"]
|
||||
static={"previous_phases":previous["all_passed"],"phase4m":all(phase4m.values()),"phase1_to_3b":dataset["all_passed"],
|
||||
"frozen_hashes_unchanged":frozen_hashes(cfg)==reference_hashes}
|
||||
report={"static_checksums_all_passed":bool(all(static.values())),"static":static,
|
||||
"phase4c_reference_replay_first":before["phase4c_reference_regression"],"phase4c_reference_replay_second":replay,
|
||||
"dynamic_regression_all_passed":bool(before["phase4c_reference_regression"]["all_passed"] and replay["all_passed"]),
|
||||
"all_passed":bool(all(static.values()) and before["phase4c_reference_regression"]["all_passed"] and replay["all_passed"])}
|
||||
write_json(exp / "audit/frozen-integrity-final.json",report)
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument("--config",type=Path,required=True);args=ap.parse_args()
|
||||
ncfg,mcfg,cfg,basepath=load_config(args.config);exp=Path(ncfg["experiment_root"])
|
||||
identity=pd.read_parquet(exp / "edge-identity/frozen-edge-identity.parquet")
|
||||
response_curve=pd.read_parquet(exp/"specificity-curve/memory-specificity.parquet")
|
||||
eligible=response_curve[response_curve.A_specific_functional_seeds>=3]
|
||||
corrected_peak=None
|
||||
if not eligible.empty:
|
||||
best=float(eligible.mean_spike_L1_separation.max())
|
||||
eligible=eligible[np.isclose(eligible.mean_spike_L1_separation,best,atol=1e-9,rtol=0)]
|
||||
latency_best=float(eligible.mean_latency_separation_ms.max())
|
||||
eligible=eligible[eligible.mean_latency_separation_ms>=latency_best-float(cfg["model"]["dt_ms"])/2]
|
||||
corrected_peak=int(eligible.pairings.min())
|
||||
old_peak=json.loads((exp/"specificity-curve/peak.json").read_text())["pairings"]
|
||||
if corrected_peak!=old_peak:
|
||||
write_json(exp/"audit/peak-numerical-tie-correction.json",{"original_checkpoint":old_peak,"corrected_checkpoint":corrected_peak,
|
||||
"reason":"50 and 60 have the same five-seed spike-L1 separation; latency difference is sub-femtosecond float summation noise, far below 0.1 ms simulation dt. Earliest-tie rule selects 50.",
|
||||
"not_a_learning_parameter_change":True,"identity_and_E_trajectories_unchanged":True})
|
||||
write_json(exp/"specificity-curve/peak.json",{"status":"SPECIFICITY-PEAK-IDENTIFIED","pairings":corrected_peak,
|
||||
"selection_rule":ncfg["peak_rule"],"numerical_tie_correction_record":"audit/peak-numerical-tie-correction.json"})
|
||||
curves(ncfg,exp,pd.read_parquet(exp/"specificity-curve/CSI.parquet"),response_curve,identity)
|
||||
validator=Validator(cfg,basepath,exp);sim=OdorSimulator(validator,cfg,exp)
|
||||
shared_decomposition(ncfg,exp,identity,sim,validator,corrected_peak)
|
||||
del sim,validator
|
||||
stopping=json.loads((exp/"stopping-analysis/summary.json").read_text())
|
||||
stopping["functional_peak_checkpoint"]=corrected_peak
|
||||
write_json(exp/"stopping-analysis/summary.json",stopping)
|
||||
# Complete the explicitly predeclared t=0 checkpoint without rerunning any brain trial.
|
||||
groups0=pd.read_parquet(exp/"acquisition-trajectory/group-plasticity.parquet")
|
||||
csi0=pd.read_parquet(exp/"specificity-curve/CSI.parquet")
|
||||
if 0 not in set(groups0.pairings):
|
||||
z=[];cz=[]
|
||||
for seed in ncfg["train_seeds"]:
|
||||
one=np.ones(len(identity),np.float32)
|
||||
np.savez_compressed(exp/"acquisition-trajectory"/f"E-seed-{seed}-pairings-0.npz",E_bounded=one,E_unbounded=one,edge_id=identity.edge_id.to_numpy(np.int64))
|
||||
for mechanism in ("bounded","unbounded"):
|
||||
for group_name,count in identity.identity_group.value_counts().items():
|
||||
z.append({"seed":seed,"pairings":0,"mechanism":mechanism,"group":group_name,"total_edges":int(count),
|
||||
"updated_edges":0,"mean_delta_E":0.0,"median_delta_E":0.0,"weighted_transmission_reduction":0.0,"E_change_total":0.0})
|
||||
cz.append({"seed":seed,"pairings":0,"mechanism":mechanism,"A-specific":0.0,"B-specific":0.0,"shared":0.0,"inactive":0.0,
|
||||
"CSI":0.0,"A_to_shared_ratio":np.nan,"A_to_B_ratio":np.nan})
|
||||
pd.concat([groups0,pd.DataFrame(z)],ignore_index=True).sort_values(["seed","pairings","mechanism","group"]).to_parquet(exp/"acquisition-trajectory/group-plasticity.parquet",index=False)
|
||||
pd.concat([csi0,pd.DataFrame(cz)],ignore_index=True).sort_values(["seed","pairings","mechanism"]).to_parquet(exp/"specificity-curve/CSI.parquet",index=False)
|
||||
history=pd.read_parquet(Path(mcfg["experiment_root"])/"efficacy-overlay/update-history.parquet")
|
||||
peak=json.loads((exp/"specificity-curve/peak.json").read_text())["pairings"]
|
||||
onset=json.loads((exp/"specificity-curve/generalization-onset.json").read_text())["pairings"]
|
||||
contam=contamination(exp,history);dan=dan_summary(exp,identity,ncfg,cfg);shared=shared_summary(exp)
|
||||
timed_inputs=json.loads((exp/"whole-brain/B-specific-timed-input-summary.json").read_text())
|
||||
sep=separation(exp,peak);controls,independent,control_whole=control_summary(exp)
|
||||
gate_specificity=json.loads((exp/"endogenous-dan/gate-specificity.json").read_text())
|
||||
perf=json.loads((exp / "performance/summary.json").read_text())
|
||||
audit_perf=json.loads((exp / "performance/event-audit.json").read_text())
|
||||
ctrl_perf={"training_calls":independent["training_calls"],"training_wall_seconds":independent["training_wall_seconds"],
|
||||
"mean_call_seconds":independent["mean_call_seconds"],"wholebrain_wall_seconds":control_whole["total_brain_wall_seconds"]}
|
||||
perf.update({"event_audit":audit_perf,"controls":ctrl_perf,"peak_RAM_KiB_all_stages":max(perf["peak_RAM_KiB"],audit_perf["peak_RAM_KiB"],independent["peak_RAM_KiB"]),
|
||||
"shadow_trace_overhead":"included in event-audit call time; no paired unaudited timing isolation"})
|
||||
write_json(exp / "performance/summary.json",perf)
|
||||
integ=integrity(exp,ncfg,mcfg,cfg,basepath)
|
||||
event_reproduction=json.loads((exp/"regression/event-trace-reproduction.json").read_text())
|
||||
csi=pd.read_parquet(exp / "specificity-curve/CSI.parquet")
|
||||
group=pd.read_parquet(exp / "acquisition-trajectory/group-plasticity.parquet")
|
||||
iso=pd.read_parquet(exp / "specificity-curve/memory-specificity.parquet")
|
||||
long_fraction=contam["temporal_credit_weighted_fraction"].get("long_trace_>20ms")
|
||||
shared_b=next((r["spike_L1"] for r in shared if r["decomposition"]=="A+shared" and r["odor"]=="B"),None) if isinstance(shared,list) else None
|
||||
a_only_b=next((r["spike_L1"] for r in shared if r["decomposition"]=="A-only" and r["odor"]=="B"),None) if isinstance(shared,list) else None
|
||||
mechanism_tags={"SPECIFICITY-PEAK-IDENTIFIED":peak is not None,
|
||||
"GENERALIZATION-ONSET-IDENTIFIED":onset is not None,
|
||||
"ENDOGENOUS-DAN-CREDIT-CONTAMINATION":False,
|
||||
"RECURRENT-KC-CREDIT-CONTAMINATION":None,
|
||||
"LONG-TRACE-CREDIT-CONTAMINATION":bool(long_fraction is not None and long_fraction>0),
|
||||
"SHARED-EDGE-GENERALIZATION-DOMINANT":bool(shared_b is not None and a_only_b is not None and shared_b>a_only_b),
|
||||
"NO-SINGLE-DOMINANT-CONTAMINATION-SOURCE":True}
|
||||
audit_complete=bool(event_reproduction["all_passed"] and integ["all_passed"] and controls["full_control_matrix_pass"])
|
||||
summary={"phase":"4N","status":"MECHANISM-AUDIT-COMPLETE-NO-CANDIDATE" if audit_complete else "PARTIAL-MECHANISM-AUDIT-NO-CANDIDATE",
|
||||
"edge_groups":identity.identity_group.value_counts().to_dict(),"specificity_peak_pairings":peak,
|
||||
"generalization_onset_pairings_by_predeclared_CSI_rule":onset,
|
||||
"functional_collapse_first_checkpoint":next((int(r.pairings) for _,r in iso.iterrows() if r.pairings>peak and r.A_specific_functional_seeds<3),None) if peak is not None else None,
|
||||
"group_curves":group[group.mechanism=="bounded"].groupby(["pairings","group"]).mean(numeric_only=True).reset_index()[["pairings","group","mean_delta_E","weighted_transmission_reduction"]].to_dict("records"),
|
||||
"isolated_curve":iso.to_dict("records"),"CSI_curve":csi[csi.mechanism=="bounded"].groupby("pairings").CSI.mean().reset_index().to_dict("records"),
|
||||
"B_specific_contamination":contam,"B_specific_timed_input_candidates":timed_inputs,"DAN":dan,"gate_specificity":gate_specificity,"shared_edge_isolated":shared,"wholebrain_specificity":sep,
|
||||
"controls":controls,"control_wholebrain":control_whole,"event_trace_reproduction":event_reproduction,"mechanism_tags":mechanism_tags,"candidate_generated":False,"candidate_gate":{"external_vs_endogenous_distinguished":True,
|
||||
"causal_B_contamination_route_identified":False,"isolated_and_wholebrain_majority_improvement_demonstrated":False},
|
||||
"frozen_integrity":integ,"limitations":["Temporal predecessor class unresolved; no causal path attribution percentage claimed",
|
||||
"All-DAN efficacy is an open-loop shadow, not a closed-loop retrained brain",
|
||||
"Phase 4C mechanosensory frozen regression showed at least one divergent run; static SHA-256 passes but dynamic integrity cannot be called PASS"]}
|
||||
write_json(exp / "audit/final-summary.json",summary)
|
||||
model=Path(ncfg["model_root"]);model.mkdir(parents=True,exist_ok=True)
|
||||
for name in ("phase4n.py","phase4n_finish.py","phase4n_events.py","phase4n_controls.py","phase4n_control_whole.py","phase4n_alldan.py","phase4n_paths.py","phase4n_preflight.py","phase4n_finalize.py","phase4n.yaml"):
|
||||
source=Path(__file__).with_name(name)
|
||||
shutil.copy2(source,model/name)
|
||||
shutil.copy2(source,exp/"audit"/name)
|
||||
write_json(model/"VERSION.json",{"phase":"4N","status":summary["status"],"candidate_generated":False,
|
||||
"identity_sha256":sha256(exp/"edge-identity/frozen-edge-identity.parquet"),"config_sha256":sha256(args.config),
|
||||
"frozen_static_integrity":integ["static_checksums_all_passed"],"frozen_dynamic_regression":integ["dynamic_regression_all_passed"]})
|
||||
seal(model,model/"MANIFEST.json",model/"checksums.sha256")
|
||||
seal(exp,exp/"manifests/manifest.json",exp/"manifests/checksums.sha256")
|
||||
self_check={}
|
||||
for root,relative in ((model,"checksums.sha256"),(exp,"manifests/checksums.sha256")):
|
||||
result=subprocess.run(["sha256sum","-c",relative],cwd=root,stdout=subprocess.DEVNULL,stderr=subprocess.PIPE,text=True)
|
||||
self_check[str(root)]=result.returncode==0
|
||||
if not all(self_check.values()):raise RuntimeError(f"Phase4N self-checksum failed {self_check}")
|
||||
print(json.dumps({"status":summary["status"],"peak":peak,"onset":onset,"B_contamination":contam,
|
||||
"DAN":dan,"wholebrain_penalty":sep["at_peak"],"integrity":integ,"self_checks":self_check}),flush=True)
|
||||
|
||||
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4N checkpoint-specific whole-brain/credit audit and conservative sealing."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import json
|
||||
import resource
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
|
||||
from phase4h_audit import sha256, verify_checksum, write_json
|
||||
from phase4i import Validator, phase4c_regression
|
||||
from phase4j import effect
|
||||
from phase4k import dataset_checks, seal
|
||||
from phase4l import OdorSimulator, pn_drive
|
||||
from phase4m import frozen_checks, frozen_hashes, load_odor_inputs
|
||||
from phase4n import GROUPS, load_config
|
||||
|
||||
|
||||
def state(exp, seed, pair):
|
||||
if pair == 0:
|
||||
return np.ones(14232, np.float32)
|
||||
with np.load(exp / "acquisition-trajectory" / f"E-seed-{seed}-pairings-{pair}.npz") as f:
|
||||
return f["E_bounded"].copy()
|
||||
|
||||
|
||||
def curves(ncfg, exp, csi, replay, identity):
|
||||
csi = csi[csi.mechanism == "bounded"]
|
||||
peak = json.loads((exp / "specificity-curve/peak.json").read_text())["pairings"]
|
||||
onset = None
|
||||
if peak is not None:
|
||||
p = csi[csi.pairings == peak]
|
||||
baseline_csi = p.CSI.mean()
|
||||
baseline_other = max(p.shared.mean(), p["B-specific"].mean())
|
||||
for pair in ncfg["checkpoints"]:
|
||||
if pair <= peak:
|
||||
continue
|
||||
q = csi[csi.pairings == pair]
|
||||
other = max(q.shared.mean(), q["B-specific"].mean())
|
||||
if q.CSI.mean() <= .8 * baseline_csi and other >= 1.1 * baseline_other and baseline_csi > 0 and baseline_other > 0:
|
||||
onset = pair
|
||||
break
|
||||
write_json(exp / "specificity-curve/generalization-onset.json", {"status": "GENERALIZATION-ONSET-IDENTIFIED" if onset is not None else "UNIDENTIFIED",
|
||||
"pairings": onset, "rule": ncfg["generalization_onset_rule"]})
|
||||
groups = pd.read_parquet(exp / "acquisition-trajectory/group-plasticity.parquet")
|
||||
groups = groups[groups.mechanism == "bounded"]
|
||||
# Dependency-free vector plot; values remain in the accompanying Parquet files.
|
||||
parts = ['<svg xmlns="http://www.w3.org/2000/svg" width="900" height="640" viewBox="0 0 900 640">',
|
||||
'<rect width="900" height="640" fill="white"/>', '<text x="55" y="30" font-size="17">Phase 4N acquisition and specificity</text>']
|
||||
def panel(series, y0, height, title):
|
||||
allv = np.concatenate([np.asarray(v, np.float64) for _, v, _ in series]); lo, hi = float(np.min(allv)), float(np.max(allv))
|
||||
if hi <= lo: hi = lo + 1.0
|
||||
parts.append(f'<text x="55" y="{y0-10}" font-size="13">{title}</text>')
|
||||
parts.append(f'<path d="M60 {y0} V{y0+height} H850" fill="none" stroke="black"/>')
|
||||
for label, vals, color in series:
|
||||
xs = np.asarray(ncfg["checkpoints"], np.float64)
|
||||
ys = np.asarray(vals, np.float64)
|
||||
coords = ' '.join(f'{60+790*x/100:.1f},{y0+height-20-(height-35)*(y-lo)/(hi-lo):.1f}' for x,y in zip(xs,ys))
|
||||
parts.append(f'<polyline points="{coords}" fill="none" stroke="{color}" stroke-width="2"/>')
|
||||
parts.append(f'<text x="{625}" y="{y0+18+17*len([s for s in series if s[0] < label])}" font-size="11" fill="{color}">{label}</text>')
|
||||
for p,color in ((peak,"green"),(onset,"red")):
|
||||
if p is not None:
|
||||
x = 60 + 790*p/100
|
||||
parts.append(f'<path d="M{x:.1f} {y0} V{y0+height}" stroke="{color}" stroke-dasharray="4,4"/>')
|
||||
first = []
|
||||
for name,color in zip(GROUPS,("#1261a0","#d4871e","#b72230","#777777")):
|
||||
g = groups[groups.group == name].groupby("pairings").weighted_transmission_reduction.mean().reindex(ncfg["checkpoints"]).fillna(0.0)
|
||||
first.append((name,g.to_numpy(),color))
|
||||
panel(first, 80, 210, "Mean anatomical-weighted E reduction")
|
||||
panel([("A-B spike L1",replay.mean_spike_L1_separation.to_numpy(),"#2059a6"),
|
||||
("A-B latency ms",replay.mean_latency_separation_ms.to_numpy(),"#b72230")], 370, 210, "Functional MBON effect difference")
|
||||
parts.append('</svg>')
|
||||
(exp / "specificity-curve/trajectory.svg").write_text('\n'.join(parts), encoding="utf-8")
|
||||
return peak, onset
|
||||
|
||||
|
||||
def shared_decomposition(ncfg, exp, identity, sim, v, peak):
|
||||
if peak is None:
|
||||
write_json(exp / "shared-edge/summary.json", {"status": "NOT-RUN-NO-SPECIFICITY-PEAK"})
|
||||
return None
|
||||
from phase4m import ReplayEngine, replay_effect
|
||||
comp = identity.compartment.astype(str).to_numpy() == str(ncfg["matched_compartment_verbatim"])
|
||||
targets = np.unique(identity.loc[comp, "target_neuron_idx"].to_numpy(np.int32))
|
||||
engine = ReplayEngine(sim, targets)
|
||||
w = identity.anatomical_weight_mv.to_numpy(np.float32)
|
||||
groups = identity.identity_group.astype(str).to_numpy()
|
||||
rows = []
|
||||
from phase4n import load_config
|
||||
_, mcfg, _, _ = load_config(Path(__file__).with_name("phase4n.yaml"))
|
||||
for seed, es in zip(ncfg["train_seeds"], ncfg["evaluation_seeds"]):
|
||||
E = state(exp, seed, peak)
|
||||
for odor in ("A", "B"):
|
||||
spikes = pd.read_parquet(Path(mcfg["experiment_root"]) / "isolated-replay" / f"KC-spikes-ODOR_{odor}-seed-{es}.parquet")
|
||||
baseline = engine.run(spikes, np.zeros(len(E), np.float32), float(mcfg["protocol"]["duration_ms"]))
|
||||
for label, included in (("A-only", ("A-specific",)), ("A+shared", ("A-specific", "shared")),
|
||||
("A+shared+B", ("A-specific", "shared", "B-specific")), ("all-updated", GROUPS)):
|
||||
delta = np.where(np.isin(groups, included), w * (E - 1), 0).astype(np.float32)
|
||||
post = engine.run(spikes, delta, float(mcfg["protocol"]["duration_ms"]))
|
||||
metrics = replay_effect(baseline, post, sim.dt)
|
||||
rows.append({"train_seed": seed, "eval_seed": es, "pairings": peak, "odor": odor, "decomposition": label,
|
||||
**{k: val for k, val in metrics.items() if k != "isolated_input_scope"}})
|
||||
pd.DataFrame(rows).to_parquet(exp / "shared-edge/isolated-decomposition.parquet", index=False)
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def wholebrain(ncfg, mcfg, cfg, exp, identity, sim, v, peak):
|
||||
wanted = sorted(set([10, 20, 50, 75, 100] + ([peak] if peak is not None else [])))
|
||||
panel, vectors, selection, mapping = load_odor_inputs(cfg)
|
||||
gloms = selection["glomeruli_order"]
|
||||
matched = float(np.median(panel.mapped_response_sum))
|
||||
pn = {}
|
||||
for label in ("A", "B"):
|
||||
oid = selection[f"ODOR_{label}"]["odor_id"]
|
||||
pn[label] = pn_drive(oid, vectors[oid], mapping, gloms, str(mcfg["protocol"]["input_mode"]), matched, cfg)[:2]
|
||||
dan = np.unique(v.confirmed.source_neuron_idx.to_numpy(np.int32))
|
||||
group_sources = {name: np.unique(identity.loc[identity.identity_group == name, "source_neuron_idx"].to_numpy(np.int32)) for name in GROUPS}
|
||||
w = identity.anatomical_weight_mv.to_numpy(np.float32)
|
||||
rows, calls = [], []
|
||||
for seed, es in zip(ncfg["train_seeds"], ncfg["evaluation_seeds"]):
|
||||
baseline = {label: sim.run_rates(*pn[label], es, float(mcfg["protocol"]["duration_ms"]), collect_kc_spikes=True) for label in pn}
|
||||
for pair in wanted:
|
||||
delta = w * (state(exp, seed, pair) - 1)
|
||||
for label in ("A", "B"):
|
||||
post = sim.run_rates(*pn[label], es, float(mcfg["protocol"]["duration_ms"]), delta=delta, collect_kc_spikes=True)
|
||||
calls.append(post["wall_seconds"])
|
||||
eff = effect(baseline[label], post)
|
||||
row = {"train_seed": seed, "eval_seed": es, "pairings": pair, "odor": label,
|
||||
"MBON_spike_L1": eff["MBON_spike_l1"], "MBON_g_max_abs_mv": eff["MBON_g_max_abs"],
|
||||
"MBON_v_max_abs_mv": eff["MBON_v_max_abs"], "MBON_latency_delta_ms": eff["MBON_first_latency_delta_ms"],
|
||||
"downstream_spike_L1": eff["downstream_spike_l1"], "KC_spike_L1": int(np.abs(post["kc_counts"]-baseline[label]["kc_counts"]).sum()),
|
||||
"endogenous_DAN_spikes": int(post["counts"][dan].sum()), "baseline_endogenous_DAN_spikes": int(baseline[label]["counts"][dan].sum()),
|
||||
"finite": bool(post["finite"]), "peak_vram_bytes": int(post["peak_vram_bytes"]), "wall_seconds": post["wall_seconds"]}
|
||||
for group, nodes in group_sources.items():
|
||||
row[f"{group}_KC_spikes"] = int(post["counts"][nodes].sum())
|
||||
row[f"{group}_KC_newly_active_vs_E1"] = int(np.count_nonzero((post["counts"][nodes] > 0) & (baseline[label]["counts"][nodes] == 0)))
|
||||
row["MBON_spikes"] = int(post["mbon_counts"].sum())
|
||||
row["other_feedback_spikes"] = int(post["downstream_counts"].sum())
|
||||
rows.append(row)
|
||||
frame = pd.DataFrame(rows)
|
||||
frame.to_parquet(exp / "whole-brain/checkpoint-responses.parquet", index=False)
|
||||
write_json(exp / "performance/whole-brain.json", {"calls": len(calls), "mean_wall_seconds": float(np.mean(calls)),
|
||||
"sum_wall_seconds": float(np.sum(calls)), "max_peak_vram_bytes": int(frame.peak_vram_bytes.max())})
|
||||
return frame
|
||||
|
||||
|
||||
def audit_updates(ncfg, mcfg, exp, identity, history, csi, peak):
|
||||
group = identity.set_index("edge_id").identity_group
|
||||
hist = history.copy()
|
||||
hist["identity_group"] = group.loc[hist.edge_id.to_numpy(np.int64)].to_numpy()
|
||||
hist["delta_E_bounded"] = hist.E_bounded_after - hist.E_bounded_before
|
||||
hist["DAN_source"] = "external-US-associated"
|
||||
hist["matched_compartment"] = True
|
||||
hist["endogenous_DAN_used_as_gate"] = False
|
||||
hist["local_KC_trace_nonzero"] = hist.eligibility > 0
|
||||
hist.to_parquet(exp / "local-eligibility/phase4m-updates-classified.parquet", index=False)
|
||||
first = hist[(hist.identity_group == "B-specific") & (hist.delta_E_bounded < -1e-7)].groupby("train_seed").pairing.min().reset_index(name="first_B_specific_update_pairing")
|
||||
first.to_parquet(exp / "b-contamination/first-B-updates.parquet", index=False)
|
||||
causes = {"direct_A_driven_KC_activity": None, "recurrent_KC_reactivation": None, "MBON_DAN_feedback": None,
|
||||
"olfactory_feedback": None, "long_eligibility_trace": None, "endogenous_DAN_teaching_interpretation": 0.0,
|
||||
"unresolved_runtime_KC_recruitment_or_trace": 1.0}
|
||||
write_json(exp / "b-contamination/provenance.json", {"classification": causes,
|
||||
"reason": "Frozen Phase 4M update history records trace at forced matched-US but no event-level causal predecessor or KC spike times. Assigning a recurrent/feedback percentage would be invented.",
|
||||
"earliest_B_update_by_seed": first.to_dict("records"), "all_updates_gated_by_external_matched_US": True})
|
||||
write_json(exp / "dan-source/phase4m-gate-audit.json", {"external_US_gates": int(history[["train_seed", "pairing"]].drop_duplicates().shape[0]),
|
||||
"endogenous_DAN_as_teaching_gate": 0, "policy": "Phase 4M implementation opens plasticity once at imposed 90 ms US, not at endogenous DAN spike times",
|
||||
"observed_endogenous_DAN_activity_is_not_equivalent_to_dopamine_concentration": True})
|
||||
score = csi[csi.mechanism == "bounded"].groupby("pairings").CSI.mean()
|
||||
write_json(exp / "stopping-analysis/summary.json", {"status": "POST-HOC-ONLY-NO-ADAPTIVE-POLICY",
|
||||
"peak_CSI_checkpoint": int(score.idxmax()), "peak_CSI": float(score.max()), "functional_peak_checkpoint": peak,
|
||||
"online_validated_criterion": False, "heldout_training_validation_required": True})
|
||||
|
||||
|
||||
def controls(exp, identity, ncfg, mcfg):
|
||||
rows = []
|
||||
for seed in ncfg["train_seeds"]:
|
||||
for pair in ncfg["checkpoints"]:
|
||||
E = state(exp, seed, pair)
|
||||
rows += [{"seed": seed, "pairings": pair, "condition": "A+matched-US", "E_changed": int(np.count_nonzero(E < 1 - 1e-7)), "status": "RUN-RECONSTRUCTED-FROZEN-PHASE4M"},
|
||||
{"seed": seed, "pairings": pair, "condition": "A+D0", "E_changed": 0, "status": "EXACT-ZERO-GATE-IDENTITY"},
|
||||
{"seed": seed, "pairings": pair, "condition": "frozen-E1", "E_changed": 0, "status": "EXACT-FROZEN-IDENTITY"},
|
||||
{"seed": seed, "pairings": pair, "condition": "external-US-only-gate", "E_changed": int(np.count_nonzero(E < 1 - 1e-7)), "status": "IDENTICAL-TO-FROZEN-PHASE4M-RULE"}]
|
||||
pd.DataFrame(rows).to_parquet(exp / "controls/trajectory-status.parquet", index=False)
|
||||
report = {"A_matched_US": "RUN-PHASE4M-TRAJECTORY-RECONSTRUCTED", "A_D0": "ANALYTIC-EXACT-IDENTITY", "frozen_E1": "ANALYTIC-EXACT-IDENTITY",
|
||||
"external_US_only": "RUN-IDENTICAL-TO-PHASE4M", "B_matched_US": "NOT-RUN", "random_US_timing": "NOT-RUN",
|
||||
"unrelated_compartment_US": "NOT-RUN", "all_DAN_gate": "NOT-RUN", "full_control_matrix_pass": False}
|
||||
write_json(exp / "controls/summary.json", report)
|
||||
return report
|
||||
|
||||
|
||||
def integrity(ncfg, mcfg, cfg, base_path, exp, frozen_before):
|
||||
gc.collect(); torch.cuda.empty_cache()
|
||||
checks = frozen_checks(cfg)
|
||||
mroot, mm = Path(mcfg["experiment_root"]), Path(mcfg["model_root"])
|
||||
mchecks = {"phase4m_exp": verify_checksum(mroot, mroot / "manifests/checksums.sha256")["passed"],
|
||||
"phase4m_model": verify_checksum(mm, mm / "checksums.sha256")["passed"]}
|
||||
dataset = dataset_checks(cfg)
|
||||
regression = phase4c_regression(cfg, base_path, exp)
|
||||
after = frozen_hashes(cfg)
|
||||
report = {"all_passed": bool(checks["all_passed"] and all(mchecks.values()) and dataset["all_passed"] and regression["all_passed"] and frozen_before == after),
|
||||
"phase4c_to_4l": checks, "phase4m": mchecks, "phase1_to_3b": dataset, "phase4c_reference_regression": regression,
|
||||
"before_after_identical": frozen_before == after, "base_csr_unmodified": True, "APL_v2_encoder_mask_unmodified": True}
|
||||
write_json(exp / "audit/frozen-integrity.json", report)
|
||||
return report
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True); args = ap.parse_args()
|
||||
ncfg, mcfg, cfg, base_path = load_config(args.config)
|
||||
exp = Path(ncfg["experiment_root"])
|
||||
identity = pd.read_parquet(exp / "edge-identity/frozen-edge-identity.parquet")
|
||||
csi = pd.read_parquet(exp / "specificity-curve/CSI.parquet")
|
||||
replay = pd.read_parquet(exp / "specificity-curve/memory-specificity.parquet")
|
||||
history = pd.read_parquet(Path(mcfg["experiment_root"]) / "efficacy-overlay/update-history.parquet")
|
||||
before = frozen_hashes(cfg)
|
||||
peak, onset = curves(ncfg, exp, csi, replay, identity)
|
||||
print(json.dumps({"stage": "specificity", "peak": peak, "generalization_onset": onset}), flush=True)
|
||||
v = Validator(cfg, base_path, exp); sim = OdorSimulator(v, cfg, exp)
|
||||
shared = shared_decomposition(ncfg, exp, identity, sim, v, peak)
|
||||
print(json.dumps({"stage": "shared_decomposition", "rows": 0 if shared is None else len(shared)}), flush=True)
|
||||
whole = wholebrain(ncfg, mcfg, cfg, exp, identity, sim, v, peak)
|
||||
print(json.dumps({"stage": "wholebrain", "rows": len(whole)}), flush=True)
|
||||
audit_updates(ncfg, mcfg, exp, identity, history, csi, peak)
|
||||
control = controls(exp, identity, ncfg, mcfg)
|
||||
del sim, v
|
||||
integ = integrity(ncfg, mcfg, cfg, base_path, exp, before)
|
||||
ru = resource.getrusage(resource.RUSAGE_SELF)
|
||||
perf = {"peak_RAM_KiB": int(ru.ru_maxrss), "edge_audit_rows": int(len(history)),
|
||||
"edge_audit_memory_bytes": int(history.memory_usage(deep=True).sum()), "shadow_trace_overhead": "NOT-MEASURED-NOT-RUN",
|
||||
"wholebrain": json.loads((exp / "performance/whole-brain.json").read_text()),
|
||||
"isolated": json.loads((exp / "performance/isolated.json").read_text())}
|
||||
write_json(exp / "performance/summary.json", perf)
|
||||
summary = {"phase": "4N", "status": "PARTIAL-MECHANISM-AUDIT-NO-CANDIDATE", "peak": peak, "generalization_onset": onset,
|
||||
"edge_groups": identity.identity_group.value_counts().to_dict(), "controls": control, "frozen_integrity": integ["all_passed"],
|
||||
"candidate_generated": False, "limitations": ["Endogenous DAN event-level gate overlay not run", "Full matched control trajectories not run", "Runtime causal predecessor and local shadow traces not captured; contamination source percentages unresolved"]}
|
||||
write_json(exp / "audit/final-summary.json", summary)
|
||||
model = Path(ncfg["model_root"]); model.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(args.config, model / "phase4n.yaml"); shutil.copy2(Path(__file__).resolve(), model / "phase4n_finish.py")
|
||||
shutil.copy2(Path(__file__).with_name("phase4n.py"), model / "phase4n.py")
|
||||
shutil.copy2(Path(__file__).resolve(), exp / "audit/phase4n_finish.py")
|
||||
write_json(model / "VERSION.json", {"phase": "4N", "status": summary["status"], "candidate_generated": False,
|
||||
"identity_sha256": sha256(exp / "edge-identity/frozen-edge-identity.parquet"), "config_sha256": sha256(args.config)})
|
||||
seal(model, model / "MANIFEST.json", model / "checksums.sha256")
|
||||
seal(exp, exp / "manifests/manifest.json", exp / "manifests/checksums.sha256")
|
||||
print(json.dumps(summary), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Timing-compatible incoming KC drive paths; not unique causal-spike proofs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from phase4h_audit import write_json
|
||||
from phase4n import load_config
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument("--config",type=Path,required=True);args=ap.parse_args()
|
||||
ncfg,mcfg,cfg,_=load_config(args.config);exp=Path(ncfg["experiment_root"])
|
||||
identity=pd.read_parquet(exp/"edge-identity/frozen-edge-identity.parquet")
|
||||
bnodes=np.unique(identity.loc[identity.identity_group=="B-specific","source_neuron_idx"].to_numpy(np.int32))
|
||||
root=Path(cfg["paths"]["connectome_root"])
|
||||
indptr=np.load(root/"gpu/csr-indptr.int64.npy",mmap_mode="r")
|
||||
indices=np.load(root/"gpu/csr-indices.int32.npy",mmap_mode="r")
|
||||
raw=np.load(root/"gpu/csr-weights.int64.npy",mmap_mode="r")
|
||||
signs=np.load("/data/digital-drosophila/experiments/phase4a-baseline/outputs/derived/synaptic-sign.int8.npy",mmap_mode="r")
|
||||
meta=pd.read_parquet(root/"neuron-index/unified-metadata.parquet",columns=["neuron_idx","body_id","annotation_class","annotation_type"])
|
||||
meta=meta.sort_values("neuron_idx").reset_index(drop=True)
|
||||
if len(signs)!=len(meta) or len(indptr)!=len(meta)+1:raise RuntimeError("frozen graph/schema mismatch")
|
||||
dan=pd.read_parquet(Path(cfg["paths"]["phase4h_root"])/"mappings/dan-to-mbon-confirmed.parquet",columns=["source_neuron_idx"])
|
||||
danset=set(map(int,dan.source_neuron_idx.unique()))
|
||||
edge_ids=np.flatnonzero(np.isin(np.asarray(indices),bnodes)).astype(np.int64)
|
||||
targets=np.asarray(indices[edge_ids],np.int32)
|
||||
sources=(np.searchsorted(indptr,edge_ids,side="right")-1).astype(np.int32)
|
||||
weights=np.asarray(raw[edge_ids],np.float32)*np.asarray(signs[sources],np.float32)*np.float32(cfg["model"]["w_syn_mv_per_anatomical_synapse"])
|
||||
by_target={int(n):np.flatnonzero(targets==n) for n in bnodes}
|
||||
classes=meta.annotation_class.fillna("").astype(str).to_numpy()
|
||||
def source_class(n):
|
||||
cls=classes[n]
|
||||
if cls in ("ALPN","olfactory"):return "PN/olfactory"
|
||||
if cls=="Kenyon_Cell":return "KC recurrent"
|
||||
if cls=="MBON":return "MBON feedback"
|
||||
if n in danset:return "DAN"
|
||||
if "APL" in str(meta.annotation_type.iat[n]):return "APL anatomical"
|
||||
return "other CNS"
|
||||
rows=[];pairings=[10,20,50,60,75,100]
|
||||
dt=float(cfg["model"]["dt_ms"]);delay=int(round(float(cfg["model"]["synaptic_delay_ms"])/dt))
|
||||
tau=float(cfg["model"]["synaptic_tau_ms"]);window=int(round(5*tau/dt))
|
||||
for seed in ncfg["train_seeds"]:
|
||||
for pair in pairings:
|
||||
file=exp/"whole-brain"/f"spike-events-seed-{seed}-pairings-{pair}.parquet"
|
||||
events=pd.read_parquet(file)
|
||||
steps_by_node={int(n):g.step.to_numpy(np.int32) for n,g in events.groupby("neuron_idx")}
|
||||
for target in bnodes:
|
||||
times=steps_by_node.get(int(target))
|
||||
if times is None or not len(times):continue
|
||||
t=int(times[0])
|
||||
if t>int(round(float(mcfg["protocol"]["dan_time_ms"])/dt)):continue
|
||||
cat_totals=defaultdict(float);rank=[]
|
||||
for position in by_target[int(target)]:
|
||||
src=int(sources[position]);w=float(weights[position]);
|
||||
if w<=0:continue
|
||||
source_steps=steps_by_node.get(src)
|
||||
if source_steps is None:continue
|
||||
arrived=source_steps+delay
|
||||
active=arrived[(arrived<=t)&(arrived>=t-window)]
|
||||
if not len(active):continue
|
||||
contribution=float(np.sum(w*np.exp(-(t-active)*dt/tau)))
|
||||
category=source_class(src);cat_totals[category]+=contribution
|
||||
latest=int(active[-1])
|
||||
rank.append((contribution,int(edge_ids[position]),src,latest,category,w))
|
||||
rank.sort(reverse=True)
|
||||
top=rank[0] if rank else None
|
||||
rows.append({"seed":seed,"pairing":pair,"B_specific_KC_neuron_idx":int(target),"B_specific_KC_body_id":int(meta.body_id.iat[target]),
|
||||
"first_activation_step":t,"first_activation_ms":t*dt,"candidate_predecessor_neuron_idx":None if top is None else top[2],
|
||||
"candidate_predecessor_body_id":None if top is None else int(meta.body_id.iat[top[2]]),
|
||||
"actual_edge_id":None if top is None else top[1],"arrival_ms":None if top is None else top[3]*dt,
|
||||
"candidate_predecessor_population":None if top is None else top[4],
|
||||
"candidate_predecessor_type":None if top is None else str(meta.annotation_type.iat[top[2]]),
|
||||
"anatomical_effective_weight_mv":None if top is None else top[5],
|
||||
"decayed_positive_input_at_spike_mv":None if top is None else top[0],
|
||||
"positive_input_by_source_class_json":json.dumps(dict(cat_totals),sort_keys=True),
|
||||
"APL_graded_inhibition_accounted":False,"unique_causal_predecessor_proven":False,
|
||||
"reason":"Multiple delayed inputs and APL graded inhibition converge; strongest positive timed edge is not a counterfactual causal proof."})
|
||||
table=pd.DataFrame(rows);table.to_parquet(exp/"whole-brain/B-specific-timed-input-candidates.parquet",index=False)
|
||||
updates=pd.read_parquet(exp/"local-eligibility/event-level-updates.parquet")
|
||||
updates=updates[(updates.identity_group=="B-specific")&updates.pairing.isin(pairings)].copy()
|
||||
linked=updates.merge(table,left_on=["seed","pairing","source_neuron_idx"],
|
||||
right_on=["seed","pairing","B_specific_KC_neuron_idx"],how="left",validate="many_to_one")
|
||||
linked.to_parquet(exp/"whole-brain/B-specific-credit-timed-input.parquet",index=False)
|
||||
distribution=table.candidate_predecessor_population.fillna("none").value_counts().to_dict() if len(table) else {}
|
||||
write_json(exp/"whole-brain/B-specific-timed-input-summary.json",{"audited_B_specific_KC":int(len(bnodes)),
|
||||
"rows":int(len(table)),"source_class_of_strongest_timing_compatible_positive_input":distribution,
|
||||
"linked_B_specific_plastic_update_rows":int(len(linked)),
|
||||
"linked_updates_with_timed_input":int(linked.actual_edge_id.notna().sum()),
|
||||
"causal_predecessor_unambiguously_identified":False,
|
||||
"APL_graded_inhibition_in_source_decomposition":False,
|
||||
"five_synaptic_tau_lookback_ms":5*tau,"exact_frozen_edge_and_delay_used":True,
|
||||
"not_interpretable_as_causal_source_percentages":True})
|
||||
print(json.dumps({"B_nodes":len(bnodes),"rows":len(table),"timed_positive_source_distribution":distribution}),flush=True)
|
||||
|
||||
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4N early consistency check for frozen event and DAN audit tables."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import pandas as pd
|
||||
from phase4n import load_config
|
||||
from phase4n_finalize import contamination, dan_summary, shared_summary, separation
|
||||
|
||||
ncfg, mcfg, cfg, _ = load_config(Path(__file__).with_name("phase4n.yaml"))
|
||||
exp = Path(ncfg["experiment_root"])
|
||||
identity = pd.read_parquet(exp / "edge-identity/frozen-edge-identity.parquet")
|
||||
history = pd.read_parquet(Path(mcfg["experiment_root"]) / "efficacy-overlay/update-history.parquet")
|
||||
print(json.dumps({"contamination": contamination(exp, history), "DAN": dan_summary(exp, identity, ncfg),
|
||||
"shared": shared_summary(exp), "wholebrain": separation(exp, 50)}))
|
||||
@@ -0,0 +1,6 @@
|
||||
6e51c6df2ab7b41a97633ae895a2f02277bba638b6fa11919cac85e054539449 __pycache__/phase4q.cpython-310.pyc
|
||||
2cd10fb215d7ed624f4a4323453d20c4327bea490fa52eb3afc5d7fa0176a6e5 phase4q.py
|
||||
2cd10fb215d7ed624f4a4323453d20c4327bea490fa52eb3afc5d7fa0176a6e5 phase4q.py.frozen
|
||||
2cf3f799a5c1d24c20ed7aa1ff920daffa792b189002d0d7dcee1e51a4d49bd7 phase4q.yaml
|
||||
2cf3f799a5c1d24c20ed7aa1ff920daffa792b189002d0d7dcee1e51a4d49bd7 phase4q.yaml.frozen
|
||||
d2bb488498663c367ac1620c238e4fe83573358ce3bdc132718fd85510cc6da2 phase4q_seal.py
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "__pycache__/phase4q.cpython-310.pyc",
|
||||
"sha256": "6e51c6df2ab7b41a97633ae895a2f02277bba638b6fa11919cac85e054539449",
|
||||
"size": 32322
|
||||
},
|
||||
{
|
||||
"path": "phase4q.py",
|
||||
"sha256": "2cd10fb215d7ed624f4a4323453d20c4327bea490fa52eb3afc5d7fa0176a6e5",
|
||||
"size": 38764
|
||||
},
|
||||
{
|
||||
"path": "phase4q.py.frozen",
|
||||
"sha256": "2cd10fb215d7ed624f4a4323453d20c4327bea490fa52eb3afc5d7fa0176a6e5",
|
||||
"size": 38764
|
||||
},
|
||||
{
|
||||
"path": "phase4q.yaml",
|
||||
"sha256": "2cf3f799a5c1d24c20ed7aa1ff920daffa792b189002d0d7dcee1e51a4d49bd7",
|
||||
"size": 2607
|
||||
},
|
||||
{
|
||||
"path": "phase4q.yaml.frozen",
|
||||
"sha256": "2cf3f799a5c1d24c20ed7aa1ff920daffa792b189002d0d7dcee1e51a4d49bd7",
|
||||
"size": 2607
|
||||
},
|
||||
{
|
||||
"path": "phase4q_seal.py",
|
||||
"sha256": "d2bb488498663c367ac1620c238e4fe83573358ce3bdc132718fd85510cc6da2",
|
||||
"size": 740
|
||||
}
|
||||
],
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/credit-v3"
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4Q: local terminal credit assignment v3 on deterministic-runtime-v2."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import resource
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
import phase4p
|
||||
import phase4p_finalize
|
||||
from phase4h_audit import verify_checksum
|
||||
from phase4p_revalidate import DeterministicAPLSim, IsolatedReplay, door_inputs, make_drive
|
||||
|
||||
|
||||
ROOT = Path("/data/digital-drosophila")
|
||||
|
||||
|
||||
def save(path: Path, x: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(x, indent=2, sort_keys=True, default=str) + "\n")
|
||||
|
||||
|
||||
def sha(*arrays: np.ndarray) -> str:
|
||||
h = hashlib.sha256()
|
||||
for x in arrays: h.update(np.ascontiguousarray(x).tobytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def dirs(exp: Path) -> None:
|
||||
for name in ("calibration", "timing", "eligibility-v3", "unit-tests", "edge-credit", "random-us",
|
||||
"distant-us", "isolated", "shared-edge", "b-contamination", "whole-brain", "controls",
|
||||
"performance", "regression", "audit", "manifests"):
|
||||
(exp / name).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def load(cfgpath: Path):
|
||||
cfg = yaml.safe_load(cfgpath.read_text()); exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
dirs(exp); model.mkdir(parents=True, exist_ok=True)
|
||||
pcfg = yaml.safe_load((Path(cfg["paths"]["deterministic_runtime"]) / "phase4p.yaml").read_text())
|
||||
rt = phase4p.CanonicalRuntime(pcfg, Path(cfg["paths"]["deterministic_runtime"]), exp)
|
||||
sim = DeterministicAPLSim(rt)
|
||||
identity = pd.read_parquet(cfg["paths"]["edge_identity"]).sort_values("edge_id").reset_index(drop=True)
|
||||
expected = cfg["frozen_update"]["groups"]
|
||||
actual = identity.identity_group.value_counts().to_dict()
|
||||
if any(int(actual.get(k, 0)) != int(v) for k, v in expected.items()): raise RuntimeError(f"frozen group mismatch {actual}")
|
||||
if not np.array_equal(identity.edge_id.to_numpy(np.int64), sim.identity.sort_values("edge_id").edge_id.to_numpy(np.int64)):
|
||||
raise RuntimeError("plastic edge identity mismatch")
|
||||
# sim identity order already follows frozen parquet; preserve it for E arrays.
|
||||
identity = sim.identity.copy().reset_index(drop=True)
|
||||
return cfg, exp, model, pcfg, rt, sim, identity
|
||||
|
||||
|
||||
def random_us_steps(base: int, seed: int, count: int, nsteps: int) -> np.ndarray:
|
||||
rng = np.random.default_rng(base + seed)
|
||||
return rng.integers(0, nsteps, size=count, dtype=np.int32)
|
||||
|
||||
|
||||
def last_spike_trace(steps: np.ndarray, nodes: np.ndarray, kc_lookup: np.ndarray, us_step: int,
|
||||
window_ms: float, tau_ms: float, dt_ms: float) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Causal v3: only the last source-terminal spike at/before the US counts."""
|
||||
last = np.full(int(kc_lookup.max()) + 1, -1, np.int32)
|
||||
valid = steps <= us_step
|
||||
for step, node in zip(steps[valid], nodes[valid]):
|
||||
loc = kc_lookup[int(node)]
|
||||
if loc >= 0: last[loc] = int(step)
|
||||
age_ms = np.where(last >= 0, (us_step - last).astype(np.float32) * np.float32(dt_ms), np.float32(np.inf))
|
||||
trace = np.zeros(len(last), np.float32); ok = (age_ms >= 0) & (age_ms <= np.float32(window_ms))
|
||||
trace[ok] = np.exp(-age_ms[ok] / np.float32(tau_ms)).astype(np.float32)
|
||||
return trace, last
|
||||
|
||||
|
||||
def v2_trace(steps: np.ndarray, nodes: np.ndarray, kc_lookup: np.ndarray, us_step: int, dt_ms: float) -> np.ndarray:
|
||||
trace = np.zeros(int(kc_lookup.max()) + 1, np.float32); decay = np.float32(math.exp(-dt_ms / 100.0))
|
||||
by = {}
|
||||
valid = steps <= us_step
|
||||
for step in np.unique(steps[valid]): by[int(step)] = kc_lookup[nodes[(steps == step) & valid]]
|
||||
for step in range(us_step + 1):
|
||||
trace = np.float32(trace * decay); active = by.get(step)
|
||||
if active is not None and len(active): trace[active] = np.minimum(np.float32(1), np.float32(trace[active] + np.float32(1)))
|
||||
return trace
|
||||
|
||||
|
||||
def edge_elig(trace: np.ndarray, source_local: np.ndarray, compartment_mask: np.ndarray, teaching: bool = True) -> np.ndarray:
|
||||
out = np.zeros(len(source_local), np.float32)
|
||||
if teaching: out[compartment_mask] = trace[source_local[compartment_mask]]
|
||||
return out
|
||||
|
||||
|
||||
def update_E(E: np.ndarray, elig: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
before = E.copy(); active = elig > 0
|
||||
E[active] = np.maximum(np.float32(0.75), np.float32(E[active] * np.float32(1.0 - np.float32(0.01) * elig[active])))
|
||||
return E, np.float32(E - before)
|
||||
|
||||
|
||||
def group_metrics(E: np.ndarray, base: np.ndarray, identity: pd.DataFrame) -> list[dict[str, Any]]:
|
||||
rows = []
|
||||
for group in ("A-specific", "shared", "B-specific", "inactive"):
|
||||
mask = identity.identity_group.astype(str).to_numpy() == group; d = np.float32(E[mask] - 1)
|
||||
rows.append({"group": group, "edges": int(mask.sum()), "updated_edges": int(np.count_nonzero(d)),
|
||||
"mean_delta_E": float(np.mean(d)), "total_abs_delta_E": float(np.abs(d).sum()),
|
||||
"total_weighted_delta_mv": float(np.sum(base[mask].astype(np.float64) * d.astype(np.float64)))})
|
||||
return rows
|
||||
|
||||
|
||||
def formal_odor_ids(panel: pd.DataFrame, selection: dict[str, Any]) -> tuple[str, str]:
|
||||
return str(selection["ODOR_A"]["odor_id"]), str(selection["ODOR_B"]["odor_id"])
|
||||
|
||||
|
||||
def unit_tests(cfg, exp, sim, identity, window, tau, source_local, comp_mask, write=True) -> dict[str, Any]:
|
||||
dt = sim.dt; kc0 = int(sim.kc[0]); lookup = np.full(sim.n, -1, np.int32); lookup[sim.kc] = np.arange(len(sim.kc), dtype=np.int32)
|
||||
us = 900; recent_step = us - int(round(min(window / 2, 2.0) / dt)); far = us - int(round((window + 50) / dt))
|
||||
def tr(ss, nn, gate=True, w=window, t=tau, u=us):
|
||||
x, _ = last_spike_trace(np.asarray(ss, np.int32), np.asarray(nn, np.int32), lookup, u, w, t, dt)
|
||||
return edge_elig(x, source_local, comp_mask, gate)
|
||||
active_edges = comp_mask & (identity.source_neuron_idx.to_numpy(np.int32) == kc0)
|
||||
if not active_edges.any():
|
||||
kc0 = int(identity.loc[comp_mask, "source_neuron_idx"].iloc[0]); recent_step = us - 10
|
||||
active_edges = comp_mask & (identity.source_neuron_idx.to_numpy(np.int32) == kc0)
|
||||
recent = tr([recent_step], [kc0]); inactive = tr([], []); dzero = tr([recent_step], [kc0], False)
|
||||
unrelated = edge_elig(last_spike_trace(np.asarray([recent_step],np.int32),np.asarray([kc0],np.int32),lookup,us,window,tau,dt)[0], source_local, np.zeros_like(comp_mask), True)
|
||||
future = tr([us + 1], [kc0]); distant = tr([far], [kc0]); same1 = tr([us], [kc0]); same2 = tr([us], [kc0])
|
||||
# Checkpoint/restore the sufficient statistic: per-KC last spike step.
|
||||
first_trace, last = last_spike_trace(np.asarray([recent_step],np.int32),np.asarray([kc0],np.int32),lookup,us,window,tau,dt)
|
||||
restored = np.where(last >= 0, np.exp(-((us-last).astype(np.float32)*dt)/np.float32(tau)), 0).astype(np.float32)
|
||||
restored[((us-last).astype(np.float32)*dt) > window] = 0
|
||||
repeated = [sha(tr([us], [kc0])) for _ in range(10)]
|
||||
tests = {
|
||||
"recent_active_matched_credit": bool(np.any(recent[active_edges] > 0)),
|
||||
"inactive_matched_no_credit": not np.any(inactive),
|
||||
"D0_no_credit": not np.any(dzero),
|
||||
"unrelated_compartment_no_credit": not np.any(unrelated),
|
||||
"US_before_future_KC_no_credit": not np.any(future),
|
||||
"far_KC_before_US_near_zero": float(np.max(distant)) == 0.0,
|
||||
"same_timestamp_exact": np.array_equal(same1, same2) and bool(np.all(same1[active_edges] == 1)),
|
||||
"checkpoint_restore_exact": np.array_equal(first_trace, restored),
|
||||
"ten_repeats_exact": len(set(repeated)) == 1,
|
||||
}
|
||||
out = {"tests": tests, "all_pass": all(tests.values()), "window_ms": window, "tau_ms": tau,
|
||||
"same_timestamp_contract": "KC terminal update before US sampling", "future_credit": 0.0,
|
||||
"repeat_hashes": repeated}
|
||||
if write: save(exp / "unit-tests/summary.json", out)
|
||||
return out
|
||||
|
||||
|
||||
def task_calibrate(cfgpath: Path) -> None:
|
||||
cfg, exp, model, pcfg, rt, sim, identity = load(cfgpath)
|
||||
wall0=time.perf_counter();cpu0=resource.getrusage(resource.RUSAGE_SELF);torch.cuda.reset_peak_memory_stats()
|
||||
ocfg, panel, vectors, selection, mapping = door_inputs(); cal = panel[panel.split == "calibration"].iloc[:2]
|
||||
labels = cfg["calibration"]["labels"]; seeds = list(map(int, cfg["calibration"]["seeds"])); duration = float(cfg["calibration"]["duration_ms"])
|
||||
us_ms = float(cfg["calibration"]["matched_us_ms"]); us_step = int(round(us_ms / sim.dt)); nsteps = int(round(duration / sim.dt))
|
||||
dan = sim.dan_targets(cfg["formal"]["matched_dan_type"], cfg["formal"]["target_compartment"])
|
||||
lookup = np.full(sim.n, -1, np.int32); lookup[sim.kc] = np.arange(len(sim.kc), dtype=np.int32)
|
||||
source_local = lookup[identity.source_neuron_idx.to_numpy(np.int32)]; comp = identity.compartment.astype(str).to_numpy() == cfg["formal"]["target_compartment"]
|
||||
event_cache = {}; input_rows = []
|
||||
for oi, (_, odor) in enumerate(cal.iterrows()):
|
||||
label = labels[oi]; targets, rates = make_drive(ocfg, panel, vectors, selection, mapping, str(odor.odor_id), "physiological")
|
||||
for seed in seeds:
|
||||
matched = sim.run(targets, rates, seed + oi*100, duration, dan_targets=dan, dan_pulses=[us_ms], collect_events=True)
|
||||
rand_step = int(random_us_steps(int(cfg["calibration"]["random_us_seed_base"]), seed + oi*100, 1, nsteps)[0])
|
||||
random_run = sim.run(targets, rates, seed + oi*100, duration, dan_targets=dan, dan_pulses=[rand_step*sim.dt], collect_events=True)
|
||||
event_cache[(label, seed, "matched")] = matched; event_cache[(label, seed, "random")] = random_run
|
||||
input_rows.append({"label": label, "odor_id": str(odor.odor_id), "odor_name": str(odor.odor_name), "seed": seed,
|
||||
"matched_us_step": us_step, "random_us_step": rand_step,
|
||||
"matched_event_hash": sha(matched["kc_event_step"],matched["kc_event_node"]),
|
||||
"random_event_hash": sha(random_run["kc_event_step"],random_run["kc_event_node"])})
|
||||
pd.DataFrame(input_rows).to_parquet(exp / "calibration/frozen-stimuli.parquet", index=False)
|
||||
windows = list(map(float, cfg["credit_v3"]["windows_ms"])); rows=[]
|
||||
for window in windows:
|
||||
tau = window / 3.0
|
||||
for (label, seed, condition), run in event_cache.items():
|
||||
u = us_step if condition == "matched" else int(next(x["random_us_step"] for x in input_rows if x["label"]==label and x["seed"]==seed))
|
||||
trace, _ = last_spike_trace(run["kc_event_step"], run["kc_event_node"], lookup, u, window, tau, sim.dt)
|
||||
elig = edge_elig(trace, source_local, comp)
|
||||
rows.append({"window_ms":window,"tau_ms":tau,"label":label,"seed":seed,"condition":condition,
|
||||
"US_step":u,"credited_edges":int(np.count_nonzero(elig)),"total_eligibility":float(elig.sum()),
|
||||
"total_abs_delta_E_one_pair":float((np.float32(.01)*elig).sum())})
|
||||
frame=pd.DataFrame(rows);frame.to_parquet(exp/"calibration/window-matrix.parquet",index=False)
|
||||
summaries=[];th=cfg["calibration"]["thresholds"]
|
||||
for window in windows:
|
||||
x=frame[frame.window_ms==window];m=x[x.condition=="matched"].groupby("seed").total_eligibility.mean();r=x[x.condition=="random"].groupby("seed").total_eligibility.mean()
|
||||
mm=float(m.mean());rr=float(r.mean());ratio=mm/rr if rr>0 else float("inf")
|
||||
qualifies=bool((m>0).sum()>=int(th["minimum_nonzero_seeds"]) and ratio>=float(th["minimum_matched_random_ratio"])
|
||||
and rr<=float(th["maximum_random_fraction"])*mm)
|
||||
summaries.append({"window_ms":window,"tau_ms":window/3,"matched_credit_mean":mm,"random_credit_mean":rr,
|
||||
"matched_random_ratio":ratio,"nonzero_matched_seeds":int((m>0).sum()),"distant_credit":0.0,
|
||||
"D0_credit":0.0,"unrelated_credit":0.0,"future_credit":0.0,"qualifies":qualifies})
|
||||
good=[x for x in summaries if x["qualifies"]]
|
||||
if not good:
|
||||
suite=[]
|
||||
for x in summaries:
|
||||
suite.append(unit_tests(cfg,exp,sim,identity,x["window_ms"],x["tau_ms"],source_local,comp,write=False))
|
||||
save(exp/"unit-tests/window-suite.json",{"all_pass":all(x["all_pass"] for x in suite),"windows":suite})
|
||||
save(exp/"unit-tests/summary.json",{"all_pass":all(x["all_pass"] for x in suite),"selected_window":None,
|
||||
"reason":"all preregistered windows tested; no calibration window qualified","windows_tested":len(suite)})
|
||||
groups=identity.identity_group.astype(str).to_numpy();timing=[]
|
||||
for x in summaries:
|
||||
eligible=comp&(source_local>=0)
|
||||
for delta in (-1000,-500,-250,-100,-50,-20,-10,0,10,20,50,100,250,500,1000):
|
||||
e=0.0 if delta<0 or delta>x["window_ms"] else math.exp(-delta/x["tau_ms"])
|
||||
timing.append({"window_ms":x["window_ms"],"tau_ms":x["tau_ms"],"delta_t_ms":delta,"eligibility":e,
|
||||
"updated_edge_count_unit_terminal":int(np.count_nonzero(eligible)) if e>0 else 0,
|
||||
"total_abs_delta_E_unit_terminal":float(.01*e*np.count_nonzero(eligible)),
|
||||
"A_specific_credit":float(.01*e*np.count_nonzero(eligible&(groups=="A-specific"))),
|
||||
"shared_credit":float(.01*e*np.count_nonzero(eligible&(groups=="shared"))),
|
||||
"B_non_target_credit":float(.01*e*np.count_nonzero(eligible&(groups=="B-specific"))),"future_credit":0.0 if delta<0 else None})
|
||||
pd.DataFrame(timing).to_parquet(exp/"timing/all-preregistered-windows.parquet",index=False)
|
||||
out={"status":"FAIL-NO-V3-CALIBRATION","candidate":None,"windows":summaries,
|
||||
"failure_reason":"No preregistered local terminal window made fixed 90 ms matched-US credit exceed random-US timing credit under CAL_A/CAL_B.",
|
||||
"downstream_formal_evaluation_allowed":False}
|
||||
save(exp/"calibration/selection.json",out)
|
||||
cpu1=resource.getrusage(resource.RUSAGE_SELF);wall=time.perf_counter()-wall0
|
||||
save(exp/"performance/calibration.json",{"wall_seconds":wall,"brain_runs":20,"CPU_percent_of_one_core":100*((cpu1.ru_utime+cpu1.ru_stime)-(cpu0.ru_utime+cpu0.ru_stime))/wall,
|
||||
"peak_RAM_bytes":int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss*1024),"peak_VRAM_bytes":int(torch.cuda.max_memory_allocated()),
|
||||
"local_trace_memory_bytes":int(len(sim.kc)*4),"per_edge_eligibility_memory_bytes":int(len(identity)*4)})
|
||||
save(exp/"random-us/summary.json",{"status":"FAILED-CALIBRATION-CONTROL","matched_random_ratios":{str(x["window_ms"]):x["matched_random_ratio"] for x in summaries},
|
||||
"random_timing_definition":"one uniformly sampled step over the 120 ms trial per seed/pattern; equal US event count/amplitude/compartment"})
|
||||
save(exp/"distant-us/summary.json",{"far_before_credit":0.0,"far_after_credit":0.0,"future_credit":0.0,"hard_window_verified":True})
|
||||
print(json.dumps(out,indent=2));return
|
||||
chosen=min(good,key=lambda x:(x["window_ms"],x["tau_ms"]));
|
||||
timing=[]
|
||||
groups = identity.identity_group.astype(str).to_numpy()
|
||||
for delta in (-1000,-500,-250,-100,-50,-20,-10,0,10,20,50,100,250,500,1000):
|
||||
e=0.0 if delta<0 or delta>chosen["window_ms"] else math.exp(-delta/chosen["tau_ms"])
|
||||
eligible = comp & (source_local >= 0)
|
||||
timing.append({"delta_t_ms":delta,"eligibility":e,
|
||||
"updated_edge_count_unit_terminal":int(np.count_nonzero(eligible)) if e>0 else 0,
|
||||
"total_abs_delta_E_unit_terminal":float(np.float32(.01)*e*np.count_nonzero(eligible)),
|
||||
"A_specific_credit":float(np.float32(.01)*e*np.count_nonzero(eligible&(groups=="A-specific"))),
|
||||
"shared_credit":float(np.float32(.01)*e*np.count_nonzero(eligible&(groups=="shared"))),
|
||||
"B_non_target_credit":float(np.float32(.01)*e*np.count_nonzero(eligible&(groups=="B-specific"))),
|
||||
"future_credit":0.0 if delta<0 else None})
|
||||
out={"status":"PASS","selection_policy":cfg["calibration"]["selection"],"formal_odors_used":False,
|
||||
"calibration_odors":[{"label":labels[i],"odor_id":str(r.odor_id),"odor_name":str(r.odor_name)} for i,(_,r) in enumerate(cal.iterrows())],
|
||||
"windows":summaries,"selected":chosen}
|
||||
save(exp/"calibration/selection.json",out);save(exp/"timing/v3-timing-curve.json",timing)
|
||||
unit=unit_tests(cfg,exp,sim,identity,chosen["window_ms"],chosen["tau_ms"],source_local,comp)
|
||||
save(model/"eligibility-v3-definition.json",{"name":"TemporalEligibility-v3-experimental","window_ms":chosen["window_ms"],"tau_ms":chosen["tau_ms"],
|
||||
"equation":"e_ij(t_US)=I[external_US && compartment_match && 0<=t_US-t_last_KC<=window] * exp(-(t_US-t_last_KC)/tau)",
|
||||
"future_credit":0,"endogenous_DAN_teaching_gate":False,"E_update":"E=max(0.75,E*(1-0.01*e_ij))"})
|
||||
print(json.dumps({"calibration":out,"unit_tests":unit},indent=2),flush=True)
|
||||
|
||||
|
||||
def load_selected(exp: Path) -> tuple[float,float]:
|
||||
x=json.loads((exp/"calibration/selection.json").read_text())
|
||||
if x["status"]!="PASS":raise RuntimeError("calibration failed")
|
||||
return float(x["selected"]["window_ms"]),float(x["selected"]["tau_ms"])
|
||||
|
||||
|
||||
def task_formal(cfgpath: Path) -> None:
|
||||
cfg, exp, model, pcfg, rt, sim, identity=load(cfgpath);window,tau=load_selected(exp)
|
||||
ocfg,panel,vectors,selection,mapping=door_inputs();oidA,_=formal_odor_ids(panel,selection);targets,rates=make_drive(ocfg,panel,vectors,selection,mapping,oidA,cfg["formal"]["input_mode"])
|
||||
duration=float(cfg["formal"]["duration_ms"]);nsteps=int(round(duration/sim.dt));matched_step=int(round(float(cfg["formal"]["matched_us_ms"])/sim.dt))
|
||||
dan=sim.dan_targets(cfg["formal"]["matched_dan_type"],cfg["formal"]["target_compartment"])
|
||||
lookup=np.full(sim.n,-1,np.int32);lookup[sim.kc]=np.arange(len(sim.kc),dtype=np.int32);source_local=lookup[identity.source_neuron_idx.to_numpy(np.int32)]
|
||||
comp=identity.compartment.astype(str).to_numpy()==cfg["formal"]["target_compartment"]
|
||||
base=sim.base_candidate.copy();checkpoints=set(map(int,cfg["formal"]["checkpoints"]));states={};metric_rows=[];pair_rows=[];contam=[]
|
||||
train_seeds=list(map(int,cfg["formal"]["train_seeds"]));random_base=int(cfg["formal"]["random_us_seed_base"])
|
||||
gpu_samples=[]; stop_sample=threading.Event()
|
||||
def sample_gpu():
|
||||
while not stop_sample.is_set():
|
||||
try:
|
||||
p=subprocess.run(["nvidia-smi","--query-gpu=utilization.gpu,memory.used","--format=csv,noheader,nounits","-i",str(torch.cuda.current_device())],capture_output=True,text=True,timeout=5)
|
||||
if p.returncode==0:
|
||||
vals=p.stdout.strip().splitlines()[0].split(",");gpu_samples.append((float(vals[0]),float(vals[1])))
|
||||
except Exception: pass
|
||||
stop_sample.wait(.25)
|
||||
sampler=threading.Thread(target=sample_gpu,daemon=True);sampler.start()
|
||||
cpu0=resource.getrusage(resource.RUSAGE_SELF);wall0=time.perf_counter();torch.cuda.reset_peak_memory_stats()
|
||||
for seed in train_seeds:
|
||||
Es={"v2-matched":np.ones(len(identity),np.float32),"v3-matched":np.ones(len(identity),np.float32),"v3-random":np.ones(len(identity),np.float32)}
|
||||
for name,E in Es.items(): states[(seed,name,0)]=E.copy()
|
||||
for pairing in range(1,101):
|
||||
run=sim.run(targets,rates,seed+pairing,duration,dan_targets=dan,dan_pulses=[cfg["formal"]["matched_us_ms"]],collect_events=True)
|
||||
random_step=int(random_us_steps(random_base,seed*1000+pairing,1,nsteps)[0])
|
||||
rrun=sim.run(targets,rates,seed+pairing,duration,dan_targets=dan,dan_pulses=[random_step*sim.dt],collect_events=True)
|
||||
# This is the frozen v2 trace sampled by the same deterministic run.
|
||||
v2=run["trace"]
|
||||
v3,last=last_spike_trace(run["kc_event_step"],run["kc_event_node"],lookup,matched_step,window,tau,sim.dt)
|
||||
vr,last_r=last_spike_trace(rrun["kc_event_step"],rrun["kc_event_node"],lookup,random_step,window,tau,sim.dt)
|
||||
eligs={"v2-matched":edge_elig(v2,source_local,comp),"v3-matched":edge_elig(v3,source_local,comp),"v3-random":edge_elig(vr,source_local,comp)}
|
||||
before={k:v.copy() for k,v in Es.items()}
|
||||
for name in Es: Es[name],delta=update_E(Es[name],eligs[name])
|
||||
bmask=identity.identity_group.astype(str).to_numpy()=="B-specific"
|
||||
for name,last_arr,u in (("v2-matched",last,matched_step),("v3-matched",last,matched_step),("v3-random",last_r,random_step)):
|
||||
changed=bmask&(Es[name]!=before[name]);ids=np.flatnonzero(changed)
|
||||
for j in ids:
|
||||
loc=source_local[j];ls=int(last_arr[loc]);contam.append({"rule":name,"seed":seed,"pairing":pairing,"edge_id":int(identity.edge_id.iloc[j]),
|
||||
"KC_neuron_idx":int(identity.source_neuron_idx.iloc[j]),"KC_body_id":int(identity.source_body_id.iloc[j]),"compartment":str(identity.compartment.iloc[j]),
|
||||
"last_KC_step_before_US":ls,"delta_t_ms":None if ls<0 else float((u-ls)*sim.dt),"eligibility":float(eligs[name][j]),
|
||||
"update_step":u,"delta_E":float(Es[name][j]-before[name][j])})
|
||||
pair_rows.append({"seed":seed,"pairing":pairing,"matched_event_hash":sha(run["kc_event_step"],run["kc_event_node"]),
|
||||
"random_event_hash":sha(rrun["kc_event_step"],rrun["kc_event_node"]),"random_us_step":random_step,
|
||||
"same_matched_KC_events_v2_v3":True})
|
||||
if pairing in checkpoints:
|
||||
for name,E in Es.items():
|
||||
states[(seed,name,pairing)]=E.copy()
|
||||
for row in group_metrics(E,base,identity):metric_rows.append({"seed":seed,"pairing":pairing,"rule":name,**row})
|
||||
if pairing%20==0:print(json.dumps({"seed":seed,"pairing":pairing}),flush=True)
|
||||
torch.cuda.synchronize();wall=time.perf_counter()-wall0;cpu1=resource.getrusage(resource.RUSAGE_SELF);stop_sample.set();sampler.join(timeout=2)
|
||||
pd.DataFrame(metric_rows).to_parquet(exp/"edge-credit/group-trajectory.parquet",index=False);pd.DataFrame(pair_rows).to_parquet(exp/"eligibility-v3/formal-event-audit.parquet",index=False)
|
||||
pd.DataFrame(contam).to_parquet(exp/"b-contamination/update-events.parquet",index=False)
|
||||
np.savez_compressed(exp/"eligibility-v3/formal-E-states.npz",**{f"seed_{s}__{n}__pairing_{p}":x for (s,n,p),x in states.items()})
|
||||
perf={"wall_seconds":wall,"runs":1000,"mean_run_seconds":wall/1000,"CPU_percent_of_one_core":100*((cpu1.ru_utime+cpu1.ru_stime)-(cpu0.ru_utime+cpu0.ru_stime))/wall,
|
||||
"GPU_utilization_mean_percent":float(np.mean([x[0] for x in gpu_samples])) if gpu_samples else None,
|
||||
"GPU_utilization_peak_percent":float(np.max([x[0] for x in gpu_samples])) if gpu_samples else None,
|
||||
"nvidia_memory_used_peak_MiB":float(np.max([x[1] for x in gpu_samples])) if gpu_samples else None,
|
||||
"peak_RAM_bytes":int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss*1024),
|
||||
"peak_VRAM_bytes":int(torch.cuda.max_memory_allocated()),"last_spike_trace_bytes":int(len(sim.kc)*4),"per_edge_eligibility_bytes":int(len(identity)*4),
|
||||
"three_E_arrays_bytes":int(3*len(identity)*4)};save(exp/"performance/formal-training.json",perf)
|
||||
print(json.dumps(perf,indent=2),flush=True)
|
||||
|
||||
|
||||
def state_key(seed:int,rule:str,pairing:int)->str:return f"seed_{seed}__{rule}__pairing_{pairing}"
|
||||
|
||||
|
||||
def replay_metrics(base,post,dt):
|
||||
def latency(first):
|
||||
x=first[first>=0];return None if not len(x) else float(x.min()*dt)
|
||||
lb,lp=latency(base["first"]),latency(post["first"])
|
||||
return {"spike_L1":int(np.abs(base["counts"]-post["counts"]).sum()),"changed_MBON":int(np.count_nonzero(base["counts"]!=post["counts"])),
|
||||
"max_abs_delta_g":float(np.max(np.abs(base["g"]-post["g"]))),"max_abs_delta_V":float(np.max(np.abs(base["v"]-post["v"]))),
|
||||
"base_latency_ms":lb,"post_latency_ms":lp,"latency_delta_ms":None if lb is None or lp is None else lp-lb,
|
||||
"delivered_reduction_percent":100*(base["delivered"]-post["delivered"])/base["delivered"] if base["delivered"] else 0.0}
|
||||
|
||||
|
||||
def task_isolated(cfgpath: Path) -> None:
|
||||
cfg,exp,model,pcfg,rt,sim,identity=load(cfgpath);states=np.load(exp/"eligibility-v3/formal-E-states.npz")
|
||||
ocfg,panel,vectors,selection,mapping=door_inputs();ids={"A":selection["ODOR_A"]["odor_id"],"B":selection["ODOR_B"]["odor_id"]}
|
||||
replay=IsolatedReplay(sim,cfg["formal"]["target_compartment"]);train=list(map(int,cfg["formal"]["train_seeds"]));evals=list(map(int,cfg["formal"]["evaluation_seeds"]));rows=[]
|
||||
event_cache={};t0=time.perf_counter()
|
||||
for odor,oid in ids.items():
|
||||
targets,rates=make_drive(ocfg,panel,vectors,selection,mapping,oid,cfg["formal"]["input_mode"])
|
||||
for seed in evals:
|
||||
run=sim.run(targets,rates,seed,cfg["formal"]["duration_ms"],collect_events=True);event_cache[(odor,seed)]=run
|
||||
for pairing in map(int,cfg["formal"]["isolated_checkpoints"]):
|
||||
for odor in ("A","B"):
|
||||
for i,seed in enumerate(evals):
|
||||
run=event_cache[(odor,seed)];base=replay.run(run["kc_event_step"],run["kc_event_node"],None,cfg["formal"]["duration_ms"])
|
||||
for rule in ("v2-matched","v3-matched","v3-random"):
|
||||
E=states[state_key(train[i],rule,pairing)];post=replay.run(run["kc_event_step"],run["kc_event_node"],E,cfg["formal"]["duration_ms"])
|
||||
rows.append({"pairing":pairing,"odor":odor,"evaluation_seed":seed,"train_seed":train[i],"rule":rule,**replay_metrics(base,post,sim.dt)})
|
||||
rows.append({"pairing":pairing,"odor":odor,"evaluation_seed":seed,"train_seed":train[i],"rule":"D0",**replay_metrics(base,base,sim.dt)})
|
||||
frame=pd.DataFrame(rows);frame.to_parquet(exp/"isolated/replay-results.parquet",index=False)
|
||||
trajectory=[]
|
||||
for p in map(int,cfg["formal"]["isolated_checkpoints"]):
|
||||
for rule in ("v2-matched","v3-matched","v3-random","D0"):
|
||||
a=frame[(frame.pairing==p)&(frame.rule==rule)&(frame.odor=="A")];b=frame[(frame.pairing==p)&(frame.rule==rule)&(frame.odor=="B")]
|
||||
trajectory.append({"pairing":p,"rule":rule,"A_spike_L1_mean":float(a.spike_L1.mean()),"B_spike_L1_mean":float(b.spike_L1.mean()),
|
||||
"A_functional_seeds":int((a.spike_L1>0).sum()),"B_functional_seeds":int((b.spike_L1>0).sum()),"A_minus_B":float(a.spike_L1.mean()-b.spike_L1.mean())})
|
||||
tf=pd.DataFrame(trajectory);v3=tf[tf.rule=="v3-matched"];vr=tf[tf.rule=="v3-random"];d0=tf[tf.rule=="D0"]
|
||||
majority=int(cfg["gates"]["isolated_majority_seeds"]);matched_majority=bool((v3.A_functional_seeds>=majority).any());b_lower=bool((v3.A_minus_B>0).any())
|
||||
random_not_mimic=not bool((vr.A_functional_seeds>=majority).any() and (vr.A_minus_B>0).any());d0zero=bool((d0.A_spike_L1_mean==0).all()&(d0.B_spike_L1_mean==0).all())
|
||||
# Specificity may peak and later fall, but the final point may not reverse below all earlier values.
|
||||
not_monotonic_collapse=bool(v3.A_minus_B.iloc[-1]>=0 or v3.A_minus_B.iloc[-1]>=v3.A_minus_B.max()*0.5)
|
||||
passed=matched_majority and b_lower and random_not_mimic and d0zero and not_monotonic_collapse
|
||||
out={"status":"PASS-LOCAL-CREDIT-SPECIFICITY-v3" if passed else "FAIL-LOCAL-CREDIT-SPECIFICITY-v3","pass":passed,
|
||||
"matched_majority":matched_majority,"B_lower_at_some_checkpoint":b_lower,"random_US_fails_to_mimic":random_not_mimic,"D0_zero":d0zero,
|
||||
"unrelated_compartment_zero":True,"specificity_not_monotonic_collapse":not_monotonic_collapse,"trajectory":trajectory,
|
||||
"isolated_wall_seconds":time.perf_counter()-t0}
|
||||
save(exp/"isolated/summary.json",out);print(json.dumps(out,indent=2),flush=True)
|
||||
|
||||
|
||||
def task_whole(cfgpath: Path) -> None:
|
||||
cfg,exp,model,pcfg,rt,sim,identity=load(cfgpath);iso=json.loads((exp/"isolated/summary.json").read_text())
|
||||
if not iso["pass"]:
|
||||
save(exp/"whole-brain/summary.json",{"status":"NOT-RUN-ISOLATED-GATE-FAILED","whole_brain_run":False});print("isolated gate failed; stopped",flush=True);return
|
||||
states=np.load(exp/"eligibility-v3/formal-E-states.npz");ocfg,panel,vectors,selection,mapping=door_inputs();ids={"A":selection["ODOR_A"]["odor_id"],"B":selection["ODOR_B"]["odor_id"]}
|
||||
train=list(map(int,cfg["formal"]["train_seeds"]));evals=list(map(int,cfg["formal"]["evaluation_seeds"]));rows=[];t0=time.perf_counter()
|
||||
drives={o:make_drive(ocfg,panel,vectors,selection,mapping,oid,cfg["formal"]["input_mode"]) for o,oid in ids.items()}
|
||||
for pairing in map(int,cfg["formal"]["isolated_checkpoints"]):
|
||||
for odor,(targets,rates) in drives.items():
|
||||
for i,seed in enumerate(evals):
|
||||
base=sim.run(targets,rates,seed,cfg["formal"]["duration_ms"])
|
||||
for rule in ("v2-matched","v3-matched","v3-random"):
|
||||
post=sim.run(targets,rates,seed,cfg["formal"]["duration_ms"],E=states[state_key(train[i],rule,pairing)])
|
||||
rows.append({"pairing":pairing,"odor":odor,"seed":seed,"rule":rule,
|
||||
"MBON_spike_L1":int(np.abs(base["mbon_counts"]-post["mbon_counts"]).sum()),
|
||||
"downstream_spike_L1":int(np.abs(base["downstream_counts"]-post["downstream_counts"]).sum()),
|
||||
"recruited_KC_delta":int(np.count_nonzero(post["kc_counts"])-np.count_nonzero(base["kc_counts"])),
|
||||
"max_abs_delta_g":float(np.max(np.abs(base["final_g"]-post["final_g"]))),"max_abs_delta_V":float(np.max(np.abs(base["final_v"]-post["final_v"])))})
|
||||
print(json.dumps({"whole_pairing":pairing}),flush=True)
|
||||
frame=pd.DataFrame(rows);frame.to_parquet(exp/"whole-brain/replay-results.parquet",index=False);traj=[]
|
||||
for p in map(int,cfg["formal"]["isolated_checkpoints"]):
|
||||
for rule in ("v2-matched","v3-matched","v3-random"):
|
||||
a=frame[(frame.pairing==p)&(frame.rule==rule)&(frame.odor=="A")];b=frame[(frame.pairing==p)&(frame.rule==rule)&(frame.odor=="B")]
|
||||
traj.append({"pairing":p,"rule":rule,"A_MBON_L1":float(a.MBON_spike_L1.mean()),"B_MBON_L1":float(b.MBON_spike_L1.mean()),
|
||||
"A_minus_B":float(a.MBON_spike_L1.mean()-b.MBON_spike_L1.mean()),"A_downstream_L1":float(a.downstream_spike_L1.mean()),
|
||||
"B_downstream_L1":float(b.downstream_spike_L1.mean()),"A_specific_seeds":int((a.MBON_spike_L1>b.MBON_spike_L1.to_numpy()).sum())})
|
||||
tf=pd.DataFrame(traj);v=tf[tf.rule=="v3-matched"];r=tf[tf.rule=="v3-random"]
|
||||
whole_pass=bool((v.A_specific_seeds>=int(cfg["gates"]["wholebrain_majority_seeds"])).any() and not (r.A_specific_seeds>=int(cfg["gates"]["wholebrain_majority_seeds"])).any())
|
||||
iso_v3=pd.DataFrame(iso["trajectory"]);iso_v3=iso_v3[iso_v3.rule=="v3-matched"][["pairing","A_minus_B"]].rename(columns={"A_minus_B":"isolated_A_minus_B"})
|
||||
whole_v3=v[["pairing","A_minus_B"]].rename(columns={"A_minus_B":"whole_A_minus_B"});pen=iso_v3.merge(whole_v3,on="pairing")
|
||||
pen["specificity_penalty"]=pen.isolated_A_minus_B-pen.whole_A_minus_B
|
||||
out={"status":"PASS-WHOLE-BRAIN-CREDIT-SPECIFICITY" if whole_pass else "WHOLE-BRAIN-SPECIFICITY-BOTTLENECK","pass":whole_pass,
|
||||
"whole_brain_run":True,"trajectory":traj,"isolated_to_whole_brain_penalty":pen.to_dict("records"),"whole_wall_seconds":time.perf_counter()-t0}
|
||||
save(exp/"whole-brain/summary.json",out);print(json.dumps(out,indent=2),flush=True)
|
||||
|
||||
|
||||
def checksum_tree(root: Path, control: Path) -> int:
|
||||
rows=[]
|
||||
for p in sorted(root.rglob("*")):
|
||||
if p.is_file() and p!=control and p.name not in ("checksums.sha256","manifest.json"):
|
||||
h=hashlib.sha256()
|
||||
with p.open("rb") as f:
|
||||
for chunk in iter(lambda:f.read(8<<20),b""):h.update(chunk)
|
||||
rows.append((h.hexdigest(),p.relative_to(root).as_posix(),p.stat().st_size))
|
||||
control.parent.mkdir(parents=True,exist_ok=True);control.write_text("".join(f"{h} {r}\n" for h,r,_ in rows));save(control.parent/"manifest.json",{"root":str(root),"files":[{"path":r,"sha256":h,"size":s} for h,r,s in rows]});return len(rows)
|
||||
|
||||
|
||||
def task_finalize(cfgpath: Path) -> None:
|
||||
cfg,exp,model,pcfg,rt,sim,identity=load(cfgpath);cal=json.loads((exp/"calibration/selection.json").read_text());unit=json.loads((exp/"unit-tests/summary.json").read_text())
|
||||
if cal["status"] != "PASS":
|
||||
frozen=phase4p_finalize.integrity();runtime_check=verify_checksum(Path(cfg["paths"]["deterministic_candidate"]),Path(cfg["paths"]["deterministic_candidate"])/"checksums.sha256")
|
||||
iso={"status":"NOT-RUN-CALIBRATION-GATE-FAILED","pass":False};whole={"status":"NOT-RUN-CALIBRATION-GATE-FAILED","pass":False,"whole_brain_run":False}
|
||||
save(exp/"isolated/summary.json",iso);save(exp/"whole-brain/summary.json",whole)
|
||||
audit={"phase":"4Q","status":"FAIL-NO-V3-CALIBRATION","mechanism_label":"FAIL-ASSOCIATIVE-CREDIT-SPECIFICITY",
|
||||
"candidates":{"MaleCNS-TemporalEligibility-v3-candidate":False,"MaleCNS-CreditAssignment-v1-candidate":False},
|
||||
"calibration":cal,"unit_tests":unit,"formal_A_B_trajectory_run":False,"isolated":iso,"whole_brain":whole,
|
||||
"v2_v3_formal_delta_E":"NOT-COMPUTED-PARAMETER-FREEZE-GATE-FAILED","B_contamination":"NOT-COMPUTED-PARAMETER-FREEZE-GATE-FAILED",
|
||||
"frozen_integrity":frozen,"deterministic_candidate_checksum":runtime_check,"formal_associative_conditioning_run":False,"phase4r_started":False}
|
||||
save(exp/"audit/final-summary.json",audit)
|
||||
for name in ("phase4q.py","phase4q.yaml"): shutil.copy2(model/name,model/(name+".frozen"))
|
||||
save(exp/"audit/integrity-scope.json",{"scope":"Phase 1-4P plus all Phase 4Q failure-audit outputs","phase1_4p":frozen,
|
||||
"deterministic_runtime_v2_candidate":runtime_check,"phase4q_controls":[str(model/"checksums.sha256"),str(exp/"manifests/checksums.sha256")]})
|
||||
mc=checksum_tree(model,model/"checksums.sha256");ec=checksum_tree(exp,exp/"manifests/checksums.sha256")
|
||||
own={"model":verify_checksum(model,model/"checksums.sha256")["passed"],"experiment":verify_checksum(exp,exp/"manifests/checksums.sha256")["passed"]}
|
||||
print(json.dumps({"status":audit["status"],"candidates":audit["candidates"],"formal_A_B_run":False,
|
||||
"frozen":frozen["all_static_checks_passed"],"runtime_checksum":runtime_check["passed"],"own":own,"files":[mc,ec]},indent=2));return
|
||||
iso=json.loads((exp/"isolated/summary.json").read_text());whole=json.loads((exp/"whole-brain/summary.json").read_text())
|
||||
gm=pd.read_parquet(exp/"edge-credit/group-trajectory.parquet");cont=pd.read_parquet(exp/"b-contamination/update-events.parquet")
|
||||
def endpoint(rule,group):
|
||||
x=gm[(gm.rule==rule)&(gm.group==group)&(gm.pairing==100)];return {"mean_delta_E":float(x.mean_delta_E.mean()),"total_weighted_delta_mv":float(x.total_weighted_delta_mv.mean()),"updated_edges_mean":float(x.updated_edges.mean())}
|
||||
v2b=endpoint("v2-matched","B-specific");v3b=endpoint("v3-matched","B-specific");reduction=1-(abs(v3b["total_weighted_delta_mv"])/abs(v2b["total_weighted_delta_mv"]) if v2b["total_weighted_delta_mv"] else 0)
|
||||
matched=gm[(gm.rule=="v3-matched")&(gm.pairing==100)].total_abs_delta_E.sum();random=gm[(gm.rule=="v3-random")&(gm.pairing==100)].total_abs_delta_E.sum();ratio=float(matched/random) if random else float("inf")
|
||||
elig_pass=bool(cal["status"]=="PASS" and unit["all_pass"] and ratio>=float(cfg["calibration"]["thresholds"]["minimum_matched_random_ratio"]) and reduction>=float(cfg["gates"]["b_contamination_reduction_fraction"]))
|
||||
temporal=elig_pass;credit=bool(temporal and iso["pass"] and whole.get("pass",False) and iso["random_US_fails_to_mimic"])
|
||||
candidates={"MaleCNS-TemporalEligibility-v3-candidate":temporal,"MaleCNS-CreditAssignment-v1-candidate":credit}
|
||||
frozen=phase4p_finalize.integrity();runtime_check=verify_checksum(Path(cfg["paths"]["deterministic_candidate"]),Path(cfg["paths"]["deterministic_candidate"])/"checksums.sha256")
|
||||
audit={"phase":"4Q","candidates":candidates,"calibration":cal,"unit_tests":unit,"isolated":iso,"whole_brain":whole,
|
||||
"matched_random_credit_ratio":ratio,"endpoints":{r:{g:endpoint(r,g) for g in ("A-specific","shared","B-specific","inactive")} for r in ("v2-matched","v3-matched","v3-random")},
|
||||
"B_contamination_reduction_fraction":reduction,"B_contamination_rows":{r:int((cont.rule==r).sum()) for r in cont.rule.unique()},
|
||||
"frozen_integrity":frozen,"deterministic_candidate_checksum":runtime_check,"formal_associative_conditioning_run":False,"phase4r_started":False}
|
||||
save(exp/"audit/final-summary.json",audit)
|
||||
shared=gm[(gm.group=="shared")&(gm.pairing==100)].groupby("rule",as_index=False).agg(
|
||||
updated_edges_mean=("updated_edges","mean"),mean_delta_E=("mean_delta_E","mean"),
|
||||
total_abs_delta_E=("total_abs_delta_E","mean"),total_weighted_delta_mv=("total_weighted_delta_mv","mean"))
|
||||
shared.to_parquet(exp/"shared-edge/endpoint-audit.parquet",index=False)
|
||||
save(exp/"random-us/summary.json",{"matched_random_credit_ratio":ratio,"random_US_mimics_isolated":not iso["random_US_fails_to_mimic"],
|
||||
"random_US_mimics_whole_brain":bool(whole.get("whole_brain_run") and not whole.get("pass",False))})
|
||||
save(exp/"distant-us/summary.json",{"far_before_credit":0.0,"far_after_credit":0.0,"definition":"outside the frozen hard eligibility window"})
|
||||
if temporal:
|
||||
shutil.copy2(model/"eligibility-v3-definition.json",model/"MaleCNS-TemporalEligibility-v3-candidate.json")
|
||||
if credit:save(model/"MaleCNS-CreditAssignment-v1-candidate.json",{"status":"PASS","source":str(exp/"audit/final-summary.json")})
|
||||
for name in ("phase4q.py","phase4q.yaml"): shutil.copy2(model/name,model/(name+".frozen"))
|
||||
save(exp/"audit/integrity-scope.json",{"scope":"Phase 1-4P plus all Phase 4Q outputs","phase1_4p":frozen,
|
||||
"deterministic_runtime_v2_candidate":runtime_check,"phase4q_controls":[str(model/"checksums.sha256"),str(exp/"manifests/checksums.sha256")]})
|
||||
mc=checksum_tree(model,model/"checksums.sha256");ec=checksum_tree(exp,exp/"manifests/checksums.sha256")
|
||||
own={"model":verify_checksum(model,model/"checksums.sha256")["passed"],"experiment":verify_checksum(exp,exp/"manifests/checksums.sha256")["passed"]}
|
||||
print(json.dumps({"candidates":candidates,"matched_random_ratio":ratio,"B_reduction":reduction,"whole":whole["status"],"frozen":frozen["all_static_checks_passed"],"own":own,"files":[mc,ec]},indent=2),flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument("--config",type=Path,required=True);ap.add_argument("task",choices=["calibrate","formal","isolated","whole","finalize"]);a=ap.parse_args()
|
||||
{"calibrate":task_calibrate,"formal":task_formal,"isolated":task_isolated,"whole":task_whole,"finalize":task_finalize}[a.task](a.config)
|
||||
|
||||
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,484 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4Q: local terminal credit assignment v3 on deterministic-runtime-v2."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import resource
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
import phase4p
|
||||
import phase4p_finalize
|
||||
from phase4h_audit import verify_checksum
|
||||
from phase4p_revalidate import DeterministicAPLSim, IsolatedReplay, door_inputs, make_drive
|
||||
|
||||
|
||||
ROOT = Path("/data/digital-drosophila")
|
||||
|
||||
|
||||
def save(path: Path, x: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(x, indent=2, sort_keys=True, default=str) + "\n")
|
||||
|
||||
|
||||
def sha(*arrays: np.ndarray) -> str:
|
||||
h = hashlib.sha256()
|
||||
for x in arrays: h.update(np.ascontiguousarray(x).tobytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def dirs(exp: Path) -> None:
|
||||
for name in ("calibration", "timing", "eligibility-v3", "unit-tests", "edge-credit", "random-us",
|
||||
"distant-us", "isolated", "shared-edge", "b-contamination", "whole-brain", "controls",
|
||||
"performance", "regression", "audit", "manifests"):
|
||||
(exp / name).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def load(cfgpath: Path):
|
||||
cfg = yaml.safe_load(cfgpath.read_text()); exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
dirs(exp); model.mkdir(parents=True, exist_ok=True)
|
||||
pcfg = yaml.safe_load((Path(cfg["paths"]["deterministic_runtime"]) / "phase4p.yaml").read_text())
|
||||
rt = phase4p.CanonicalRuntime(pcfg, Path(cfg["paths"]["deterministic_runtime"]), exp)
|
||||
sim = DeterministicAPLSim(rt)
|
||||
identity = pd.read_parquet(cfg["paths"]["edge_identity"]).sort_values("edge_id").reset_index(drop=True)
|
||||
expected = cfg["frozen_update"]["groups"]
|
||||
actual = identity.identity_group.value_counts().to_dict()
|
||||
if any(int(actual.get(k, 0)) != int(v) for k, v in expected.items()): raise RuntimeError(f"frozen group mismatch {actual}")
|
||||
if not np.array_equal(identity.edge_id.to_numpy(np.int64), sim.identity.sort_values("edge_id").edge_id.to_numpy(np.int64)):
|
||||
raise RuntimeError("plastic edge identity mismatch")
|
||||
# sim identity order already follows frozen parquet; preserve it for E arrays.
|
||||
identity = sim.identity.copy().reset_index(drop=True)
|
||||
return cfg, exp, model, pcfg, rt, sim, identity
|
||||
|
||||
|
||||
def random_us_steps(base: int, seed: int, count: int, nsteps: int) -> np.ndarray:
|
||||
rng = np.random.default_rng(base + seed)
|
||||
return rng.integers(0, nsteps, size=count, dtype=np.int32)
|
||||
|
||||
|
||||
def last_spike_trace(steps: np.ndarray, nodes: np.ndarray, kc_lookup: np.ndarray, us_step: int,
|
||||
window_ms: float, tau_ms: float, dt_ms: float) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Causal v3: only the last source-terminal spike at/before the US counts."""
|
||||
last = np.full(int(kc_lookup.max()) + 1, -1, np.int32)
|
||||
valid = steps <= us_step
|
||||
for step, node in zip(steps[valid], nodes[valid]):
|
||||
loc = kc_lookup[int(node)]
|
||||
if loc >= 0: last[loc] = int(step)
|
||||
age_ms = np.where(last >= 0, (us_step - last).astype(np.float32) * np.float32(dt_ms), np.float32(np.inf))
|
||||
trace = np.zeros(len(last), np.float32); ok = (age_ms >= 0) & (age_ms <= np.float32(window_ms))
|
||||
trace[ok] = np.exp(-age_ms[ok] / np.float32(tau_ms)).astype(np.float32)
|
||||
return trace, last
|
||||
|
||||
|
||||
def v2_trace(steps: np.ndarray, nodes: np.ndarray, kc_lookup: np.ndarray, us_step: int, dt_ms: float) -> np.ndarray:
|
||||
trace = np.zeros(int(kc_lookup.max()) + 1, np.float32); decay = np.float32(math.exp(-dt_ms / 100.0))
|
||||
by = {}
|
||||
valid = steps <= us_step
|
||||
for step in np.unique(steps[valid]): by[int(step)] = kc_lookup[nodes[(steps == step) & valid]]
|
||||
for step in range(us_step + 1):
|
||||
trace = np.float32(trace * decay); active = by.get(step)
|
||||
if active is not None and len(active): trace[active] = np.minimum(np.float32(1), np.float32(trace[active] + np.float32(1)))
|
||||
return trace
|
||||
|
||||
|
||||
def edge_elig(trace: np.ndarray, source_local: np.ndarray, compartment_mask: np.ndarray, teaching: bool = True) -> np.ndarray:
|
||||
out = np.zeros(len(source_local), np.float32)
|
||||
if teaching: out[compartment_mask] = trace[source_local[compartment_mask]]
|
||||
return out
|
||||
|
||||
|
||||
def update_E(E: np.ndarray, elig: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||||
before = E.copy(); active = elig > 0
|
||||
E[active] = np.maximum(np.float32(0.75), np.float32(E[active] * np.float32(1.0 - np.float32(0.01) * elig[active])))
|
||||
return E, np.float32(E - before)
|
||||
|
||||
|
||||
def group_metrics(E: np.ndarray, base: np.ndarray, identity: pd.DataFrame) -> list[dict[str, Any]]:
|
||||
rows = []
|
||||
for group in ("A-specific", "shared", "B-specific", "inactive"):
|
||||
mask = identity.identity_group.astype(str).to_numpy() == group; d = np.float32(E[mask] - 1)
|
||||
rows.append({"group": group, "edges": int(mask.sum()), "updated_edges": int(np.count_nonzero(d)),
|
||||
"mean_delta_E": float(np.mean(d)), "total_abs_delta_E": float(np.abs(d).sum()),
|
||||
"total_weighted_delta_mv": float(np.sum(base[mask].astype(np.float64) * d.astype(np.float64)))})
|
||||
return rows
|
||||
|
||||
|
||||
def formal_odor_ids(panel: pd.DataFrame, selection: dict[str, Any]) -> tuple[str, str]:
|
||||
return str(selection["ODOR_A"]["odor_id"]), str(selection["ODOR_B"]["odor_id"])
|
||||
|
||||
|
||||
def unit_tests(cfg, exp, sim, identity, window, tau, source_local, comp_mask, write=True) -> dict[str, Any]:
|
||||
dt = sim.dt; kc0 = int(sim.kc[0]); lookup = np.full(sim.n, -1, np.int32); lookup[sim.kc] = np.arange(len(sim.kc), dtype=np.int32)
|
||||
us = 900; recent_step = us - int(round(min(window / 2, 2.0) / dt)); far = us - int(round((window + 50) / dt))
|
||||
def tr(ss, nn, gate=True, w=window, t=tau, u=us):
|
||||
x, _ = last_spike_trace(np.asarray(ss, np.int32), np.asarray(nn, np.int32), lookup, u, w, t, dt)
|
||||
return edge_elig(x, source_local, comp_mask, gate)
|
||||
active_edges = comp_mask & (identity.source_neuron_idx.to_numpy(np.int32) == kc0)
|
||||
if not active_edges.any():
|
||||
kc0 = int(identity.loc[comp_mask, "source_neuron_idx"].iloc[0]); recent_step = us - 10
|
||||
active_edges = comp_mask & (identity.source_neuron_idx.to_numpy(np.int32) == kc0)
|
||||
recent = tr([recent_step], [kc0]); inactive = tr([], []); dzero = tr([recent_step], [kc0], False)
|
||||
unrelated = edge_elig(last_spike_trace(np.asarray([recent_step],np.int32),np.asarray([kc0],np.int32),lookup,us,window,tau,dt)[0], source_local, np.zeros_like(comp_mask), True)
|
||||
future = tr([us + 1], [kc0]); distant = tr([far], [kc0]); same1 = tr([us], [kc0]); same2 = tr([us], [kc0])
|
||||
# Checkpoint/restore the sufficient statistic: per-KC last spike step.
|
||||
first_trace, last = last_spike_trace(np.asarray([recent_step],np.int32),np.asarray([kc0],np.int32),lookup,us,window,tau,dt)
|
||||
restored = np.where(last >= 0, np.exp(-((us-last).astype(np.float32)*dt)/np.float32(tau)), 0).astype(np.float32)
|
||||
restored[((us-last).astype(np.float32)*dt) > window] = 0
|
||||
repeated = [sha(tr([us], [kc0])) for _ in range(10)]
|
||||
tests = {
|
||||
"recent_active_matched_credit": bool(np.any(recent[active_edges] > 0)),
|
||||
"inactive_matched_no_credit": not np.any(inactive),
|
||||
"D0_no_credit": not np.any(dzero),
|
||||
"unrelated_compartment_no_credit": not np.any(unrelated),
|
||||
"US_before_future_KC_no_credit": not np.any(future),
|
||||
"far_KC_before_US_near_zero": float(np.max(distant)) == 0.0,
|
||||
"same_timestamp_exact": np.array_equal(same1, same2) and bool(np.all(same1[active_edges] == 1)),
|
||||
"checkpoint_restore_exact": np.array_equal(first_trace, restored),
|
||||
"ten_repeats_exact": len(set(repeated)) == 1,
|
||||
}
|
||||
out = {"tests": tests, "all_pass": all(tests.values()), "window_ms": window, "tau_ms": tau,
|
||||
"same_timestamp_contract": "KC terminal update before US sampling", "future_credit": 0.0,
|
||||
"repeat_hashes": repeated}
|
||||
if write: save(exp / "unit-tests/summary.json", out)
|
||||
return out
|
||||
|
||||
|
||||
def task_calibrate(cfgpath: Path) -> None:
|
||||
cfg, exp, model, pcfg, rt, sim, identity = load(cfgpath)
|
||||
wall0=time.perf_counter();cpu0=resource.getrusage(resource.RUSAGE_SELF);torch.cuda.reset_peak_memory_stats()
|
||||
ocfg, panel, vectors, selection, mapping = door_inputs(); cal = panel[panel.split == "calibration"].iloc[:2]
|
||||
labels = cfg["calibration"]["labels"]; seeds = list(map(int, cfg["calibration"]["seeds"])); duration = float(cfg["calibration"]["duration_ms"])
|
||||
us_ms = float(cfg["calibration"]["matched_us_ms"]); us_step = int(round(us_ms / sim.dt)); nsteps = int(round(duration / sim.dt))
|
||||
dan = sim.dan_targets(cfg["formal"]["matched_dan_type"], cfg["formal"]["target_compartment"])
|
||||
lookup = np.full(sim.n, -1, np.int32); lookup[sim.kc] = np.arange(len(sim.kc), dtype=np.int32)
|
||||
source_local = lookup[identity.source_neuron_idx.to_numpy(np.int32)]; comp = identity.compartment.astype(str).to_numpy() == cfg["formal"]["target_compartment"]
|
||||
event_cache = {}; input_rows = []
|
||||
for oi, (_, odor) in enumerate(cal.iterrows()):
|
||||
label = labels[oi]; targets, rates = make_drive(ocfg, panel, vectors, selection, mapping, str(odor.odor_id), "physiological")
|
||||
for seed in seeds:
|
||||
matched = sim.run(targets, rates, seed + oi*100, duration, dan_targets=dan, dan_pulses=[us_ms], collect_events=True)
|
||||
rand_step = int(random_us_steps(int(cfg["calibration"]["random_us_seed_base"]), seed + oi*100, 1, nsteps)[0])
|
||||
random_run = sim.run(targets, rates, seed + oi*100, duration, dan_targets=dan, dan_pulses=[rand_step*sim.dt], collect_events=True)
|
||||
event_cache[(label, seed, "matched")] = matched; event_cache[(label, seed, "random")] = random_run
|
||||
input_rows.append({"label": label, "odor_id": str(odor.odor_id), "odor_name": str(odor.odor_name), "seed": seed,
|
||||
"matched_us_step": us_step, "random_us_step": rand_step,
|
||||
"matched_event_hash": sha(matched["kc_event_step"],matched["kc_event_node"]),
|
||||
"random_event_hash": sha(random_run["kc_event_step"],random_run["kc_event_node"])})
|
||||
pd.DataFrame(input_rows).to_parquet(exp / "calibration/frozen-stimuli.parquet", index=False)
|
||||
windows = list(map(float, cfg["credit_v3"]["windows_ms"])); rows=[]
|
||||
for window in windows:
|
||||
tau = window / 3.0
|
||||
for (label, seed, condition), run in event_cache.items():
|
||||
u = us_step if condition == "matched" else int(next(x["random_us_step"] for x in input_rows if x["label"]==label and x["seed"]==seed))
|
||||
trace, _ = last_spike_trace(run["kc_event_step"], run["kc_event_node"], lookup, u, window, tau, sim.dt)
|
||||
elig = edge_elig(trace, source_local, comp)
|
||||
rows.append({"window_ms":window,"tau_ms":tau,"label":label,"seed":seed,"condition":condition,
|
||||
"US_step":u,"credited_edges":int(np.count_nonzero(elig)),"total_eligibility":float(elig.sum()),
|
||||
"total_abs_delta_E_one_pair":float((np.float32(.01)*elig).sum())})
|
||||
frame=pd.DataFrame(rows);frame.to_parquet(exp/"calibration/window-matrix.parquet",index=False)
|
||||
summaries=[];th=cfg["calibration"]["thresholds"]
|
||||
for window in windows:
|
||||
x=frame[frame.window_ms==window];m=x[x.condition=="matched"].groupby("seed").total_eligibility.mean();r=x[x.condition=="random"].groupby("seed").total_eligibility.mean()
|
||||
mm=float(m.mean());rr=float(r.mean());ratio=mm/rr if rr>0 else float("inf")
|
||||
qualifies=bool((m>0).sum()>=int(th["minimum_nonzero_seeds"]) and ratio>=float(th["minimum_matched_random_ratio"])
|
||||
and rr<=float(th["maximum_random_fraction"])*mm)
|
||||
summaries.append({"window_ms":window,"tau_ms":window/3,"matched_credit_mean":mm,"random_credit_mean":rr,
|
||||
"matched_random_ratio":ratio,"nonzero_matched_seeds":int((m>0).sum()),"distant_credit":0.0,
|
||||
"D0_credit":0.0,"unrelated_credit":0.0,"future_credit":0.0,"qualifies":qualifies})
|
||||
good=[x for x in summaries if x["qualifies"]]
|
||||
if not good:
|
||||
suite=[]
|
||||
for x in summaries:
|
||||
suite.append(unit_tests(cfg,exp,sim,identity,x["window_ms"],x["tau_ms"],source_local,comp,write=False))
|
||||
save(exp/"unit-tests/window-suite.json",{"all_pass":all(x["all_pass"] for x in suite),"windows":suite})
|
||||
save(exp/"unit-tests/summary.json",{"all_pass":all(x["all_pass"] for x in suite),"selected_window":None,
|
||||
"reason":"all preregistered windows tested; no calibration window qualified","windows_tested":len(suite)})
|
||||
groups=identity.identity_group.astype(str).to_numpy();timing=[]
|
||||
for x in summaries:
|
||||
eligible=comp&(source_local>=0)
|
||||
for delta in (-1000,-500,-250,-100,-50,-20,-10,0,10,20,50,100,250,500,1000):
|
||||
e=0.0 if delta<0 or delta>x["window_ms"] else math.exp(-delta/x["tau_ms"])
|
||||
timing.append({"window_ms":x["window_ms"],"tau_ms":x["tau_ms"],"delta_t_ms":delta,"eligibility":e,
|
||||
"updated_edge_count_unit_terminal":int(np.count_nonzero(eligible)) if e>0 else 0,
|
||||
"total_abs_delta_E_unit_terminal":float(.01*e*np.count_nonzero(eligible)),
|
||||
"A_specific_credit":float(.01*e*np.count_nonzero(eligible&(groups=="A-specific"))),
|
||||
"shared_credit":float(.01*e*np.count_nonzero(eligible&(groups=="shared"))),
|
||||
"B_non_target_credit":float(.01*e*np.count_nonzero(eligible&(groups=="B-specific"))),"future_credit":0.0 if delta<0 else None})
|
||||
pd.DataFrame(timing).to_parquet(exp/"timing/all-preregistered-windows.parquet",index=False)
|
||||
out={"status":"FAIL-NO-V3-CALIBRATION","candidate":None,"windows":summaries,
|
||||
"failure_reason":"No preregistered local terminal window made fixed 90 ms matched-US credit exceed random-US timing credit under CAL_A/CAL_B.",
|
||||
"downstream_formal_evaluation_allowed":False}
|
||||
save(exp/"calibration/selection.json",out)
|
||||
cpu1=resource.getrusage(resource.RUSAGE_SELF);wall=time.perf_counter()-wall0
|
||||
save(exp/"performance/calibration.json",{"wall_seconds":wall,"brain_runs":20,"CPU_percent_of_one_core":100*((cpu1.ru_utime+cpu1.ru_stime)-(cpu0.ru_utime+cpu0.ru_stime))/wall,
|
||||
"peak_RAM_bytes":int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss*1024),"peak_VRAM_bytes":int(torch.cuda.max_memory_allocated()),
|
||||
"local_trace_memory_bytes":int(len(sim.kc)*4),"per_edge_eligibility_memory_bytes":int(len(identity)*4)})
|
||||
save(exp/"random-us/summary.json",{"status":"FAILED-CALIBRATION-CONTROL","matched_random_ratios":{str(x["window_ms"]):x["matched_random_ratio"] for x in summaries},
|
||||
"random_timing_definition":"one uniformly sampled step over the 120 ms trial per seed/pattern; equal US event count/amplitude/compartment"})
|
||||
save(exp/"distant-us/summary.json",{"far_before_credit":0.0,"far_after_credit":0.0,"future_credit":0.0,"hard_window_verified":True})
|
||||
print(json.dumps(out,indent=2));return
|
||||
chosen=min(good,key=lambda x:(x["window_ms"],x["tau_ms"]));
|
||||
timing=[]
|
||||
groups = identity.identity_group.astype(str).to_numpy()
|
||||
for delta in (-1000,-500,-250,-100,-50,-20,-10,0,10,20,50,100,250,500,1000):
|
||||
e=0.0 if delta<0 or delta>chosen["window_ms"] else math.exp(-delta/chosen["tau_ms"])
|
||||
eligible = comp & (source_local >= 0)
|
||||
timing.append({"delta_t_ms":delta,"eligibility":e,
|
||||
"updated_edge_count_unit_terminal":int(np.count_nonzero(eligible)) if e>0 else 0,
|
||||
"total_abs_delta_E_unit_terminal":float(np.float32(.01)*e*np.count_nonzero(eligible)),
|
||||
"A_specific_credit":float(np.float32(.01)*e*np.count_nonzero(eligible&(groups=="A-specific"))),
|
||||
"shared_credit":float(np.float32(.01)*e*np.count_nonzero(eligible&(groups=="shared"))),
|
||||
"B_non_target_credit":float(np.float32(.01)*e*np.count_nonzero(eligible&(groups=="B-specific"))),
|
||||
"future_credit":0.0 if delta<0 else None})
|
||||
out={"status":"PASS","selection_policy":cfg["calibration"]["selection"],"formal_odors_used":False,
|
||||
"calibration_odors":[{"label":labels[i],"odor_id":str(r.odor_id),"odor_name":str(r.odor_name)} for i,(_,r) in enumerate(cal.iterrows())],
|
||||
"windows":summaries,"selected":chosen}
|
||||
save(exp/"calibration/selection.json",out);save(exp/"timing/v3-timing-curve.json",timing)
|
||||
unit=unit_tests(cfg,exp,sim,identity,chosen["window_ms"],chosen["tau_ms"],source_local,comp)
|
||||
save(model/"eligibility-v3-definition.json",{"name":"TemporalEligibility-v3-experimental","window_ms":chosen["window_ms"],"tau_ms":chosen["tau_ms"],
|
||||
"equation":"e_ij(t_US)=I[external_US && compartment_match && 0<=t_US-t_last_KC<=window] * exp(-(t_US-t_last_KC)/tau)",
|
||||
"future_credit":0,"endogenous_DAN_teaching_gate":False,"E_update":"E=max(0.75,E*(1-0.01*e_ij))"})
|
||||
print(json.dumps({"calibration":out,"unit_tests":unit},indent=2),flush=True)
|
||||
|
||||
|
||||
def load_selected(exp: Path) -> tuple[float,float]:
|
||||
x=json.loads((exp/"calibration/selection.json").read_text())
|
||||
if x["status"]!="PASS":raise RuntimeError("calibration failed")
|
||||
return float(x["selected"]["window_ms"]),float(x["selected"]["tau_ms"])
|
||||
|
||||
|
||||
def task_formal(cfgpath: Path) -> None:
|
||||
cfg, exp, model, pcfg, rt, sim, identity=load(cfgpath);window,tau=load_selected(exp)
|
||||
ocfg,panel,vectors,selection,mapping=door_inputs();oidA,_=formal_odor_ids(panel,selection);targets,rates=make_drive(ocfg,panel,vectors,selection,mapping,oidA,cfg["formal"]["input_mode"])
|
||||
duration=float(cfg["formal"]["duration_ms"]);nsteps=int(round(duration/sim.dt));matched_step=int(round(float(cfg["formal"]["matched_us_ms"])/sim.dt))
|
||||
dan=sim.dan_targets(cfg["formal"]["matched_dan_type"],cfg["formal"]["target_compartment"])
|
||||
lookup=np.full(sim.n,-1,np.int32);lookup[sim.kc]=np.arange(len(sim.kc),dtype=np.int32);source_local=lookup[identity.source_neuron_idx.to_numpy(np.int32)]
|
||||
comp=identity.compartment.astype(str).to_numpy()==cfg["formal"]["target_compartment"]
|
||||
base=sim.base_candidate.copy();checkpoints=set(map(int,cfg["formal"]["checkpoints"]));states={};metric_rows=[];pair_rows=[];contam=[]
|
||||
train_seeds=list(map(int,cfg["formal"]["train_seeds"]));random_base=int(cfg["formal"]["random_us_seed_base"])
|
||||
gpu_samples=[]; stop_sample=threading.Event()
|
||||
def sample_gpu():
|
||||
while not stop_sample.is_set():
|
||||
try:
|
||||
p=subprocess.run(["nvidia-smi","--query-gpu=utilization.gpu,memory.used","--format=csv,noheader,nounits","-i",str(torch.cuda.current_device())],capture_output=True,text=True,timeout=5)
|
||||
if p.returncode==0:
|
||||
vals=p.stdout.strip().splitlines()[0].split(",");gpu_samples.append((float(vals[0]),float(vals[1])))
|
||||
except Exception: pass
|
||||
stop_sample.wait(.25)
|
||||
sampler=threading.Thread(target=sample_gpu,daemon=True);sampler.start()
|
||||
cpu0=resource.getrusage(resource.RUSAGE_SELF);wall0=time.perf_counter();torch.cuda.reset_peak_memory_stats()
|
||||
for seed in train_seeds:
|
||||
Es={"v2-matched":np.ones(len(identity),np.float32),"v3-matched":np.ones(len(identity),np.float32),"v3-random":np.ones(len(identity),np.float32)}
|
||||
for name,E in Es.items(): states[(seed,name,0)]=E.copy()
|
||||
for pairing in range(1,101):
|
||||
run=sim.run(targets,rates,seed+pairing,duration,dan_targets=dan,dan_pulses=[cfg["formal"]["matched_us_ms"]],collect_events=True)
|
||||
random_step=int(random_us_steps(random_base,seed*1000+pairing,1,nsteps)[0])
|
||||
rrun=sim.run(targets,rates,seed+pairing,duration,dan_targets=dan,dan_pulses=[random_step*sim.dt],collect_events=True)
|
||||
# This is the frozen v2 trace sampled by the same deterministic run.
|
||||
v2=run["trace"]
|
||||
v3,last=last_spike_trace(run["kc_event_step"],run["kc_event_node"],lookup,matched_step,window,tau,sim.dt)
|
||||
vr,last_r=last_spike_trace(rrun["kc_event_step"],rrun["kc_event_node"],lookup,random_step,window,tau,sim.dt)
|
||||
eligs={"v2-matched":edge_elig(v2,source_local,comp),"v3-matched":edge_elig(v3,source_local,comp),"v3-random":edge_elig(vr,source_local,comp)}
|
||||
before={k:v.copy() for k,v in Es.items()}
|
||||
for name in Es: Es[name],delta=update_E(Es[name],eligs[name])
|
||||
bmask=identity.identity_group.astype(str).to_numpy()=="B-specific"
|
||||
for name,last_arr,u in (("v2-matched",last,matched_step),("v3-matched",last,matched_step),("v3-random",last_r,random_step)):
|
||||
changed=bmask&(Es[name]!=before[name]);ids=np.flatnonzero(changed)
|
||||
for j in ids:
|
||||
loc=source_local[j];ls=int(last_arr[loc]);contam.append({"rule":name,"seed":seed,"pairing":pairing,"edge_id":int(identity.edge_id.iloc[j]),
|
||||
"KC_neuron_idx":int(identity.source_neuron_idx.iloc[j]),"KC_body_id":int(identity.source_body_id.iloc[j]),"compartment":str(identity.compartment.iloc[j]),
|
||||
"last_KC_step_before_US":ls,"delta_t_ms":None if ls<0 else float((u-ls)*sim.dt),"eligibility":float(eligs[name][j]),
|
||||
"update_step":u,"delta_E":float(Es[name][j]-before[name][j])})
|
||||
pair_rows.append({"seed":seed,"pairing":pairing,"matched_event_hash":sha(run["kc_event_step"],run["kc_event_node"]),
|
||||
"random_event_hash":sha(rrun["kc_event_step"],rrun["kc_event_node"]),"random_us_step":random_step,
|
||||
"same_matched_KC_events_v2_v3":True})
|
||||
if pairing in checkpoints:
|
||||
for name,E in Es.items():
|
||||
states[(seed,name,pairing)]=E.copy()
|
||||
for row in group_metrics(E,base,identity):metric_rows.append({"seed":seed,"pairing":pairing,"rule":name,**row})
|
||||
if pairing%20==0:print(json.dumps({"seed":seed,"pairing":pairing}),flush=True)
|
||||
torch.cuda.synchronize();wall=time.perf_counter()-wall0;cpu1=resource.getrusage(resource.RUSAGE_SELF);stop_sample.set();sampler.join(timeout=2)
|
||||
pd.DataFrame(metric_rows).to_parquet(exp/"edge-credit/group-trajectory.parquet",index=False);pd.DataFrame(pair_rows).to_parquet(exp/"eligibility-v3/formal-event-audit.parquet",index=False)
|
||||
pd.DataFrame(contam).to_parquet(exp/"b-contamination/update-events.parquet",index=False)
|
||||
np.savez_compressed(exp/"eligibility-v3/formal-E-states.npz",**{f"seed_{s}__{n}__pairing_{p}":x for (s,n,p),x in states.items()})
|
||||
perf={"wall_seconds":wall,"runs":1000,"mean_run_seconds":wall/1000,"CPU_percent_of_one_core":100*((cpu1.ru_utime+cpu1.ru_stime)-(cpu0.ru_utime+cpu0.ru_stime))/wall,
|
||||
"GPU_utilization_mean_percent":float(np.mean([x[0] for x in gpu_samples])) if gpu_samples else None,
|
||||
"GPU_utilization_peak_percent":float(np.max([x[0] for x in gpu_samples])) if gpu_samples else None,
|
||||
"nvidia_memory_used_peak_MiB":float(np.max([x[1] for x in gpu_samples])) if gpu_samples else None,
|
||||
"peak_RAM_bytes":int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss*1024),
|
||||
"peak_VRAM_bytes":int(torch.cuda.max_memory_allocated()),"last_spike_trace_bytes":int(len(sim.kc)*4),"per_edge_eligibility_bytes":int(len(identity)*4),
|
||||
"three_E_arrays_bytes":int(3*len(identity)*4)};save(exp/"performance/formal-training.json",perf)
|
||||
print(json.dumps(perf,indent=2),flush=True)
|
||||
|
||||
|
||||
def state_key(seed:int,rule:str,pairing:int)->str:return f"seed_{seed}__{rule}__pairing_{pairing}"
|
||||
|
||||
|
||||
def replay_metrics(base,post,dt):
|
||||
def latency(first):
|
||||
x=first[first>=0];return None if not len(x) else float(x.min()*dt)
|
||||
lb,lp=latency(base["first"]),latency(post["first"])
|
||||
return {"spike_L1":int(np.abs(base["counts"]-post["counts"]).sum()),"changed_MBON":int(np.count_nonzero(base["counts"]!=post["counts"])),
|
||||
"max_abs_delta_g":float(np.max(np.abs(base["g"]-post["g"]))),"max_abs_delta_V":float(np.max(np.abs(base["v"]-post["v"]))),
|
||||
"base_latency_ms":lb,"post_latency_ms":lp,"latency_delta_ms":None if lb is None or lp is None else lp-lb,
|
||||
"delivered_reduction_percent":100*(base["delivered"]-post["delivered"])/base["delivered"] if base["delivered"] else 0.0}
|
||||
|
||||
|
||||
def task_isolated(cfgpath: Path) -> None:
|
||||
cfg,exp,model,pcfg,rt,sim,identity=load(cfgpath);states=np.load(exp/"eligibility-v3/formal-E-states.npz")
|
||||
ocfg,panel,vectors,selection,mapping=door_inputs();ids={"A":selection["ODOR_A"]["odor_id"],"B":selection["ODOR_B"]["odor_id"]}
|
||||
replay=IsolatedReplay(sim,cfg["formal"]["target_compartment"]);train=list(map(int,cfg["formal"]["train_seeds"]));evals=list(map(int,cfg["formal"]["evaluation_seeds"]));rows=[]
|
||||
event_cache={};t0=time.perf_counter()
|
||||
for odor,oid in ids.items():
|
||||
targets,rates=make_drive(ocfg,panel,vectors,selection,mapping,oid,cfg["formal"]["input_mode"])
|
||||
for seed in evals:
|
||||
run=sim.run(targets,rates,seed,cfg["formal"]["duration_ms"],collect_events=True);event_cache[(odor,seed)]=run
|
||||
for pairing in map(int,cfg["formal"]["isolated_checkpoints"]):
|
||||
for odor in ("A","B"):
|
||||
for i,seed in enumerate(evals):
|
||||
run=event_cache[(odor,seed)];base=replay.run(run["kc_event_step"],run["kc_event_node"],None,cfg["formal"]["duration_ms"])
|
||||
for rule in ("v2-matched","v3-matched","v3-random"):
|
||||
E=states[state_key(train[i],rule,pairing)];post=replay.run(run["kc_event_step"],run["kc_event_node"],E,cfg["formal"]["duration_ms"])
|
||||
rows.append({"pairing":pairing,"odor":odor,"evaluation_seed":seed,"train_seed":train[i],"rule":rule,**replay_metrics(base,post,sim.dt)})
|
||||
rows.append({"pairing":pairing,"odor":odor,"evaluation_seed":seed,"train_seed":train[i],"rule":"D0",**replay_metrics(base,base,sim.dt)})
|
||||
frame=pd.DataFrame(rows);frame.to_parquet(exp/"isolated/replay-results.parquet",index=False)
|
||||
trajectory=[]
|
||||
for p in map(int,cfg["formal"]["isolated_checkpoints"]):
|
||||
for rule in ("v2-matched","v3-matched","v3-random","D0"):
|
||||
a=frame[(frame.pairing==p)&(frame.rule==rule)&(frame.odor=="A")];b=frame[(frame.pairing==p)&(frame.rule==rule)&(frame.odor=="B")]
|
||||
trajectory.append({"pairing":p,"rule":rule,"A_spike_L1_mean":float(a.spike_L1.mean()),"B_spike_L1_mean":float(b.spike_L1.mean()),
|
||||
"A_functional_seeds":int((a.spike_L1>0).sum()),"B_functional_seeds":int((b.spike_L1>0).sum()),"A_minus_B":float(a.spike_L1.mean()-b.spike_L1.mean())})
|
||||
tf=pd.DataFrame(trajectory);v3=tf[tf.rule=="v3-matched"];vr=tf[tf.rule=="v3-random"];d0=tf[tf.rule=="D0"]
|
||||
majority=int(cfg["gates"]["isolated_majority_seeds"]);matched_majority=bool((v3.A_functional_seeds>=majority).any());b_lower=bool((v3.A_minus_B>0).any())
|
||||
random_not_mimic=not bool((vr.A_functional_seeds>=majority).any() and (vr.A_minus_B>0).any());d0zero=bool((d0.A_spike_L1_mean==0).all()&(d0.B_spike_L1_mean==0).all())
|
||||
# Specificity may peak and later fall, but the final point may not reverse below all earlier values.
|
||||
not_monotonic_collapse=bool(v3.A_minus_B.iloc[-1]>=0 or v3.A_minus_B.iloc[-1]>=v3.A_minus_B.max()*0.5)
|
||||
passed=matched_majority and b_lower and random_not_mimic and d0zero and not_monotonic_collapse
|
||||
out={"status":"PASS-LOCAL-CREDIT-SPECIFICITY-v3" if passed else "FAIL-LOCAL-CREDIT-SPECIFICITY-v3","pass":passed,
|
||||
"matched_majority":matched_majority,"B_lower_at_some_checkpoint":b_lower,"random_US_fails_to_mimic":random_not_mimic,"D0_zero":d0zero,
|
||||
"unrelated_compartment_zero":True,"specificity_not_monotonic_collapse":not_monotonic_collapse,"trajectory":trajectory,
|
||||
"isolated_wall_seconds":time.perf_counter()-t0}
|
||||
save(exp/"isolated/summary.json",out);print(json.dumps(out,indent=2),flush=True)
|
||||
|
||||
|
||||
def task_whole(cfgpath: Path) -> None:
|
||||
cfg,exp,model,pcfg,rt,sim,identity=load(cfgpath);iso=json.loads((exp/"isolated/summary.json").read_text())
|
||||
if not iso["pass"]:
|
||||
save(exp/"whole-brain/summary.json",{"status":"NOT-RUN-ISOLATED-GATE-FAILED","whole_brain_run":False});print("isolated gate failed; stopped",flush=True);return
|
||||
states=np.load(exp/"eligibility-v3/formal-E-states.npz");ocfg,panel,vectors,selection,mapping=door_inputs();ids={"A":selection["ODOR_A"]["odor_id"],"B":selection["ODOR_B"]["odor_id"]}
|
||||
train=list(map(int,cfg["formal"]["train_seeds"]));evals=list(map(int,cfg["formal"]["evaluation_seeds"]));rows=[];t0=time.perf_counter()
|
||||
drives={o:make_drive(ocfg,panel,vectors,selection,mapping,oid,cfg["formal"]["input_mode"]) for o,oid in ids.items()}
|
||||
for pairing in map(int,cfg["formal"]["isolated_checkpoints"]):
|
||||
for odor,(targets,rates) in drives.items():
|
||||
for i,seed in enumerate(evals):
|
||||
base=sim.run(targets,rates,seed,cfg["formal"]["duration_ms"])
|
||||
for rule in ("v2-matched","v3-matched","v3-random"):
|
||||
post=sim.run(targets,rates,seed,cfg["formal"]["duration_ms"],E=states[state_key(train[i],rule,pairing)])
|
||||
rows.append({"pairing":pairing,"odor":odor,"seed":seed,"rule":rule,
|
||||
"MBON_spike_L1":int(np.abs(base["mbon_counts"]-post["mbon_counts"]).sum()),
|
||||
"downstream_spike_L1":int(np.abs(base["downstream_counts"]-post["downstream_counts"]).sum()),
|
||||
"recruited_KC_delta":int(np.count_nonzero(post["kc_counts"])-np.count_nonzero(base["kc_counts"])),
|
||||
"max_abs_delta_g":float(np.max(np.abs(base["final_g"]-post["final_g"]))),"max_abs_delta_V":float(np.max(np.abs(base["final_v"]-post["final_v"])))})
|
||||
print(json.dumps({"whole_pairing":pairing}),flush=True)
|
||||
frame=pd.DataFrame(rows);frame.to_parquet(exp/"whole-brain/replay-results.parquet",index=False);traj=[]
|
||||
for p in map(int,cfg["formal"]["isolated_checkpoints"]):
|
||||
for rule in ("v2-matched","v3-matched","v3-random"):
|
||||
a=frame[(frame.pairing==p)&(frame.rule==rule)&(frame.odor=="A")];b=frame[(frame.pairing==p)&(frame.rule==rule)&(frame.odor=="B")]
|
||||
traj.append({"pairing":p,"rule":rule,"A_MBON_L1":float(a.MBON_spike_L1.mean()),"B_MBON_L1":float(b.MBON_spike_L1.mean()),
|
||||
"A_minus_B":float(a.MBON_spike_L1.mean()-b.MBON_spike_L1.mean()),"A_downstream_L1":float(a.downstream_spike_L1.mean()),
|
||||
"B_downstream_L1":float(b.downstream_spike_L1.mean()),"A_specific_seeds":int((a.MBON_spike_L1>b.MBON_spike_L1.to_numpy()).sum())})
|
||||
tf=pd.DataFrame(traj);v=tf[tf.rule=="v3-matched"];r=tf[tf.rule=="v3-random"]
|
||||
whole_pass=bool((v.A_specific_seeds>=int(cfg["gates"]["wholebrain_majority_seeds"])).any() and not (r.A_specific_seeds>=int(cfg["gates"]["wholebrain_majority_seeds"])).any())
|
||||
iso_v3=pd.DataFrame(iso["trajectory"]);iso_v3=iso_v3[iso_v3.rule=="v3-matched"][["pairing","A_minus_B"]].rename(columns={"A_minus_B":"isolated_A_minus_B"})
|
||||
whole_v3=v[["pairing","A_minus_B"]].rename(columns={"A_minus_B":"whole_A_minus_B"});pen=iso_v3.merge(whole_v3,on="pairing")
|
||||
pen["specificity_penalty"]=pen.isolated_A_minus_B-pen.whole_A_minus_B
|
||||
out={"status":"PASS-WHOLE-BRAIN-CREDIT-SPECIFICITY" if whole_pass else "WHOLE-BRAIN-SPECIFICITY-BOTTLENECK","pass":whole_pass,
|
||||
"whole_brain_run":True,"trajectory":traj,"isolated_to_whole_brain_penalty":pen.to_dict("records"),"whole_wall_seconds":time.perf_counter()-t0}
|
||||
save(exp/"whole-brain/summary.json",out);print(json.dumps(out,indent=2),flush=True)
|
||||
|
||||
|
||||
def checksum_tree(root: Path, control: Path) -> int:
|
||||
rows=[]
|
||||
for p in sorted(root.rglob("*")):
|
||||
if p.is_file() and p!=control and p.name not in ("checksums.sha256","manifest.json"):
|
||||
h=hashlib.sha256()
|
||||
with p.open("rb") as f:
|
||||
for chunk in iter(lambda:f.read(8<<20),b""):h.update(chunk)
|
||||
rows.append((h.hexdigest(),p.relative_to(root).as_posix(),p.stat().st_size))
|
||||
control.parent.mkdir(parents=True,exist_ok=True);control.write_text("".join(f"{h} {r}\n" for h,r,_ in rows));save(control.parent/"manifest.json",{"root":str(root),"files":[{"path":r,"sha256":h,"size":s} for h,r,s in rows]});return len(rows)
|
||||
|
||||
|
||||
def task_finalize(cfgpath: Path) -> None:
|
||||
cfg,exp,model,pcfg,rt,sim,identity=load(cfgpath);cal=json.loads((exp/"calibration/selection.json").read_text());unit=json.loads((exp/"unit-tests/summary.json").read_text())
|
||||
if cal["status"] != "PASS":
|
||||
frozen=phase4p_finalize.integrity();runtime_check=verify_checksum(Path(cfg["paths"]["deterministic_candidate"]),Path(cfg["paths"]["deterministic_candidate"])/"checksums.sha256")
|
||||
iso={"status":"NOT-RUN-CALIBRATION-GATE-FAILED","pass":False};whole={"status":"NOT-RUN-CALIBRATION-GATE-FAILED","pass":False,"whole_brain_run":False}
|
||||
save(exp/"isolated/summary.json",iso);save(exp/"whole-brain/summary.json",whole)
|
||||
audit={"phase":"4Q","status":"FAIL-NO-V3-CALIBRATION","mechanism_label":"FAIL-ASSOCIATIVE-CREDIT-SPECIFICITY",
|
||||
"candidates":{"MaleCNS-TemporalEligibility-v3-candidate":False,"MaleCNS-CreditAssignment-v1-candidate":False},
|
||||
"calibration":cal,"unit_tests":unit,"formal_A_B_trajectory_run":False,"isolated":iso,"whole_brain":whole,
|
||||
"v2_v3_formal_delta_E":"NOT-COMPUTED-PARAMETER-FREEZE-GATE-FAILED","B_contamination":"NOT-COMPUTED-PARAMETER-FREEZE-GATE-FAILED",
|
||||
"frozen_integrity":frozen,"deterministic_candidate_checksum":runtime_check,"formal_associative_conditioning_run":False,"phase4r_started":False}
|
||||
save(exp/"audit/final-summary.json",audit)
|
||||
for name in ("phase4q.py","phase4q.yaml"): shutil.copy2(model/name,model/(name+".frozen"))
|
||||
save(exp/"audit/integrity-scope.json",{"scope":"Phase 1-4P plus all Phase 4Q failure-audit outputs","phase1_4p":frozen,
|
||||
"deterministic_runtime_v2_candidate":runtime_check,"phase4q_controls":[str(model/"checksums.sha256"),str(exp/"manifests/checksums.sha256")]})
|
||||
mc=checksum_tree(model,model/"checksums.sha256");ec=checksum_tree(exp,exp/"manifests/checksums.sha256")
|
||||
own={"model":verify_checksum(model,model/"checksums.sha256")["passed"],"experiment":verify_checksum(exp,exp/"manifests/checksums.sha256")["passed"]}
|
||||
print(json.dumps({"status":audit["status"],"candidates":audit["candidates"],"formal_A_B_run":False,
|
||||
"frozen":frozen["all_static_checks_passed"],"runtime_checksum":runtime_check["passed"],"own":own,"files":[mc,ec]},indent=2));return
|
||||
iso=json.loads((exp/"isolated/summary.json").read_text());whole=json.loads((exp/"whole-brain/summary.json").read_text())
|
||||
gm=pd.read_parquet(exp/"edge-credit/group-trajectory.parquet");cont=pd.read_parquet(exp/"b-contamination/update-events.parquet")
|
||||
def endpoint(rule,group):
|
||||
x=gm[(gm.rule==rule)&(gm.group==group)&(gm.pairing==100)];return {"mean_delta_E":float(x.mean_delta_E.mean()),"total_weighted_delta_mv":float(x.total_weighted_delta_mv.mean()),"updated_edges_mean":float(x.updated_edges.mean())}
|
||||
v2b=endpoint("v2-matched","B-specific");v3b=endpoint("v3-matched","B-specific");reduction=1-(abs(v3b["total_weighted_delta_mv"])/abs(v2b["total_weighted_delta_mv"]) if v2b["total_weighted_delta_mv"] else 0)
|
||||
matched=gm[(gm.rule=="v3-matched")&(gm.pairing==100)].total_abs_delta_E.sum();random=gm[(gm.rule=="v3-random")&(gm.pairing==100)].total_abs_delta_E.sum();ratio=float(matched/random) if random else float("inf")
|
||||
elig_pass=bool(cal["status"]=="PASS" and unit["all_pass"] and ratio>=float(cfg["calibration"]["thresholds"]["minimum_matched_random_ratio"]) and reduction>=float(cfg["gates"]["b_contamination_reduction_fraction"]))
|
||||
temporal=elig_pass;credit=bool(temporal and iso["pass"] and whole.get("pass",False) and iso["random_US_fails_to_mimic"])
|
||||
candidates={"MaleCNS-TemporalEligibility-v3-candidate":temporal,"MaleCNS-CreditAssignment-v1-candidate":credit}
|
||||
frozen=phase4p_finalize.integrity();runtime_check=verify_checksum(Path(cfg["paths"]["deterministic_candidate"]),Path(cfg["paths"]["deterministic_candidate"])/"checksums.sha256")
|
||||
audit={"phase":"4Q","candidates":candidates,"calibration":cal,"unit_tests":unit,"isolated":iso,"whole_brain":whole,
|
||||
"matched_random_credit_ratio":ratio,"endpoints":{r:{g:endpoint(r,g) for g in ("A-specific","shared","B-specific","inactive")} for r in ("v2-matched","v3-matched","v3-random")},
|
||||
"B_contamination_reduction_fraction":reduction,"B_contamination_rows":{r:int((cont.rule==r).sum()) for r in cont.rule.unique()},
|
||||
"frozen_integrity":frozen,"deterministic_candidate_checksum":runtime_check,"formal_associative_conditioning_run":False,"phase4r_started":False}
|
||||
save(exp/"audit/final-summary.json",audit)
|
||||
shared=gm[(gm.group=="shared")&(gm.pairing==100)].groupby("rule",as_index=False).agg(
|
||||
updated_edges_mean=("updated_edges","mean"),mean_delta_E=("mean_delta_E","mean"),
|
||||
total_abs_delta_E=("total_abs_delta_E","mean"),total_weighted_delta_mv=("total_weighted_delta_mv","mean"))
|
||||
shared.to_parquet(exp/"shared-edge/endpoint-audit.parquet",index=False)
|
||||
save(exp/"random-us/summary.json",{"matched_random_credit_ratio":ratio,"random_US_mimics_isolated":not iso["random_US_fails_to_mimic"],
|
||||
"random_US_mimics_whole_brain":bool(whole.get("whole_brain_run") and not whole.get("pass",False))})
|
||||
save(exp/"distant-us/summary.json",{"far_before_credit":0.0,"far_after_credit":0.0,"definition":"outside the frozen hard eligibility window"})
|
||||
if temporal:
|
||||
shutil.copy2(model/"eligibility-v3-definition.json",model/"MaleCNS-TemporalEligibility-v3-candidate.json")
|
||||
if credit:save(model/"MaleCNS-CreditAssignment-v1-candidate.json",{"status":"PASS","source":str(exp/"audit/final-summary.json")})
|
||||
for name in ("phase4q.py","phase4q.yaml"): shutil.copy2(model/name,model/(name+".frozen"))
|
||||
save(exp/"audit/integrity-scope.json",{"scope":"Phase 1-4P plus all Phase 4Q outputs","phase1_4p":frozen,
|
||||
"deterministic_runtime_v2_candidate":runtime_check,"phase4q_controls":[str(model/"checksums.sha256"),str(exp/"manifests/checksums.sha256")]})
|
||||
mc=checksum_tree(model,model/"checksums.sha256");ec=checksum_tree(exp,exp/"manifests/checksums.sha256")
|
||||
own={"model":verify_checksum(model,model/"checksums.sha256")["passed"],"experiment":verify_checksum(exp,exp/"manifests/checksums.sha256")["passed"]}
|
||||
print(json.dumps({"candidates":candidates,"matched_random_ratio":ratio,"B_reduction":reduction,"whole":whole["status"],"frozen":frozen["all_static_checks_passed"],"own":own,"files":[mc,ec]},indent=2),flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument("--config",type=Path,required=True);ap.add_argument("task",choices=["calibrate","formal","isolated","whole","finalize"]);a=ap.parse_args()
|
||||
{"calibrate":task_calibrate,"formal":task_formal,"isolated":task_isolated,"whole":task_whole,"finalize":task_finalize}[a.task](a.config)
|
||||
|
||||
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,62 @@
|
||||
phase: 4Q
|
||||
name: Local-Credit-Assignment-v3
|
||||
paths:
|
||||
deterministic_runtime: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2
|
||||
deterministic_candidate: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2/candidate
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4q-credit-v3
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif/credit-v3
|
||||
phase4l_config: /data/digital-drosophila/models/male-cns-lif/olfactory-grounding-v1/phase4l.yaml
|
||||
phase4m_config: /data/digital-drosophila/models/male-cns-lif/kcmbon-efficacy-v1/phase4m.yaml
|
||||
edge_identity: /data/digital-drosophila/experiments/phase4n-credit-assignment/edge-identity/frozen-edge-identity.parquet
|
||||
credit_v3:
|
||||
definition: terminal-local causal exponential trace with a hard finite window and external matched-compartment US gate
|
||||
windows_ms: [5, 10, 20, 50, 100, 250]
|
||||
tau_rule: tau_ms = window_ms / 3
|
||||
candidate_tau_ms: [1.6666666666666667, 3.3333333333333335, 6.666666666666667, 16.666666666666668, 33.333333333333336, 83.33333333333333]
|
||||
future_credit: zero
|
||||
same_timestamp_order: KC spike updates terminal trace before external US samples it
|
||||
endogenous_dan_is_teaching_gate: false
|
||||
calibration:
|
||||
odor_split: calibration
|
||||
labels: [CAL_A, CAL_B]
|
||||
seeds: [52101, 52102, 52103, 52104, 52105]
|
||||
matched_us_ms: 90.0
|
||||
duration_ms: 120.0
|
||||
random_us_seed_base: 930000
|
||||
thresholds:
|
||||
minimum_nonzero_seeds: 5
|
||||
minimum_matched_random_ratio: 3.0
|
||||
maximum_distant_fraction: 0.01
|
||||
maximum_random_fraction: 0.3333333333333333
|
||||
selection: shortest qualifying window; ties resolved by smaller tau; formal odors/results forbidden
|
||||
formal:
|
||||
odor_A: 4-ethylphenol
|
||||
odor_B: 3-hexanone
|
||||
input_mode: physiological
|
||||
matched_us_ms: 90.0
|
||||
duration_ms: 120.0
|
||||
train_seeds: [51201, 51202, 51203, 51204, 51205]
|
||||
evaluation_seeds: [51301, 51302, 51303, 51304, 51305]
|
||||
checkpoints: [0, 1, 2, 5, 10, 20, 30, 40, 50, 60, 75, 100]
|
||||
isolated_checkpoints: [10, 20, 30, 40, 50, 75, 100]
|
||||
target_compartment: "y5B'2a"
|
||||
unrelated_compartment: "y2a'1"
|
||||
matched_dan_type: PAM15
|
||||
random_us_seed_base: 940000
|
||||
frozen_update:
|
||||
eta_over_wsyn: 0.01
|
||||
E_lower_bound: 0.75
|
||||
plastic_edges: 14232
|
||||
groups: {A-specific: 105, shared: 369, B-specific: 221, inactive: 13537}
|
||||
gates:
|
||||
isolated_majority_seeds: 3
|
||||
b_contamination_reduction_fraction: 0.25
|
||||
random_credit_max_fraction_of_matched: 0.3333333333333333
|
||||
wholebrain_majority_seeds: 3
|
||||
prohibitions:
|
||||
change_runtime: true
|
||||
change_eta: true
|
||||
change_lower_bound: true
|
||||
expand_mask: true
|
||||
formal_associative_conditioning: true
|
||||
phase4r: true
|
||||
@@ -0,0 +1,62 @@
|
||||
phase: 4Q
|
||||
name: Local-Credit-Assignment-v3
|
||||
paths:
|
||||
deterministic_runtime: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2
|
||||
deterministic_candidate: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2/candidate
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4q-credit-v3
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif/credit-v3
|
||||
phase4l_config: /data/digital-drosophila/models/male-cns-lif/olfactory-grounding-v1/phase4l.yaml
|
||||
phase4m_config: /data/digital-drosophila/models/male-cns-lif/kcmbon-efficacy-v1/phase4m.yaml
|
||||
edge_identity: /data/digital-drosophila/experiments/phase4n-credit-assignment/edge-identity/frozen-edge-identity.parquet
|
||||
credit_v3:
|
||||
definition: terminal-local causal exponential trace with a hard finite window and external matched-compartment US gate
|
||||
windows_ms: [5, 10, 20, 50, 100, 250]
|
||||
tau_rule: tau_ms = window_ms / 3
|
||||
candidate_tau_ms: [1.6666666666666667, 3.3333333333333335, 6.666666666666667, 16.666666666666668, 33.333333333333336, 83.33333333333333]
|
||||
future_credit: zero
|
||||
same_timestamp_order: KC spike updates terminal trace before external US samples it
|
||||
endogenous_dan_is_teaching_gate: false
|
||||
calibration:
|
||||
odor_split: calibration
|
||||
labels: [CAL_A, CAL_B]
|
||||
seeds: [52101, 52102, 52103, 52104, 52105]
|
||||
matched_us_ms: 90.0
|
||||
duration_ms: 120.0
|
||||
random_us_seed_base: 930000
|
||||
thresholds:
|
||||
minimum_nonzero_seeds: 5
|
||||
minimum_matched_random_ratio: 3.0
|
||||
maximum_distant_fraction: 0.01
|
||||
maximum_random_fraction: 0.3333333333333333
|
||||
selection: shortest qualifying window; ties resolved by smaller tau; formal odors/results forbidden
|
||||
formal:
|
||||
odor_A: 4-ethylphenol
|
||||
odor_B: 3-hexanone
|
||||
input_mode: physiological
|
||||
matched_us_ms: 90.0
|
||||
duration_ms: 120.0
|
||||
train_seeds: [51201, 51202, 51203, 51204, 51205]
|
||||
evaluation_seeds: [51301, 51302, 51303, 51304, 51305]
|
||||
checkpoints: [0, 1, 2, 5, 10, 20, 30, 40, 50, 60, 75, 100]
|
||||
isolated_checkpoints: [10, 20, 30, 40, 50, 75, 100]
|
||||
target_compartment: "y5B'2a"
|
||||
unrelated_compartment: "y2a'1"
|
||||
matched_dan_type: PAM15
|
||||
random_us_seed_base: 940000
|
||||
frozen_update:
|
||||
eta_over_wsyn: 0.01
|
||||
E_lower_bound: 0.75
|
||||
plastic_edges: 14232
|
||||
groups: {A-specific: 105, shared: 369, B-specific: 221, inactive: 13537}
|
||||
gates:
|
||||
isolated_majority_seeds: 3
|
||||
b_contamination_reduction_fraction: 0.25
|
||||
random_credit_max_fraction_of_matched: 0.3333333333333333
|
||||
wholebrain_majority_seeds: 3
|
||||
prohibitions:
|
||||
change_runtime: true
|
||||
change_eta: true
|
||||
change_lower_bound: true
|
||||
expand_mask: true
|
||||
formal_associative_conditioning: true
|
||||
phase4r: true
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seal and independently verify the completed Phase 4Q artifact trees."""
|
||||
from pathlib import Path
|
||||
|
||||
from phase4h_audit import verify_checksum
|
||||
from phase4q import checksum_tree
|
||||
|
||||
|
||||
model = Path("/data/digital-drosophila/models/male-cns-lif/credit-v3")
|
||||
experiment = Path("/data/digital-drosophila/experiments/phase4q-credit-v3")
|
||||
model_count = checksum_tree(model, model / "checksums.sha256")
|
||||
experiment_count = checksum_tree(experiment, experiment / "manifests/checksums.sha256")
|
||||
print({
|
||||
"model_files": model_count,
|
||||
"experiment_files": experiment_count,
|
||||
"model": verify_checksum(model, model / "checksums.sha256"),
|
||||
"experiment": verify_checksum(experiment, experiment / "manifests/checksums.sha256"),
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
771016bac7aa91a376d69f33fa065cd080c9d488258d666eaef119a5a59f60cb phase4v-literature.md
|
||||
9007a0b7b01760d05a0d29d01e8551452d8029dee507992f4f06b1aa74885e53 phase4v.py
|
||||
f35a45efb55f7702eb85adedd4d6ac284735c39470f0cb8b515f501927a7539e phase4v.yaml
|
||||
7151ae11391a3e6e9dd011cc7667cfc7dcccffc7e0943fd87d4603b1405f2a99 phase4v_analyze.py
|
||||
d8de7192d56bc719efbfb6a6ea5048eb7e36a4de64e690db41fac7e4e7bddde7 manifest.json
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"bytes": 2761,
|
||||
"path": "phase4v-literature.md",
|
||||
"sha256": "771016bac7aa91a376d69f33fa065cd080c9d488258d666eaef119a5a59f60cb"
|
||||
},
|
||||
{
|
||||
"bytes": 16601,
|
||||
"path": "phase4v.py",
|
||||
"sha256": "9007a0b7b01760d05a0d29d01e8551452d8029dee507992f4f06b1aa74885e53"
|
||||
},
|
||||
{
|
||||
"bytes": 1727,
|
||||
"path": "phase4v.yaml",
|
||||
"sha256": "f35a45efb55f7702eb85adedd4d6ac284735c39470f0cb8b515f501927a7539e"
|
||||
},
|
||||
{
|
||||
"bytes": 6801,
|
||||
"path": "phase4v_analyze.py",
|
||||
"sha256": "7151ae11391a3e6e9dd011cc7667cfc7dcccffc7e0943fd87d4603b1405f2a99"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# CS/US 语义与时间尺度依据
|
||||
|
||||
核对:2026-09-29。延续 Phase 4U 的 Google Research / Google DeepMind 研究边界:连接组、外部 RL 身体控制与脑内关联学习分别验收。本轮不引入 RL。
|
||||
|
||||
## 原始实验,不直接移植到模型
|
||||
|
||||
- [Handler et al., Cell 2019](https://pmc.ncbi.nlm.nih.gov/articles/PMC9012144/),doi:10.1016/j.cell.2019.05.040:公开方法描述的一套闭环实验用 10 s ACV 气味,最后 1 s 进行 PAM 光刺激;反向配对先进行 1 s DAN 刺激,随后隔 1 s 呈现 10 s 气味。训练结束到测试开始为 40 s。这是特定制备/通路的实验,不是所有 compartment 的统一参数。
|
||||
- [Dylla et al., Frontiers in Neural Circuits 2017](https://www.frontiersin.org/journals/neural-circuits/articles/10.3389/fncir.2017.00042/full),doi:10.3389/fncir.2017.00042:一套 trace-conditioning 实验采用 10 s CS,CS/US 起点间隔 15 s(配对)或 90 s(非配对),US 为四次 1.5 s 电刺激;试次间隔 210 s。刺激持续时间、起点间隔、刺激之间的空隙与试次间隔是不同变量。
|
||||
|
||||
以上为简短方法归纳,不转载正文。不将电压、光照强度、DAN spike count 与 dopamine concentration 等同;不把秒级实验任意压缩成毫秒并称为生理复现。
|
||||
|
||||
## 当前模型的差别
|
||||
|
||||
1. CS:DoOR 静态归一化响应映射为 PN Bernoulli 放电请求,整个 120 ms 试次保持同一 rate,没有气味输送/受体时序模型。
|
||||
2. 初态:每试次 V=rest,g/ref/delay queue=0,APL state=0;没有经过真实试次间隔的连续状态。
|
||||
3. US:指定 DAN 一步放电请求。refractory 或当步已有 spike 时请求不会额外产生 spike。
|
||||
4. 教学 gate:由外部计划时刻取 KC trace;未以实际 DAN 接受数、dopamine release、receptor kinetics 或 second messenger 定义。
|
||||
5. Gate 和神经传输区分:KC/DAN spikes 的网络效应保留 18 步延迟,外部教学 gate 使用 emission 时刻。两者共同平移不改变相对间隔,但并不构成生化模型。
|
||||
|
||||
## 本轮检验
|
||||
|
||||
- 先复现短试次,确认仅添加观测没有改变动力学。
|
||||
- 观察 1 s 持续同率输入,以及 120 ms 输入后关闭的对照。不把增加持续时间当作改善 learning 的调参。
|
||||
- 使用完全相同前 120 ms 输入,防止不同随机矩阵形状造成前缀改变。
|
||||
- 100 ms 无输入预热是否仅平移响应?若初态是吸收静息态,空等不等于生理稳态预热。
|
||||
- 分开记录 scheduled US、实际新增 DAN spikes、已有自然 spikes、refractory 拒绝、外部 teaching gate。
|
||||
- 无权重更新、无 US 扫描、无 APL ablation、无新学习候选。失败边界只能约束当前模型,不能替真实果蝇作不可能性结论。
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Read-only CS/US semantics diagnostics; no learning or parameter selection."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import yaml
|
||||
import phase4p
|
||||
import phase4p_finalize
|
||||
import phase4s
|
||||
import phase4u
|
||||
from phase4p_revalidate import DeterministicAPLSim, door_inputs, make_drive
|
||||
from phase4r import save, digest, arrhash
|
||||
from phase4h_audit import verify_checksum
|
||||
|
||||
|
||||
def checks(cfg):
|
||||
old = yaml.safe_load((Path(cfg['prior_model'])/'phase4u.yaml').read_text())
|
||||
out = phase4u.checks(old)
|
||||
for key in ('prior_root', 'prior_model'):
|
||||
root = Path(cfg[key]); out['U_'+key] = verify_checksum(root, root/'checksums.sha256')
|
||||
assert all(x['passed'] for x in out.values())
|
||||
return out
|
||||
|
||||
|
||||
def forcing(sim, targets, rates, seed, cfg, mode):
|
||||
block = round(cfg['forcing_block_ms']/sim.dt)
|
||||
steps = round((cfg['observation_duration_ms'] if mode in ('sustained','offset') else cfg['short_duration_ms'])/sim.dt)
|
||||
probs = torch.as_tensor(np.asarray(rates, np.float32)*sim.dt/1000., device=sim.device)
|
||||
parts = []
|
||||
for b in range(math.ceil(steps/block)):
|
||||
gen = torch.Generator(device=sim.device); gen.manual_seed(int(seed+b*cfg['forcing_block_seed_stride']))
|
||||
parts.append((torch.rand((block,len(targets)),generator=gen,device=sim.device)<probs[None,:]).to(torch.uint8))
|
||||
out = torch.cat(parts)[:steps]
|
||||
if mode == 'offset': out[block:] = 0
|
||||
if mode == 'warmup':
|
||||
out = torch.cat((torch.zeros((round(cfg['warmup_ms']/sim.dt),len(targets)),dtype=torch.uint8,device=sim.device),out))
|
||||
return out
|
||||
|
||||
|
||||
def observed(sim, targets, forced, target_kc, cfg, dan=None, pulse=None):
|
||||
"""Statement ordering and all state mutations mirror frozen DeterministicAPLSim.run."""
|
||||
rt=sim.rt; sim.install(None); rt.reset(); steps=len(forced)
|
||||
tg=torch.as_tensor(targets,dtype=torch.int64,device=sim.device)
|
||||
dan=np.empty(0,np.int32) if dan is None else dan
|
||||
dg=torch.as_tensor(dan,dtype=torch.int64,device=sim.device)
|
||||
df=torch.zeros((steps,len(dan)),dtype=torch.uint8,device=sim.device)
|
||||
ds=set() if pulse is None else {round(pulse/sim.dt)}
|
||||
for s in ds: df[s]=1
|
||||
apl=torch.zeros(len(sim.apl),dtype=torch.float32,device=sim.device)
|
||||
trace=torch.zeros(len(sim.kc),dtype=torch.float32,device=sim.device)
|
||||
first=torch.full((sim.n,),-1,dtype=torch.int32,device=sim.device)
|
||||
ad=np.float32(math.exp(-sim.dt/20.)); td=np.float32(math.exp(-sim.dt/100.))
|
||||
trace_at=[]; evs=[]; evn=[]; samples=[]; us=[]; prefix=None
|
||||
stride=round(cfg['sampling_ms']/sim.dt); short=round(cfg['short_duration_ms']/sim.dt)
|
||||
requests=0; accepted=0; existing=0; blocked=0
|
||||
target_gpu=torch.as_tensor(target_kc,dtype=torch.int64,device=sim.device)
|
||||
torch.cuda.synchronize(); start=time.perf_counter()
|
||||
for step in range(steps):
|
||||
rt.ext.state_step(rt.v,rt.g,rt.ref,rt.queue[step%rt.qlen],rt.spike_u8,rt.counts,
|
||||
float(rt.gd),float(rt.alpha),float(rt.rest),float(rt.resetv),float(rt.threshold),rt.ref_steps)
|
||||
pre=rt.spike_u8[tg].clone(); pre_ref=rt.ref[tg].clone()
|
||||
rt.ext.force_step(rt.v,rt.ref,rt.spike_u8,rt.counts,tg,forced[step],float(rt.resetv),rt.ref_steps)
|
||||
req=forced[step].bool(); requests+=int(req.sum())
|
||||
accepted+=int((req & ~pre.bool() & (pre_ref==0)).sum())
|
||||
existing+=int((req & pre.bool()).sum()); blocked+=int((req & ~pre.bool() & (pre_ref!=0)).sum())
|
||||
if step in ds: before=rt.spike_u8[dg].clone(); refs=rt.ref[dg].clone()
|
||||
if len(dan): rt.ext.force_step(rt.v,rt.ref,rt.spike_u8,rt.counts,dg,df[step],float(rt.resetv),rt.ref_steps)
|
||||
if step in ds:
|
||||
after=rt.spike_u8[dg]
|
||||
for j,node in enumerate(dan):
|
||||
us.append({'step':step,'time_ms':step*sim.dt,'neuron_idx':int(node),'preexisting_spike':bool(before[j]),
|
||||
'pre_force_refractory':int(refs[j]),'accepted_new_spike':bool(after[j] and not before[j]),
|
||||
'actual_output_spike':bool(after[j]),'teaching_gate_sampled':True,'first_edge_arrival_ms':(step+rt.delay_steps)*sim.dt})
|
||||
sources=torch.nonzero(rt.spike_u8,as_tuple=False).flatten()
|
||||
if sources.numel():
|
||||
missing=first[sources]<0; first[sources[missing]]=step
|
||||
rt.canonical_fanout(sources,rt.queue[(step+rt.delay_steps)%rt.qlen])
|
||||
kc_spike=rt.spike_u8[sim.kc_gpu]
|
||||
trace.mul_(float(td)).add_(kc_spike.to(torch.float32)).clamp_max_(1.)
|
||||
if step in ds: trace_at.append(trace.clone())
|
||||
active=torch.nonzero(kc_spike,as_tuple=False).flatten()
|
||||
if active.numel():
|
||||
node=sim.kc_gpu[active].cpu().numpy().astype(np.int32)
|
||||
evn.append(node); evs.append(np.full(len(node),step,np.int32))
|
||||
apl.mul_(float(ad)).add_(torch.mv(sim.input_gpu,rt.spike_u8.to(torch.float32)))
|
||||
inhibition=torch.matmul(apl,sim.output_gpu)*np.float32(.01)
|
||||
rt.g[sim.kc_gpu]=rt.g[sim.kc_gpu]-inhibition
|
||||
rt.step_index+=1
|
||||
if step==short-1:
|
||||
prefix={k:x.cpu().numpy().copy() for k,x in [('counts',rt.counts),('final_v',rt.v),('final_g',rt.g)]}
|
||||
if (step+1)%stride==0:
|
||||
samples.append({'step':step,'time_ms':(step+1)*sim.dt,'PN_requested_cumulative':requests,
|
||||
'PN_accepted_cumulative':accepted,'PN_preexisting_cumulative':existing,'PN_blocked_cumulative':blocked,
|
||||
'KC_spikes_cumulative':int(rt.counts[sim.kc_gpu].sum()),'target_KC_spikes_cumulative':int(rt.counts[target_gpu].sum()),
|
||||
'KC_active_cumulative':int((rt.counts[sim.kc_gpu]>0).sum()),'KC_g_mean':float(rt.g[sim.kc_gpu].mean()),
|
||||
'KC_V_mean':float(rt.v[sim.kc_gpu].mean()),'KC_threshold_margin_min':float((rt.threshold-rt.v[sim.kc_gpu]).min()),
|
||||
'APL_state_0':float(apl[0]),'APL_state_1':float(apl[1]),'APL_inhibition_mean':float(inhibition.mean()),
|
||||
'finite':bool(torch.isfinite(rt.v).all() and torch.isfinite(rt.g).all())})
|
||||
torch.cuda.synchronize()
|
||||
counts=rt.counts.cpu().numpy().copy()
|
||||
return {'counts':counts,'kc_counts':counts[sim.kc], 'first':first.cpu().numpy().copy(),
|
||||
'final_v':rt.v.cpu().numpy().copy(),'final_g':rt.g.cpu().numpy().copy(),
|
||||
'trace':np.zeros(len(sim.kc),np.float32) if not trace_at else trace_at[-1].cpu().numpy().copy(),
|
||||
'kc_event_step':np.concatenate(evs) if evs else np.empty(0,np.int32),
|
||||
'kc_event_node':np.concatenate(evn) if evn else np.empty(0,np.int32),
|
||||
'prefix':prefix,'samples':samples,'US':us,'wall_seconds':time.perf_counter()-start,
|
||||
'finite':all(x['finite'] for x in samples)}
|
||||
|
||||
|
||||
def exact(a,b,keys):
|
||||
return {k:bool(np.array_equal(a[k],b[k])) for k in keys}
|
||||
|
||||
|
||||
def run(cfg,exp,model):
|
||||
for name in ('protocol','audit','events','states','observations','regression','performance'): (exp/name).mkdir(parents=True,exist_ok=True)
|
||||
assert not (exp/'protocol/preregistered.json').exists(),'Refusing overwrite'
|
||||
save(exp/'audit/preflight.json',checks(cfg))
|
||||
oc,panel,vectors,selection,mapping=door_inputs(); panel=panel[panel.split=='calibration'].reset_index(drop=True)
|
||||
assert len(panel)==6
|
||||
save(exp/'protocol/preregistered.json',{'config':cfg,'panel':panel.to_dict('records'),
|
||||
'sources':{p.name:digest(p) for p in model.iterdir() if p.is_file()},'before_simulation':True,'no_parameter_selection':True})
|
||||
pc=yaml.safe_load((Path(cfg['runtime_root'])/'phase4p.yaml').read_text())
|
||||
rt=phase4p.CanonicalRuntime(pc,Path(cfg['runtime_root']),exp); sim=DeterministicAPLSim(rt)
|
||||
assert len(sim.identity)==14232
|
||||
target=np.unique(sim.identity.loc[sim.identity.compartment.eq(cfg['target_compartment']),'source_neuron_idx'].to_numpy(np.int32))
|
||||
dan=sim.dan_targets(cfg['matched_DAN'],cfg['target_compartment'])
|
||||
save(exp/'audit/semantics.json',{'CS':'frozen physiological DoOR-to-PN rates; Bernoulli force request per 0.1ms step; refractory may refuse',
|
||||
'US':'one scheduled force request per confirmed PAM15 neuron; engineering teaching trace sampled at scheduled emission step regardless acceptance',
|
||||
'dopamine_concentration_measured':False,'initial_state':'all V at rest; g/ref/queue/APL/trace zero',
|
||||
'delay_steps':rt.delay_steps,'delay_ms':rt.delay_steps*sim.dt,'DAN_ids':dan.tolist(),'target_KC_ids':target.tolist(),
|
||||
'long_input':'120ms RNG blocks with explicit seed stride; first block identical, later sequence not claimed to equal legacy long run',
|
||||
'time_scaling_to_biological_seconds':'unknown; 1000ms is observation, not conditioning replication'})
|
||||
keys=['counts','first','final_v','final_g','trace','kc_event_step','kc_event_node']; regress=[]; summaries=[]; usrows=[]
|
||||
start=time.perf_counter(); torch.cuda.reset_peak_memory_stats()
|
||||
def persist(name,r,oi,seed,mode):
|
||||
assert r['finite'],'Nonfinite state'
|
||||
phase4s.persist_run(exp,name,r)
|
||||
np.savez_compressed(exp/'states'/f'{name}.npz',**{k:r[k] for k in ('counts','final_v','final_g','trace')})
|
||||
pd.DataFrame(r['samples']).to_parquet(exp/'observations'/f'{name}.parquet',index=False)
|
||||
steps=r['kc_event_step']; nodes=r['kc_event_node']; t=steps[np.isin(nodes,target)]*sim.dt
|
||||
row={'odor_index':oi,'odor_name':panel.iloc[oi].odor_name,'seed':seed,'condition':mode,
|
||||
'KC_active':int(np.count_nonzero(r['kc_counts'])),'KC_events':len(steps),'target_events':len(t),
|
||||
'first_target_ms':float(t.min()) if len(t) else None,'median_target_ms':float(np.median(t)) if len(t) else None,
|
||||
'target_events_after120ms':int(np.count_nonzero(t>=120)), 'KC_events_after120ms':int(np.count_nonzero(steps*sim.dt>=120)),
|
||||
'last_KC_event_ms':float(steps.max()*sim.dt) if len(steps) else None,'wall_seconds':r['wall_seconds'],'finite':r['finite']}
|
||||
summaries.append(row)
|
||||
for oi,odor in panel.iterrows():
|
||||
targets,rates=make_drive(oc,panel,vectors,selection,mapping,odor.odor_id,'physiological')
|
||||
for si,seed in enumerate(cfg['seeds']):
|
||||
actual=cfg['seed_namespace']+oi*cfg['odor_seed_offset']+seed
|
||||
legacy=sim.run(targets,rates,actual,cfg['short_duration_ms'],collect_events=True)
|
||||
f=forcing(sim,targets,rates,actual,cfg,'short')
|
||||
short=observed(sim,targets,f,target,cfg)
|
||||
eq=exact(legacy,short,keys); regress.append({'odor':oi,'seed':seed,'test':'short-exact','fields':eq})
|
||||
save(exp/'regression/instrumentation.json',regress); assert all(eq.values()),'Instrumentation diverges'
|
||||
persist(f'{oi}-{seed}-short',short,oi,seed,'short')
|
||||
for mode in ('sustained','offset'):
|
||||
ff=forcing(sim,targets,rates,actual,cfg,mode); assert torch.equal(ff[:len(f)],f)
|
||||
r=observed(sim,targets,ff,target,cfg)
|
||||
eq=exact(legacy,r['prefix'],['counts','final_v','final_g'])
|
||||
mask=r['kc_event_step']<len(f)
|
||||
eq['events']=arrhash(r['kc_event_step'][mask],r['kc_event_node'][mask])==arrhash(legacy['kc_event_step'],legacy['kc_event_node'])
|
||||
regress.append({'odor':oi,'seed':seed,'test':mode+'-prefix','fields':eq}); assert all(eq.values())
|
||||
persist(f'{oi}-{seed}-{mode}',r,oi,seed,mode)
|
||||
if si==0:
|
||||
warm=observed(sim,targets,forcing(sim,targets,rates,actual,cfg,'warmup'),target,cfg)
|
||||
eq=exact(legacy,warm,['counts','final_v','final_g','trace','kc_event_node'])
|
||||
eq['shifted_events']=np.array_equal(warm['kc_event_step']-round(cfg['warmup_ms']/sim.dt),legacy['kc_event_step'])
|
||||
regress.append({'odor':oi,'seed':seed,'test':'quiet-warmup','fields':eq})
|
||||
persist(f'{oi}-{seed}-warmup',warm,oi,seed,'warmup')
|
||||
for pulse in cfg['US_audit_ms']:
|
||||
lr=sim.run(targets,rates,actual,cfg['short_duration_ms'],dan_targets=dan,dan_pulses=[pulse],collect_events=True)
|
||||
r=observed(sim,targets,f,target,cfg,dan,pulse); eq=exact(lr,r,keys)
|
||||
regress.append({'odor':oi,'seed':seed,'test':f'US{pulse}-exact','fields':eq}); assert all(eq.values())
|
||||
usrows.extend([dict(x,odor_index=oi,seed=seed,US_ms=pulse) for x in r['US']])
|
||||
persist(f'{oi}-{seed}-US{pulse}',r,oi,seed,f'US{pulse}')
|
||||
save(exp/'regression/instrumentation.json',regress)
|
||||
print(json.dumps({'completed_odor':oi,'seed':seed,'trials':len(summaries)}),flush=True)
|
||||
pd.DataFrame(summaries).to_parquet(exp/'audit/trials.parquet',index=False)
|
||||
pd.DataFrame(usrows).to_parquet(exp/'audit/US-acceptance.parquet',index=False)
|
||||
reference=phase4s.evidence_regression(cfg,exp,rt,sim)
|
||||
result={'phase':'4V','status':'COMPLETE-DIAGNOSTIC-ONLY','reference_pass':reference,
|
||||
'exact_instrumentation_pass':all(all(x['fields'].values()) for x in regress),'diagnostic_trials':len(summaries),
|
||||
'weight_updates':0,'candidate_created':False,'reserved_test_odors_consumed':0,
|
||||
'DAN_requests':len(usrows),'DAN_accepted_new':sum(x['accepted_new_spike'] for x in usrows),
|
||||
'DAN_preexisting':sum(x['preexisting_spike'] for x in usrows),
|
||||
'DAN_refused':sum(not x['actual_output_spike'] for x in usrows)}
|
||||
assert result['exact_instrumentation_pass'] and reference
|
||||
save(exp/'audit/results.json',result)
|
||||
save(exp/'performance/summary.json',{'wall_seconds_excluding_build':time.perf_counter()-start,
|
||||
'peak_VRAM_bytes':torch.cuda.max_memory_allocated(),'GPU':torch.cuda.get_device_name(0),
|
||||
'measurement':'instrumented diagnostics include synchronous observation overhead; not runtime speed benchmark'})
|
||||
print(json.dumps(result),flush=True)
|
||||
|
||||
|
||||
def seal(cfg,exp,model):
|
||||
now=checks(cfg); history=phase4p_finalize.integrity(); pre=json.loads((exp/'protocol/preregistered.json').read_text())
|
||||
passed=history['all_static_checks_passed'] and now==json.loads((exp/'audit/preflight.json').read_text()) and all(digest(model/n)==h for n,h in pre['sources'].items())
|
||||
save(exp/'audit/frozen-integrity.json',{'pass':passed,'history':history,'current':now}); assert passed
|
||||
r=json.loads((exp/'audit/results.json').read_text()); r['integrity_pass']=True; save(exp/'audit/results.json',r)
|
||||
df=pd.read_parquet(exp/'audit/trials.parquet'); lines=['# Phase 4V — CS/US semantics and timescale audit','',
|
||||
'Status: COMPLETE-DIAGNOSTIC-ONLY. No training, selection, new timing policy or learned candidate.',
|
||||
'Frozen instrumentation and five reference trials: exact PASS. Frozen checksums: PASS.',
|
||||
'Only six old calibration odors were used; six reserved test odors remain unconsumed.','',
|
||||
'| Odor | Condition | mean KC events | mean target events | mean target events >=120ms |',
|
||||
'|---|---|---:|---:|---:|']
|
||||
for (odor,mode),g in df.groupby(['odor_name','condition'],sort=False):
|
||||
lines.append(f"| {odor} | {mode} | {g.KC_events.mean():.2f} | {g.target_events.mean():.2f} | {g.target_events_after120ms.mean():.2f} |")
|
||||
lines += ['',f"DAN requests: {r['DAN_requests']}; accepted new spikes: {r['DAN_accepted_new']}; preexisting: {r['DAN_preexisting']}; refused: {r['DAN_refused']}.",
|
||||
'US is a force request, not measured dopamine. Teaching trace is sampled at the scheduled emission step; synaptic arrivals are 1.8ms later.',
|
||||
'Observation does not establish biological time scaling or associative memory. Literature boundaries are in phase4v-literature.md.',
|
||||
'No game, decoder, RL, visual input, gain/threshold changes, eligibility changes, or plastic-mask changes.']
|
||||
(exp/'REPORT.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
|
||||
for root in (model,exp):
|
||||
rows=[{'path':p.relative_to(root).as_posix(),'sha256':digest(p),'bytes':p.stat().st_size} for p in sorted(root.rglob('*')) if p.is_file() and p.name not in ('manifest.json','checksums.sha256')]
|
||||
save(root/'manifest.json',{'files':rows}); rows.append({'path':'manifest.json','sha256':digest(root/'manifest.json')})
|
||||
(root/'checksums.sha256').write_text(''.join(x['sha256']+' '+x['path']+'\n' for x in rows))
|
||||
check=verify_checksum(root,root/'checksums.sha256'); assert check['passed']; print(json.dumps(check),flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser(); p.add_argument('task',choices=['run','seal']); p.add_argument('--config',required=True); a=p.parse_args()
|
||||
cfg=yaml.safe_load(Path(a.config).read_text()); (run if a.task=='run' else seal)(cfg,Path(cfg['experiment_root']),Path(cfg['model_root']))
|
||||
@@ -0,0 +1,40 @@
|
||||
phase: 4V
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4v-csus-semantics
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif/csus-audit-v1
|
||||
runtime_root: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2
|
||||
phase4p_root: /data/digital-drosophila/experiments/phase4p-deterministic-runtime
|
||||
prior_root: /data/digital-drosophila/experiments/phase4u-odor-generalization
|
||||
prior_model: /data/digital-drosophila/models/male-cns-lif/odor-timing-generalization-v1
|
||||
target_compartment: "y5B'2a"
|
||||
matched_DAN: PAM15
|
||||
odors: all six original calibration odors only; do not consume six reserved Phase4U test odors
|
||||
seeds: [131101,131102,131103]
|
||||
seed_namespace: 80000000
|
||||
odor_seed_offset: 1000000
|
||||
short_duration_ms: 120
|
||||
observation_duration_ms: 1000
|
||||
warmup_ms: 100
|
||||
US_audit_ms: [25,90]
|
||||
forcing_block_ms: 120
|
||||
forcing_block_seed_stride: 1000003
|
||||
sampling_ms: 1
|
||||
activity_bin_ms: 10
|
||||
conditions:
|
||||
- short frozen replay for every seed
|
||||
- sustained same-rate PN input to 1000ms
|
||||
- same first120ms then PN input off until1000ms
|
||||
- silent100ms warmup followed by same120ms input, first seed each odor
|
||||
- scheduled US25 or90ms force-acceptance audit, first seed each odor
|
||||
checks:
|
||||
mandatory_before_long_trials: short instrumented run vs frozen original counts/events/finalV/finalG/trace exact
|
||||
long_trial_prefix: first120ms inputs and complete neuron counts/V/g match original
|
||||
no_runtime_source_modification: true
|
||||
frozen_reference: five original deterministic trials
|
||||
scope:
|
||||
diagnostic_only: true
|
||||
learning: false
|
||||
parameter_selection: false
|
||||
new_timing_candidate: false
|
||||
new_US_policy: false
|
||||
unknown_biological_time_scaling: true
|
||||
long_input_is_observation_not_biological_conditioning_replication: true
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Descriptive audit only; no parameter ranking, fitting or candidate selection."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yaml
|
||||
from phase4r import save
|
||||
|
||||
cfg=yaml.safe_load(Path(__file__).with_name('phase4v.yaml').read_text())
|
||||
exp=Path(cfg['experiment_root']); prior=Path(cfg['prior_root'])
|
||||
assert not (exp/'checksums.sha256').exists(),'Already sealed'
|
||||
target=set(json.loads((exp/'audit/semantics.json').read_text())['target_KC_ids'])
|
||||
old=[]
|
||||
for p in sorted((prior/'events').glob('calibration-*.parquet')):
|
||||
x=pd.read_parquet(p); t=x.loc[x.neuron_idx.isin(target),'step'].to_numpy()*.1
|
||||
old.append({'trial':p.stem,'events':len(t),'first_ms':float(t.min()) if len(t) else None,
|
||||
'fraction_after25ms':float((t>25).mean()) if len(t) else None,
|
||||
'fraction_after60ms':float((t>60).mean()) if len(t) else None,
|
||||
'fraction_after90ms':float((t>90).mean()) if len(t) else None})
|
||||
assert len(old)==120
|
||||
pd.DataFrame(old).to_parquet(exp/'audit/prior120-event-distribution.parquet',index=False)
|
||||
trials=pd.read_parquet(exp/'audit/trials.parquet'); details=[]; bins=[]
|
||||
for _,r in trials.iterrows():
|
||||
name=f'{r.odor_index}-{r.seed}-{r.condition}'
|
||||
x=pd.read_parquet(exp/'events'/f'{name}.parquet')
|
||||
obs=pd.read_parquet(exp/'observations'/f'{name}.parquet')
|
||||
assert (obs.PN_requested_cumulative == obs.PN_accepted_cumulative+obs.PN_preexisting_cumulative+obs.PN_blocked_cumulative).all()
|
||||
if r.condition=='offset':
|
||||
assert obs.loc[obs.time_ms>=cfg['short_duration_ms'],'PN_requested_cumulative'].nunique()==1
|
||||
t=x.step.to_numpy()*.1; tt=x.loc[x.neuron_idx.isin(target),'step'].to_numpy()*.1
|
||||
for start in np.arange(0,float(obs.time_ms.max()),cfg['activity_bin_ms']):
|
||||
end=start+cfg['activity_bin_ms']; mask=(t>=start)&(t<end)
|
||||
bins.append({'odor_index':int(r.odor_index),'seed':int(r.seed),'condition':r.condition,'start_ms':float(start),
|
||||
'KC_events':int(mask.sum()),'KC_active':int(x.loc[mask,'neuron_idx'].nunique()),
|
||||
'target_events':int(((tt>=start)&(tt<end)).sum())})
|
||||
with np.load(exp/'states'/f'{name}.npz') as state:
|
||||
count=state['counts']; total=int(count.astype(np.int64).sum()); active=int(np.count_nonzero(count))
|
||||
row=r.to_dict(); row.update({'whole_brain_spikes':total,'whole_brain_active':active,
|
||||
'KC_events_120_200ms':int(((t>=120)&(t<200)).sum()),
|
||||
'KC_events_after200ms':int((t>=200).sum()),'KC_events_after500ms':int((t>=500).sum()),
|
||||
'PN_requests':int(obs.iloc[-1].PN_requested_cumulative),'PN_new_spikes':int(obs.iloc[-1].PN_accepted_cumulative),
|
||||
'PN_refractory_blocked':int(obs.iloc[-1].PN_blocked_cumulative),
|
||||
'late_KC_g_mean':float(obs[obs.time_ms>=max(obs.time_ms)-100].KC_g_mean.mean()),
|
||||
'late_APL_inhibition_mean':float(obs[obs.time_ms>=max(obs.time_ms)-100].APL_inhibition_mean.mean())})
|
||||
details.append(row)
|
||||
pd.DataFrame(bins).to_parquet(exp/'audit/activity-10ms-bins.parquet',index=False)
|
||||
df=pd.DataFrame(details); df.to_parquet(exp/'audit/detailed-trials.parquet',index=False)
|
||||
rows=[]
|
||||
for (odor,condition),g in df.groupby(['odor_name','condition'],sort=False):
|
||||
row={'odor':odor,'condition':condition,'n':len(g)}
|
||||
for k in ('KC_events','target_events','KC_events_after120ms','KC_events_after200ms','KC_events_after500ms','whole_brain_spikes','whole_brain_active','PN_requests','PN_new_spikes','PN_refractory_blocked','late_KC_g_mean','late_APL_inhibition_mean'):
|
||||
row[k+'_mean']=float(g[k].mean())
|
||||
rows.append(row)
|
||||
save(exp/'audit/descriptive-summary.json',{'groups':rows,'analysis':'post-run descriptive summaries, not hypothesis selection or tuning'})
|
||||
us=pd.read_parquet(exp/'audit/US-acceptance.parquet')
|
||||
us['scheduled_arrival_slot_ms']=us.first_edge_arrival_ms
|
||||
us['output_spike_arrival_ms']=us.first_edge_arrival_ms.where(us.actual_output_spike)
|
||||
us.to_parquet(exp/'audit/US-arrival-semantics.parquet',index=False)
|
||||
save(exp/'audit/field-semantics.json',{
|
||||
'PN_request_accounting_partition_exact':True,
|
||||
'offset_no_external_requests_after120ms':True,
|
||||
'US_original_first_edge_arrival_ms':'scheduled emission step + frozen delay; not evidence of arrival if force rejected and no actual spike',
|
||||
'US_output_spike_arrival_ms':'nominal edge delivery time only for actual output spikes; not dopamine release/concentration',
|
||||
'samples_time_ms':'state after step; (step+1)*dt',
|
||||
'events_time_ms':'runtime spike convention; step*dt',
|
||||
'analysis_source_role':'descriptive audit added after simulation preregistration; cannot select parameters or candidate'})
|
||||
late=[]
|
||||
for _,r in trials[trials.condition=='offset'].iterrows():
|
||||
e=pd.read_parquet(exp/'events'/f'{r.odor_index}-{r.seed}-offset.parquet')
|
||||
late.append((int(r.odor_index),set(map(int,e.loc[e.step>=5000,'neuron_idx']))))
|
||||
pairs={'within_odor':[],'between_odor':[]}
|
||||
for i,(oi,a) in enumerate(late):
|
||||
for oj,b in late[i+1:]:
|
||||
pairs['within_odor' if oi==oj else 'between_odor'].append(len(a&b)/len(a|b) if a|b else 1.)
|
||||
save(exp/'audit/offset-late-KC-overlap.json',{
|
||||
'scope':'posthoc descriptive >=500ms KC activity; no memory/learning gate',
|
||||
'trials':len(late),'late_active_KC_counts':[len(x[1]) for x in late],
|
||||
'common_KC_ids':sorted(set.intersection(*(x[1] for x in late))),
|
||||
'pairwise_Jaccard':{k:{'pairs':len(v),'mean':float(np.mean(v)),'min':float(min(v)),'max':float(max(v))} for k,v in pairs.items()}})
|
||||
lines=['# Mechanism observations (descriptive, no training)','',
|
||||
'| Odor | Condition | KC events | target events | KC events >=200ms | late mean KC g |',
|
||||
'|---|---|---:|---:|---:|---:|']
|
||||
for r in rows:
|
||||
if r['condition'] in ('short','sustained','offset'):
|
||||
lines.append(f"| {r['odor']} | {r['condition']} | {r['KC_events_mean']:.2f} | {r['target_events_mean']:.2f} | {r['KC_events_after200ms_mean']:.2f} | {r['late_KC_g_mean_mean']:.6f} |")
|
||||
lines += ['', 'The longer trials are diagnostics of the unchanged brain at unchanged input rates. Differences are not evidence of learning.',
|
||||
'Counter observations add synchronization overhead and cannot be used as runtime speed benchmarks.',
|
||||
'No baseline-activity input is added. A silent warmup is not a physiological ongoing-state initialization.',
|
||||
'Turning external PN forcing off does not silence PN neurons or recurrent network activity. Persistent activity is not itself retained associative memory.',
|
||||
'A scheduled teaching gate is an engineering signal, not measured dopamine release or receptor kinetics.',
|
||||
'No new US policy or seconds-to-milliseconds conversion is selected from these observations.']
|
||||
(exp/'audit/MECHANISM.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
|
||||
print(json.dumps({'old_trials':len(old),'diagnostic_trials':len(df),'groups':rows},indent=2))
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "MaleCNS-NumericalContract-v1.md",
|
||||
"sha256": "9505cce8e2f7e03338f7425d698b94e7798cf43e924e6f41c1155719b63204cd",
|
||||
"size": 1958
|
||||
},
|
||||
{
|
||||
"path": "VERSION.json",
|
||||
"sha256": "7f95c99c377944bd05987a3e0953f743ee278807f3c57145aa2b62e3627bc3aa",
|
||||
"size": 281
|
||||
},
|
||||
{
|
||||
"path": "phase4p.py",
|
||||
"sha256": "c8a13dbdd58dd222f73c1224de448e54bdf90c3f533163427a2bb25446a03999",
|
||||
"size": 10909
|
||||
},
|
||||
{
|
||||
"path": "phase4p.yaml",
|
||||
"sha256": "6d0189f4ec570cb3352d15ed8d195d1ccb32ee1d29c33dc1e4f00d377b74787e",
|
||||
"size": 2525
|
||||
},
|
||||
{
|
||||
"path": "phase4p_benchmark.py",
|
||||
"sha256": "8d8bad3db949da876c5a7fe9f20845a10c4128523da69bcb4b5c3fab69c15e7a",
|
||||
"size": 4284
|
||||
},
|
||||
{
|
||||
"path": "phase4p_ext.cpp",
|
||||
"sha256": "9d204d77d70b3723567671a61ce8e2e6162e261f48825e86fb34565e3faa622d",
|
||||
"size": 1435
|
||||
},
|
||||
{
|
||||
"path": "phase4p_ext.cu",
|
||||
"sha256": "c24985f4e366f90790d25ab5eceb9d6aff0f9064dd61c7413c13d2a2a4fa5fcd",
|
||||
"size": 5064
|
||||
},
|
||||
{
|
||||
"path": "phase4p_finalize.py",
|
||||
"sha256": "b627fb0934bca8f04ec7c595a7b5e6690a2af62a44bc00ff2972f9f764671cd4",
|
||||
"size": 6047
|
||||
},
|
||||
{
|
||||
"path": "phase4p_revalidate.py",
|
||||
"sha256": "c7e693a2da70acdce4f53af5c2853e52e8333acc93f88a46a5e8083eac639346",
|
||||
"size": 31558
|
||||
},
|
||||
{
|
||||
"path": "phase4p_validate.py",
|
||||
"sha256": "ce06aa5cb6439450440a91603af1c0d187b20dc11abda6528ea0dfc6b71569b5",
|
||||
"size": 30605
|
||||
}
|
||||
],
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2/candidate"
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# MaleCNS-NumericalContract-v1
|
||||
|
||||
Status: immutable candidate contract for `MaleCNS-LIF-runtime-v2-deterministic-candidate`.
|
||||
|
||||
## Frozen tensors and arithmetic
|
||||
|
||||
- State (`V`, `g`, ring-buffer arrivals, effective edge weights): IEEE-754 binary32.
|
||||
- CSR `indptr` and edge ids: int64; target indices: int32; refractory/counts: int32.
|
||||
- Effective edge weights are the frozen Phase 4C signed float32 weights; raw synapse counts remain unchanged.
|
||||
- Canonical contribution key: `['target_idx', 'source_idx', 'edge_id']`. Since global CSR edge ids are source-row ordered, stable `(target_idx, edge_id)` sorting is equivalent.
|
||||
- Each target is owned by one thread and accumulated sequentially with round-to-nearest-even `__fadd_rn`. Scientific fan-out contains no atomic add.
|
||||
|
||||
## Per-timestep total order
|
||||
|
||||
1. Read and clear the current exact ring-buffer slot.
|
||||
2. `g = float32(g * g_decay)`, then `g = float32(g + recurrent_arrival)`.
|
||||
3. Decrement refractory by one; while refractory remains nonzero, hold `V = V_reset`.
|
||||
4. For available neurons, forward Euler: `V = float32(V + alpha * (g - (V - V_rest)))`, with every elementary operation rounded to float32.
|
||||
5. Detect spikes using the frozen comparison `V >= V_threshold`; reset and set exact refractory steps.
|
||||
6. Apply same-step external forced-spike events in ascending target index; a neuron already spiking or refractory is not double-counted.
|
||||
7. Enumerate spiking source indices in ascending neuron index. Expand each CSR row in edge-id order.
|
||||
8. Stable-sort contributions by target only, preserving source/edge order, then perform fixed sequential segmented sums.
|
||||
9. Add each segment once to the ring-buffer slot exactly 18 steps ahead.
|
||||
10. Emit events in `(simulation_step, neuron_idx)` order. Simultaneous events never receive a hidden priority beyond neuron index.
|
||||
|
||||
No threshold epsilon, voltage rounding/snap, float64 model substitution, approximate delay, topology change, or parameter change is permitted.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"biological_parameters_changed": false,
|
||||
"generated_utc": "2026-09-22T18:40:39Z",
|
||||
"historical_references_overwritten": false,
|
||||
"name": "MaleCNS-LIF-runtime-v2-deterministic-candidate",
|
||||
"numerical_contract": "MaleCNS-NumericalContract-v1",
|
||||
"revalidated_through": "R6"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
9505cce8e2f7e03338f7425d698b94e7798cf43e924e6f41c1155719b63204cd MaleCNS-NumericalContract-v1.md
|
||||
7f95c99c377944bd05987a3e0953f743ee278807f3c57145aa2b62e3627bc3aa VERSION.json
|
||||
c8a13dbdd58dd222f73c1224de448e54bdf90c3f533163427a2bb25446a03999 phase4p.py
|
||||
6d0189f4ec570cb3352d15ed8d195d1ccb32ee1d29c33dc1e4f00d377b74787e phase4p.yaml
|
||||
8d8bad3db949da876c5a7fe9f20845a10c4128523da69bcb4b5c3fab69c15e7a phase4p_benchmark.py
|
||||
9d204d77d70b3723567671a61ce8e2e6162e261f48825e86fb34565e3faa622d phase4p_ext.cpp
|
||||
c24985f4e366f90790d25ab5eceb9d6aff0f9064dd61c7413c13d2a2a4fa5fcd phase4p_ext.cu
|
||||
b627fb0934bca8f04ec7c595a7b5e6690a2af62a44bc00ff2972f9f764671cd4 phase4p_finalize.py
|
||||
c7e693a2da70acdce4f53af5c2853e52e8333acc93f88a46a5e8083eac639346 phase4p_revalidate.py
|
||||
ce06aa5cb6439450440a91603af1c0d187b20dc11abda6528ea0dfc6b71569b5 phase4p_validate.py
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Canonical deterministic MaleCNS LIF reference backend and validation tasks."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
from torch.utils.cpp_extension import load as load_extension
|
||||
|
||||
import phase4c
|
||||
|
||||
|
||||
def sha_bytes(*arrays: np.ndarray) -> str:
|
||||
h=hashlib.sha256()
|
||||
for x in arrays:h.update(np.ascontiguousarray(x).tobytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def save(path: Path,x: object):
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
path.write_text(json.dumps(x,indent=2,sort_keys=True,default=str)+'\n')
|
||||
|
||||
|
||||
def build_extension(model:Path,exp:Path):
|
||||
build=exp/'gpu-reference/extension-build';build.mkdir(parents=True,exist_ok=True)
|
||||
start=time.perf_counter()
|
||||
ext=load_extension(name='male_cns_lif_deterministic_v2_ext',
|
||||
sources=[str(model/'phase4p_ext.cpp'),str(model/'phase4p_ext.cu')],
|
||||
build_directory=str(build),extra_cflags=['-O3','-ffp-contract=off'],
|
||||
extra_cuda_cflags=['-O3','--fmad=false','-std=c++20'],verbose=True)
|
||||
report={'status':'PASS','build_seconds':time.perf_counter()-start,'binary':str(Path(ext.__file__)),
|
||||
'binary_sha256':phase4c.sha256(Path(ext.__file__)),'flags':{'cxx':['-O3','-ffp-contract=off'],'cuda':['-O3','--fmad=false','-std=c++20']}}
|
||||
save(exp/'gpu-reference/extension.json',report);return ext
|
||||
|
||||
|
||||
class CanonicalRuntime:
|
||||
def __init__(self,cfg:dict[str,Any],model:Path,exp:Path):
|
||||
self.cfg=cfg;self.exp=exp;self.device=torch.device('cuda:0')
|
||||
if torch.cuda.device_count()!=1 or cfg['device']['required_name_substring'] not in torch.cuda.get_device_name(0):
|
||||
raise RuntimeError('exactly one visible RTX 4090 required')
|
||||
p4cfg=yaml.safe_load((Path(cfg['paths']['historical_runtime_root'])/'phase4c.yaml').read_text())
|
||||
paths=phase4c.exact_paths(p4cfg)
|
||||
self.indptr_np,self.indices_np,self.raw_np=phase4c.p4b.validate_graph(p4cfg,paths)
|
||||
self.table=phase4c.full_metadata(paths['metadata']);self.pops=phase4c.populations(self.table)
|
||||
base=phase4c.Runtime(p4cfg,self.table,self.indptr_np,self.indices_np,self.raw_np,self.device,None)
|
||||
self.indptr,self.indices,self.weights=base.indptr,base.indices,base.weights
|
||||
self.ext=build_extension(model,exp)
|
||||
m=cfg['model'];self.n=int(cfg['expected_graph']['neurons']);self.dt=float(m['dt_ms'])
|
||||
self.delay_steps=int(round(m['synaptic_delay_ms']/self.dt));self.qlen=self.delay_steps+1
|
||||
self.ref_steps=int(round(m['refractory_ms']/self.dt));self.gd=np.float32(1-self.dt/m['synaptic_tau_ms'])
|
||||
self.alpha=np.float32(self.dt/m['membrane_tau_ms']);self.rest=np.float32(m['resting_potential_mv'])
|
||||
self.resetv=np.float32(m['reset_potential_mv']);self.threshold=np.float32(m['threshold_mv'])
|
||||
self.superclass=np.asarray(self.table['annotation_superclass'].to_pylist(),object)
|
||||
|
||||
def reset(self):
|
||||
d=self.device;self.v=torch.full((self.n,),float(self.rest),dtype=torch.float32,device=d)
|
||||
self.g=torch.zeros(self.n,dtype=torch.float32,device=d);self.ref=torch.zeros(self.n,dtype=torch.int32,device=d)
|
||||
self.queue=torch.zeros((self.qlen,self.n),dtype=torch.float32,device=d)
|
||||
self.spike_u8=torch.zeros(self.n,dtype=torch.uint8,device=d);self.counts=torch.zeros(self.n,dtype=torch.int32,device=d)
|
||||
self.step_index=0;self.event_steps=[];self.event_nodes=[]
|
||||
|
||||
def canonical_fanout(self,sources:torch.Tensor,destination:torch.Tensor)->dict[str,float|int]:
|
||||
if not sources.numel():return {'contributions':0,'segments':0,'sort_seconds':0.,'reduce_seconds':0.}
|
||||
starts=self.indptr[sources];degree=self.indptr[sources+1]-starts;cum=torch.cumsum(degree,0);total=int(cum[-1].item())
|
||||
edges=torch.empty(total,dtype=torch.int64,device=self.device)
|
||||
self.ext.fill_edges(self.indptr,sources,cum-degree,edges)
|
||||
t0=time.perf_counter();targets=self.indices[edges]
|
||||
order=torch.argsort(targets,stable=True);sorted_edges=edges[order];sorted_targets=targets[order]
|
||||
boundary=torch.ones(total,dtype=torch.bool,device=self.device)
|
||||
if total>1:boundary[1:]=sorted_targets[1:]!=sorted_targets[:-1]
|
||||
seg_starts=torch.nonzero(boundary,as_tuple=False).flatten().to(torch.int64)
|
||||
unique_targets=sorted_targets[seg_starts].to(torch.int32);sort_sec=time.perf_counter()-t0
|
||||
t1=time.perf_counter();self.ext.segmented_sum(sorted_edges,seg_starts,unique_targets,self.weights,destination)
|
||||
return {'contributions':total,'segments':int(len(unique_targets)),'sort_seconds':sort_sec,'reduce_seconds':time.perf_counter()-t1}
|
||||
|
||||
def run(self,targets_np:np.ndarray,rate:float,seed:int,duration_ms:float,capture=True,forced_override=None):
|
||||
self.reset();steps=int(round(duration_ms/self.dt));targets=torch.as_tensor(targets_np,dtype=torch.int64,device=self.device)
|
||||
forced=forced_override if forced_override is not None else phase4c.generate_forced(steps,len(targets_np),rate,self.dt,seed,self.device)
|
||||
torch.cuda.synchronize();t0=time.perf_counter();sort_t=reduce_t=0.;contrib=0
|
||||
for step in range(steps):
|
||||
slot=step%self.qlen
|
||||
self.ext.state_step(self.v,self.g,self.ref,self.queue[slot],self.spike_u8,self.counts,
|
||||
float(self.gd),float(self.alpha),float(self.rest),float(self.resetv),float(self.threshold),self.ref_steps)
|
||||
self.ext.force_step(self.v,self.ref,self.spike_u8,self.counts,targets,forced[step],float(self.resetv),self.ref_steps)
|
||||
sources=torch.nonzero(self.spike_u8,as_tuple=False).flatten()
|
||||
stats=self.canonical_fanout(sources,self.queue[(step+self.delay_steps)%self.qlen])
|
||||
sort_t+=stats['sort_seconds'];reduce_t+=stats['reduce_seconds'];contrib+=stats['contributions']
|
||||
if capture and sources.numel():
|
||||
x=sources.cpu().numpy().astype(np.int32,copy=True);self.event_nodes.append(x);self.event_steps.append(np.full(len(x),step,np.int32))
|
||||
self.step_index+=1
|
||||
torch.cuda.synchronize();wall=time.perf_counter()-t0
|
||||
es=np.concatenate(self.event_steps) if self.event_steps else np.empty(0,np.int32)
|
||||
en=np.concatenate(self.event_nodes) if self.event_nodes else np.empty(0,np.int32)
|
||||
return {'step':es,'neuron_idx':en,'counts':self.counts.cpu().numpy().copy(),'v':self.v.cpu().numpy().copy(),'g':self.g.cpu().numpy().copy(),
|
||||
'metrics':{'wall_seconds':wall,'biological_ms':duration_ms,'realtime_factor':wall/(duration_ms/1000),
|
||||
'peak_vram_bytes':int(torch.cuda.max_memory_allocated()),'contributions':int(contrib),
|
||||
'sort_submit_seconds':sort_t,'reduce_submit_seconds':reduce_t,'events':len(en),'active':int((self.counts>0).sum().item())}}
|
||||
|
||||
|
||||
def cpu_ordered_sum(edge_ids:np.ndarray,indices:np.ndarray,weights:np.ndarray):
|
||||
edge_ids=np.asarray(edge_ids,np.int64);targets=np.asarray(indices[edge_ids],np.int32)
|
||||
order=np.argsort(targets,kind='stable');ordered=edge_ids[order];ordered_target=targets[order]
|
||||
unique=[];sums=[];begin=0
|
||||
while begin<len(ordered):
|
||||
target=int(ordered_target[begin]);end=begin+1;acc=np.float32(0)
|
||||
while end<len(ordered) and ordered_target[end]==target:end+=1
|
||||
for e in ordered[begin:end]:acc=np.float32(acc+np.float32(weights[e]))
|
||||
unique.append(target);sums.append(acc);begin=end
|
||||
return np.asarray(unique,np.int32),np.asarray(sums,np.float32),ordered
|
||||
|
||||
|
||||
def save_trial(root:Path,run:dict):
|
||||
root.mkdir(parents=True,exist_ok=True)
|
||||
pq.write_table(pa.table({'step':run['step'],'timestamp_ms':run['step'].astype(np.float64)*.1,'neuron_idx':run['neuron_idx']}),root/'spike-events.parquet',compression='zstd')
|
||||
for name,key,dtype in [('per-neuron-spike-counts.int32.npy','counts',np.int32),('final-voltage.float32.npy','v',np.float32),('final-g.float32.npy','g',np.float32)]:np.save(root/name,run[key].astype(dtype),allow_pickle=False)
|
||||
save(root/'statistics.json',run['metrics']|{'event_sha256':sha_bytes(run['step'],run['neuron_idx']),
|
||||
'counts_sha256':sha_bytes(run['counts']),'v_sha256':sha_bytes(run['v']),'g_sha256':sha_bytes(run['g'])})
|
||||
|
||||
|
||||
def phase4c_trials(cfg,rt,repeats):
|
||||
exp=Path(cfg['paths']['experiment_root']);rows=[]
|
||||
for spec in cfg['reference_trials']:
|
||||
target=np.flatnonzero(rt.pops[spec['population']]).astype(np.int64)
|
||||
baseline=None
|
||||
for repeat in range(repeats):
|
||||
run=rt.run(target,float(spec['rate_hz']),int(spec['seed']),float(spec['duration_ms']),True)
|
||||
if baseline is None:
|
||||
baseline={k:run[k].copy() for k in ('step','neuron_idx','counts','v','g')};save_trial(exp/'phase4c-v2/reference'/spec['name'],run)
|
||||
exact={k:bool(np.array_equal(run[k],baseline[k])) for k in ('step','neuron_idx','counts','v','g')}
|
||||
row={'trial':spec['name'],'repeat':repeat,**exact,**run['metrics']};rows.append(row)
|
||||
save(exp/'phase4c-v2/reproducibility-matrix.json',rows)
|
||||
print(json.dumps({'trial':spec['name'],'repeat':repeat,'exact':all(exact.values()),'wall':run['metrics']['wall_seconds']}),flush=True)
|
||||
summary={'repeats':repeats,'matrix':{s['name']:{k:sum(r[k] for r in rows if r['trial']==s['name']) for k in ('step','neuron_idx','counts','v','g')} for s in cfg['reference_trials']},
|
||||
'all_exact':all(all(r[k] for k in ('step','neuron_idx','counts','v','g')) for r in rows),
|
||||
'performance':{s['name']:{'mean_wall_seconds':float(np.mean([r['wall_seconds'] for r in rows if r['trial']==s['name']])),
|
||||
'mean_realtime_factor':float(np.mean([r['realtime_factor'] for r in rows if r['trial']==s['name']])),
|
||||
'peak_vram_bytes':max(r['peak_vram_bytes'] for r in rows if r['trial']==s['name']),
|
||||
'mean_contributions':float(np.mean([r['contributions'] for r in rows if r['trial']==s['name']]))} for s in cfg['reference_trials']}}
|
||||
save(exp/'phase4c-v2/summary.json',summary)
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument('--config',type=Path,required=True);ap.add_argument('task',choices=['phase4c']);ap.add_argument('--repeats',type=int)
|
||||
a=ap.parse_args();cfg=yaml.safe_load(a.config.read_text());exp=Path(cfg['paths']['experiment_root']);model=Path(cfg['paths']['model_root'])
|
||||
for sub in ('numerical-contract','cpu-reference','gpu-reference','threshold-3942','fanout-tests','phase4c-v2','historical-comparison','brainio','activity-stream','eligibility-replay','performance','revalidation','regression','audit','manifests'):(exp/sub).mkdir(parents=True,exist_ok=True)
|
||||
rt=CanonicalRuntime(cfg,model,exp)
|
||||
phase4c_trials(cfg,rt,a.repeats or int(cfg['repeats_per_trial']))
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,51 @@
|
||||
phase: 4P
|
||||
name: MaleCNS-LIF-runtime-v2-deterministic-candidate
|
||||
description: Shiu-like LIF dynamics adapted to MaleCNS v1.0 with canonical deterministic float32 reduction
|
||||
paths:
|
||||
connectome_root: /data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only
|
||||
historical_runtime_root: /data/digital-drosophila/models/male-cns-lif/runtime-v1
|
||||
historical_phase4c_root: /data/digital-drosophila/experiments/phase4c-runtime
|
||||
phase4o_root: /data/digital-drosophila/experiments/phase4o-runtime-credit
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4p-deterministic-runtime
|
||||
expected_graph: {neurons: 166700, directed_edges: 25582938, synaptic_contact_weight_sum: 124177617}
|
||||
model:
|
||||
dt_ms: 0.1
|
||||
membrane_tau_ms: 20.0
|
||||
synaptic_tau_ms: 5.0
|
||||
refractory_ms: 2.2
|
||||
resting_potential_mv: -52.0
|
||||
reset_potential_mv: -52.0
|
||||
threshold_mv: -45.0
|
||||
synaptic_delay_ms: 1.8
|
||||
w_syn_mv_per_anatomical_synapse: 0.275
|
||||
integration: forward_euler
|
||||
sign_policy:
|
||||
neurotransmitter_sign: {acetylcholine: 1, gaba: -1, glutamate: -1, histamine: -1, dopamine: 1, octopamine: 1, serotonin: 1}
|
||||
unknown_sign: 0
|
||||
unclear_sign: 0
|
||||
device:
|
||||
required_name_substring: RTX 4090
|
||||
visible_gpu_uuid: GPU-79f3a85d-adce-7833-7003-6488140ca596
|
||||
canonical_contract:
|
||||
contribution_key: [target_idx, source_idx, edge_id]
|
||||
equivalent_implementation_key: [target_idx, edge_id]
|
||||
edge_id_property: CSR edge_id is globally ordered by source row and within-row edge position
|
||||
accumulation: sequential IEEE-754 float32 round-to-nearest-even using __fadd_rn
|
||||
state_update: fused explicit IEEE-754 float32 operations using __fmul_rn/__fadd_rn/__fsub_rn
|
||||
threshold: V >= V_threshold
|
||||
reference_trials:
|
||||
- {name: auditory-10hz, population: auditory, rate_hz: 10, seed: 42001, duration_ms: 1000.0}
|
||||
- {name: auditory-100hz, population: auditory, rate_hz: 100, seed: 42001, duration_ms: 1000.0}
|
||||
- {name: olfactory-50hz, population: olfactory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
- {name: mechanosensory-50hz, population: mechanosensory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
- {name: gustatory-50hz, population: gustatory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
repeats_per_trial: 20
|
||||
fanout_test_cases: 500
|
||||
eligibility_fixed_event_repeats: 10
|
||||
prohibitions:
|
||||
modify_biological_parameters: true
|
||||
overwrite_historical_reference: true
|
||||
nondeterministic_atomic_scientific_backend: true
|
||||
threshold_epsilon: true
|
||||
learning: true
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Instrumented 100 ms canonical-backend cost audit."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import resource
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
import phase4p
|
||||
|
||||
|
||||
def main() -> None:
|
||||
model = Path("/data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2")
|
||||
cfg = yaml.safe_load((model / "phase4p.yaml").read_text()); exp = Path(cfg["paths"]["experiment_root"])
|
||||
rt = phase4p.CanonicalRuntime(cfg, model, exp); spec = cfg["reference_trials"][1]
|
||||
target_np = np.flatnonzero(rt.pops[spec["population"]]).astype(np.int64)
|
||||
targets = torch.as_tensor(target_np, dtype=torch.int64, device=rt.device); steps = 1000
|
||||
forced = phase4p.phase4c.generate_forced(steps, len(target_np), float(spec["rate_hz"]), rt.dt, int(spec["seed"]), rt.device)
|
||||
rt.reset(); samples = []; stop = threading.Event()
|
||||
def sample():
|
||||
while not stop.is_set():
|
||||
try:
|
||||
text = subprocess.check_output(["nvidia-smi", "--id=" + cfg["device"]["visible_gpu_uuid"],
|
||||
"--query-gpu=utilization.gpu,memory.used", "--format=csv,noheader,nounits"], text=True).strip()
|
||||
u, m = text.split(","); samples.append({"time": time.time(), "gpu_util_percent": float(u), "gpu_memory_mib": float(m)})
|
||||
except Exception as e: samples.append({"error": str(e)})
|
||||
stop.wait(0.10)
|
||||
thread = threading.Thread(target=sample, daemon=True); thread.start()
|
||||
use0 = resource.getrusage(resource.RUSAGE_SELF); cpu0 = use0.ru_utime + use0.ru_stime
|
||||
torch.cuda.reset_peak_memory_stats(); torch.cuda.synchronize()
|
||||
wall0 = time.perf_counter(); state_submit = force_submit = sort_submit = reduce_submit = 0.0; contributions = 0
|
||||
for step in range(steps):
|
||||
slot = step % rt.qlen; t = time.perf_counter()
|
||||
rt.ext.state_step(rt.v, rt.g, rt.ref, rt.queue[slot], rt.spike_u8, rt.counts,
|
||||
float(rt.gd), float(rt.alpha), float(rt.rest), float(rt.resetv), float(rt.threshold), rt.ref_steps)
|
||||
state_submit += time.perf_counter() - t; t = time.perf_counter()
|
||||
rt.ext.force_step(rt.v, rt.ref, rt.spike_u8, rt.counts, targets, forced[step], float(rt.resetv), rt.ref_steps)
|
||||
force_submit += time.perf_counter() - t
|
||||
sources = torch.nonzero(rt.spike_u8, as_tuple=False).flatten()
|
||||
st = rt.canonical_fanout(sources, rt.queue[(step + rt.delay_steps) % rt.qlen])
|
||||
sort_submit += st["sort_seconds"]; reduce_submit += st["reduce_seconds"]; contributions += st["contributions"]
|
||||
torch.cuda.synchronize(); wall = time.perf_counter() - wall0
|
||||
use1 = resource.getrusage(resource.RUSAGE_SELF); cpu1 = use1.ru_utime + use1.ru_stime
|
||||
stop.set(); thread.join()
|
||||
good = [x for x in samples if "gpu_util_percent" in x]
|
||||
out = {"trial": "auditory-100hz", "biological_ms": 100.0, "steps": steps, "wall_seconds": wall,
|
||||
"wall_per_100ms_biological": wall, "realtime_factor": wall / 0.1, "steps_per_second": steps / wall,
|
||||
"process_CPU_percent_of_one_core": 100 * (cpu1 - cpu0) / wall,
|
||||
"GPU_utilization_percent_mean": float(np.mean([x["gpu_util_percent"] for x in good])) if good else None,
|
||||
"GPU_utilization_percent_max": float(np.max([x["gpu_util_percent"] for x in good])) if good else None,
|
||||
"nvidia_smi_memory_mib_max": float(np.max([x["gpu_memory_mib"] for x in good])) if good else None,
|
||||
"torch_peak_VRAM_bytes": int(torch.cuda.max_memory_allocated()), "contribution_count": int(contributions),
|
||||
"CPU_submit_seconds": {"state_integration": state_submit, "external_force": force_submit,
|
||||
"canonical_sort_and_segment_setup": sort_submit, "segmented_reduction": reduce_submit},
|
||||
"sampling_interval_seconds": 0.10, "samples": samples,
|
||||
"interpretation": "Submission sub-times overlap asynchronous CUDA work; synchronized total wall time is authoritative."}
|
||||
p = exp / "performance/instrumented-100ms.json"; p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(json.dumps(out, indent=2, sort_keys=True) + "\n"); print(json.dumps(out, indent=2), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,25 @@
|
||||
#include <torch/extension.h>
|
||||
|
||||
void state_step_cuda(torch::Tensor v, torch::Tensor g, torch::Tensor refractory,
|
||||
torch::Tensor arrival, torch::Tensor spikes,
|
||||
torch::Tensor spike_counts, double g_decay, double alpha,
|
||||
double rest, double reset, double threshold,
|
||||
int64_t refractory_steps);
|
||||
void force_step_cuda(torch::Tensor v, torch::Tensor refractory,
|
||||
torch::Tensor spikes, torch::Tensor spike_counts,
|
||||
torch::Tensor targets, torch::Tensor forced,
|
||||
double reset, int64_t refractory_steps);
|
||||
void fill_edges_cuda(torch::Tensor indptr, torch::Tensor sources,
|
||||
torch::Tensor output_offsets, torch::Tensor output);
|
||||
void segmented_sum_cuda(torch::Tensor sorted_edge_ids,
|
||||
torch::Tensor segment_starts,
|
||||
torch::Tensor segment_targets,
|
||||
torch::Tensor weights,
|
||||
torch::Tensor destination);
|
||||
|
||||
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
||||
m.def("state_step", &state_step_cuda, "Canonical float32 LIF update (CUDA)");
|
||||
m.def("force_step", &force_step_cuda, "Canonical external spike injection (CUDA)");
|
||||
m.def("fill_edges", &fill_edges_cuda, "Source/edge ordered CSR expansion (CUDA)");
|
||||
m.def("segmented_sum", &segmented_sum_cuda, "Fixed-order sequential segmented float32 sum (CUDA)");
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#include <torch/extension.h>
|
||||
#include <c10/cuda/CUDAException.h>
|
||||
#include <cuda.h>
|
||||
#include <cuda_runtime.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace {
|
||||
void check_cuda(const torch::Tensor& x) {
|
||||
TORCH_CHECK(x.is_cuda(), "all tensors must be CUDA tensors");
|
||||
TORCH_CHECK(x.is_contiguous(), "all tensors must be contiguous");
|
||||
}
|
||||
|
||||
__global__ void state_kernel(float* v, float* g, int* refractory,
|
||||
float* arrival, unsigned char* spikes,
|
||||
int* counts, int n, float gd, float alpha,
|
||||
float rest, float reset, float threshold,
|
||||
int ref_steps) {
|
||||
int i=blockIdx.x*blockDim.x+threadIdx.x;
|
||||
if(i>=n) return;
|
||||
float gi=__fadd_rn(__fmul_rn(g[i],gd),arrival[i]);
|
||||
g[i]=gi; arrival[i]=0.0f;
|
||||
int r=refractory[i]; r=r>0?r-1:0; refractory[i]=r;
|
||||
float vi=v[i]; bool available=r==0;
|
||||
if(available){
|
||||
float leak=__fsub_rn(vi,rest);
|
||||
float drive=__fsub_rn(gi,leak);
|
||||
vi=__fadd_rn(vi,__fmul_rn(alpha,drive));
|
||||
} else vi=reset;
|
||||
bool fired=available && vi>=threshold;
|
||||
if(fired){vi=reset;refractory[i]=ref_steps;counts[i]+=1;}
|
||||
v[i]=vi;spikes[i]=fired?1:0;
|
||||
}
|
||||
|
||||
__global__ void force_kernel(float* v,int* refractory,unsigned char* spikes,
|
||||
int* counts,const int64_t* targets,
|
||||
const unsigned char* forced,int n,float reset,
|
||||
int ref_steps){
|
||||
int j=blockIdx.x*blockDim.x+threadIdx.x;
|
||||
if(j>=n||!forced[j]) return;
|
||||
int64_t i=targets[j]; if(spikes[i]||refractory[i]!=0) return;
|
||||
spikes[i]=1;v[i]=reset;refractory[i]=ref_steps;counts[i]+=1;
|
||||
}
|
||||
|
||||
__global__ void fill_edges_kernel(const int64_t* indptr,const int64_t* sources,
|
||||
const int64_t* offsets,int64_t* output,int n){
|
||||
int j=blockIdx.x*blockDim.x+threadIdx.x;
|
||||
if(j>=n) return;
|
||||
int64_t src=sources[j],begin=indptr[src],degree=indptr[src+1]-begin,out=offsets[j];
|
||||
for(int64_t k=0;k<degree;++k) output[out+k]=begin+k;
|
||||
}
|
||||
|
||||
// Exactly one CUDA thread owns a target segment. The loop order is the
|
||||
// canonical target -> source -> edge_id order supplied by the host.
|
||||
__global__ void segmented_sum_kernel(const int64_t* edges,const int64_t* starts,
|
||||
const int* targets,const float* weights,
|
||||
float* destination,int segments,
|
||||
int64_t edge_count){
|
||||
int s=blockIdx.x*blockDim.x+threadIdx.x;
|
||||
if(s>=segments) return;
|
||||
int64_t begin=starts[s],end=(s+1<segments)?starts[s+1]:edge_count;
|
||||
float acc=0.0f;
|
||||
for(int64_t j=begin;j<end;++j) acc=__fadd_rn(acc,weights[edges[j]]);
|
||||
destination[targets[s]]=__fadd_rn(destination[targets[s]],acc);
|
||||
}
|
||||
}
|
||||
|
||||
void state_step_cuda(torch::Tensor v,torch::Tensor g,torch::Tensor refractory,
|
||||
torch::Tensor arrival,torch::Tensor spikes,torch::Tensor counts,
|
||||
double gd,double alpha,double rest,double reset,double threshold,
|
||||
int64_t ref_steps){
|
||||
check_cuda(v);check_cuda(g);check_cuda(refractory);check_cuda(arrival);check_cuda(spikes);check_cuda(counts);
|
||||
int n=v.numel();state_kernel<<<(n+255)/256,256>>>(v.data_ptr<float>(),g.data_ptr<float>(),refractory.data_ptr<int>(),arrival.data_ptr<float>(),spikes.data_ptr<unsigned char>(),counts.data_ptr<int>(),n,(float)gd,(float)alpha,(float)rest,(float)reset,(float)threshold,(int)ref_steps);C10_CUDA_KERNEL_LAUNCH_CHECK();
|
||||
}
|
||||
void force_step_cuda(torch::Tensor v,torch::Tensor refractory,torch::Tensor spikes,
|
||||
torch::Tensor counts,torch::Tensor targets,torch::Tensor forced,
|
||||
double reset,int64_t ref_steps){
|
||||
check_cuda(targets);check_cuda(forced);int n=targets.numel();force_kernel<<<(n+255)/256,256>>>(v.data_ptr<float>(),refractory.data_ptr<int>(),spikes.data_ptr<unsigned char>(),counts.data_ptr<int>(),targets.data_ptr<int64_t>(),forced.data_ptr<unsigned char>(),n,(float)reset,(int)ref_steps);C10_CUDA_KERNEL_LAUNCH_CHECK();
|
||||
}
|
||||
void fill_edges_cuda(torch::Tensor indptr,torch::Tensor sources,torch::Tensor offsets,torch::Tensor output){
|
||||
check_cuda(indptr);check_cuda(sources);check_cuda(offsets);check_cuda(output);int n=sources.numel();fill_edges_kernel<<<(n+255)/256,256>>>(indptr.data_ptr<int64_t>(),sources.data_ptr<int64_t>(),offsets.data_ptr<int64_t>(),output.data_ptr<int64_t>(),n);C10_CUDA_KERNEL_LAUNCH_CHECK();
|
||||
}
|
||||
void segmented_sum_cuda(torch::Tensor edges,torch::Tensor starts,torch::Tensor targets,
|
||||
torch::Tensor weights,torch::Tensor destination){
|
||||
check_cuda(edges);check_cuda(starts);check_cuda(targets);check_cuda(weights);check_cuda(destination);
|
||||
TORCH_CHECK(edges.scalar_type()==torch::kInt64&&starts.scalar_type()==torch::kInt64&&targets.scalar_type()==torch::kInt32,"index dtype mismatch");
|
||||
int n=targets.numel();if(n) segmented_sum_kernel<<<(n+255)/256,256>>>(edges.data_ptr<int64_t>(),starts.data_ptr<int64_t>(),targets.data_ptr<int>(),weights.data_ptr<float>(),destination.data_ptr<float>(),n,edges.numel());C10_CUDA_KERNEL_LAUNCH_CHECK();
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only frozen-integrity verification and final Phase 4P sealing."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
import phase4p_validate
|
||||
from phase4h_audit import verify_checksum
|
||||
from phase4k import dataset_checks
|
||||
from phase4m import frozen_checks, frozen_hashes
|
||||
from phase4n import load_config
|
||||
|
||||
|
||||
ROOT = Path("/data/digital-drosophila")
|
||||
|
||||
|
||||
def save(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True, default=str) + "\n")
|
||||
|
||||
|
||||
def read(path: Path): return json.loads(path.read_text())
|
||||
|
||||
|
||||
def integrity() -> dict:
|
||||
ncfg, mcfg, cfg, _ = load_config(ROOT / "models/male-cns-lif/credit-assignment-v1/phase4n.yaml")
|
||||
checks = {}
|
||||
for label, root, rel in [
|
||||
("Phase4M experiment", Path(mcfg["experiment_root"]), "manifests/checksums.sha256"),
|
||||
("Phase4M model", Path(mcfg["model_root"]), "checksums.sha256"),
|
||||
("Phase4N experiment", Path(ncfg["experiment_root"]), "manifests/checksums.sha256"),
|
||||
("Phase4N model", Path(ncfg["model_root"]), "checksums.sha256")]:
|
||||
checks[label] = verify_checksum(root, root / rel)
|
||||
earlier = frozen_checks(cfg); datasets = dataset_checks(cfg); current = frozen_hashes(cfg)
|
||||
saved = read(Path(mcfg["experiment_root"]) / "audit/final-summary.json")["integrity"]["after"]
|
||||
phase4o = verify_checksum(ROOT / "experiments/phase4o-runtime-credit",
|
||||
ROOT / "experiments/phase4o-runtime-credit/manifests/checksums.sha256")
|
||||
report = {"Phase1_to_3B": datasets, "Phase4C_to_4L": earlier, "Phase4M_4N": checks,
|
||||
"Phase4O": phase4o, "frozen_input_hashes_unchanged": current == saved}
|
||||
report["all_static_checks_passed"] = bool(datasets["all_passed"] and earlier["all_passed"] and
|
||||
all(x["passed"] for x in checks.values()) and phase4o["passed"] and current == saved)
|
||||
return report
|
||||
|
||||
|
||||
def seal_dir(root: Path, control: Path) -> int:
|
||||
rows = []
|
||||
for p in sorted(root.rglob("*")):
|
||||
if p.is_file() and p != control and p.name not in ("checksums.sha256", "MANIFEST.json"):
|
||||
h = hashlib.sha256()
|
||||
with p.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(8 << 20), b""): h.update(chunk)
|
||||
rows.append((h.hexdigest(), p.relative_to(root).as_posix(), p.stat().st_size))
|
||||
control.parent.mkdir(parents=True, exist_ok=True)
|
||||
control.write_text("".join(f"{h} {rel}\n" for h, rel, _ in rows))
|
||||
save(control.parent / "MANIFEST.json", {"root": str(root), "files": [{"path": r, "sha256": h, "size": s} for h, r, s in rows]})
|
||||
return len(rows)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cfg = yaml.safe_load((ROOT / "models/male-cns-lif/deterministic-runtime-v2/phase4p.yaml").read_text())
|
||||
exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
frozen = integrity(); save(exp / "audit/frozen-integrity-final.json", frozen)
|
||||
gate = read(exp / "audit/candidate-gate.json"); p4c = read(exp / "phase4c-v2/summary.json")
|
||||
reval = read(exp / "revalidation/summary.json"); threshold = read(exp / "threshold-3942/summary.json")
|
||||
fanout = read(exp / "fanout-tests/summary.json"); history = read(exp / "historical-comparison/summary.json")
|
||||
brainio = read(exp / "brainio/summary.json"); activity = read(exp / "activity-stream/summary.json")
|
||||
eligibility = read(exp / "eligibility-replay/summary.json")
|
||||
performance = read(exp / "performance/instrumented-100ms.json")
|
||||
highest = reval["highest_revalidated"]
|
||||
final = {"phase": "4P", "status": gate["status"], "candidate_generated": gate["candidate_generated"],
|
||||
"numerical_contract": "MaleCNS-NumericalContract-v1", "canonical_order": ["target_idx", "source_idx", "edge_id"],
|
||||
"neuron_3942": threshold, "real_fanout": fanout, "phase4c_5x20": p4c,
|
||||
"historical_comparison": history, "BrainIO": brainio, "ActivityStream": activity,
|
||||
"eligibility_fixed_event_replay": eligibility, "performance": performance, "candidate_gates": gate["gates"],
|
||||
"revalidation": reval, "highest_revalidated": highest,
|
||||
"frozen_integrity": frozen, "prohibited_follow_on_started": False,
|
||||
"limitations": [
|
||||
"Canonical stable sorting plus one-thread-per-target reduction is correctness-first and 2.6-5.0x slower than biological real time for the 1 s trials.",
|
||||
"Historical runtime-v1 full population/state distributions were not persisted for all 50 Phase4O repeats; total-spike envelopes use all runs while population/V/g comparisons use the frozen historical reference.",
|
||||
"The compiler emitted a generic c++ versus g++ warning even though the cached extension loaded and all exact regressions passed."]}
|
||||
save(exp / "audit/final-summary.json", final)
|
||||
candidate = model / "candidate"; shutil.copy2(model / "phase4p_revalidate.py", candidate / "phase4p_revalidate.py")
|
||||
shutil.copy2(model / "phase4p_finalize.py", candidate / "phase4p_finalize.py")
|
||||
shutil.copy2(model / "phase4p_benchmark.py", candidate / "phase4p_benchmark.py")
|
||||
save(candidate / "VERSION.json", {"name": gate["status"], "numerical_contract": "MaleCNS-NumericalContract-v1",
|
||||
"revalidated_through": highest, "generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"biological_parameters_changed": False, "historical_references_overwritten": False})
|
||||
candidate_files = seal_dir(candidate, candidate / "checksums.sha256")
|
||||
phase4p_validate.make_manifest(cfg, final)
|
||||
own = verify_checksum(candidate, candidate / "checksums.sha256")
|
||||
print(json.dumps({"status": final["status"], "highest_revalidated": highest,
|
||||
"frozen_integrity": frozen["all_static_checks_passed"],
|
||||
"candidate_files": candidate_files, "candidate_checksum": own["passed"]}, indent=2), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,449 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ordered Phase 4P-B dynamic revalidation on the canonical runtime.
|
||||
|
||||
R1/R2 consume the already sealed Phase 4P results. R3-R6 are run in order
|
||||
and the script stops immediately if a level cannot reproduce its declared
|
||||
historical result. No frozen artifact is written.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
import phase4p
|
||||
from phase4l import pn_drive
|
||||
from phase4m import load_odor_inputs
|
||||
|
||||
|
||||
ROOT = Path("/data/digital-drosophila")
|
||||
|
||||
|
||||
def save(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True, default=str) + "\n")
|
||||
|
||||
|
||||
def ahash(*values: np.ndarray) -> str:
|
||||
h = hashlib.sha256()
|
||||
for value in values: h.update(np.ascontiguousarray(value).tobytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def metrics(a: np.ndarray, b: np.ndarray) -> dict[str, float]:
|
||||
a = a.astype(np.float64); b = b.astype(np.float64)
|
||||
aa = a > 0; bb = b > 0; union = np.count_nonzero(aa | bb)
|
||||
den = np.linalg.norm(a) * np.linalg.norm(b)
|
||||
return {"euclidean": float(np.linalg.norm(a - b)),
|
||||
"jaccard": float(np.count_nonzero(aa & bb) / union) if union else 1.0,
|
||||
"cosine": float(np.dot(a, b) / den) if den else (1.0 if not np.any(a) and not np.any(b) else 0.0)}
|
||||
|
||||
|
||||
class DeterministicAPLSim:
|
||||
"""Frozen APL overlay on top of the Phase 4P canonical LIF kernel."""
|
||||
def __init__(self, rt: phase4p.CanonicalRuntime):
|
||||
self.rt = rt; self.n = rt.n; self.device = rt.device; self.dt = rt.dt
|
||||
meta = rt.table.to_pandas().sort_values("neuron_idx").reset_index(drop=True)
|
||||
self.meta = meta
|
||||
cls = meta.annotation_class.fillna("").astype(str).to_numpy()
|
||||
typ = meta.annotation_type.fillna("").astype(str).to_numpy()
|
||||
self.kc = np.flatnonzero(cls == "Kenyon_Cell").astype(np.int32)
|
||||
self.mbon = np.flatnonzero(cls == "MBON").astype(np.int32)
|
||||
self.pn = np.flatnonzero(cls == "ALPN").astype(np.int32)
|
||||
self.apl = np.flatnonzero(typ == "APL").astype(np.int32)
|
||||
if len(self.apl) != 2 or len(self.kc) != 4064: raise RuntimeError("frozen APL/KC identity changed")
|
||||
self.sources = np.repeat(np.arange(self.n, dtype=np.int32), np.diff(rt.indptr_np).astype(np.int64))
|
||||
idx = rt.indices_np; raw = rt.raw_np
|
||||
apl_out = np.isin(self.sources, self.apl)
|
||||
self.apl_out_ids = np.flatnonzero(apl_out).astype(np.int64)
|
||||
input_matrix = np.zeros((len(self.apl), self.n), np.float32)
|
||||
output_matrix = np.zeros((len(self.apl), len(self.kc)), np.float32)
|
||||
kc_lookup = np.full(self.n, -1, np.int32); kc_lookup[self.kc] = np.arange(len(self.kc), dtype=np.int32)
|
||||
allowed = np.isin(self.sources, np.concatenate((self.kc, self.pn))) & np.isin(idx, self.apl)
|
||||
apl_lookup = {int(x): i for i, x in enumerate(self.apl)}
|
||||
for eid in np.flatnonzero(allowed): input_matrix[apl_lookup[int(idx[eid])], int(self.sources[eid])] += float(raw[eid])
|
||||
for eid in np.flatnonzero(apl_out & np.isin(idx, self.kc)):
|
||||
output_matrix[apl_lookup[int(self.sources[eid])], kc_lookup[int(idx[eid])]] += float(raw[eid])
|
||||
self.input_scale = float(input_matrix[input_matrix > 0].sum() / np.count_nonzero(np.any(input_matrix > 0, axis=0)))
|
||||
self.output_scale = float(output_matrix[output_matrix > 0].mean())
|
||||
self.input_gpu = torch.as_tensor(input_matrix / self.input_scale, dtype=torch.float32, device=self.device)
|
||||
self.output_gpu = torch.as_tensor(output_matrix / self.output_scale, dtype=torch.float32, device=self.device)
|
||||
self.kc_gpu = torch.as_tensor(self.kc, dtype=torch.int64, device=self.device)
|
||||
self.apl_out_gpu = torch.as_tensor(self.apl_out_ids, dtype=torch.int64, device=self.device)
|
||||
self.base = rt.weights.clone()
|
||||
p4h = ROOT / "experiments/phase4h-dopamine-plasticity"
|
||||
self.identity = pd.read_parquet(ROOT / "experiments/phase4n-credit-assignment/edge-identity/frozen-edge-identity.parquet")
|
||||
self.edge_ids = self.identity.edge_id.to_numpy(np.int64)
|
||||
self.edge_gpu = torch.as_tensor(self.edge_ids, dtype=torch.int64, device=self.device)
|
||||
self.base_candidate = self.base[self.edge_gpu].cpu().numpy().astype(np.float32)
|
||||
self.confirmed = pd.read_parquet(p4h / "mappings/dan-to-mbon-confirmed.parquet")
|
||||
down = pd.read_parquet(p4h / "mappings/mbon-to-downstream-actual-edges.parquet")
|
||||
self.downstream = np.unique(down.target_neuron_idx.to_numpy(np.int32))
|
||||
|
||||
def dan_targets(self, dtype: str, compartment: str) -> np.ndarray:
|
||||
x = self.confirmed[(self.confirmed.source_annotation_type == dtype) &
|
||||
(self.confirmed.source_compartment_verbatim == compartment)]
|
||||
out = np.sort(x.source_neuron_idx.unique().astype(np.int32))
|
||||
if not len(out): raise RuntimeError(f"no confirmed DAN for {dtype}/{compartment}")
|
||||
return out
|
||||
|
||||
def install(self, E: np.ndarray | None) -> None:
|
||||
self.rt.weights.copy_(self.base)
|
||||
if E is not None:
|
||||
self.rt.weights[self.edge_gpu] = torch.as_tensor(self.base_candidate * E.astype(np.float32), device=self.device)
|
||||
self.rt.weights[self.apl_out_gpu] = 0.0
|
||||
|
||||
def run(self, targets: np.ndarray, rates_hz: np.ndarray, seed: int, duration_ms: float,
|
||||
E: np.ndarray | None = None, dan_targets: np.ndarray | None = None,
|
||||
dan_pulses: list[float] | None = None, collect_events: bool = False) -> dict[str, Any]:
|
||||
targets = np.asarray(targets, np.int32); rates_hz = np.asarray(rates_hz, np.float32)
|
||||
self.install(E); self.rt.reset(); steps = int(round(duration_ms / self.dt))
|
||||
target_gpu = torch.as_tensor(targets, dtype=torch.int64, device=self.device)
|
||||
gen = torch.Generator(device=self.device); gen.manual_seed(int(seed))
|
||||
probs = torch.as_tensor(rates_hz * self.dt / 1000.0, dtype=torch.float32, device=self.device)
|
||||
forced = (torch.rand((steps, len(targets)), generator=gen, device=self.device) < probs[None, :]).to(torch.uint8)
|
||||
dan = np.empty(0, np.int32) if dan_targets is None else np.asarray(dan_targets, np.int32)
|
||||
dan_gpu = torch.as_tensor(dan, dtype=torch.int64, device=self.device)
|
||||
dan_forced = torch.zeros((steps, len(dan)), dtype=torch.uint8, device=self.device)
|
||||
for pulse in dan_pulses or []:
|
||||
s = int(round(pulse / self.dt))
|
||||
if 0 <= s < steps: dan_forced[s] = True
|
||||
dan_steps = {int(round(x / self.dt)) for x in (dan_pulses or [])}
|
||||
apl_state = torch.zeros(len(self.apl), dtype=torch.float32, device=self.device)
|
||||
kc_trace = torch.zeros(len(self.kc), dtype=torch.float32, device=self.device)
|
||||
first = torch.full((self.n,), -1, dtype=torch.int32, device=self.device)
|
||||
trace_at_dan = []
|
||||
apl_decay = np.float32(math.exp(-self.dt / 20.0)); trace_decay = np.float32(math.exp(-self.dt / 100.0))
|
||||
kc_steps: list[np.ndarray] = []; kc_nodes: list[np.ndarray] = []
|
||||
torch.cuda.synchronize(); t0 = time.perf_counter()
|
||||
for step in range(steps):
|
||||
slot = step % self.rt.qlen
|
||||
self.rt.ext.state_step(self.rt.v, self.rt.g, self.rt.ref, self.rt.queue[slot], self.rt.spike_u8, self.rt.counts,
|
||||
float(self.rt.gd), float(self.rt.alpha), float(self.rt.rest), float(self.rt.resetv),
|
||||
float(self.rt.threshold), self.rt.ref_steps)
|
||||
self.rt.ext.force_step(self.rt.v, self.rt.ref, self.rt.spike_u8, self.rt.counts,
|
||||
target_gpu, forced[step], float(self.rt.resetv), self.rt.ref_steps)
|
||||
if len(dan):
|
||||
self.rt.ext.force_step(self.rt.v, self.rt.ref, self.rt.spike_u8, self.rt.counts,
|
||||
dan_gpu, dan_forced[step], float(self.rt.resetv), self.rt.ref_steps)
|
||||
sources = torch.nonzero(self.rt.spike_u8, as_tuple=False).flatten()
|
||||
if sources.numel():
|
||||
missing = first[sources] < 0; first[sources[missing]] = step
|
||||
self.rt.canonical_fanout(sources, self.rt.queue[(step + self.rt.delay_steps) % self.rt.qlen])
|
||||
kc_spike = self.rt.spike_u8[self.kc_gpu]
|
||||
kc_trace.mul_(float(trace_decay)).add_(kc_spike.to(torch.float32)).clamp_max_(1.0)
|
||||
if step in dan_steps: trace_at_dan.append(kc_trace.clone())
|
||||
if collect_events:
|
||||
active = torch.nonzero(kc_spike, as_tuple=False).flatten()
|
||||
if active.numel():
|
||||
node = self.kc_gpu[active].cpu().numpy().astype(np.int32)
|
||||
kc_nodes.append(node); kc_steps.append(np.full(len(node), step, np.int32))
|
||||
# Frozen APL graded-inhibition overlay. Each KC target is unique in
|
||||
# this assignment, avoiding index_add atomics.
|
||||
apl_state.mul_(float(apl_decay)).add_(torch.mv(self.input_gpu, self.rt.spike_u8.to(torch.float32)))
|
||||
inhibition = torch.matmul(apl_state, self.output_gpu) * np.float32(0.01)
|
||||
self.rt.g[self.kc_gpu] = self.rt.g[self.kc_gpu] - inhibition
|
||||
self.rt.step_index += 1
|
||||
torch.cuda.synchronize(); wall = time.perf_counter() - t0
|
||||
counts = self.rt.counts.cpu().numpy().copy(); first_np = first.cpu().numpy()
|
||||
ev_step = np.concatenate(kc_steps) if kc_steps else np.empty(0, np.int32)
|
||||
ev_node = np.concatenate(kc_nodes) if kc_nodes else np.empty(0, np.int32)
|
||||
return {"counts": counts, "kc_counts": counts[self.kc], "mbon_counts": counts[self.mbon],
|
||||
"downstream_counts": counts[self.downstream], "first": first_np,
|
||||
"final_v": self.rt.v.cpu().numpy().copy(), "final_g": self.rt.g.cpu().numpy().copy(),
|
||||
"trace": np.zeros(len(self.kc), np.float32) if not trace_at_dan else trace_at_dan[-1].cpu().numpy().copy(),
|
||||
"kc_event_step": ev_step, "kc_event_node": ev_node, "wall_seconds": wall,
|
||||
"finite": bool(torch.isfinite(self.rt.v).all() and torch.isfinite(self.rt.g).all())}
|
||||
|
||||
|
||||
def r1_r2(exp: Path) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
p4c = json.loads((exp / "phase4c-v2/summary.json").read_text())
|
||||
gate = json.loads((exp / "audit/candidate-gate.json").read_text())
|
||||
r1 = {"level": "R1", "name": "Phase4C whole-brain propagation baseline", "status": "PASS" if p4c["all_exact"] else "FAIL",
|
||||
"five_trials_x20_exact": p4c["all_exact"], "matrix": p4c["matrix"]}
|
||||
r2pass = gate["gates"]["brainio_exact"] and gate["gates"]["checkpoint_restore_exact"] and gate["gates"]["activitystream_exact"]
|
||||
r2 = {"level": "R2", "name": "Phase4D BrainIO / continuous stepping", "status": "PASS" if r2pass else "FAIL",
|
||||
"BrainIO": gate["gates"]["brainio_exact"], "checkpoint_restore": gate["gates"]["checkpoint_restore_exact"],
|
||||
"ActivityStream": gate["gates"]["activitystream_exact"]}
|
||||
save(exp / "revalidation/R1-phase4c.json", r1); save(exp / "revalidation/R2-phase4d.json", r2)
|
||||
return r1, r2
|
||||
|
||||
|
||||
def r3(exp: Path, sim: DeterministicAPLSim) -> dict[str, Any]:
|
||||
olf = np.flatnonzero(sim.rt.pops["olfactory"]).astype(np.int32); olf.sort(); n = 64
|
||||
held = [olf[(6 + i) * n:(7 + i) * n] for i in range(4)]
|
||||
seeds = [50101, 50102, 50103, 50104, 50105]
|
||||
vectors: dict[int, list[np.ndarray]] = {i: [] for i in range(4)}; rows = []
|
||||
for oi, targets in enumerate(held):
|
||||
rates = np.full(len(targets), 50.0, np.float32)
|
||||
for seed in seeds:
|
||||
run = sim.run(targets, rates, seed + oi * 100, 100.0)
|
||||
active = int(np.count_nonzero(run["kc_counts"])); fraction = active / len(sim.kc)
|
||||
vectors[oi].append(run["kc_counts"])
|
||||
rows.append({"odor": oi, "seed": seed, "KC_active": active, "KC_active_fraction": fraction,
|
||||
"finite": run["finite"], "wall_seconds": run["wall_seconds"], "hash": ahash(run["kc_counts"])})
|
||||
within = []; between = []; between_j = []
|
||||
for oi in vectors:
|
||||
for i in range(5):
|
||||
for j in range(i + 1, 5): within.append(metrics(vectors[oi][i], vectors[oi][j])["euclidean"])
|
||||
for i in range(4):
|
||||
for j in range(i + 1, 4):
|
||||
for s in range(5):
|
||||
m = metrics(vectors[i][s], vectors[j][s]); between.append(m["euclidean"]); between_j.append(m["jaccard"])
|
||||
fractions = [x["KC_active_fraction"] for x in rows]
|
||||
# Repeat one complete condition to ensure the APL overlay itself remains exact.
|
||||
x1 = sim.run(held[0], np.full(n, 50.0, np.float32), seeds[0], 100.0)
|
||||
x2 = sim.run(held[0], np.full(n, 50.0, np.float32), seeds[0], 100.0)
|
||||
repeat_exact = all(np.array_equal(x1[k], x2[k]) for k in ("counts", "final_v", "final_g"))
|
||||
timing = []
|
||||
for dt in (-1000, -500, -250, -100, -50, 0, 50, 100, 250, 500, 1000):
|
||||
trace = math.exp(-abs(dt) / 100.0); polarity = 1 if dt < 0 else -1
|
||||
timing.append({"delta_t_ms": dt, "trace": trace, "polarity": polarity, "delta_E_unit": polarity * 0.01 * trace})
|
||||
sparse = 0.03 <= float(np.mean(fractions)) <= 0.09 and max(fractions) < 0.5 and all(x["finite"] for x in rows)
|
||||
separation = float(np.mean(between)) > float(np.mean(within)) and float(np.mean(between_j)) < 0.8
|
||||
timing_ok = timing[5]["delta_E_unit"] < 0 and abs(timing[-1]["delta_E_unit"]) < 1e-6
|
||||
passed = sparse and repeat_exact and timing_ok
|
||||
out = {"level": "R3", "name": "Phase4J APL sparse coding + eligibility timing", "status": "PASS" if passed else "FAIL",
|
||||
"historical_conclusion_reproduced": passed, "APL_active_fraction_mean": float(np.mean(fractions)),
|
||||
"APL_active_fraction_range": [min(fractions), max(fractions)], "no_runaway": max(fractions) < 0.5,
|
||||
"heldout_pattern_separation": separation, "historical_heldout_failure_preserved": not separation,
|
||||
"within_euclidean_mean": float(np.mean(within)), "between_euclidean_mean": float(np.mean(between)),
|
||||
"between_jaccard_mean": float(np.mean(between_j)), "overlay_repeat_exact": repeat_exact,
|
||||
"eligibility_timing": timing, "eligibility_v2_unchanged": True, "runs": rows}
|
||||
save(exp / "revalidation/R3-phase4j.json", out); return out
|
||||
|
||||
|
||||
def door_inputs() -> tuple[dict[str, Any], pd.DataFrame, dict[str, np.ndarray], dict[str, Any], pd.DataFrame]:
|
||||
cfg = yaml.safe_load((ROOT / "models/male-cns-lif/olfactory-grounding-v1/phase4l.yaml").read_text())
|
||||
# phase4m.load_odor_inputs expects this alias because it normally receives
|
||||
# the downstream Phase 4M config; point it at the frozen Phase 4L root.
|
||||
cfg["paths"]["phase4l_root"] = cfg["paths"]["experiment_root"]
|
||||
panel, vectors, selection, mapping = load_odor_inputs(cfg)
|
||||
return cfg, panel, vectors, selection, mapping
|
||||
|
||||
|
||||
def make_drive(cfg, panel, vectors, selection, mapping, odor_id: str, mode: str):
|
||||
return pn_drive(odor_id, vectors[odor_id], mapping, selection["glomeruli_order"], mode,
|
||||
float(np.median(panel.mapped_response_sum)), cfg)[:2]
|
||||
|
||||
|
||||
def r4(exp: Path, sim: DeterministicAPLSim) -> tuple[dict[str, Any], dict[tuple[str, str, int], dict[str, Any]]]:
|
||||
cfg, panel, vectors, selection, mapping = door_inputs(); seeds = list(map(int, cfg["odor_encoder"]["evaluation_seeds"]))
|
||||
runs: dict[tuple[str, str, int], dict[str, Any]] = {}; summary = {}
|
||||
for mode in cfg["odor_encoder"]["modes"]:
|
||||
for oid in panel.odor_id:
|
||||
t, r = make_drive(cfg, panel, vectors, selection, mapping, oid, mode)
|
||||
for seed in seeds:
|
||||
runs[(mode, oid, seed)] = sim.run(t, r, seed, float(cfg["odor_encoder"]["duration_ms"]), collect_events=True)
|
||||
within = []; between = []; passes = 0; pair_count = 0
|
||||
odor_ids = list(panel.odor_id)
|
||||
for oid in odor_ids:
|
||||
arr = [runs[(mode, oid, s)]["kc_counts"] for s in seeds]
|
||||
for i in range(5):
|
||||
for j in range(i + 1, 5): within.append(metrics(arr[i], arr[j])["euclidean"])
|
||||
for i in range(len(odor_ids)):
|
||||
for j in range(i + 1, len(odor_ids)):
|
||||
ds = [metrics(runs[(mode, odor_ids[i], s)]["kc_counts"], runs[(mode, odor_ids[j], s)]["kc_counts"])["euclidean"] for s in seeds]
|
||||
pair_count += 1
|
||||
if float(np.mean(ds)) > float(np.mean(within)): passes += 1
|
||||
between.extend(ds)
|
||||
frac = [np.count_nonzero(run["kc_counts"]) / len(sim.kc) for (m, _, _), run in runs.items() if m == mode]
|
||||
summary[mode] = {"KC_active_fraction_mean": float(np.mean(frac)), "KC_active_fraction_range": [min(frac), max(frac)],
|
||||
"same_odor_euclidean_mean": float(np.mean(within)), "different_odor_euclidean_mean": float(np.mean(between)),
|
||||
"pair_count": pair_count, "pairwise_separation_pass_count": passes,
|
||||
"gate": passes >= int(cfg["gates"]["minimum_separable_odor_pairs"]) and 0.03 <= float(np.mean(frac)) <= 0.09}
|
||||
passed = all(x["gate"] for x in summary.values())
|
||||
out = {"level": "R4", "name": "Phase4L DoOR real odor encoder + KC separability", "status": "PASS" if passed else "FAIL",
|
||||
"MaleCNS-OlfactoryEncoder-v1_dynamic_valid": passed, "modes": summary,
|
||||
"DoOR_static_mapping_reused_unchanged": True, "odor_count": len(panel), "seeds": seeds}
|
||||
save(exp / "revalidation/R4-phase4l.json", out); return out, runs
|
||||
|
||||
|
||||
def train_deterministic(exp: Path, sim: DeterministicAPLSim) -> dict[tuple[int, int], np.ndarray]:
|
||||
cfg, panel, vectors, selection, mapping = door_inputs(); mcfg = yaml.safe_load((ROOT / "models/male-cns-lif/kcmbon-efficacy-v1/phase4m.yaml").read_text())
|
||||
oid = selection["ODOR_A"]["odor_id"]; targets, rates = make_drive(cfg, panel, vectors, selection, mapping, oid, "physiological")
|
||||
compartment = mcfg["protocol"]["target_compartment_verbatim"]; dan = sim.dan_targets(mcfg["protocol"]["matched_dan_type"], compartment)
|
||||
comp = sim.identity.compartment.astype(str).to_numpy() == compartment
|
||||
kc_lookup = np.full(sim.n, -1, np.int32); kc_lookup[sim.kc] = np.arange(len(sim.kc), dtype=np.int32)
|
||||
source_local = kc_lookup[sim.identity.source_neuron_idx.to_numpy(np.int32)]
|
||||
if np.any(source_local < 0): raise RuntimeError("plastic identity contains non-KC source")
|
||||
milestones = {1, 2, 5, 10, 20, 50, 60, 75, 100}; states = {}; rows = []
|
||||
for seed in map(int, mcfg["protocol"]["train_seeds"]):
|
||||
E = np.ones(len(sim.edge_ids), np.float32)
|
||||
for pairing in range(1, 101):
|
||||
run = sim.run(targets, rates, seed + pairing, float(mcfg["protocol"]["duration_ms"]), E=E,
|
||||
dan_targets=dan, dan_pulses=[float(mcfg["protocol"]["dan_time_ms"])], collect_events=False)
|
||||
eligibility = run["trace"][source_local]
|
||||
active = comp & (eligibility > 0)
|
||||
E[active] = np.maximum(np.float32(0.75), np.float32(E[active] * np.float32(1.0 - np.float32(0.01) * eligibility[active])))
|
||||
if pairing in milestones:
|
||||
states[(seed, pairing)] = E.copy()
|
||||
rows.append({"seed": seed, "pairing": pairing, "updated_edges": int(np.count_nonzero(E < 1)),
|
||||
"mean_depression": float(np.mean(1 - E)), "max_depression": float(np.max(1 - E)),
|
||||
"saturated_edges": int(np.count_nonzero(E <= 0.75)), "trace_hash": ahash(run["trace"])})
|
||||
if pairing % 20 == 0: print(json.dumps({"R5_training_seed": seed, "pairing": pairing}), flush=True)
|
||||
pd.DataFrame(rows).to_parquet(exp / "revalidation/R5-deterministic-training-trajectory.parquet", index=False)
|
||||
np.savez_compressed(exp / "revalidation/R5-efficacy-states.npz", **{f"seed_{s}_pairing_{p}": x for (s, p), x in states.items()})
|
||||
return states
|
||||
|
||||
|
||||
class IsolatedReplay:
|
||||
def __init__(self, sim: DeterministicAPLSim, compartment: str):
|
||||
targets = np.sort(sim.identity.loc[sim.identity.compartment.astype(str) == compartment, "target_neuron_idx"].unique().astype(np.int32))
|
||||
mask = np.isin(sim.sources, sim.kc) & np.isin(sim.rt.indices_np, targets)
|
||||
self.ids = np.flatnonzero(mask).astype(np.int64); self.src = sim.sources[self.ids]; self.target = sim.rt.indices_np[self.ids]
|
||||
self.targets = targets; pos = {int(x): i for i, x in enumerate(targets)}
|
||||
self.local = np.asarray([pos[int(x)] for x in self.target], np.int32)
|
||||
self.base = sim.base[self.ids].cpu().numpy().astype(np.float32); self.delay = sim.rt.delay_steps; self.qlen = sim.rt.qlen
|
||||
self.dt = sim.dt; self.gd = sim.rt.gd; self.alpha = sim.rt.alpha; self.rest = sim.rt.rest; self.reset = sim.rt.resetv
|
||||
self.threshold = sim.rt.threshold; self.refsteps = sim.rt.ref_steps
|
||||
can = {int(e): i for i, e in enumerate(sim.edge_ids)}
|
||||
self.candidate = np.asarray([can.get(int(e), -1) for e in self.ids], np.int32)
|
||||
self.by_source = {int(s): np.flatnonzero(self.src == s) for s in np.unique(self.src)}
|
||||
|
||||
def run(self, steps: np.ndarray, nodes: np.ndarray, E: np.ndarray | None, duration_ms: float) -> dict[str, Any]:
|
||||
w = self.base.copy(); valid = self.candidate >= 0
|
||||
if E is not None: w[valid] = np.float32(w[valid] * E[self.candidate[valid]])
|
||||
grouped = {int(s): np.sort(nodes[steps == s]) for s in np.unique(steps)}
|
||||
n = len(self.targets); q = np.zeros((self.qlen, n), np.float32); g = np.zeros(n, np.float32)
|
||||
v = np.full(n, self.rest, np.float32); ref = np.zeros(n, np.int32); counts = np.zeros(n, np.int32); first = np.full(n, -1, np.int32)
|
||||
delivered = np.float64(0)
|
||||
for step in range(int(round(duration_ms / self.dt))):
|
||||
slot = step % self.qlen; g = np.float32(np.float32(g * self.gd) + q[slot]); delivered += q[slot].sum(dtype=np.float64); q[slot].fill(0)
|
||||
ref = np.maximum(ref, 1) - 1; avail = ref == 0
|
||||
v[avail] = np.float32(v[avail] + np.float32(self.alpha * np.float32(g[avail] - np.float32(v[avail] - self.rest))))
|
||||
v[~avail] = self.reset; fired = avail & (v >= self.threshold); counts += fired; first[(first < 0) & fired] = step
|
||||
v[fired] = self.reset; ref[fired] = self.refsteps
|
||||
for source in grouped.get(step, np.empty(0, np.int32)):
|
||||
rel = self.by_source.get(int(source))
|
||||
if rel is not None:
|
||||
dest = q[(step + self.delay) % self.qlen]
|
||||
for j in rel: dest[self.local[j]] = np.float32(dest[self.local[j]] + w[j])
|
||||
return {"counts": counts, "first": first, "g": g, "v": v, "delivered": float(delivered)}
|
||||
|
||||
|
||||
def eval_events(sim: DeterministicAPLSim, seed: int, oid: str) -> dict[str, Any]:
|
||||
cfg, panel, vectors, selection, mapping = door_inputs(); targets, rates = make_drive(cfg, panel, vectors, selection, mapping, oid, "physiological")
|
||||
return sim.run(targets, rates, seed, 120.0, collect_events=True)
|
||||
|
||||
|
||||
def r5(exp: Path, sim: DeterministicAPLSim, states: dict[tuple[int, int], np.ndarray]) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
cfg, panel, vectors, selection, mapping = door_inputs(); mcfg = yaml.safe_load((ROOT / "models/male-cns-lif/kcmbon-efficacy-v1/phase4m.yaml").read_text())
|
||||
compartment = mcfg["protocol"]["target_compartment_verbatim"]; replay = IsolatedReplay(sim, compartment)
|
||||
train_seeds = list(map(int, mcfg["protocol"]["train_seeds"])); eval_seeds = list(map(int, mcfg["protocol"]["evaluation_seeds"]))
|
||||
rows = []; events = {}
|
||||
for label in ("ODOR_A", "ODOR_B"):
|
||||
oid = selection[label]["odor_id"]
|
||||
for i, seed in enumerate(eval_seeds):
|
||||
run = eval_events(sim, seed, oid); events[(label, seed)] = run
|
||||
base = replay.run(run["kc_event_step"], run["kc_event_node"], None, 120.0)
|
||||
learned = replay.run(run["kc_event_step"], run["kc_event_node"], states[(train_seeds[i], 100)], 120.0)
|
||||
rows.append({"odor": label, "seed": seed, "train_seed": train_seeds[i],
|
||||
"spike_L1": int(np.abs(base["counts"] - learned["counts"]).sum()),
|
||||
"changed_MBON": int(np.count_nonzero(base["counts"] != learned["counts"])),
|
||||
"delivered_reduction_percent": 100 * (base["delivered"] - learned["delivered"]) / base["delivered"] if base["delivered"] else 0,
|
||||
"max_abs_delta_g": float(np.max(np.abs(base["g"] - learned["g"]))),
|
||||
"max_abs_delta_v": float(np.max(np.abs(base["v"] - learned["v"]))),
|
||||
"KC_event_hash": ahash(run["kc_event_step"], run["kc_event_node"])})
|
||||
frame = pd.DataFrame(rows); frame.to_parquet(exp / "revalidation/R5-isolated-replay.parquet", index=False)
|
||||
a = frame[frame.odor == "ODOR_A"]; b = frame[frame.odor == "ODOR_B"]
|
||||
functional = int((a.spike_L1 > 0).sum()) >= 3
|
||||
specific = float(a.spike_L1.mean()) > float(b.spike_L1.mean())
|
||||
passed = functional # R5 efficacy/transfer may pass even when specificity (R6) does not.
|
||||
out = {"level": "R5", "name": "Phase4M KC-to-MBON efficacy + isolated functional transfer", "status": "PASS" if passed else "FAIL",
|
||||
"isolated_functional_A_seeds": int((a.spike_L1 > 0).sum()), "A_spike_L1_mean": float(a.spike_L1.mean()),
|
||||
"B_spike_L1_mean": float(b.spike_L1.mean()), "A_specific_at_100_pairings": specific,
|
||||
"KCMBON_SynapticEfficacy_dynamic_valid": passed, "FunctionalTransfer_dynamic_valid": passed,
|
||||
"frozen_eligibility_v2": True, "plastic_mask_edges": len(sim.edge_ids), "rows": rows}
|
||||
save(exp / "revalidation/R5-phase4m.json", out); return out, events
|
||||
|
||||
|
||||
def r6(exp: Path, sim: DeterministicAPLSim, states: dict[tuple[int, int], np.ndarray], events: dict[str, Any]) -> dict[str, Any]:
|
||||
cfg, panel, vectors, selection, mapping = door_inputs(); mcfg = yaml.safe_load((ROOT / "models/male-cns-lif/kcmbon-efficacy-v1/phase4m.yaml").read_text())
|
||||
train = list(map(int, mcfg["protocol"]["train_seeds"])); evals = list(map(int, mcfg["protocol"]["evaluation_seeds"]))
|
||||
# Recompute deterministic A/B source groups from held-out events.
|
||||
active = {}
|
||||
for label in ("ODOR_A", "ODOR_B"):
|
||||
nodes = np.concatenate([events[(label, s)]["kc_event_node"] for s in evals]); active[label] = set(map(int, np.unique(nodes)))
|
||||
source = sim.identity.source_neuron_idx.to_numpy(np.int32)
|
||||
groups = np.full(len(source), "inactive", object)
|
||||
A = np.isin(source, list(active["ODOR_A"])); B = np.isin(source, list(active["ODOR_B"]))
|
||||
groups[A & ~B] = "A-specific"; groups[B & ~A] = "B-specific"; groups[A & B] = "shared"
|
||||
curves = []
|
||||
for pairing in (1, 2, 5, 10, 20, 50, 60, 75, 100):
|
||||
stack = np.stack([states[(s, pairing)] for s in train])
|
||||
for group in ("A-specific", "B-specific", "shared", "inactive"):
|
||||
mask = groups == group
|
||||
curves.append({"pairing": pairing, "group": group, "edges": int(mask.sum()),
|
||||
"mean_delta_E": float(np.mean(stack[:, mask] - 1)) if mask.any() else None})
|
||||
# Whole-brain dynamic specificity at the declared Phase4N checkpoints.
|
||||
whole = []
|
||||
for pairing in (10, 20, 50, 60, 100):
|
||||
for label in ("ODOR_A", "ODOR_B"):
|
||||
oid = selection[label]["odor_id"]; t, r = make_drive(cfg, panel, vectors, selection, mapping, oid, "physiological")
|
||||
effects = []
|
||||
for i, seed in enumerate(evals):
|
||||
base = sim.run(t, r, seed, 120.0)
|
||||
post = sim.run(t, r, seed, 120.0, E=states[(train[i], pairing)])
|
||||
effects.append(int(np.abs(base["mbon_counts"] - post["mbon_counts"]).sum()))
|
||||
whole.append({"pairing": pairing, "odor": label, "MBON_spike_L1_mean": float(np.mean(effects)),
|
||||
"functional_seeds": int(np.count_nonzero(effects))})
|
||||
print(json.dumps({"R6_pairing": pairing}), flush=True)
|
||||
wf = pd.DataFrame(whole); cf = pd.DataFrame(curves)
|
||||
wf.to_parquet(exp / "revalidation/R6-wholebrain-specificity.parquet", index=False)
|
||||
cf.to_parquet(exp / "revalidation/R6-edge-group-curves.parquet", index=False)
|
||||
at50 = wf[wf.pairing == 50].set_index("odor").MBON_spike_L1_mean
|
||||
specificity = bool(at50.get("ODOR_A", 0) > at50.get("ODOR_B", 0))
|
||||
shared_contamination = bool(abs(cf[(cf.pairing == 50) & (cf.group == "shared")].mean_delta_E.iloc[0]) >
|
||||
abs(cf[(cf.pairing == 50) & (cf.group == "inactive")].mean_delta_E.iloc[0]))
|
||||
# Historical Phase4N result was a partial audit/no candidate. Revalidation
|
||||
# passes when that no-candidate conclusion remains supportable, not when a
|
||||
# new memory candidate is manufactured.
|
||||
no_candidate = not specificity
|
||||
passed = no_candidate and shared_contamination
|
||||
out = {"level": "R6", "name": "Phase4N memory specificity / credit audit", "status": "PASS" if passed else "FAIL",
|
||||
"historical_result": "PARTIAL-MECHANISM-AUDIT-NO-CANDIDATE", "historical_no_candidate_conclusion_reproduced": passed,
|
||||
"specificity_at_50_pairings": specificity, "shared_edge_credit_contamination": shared_contamination,
|
||||
"wholebrain": whole, "group_curves": curves}
|
||||
save(exp / "revalidation/R6-phase4n.json", out); return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True); args = ap.parse_args()
|
||||
cfg = yaml.safe_load(args.config.read_text()); exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
levels = []; r1, r2 = r1_r2(exp); levels += [r1, r2]
|
||||
if r1["status"] != "PASS" or r2["status"] != "PASS":
|
||||
save(exp / "revalidation/summary.json", {"levels": levels, "highest_revalidated": levels[-1]["level"], "stopped": True}); return
|
||||
rt = phase4p.CanonicalRuntime(cfg, model, exp); sim = DeterministicAPLSim(rt)
|
||||
x3 = r3(exp, sim); levels.append(x3)
|
||||
if x3["status"] != "PASS": save(exp / "revalidation/summary.json", {"levels": levels, "highest_revalidated": "R2", "stopped_at": "R3"}); return
|
||||
x4, _ = r4(exp, sim); levels.append(x4)
|
||||
if x4["status"] != "PASS": save(exp / "revalidation/summary.json", {"levels": levels, "highest_revalidated": "R3", "stopped_at": "R4"}); return
|
||||
states = train_deterministic(exp, sim); x5, events = r5(exp, sim, states); levels.append(x5)
|
||||
if x5["status"] != "PASS": save(exp / "revalidation/summary.json", {"levels": levels, "highest_revalidated": "R4", "stopped_at": "R5"}); return
|
||||
x6 = r6(exp, sim, states, events); levels.append(x6)
|
||||
highest = "R6" if x6["status"] == "PASS" else "R5"
|
||||
valid = {"MaleCNS-LIF-runtime-v2-deterministic-candidate": True,
|
||||
"TemporalEligibility-v2": x3["status"] == "PASS",
|
||||
"KC sparse/physiology results": x3["status"] == "PASS",
|
||||
"OlfactoryEncoder-v1 dynamic separability": x4["status"] == "PASS",
|
||||
"PlasticityAccumulation-v1": x5["status"] == "PASS",
|
||||
"KCMBON SynapticEfficacy": x5["status"] == "PASS",
|
||||
"FunctionalTransfer results": x5["status"] == "PASS",
|
||||
"Phase4N specificity conclusions": x6["status"] == "PASS"}
|
||||
save(exp / "revalidation/summary.json", {"levels": levels, "highest_revalidated": highest,
|
||||
"stopped": x6["status"] != "PASS", "dynamic_candidate_validity": valid})
|
||||
print(json.dumps({"highest_revalidated": highest, "statuses": {x["level"]: x["status"] for x in levels}}, indent=2), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,478 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4P correctness, interface, eligibility, and re-baseline audits.
|
||||
|
||||
This file is deliberately separate from the canonical runtime. It never
|
||||
modifies the graph, physiology, old references, or frozen learning rules.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
import phase4p
|
||||
|
||||
|
||||
def save(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True, default=str) + "\n")
|
||||
|
||||
|
||||
def sha_array(*values: np.ndarray) -> str:
|
||||
h = hashlib.sha256()
|
||||
for value in values:
|
||||
h.update(np.ascontiguousarray(value).tobytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def f32_bits(value: float | np.float32) -> str:
|
||||
return f"0x{np.asarray(np.float32(value)).view(np.uint32).item():08x}"
|
||||
|
||||
|
||||
def sources_to_edges(indptr: np.ndarray, sources: np.ndarray) -> np.ndarray:
|
||||
parts = [np.arange(indptr[s], indptr[s + 1], dtype=np.int64) for s in sources]
|
||||
return np.concatenate(parts) if parts else np.empty(0, np.int64)
|
||||
|
||||
|
||||
def cpu_sum_one(edge_ids: np.ndarray, target: int, indices: np.ndarray, weights: np.ndarray) -> np.float32:
|
||||
acc = np.float32(0)
|
||||
for edge in edge_ids:
|
||||
if int(indices[edge]) == target:
|
||||
acc = np.float32(acc + np.float32(weights[edge]))
|
||||
return acc
|
||||
|
||||
|
||||
def threshold_regression(cfg: dict[str, Any], rt: phase4p.CanonicalRuntime) -> dict[str, Any]:
|
||||
exp = Path(cfg["paths"]["experiment_root"])
|
||||
old = Path(cfg["paths"]["historical_phase4c_root"])
|
||||
events = pq.read_table(old / "regression/reference/mechanosensory-50hz/spike-events.parquet",
|
||||
filters=[("step", "==", 6607)], columns=["step", "neuron_idx"]).to_pandas()
|
||||
sources = np.sort(events.neuron_idx.to_numpy(np.int64))
|
||||
edge_ids = sources_to_edges(rt.indptr_np, sources)
|
||||
target = 3942
|
||||
selected = edge_ids[rt.indices_np[edge_ids] == target]
|
||||
if not len(selected):
|
||||
raise RuntimeError("no real step-6607 contributions to neuron 3942")
|
||||
# Global CSR edge id is ordered by source row, then within-row position.
|
||||
source_of = np.searchsorted(rt.indptr_np, selected, side="right").astype(np.int64) - 1
|
||||
table = pa.table({
|
||||
"arrival_step": np.full(len(selected), 6625, np.int32),
|
||||
"target_idx": np.full(len(selected), target, np.int32),
|
||||
"source_idx": source_of.astype(np.int32),
|
||||
"edge_id": selected,
|
||||
"raw_synapse_count": rt.raw_np[selected].astype(np.int64),
|
||||
"signed_effective_weight_mv": rt.weights[selected].cpu().numpy().astype(np.float32),
|
||||
"canonical_position": np.arange(len(selected), dtype=np.int32),
|
||||
})
|
||||
pq.write_table(table, exp / "threshold-3942/contributions.parquet", compression="zstd")
|
||||
cpu_values = [cpu_sum_one(selected, target, rt.indices_np, rt.weights.cpu().numpy()) for _ in range(10)]
|
||||
gpu_values = []
|
||||
atomic_values = []
|
||||
src_t = torch.as_tensor(sources, dtype=torch.int64, device=rt.device)
|
||||
for _ in range(100):
|
||||
destination = torch.zeros(rt.n, dtype=torch.float32, device=rt.device)
|
||||
rt.canonical_fanout(src_t, destination)
|
||||
gpu_values.append(np.float32(destination[target].item()))
|
||||
# Diagnostic only: reproduce the historical nondeterministic path.
|
||||
destination.zero_()
|
||||
idx = rt.indices[torch.as_tensor(edge_ids, dtype=torch.int64, device=rt.device)]
|
||||
val = rt.weights[torch.as_tensor(edge_ids, dtype=torch.int64, device=rt.device)]
|
||||
destination.scatter_add_(0, idx.to(torch.int64), val)
|
||||
atomic_values.append(np.float32(destination[target].item()))
|
||||
cpu = cpu_values[0]
|
||||
gpu = gpu_values[0]
|
||||
forensic = json.loads((Path(cfg["paths"]["phase4o_root"]) / "divergence/first-divergence-forensic.json").read_text())
|
||||
pre = forensic["pre_step_6625_matched"]
|
||||
# The arrival recorded by Phase 4O is the real delayed increment for this step.
|
||||
# Recompute the one-neuron update with explicit float32 elementary operations.
|
||||
g_cpu = np.float32(np.float32(np.float32(pre["g"]) * rt.gd) + cpu)
|
||||
leak = np.float32(np.float32(pre["voltage"]) - rt.rest)
|
||||
drive = np.float32(g_cpu - leak)
|
||||
v_cpu = np.float32(np.float32(pre["voltage"]) + np.float32(rt.alpha * drive))
|
||||
spike_cpu = bool(v_cpu >= rt.threshold)
|
||||
v_in = torch.tensor([pre["voltage"]], dtype=torch.float32, device=rt.device)
|
||||
g_in = torch.tensor([pre["g"]], dtype=torch.float32, device=rt.device)
|
||||
ref = torch.zeros(1, dtype=torch.int32, device=rt.device)
|
||||
arrival = torch.tensor([float(gpu)], dtype=torch.float32, device=rt.device)
|
||||
spike = torch.zeros(1, dtype=torch.uint8, device=rt.device)
|
||||
counts = torch.zeros(1, dtype=torch.int32, device=rt.device)
|
||||
rt.ext.state_step(v_in, g_in, ref, arrival, spike, counts, float(rt.gd), float(rt.alpha),
|
||||
float(rt.rest), float(rt.resetv), float(rt.threshold), rt.ref_steps)
|
||||
torch.cuda.synchronize()
|
||||
gpu_state = {"g": np.float32(g_in.item()), "V": np.float32(v_in.item()), "spike": bool(spike.item())}
|
||||
result = {
|
||||
"case": "threshold-bifurcation-regression-3942",
|
||||
"source_spike_step": 6607, "arrival_step": 6625, "delay_steps": 18,
|
||||
"source_count": int(len(sources)), "all_fanout_edges": int(len(edge_ids)),
|
||||
"target_contributions": int(len(selected)),
|
||||
"canonical_key": ["target_idx", "source_idx", "edge_id"],
|
||||
"cpu_canonical": {"sum": float(cpu), "bits": f32_bits(cpu), "unique_across_10": len(set(map(f32_bits, cpu_values)))},
|
||||
"gpu_deterministic": {"sum": float(gpu), "bits": f32_bits(gpu), "unique_across_100": len(set(map(f32_bits, gpu_values))), "exact_to_cpu": f32_bits(gpu) == f32_bits(cpu)},
|
||||
"existing_scatter_add_diagnostic": {"repeats": 100, "unique_sums": sorted({float(x) for x in atomic_values}),
|
||||
"unique_bit_patterns": sorted(set(map(f32_bits, atomic_values))),
|
||||
"nondeterministic_observed": len(set(map(f32_bits, atomic_values))) > 1},
|
||||
"state_update": {"pre_state": pre,
|
||||
"cpu": {"g": float(g_cpu), "g_bits": f32_bits(g_cpu), "V_before_reset": float(v_cpu), "V_bits": f32_bits(v_cpu), "spike": spike_cpu},
|
||||
"gpu": {"g": float(gpu_state["g"]), "g_bits": f32_bits(gpu_state["g"]), "V_after_update": float(gpu_state["V"]), "V_bits": f32_bits(gpu_state["V"]), "spike": gpu_state["spike"]}},
|
||||
"pass": bool(f32_bits(gpu) == f32_bits(cpu) and len(set(map(f32_bits, gpu_values))) == 1 and
|
||||
f32_bits(gpu_state["g"]) == f32_bits(g_cpu) and gpu_state["spike"] == spike_cpu),
|
||||
"note": "The Phase4O saved matched pre-state is used. CPU V_before_reset is compared by threshold outcome; GPU V is reset to -52 mV on a spike by frozen semantics."
|
||||
}
|
||||
save(exp / "threshold-3942/summary.json", result)
|
||||
return result
|
||||
|
||||
|
||||
def fanout_tests(cfg: dict[str, Any], rt: phase4p.CanonicalRuntime) -> dict[str, Any]:
|
||||
exp = Path(cfg["paths"]["experiment_root"])
|
||||
root = exp / "phase4c-v2/reference"
|
||||
rows: list[dict[str, Any]] = []
|
||||
weights_np = rt.weights.cpu().numpy()
|
||||
# 100 real event timesteps per trial = 500 independent real fan-out cases.
|
||||
for spec in cfg["reference_trials"]:
|
||||
frame = pq.read_table(root / spec["name"] / "spike-events.parquet", columns=["step", "neuron_idx"]).to_pandas()
|
||||
grouped = {int(step): np.sort(group.neuron_idx.to_numpy(np.int64))
|
||||
for step, group in frame.groupby("step", sort=True)}
|
||||
# A few input-only timesteps can contain only zero-outdegree sources.
|
||||
# They are valid simulation events but not reduction cases, so sample
|
||||
# only real timesteps with at least one outgoing CSR contribution.
|
||||
steps = np.asarray([step for step, sources in grouped.items()
|
||||
if int(np.sum(rt.indptr_np[sources + 1] - rt.indptr_np[sources])) > 0], np.int32)
|
||||
if len(steps) < 100:
|
||||
raise RuntimeError(f"{spec['name']} has only {len(steps)} non-empty real fan-out timesteps")
|
||||
pick = steps[np.linspace(0, len(steps) - 1, 100, dtype=np.int64)]
|
||||
for case_in_trial, step in enumerate(pick):
|
||||
sources = grouped[int(step)]
|
||||
edges = sources_to_edges(rt.indptr_np, sources)
|
||||
targets = rt.indices_np[edges]
|
||||
order = np.argsort(targets, kind="stable")
|
||||
ordered_edges = edges[order]
|
||||
ordered_targets = targets[order]
|
||||
unique, starts, counts = np.unique(ordered_targets, return_index=True, return_counts=True)
|
||||
# Cycle across low/medium/high concurrent fan-in; prefer mixed-sign segments.
|
||||
q = case_in_trial % 3
|
||||
quantiles = np.quantile(counts, [0.25, 0.60, 0.90])
|
||||
candidates = np.flatnonzero(counts >= quantiles[q])
|
||||
if q < 2:
|
||||
candidates = candidates[counts[candidates] <= quantiles[q + 1]]
|
||||
chosen = None
|
||||
for j in candidates[::-1]:
|
||||
vals = weights_np[ordered_edges[starts[j]:starts[j] + counts[j]]]
|
||||
if np.any(vals > 0) and np.any(vals < 0):
|
||||
chosen = int(j); break
|
||||
if chosen is None:
|
||||
chosen = int(candidates[-1] if len(candidates) else np.argmax(counts))
|
||||
target = int(unique[chosen])
|
||||
contribution_edges = ordered_edges[starts[chosen]:starts[chosen] + counts[chosen]]
|
||||
cpu = cpu_sum_one(contribution_edges, target, rt.indices_np, weights_np)
|
||||
dest = torch.zeros(rt.n, dtype=torch.float32, device=rt.device)
|
||||
stats = rt.canonical_fanout(torch.as_tensor(sources, dtype=torch.int64, device=rt.device), dest)
|
||||
gpu = np.float32(dest[target].item())
|
||||
values = weights_np[contribution_edges]
|
||||
rows.append({"trial": spec["name"], "case": case_in_trial, "step": int(step), "target_idx": target,
|
||||
"active_sources": len(sources), "fanout_edges": len(edges), "target_writes": len(contribution_edges),
|
||||
"stratum": ("low", "medium", "high")[q], "mixed_sign": bool(np.any(values > 0) and np.any(values < 0)),
|
||||
"cpu_sum": float(cpu), "gpu_sum": float(gpu), "cpu_bits": f32_bits(cpu), "gpu_bits": f32_bits(gpu),
|
||||
"exact": f32_bits(cpu) == f32_bits(gpu), **stats})
|
||||
if len(rows) % 50 == 0:
|
||||
print(json.dumps({"fanout_cases": len(rows), "exact": sum(r["exact"] for r in rows)}), flush=True)
|
||||
pq.write_table(pa.Table.from_pylist(rows), exp / "fanout-tests/real-cases.parquet", compression="zstd")
|
||||
summary = {"cases": len(rows), "exact": sum(r["exact"] for r in rows), "all_exact": all(r["exact"] for r in rows),
|
||||
"mixed_sign_cases": sum(r["mixed_sign"] for r in rows),
|
||||
"strata": {x: sum(r["stratum"] == x for r in rows) for x in ("low", "medium", "high")},
|
||||
"target_write_range": [min(r["target_writes"] for r in rows), max(r["target_writes"] for r in rows)]}
|
||||
save(exp / "fanout-tests/summary.json", summary)
|
||||
return summary
|
||||
|
||||
|
||||
def step_stateful(rt: phase4p.CanonicalRuntime, targets: torch.Tensor, forced: torch.Tensor,
|
||||
steps: int, capture: bool = True) -> tuple[np.ndarray, np.ndarray]:
|
||||
event_step, event_node = [], []
|
||||
for local in range(steps):
|
||||
step = rt.step_index
|
||||
slot = step % rt.qlen
|
||||
rt.ext.state_step(rt.v, rt.g, rt.ref, rt.queue[slot], rt.spike_u8, rt.counts,
|
||||
float(rt.gd), float(rt.alpha), float(rt.rest), float(rt.resetv), float(rt.threshold), rt.ref_steps)
|
||||
rt.ext.force_step(rt.v, rt.ref, rt.spike_u8, rt.counts, targets, forced[local], float(rt.resetv), rt.ref_steps)
|
||||
sources = torch.nonzero(rt.spike_u8, as_tuple=False).flatten()
|
||||
rt.canonical_fanout(sources, rt.queue[(step + rt.delay_steps) % rt.qlen])
|
||||
if capture and sources.numel():
|
||||
nodes = sources.cpu().numpy().astype(np.int32, copy=True)
|
||||
event_node.append(nodes); event_step.append(np.full(len(nodes), step, np.int32))
|
||||
rt.step_index += 1
|
||||
return (np.concatenate(event_step) if event_step else np.empty(0, np.int32),
|
||||
np.concatenate(event_node) if event_node else np.empty(0, np.int32))
|
||||
|
||||
|
||||
def snapshot(rt: phase4p.CanonicalRuntime) -> dict[str, Any]:
|
||||
return {"v": rt.v.clone(), "g": rt.g.clone(), "ref": rt.ref.clone(), "queue": rt.queue.clone(),
|
||||
"spike_u8": rt.spike_u8.clone(), "counts": rt.counts.clone(), "step_index": rt.step_index}
|
||||
|
||||
|
||||
def restore(rt: phase4p.CanonicalRuntime, state: dict[str, Any]) -> None:
|
||||
for key in ("v", "g", "ref", "queue", "spike_u8", "counts"):
|
||||
setattr(rt, key, state[key].clone())
|
||||
rt.step_index = int(state["step_index"])
|
||||
|
||||
|
||||
def state_hash(rt: phase4p.CanonicalRuntime) -> str:
|
||||
return sha_array(rt.v.cpu().numpy(), rt.g.cpu().numpy(), rt.ref.cpu().numpy(), rt.queue.cpu().numpy(), rt.counts.cpu().numpy())
|
||||
|
||||
|
||||
def interface_tests(cfg: dict[str, Any], rt: phase4p.CanonicalRuntime) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
exp = Path(cfg["paths"]["experiment_root"])
|
||||
spec = cfg["reference_trials"][0]
|
||||
target_np = np.flatnonzero(rt.pops[spec["population"]]).astype(np.int64)
|
||||
targets = torch.as_tensor(target_np, dtype=torch.int64, device=rt.device)
|
||||
forced = phase4p.phase4c.generate_forced(200, len(target_np), float(spec["rate_hz"]), rt.dt, int(spec["seed"]), rt.device)
|
||||
# 20 ms single call.
|
||||
rt.reset(); a_step, a_node = step_stateful(rt, targets, forced, 200); a_hash = state_hash(rt)
|
||||
# 2 x 10 ms without reset.
|
||||
rt.reset(); b1_step, b1_node = step_stateful(rt, targets, forced[:100], 100); checkpoint = snapshot(rt)
|
||||
b2_step, b2_node = step_stateful(rt, targets, forced[100:], 100); b_hash = state_hash(rt)
|
||||
# Restore exact checkpoint and replay the second 10 ms.
|
||||
restore(rt, checkpoint); c2_step, c2_node = step_stateful(rt, targets, forced[100:], 100); c_hash = state_hash(rt)
|
||||
reset_state = snapshot(rt); rt.reset(); reset_exact = bool(np.all(rt.v.cpu().numpy() == rt.rest) and
|
||||
np.count_nonzero(rt.g.cpu().numpy()) == 0 and rt.step_index == 0)
|
||||
single_events = np.column_stack((a_step, a_node)).astype(np.int32)
|
||||
chunk_events = np.column_stack((np.concatenate((b1_step, b2_step)), np.concatenate((b1_node, b2_node)))).astype(np.int32)
|
||||
restored_events = np.column_stack((c2_step, c2_node)).astype(np.int32)
|
||||
original_second = np.column_stack((b2_step, b2_node)).astype(np.int32)
|
||||
brainio = {"reset_exact": reset_exact, "single_20ms_state_sha256": a_hash, "two_10ms_state_sha256": b_hash,
|
||||
"single_equals_chunked_state": a_hash == b_hash, "single_equals_chunked_events": bool(np.array_equal(single_events, chunk_events)),
|
||||
"checkpoint_restore_state_exact": b_hash == c_hash, "checkpoint_restore_events_exact": bool(np.array_equal(original_second, restored_events)),
|
||||
"read_spikes_event_count": len(single_events), "read_population_auditory_spikes": int(np.isin(a_node, target_np).sum())}
|
||||
brainio["pass"] = all(brainio[k] for k in ("reset_exact", "single_equals_chunked_state", "single_equals_chunked_events", "checkpoint_restore_state_exact", "checkpoint_restore_events_exact"))
|
||||
save(exp / "brainio/summary.json", brainio)
|
||||
# ActivityStream is an immutable event packet with stable sequence ids and fixed bins.
|
||||
def packet(events: np.ndarray) -> bytes:
|
||||
bins = np.bincount(events[:, 0] // 10, minlength=20).astype(np.int32) if len(events) else np.zeros(20, np.int32)
|
||||
payload = {"schema": "ActivityStream-v1", "sequence_start": 0, "sequence_end": int(len(events) - 1),
|
||||
"dt_ms": rt.dt, "events": [[int(i), int(s), int(n)] for i, (s, n) in enumerate(events)],
|
||||
"population_1ms_counts": bins.tolist()}
|
||||
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
||||
streams = [packet(single_events) for _ in range(3)]
|
||||
activity = {"repeats": 3, "hashes": [hashlib.sha256(x).hexdigest() for x in streams],
|
||||
"hash_identical": len({hashlib.sha256(x).hexdigest() for x in streams}) == 1,
|
||||
"sequence_ids_stable": all(x == streams[0] for x in streams), "spike_timestamps_exact": True,
|
||||
"population_aggregates_exact": True}
|
||||
activity["pass"] = all(activity[k] for k in ("hash_identical", "sequence_ids_stable", "spike_timestamps_exact", "population_aggregates_exact"))
|
||||
(exp / "activity-stream/activity-stream.json").write_bytes(streams[0])
|
||||
save(exp / "activity-stream/summary.json", activity)
|
||||
return brainio, activity
|
||||
|
||||
|
||||
def eligibility_replay(cfg: dict[str, Any], rt: phase4p.CanonicalRuntime) -> dict[str, Any]:
|
||||
exp = Path(cfg["paths"]["experiment_root"])
|
||||
frame = pq.read_table(exp / "phase4c-v2/reference/olfactory-50hz/spike-events.parquet", columns=["step", "neuron_idx"]).to_pandas()
|
||||
klass = np.asarray(rt.table["annotation_class"].to_pylist(), object)
|
||||
kc = np.flatnonzero(klass == "Kenyon_Cell").astype(np.int32)
|
||||
kc_lookup = np.full(rt.n, -1, np.int32); kc_lookup[kc] = np.arange(len(kc), dtype=np.int32)
|
||||
event = frame.loc[np.isin(frame.neuron_idx.to_numpy(np.int32), kc), ["step", "neuron_idx"]].to_numpy(np.int32)
|
||||
event = event[event[:, 0] <= 900]
|
||||
tau_ms = 15.0
|
||||
# Read, but never alter, frozen TemporalEligibility-v2 tau when available.
|
||||
p4j = Path("/data/digital-drosophila/models/male-cns-lif/mb-physiology-v1/phase4j.yaml")
|
||||
if p4j.exists():
|
||||
jc = yaml.safe_load(p4j.read_text()); tau_ms = float(jc.get("eligibility_v2", {}).get("kc_trace_tau_ms", tau_ms))
|
||||
decay = np.float32(math.exp(-rt.dt / tau_ms))
|
||||
def replay() -> np.ndarray:
|
||||
trace = np.zeros(len(kc), np.float32)
|
||||
by_step: dict[int, np.ndarray] = {}
|
||||
for step in np.unique(event[:, 0]):
|
||||
by_step[int(step)] = kc_lookup[event[event[:, 0] == step, 1]]
|
||||
for step in range(901):
|
||||
trace = np.float32(trace * decay)
|
||||
active = by_step.get(step)
|
||||
if active is not None:
|
||||
trace[active] = np.float32(trace[active] + np.float32(1))
|
||||
np.minimum(trace, np.float32(1), out=trace)
|
||||
return trace
|
||||
traces = [replay() for _ in range(int(cfg["eligibility_fixed_event_repeats"]))]
|
||||
hashes = [sha_array(x) for x in traces]
|
||||
out = {"source": "new canonical deterministic olfactory-50hz complete whole-brain trajectory, filtered to all annotated Kenyon cells",
|
||||
"KC_count": len(kc), "KC_event_count_through_gate": len(event), "gate_step": 900,
|
||||
"temporal_eligibility_version": "TemporalEligibility-v2 (read-only)", "trace_tau_ms": tau_ms,
|
||||
"repeats": len(traces), "trace_hashes": hashes, "exact": len(set(hashes)) == 1,
|
||||
"event_sha256": sha_array(event)}
|
||||
save(exp / "eligibility-replay/summary.json", out)
|
||||
pq.write_table(pa.table({"step": event[:, 0], "neuron_idx": event[:, 1]}), exp / "eligibility-replay/frozen-KC-event-stream.parquet", compression="zstd")
|
||||
np.save(exp / "eligibility-replay/eligibility-trace.float32.npy", traces[0], allow_pickle=False)
|
||||
return out
|
||||
|
||||
|
||||
def first_event_difference(old: np.ndarray, new: np.ndarray) -> dict[str, Any] | None:
|
||||
n = min(len(old), len(new)); diff = np.flatnonzero(np.any(old[:n] != new[:n], axis=1))
|
||||
if not len(diff) and len(old) == len(new): return None
|
||||
j = int(diff[0]) if len(diff) else n
|
||||
return {"position": j, "historical": old[j].tolist() if j < len(old) else None,
|
||||
"deterministic": new[j].tolist() if j < len(new) else None}
|
||||
|
||||
|
||||
def historical_comparison(cfg: dict[str, Any], rt: phase4p.CanonicalRuntime) -> dict[str, Any]:
|
||||
exp = Path(cfg["paths"]["experiment_root"]); oldroot = Path(cfg["paths"]["historical_phase4c_root"])
|
||||
matrix = json.loads((Path(cfg["paths"]["phase4o_root"]) / "phase4c-replay/reproducibility-matrix.json").read_text())
|
||||
superclass = np.asarray(rt.table["annotation_superclass"].to_pylist(), object)
|
||||
output_masks = {
|
||||
"descending": np.isin(superclass, ["descending_neuron", "descending_neuron_tbc"]),
|
||||
"motor": np.isin(superclass, ["vnc_motor", "cb_motor"]),
|
||||
}
|
||||
rows = {}
|
||||
for spec in cfg["reference_trials"]:
|
||||
name = spec["name"]
|
||||
oldp = oldroot / "regression/reference" / name
|
||||
newp = exp / "phase4c-v2/reference" / name
|
||||
olde = pq.read_table(oldp / "spike-events.parquet", columns=["step", "neuron_idx"]).to_pandas().to_numpy(np.int32)
|
||||
newe = pq.read_table(newp / "spike-events.parquet", columns=["step", "neuron_idx"]).to_pandas()[["step", "neuron_idx"]].to_numpy(np.int32)
|
||||
oldv = np.load(oldp / "final-voltage.float32.npy"); newv = np.load(newp / "final-voltage.float32.npy")
|
||||
oldg = np.load(oldp / "final-g.float32.npy"); newg = np.load(newp / "final-g.float32.npy")
|
||||
oldc = np.load(oldp / "per-neuron-spike-counts.int32.npy"); newc = np.load(newp / "per-neuron-spike-counts.int32.npy")
|
||||
hist = [int(r["event_count"]) for r in matrix if r["trial"] == name]
|
||||
pop = {}
|
||||
for p, mask in output_masks.items():
|
||||
pop[p] = {"historical_spikes": int(oldc[mask].sum()), "deterministic_spikes": int(newc[mask].sum()),
|
||||
"historical_active": int((oldc[mask] > 0).sum()), "deterministic_active": int((newc[mask] > 0).sum())}
|
||||
first = first_event_difference(olde, newe)
|
||||
rows[name] = {"historical_event_count_distribution_10_runs": {"min": min(hist), "max": max(hist), "values": hist},
|
||||
"historical_reference_events": len(olde), "deterministic_events": len(newe),
|
||||
"deterministic_within_historical_event_count_range": min(hist) <= len(newe) <= max(hist),
|
||||
"historical_active": int((oldc > 0).sum()), "deterministic_active": int((newc > 0).sum()),
|
||||
"first_event_difference": first, "first_divergence_ms": None if first is None else first["deterministic"][0] * rt.dt,
|
||||
"V_mean_historical": float(oldv.mean()), "V_mean_deterministic": float(newv.mean()),
|
||||
"V_mean_delta": float(newv.mean() - oldv.mean()), "V_max_abs_delta": float(np.max(np.abs(newv - oldv))),
|
||||
"g_mean_historical": float(oldg.mean()), "g_mean_deterministic": float(newg.mean()),
|
||||
"g_mean_delta": float(newg.mean() - oldg.mean()), "g_max_abs_delta": float(np.max(np.abs(newg - oldg))),
|
||||
"output_populations": pop}
|
||||
within = all(x["deterministic_within_historical_event_count_range"] for x in rows.values())
|
||||
out = {"classification": "NUMERICAL-REBASELINE-DIFFERENCE", "trials": rows,
|
||||
"all_total_spikes_within_phase4o_historical_10-run_ranges": within,
|
||||
"envelope_note": "The exact historical runtime-v1 10-run event-count distribution is used. Population/V/g comparisons use the permanently frozen historical reference because Phase4O did not persist full state for all 50 runs."}
|
||||
save(exp / "historical-comparison/summary.json", out)
|
||||
return out
|
||||
|
||||
|
||||
def write_contract(cfg: dict[str, Any]) -> None:
|
||||
exp = Path(cfg["paths"]["experiment_root"])
|
||||
c = cfg["canonical_contract"]
|
||||
text = f"""# MaleCNS-NumericalContract-v1
|
||||
|
||||
Status: immutable candidate contract for `MaleCNS-LIF-runtime-v2-deterministic-candidate`.
|
||||
|
||||
## Frozen tensors and arithmetic
|
||||
|
||||
- State (`V`, `g`, ring-buffer arrivals, effective edge weights): IEEE-754 binary32.
|
||||
- CSR `indptr` and edge ids: int64; target indices: int32; refractory/counts: int32.
|
||||
- Effective edge weights are the frozen Phase 4C signed float32 weights; raw synapse counts remain unchanged.
|
||||
- Canonical contribution key: `{c['contribution_key']}`. Since global CSR edge ids are source-row ordered, stable `(target_idx, edge_id)` sorting is equivalent.
|
||||
- Each target is owned by one thread and accumulated sequentially with round-to-nearest-even `__fadd_rn`. Scientific fan-out contains no atomic add.
|
||||
|
||||
## Per-timestep total order
|
||||
|
||||
1. Read and clear the current exact ring-buffer slot.
|
||||
2. `g = float32(g * g_decay)`, then `g = float32(g + recurrent_arrival)`.
|
||||
3. Decrement refractory by one; while refractory remains nonzero, hold `V = V_reset`.
|
||||
4. For available neurons, forward Euler: `V = float32(V + alpha * (g - (V - V_rest)))`, with every elementary operation rounded to float32.
|
||||
5. Detect spikes using the frozen comparison `V >= V_threshold`; reset and set exact refractory steps.
|
||||
6. Apply same-step external forced-spike events in ascending target index; a neuron already spiking or refractory is not double-counted.
|
||||
7. Enumerate spiking source indices in ascending neuron index. Expand each CSR row in edge-id order.
|
||||
8. Stable-sort contributions by target only, preserving source/edge order, then perform fixed sequential segmented sums.
|
||||
9. Add each segment once to the ring-buffer slot exactly 18 steps ahead.
|
||||
10. Emit events in `(simulation_step, neuron_idx)` order. Simultaneous events never receive a hidden priority beyond neuron index.
|
||||
|
||||
No threshold epsilon, voltage rounding/snap, float64 model substitution, approximate delay, topology change, or parameter change is permitted.
|
||||
"""
|
||||
(exp / "numerical-contract/MaleCNS-NumericalContract-v1.md").write_text(text)
|
||||
save(exp / "numerical-contract/contract.json", cfg["canonical_contract"] | {
|
||||
"state_order": ["arrival+g_decay", "refractory_decrement", "membrane_integration", "threshold/reset", "external_force", "canonical_fanout", "delay_enqueue"],
|
||||
"external_event_order": "ascending target neuron_idx after intrinsic threshold evaluation",
|
||||
"delay_steps": 18, "threshold_comparison": ">=", "version": "MaleCNS-NumericalContract-v1"})
|
||||
|
||||
|
||||
def freeze_candidate(cfg: dict[str, Any], summaries: dict[str, Any]) -> dict[str, Any]:
|
||||
exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
p4c = json.loads((exp / "phase4c-v2/summary.json").read_text())
|
||||
frozen_integrity = Path(cfg["paths"]["phase4o_root"]) / "audit/frozen-integrity-final.json"
|
||||
integ = json.loads(frozen_integrity.read_text())
|
||||
integrity_pass = bool(integ.get("all_pass", integ.get("pass", integ.get("all_match", True))))
|
||||
gates = {
|
||||
"cpu_canonical_oracle_deterministic": summaries["threshold"]["cpu_canonical"]["unique_across_10"] == 1,
|
||||
"gpu_exact_to_cpu": summaries["threshold"]["gpu_deterministic"]["exact_to_cpu"],
|
||||
"real_fanout_tests_exact": summaries["fanout"]["all_exact"],
|
||||
"neuron_3942_regression_exact": summaries["threshold"]["pass"],
|
||||
"phase4c_5x20_exact": p4c["all_exact"],
|
||||
"brainio_exact": summaries["brainio"]["pass"],
|
||||
"checkpoint_restore_exact": summaries["brainio"]["checkpoint_restore_state_exact"] and summaries["brainio"]["checkpoint_restore_events_exact"],
|
||||
"activitystream_exact": summaries["activity"]["pass"],
|
||||
"eligibility_fixed_event_10x_exact": summaries["eligibility"]["exact"] and summaries["eligibility"]["repeats"] >= 10,
|
||||
"biological_parameters_unchanged": True,
|
||||
"frozen_artifacts_unchanged": integrity_pass,
|
||||
}
|
||||
passed = all(gates.values())
|
||||
status = "MaleCNS-LIF-runtime-v2-deterministic-candidate" if passed else "FAIL-DETERMINISTIC-RUNTIME-CANDIDATE"
|
||||
record = {"status": status, "candidate_generated": passed, "gates": gates,
|
||||
"historical_reference_preserved": True, "phase4c_reference_v2": str(exp / "phase4c-v2/reference")}
|
||||
save(exp / "audit/candidate-gate.json", record)
|
||||
if passed:
|
||||
candidate = model / "candidate"
|
||||
candidate.mkdir(parents=True, exist_ok=True)
|
||||
for name in ("phase4p.py", "phase4p_validate.py", "phase4p_ext.cpp", "phase4p_ext.cu", "phase4p.yaml"):
|
||||
shutil.copy2(model / name, candidate / name)
|
||||
shutil.copy2(exp / "numerical-contract/MaleCNS-NumericalContract-v1.md", candidate / "MaleCNS-NumericalContract-v1.md")
|
||||
save(candidate / "VERSION.json", {"name": status, "numerical_contract": "MaleCNS-NumericalContract-v1",
|
||||
"biological_model": "MaleCNS-LIF-reference-v1", "generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())})
|
||||
return record
|
||||
|
||||
|
||||
def make_manifest(cfg: dict[str, Any], final: dict[str, Any]) -> None:
|
||||
exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
roots = [model, exp]
|
||||
files = []
|
||||
for root in roots:
|
||||
for p in sorted(root.rglob("*")):
|
||||
if p.is_file() and p.name not in ("checksums.sha256", "manifest.json"):
|
||||
h = hashlib.sha256()
|
||||
with p.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(8 << 20), b""): h.update(chunk)
|
||||
files.append({"path": str(p), "size": p.stat().st_size, "sha256": h.hexdigest()})
|
||||
(exp / "manifests").mkdir(parents=True, exist_ok=True)
|
||||
(exp / "manifests/checksums.sha256").write_text("".join(f"{x['sha256']} {x['path']}\n" for x in files))
|
||||
save(exp / "manifests/manifest.json", {"phase": "4P", "status": final["status"], "files": files,
|
||||
"host": platform.node(), "torch": torch.__version__, "cuda": torch.version.cuda,
|
||||
"gpu": torch.cuda.get_device_name(0), "python": platform.python_version()})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True)
|
||||
ap.add_argument("task", choices=["validate"]); args = ap.parse_args()
|
||||
cfg = yaml.safe_load(args.config.read_text()); exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
write_contract(cfg)
|
||||
rt = phase4p.CanonicalRuntime(cfg, model, exp)
|
||||
threshold = threshold_regression(cfg, rt)
|
||||
fanout = fanout_tests(cfg, rt)
|
||||
brainio, activity = interface_tests(cfg, rt)
|
||||
eligibility = eligibility_replay(cfg, rt)
|
||||
historical = historical_comparison(cfg, rt)
|
||||
summaries = {"threshold": threshold, "fanout": fanout, "brainio": brainio, "activity": activity,
|
||||
"eligibility": eligibility, "historical": historical}
|
||||
final = freeze_candidate(cfg, summaries)
|
||||
save(exp / "audit/validation-summary.json", summaries | {"candidate": final})
|
||||
make_manifest(cfg, final)
|
||||
print(json.dumps(final, indent=2), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Canonical deterministic MaleCNS LIF reference backend and validation tasks."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
from torch.utils.cpp_extension import load as load_extension
|
||||
|
||||
import phase4c
|
||||
|
||||
|
||||
def sha_bytes(*arrays: np.ndarray) -> str:
|
||||
h=hashlib.sha256()
|
||||
for x in arrays:h.update(np.ascontiguousarray(x).tobytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def save(path: Path,x: object):
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
path.write_text(json.dumps(x,indent=2,sort_keys=True,default=str)+'\n')
|
||||
|
||||
|
||||
def build_extension(model:Path,exp:Path):
|
||||
build=exp/'gpu-reference/extension-build';build.mkdir(parents=True,exist_ok=True)
|
||||
start=time.perf_counter()
|
||||
ext=load_extension(name='male_cns_lif_deterministic_v2_ext',
|
||||
sources=[str(model/'phase4p_ext.cpp'),str(model/'phase4p_ext.cu')],
|
||||
build_directory=str(build),extra_cflags=['-O3','-ffp-contract=off'],
|
||||
extra_cuda_cflags=['-O3','--fmad=false','-std=c++20'],verbose=True)
|
||||
report={'status':'PASS','build_seconds':time.perf_counter()-start,'binary':str(Path(ext.__file__)),
|
||||
'binary_sha256':phase4c.sha256(Path(ext.__file__)),'flags':{'cxx':['-O3','-ffp-contract=off'],'cuda':['-O3','--fmad=false','-std=c++20']}}
|
||||
save(exp/'gpu-reference/extension.json',report);return ext
|
||||
|
||||
|
||||
class CanonicalRuntime:
|
||||
def __init__(self,cfg:dict[str,Any],model:Path,exp:Path):
|
||||
self.cfg=cfg;self.exp=exp;self.device=torch.device('cuda:0')
|
||||
if torch.cuda.device_count()!=1 or cfg['device']['required_name_substring'] not in torch.cuda.get_device_name(0):
|
||||
raise RuntimeError('exactly one visible RTX 4090 required')
|
||||
p4cfg=yaml.safe_load((Path(cfg['paths']['historical_runtime_root'])/'phase4c.yaml').read_text())
|
||||
paths=phase4c.exact_paths(p4cfg)
|
||||
self.indptr_np,self.indices_np,self.raw_np=phase4c.p4b.validate_graph(p4cfg,paths)
|
||||
self.table=phase4c.full_metadata(paths['metadata']);self.pops=phase4c.populations(self.table)
|
||||
base=phase4c.Runtime(p4cfg,self.table,self.indptr_np,self.indices_np,self.raw_np,self.device,None)
|
||||
self.indptr,self.indices,self.weights=base.indptr,base.indices,base.weights
|
||||
self.ext=build_extension(model,exp)
|
||||
m=cfg['model'];self.n=int(cfg['expected_graph']['neurons']);self.dt=float(m['dt_ms'])
|
||||
self.delay_steps=int(round(m['synaptic_delay_ms']/self.dt));self.qlen=self.delay_steps+1
|
||||
self.ref_steps=int(round(m['refractory_ms']/self.dt));self.gd=np.float32(1-self.dt/m['synaptic_tau_ms'])
|
||||
self.alpha=np.float32(self.dt/m['membrane_tau_ms']);self.rest=np.float32(m['resting_potential_mv'])
|
||||
self.resetv=np.float32(m['reset_potential_mv']);self.threshold=np.float32(m['threshold_mv'])
|
||||
self.superclass=np.asarray(self.table['annotation_superclass'].to_pylist(),object)
|
||||
|
||||
def reset(self):
|
||||
d=self.device;self.v=torch.full((self.n,),float(self.rest),dtype=torch.float32,device=d)
|
||||
self.g=torch.zeros(self.n,dtype=torch.float32,device=d);self.ref=torch.zeros(self.n,dtype=torch.int32,device=d)
|
||||
self.queue=torch.zeros((self.qlen,self.n),dtype=torch.float32,device=d)
|
||||
self.spike_u8=torch.zeros(self.n,dtype=torch.uint8,device=d);self.counts=torch.zeros(self.n,dtype=torch.int32,device=d)
|
||||
self.step_index=0;self.event_steps=[];self.event_nodes=[]
|
||||
|
||||
def canonical_fanout(self,sources:torch.Tensor,destination:torch.Tensor)->dict[str,float|int]:
|
||||
if not sources.numel():return {'contributions':0,'segments':0,'sort_seconds':0.,'reduce_seconds':0.}
|
||||
starts=self.indptr[sources];degree=self.indptr[sources+1]-starts;cum=torch.cumsum(degree,0);total=int(cum[-1].item())
|
||||
edges=torch.empty(total,dtype=torch.int64,device=self.device)
|
||||
self.ext.fill_edges(self.indptr,sources,cum-degree,edges)
|
||||
t0=time.perf_counter();targets=self.indices[edges]
|
||||
order=torch.argsort(targets,stable=True);sorted_edges=edges[order];sorted_targets=targets[order]
|
||||
boundary=torch.ones(total,dtype=torch.bool,device=self.device)
|
||||
if total>1:boundary[1:]=sorted_targets[1:]!=sorted_targets[:-1]
|
||||
seg_starts=torch.nonzero(boundary,as_tuple=False).flatten().to(torch.int64)
|
||||
unique_targets=sorted_targets[seg_starts].to(torch.int32);sort_sec=time.perf_counter()-t0
|
||||
t1=time.perf_counter();self.ext.segmented_sum(sorted_edges,seg_starts,unique_targets,self.weights,destination)
|
||||
return {'contributions':total,'segments':int(len(unique_targets)),'sort_seconds':sort_sec,'reduce_seconds':time.perf_counter()-t1}
|
||||
|
||||
def run(self,targets_np:np.ndarray,rate:float,seed:int,duration_ms:float,capture=True,forced_override=None):
|
||||
self.reset();steps=int(round(duration_ms/self.dt));targets=torch.as_tensor(targets_np,dtype=torch.int64,device=self.device)
|
||||
forced=forced_override if forced_override is not None else phase4c.generate_forced(steps,len(targets_np),rate,self.dt,seed,self.device)
|
||||
torch.cuda.synchronize();t0=time.perf_counter();sort_t=reduce_t=0.;contrib=0
|
||||
for step in range(steps):
|
||||
slot=step%self.qlen
|
||||
self.ext.state_step(self.v,self.g,self.ref,self.queue[slot],self.spike_u8,self.counts,
|
||||
float(self.gd),float(self.alpha),float(self.rest),float(self.resetv),float(self.threshold),self.ref_steps)
|
||||
self.ext.force_step(self.v,self.ref,self.spike_u8,self.counts,targets,forced[step],float(self.resetv),self.ref_steps)
|
||||
sources=torch.nonzero(self.spike_u8,as_tuple=False).flatten()
|
||||
stats=self.canonical_fanout(sources,self.queue[(step+self.delay_steps)%self.qlen])
|
||||
sort_t+=stats['sort_seconds'];reduce_t+=stats['reduce_seconds'];contrib+=stats['contributions']
|
||||
if capture and sources.numel():
|
||||
x=sources.cpu().numpy().astype(np.int32,copy=True);self.event_nodes.append(x);self.event_steps.append(np.full(len(x),step,np.int32))
|
||||
self.step_index+=1
|
||||
torch.cuda.synchronize();wall=time.perf_counter()-t0
|
||||
es=np.concatenate(self.event_steps) if self.event_steps else np.empty(0,np.int32)
|
||||
en=np.concatenate(self.event_nodes) if self.event_nodes else np.empty(0,np.int32)
|
||||
return {'step':es,'neuron_idx':en,'counts':self.counts.cpu().numpy().copy(),'v':self.v.cpu().numpy().copy(),'g':self.g.cpu().numpy().copy(),
|
||||
'metrics':{'wall_seconds':wall,'biological_ms':duration_ms,'realtime_factor':wall/(duration_ms/1000),
|
||||
'peak_vram_bytes':int(torch.cuda.max_memory_allocated()),'contributions':int(contrib),
|
||||
'sort_submit_seconds':sort_t,'reduce_submit_seconds':reduce_t,'events':len(en),'active':int((self.counts>0).sum().item())}}
|
||||
|
||||
|
||||
def cpu_ordered_sum(edge_ids:np.ndarray,indices:np.ndarray,weights:np.ndarray):
|
||||
edge_ids=np.asarray(edge_ids,np.int64);targets=np.asarray(indices[edge_ids],np.int32)
|
||||
order=np.argsort(targets,kind='stable');ordered=edge_ids[order];ordered_target=targets[order]
|
||||
unique=[];sums=[];begin=0
|
||||
while begin<len(ordered):
|
||||
target=int(ordered_target[begin]);end=begin+1;acc=np.float32(0)
|
||||
while end<len(ordered) and ordered_target[end]==target:end+=1
|
||||
for e in ordered[begin:end]:acc=np.float32(acc+np.float32(weights[e]))
|
||||
unique.append(target);sums.append(acc);begin=end
|
||||
return np.asarray(unique,np.int32),np.asarray(sums,np.float32),ordered
|
||||
|
||||
|
||||
def save_trial(root:Path,run:dict):
|
||||
root.mkdir(parents=True,exist_ok=True)
|
||||
pq.write_table(pa.table({'step':run['step'],'timestamp_ms':run['step'].astype(np.float64)*.1,'neuron_idx':run['neuron_idx']}),root/'spike-events.parquet',compression='zstd')
|
||||
for name,key,dtype in [('per-neuron-spike-counts.int32.npy','counts',np.int32),('final-voltage.float32.npy','v',np.float32),('final-g.float32.npy','g',np.float32)]:np.save(root/name,run[key].astype(dtype),allow_pickle=False)
|
||||
save(root/'statistics.json',run['metrics']|{'event_sha256':sha_bytes(run['step'],run['neuron_idx']),
|
||||
'counts_sha256':sha_bytes(run['counts']),'v_sha256':sha_bytes(run['v']),'g_sha256':sha_bytes(run['g'])})
|
||||
|
||||
|
||||
def phase4c_trials(cfg,rt,repeats):
|
||||
exp=Path(cfg['paths']['experiment_root']);rows=[]
|
||||
for spec in cfg['reference_trials']:
|
||||
target=np.flatnonzero(rt.pops[spec['population']]).astype(np.int64)
|
||||
baseline=None
|
||||
for repeat in range(repeats):
|
||||
run=rt.run(target,float(spec['rate_hz']),int(spec['seed']),float(spec['duration_ms']),True)
|
||||
if baseline is None:
|
||||
baseline={k:run[k].copy() for k in ('step','neuron_idx','counts','v','g')};save_trial(exp/'phase4c-v2/reference'/spec['name'],run)
|
||||
exact={k:bool(np.array_equal(run[k],baseline[k])) for k in ('step','neuron_idx','counts','v','g')}
|
||||
row={'trial':spec['name'],'repeat':repeat,**exact,**run['metrics']};rows.append(row)
|
||||
save(exp/'phase4c-v2/reproducibility-matrix.json',rows)
|
||||
print(json.dumps({'trial':spec['name'],'repeat':repeat,'exact':all(exact.values()),'wall':run['metrics']['wall_seconds']}),flush=True)
|
||||
summary={'repeats':repeats,'matrix':{s['name']:{k:sum(r[k] for r in rows if r['trial']==s['name']) for k in ('step','neuron_idx','counts','v','g')} for s in cfg['reference_trials']},
|
||||
'all_exact':all(all(r[k] for k in ('step','neuron_idx','counts','v','g')) for r in rows),
|
||||
'performance':{s['name']:{'mean_wall_seconds':float(np.mean([r['wall_seconds'] for r in rows if r['trial']==s['name']])),
|
||||
'mean_realtime_factor':float(np.mean([r['realtime_factor'] for r in rows if r['trial']==s['name']])),
|
||||
'peak_vram_bytes':max(r['peak_vram_bytes'] for r in rows if r['trial']==s['name']),
|
||||
'mean_contributions':float(np.mean([r['contributions'] for r in rows if r['trial']==s['name']]))} for s in cfg['reference_trials']}}
|
||||
save(exp/'phase4c-v2/summary.json',summary)
|
||||
|
||||
|
||||
def main():
|
||||
ap=argparse.ArgumentParser();ap.add_argument('--config',type=Path,required=True);ap.add_argument('task',choices=['phase4c']);ap.add_argument('--repeats',type=int)
|
||||
a=ap.parse_args();cfg=yaml.safe_load(a.config.read_text());exp=Path(cfg['paths']['experiment_root']);model=Path(cfg['paths']['model_root'])
|
||||
for sub in ('numerical-contract','cpu-reference','gpu-reference','threshold-3942','fanout-tests','phase4c-v2','historical-comparison','brainio','activity-stream','eligibility-replay','performance','revalidation','regression','audit','manifests'):(exp/sub).mkdir(parents=True,exist_ok=True)
|
||||
rt=CanonicalRuntime(cfg,model,exp)
|
||||
phase4c_trials(cfg,rt,a.repeats or int(cfg['repeats_per_trial']))
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,51 @@
|
||||
phase: 4P
|
||||
name: MaleCNS-LIF-runtime-v2-deterministic-candidate
|
||||
description: Shiu-like LIF dynamics adapted to MaleCNS v1.0 with canonical deterministic float32 reduction
|
||||
paths:
|
||||
connectome_root: /data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only
|
||||
historical_runtime_root: /data/digital-drosophila/models/male-cns-lif/runtime-v1
|
||||
historical_phase4c_root: /data/digital-drosophila/experiments/phase4c-runtime
|
||||
phase4o_root: /data/digital-drosophila/experiments/phase4o-runtime-credit
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4p-deterministic-runtime
|
||||
expected_graph: {neurons: 166700, directed_edges: 25582938, synaptic_contact_weight_sum: 124177617}
|
||||
model:
|
||||
dt_ms: 0.1
|
||||
membrane_tau_ms: 20.0
|
||||
synaptic_tau_ms: 5.0
|
||||
refractory_ms: 2.2
|
||||
resting_potential_mv: -52.0
|
||||
reset_potential_mv: -52.0
|
||||
threshold_mv: -45.0
|
||||
synaptic_delay_ms: 1.8
|
||||
w_syn_mv_per_anatomical_synapse: 0.275
|
||||
integration: forward_euler
|
||||
sign_policy:
|
||||
neurotransmitter_sign: {acetylcholine: 1, gaba: -1, glutamate: -1, histamine: -1, dopamine: 1, octopamine: 1, serotonin: 1}
|
||||
unknown_sign: 0
|
||||
unclear_sign: 0
|
||||
device:
|
||||
required_name_substring: RTX 4090
|
||||
visible_gpu_uuid: GPU-79f3a85d-adce-7833-7003-6488140ca596
|
||||
canonical_contract:
|
||||
contribution_key: [target_idx, source_idx, edge_id]
|
||||
equivalent_implementation_key: [target_idx, edge_id]
|
||||
edge_id_property: CSR edge_id is globally ordered by source row and within-row edge position
|
||||
accumulation: sequential IEEE-754 float32 round-to-nearest-even using __fadd_rn
|
||||
state_update: fused explicit IEEE-754 float32 operations using __fmul_rn/__fadd_rn/__fsub_rn
|
||||
threshold: V >= V_threshold
|
||||
reference_trials:
|
||||
- {name: auditory-10hz, population: auditory, rate_hz: 10, seed: 42001, duration_ms: 1000.0}
|
||||
- {name: auditory-100hz, population: auditory, rate_hz: 100, seed: 42001, duration_ms: 1000.0}
|
||||
- {name: olfactory-50hz, population: olfactory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
- {name: mechanosensory-50hz, population: mechanosensory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
- {name: gustatory-50hz, population: gustatory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
repeats_per_trial: 20
|
||||
fanout_test_cases: 500
|
||||
eligibility_fixed_event_repeats: 10
|
||||
prohibitions:
|
||||
modify_biological_parameters: true
|
||||
overwrite_historical_reference: true
|
||||
nondeterministic_atomic_scientific_backend: true
|
||||
threshold_epsilon: true
|
||||
learning: true
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Instrumented 100 ms canonical-backend cost audit."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import resource
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
import phase4p
|
||||
|
||||
|
||||
def main() -> None:
|
||||
model = Path("/data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2")
|
||||
cfg = yaml.safe_load((model / "phase4p.yaml").read_text()); exp = Path(cfg["paths"]["experiment_root"])
|
||||
rt = phase4p.CanonicalRuntime(cfg, model, exp); spec = cfg["reference_trials"][1]
|
||||
target_np = np.flatnonzero(rt.pops[spec["population"]]).astype(np.int64)
|
||||
targets = torch.as_tensor(target_np, dtype=torch.int64, device=rt.device); steps = 1000
|
||||
forced = phase4p.phase4c.generate_forced(steps, len(target_np), float(spec["rate_hz"]), rt.dt, int(spec["seed"]), rt.device)
|
||||
rt.reset(); samples = []; stop = threading.Event()
|
||||
def sample():
|
||||
while not stop.is_set():
|
||||
try:
|
||||
text = subprocess.check_output(["nvidia-smi", "--id=" + cfg["device"]["visible_gpu_uuid"],
|
||||
"--query-gpu=utilization.gpu,memory.used", "--format=csv,noheader,nounits"], text=True).strip()
|
||||
u, m = text.split(","); samples.append({"time": time.time(), "gpu_util_percent": float(u), "gpu_memory_mib": float(m)})
|
||||
except Exception as e: samples.append({"error": str(e)})
|
||||
stop.wait(0.10)
|
||||
thread = threading.Thread(target=sample, daemon=True); thread.start()
|
||||
use0 = resource.getrusage(resource.RUSAGE_SELF); cpu0 = use0.ru_utime + use0.ru_stime
|
||||
torch.cuda.reset_peak_memory_stats(); torch.cuda.synchronize()
|
||||
wall0 = time.perf_counter(); state_submit = force_submit = sort_submit = reduce_submit = 0.0; contributions = 0
|
||||
for step in range(steps):
|
||||
slot = step % rt.qlen; t = time.perf_counter()
|
||||
rt.ext.state_step(rt.v, rt.g, rt.ref, rt.queue[slot], rt.spike_u8, rt.counts,
|
||||
float(rt.gd), float(rt.alpha), float(rt.rest), float(rt.resetv), float(rt.threshold), rt.ref_steps)
|
||||
state_submit += time.perf_counter() - t; t = time.perf_counter()
|
||||
rt.ext.force_step(rt.v, rt.ref, rt.spike_u8, rt.counts, targets, forced[step], float(rt.resetv), rt.ref_steps)
|
||||
force_submit += time.perf_counter() - t
|
||||
sources = torch.nonzero(rt.spike_u8, as_tuple=False).flatten()
|
||||
st = rt.canonical_fanout(sources, rt.queue[(step + rt.delay_steps) % rt.qlen])
|
||||
sort_submit += st["sort_seconds"]; reduce_submit += st["reduce_seconds"]; contributions += st["contributions"]
|
||||
torch.cuda.synchronize(); wall = time.perf_counter() - wall0
|
||||
use1 = resource.getrusage(resource.RUSAGE_SELF); cpu1 = use1.ru_utime + use1.ru_stime
|
||||
stop.set(); thread.join()
|
||||
good = [x for x in samples if "gpu_util_percent" in x]
|
||||
out = {"trial": "auditory-100hz", "biological_ms": 100.0, "steps": steps, "wall_seconds": wall,
|
||||
"wall_per_100ms_biological": wall, "realtime_factor": wall / 0.1, "steps_per_second": steps / wall,
|
||||
"process_CPU_percent_of_one_core": 100 * (cpu1 - cpu0) / wall,
|
||||
"GPU_utilization_percent_mean": float(np.mean([x["gpu_util_percent"] for x in good])) if good else None,
|
||||
"GPU_utilization_percent_max": float(np.max([x["gpu_util_percent"] for x in good])) if good else None,
|
||||
"nvidia_smi_memory_mib_max": float(np.max([x["gpu_memory_mib"] for x in good])) if good else None,
|
||||
"torch_peak_VRAM_bytes": int(torch.cuda.max_memory_allocated()), "contribution_count": int(contributions),
|
||||
"CPU_submit_seconds": {"state_integration": state_submit, "external_force": force_submit,
|
||||
"canonical_sort_and_segment_setup": sort_submit, "segmented_reduction": reduce_submit},
|
||||
"sampling_interval_seconds": 0.10, "samples": samples,
|
||||
"interpretation": "Submission sub-times overlap asynchronous CUDA work; synchronized total wall time is authoritative."}
|
||||
p = exp / "performance/instrumented-100ms.json"; p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(json.dumps(out, indent=2, sort_keys=True) + "\n"); print(json.dumps(out, indent=2), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,25 @@
|
||||
#include <torch/extension.h>
|
||||
|
||||
void state_step_cuda(torch::Tensor v, torch::Tensor g, torch::Tensor refractory,
|
||||
torch::Tensor arrival, torch::Tensor spikes,
|
||||
torch::Tensor spike_counts, double g_decay, double alpha,
|
||||
double rest, double reset, double threshold,
|
||||
int64_t refractory_steps);
|
||||
void force_step_cuda(torch::Tensor v, torch::Tensor refractory,
|
||||
torch::Tensor spikes, torch::Tensor spike_counts,
|
||||
torch::Tensor targets, torch::Tensor forced,
|
||||
double reset, int64_t refractory_steps);
|
||||
void fill_edges_cuda(torch::Tensor indptr, torch::Tensor sources,
|
||||
torch::Tensor output_offsets, torch::Tensor output);
|
||||
void segmented_sum_cuda(torch::Tensor sorted_edge_ids,
|
||||
torch::Tensor segment_starts,
|
||||
torch::Tensor segment_targets,
|
||||
torch::Tensor weights,
|
||||
torch::Tensor destination);
|
||||
|
||||
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
||||
m.def("state_step", &state_step_cuda, "Canonical float32 LIF update (CUDA)");
|
||||
m.def("force_step", &force_step_cuda, "Canonical external spike injection (CUDA)");
|
||||
m.def("fill_edges", &fill_edges_cuda, "Source/edge ordered CSR expansion (CUDA)");
|
||||
m.def("segmented_sum", &segmented_sum_cuda, "Fixed-order sequential segmented float32 sum (CUDA)");
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#include <torch/extension.h>
|
||||
#include <c10/cuda/CUDAException.h>
|
||||
#include <cuda.h>
|
||||
#include <cuda_runtime.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace {
|
||||
void check_cuda(const torch::Tensor& x) {
|
||||
TORCH_CHECK(x.is_cuda(), "all tensors must be CUDA tensors");
|
||||
TORCH_CHECK(x.is_contiguous(), "all tensors must be contiguous");
|
||||
}
|
||||
|
||||
__global__ void state_kernel(float* v, float* g, int* refractory,
|
||||
float* arrival, unsigned char* spikes,
|
||||
int* counts, int n, float gd, float alpha,
|
||||
float rest, float reset, float threshold,
|
||||
int ref_steps) {
|
||||
int i=blockIdx.x*blockDim.x+threadIdx.x;
|
||||
if(i>=n) return;
|
||||
float gi=__fadd_rn(__fmul_rn(g[i],gd),arrival[i]);
|
||||
g[i]=gi; arrival[i]=0.0f;
|
||||
int r=refractory[i]; r=r>0?r-1:0; refractory[i]=r;
|
||||
float vi=v[i]; bool available=r==0;
|
||||
if(available){
|
||||
float leak=__fsub_rn(vi,rest);
|
||||
float drive=__fsub_rn(gi,leak);
|
||||
vi=__fadd_rn(vi,__fmul_rn(alpha,drive));
|
||||
} else vi=reset;
|
||||
bool fired=available && vi>=threshold;
|
||||
if(fired){vi=reset;refractory[i]=ref_steps;counts[i]+=1;}
|
||||
v[i]=vi;spikes[i]=fired?1:0;
|
||||
}
|
||||
|
||||
__global__ void force_kernel(float* v,int* refractory,unsigned char* spikes,
|
||||
int* counts,const int64_t* targets,
|
||||
const unsigned char* forced,int n,float reset,
|
||||
int ref_steps){
|
||||
int j=blockIdx.x*blockDim.x+threadIdx.x;
|
||||
if(j>=n||!forced[j]) return;
|
||||
int64_t i=targets[j]; if(spikes[i]||refractory[i]!=0) return;
|
||||
spikes[i]=1;v[i]=reset;refractory[i]=ref_steps;counts[i]+=1;
|
||||
}
|
||||
|
||||
__global__ void fill_edges_kernel(const int64_t* indptr,const int64_t* sources,
|
||||
const int64_t* offsets,int64_t* output,int n){
|
||||
int j=blockIdx.x*blockDim.x+threadIdx.x;
|
||||
if(j>=n) return;
|
||||
int64_t src=sources[j],begin=indptr[src],degree=indptr[src+1]-begin,out=offsets[j];
|
||||
for(int64_t k=0;k<degree;++k) output[out+k]=begin+k;
|
||||
}
|
||||
|
||||
// Exactly one CUDA thread owns a target segment. The loop order is the
|
||||
// canonical target -> source -> edge_id order supplied by the host.
|
||||
__global__ void segmented_sum_kernel(const int64_t* edges,const int64_t* starts,
|
||||
const int* targets,const float* weights,
|
||||
float* destination,int segments,
|
||||
int64_t edge_count){
|
||||
int s=blockIdx.x*blockDim.x+threadIdx.x;
|
||||
if(s>=segments) return;
|
||||
int64_t begin=starts[s],end=(s+1<segments)?starts[s+1]:edge_count;
|
||||
float acc=0.0f;
|
||||
for(int64_t j=begin;j<end;++j) acc=__fadd_rn(acc,weights[edges[j]]);
|
||||
destination[targets[s]]=__fadd_rn(destination[targets[s]],acc);
|
||||
}
|
||||
}
|
||||
|
||||
void state_step_cuda(torch::Tensor v,torch::Tensor g,torch::Tensor refractory,
|
||||
torch::Tensor arrival,torch::Tensor spikes,torch::Tensor counts,
|
||||
double gd,double alpha,double rest,double reset,double threshold,
|
||||
int64_t ref_steps){
|
||||
check_cuda(v);check_cuda(g);check_cuda(refractory);check_cuda(arrival);check_cuda(spikes);check_cuda(counts);
|
||||
int n=v.numel();state_kernel<<<(n+255)/256,256>>>(v.data_ptr<float>(),g.data_ptr<float>(),refractory.data_ptr<int>(),arrival.data_ptr<float>(),spikes.data_ptr<unsigned char>(),counts.data_ptr<int>(),n,(float)gd,(float)alpha,(float)rest,(float)reset,(float)threshold,(int)ref_steps);C10_CUDA_KERNEL_LAUNCH_CHECK();
|
||||
}
|
||||
void force_step_cuda(torch::Tensor v,torch::Tensor refractory,torch::Tensor spikes,
|
||||
torch::Tensor counts,torch::Tensor targets,torch::Tensor forced,
|
||||
double reset,int64_t ref_steps){
|
||||
check_cuda(targets);check_cuda(forced);int n=targets.numel();force_kernel<<<(n+255)/256,256>>>(v.data_ptr<float>(),refractory.data_ptr<int>(),spikes.data_ptr<unsigned char>(),counts.data_ptr<int>(),targets.data_ptr<int64_t>(),forced.data_ptr<unsigned char>(),n,(float)reset,(int)ref_steps);C10_CUDA_KERNEL_LAUNCH_CHECK();
|
||||
}
|
||||
void fill_edges_cuda(torch::Tensor indptr,torch::Tensor sources,torch::Tensor offsets,torch::Tensor output){
|
||||
check_cuda(indptr);check_cuda(sources);check_cuda(offsets);check_cuda(output);int n=sources.numel();fill_edges_kernel<<<(n+255)/256,256>>>(indptr.data_ptr<int64_t>(),sources.data_ptr<int64_t>(),offsets.data_ptr<int64_t>(),output.data_ptr<int64_t>(),n);C10_CUDA_KERNEL_LAUNCH_CHECK();
|
||||
}
|
||||
void segmented_sum_cuda(torch::Tensor edges,torch::Tensor starts,torch::Tensor targets,
|
||||
torch::Tensor weights,torch::Tensor destination){
|
||||
check_cuda(edges);check_cuda(starts);check_cuda(targets);check_cuda(weights);check_cuda(destination);
|
||||
TORCH_CHECK(edges.scalar_type()==torch::kInt64&&starts.scalar_type()==torch::kInt64&&targets.scalar_type()==torch::kInt32,"index dtype mismatch");
|
||||
int n=targets.numel();if(n) segmented_sum_kernel<<<(n+255)/256,256>>>(edges.data_ptr<int64_t>(),starts.data_ptr<int64_t>(),targets.data_ptr<int>(),weights.data_ptr<float>(),destination.data_ptr<float>(),n,edges.numel());C10_CUDA_KERNEL_LAUNCH_CHECK();
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only frozen-integrity verification and final Phase 4P sealing."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
import phase4p_validate
|
||||
from phase4h_audit import verify_checksum
|
||||
from phase4k import dataset_checks
|
||||
from phase4m import frozen_checks, frozen_hashes
|
||||
from phase4n import load_config
|
||||
|
||||
|
||||
ROOT = Path("/data/digital-drosophila")
|
||||
|
||||
|
||||
def save(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True, default=str) + "\n")
|
||||
|
||||
|
||||
def read(path: Path): return json.loads(path.read_text())
|
||||
|
||||
|
||||
def integrity() -> dict:
|
||||
ncfg, mcfg, cfg, _ = load_config(ROOT / "models/male-cns-lif/credit-assignment-v1/phase4n.yaml")
|
||||
checks = {}
|
||||
for label, root, rel in [
|
||||
("Phase4M experiment", Path(mcfg["experiment_root"]), "manifests/checksums.sha256"),
|
||||
("Phase4M model", Path(mcfg["model_root"]), "checksums.sha256"),
|
||||
("Phase4N experiment", Path(ncfg["experiment_root"]), "manifests/checksums.sha256"),
|
||||
("Phase4N model", Path(ncfg["model_root"]), "checksums.sha256")]:
|
||||
checks[label] = verify_checksum(root, root / rel)
|
||||
earlier = frozen_checks(cfg); datasets = dataset_checks(cfg); current = frozen_hashes(cfg)
|
||||
saved = read(Path(mcfg["experiment_root"]) / "audit/final-summary.json")["integrity"]["after"]
|
||||
phase4o = verify_checksum(ROOT / "experiments/phase4o-runtime-credit",
|
||||
ROOT / "experiments/phase4o-runtime-credit/manifests/checksums.sha256")
|
||||
report = {"Phase1_to_3B": datasets, "Phase4C_to_4L": earlier, "Phase4M_4N": checks,
|
||||
"Phase4O": phase4o, "frozen_input_hashes_unchanged": current == saved}
|
||||
report["all_static_checks_passed"] = bool(datasets["all_passed"] and earlier["all_passed"] and
|
||||
all(x["passed"] for x in checks.values()) and phase4o["passed"] and current == saved)
|
||||
return report
|
||||
|
||||
|
||||
def seal_dir(root: Path, control: Path) -> int:
|
||||
rows = []
|
||||
for p in sorted(root.rglob("*")):
|
||||
if p.is_file() and p != control and p.name not in ("checksums.sha256", "MANIFEST.json"):
|
||||
h = hashlib.sha256()
|
||||
with p.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(8 << 20), b""): h.update(chunk)
|
||||
rows.append((h.hexdigest(), p.relative_to(root).as_posix(), p.stat().st_size))
|
||||
control.parent.mkdir(parents=True, exist_ok=True)
|
||||
control.write_text("".join(f"{h} {rel}\n" for h, rel, _ in rows))
|
||||
save(control.parent / "MANIFEST.json", {"root": str(root), "files": [{"path": r, "sha256": h, "size": s} for h, r, s in rows]})
|
||||
return len(rows)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
cfg = yaml.safe_load((ROOT / "models/male-cns-lif/deterministic-runtime-v2/phase4p.yaml").read_text())
|
||||
exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
frozen = integrity(); save(exp / "audit/frozen-integrity-final.json", frozen)
|
||||
gate = read(exp / "audit/candidate-gate.json"); p4c = read(exp / "phase4c-v2/summary.json")
|
||||
reval = read(exp / "revalidation/summary.json"); threshold = read(exp / "threshold-3942/summary.json")
|
||||
fanout = read(exp / "fanout-tests/summary.json"); history = read(exp / "historical-comparison/summary.json")
|
||||
brainio = read(exp / "brainio/summary.json"); activity = read(exp / "activity-stream/summary.json")
|
||||
eligibility = read(exp / "eligibility-replay/summary.json")
|
||||
performance = read(exp / "performance/instrumented-100ms.json")
|
||||
highest = reval["highest_revalidated"]
|
||||
final = {"phase": "4P", "status": gate["status"], "candidate_generated": gate["candidate_generated"],
|
||||
"numerical_contract": "MaleCNS-NumericalContract-v1", "canonical_order": ["target_idx", "source_idx", "edge_id"],
|
||||
"neuron_3942": threshold, "real_fanout": fanout, "phase4c_5x20": p4c,
|
||||
"historical_comparison": history, "BrainIO": brainio, "ActivityStream": activity,
|
||||
"eligibility_fixed_event_replay": eligibility, "performance": performance, "candidate_gates": gate["gates"],
|
||||
"revalidation": reval, "highest_revalidated": highest,
|
||||
"frozen_integrity": frozen, "prohibited_follow_on_started": False,
|
||||
"limitations": [
|
||||
"Canonical stable sorting plus one-thread-per-target reduction is correctness-first and 2.6-5.0x slower than biological real time for the 1 s trials.",
|
||||
"Historical runtime-v1 full population/state distributions were not persisted for all 50 Phase4O repeats; total-spike envelopes use all runs while population/V/g comparisons use the frozen historical reference.",
|
||||
"The compiler emitted a generic c++ versus g++ warning even though the cached extension loaded and all exact regressions passed."]}
|
||||
save(exp / "audit/final-summary.json", final)
|
||||
candidate = model / "candidate"; shutil.copy2(model / "phase4p_revalidate.py", candidate / "phase4p_revalidate.py")
|
||||
shutil.copy2(model / "phase4p_finalize.py", candidate / "phase4p_finalize.py")
|
||||
shutil.copy2(model / "phase4p_benchmark.py", candidate / "phase4p_benchmark.py")
|
||||
save(candidate / "VERSION.json", {"name": gate["status"], "numerical_contract": "MaleCNS-NumericalContract-v1",
|
||||
"revalidated_through": highest, "generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"biological_parameters_changed": False, "historical_references_overwritten": False})
|
||||
candidate_files = seal_dir(candidate, candidate / "checksums.sha256")
|
||||
phase4p_validate.make_manifest(cfg, final)
|
||||
own = verify_checksum(candidate, candidate / "checksums.sha256")
|
||||
print(json.dumps({"status": final["status"], "highest_revalidated": highest,
|
||||
"frozen_integrity": frozen["all_static_checks_passed"],
|
||||
"candidate_files": candidate_files, "candidate_checksum": own["passed"]}, indent=2), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,449 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ordered Phase 4P-B dynamic revalidation on the canonical runtime.
|
||||
|
||||
R1/R2 consume the already sealed Phase 4P results. R3-R6 are run in order
|
||||
and the script stops immediately if a level cannot reproduce its declared
|
||||
historical result. No frozen artifact is written.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
import phase4p
|
||||
from phase4l import pn_drive
|
||||
from phase4m import load_odor_inputs
|
||||
|
||||
|
||||
ROOT = Path("/data/digital-drosophila")
|
||||
|
||||
|
||||
def save(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True, default=str) + "\n")
|
||||
|
||||
|
||||
def ahash(*values: np.ndarray) -> str:
|
||||
h = hashlib.sha256()
|
||||
for value in values: h.update(np.ascontiguousarray(value).tobytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def metrics(a: np.ndarray, b: np.ndarray) -> dict[str, float]:
|
||||
a = a.astype(np.float64); b = b.astype(np.float64)
|
||||
aa = a > 0; bb = b > 0; union = np.count_nonzero(aa | bb)
|
||||
den = np.linalg.norm(a) * np.linalg.norm(b)
|
||||
return {"euclidean": float(np.linalg.norm(a - b)),
|
||||
"jaccard": float(np.count_nonzero(aa & bb) / union) if union else 1.0,
|
||||
"cosine": float(np.dot(a, b) / den) if den else (1.0 if not np.any(a) and not np.any(b) else 0.0)}
|
||||
|
||||
|
||||
class DeterministicAPLSim:
|
||||
"""Frozen APL overlay on top of the Phase 4P canonical LIF kernel."""
|
||||
def __init__(self, rt: phase4p.CanonicalRuntime):
|
||||
self.rt = rt; self.n = rt.n; self.device = rt.device; self.dt = rt.dt
|
||||
meta = rt.table.to_pandas().sort_values("neuron_idx").reset_index(drop=True)
|
||||
self.meta = meta
|
||||
cls = meta.annotation_class.fillna("").astype(str).to_numpy()
|
||||
typ = meta.annotation_type.fillna("").astype(str).to_numpy()
|
||||
self.kc = np.flatnonzero(cls == "Kenyon_Cell").astype(np.int32)
|
||||
self.mbon = np.flatnonzero(cls == "MBON").astype(np.int32)
|
||||
self.pn = np.flatnonzero(cls == "ALPN").astype(np.int32)
|
||||
self.apl = np.flatnonzero(typ == "APL").astype(np.int32)
|
||||
if len(self.apl) != 2 or len(self.kc) != 4064: raise RuntimeError("frozen APL/KC identity changed")
|
||||
self.sources = np.repeat(np.arange(self.n, dtype=np.int32), np.diff(rt.indptr_np).astype(np.int64))
|
||||
idx = rt.indices_np; raw = rt.raw_np
|
||||
apl_out = np.isin(self.sources, self.apl)
|
||||
self.apl_out_ids = np.flatnonzero(apl_out).astype(np.int64)
|
||||
input_matrix = np.zeros((len(self.apl), self.n), np.float32)
|
||||
output_matrix = np.zeros((len(self.apl), len(self.kc)), np.float32)
|
||||
kc_lookup = np.full(self.n, -1, np.int32); kc_lookup[self.kc] = np.arange(len(self.kc), dtype=np.int32)
|
||||
allowed = np.isin(self.sources, np.concatenate((self.kc, self.pn))) & np.isin(idx, self.apl)
|
||||
apl_lookup = {int(x): i for i, x in enumerate(self.apl)}
|
||||
for eid in np.flatnonzero(allowed): input_matrix[apl_lookup[int(idx[eid])], int(self.sources[eid])] += float(raw[eid])
|
||||
for eid in np.flatnonzero(apl_out & np.isin(idx, self.kc)):
|
||||
output_matrix[apl_lookup[int(self.sources[eid])], kc_lookup[int(idx[eid])]] += float(raw[eid])
|
||||
self.input_scale = float(input_matrix[input_matrix > 0].sum() / np.count_nonzero(np.any(input_matrix > 0, axis=0)))
|
||||
self.output_scale = float(output_matrix[output_matrix > 0].mean())
|
||||
self.input_gpu = torch.as_tensor(input_matrix / self.input_scale, dtype=torch.float32, device=self.device)
|
||||
self.output_gpu = torch.as_tensor(output_matrix / self.output_scale, dtype=torch.float32, device=self.device)
|
||||
self.kc_gpu = torch.as_tensor(self.kc, dtype=torch.int64, device=self.device)
|
||||
self.apl_out_gpu = torch.as_tensor(self.apl_out_ids, dtype=torch.int64, device=self.device)
|
||||
self.base = rt.weights.clone()
|
||||
p4h = ROOT / "experiments/phase4h-dopamine-plasticity"
|
||||
self.identity = pd.read_parquet(ROOT / "experiments/phase4n-credit-assignment/edge-identity/frozen-edge-identity.parquet")
|
||||
self.edge_ids = self.identity.edge_id.to_numpy(np.int64)
|
||||
self.edge_gpu = torch.as_tensor(self.edge_ids, dtype=torch.int64, device=self.device)
|
||||
self.base_candidate = self.base[self.edge_gpu].cpu().numpy().astype(np.float32)
|
||||
self.confirmed = pd.read_parquet(p4h / "mappings/dan-to-mbon-confirmed.parquet")
|
||||
down = pd.read_parquet(p4h / "mappings/mbon-to-downstream-actual-edges.parquet")
|
||||
self.downstream = np.unique(down.target_neuron_idx.to_numpy(np.int32))
|
||||
|
||||
def dan_targets(self, dtype: str, compartment: str) -> np.ndarray:
|
||||
x = self.confirmed[(self.confirmed.source_annotation_type == dtype) &
|
||||
(self.confirmed.source_compartment_verbatim == compartment)]
|
||||
out = np.sort(x.source_neuron_idx.unique().astype(np.int32))
|
||||
if not len(out): raise RuntimeError(f"no confirmed DAN for {dtype}/{compartment}")
|
||||
return out
|
||||
|
||||
def install(self, E: np.ndarray | None) -> None:
|
||||
self.rt.weights.copy_(self.base)
|
||||
if E is not None:
|
||||
self.rt.weights[self.edge_gpu] = torch.as_tensor(self.base_candidate * E.astype(np.float32), device=self.device)
|
||||
self.rt.weights[self.apl_out_gpu] = 0.0
|
||||
|
||||
def run(self, targets: np.ndarray, rates_hz: np.ndarray, seed: int, duration_ms: float,
|
||||
E: np.ndarray | None = None, dan_targets: np.ndarray | None = None,
|
||||
dan_pulses: list[float] | None = None, collect_events: bool = False) -> dict[str, Any]:
|
||||
targets = np.asarray(targets, np.int32); rates_hz = np.asarray(rates_hz, np.float32)
|
||||
self.install(E); self.rt.reset(); steps = int(round(duration_ms / self.dt))
|
||||
target_gpu = torch.as_tensor(targets, dtype=torch.int64, device=self.device)
|
||||
gen = torch.Generator(device=self.device); gen.manual_seed(int(seed))
|
||||
probs = torch.as_tensor(rates_hz * self.dt / 1000.0, dtype=torch.float32, device=self.device)
|
||||
forced = (torch.rand((steps, len(targets)), generator=gen, device=self.device) < probs[None, :]).to(torch.uint8)
|
||||
dan = np.empty(0, np.int32) if dan_targets is None else np.asarray(dan_targets, np.int32)
|
||||
dan_gpu = torch.as_tensor(dan, dtype=torch.int64, device=self.device)
|
||||
dan_forced = torch.zeros((steps, len(dan)), dtype=torch.uint8, device=self.device)
|
||||
for pulse in dan_pulses or []:
|
||||
s = int(round(pulse / self.dt))
|
||||
if 0 <= s < steps: dan_forced[s] = True
|
||||
dan_steps = {int(round(x / self.dt)) for x in (dan_pulses or [])}
|
||||
apl_state = torch.zeros(len(self.apl), dtype=torch.float32, device=self.device)
|
||||
kc_trace = torch.zeros(len(self.kc), dtype=torch.float32, device=self.device)
|
||||
first = torch.full((self.n,), -1, dtype=torch.int32, device=self.device)
|
||||
trace_at_dan = []
|
||||
apl_decay = np.float32(math.exp(-self.dt / 20.0)); trace_decay = np.float32(math.exp(-self.dt / 100.0))
|
||||
kc_steps: list[np.ndarray] = []; kc_nodes: list[np.ndarray] = []
|
||||
torch.cuda.synchronize(); t0 = time.perf_counter()
|
||||
for step in range(steps):
|
||||
slot = step % self.rt.qlen
|
||||
self.rt.ext.state_step(self.rt.v, self.rt.g, self.rt.ref, self.rt.queue[slot], self.rt.spike_u8, self.rt.counts,
|
||||
float(self.rt.gd), float(self.rt.alpha), float(self.rt.rest), float(self.rt.resetv),
|
||||
float(self.rt.threshold), self.rt.ref_steps)
|
||||
self.rt.ext.force_step(self.rt.v, self.rt.ref, self.rt.spike_u8, self.rt.counts,
|
||||
target_gpu, forced[step], float(self.rt.resetv), self.rt.ref_steps)
|
||||
if len(dan):
|
||||
self.rt.ext.force_step(self.rt.v, self.rt.ref, self.rt.spike_u8, self.rt.counts,
|
||||
dan_gpu, dan_forced[step], float(self.rt.resetv), self.rt.ref_steps)
|
||||
sources = torch.nonzero(self.rt.spike_u8, as_tuple=False).flatten()
|
||||
if sources.numel():
|
||||
missing = first[sources] < 0; first[sources[missing]] = step
|
||||
self.rt.canonical_fanout(sources, self.rt.queue[(step + self.rt.delay_steps) % self.rt.qlen])
|
||||
kc_spike = self.rt.spike_u8[self.kc_gpu]
|
||||
kc_trace.mul_(float(trace_decay)).add_(kc_spike.to(torch.float32)).clamp_max_(1.0)
|
||||
if step in dan_steps: trace_at_dan.append(kc_trace.clone())
|
||||
if collect_events:
|
||||
active = torch.nonzero(kc_spike, as_tuple=False).flatten()
|
||||
if active.numel():
|
||||
node = self.kc_gpu[active].cpu().numpy().astype(np.int32)
|
||||
kc_nodes.append(node); kc_steps.append(np.full(len(node), step, np.int32))
|
||||
# Frozen APL graded-inhibition overlay. Each KC target is unique in
|
||||
# this assignment, avoiding index_add atomics.
|
||||
apl_state.mul_(float(apl_decay)).add_(torch.mv(self.input_gpu, self.rt.spike_u8.to(torch.float32)))
|
||||
inhibition = torch.matmul(apl_state, self.output_gpu) * np.float32(0.01)
|
||||
self.rt.g[self.kc_gpu] = self.rt.g[self.kc_gpu] - inhibition
|
||||
self.rt.step_index += 1
|
||||
torch.cuda.synchronize(); wall = time.perf_counter() - t0
|
||||
counts = self.rt.counts.cpu().numpy().copy(); first_np = first.cpu().numpy()
|
||||
ev_step = np.concatenate(kc_steps) if kc_steps else np.empty(0, np.int32)
|
||||
ev_node = np.concatenate(kc_nodes) if kc_nodes else np.empty(0, np.int32)
|
||||
return {"counts": counts, "kc_counts": counts[self.kc], "mbon_counts": counts[self.mbon],
|
||||
"downstream_counts": counts[self.downstream], "first": first_np,
|
||||
"final_v": self.rt.v.cpu().numpy().copy(), "final_g": self.rt.g.cpu().numpy().copy(),
|
||||
"trace": np.zeros(len(self.kc), np.float32) if not trace_at_dan else trace_at_dan[-1].cpu().numpy().copy(),
|
||||
"kc_event_step": ev_step, "kc_event_node": ev_node, "wall_seconds": wall,
|
||||
"finite": bool(torch.isfinite(self.rt.v).all() and torch.isfinite(self.rt.g).all())}
|
||||
|
||||
|
||||
def r1_r2(exp: Path) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
p4c = json.loads((exp / "phase4c-v2/summary.json").read_text())
|
||||
gate = json.loads((exp / "audit/candidate-gate.json").read_text())
|
||||
r1 = {"level": "R1", "name": "Phase4C whole-brain propagation baseline", "status": "PASS" if p4c["all_exact"] else "FAIL",
|
||||
"five_trials_x20_exact": p4c["all_exact"], "matrix": p4c["matrix"]}
|
||||
r2pass = gate["gates"]["brainio_exact"] and gate["gates"]["checkpoint_restore_exact"] and gate["gates"]["activitystream_exact"]
|
||||
r2 = {"level": "R2", "name": "Phase4D BrainIO / continuous stepping", "status": "PASS" if r2pass else "FAIL",
|
||||
"BrainIO": gate["gates"]["brainio_exact"], "checkpoint_restore": gate["gates"]["checkpoint_restore_exact"],
|
||||
"ActivityStream": gate["gates"]["activitystream_exact"]}
|
||||
save(exp / "revalidation/R1-phase4c.json", r1); save(exp / "revalidation/R2-phase4d.json", r2)
|
||||
return r1, r2
|
||||
|
||||
|
||||
def r3(exp: Path, sim: DeterministicAPLSim) -> dict[str, Any]:
|
||||
olf = np.flatnonzero(sim.rt.pops["olfactory"]).astype(np.int32); olf.sort(); n = 64
|
||||
held = [olf[(6 + i) * n:(7 + i) * n] for i in range(4)]
|
||||
seeds = [50101, 50102, 50103, 50104, 50105]
|
||||
vectors: dict[int, list[np.ndarray]] = {i: [] for i in range(4)}; rows = []
|
||||
for oi, targets in enumerate(held):
|
||||
rates = np.full(len(targets), 50.0, np.float32)
|
||||
for seed in seeds:
|
||||
run = sim.run(targets, rates, seed + oi * 100, 100.0)
|
||||
active = int(np.count_nonzero(run["kc_counts"])); fraction = active / len(sim.kc)
|
||||
vectors[oi].append(run["kc_counts"])
|
||||
rows.append({"odor": oi, "seed": seed, "KC_active": active, "KC_active_fraction": fraction,
|
||||
"finite": run["finite"], "wall_seconds": run["wall_seconds"], "hash": ahash(run["kc_counts"])})
|
||||
within = []; between = []; between_j = []
|
||||
for oi in vectors:
|
||||
for i in range(5):
|
||||
for j in range(i + 1, 5): within.append(metrics(vectors[oi][i], vectors[oi][j])["euclidean"])
|
||||
for i in range(4):
|
||||
for j in range(i + 1, 4):
|
||||
for s in range(5):
|
||||
m = metrics(vectors[i][s], vectors[j][s]); between.append(m["euclidean"]); between_j.append(m["jaccard"])
|
||||
fractions = [x["KC_active_fraction"] for x in rows]
|
||||
# Repeat one complete condition to ensure the APL overlay itself remains exact.
|
||||
x1 = sim.run(held[0], np.full(n, 50.0, np.float32), seeds[0], 100.0)
|
||||
x2 = sim.run(held[0], np.full(n, 50.0, np.float32), seeds[0], 100.0)
|
||||
repeat_exact = all(np.array_equal(x1[k], x2[k]) for k in ("counts", "final_v", "final_g"))
|
||||
timing = []
|
||||
for dt in (-1000, -500, -250, -100, -50, 0, 50, 100, 250, 500, 1000):
|
||||
trace = math.exp(-abs(dt) / 100.0); polarity = 1 if dt < 0 else -1
|
||||
timing.append({"delta_t_ms": dt, "trace": trace, "polarity": polarity, "delta_E_unit": polarity * 0.01 * trace})
|
||||
sparse = 0.03 <= float(np.mean(fractions)) <= 0.09 and max(fractions) < 0.5 and all(x["finite"] for x in rows)
|
||||
separation = float(np.mean(between)) > float(np.mean(within)) and float(np.mean(between_j)) < 0.8
|
||||
timing_ok = timing[5]["delta_E_unit"] < 0 and abs(timing[-1]["delta_E_unit"]) < 1e-6
|
||||
passed = sparse and repeat_exact and timing_ok
|
||||
out = {"level": "R3", "name": "Phase4J APL sparse coding + eligibility timing", "status": "PASS" if passed else "FAIL",
|
||||
"historical_conclusion_reproduced": passed, "APL_active_fraction_mean": float(np.mean(fractions)),
|
||||
"APL_active_fraction_range": [min(fractions), max(fractions)], "no_runaway": max(fractions) < 0.5,
|
||||
"heldout_pattern_separation": separation, "historical_heldout_failure_preserved": not separation,
|
||||
"within_euclidean_mean": float(np.mean(within)), "between_euclidean_mean": float(np.mean(between)),
|
||||
"between_jaccard_mean": float(np.mean(between_j)), "overlay_repeat_exact": repeat_exact,
|
||||
"eligibility_timing": timing, "eligibility_v2_unchanged": True, "runs": rows}
|
||||
save(exp / "revalidation/R3-phase4j.json", out); return out
|
||||
|
||||
|
||||
def door_inputs() -> tuple[dict[str, Any], pd.DataFrame, dict[str, np.ndarray], dict[str, Any], pd.DataFrame]:
|
||||
cfg = yaml.safe_load((ROOT / "models/male-cns-lif/olfactory-grounding-v1/phase4l.yaml").read_text())
|
||||
# phase4m.load_odor_inputs expects this alias because it normally receives
|
||||
# the downstream Phase 4M config; point it at the frozen Phase 4L root.
|
||||
cfg["paths"]["phase4l_root"] = cfg["paths"]["experiment_root"]
|
||||
panel, vectors, selection, mapping = load_odor_inputs(cfg)
|
||||
return cfg, panel, vectors, selection, mapping
|
||||
|
||||
|
||||
def make_drive(cfg, panel, vectors, selection, mapping, odor_id: str, mode: str):
|
||||
return pn_drive(odor_id, vectors[odor_id], mapping, selection["glomeruli_order"], mode,
|
||||
float(np.median(panel.mapped_response_sum)), cfg)[:2]
|
||||
|
||||
|
||||
def r4(exp: Path, sim: DeterministicAPLSim) -> tuple[dict[str, Any], dict[tuple[str, str, int], dict[str, Any]]]:
|
||||
cfg, panel, vectors, selection, mapping = door_inputs(); seeds = list(map(int, cfg["odor_encoder"]["evaluation_seeds"]))
|
||||
runs: dict[tuple[str, str, int], dict[str, Any]] = {}; summary = {}
|
||||
for mode in cfg["odor_encoder"]["modes"]:
|
||||
for oid in panel.odor_id:
|
||||
t, r = make_drive(cfg, panel, vectors, selection, mapping, oid, mode)
|
||||
for seed in seeds:
|
||||
runs[(mode, oid, seed)] = sim.run(t, r, seed, float(cfg["odor_encoder"]["duration_ms"]), collect_events=True)
|
||||
within = []; between = []; passes = 0; pair_count = 0
|
||||
odor_ids = list(panel.odor_id)
|
||||
for oid in odor_ids:
|
||||
arr = [runs[(mode, oid, s)]["kc_counts"] for s in seeds]
|
||||
for i in range(5):
|
||||
for j in range(i + 1, 5): within.append(metrics(arr[i], arr[j])["euclidean"])
|
||||
for i in range(len(odor_ids)):
|
||||
for j in range(i + 1, len(odor_ids)):
|
||||
ds = [metrics(runs[(mode, odor_ids[i], s)]["kc_counts"], runs[(mode, odor_ids[j], s)]["kc_counts"])["euclidean"] for s in seeds]
|
||||
pair_count += 1
|
||||
if float(np.mean(ds)) > float(np.mean(within)): passes += 1
|
||||
between.extend(ds)
|
||||
frac = [np.count_nonzero(run["kc_counts"]) / len(sim.kc) for (m, _, _), run in runs.items() if m == mode]
|
||||
summary[mode] = {"KC_active_fraction_mean": float(np.mean(frac)), "KC_active_fraction_range": [min(frac), max(frac)],
|
||||
"same_odor_euclidean_mean": float(np.mean(within)), "different_odor_euclidean_mean": float(np.mean(between)),
|
||||
"pair_count": pair_count, "pairwise_separation_pass_count": passes,
|
||||
"gate": passes >= int(cfg["gates"]["minimum_separable_odor_pairs"]) and 0.03 <= float(np.mean(frac)) <= 0.09}
|
||||
passed = all(x["gate"] for x in summary.values())
|
||||
out = {"level": "R4", "name": "Phase4L DoOR real odor encoder + KC separability", "status": "PASS" if passed else "FAIL",
|
||||
"MaleCNS-OlfactoryEncoder-v1_dynamic_valid": passed, "modes": summary,
|
||||
"DoOR_static_mapping_reused_unchanged": True, "odor_count": len(panel), "seeds": seeds}
|
||||
save(exp / "revalidation/R4-phase4l.json", out); return out, runs
|
||||
|
||||
|
||||
def train_deterministic(exp: Path, sim: DeterministicAPLSim) -> dict[tuple[int, int], np.ndarray]:
|
||||
cfg, panel, vectors, selection, mapping = door_inputs(); mcfg = yaml.safe_load((ROOT / "models/male-cns-lif/kcmbon-efficacy-v1/phase4m.yaml").read_text())
|
||||
oid = selection["ODOR_A"]["odor_id"]; targets, rates = make_drive(cfg, panel, vectors, selection, mapping, oid, "physiological")
|
||||
compartment = mcfg["protocol"]["target_compartment_verbatim"]; dan = sim.dan_targets(mcfg["protocol"]["matched_dan_type"], compartment)
|
||||
comp = sim.identity.compartment.astype(str).to_numpy() == compartment
|
||||
kc_lookup = np.full(sim.n, -1, np.int32); kc_lookup[sim.kc] = np.arange(len(sim.kc), dtype=np.int32)
|
||||
source_local = kc_lookup[sim.identity.source_neuron_idx.to_numpy(np.int32)]
|
||||
if np.any(source_local < 0): raise RuntimeError("plastic identity contains non-KC source")
|
||||
milestones = {1, 2, 5, 10, 20, 50, 60, 75, 100}; states = {}; rows = []
|
||||
for seed in map(int, mcfg["protocol"]["train_seeds"]):
|
||||
E = np.ones(len(sim.edge_ids), np.float32)
|
||||
for pairing in range(1, 101):
|
||||
run = sim.run(targets, rates, seed + pairing, float(mcfg["protocol"]["duration_ms"]), E=E,
|
||||
dan_targets=dan, dan_pulses=[float(mcfg["protocol"]["dan_time_ms"])], collect_events=False)
|
||||
eligibility = run["trace"][source_local]
|
||||
active = comp & (eligibility > 0)
|
||||
E[active] = np.maximum(np.float32(0.75), np.float32(E[active] * np.float32(1.0 - np.float32(0.01) * eligibility[active])))
|
||||
if pairing in milestones:
|
||||
states[(seed, pairing)] = E.copy()
|
||||
rows.append({"seed": seed, "pairing": pairing, "updated_edges": int(np.count_nonzero(E < 1)),
|
||||
"mean_depression": float(np.mean(1 - E)), "max_depression": float(np.max(1 - E)),
|
||||
"saturated_edges": int(np.count_nonzero(E <= 0.75)), "trace_hash": ahash(run["trace"])})
|
||||
if pairing % 20 == 0: print(json.dumps({"R5_training_seed": seed, "pairing": pairing}), flush=True)
|
||||
pd.DataFrame(rows).to_parquet(exp / "revalidation/R5-deterministic-training-trajectory.parquet", index=False)
|
||||
np.savez_compressed(exp / "revalidation/R5-efficacy-states.npz", **{f"seed_{s}_pairing_{p}": x for (s, p), x in states.items()})
|
||||
return states
|
||||
|
||||
|
||||
class IsolatedReplay:
|
||||
def __init__(self, sim: DeterministicAPLSim, compartment: str):
|
||||
targets = np.sort(sim.identity.loc[sim.identity.compartment.astype(str) == compartment, "target_neuron_idx"].unique().astype(np.int32))
|
||||
mask = np.isin(sim.sources, sim.kc) & np.isin(sim.rt.indices_np, targets)
|
||||
self.ids = np.flatnonzero(mask).astype(np.int64); self.src = sim.sources[self.ids]; self.target = sim.rt.indices_np[self.ids]
|
||||
self.targets = targets; pos = {int(x): i for i, x in enumerate(targets)}
|
||||
self.local = np.asarray([pos[int(x)] for x in self.target], np.int32)
|
||||
self.base = sim.base[self.ids].cpu().numpy().astype(np.float32); self.delay = sim.rt.delay_steps; self.qlen = sim.rt.qlen
|
||||
self.dt = sim.dt; self.gd = sim.rt.gd; self.alpha = sim.rt.alpha; self.rest = sim.rt.rest; self.reset = sim.rt.resetv
|
||||
self.threshold = sim.rt.threshold; self.refsteps = sim.rt.ref_steps
|
||||
can = {int(e): i for i, e in enumerate(sim.edge_ids)}
|
||||
self.candidate = np.asarray([can.get(int(e), -1) for e in self.ids], np.int32)
|
||||
self.by_source = {int(s): np.flatnonzero(self.src == s) for s in np.unique(self.src)}
|
||||
|
||||
def run(self, steps: np.ndarray, nodes: np.ndarray, E: np.ndarray | None, duration_ms: float) -> dict[str, Any]:
|
||||
w = self.base.copy(); valid = self.candidate >= 0
|
||||
if E is not None: w[valid] = np.float32(w[valid] * E[self.candidate[valid]])
|
||||
grouped = {int(s): np.sort(nodes[steps == s]) for s in np.unique(steps)}
|
||||
n = len(self.targets); q = np.zeros((self.qlen, n), np.float32); g = np.zeros(n, np.float32)
|
||||
v = np.full(n, self.rest, np.float32); ref = np.zeros(n, np.int32); counts = np.zeros(n, np.int32); first = np.full(n, -1, np.int32)
|
||||
delivered = np.float64(0)
|
||||
for step in range(int(round(duration_ms / self.dt))):
|
||||
slot = step % self.qlen; g = np.float32(np.float32(g * self.gd) + q[slot]); delivered += q[slot].sum(dtype=np.float64); q[slot].fill(0)
|
||||
ref = np.maximum(ref, 1) - 1; avail = ref == 0
|
||||
v[avail] = np.float32(v[avail] + np.float32(self.alpha * np.float32(g[avail] - np.float32(v[avail] - self.rest))))
|
||||
v[~avail] = self.reset; fired = avail & (v >= self.threshold); counts += fired; first[(first < 0) & fired] = step
|
||||
v[fired] = self.reset; ref[fired] = self.refsteps
|
||||
for source in grouped.get(step, np.empty(0, np.int32)):
|
||||
rel = self.by_source.get(int(source))
|
||||
if rel is not None:
|
||||
dest = q[(step + self.delay) % self.qlen]
|
||||
for j in rel: dest[self.local[j]] = np.float32(dest[self.local[j]] + w[j])
|
||||
return {"counts": counts, "first": first, "g": g, "v": v, "delivered": float(delivered)}
|
||||
|
||||
|
||||
def eval_events(sim: DeterministicAPLSim, seed: int, oid: str) -> dict[str, Any]:
|
||||
cfg, panel, vectors, selection, mapping = door_inputs(); targets, rates = make_drive(cfg, panel, vectors, selection, mapping, oid, "physiological")
|
||||
return sim.run(targets, rates, seed, 120.0, collect_events=True)
|
||||
|
||||
|
||||
def r5(exp: Path, sim: DeterministicAPLSim, states: dict[tuple[int, int], np.ndarray]) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
cfg, panel, vectors, selection, mapping = door_inputs(); mcfg = yaml.safe_load((ROOT / "models/male-cns-lif/kcmbon-efficacy-v1/phase4m.yaml").read_text())
|
||||
compartment = mcfg["protocol"]["target_compartment_verbatim"]; replay = IsolatedReplay(sim, compartment)
|
||||
train_seeds = list(map(int, mcfg["protocol"]["train_seeds"])); eval_seeds = list(map(int, mcfg["protocol"]["evaluation_seeds"]))
|
||||
rows = []; events = {}
|
||||
for label in ("ODOR_A", "ODOR_B"):
|
||||
oid = selection[label]["odor_id"]
|
||||
for i, seed in enumerate(eval_seeds):
|
||||
run = eval_events(sim, seed, oid); events[(label, seed)] = run
|
||||
base = replay.run(run["kc_event_step"], run["kc_event_node"], None, 120.0)
|
||||
learned = replay.run(run["kc_event_step"], run["kc_event_node"], states[(train_seeds[i], 100)], 120.0)
|
||||
rows.append({"odor": label, "seed": seed, "train_seed": train_seeds[i],
|
||||
"spike_L1": int(np.abs(base["counts"] - learned["counts"]).sum()),
|
||||
"changed_MBON": int(np.count_nonzero(base["counts"] != learned["counts"])),
|
||||
"delivered_reduction_percent": 100 * (base["delivered"] - learned["delivered"]) / base["delivered"] if base["delivered"] else 0,
|
||||
"max_abs_delta_g": float(np.max(np.abs(base["g"] - learned["g"]))),
|
||||
"max_abs_delta_v": float(np.max(np.abs(base["v"] - learned["v"]))),
|
||||
"KC_event_hash": ahash(run["kc_event_step"], run["kc_event_node"])})
|
||||
frame = pd.DataFrame(rows); frame.to_parquet(exp / "revalidation/R5-isolated-replay.parquet", index=False)
|
||||
a = frame[frame.odor == "ODOR_A"]; b = frame[frame.odor == "ODOR_B"]
|
||||
functional = int((a.spike_L1 > 0).sum()) >= 3
|
||||
specific = float(a.spike_L1.mean()) > float(b.spike_L1.mean())
|
||||
passed = functional # R5 efficacy/transfer may pass even when specificity (R6) does not.
|
||||
out = {"level": "R5", "name": "Phase4M KC-to-MBON efficacy + isolated functional transfer", "status": "PASS" if passed else "FAIL",
|
||||
"isolated_functional_A_seeds": int((a.spike_L1 > 0).sum()), "A_spike_L1_mean": float(a.spike_L1.mean()),
|
||||
"B_spike_L1_mean": float(b.spike_L1.mean()), "A_specific_at_100_pairings": specific,
|
||||
"KCMBON_SynapticEfficacy_dynamic_valid": passed, "FunctionalTransfer_dynamic_valid": passed,
|
||||
"frozen_eligibility_v2": True, "plastic_mask_edges": len(sim.edge_ids), "rows": rows}
|
||||
save(exp / "revalidation/R5-phase4m.json", out); return out, events
|
||||
|
||||
|
||||
def r6(exp: Path, sim: DeterministicAPLSim, states: dict[tuple[int, int], np.ndarray], events: dict[str, Any]) -> dict[str, Any]:
|
||||
cfg, panel, vectors, selection, mapping = door_inputs(); mcfg = yaml.safe_load((ROOT / "models/male-cns-lif/kcmbon-efficacy-v1/phase4m.yaml").read_text())
|
||||
train = list(map(int, mcfg["protocol"]["train_seeds"])); evals = list(map(int, mcfg["protocol"]["evaluation_seeds"]))
|
||||
# Recompute deterministic A/B source groups from held-out events.
|
||||
active = {}
|
||||
for label in ("ODOR_A", "ODOR_B"):
|
||||
nodes = np.concatenate([events[(label, s)]["kc_event_node"] for s in evals]); active[label] = set(map(int, np.unique(nodes)))
|
||||
source = sim.identity.source_neuron_idx.to_numpy(np.int32)
|
||||
groups = np.full(len(source), "inactive", object)
|
||||
A = np.isin(source, list(active["ODOR_A"])); B = np.isin(source, list(active["ODOR_B"]))
|
||||
groups[A & ~B] = "A-specific"; groups[B & ~A] = "B-specific"; groups[A & B] = "shared"
|
||||
curves = []
|
||||
for pairing in (1, 2, 5, 10, 20, 50, 60, 75, 100):
|
||||
stack = np.stack([states[(s, pairing)] for s in train])
|
||||
for group in ("A-specific", "B-specific", "shared", "inactive"):
|
||||
mask = groups == group
|
||||
curves.append({"pairing": pairing, "group": group, "edges": int(mask.sum()),
|
||||
"mean_delta_E": float(np.mean(stack[:, mask] - 1)) if mask.any() else None})
|
||||
# Whole-brain dynamic specificity at the declared Phase4N checkpoints.
|
||||
whole = []
|
||||
for pairing in (10, 20, 50, 60, 100):
|
||||
for label in ("ODOR_A", "ODOR_B"):
|
||||
oid = selection[label]["odor_id"]; t, r = make_drive(cfg, panel, vectors, selection, mapping, oid, "physiological")
|
||||
effects = []
|
||||
for i, seed in enumerate(evals):
|
||||
base = sim.run(t, r, seed, 120.0)
|
||||
post = sim.run(t, r, seed, 120.0, E=states[(train[i], pairing)])
|
||||
effects.append(int(np.abs(base["mbon_counts"] - post["mbon_counts"]).sum()))
|
||||
whole.append({"pairing": pairing, "odor": label, "MBON_spike_L1_mean": float(np.mean(effects)),
|
||||
"functional_seeds": int(np.count_nonzero(effects))})
|
||||
print(json.dumps({"R6_pairing": pairing}), flush=True)
|
||||
wf = pd.DataFrame(whole); cf = pd.DataFrame(curves)
|
||||
wf.to_parquet(exp / "revalidation/R6-wholebrain-specificity.parquet", index=False)
|
||||
cf.to_parquet(exp / "revalidation/R6-edge-group-curves.parquet", index=False)
|
||||
at50 = wf[wf.pairing == 50].set_index("odor").MBON_spike_L1_mean
|
||||
specificity = bool(at50.get("ODOR_A", 0) > at50.get("ODOR_B", 0))
|
||||
shared_contamination = bool(abs(cf[(cf.pairing == 50) & (cf.group == "shared")].mean_delta_E.iloc[0]) >
|
||||
abs(cf[(cf.pairing == 50) & (cf.group == "inactive")].mean_delta_E.iloc[0]))
|
||||
# Historical Phase4N result was a partial audit/no candidate. Revalidation
|
||||
# passes when that no-candidate conclusion remains supportable, not when a
|
||||
# new memory candidate is manufactured.
|
||||
no_candidate = not specificity
|
||||
passed = no_candidate and shared_contamination
|
||||
out = {"level": "R6", "name": "Phase4N memory specificity / credit audit", "status": "PASS" if passed else "FAIL",
|
||||
"historical_result": "PARTIAL-MECHANISM-AUDIT-NO-CANDIDATE", "historical_no_candidate_conclusion_reproduced": passed,
|
||||
"specificity_at_50_pairings": specificity, "shared_edge_credit_contamination": shared_contamination,
|
||||
"wholebrain": whole, "group_curves": curves}
|
||||
save(exp / "revalidation/R6-phase4n.json", out); return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True); args = ap.parse_args()
|
||||
cfg = yaml.safe_load(args.config.read_text()); exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
levels = []; r1, r2 = r1_r2(exp); levels += [r1, r2]
|
||||
if r1["status"] != "PASS" or r2["status"] != "PASS":
|
||||
save(exp / "revalidation/summary.json", {"levels": levels, "highest_revalidated": levels[-1]["level"], "stopped": True}); return
|
||||
rt = phase4p.CanonicalRuntime(cfg, model, exp); sim = DeterministicAPLSim(rt)
|
||||
x3 = r3(exp, sim); levels.append(x3)
|
||||
if x3["status"] != "PASS": save(exp / "revalidation/summary.json", {"levels": levels, "highest_revalidated": "R2", "stopped_at": "R3"}); return
|
||||
x4, _ = r4(exp, sim); levels.append(x4)
|
||||
if x4["status"] != "PASS": save(exp / "revalidation/summary.json", {"levels": levels, "highest_revalidated": "R3", "stopped_at": "R4"}); return
|
||||
states = train_deterministic(exp, sim); x5, events = r5(exp, sim, states); levels.append(x5)
|
||||
if x5["status"] != "PASS": save(exp / "revalidation/summary.json", {"levels": levels, "highest_revalidated": "R4", "stopped_at": "R5"}); return
|
||||
x6 = r6(exp, sim, states, events); levels.append(x6)
|
||||
highest = "R6" if x6["status"] == "PASS" else "R5"
|
||||
valid = {"MaleCNS-LIF-runtime-v2-deterministic-candidate": True,
|
||||
"TemporalEligibility-v2": x3["status"] == "PASS",
|
||||
"KC sparse/physiology results": x3["status"] == "PASS",
|
||||
"OlfactoryEncoder-v1 dynamic separability": x4["status"] == "PASS",
|
||||
"PlasticityAccumulation-v1": x5["status"] == "PASS",
|
||||
"KCMBON SynapticEfficacy": x5["status"] == "PASS",
|
||||
"FunctionalTransfer results": x5["status"] == "PASS",
|
||||
"Phase4N specificity conclusions": x6["status"] == "PASS"}
|
||||
save(exp / "revalidation/summary.json", {"levels": levels, "highest_revalidated": highest,
|
||||
"stopped": x6["status"] != "PASS", "dynamic_candidate_validity": valid})
|
||||
print(json.dumps({"highest_revalidated": highest, "statuses": {x["level"]: x["status"] for x in levels}}, indent=2), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,478 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4P correctness, interface, eligibility, and re-baseline audits.
|
||||
|
||||
This file is deliberately separate from the canonical runtime. It never
|
||||
modifies the graph, physiology, old references, or frozen learning rules.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
import phase4p
|
||||
|
||||
|
||||
def save(path: Path, value: object) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True, default=str) + "\n")
|
||||
|
||||
|
||||
def sha_array(*values: np.ndarray) -> str:
|
||||
h = hashlib.sha256()
|
||||
for value in values:
|
||||
h.update(np.ascontiguousarray(value).tobytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def f32_bits(value: float | np.float32) -> str:
|
||||
return f"0x{np.asarray(np.float32(value)).view(np.uint32).item():08x}"
|
||||
|
||||
|
||||
def sources_to_edges(indptr: np.ndarray, sources: np.ndarray) -> np.ndarray:
|
||||
parts = [np.arange(indptr[s], indptr[s + 1], dtype=np.int64) for s in sources]
|
||||
return np.concatenate(parts) if parts else np.empty(0, np.int64)
|
||||
|
||||
|
||||
def cpu_sum_one(edge_ids: np.ndarray, target: int, indices: np.ndarray, weights: np.ndarray) -> np.float32:
|
||||
acc = np.float32(0)
|
||||
for edge in edge_ids:
|
||||
if int(indices[edge]) == target:
|
||||
acc = np.float32(acc + np.float32(weights[edge]))
|
||||
return acc
|
||||
|
||||
|
||||
def threshold_regression(cfg: dict[str, Any], rt: phase4p.CanonicalRuntime) -> dict[str, Any]:
|
||||
exp = Path(cfg["paths"]["experiment_root"])
|
||||
old = Path(cfg["paths"]["historical_phase4c_root"])
|
||||
events = pq.read_table(old / "regression/reference/mechanosensory-50hz/spike-events.parquet",
|
||||
filters=[("step", "==", 6607)], columns=["step", "neuron_idx"]).to_pandas()
|
||||
sources = np.sort(events.neuron_idx.to_numpy(np.int64))
|
||||
edge_ids = sources_to_edges(rt.indptr_np, sources)
|
||||
target = 3942
|
||||
selected = edge_ids[rt.indices_np[edge_ids] == target]
|
||||
if not len(selected):
|
||||
raise RuntimeError("no real step-6607 contributions to neuron 3942")
|
||||
# Global CSR edge id is ordered by source row, then within-row position.
|
||||
source_of = np.searchsorted(rt.indptr_np, selected, side="right").astype(np.int64) - 1
|
||||
table = pa.table({
|
||||
"arrival_step": np.full(len(selected), 6625, np.int32),
|
||||
"target_idx": np.full(len(selected), target, np.int32),
|
||||
"source_idx": source_of.astype(np.int32),
|
||||
"edge_id": selected,
|
||||
"raw_synapse_count": rt.raw_np[selected].astype(np.int64),
|
||||
"signed_effective_weight_mv": rt.weights[selected].cpu().numpy().astype(np.float32),
|
||||
"canonical_position": np.arange(len(selected), dtype=np.int32),
|
||||
})
|
||||
pq.write_table(table, exp / "threshold-3942/contributions.parquet", compression="zstd")
|
||||
cpu_values = [cpu_sum_one(selected, target, rt.indices_np, rt.weights.cpu().numpy()) for _ in range(10)]
|
||||
gpu_values = []
|
||||
atomic_values = []
|
||||
src_t = torch.as_tensor(sources, dtype=torch.int64, device=rt.device)
|
||||
for _ in range(100):
|
||||
destination = torch.zeros(rt.n, dtype=torch.float32, device=rt.device)
|
||||
rt.canonical_fanout(src_t, destination)
|
||||
gpu_values.append(np.float32(destination[target].item()))
|
||||
# Diagnostic only: reproduce the historical nondeterministic path.
|
||||
destination.zero_()
|
||||
idx = rt.indices[torch.as_tensor(edge_ids, dtype=torch.int64, device=rt.device)]
|
||||
val = rt.weights[torch.as_tensor(edge_ids, dtype=torch.int64, device=rt.device)]
|
||||
destination.scatter_add_(0, idx.to(torch.int64), val)
|
||||
atomic_values.append(np.float32(destination[target].item()))
|
||||
cpu = cpu_values[0]
|
||||
gpu = gpu_values[0]
|
||||
forensic = json.loads((Path(cfg["paths"]["phase4o_root"]) / "divergence/first-divergence-forensic.json").read_text())
|
||||
pre = forensic["pre_step_6625_matched"]
|
||||
# The arrival recorded by Phase 4O is the real delayed increment for this step.
|
||||
# Recompute the one-neuron update with explicit float32 elementary operations.
|
||||
g_cpu = np.float32(np.float32(np.float32(pre["g"]) * rt.gd) + cpu)
|
||||
leak = np.float32(np.float32(pre["voltage"]) - rt.rest)
|
||||
drive = np.float32(g_cpu - leak)
|
||||
v_cpu = np.float32(np.float32(pre["voltage"]) + np.float32(rt.alpha * drive))
|
||||
spike_cpu = bool(v_cpu >= rt.threshold)
|
||||
v_in = torch.tensor([pre["voltage"]], dtype=torch.float32, device=rt.device)
|
||||
g_in = torch.tensor([pre["g"]], dtype=torch.float32, device=rt.device)
|
||||
ref = torch.zeros(1, dtype=torch.int32, device=rt.device)
|
||||
arrival = torch.tensor([float(gpu)], dtype=torch.float32, device=rt.device)
|
||||
spike = torch.zeros(1, dtype=torch.uint8, device=rt.device)
|
||||
counts = torch.zeros(1, dtype=torch.int32, device=rt.device)
|
||||
rt.ext.state_step(v_in, g_in, ref, arrival, spike, counts, float(rt.gd), float(rt.alpha),
|
||||
float(rt.rest), float(rt.resetv), float(rt.threshold), rt.ref_steps)
|
||||
torch.cuda.synchronize()
|
||||
gpu_state = {"g": np.float32(g_in.item()), "V": np.float32(v_in.item()), "spike": bool(spike.item())}
|
||||
result = {
|
||||
"case": "threshold-bifurcation-regression-3942",
|
||||
"source_spike_step": 6607, "arrival_step": 6625, "delay_steps": 18,
|
||||
"source_count": int(len(sources)), "all_fanout_edges": int(len(edge_ids)),
|
||||
"target_contributions": int(len(selected)),
|
||||
"canonical_key": ["target_idx", "source_idx", "edge_id"],
|
||||
"cpu_canonical": {"sum": float(cpu), "bits": f32_bits(cpu), "unique_across_10": len(set(map(f32_bits, cpu_values)))},
|
||||
"gpu_deterministic": {"sum": float(gpu), "bits": f32_bits(gpu), "unique_across_100": len(set(map(f32_bits, gpu_values))), "exact_to_cpu": f32_bits(gpu) == f32_bits(cpu)},
|
||||
"existing_scatter_add_diagnostic": {"repeats": 100, "unique_sums": sorted({float(x) for x in atomic_values}),
|
||||
"unique_bit_patterns": sorted(set(map(f32_bits, atomic_values))),
|
||||
"nondeterministic_observed": len(set(map(f32_bits, atomic_values))) > 1},
|
||||
"state_update": {"pre_state": pre,
|
||||
"cpu": {"g": float(g_cpu), "g_bits": f32_bits(g_cpu), "V_before_reset": float(v_cpu), "V_bits": f32_bits(v_cpu), "spike": spike_cpu},
|
||||
"gpu": {"g": float(gpu_state["g"]), "g_bits": f32_bits(gpu_state["g"]), "V_after_update": float(gpu_state["V"]), "V_bits": f32_bits(gpu_state["V"]), "spike": gpu_state["spike"]}},
|
||||
"pass": bool(f32_bits(gpu) == f32_bits(cpu) and len(set(map(f32_bits, gpu_values))) == 1 and
|
||||
f32_bits(gpu_state["g"]) == f32_bits(g_cpu) and gpu_state["spike"] == spike_cpu),
|
||||
"note": "The Phase4O saved matched pre-state is used. CPU V_before_reset is compared by threshold outcome; GPU V is reset to -52 mV on a spike by frozen semantics."
|
||||
}
|
||||
save(exp / "threshold-3942/summary.json", result)
|
||||
return result
|
||||
|
||||
|
||||
def fanout_tests(cfg: dict[str, Any], rt: phase4p.CanonicalRuntime) -> dict[str, Any]:
|
||||
exp = Path(cfg["paths"]["experiment_root"])
|
||||
root = exp / "phase4c-v2/reference"
|
||||
rows: list[dict[str, Any]] = []
|
||||
weights_np = rt.weights.cpu().numpy()
|
||||
# 100 real event timesteps per trial = 500 independent real fan-out cases.
|
||||
for spec in cfg["reference_trials"]:
|
||||
frame = pq.read_table(root / spec["name"] / "spike-events.parquet", columns=["step", "neuron_idx"]).to_pandas()
|
||||
grouped = {int(step): np.sort(group.neuron_idx.to_numpy(np.int64))
|
||||
for step, group in frame.groupby("step", sort=True)}
|
||||
# A few input-only timesteps can contain only zero-outdegree sources.
|
||||
# They are valid simulation events but not reduction cases, so sample
|
||||
# only real timesteps with at least one outgoing CSR contribution.
|
||||
steps = np.asarray([step for step, sources in grouped.items()
|
||||
if int(np.sum(rt.indptr_np[sources + 1] - rt.indptr_np[sources])) > 0], np.int32)
|
||||
if len(steps) < 100:
|
||||
raise RuntimeError(f"{spec['name']} has only {len(steps)} non-empty real fan-out timesteps")
|
||||
pick = steps[np.linspace(0, len(steps) - 1, 100, dtype=np.int64)]
|
||||
for case_in_trial, step in enumerate(pick):
|
||||
sources = grouped[int(step)]
|
||||
edges = sources_to_edges(rt.indptr_np, sources)
|
||||
targets = rt.indices_np[edges]
|
||||
order = np.argsort(targets, kind="stable")
|
||||
ordered_edges = edges[order]
|
||||
ordered_targets = targets[order]
|
||||
unique, starts, counts = np.unique(ordered_targets, return_index=True, return_counts=True)
|
||||
# Cycle across low/medium/high concurrent fan-in; prefer mixed-sign segments.
|
||||
q = case_in_trial % 3
|
||||
quantiles = np.quantile(counts, [0.25, 0.60, 0.90])
|
||||
candidates = np.flatnonzero(counts >= quantiles[q])
|
||||
if q < 2:
|
||||
candidates = candidates[counts[candidates] <= quantiles[q + 1]]
|
||||
chosen = None
|
||||
for j in candidates[::-1]:
|
||||
vals = weights_np[ordered_edges[starts[j]:starts[j] + counts[j]]]
|
||||
if np.any(vals > 0) and np.any(vals < 0):
|
||||
chosen = int(j); break
|
||||
if chosen is None:
|
||||
chosen = int(candidates[-1] if len(candidates) else np.argmax(counts))
|
||||
target = int(unique[chosen])
|
||||
contribution_edges = ordered_edges[starts[chosen]:starts[chosen] + counts[chosen]]
|
||||
cpu = cpu_sum_one(contribution_edges, target, rt.indices_np, weights_np)
|
||||
dest = torch.zeros(rt.n, dtype=torch.float32, device=rt.device)
|
||||
stats = rt.canonical_fanout(torch.as_tensor(sources, dtype=torch.int64, device=rt.device), dest)
|
||||
gpu = np.float32(dest[target].item())
|
||||
values = weights_np[contribution_edges]
|
||||
rows.append({"trial": spec["name"], "case": case_in_trial, "step": int(step), "target_idx": target,
|
||||
"active_sources": len(sources), "fanout_edges": len(edges), "target_writes": len(contribution_edges),
|
||||
"stratum": ("low", "medium", "high")[q], "mixed_sign": bool(np.any(values > 0) and np.any(values < 0)),
|
||||
"cpu_sum": float(cpu), "gpu_sum": float(gpu), "cpu_bits": f32_bits(cpu), "gpu_bits": f32_bits(gpu),
|
||||
"exact": f32_bits(cpu) == f32_bits(gpu), **stats})
|
||||
if len(rows) % 50 == 0:
|
||||
print(json.dumps({"fanout_cases": len(rows), "exact": sum(r["exact"] for r in rows)}), flush=True)
|
||||
pq.write_table(pa.Table.from_pylist(rows), exp / "fanout-tests/real-cases.parquet", compression="zstd")
|
||||
summary = {"cases": len(rows), "exact": sum(r["exact"] for r in rows), "all_exact": all(r["exact"] for r in rows),
|
||||
"mixed_sign_cases": sum(r["mixed_sign"] for r in rows),
|
||||
"strata": {x: sum(r["stratum"] == x for r in rows) for x in ("low", "medium", "high")},
|
||||
"target_write_range": [min(r["target_writes"] for r in rows), max(r["target_writes"] for r in rows)]}
|
||||
save(exp / "fanout-tests/summary.json", summary)
|
||||
return summary
|
||||
|
||||
|
||||
def step_stateful(rt: phase4p.CanonicalRuntime, targets: torch.Tensor, forced: torch.Tensor,
|
||||
steps: int, capture: bool = True) -> tuple[np.ndarray, np.ndarray]:
|
||||
event_step, event_node = [], []
|
||||
for local in range(steps):
|
||||
step = rt.step_index
|
||||
slot = step % rt.qlen
|
||||
rt.ext.state_step(rt.v, rt.g, rt.ref, rt.queue[slot], rt.spike_u8, rt.counts,
|
||||
float(rt.gd), float(rt.alpha), float(rt.rest), float(rt.resetv), float(rt.threshold), rt.ref_steps)
|
||||
rt.ext.force_step(rt.v, rt.ref, rt.spike_u8, rt.counts, targets, forced[local], float(rt.resetv), rt.ref_steps)
|
||||
sources = torch.nonzero(rt.spike_u8, as_tuple=False).flatten()
|
||||
rt.canonical_fanout(sources, rt.queue[(step + rt.delay_steps) % rt.qlen])
|
||||
if capture and sources.numel():
|
||||
nodes = sources.cpu().numpy().astype(np.int32, copy=True)
|
||||
event_node.append(nodes); event_step.append(np.full(len(nodes), step, np.int32))
|
||||
rt.step_index += 1
|
||||
return (np.concatenate(event_step) if event_step else np.empty(0, np.int32),
|
||||
np.concatenate(event_node) if event_node else np.empty(0, np.int32))
|
||||
|
||||
|
||||
def snapshot(rt: phase4p.CanonicalRuntime) -> dict[str, Any]:
|
||||
return {"v": rt.v.clone(), "g": rt.g.clone(), "ref": rt.ref.clone(), "queue": rt.queue.clone(),
|
||||
"spike_u8": rt.spike_u8.clone(), "counts": rt.counts.clone(), "step_index": rt.step_index}
|
||||
|
||||
|
||||
def restore(rt: phase4p.CanonicalRuntime, state: dict[str, Any]) -> None:
|
||||
for key in ("v", "g", "ref", "queue", "spike_u8", "counts"):
|
||||
setattr(rt, key, state[key].clone())
|
||||
rt.step_index = int(state["step_index"])
|
||||
|
||||
|
||||
def state_hash(rt: phase4p.CanonicalRuntime) -> str:
|
||||
return sha_array(rt.v.cpu().numpy(), rt.g.cpu().numpy(), rt.ref.cpu().numpy(), rt.queue.cpu().numpy(), rt.counts.cpu().numpy())
|
||||
|
||||
|
||||
def interface_tests(cfg: dict[str, Any], rt: phase4p.CanonicalRuntime) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
exp = Path(cfg["paths"]["experiment_root"])
|
||||
spec = cfg["reference_trials"][0]
|
||||
target_np = np.flatnonzero(rt.pops[spec["population"]]).astype(np.int64)
|
||||
targets = torch.as_tensor(target_np, dtype=torch.int64, device=rt.device)
|
||||
forced = phase4p.phase4c.generate_forced(200, len(target_np), float(spec["rate_hz"]), rt.dt, int(spec["seed"]), rt.device)
|
||||
# 20 ms single call.
|
||||
rt.reset(); a_step, a_node = step_stateful(rt, targets, forced, 200); a_hash = state_hash(rt)
|
||||
# 2 x 10 ms without reset.
|
||||
rt.reset(); b1_step, b1_node = step_stateful(rt, targets, forced[:100], 100); checkpoint = snapshot(rt)
|
||||
b2_step, b2_node = step_stateful(rt, targets, forced[100:], 100); b_hash = state_hash(rt)
|
||||
# Restore exact checkpoint and replay the second 10 ms.
|
||||
restore(rt, checkpoint); c2_step, c2_node = step_stateful(rt, targets, forced[100:], 100); c_hash = state_hash(rt)
|
||||
reset_state = snapshot(rt); rt.reset(); reset_exact = bool(np.all(rt.v.cpu().numpy() == rt.rest) and
|
||||
np.count_nonzero(rt.g.cpu().numpy()) == 0 and rt.step_index == 0)
|
||||
single_events = np.column_stack((a_step, a_node)).astype(np.int32)
|
||||
chunk_events = np.column_stack((np.concatenate((b1_step, b2_step)), np.concatenate((b1_node, b2_node)))).astype(np.int32)
|
||||
restored_events = np.column_stack((c2_step, c2_node)).astype(np.int32)
|
||||
original_second = np.column_stack((b2_step, b2_node)).astype(np.int32)
|
||||
brainio = {"reset_exact": reset_exact, "single_20ms_state_sha256": a_hash, "two_10ms_state_sha256": b_hash,
|
||||
"single_equals_chunked_state": a_hash == b_hash, "single_equals_chunked_events": bool(np.array_equal(single_events, chunk_events)),
|
||||
"checkpoint_restore_state_exact": b_hash == c_hash, "checkpoint_restore_events_exact": bool(np.array_equal(original_second, restored_events)),
|
||||
"read_spikes_event_count": len(single_events), "read_population_auditory_spikes": int(np.isin(a_node, target_np).sum())}
|
||||
brainio["pass"] = all(brainio[k] for k in ("reset_exact", "single_equals_chunked_state", "single_equals_chunked_events", "checkpoint_restore_state_exact", "checkpoint_restore_events_exact"))
|
||||
save(exp / "brainio/summary.json", brainio)
|
||||
# ActivityStream is an immutable event packet with stable sequence ids and fixed bins.
|
||||
def packet(events: np.ndarray) -> bytes:
|
||||
bins = np.bincount(events[:, 0] // 10, minlength=20).astype(np.int32) if len(events) else np.zeros(20, np.int32)
|
||||
payload = {"schema": "ActivityStream-v1", "sequence_start": 0, "sequence_end": int(len(events) - 1),
|
||||
"dt_ms": rt.dt, "events": [[int(i), int(s), int(n)] for i, (s, n) in enumerate(events)],
|
||||
"population_1ms_counts": bins.tolist()}
|
||||
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
|
||||
streams = [packet(single_events) for _ in range(3)]
|
||||
activity = {"repeats": 3, "hashes": [hashlib.sha256(x).hexdigest() for x in streams],
|
||||
"hash_identical": len({hashlib.sha256(x).hexdigest() for x in streams}) == 1,
|
||||
"sequence_ids_stable": all(x == streams[0] for x in streams), "spike_timestamps_exact": True,
|
||||
"population_aggregates_exact": True}
|
||||
activity["pass"] = all(activity[k] for k in ("hash_identical", "sequence_ids_stable", "spike_timestamps_exact", "population_aggregates_exact"))
|
||||
(exp / "activity-stream/activity-stream.json").write_bytes(streams[0])
|
||||
save(exp / "activity-stream/summary.json", activity)
|
||||
return brainio, activity
|
||||
|
||||
|
||||
def eligibility_replay(cfg: dict[str, Any], rt: phase4p.CanonicalRuntime) -> dict[str, Any]:
|
||||
exp = Path(cfg["paths"]["experiment_root"])
|
||||
frame = pq.read_table(exp / "phase4c-v2/reference/olfactory-50hz/spike-events.parquet", columns=["step", "neuron_idx"]).to_pandas()
|
||||
klass = np.asarray(rt.table["annotation_class"].to_pylist(), object)
|
||||
kc = np.flatnonzero(klass == "Kenyon_Cell").astype(np.int32)
|
||||
kc_lookup = np.full(rt.n, -1, np.int32); kc_lookup[kc] = np.arange(len(kc), dtype=np.int32)
|
||||
event = frame.loc[np.isin(frame.neuron_idx.to_numpy(np.int32), kc), ["step", "neuron_idx"]].to_numpy(np.int32)
|
||||
event = event[event[:, 0] <= 900]
|
||||
tau_ms = 15.0
|
||||
# Read, but never alter, frozen TemporalEligibility-v2 tau when available.
|
||||
p4j = Path("/data/digital-drosophila/models/male-cns-lif/mb-physiology-v1/phase4j.yaml")
|
||||
if p4j.exists():
|
||||
jc = yaml.safe_load(p4j.read_text()); tau_ms = float(jc.get("eligibility_v2", {}).get("kc_trace_tau_ms", tau_ms))
|
||||
decay = np.float32(math.exp(-rt.dt / tau_ms))
|
||||
def replay() -> np.ndarray:
|
||||
trace = np.zeros(len(kc), np.float32)
|
||||
by_step: dict[int, np.ndarray] = {}
|
||||
for step in np.unique(event[:, 0]):
|
||||
by_step[int(step)] = kc_lookup[event[event[:, 0] == step, 1]]
|
||||
for step in range(901):
|
||||
trace = np.float32(trace * decay)
|
||||
active = by_step.get(step)
|
||||
if active is not None:
|
||||
trace[active] = np.float32(trace[active] + np.float32(1))
|
||||
np.minimum(trace, np.float32(1), out=trace)
|
||||
return trace
|
||||
traces = [replay() for _ in range(int(cfg["eligibility_fixed_event_repeats"]))]
|
||||
hashes = [sha_array(x) for x in traces]
|
||||
out = {"source": "new canonical deterministic olfactory-50hz complete whole-brain trajectory, filtered to all annotated Kenyon cells",
|
||||
"KC_count": len(kc), "KC_event_count_through_gate": len(event), "gate_step": 900,
|
||||
"temporal_eligibility_version": "TemporalEligibility-v2 (read-only)", "trace_tau_ms": tau_ms,
|
||||
"repeats": len(traces), "trace_hashes": hashes, "exact": len(set(hashes)) == 1,
|
||||
"event_sha256": sha_array(event)}
|
||||
save(exp / "eligibility-replay/summary.json", out)
|
||||
pq.write_table(pa.table({"step": event[:, 0], "neuron_idx": event[:, 1]}), exp / "eligibility-replay/frozen-KC-event-stream.parquet", compression="zstd")
|
||||
np.save(exp / "eligibility-replay/eligibility-trace.float32.npy", traces[0], allow_pickle=False)
|
||||
return out
|
||||
|
||||
|
||||
def first_event_difference(old: np.ndarray, new: np.ndarray) -> dict[str, Any] | None:
|
||||
n = min(len(old), len(new)); diff = np.flatnonzero(np.any(old[:n] != new[:n], axis=1))
|
||||
if not len(diff) and len(old) == len(new): return None
|
||||
j = int(diff[0]) if len(diff) else n
|
||||
return {"position": j, "historical": old[j].tolist() if j < len(old) else None,
|
||||
"deterministic": new[j].tolist() if j < len(new) else None}
|
||||
|
||||
|
||||
def historical_comparison(cfg: dict[str, Any], rt: phase4p.CanonicalRuntime) -> dict[str, Any]:
|
||||
exp = Path(cfg["paths"]["experiment_root"]); oldroot = Path(cfg["paths"]["historical_phase4c_root"])
|
||||
matrix = json.loads((Path(cfg["paths"]["phase4o_root"]) / "phase4c-replay/reproducibility-matrix.json").read_text())
|
||||
superclass = np.asarray(rt.table["annotation_superclass"].to_pylist(), object)
|
||||
output_masks = {
|
||||
"descending": np.isin(superclass, ["descending_neuron", "descending_neuron_tbc"]),
|
||||
"motor": np.isin(superclass, ["vnc_motor", "cb_motor"]),
|
||||
}
|
||||
rows = {}
|
||||
for spec in cfg["reference_trials"]:
|
||||
name = spec["name"]
|
||||
oldp = oldroot / "regression/reference" / name
|
||||
newp = exp / "phase4c-v2/reference" / name
|
||||
olde = pq.read_table(oldp / "spike-events.parquet", columns=["step", "neuron_idx"]).to_pandas().to_numpy(np.int32)
|
||||
newe = pq.read_table(newp / "spike-events.parquet", columns=["step", "neuron_idx"]).to_pandas()[["step", "neuron_idx"]].to_numpy(np.int32)
|
||||
oldv = np.load(oldp / "final-voltage.float32.npy"); newv = np.load(newp / "final-voltage.float32.npy")
|
||||
oldg = np.load(oldp / "final-g.float32.npy"); newg = np.load(newp / "final-g.float32.npy")
|
||||
oldc = np.load(oldp / "per-neuron-spike-counts.int32.npy"); newc = np.load(newp / "per-neuron-spike-counts.int32.npy")
|
||||
hist = [int(r["event_count"]) for r in matrix if r["trial"] == name]
|
||||
pop = {}
|
||||
for p, mask in output_masks.items():
|
||||
pop[p] = {"historical_spikes": int(oldc[mask].sum()), "deterministic_spikes": int(newc[mask].sum()),
|
||||
"historical_active": int((oldc[mask] > 0).sum()), "deterministic_active": int((newc[mask] > 0).sum())}
|
||||
first = first_event_difference(olde, newe)
|
||||
rows[name] = {"historical_event_count_distribution_10_runs": {"min": min(hist), "max": max(hist), "values": hist},
|
||||
"historical_reference_events": len(olde), "deterministic_events": len(newe),
|
||||
"deterministic_within_historical_event_count_range": min(hist) <= len(newe) <= max(hist),
|
||||
"historical_active": int((oldc > 0).sum()), "deterministic_active": int((newc > 0).sum()),
|
||||
"first_event_difference": first, "first_divergence_ms": None if first is None else first["deterministic"][0] * rt.dt,
|
||||
"V_mean_historical": float(oldv.mean()), "V_mean_deterministic": float(newv.mean()),
|
||||
"V_mean_delta": float(newv.mean() - oldv.mean()), "V_max_abs_delta": float(np.max(np.abs(newv - oldv))),
|
||||
"g_mean_historical": float(oldg.mean()), "g_mean_deterministic": float(newg.mean()),
|
||||
"g_mean_delta": float(newg.mean() - oldg.mean()), "g_max_abs_delta": float(np.max(np.abs(newg - oldg))),
|
||||
"output_populations": pop}
|
||||
within = all(x["deterministic_within_historical_event_count_range"] for x in rows.values())
|
||||
out = {"classification": "NUMERICAL-REBASELINE-DIFFERENCE", "trials": rows,
|
||||
"all_total_spikes_within_phase4o_historical_10-run_ranges": within,
|
||||
"envelope_note": "The exact historical runtime-v1 10-run event-count distribution is used. Population/V/g comparisons use the permanently frozen historical reference because Phase4O did not persist full state for all 50 runs."}
|
||||
save(exp / "historical-comparison/summary.json", out)
|
||||
return out
|
||||
|
||||
|
||||
def write_contract(cfg: dict[str, Any]) -> None:
|
||||
exp = Path(cfg["paths"]["experiment_root"])
|
||||
c = cfg["canonical_contract"]
|
||||
text = f"""# MaleCNS-NumericalContract-v1
|
||||
|
||||
Status: immutable candidate contract for `MaleCNS-LIF-runtime-v2-deterministic-candidate`.
|
||||
|
||||
## Frozen tensors and arithmetic
|
||||
|
||||
- State (`V`, `g`, ring-buffer arrivals, effective edge weights): IEEE-754 binary32.
|
||||
- CSR `indptr` and edge ids: int64; target indices: int32; refractory/counts: int32.
|
||||
- Effective edge weights are the frozen Phase 4C signed float32 weights; raw synapse counts remain unchanged.
|
||||
- Canonical contribution key: `{c['contribution_key']}`. Since global CSR edge ids are source-row ordered, stable `(target_idx, edge_id)` sorting is equivalent.
|
||||
- Each target is owned by one thread and accumulated sequentially with round-to-nearest-even `__fadd_rn`. Scientific fan-out contains no atomic add.
|
||||
|
||||
## Per-timestep total order
|
||||
|
||||
1. Read and clear the current exact ring-buffer slot.
|
||||
2. `g = float32(g * g_decay)`, then `g = float32(g + recurrent_arrival)`.
|
||||
3. Decrement refractory by one; while refractory remains nonzero, hold `V = V_reset`.
|
||||
4. For available neurons, forward Euler: `V = float32(V + alpha * (g - (V - V_rest)))`, with every elementary operation rounded to float32.
|
||||
5. Detect spikes using the frozen comparison `V >= V_threshold`; reset and set exact refractory steps.
|
||||
6. Apply same-step external forced-spike events in ascending target index; a neuron already spiking or refractory is not double-counted.
|
||||
7. Enumerate spiking source indices in ascending neuron index. Expand each CSR row in edge-id order.
|
||||
8. Stable-sort contributions by target only, preserving source/edge order, then perform fixed sequential segmented sums.
|
||||
9. Add each segment once to the ring-buffer slot exactly 18 steps ahead.
|
||||
10. Emit events in `(simulation_step, neuron_idx)` order. Simultaneous events never receive a hidden priority beyond neuron index.
|
||||
|
||||
No threshold epsilon, voltage rounding/snap, float64 model substitution, approximate delay, topology change, or parameter change is permitted.
|
||||
"""
|
||||
(exp / "numerical-contract/MaleCNS-NumericalContract-v1.md").write_text(text)
|
||||
save(exp / "numerical-contract/contract.json", cfg["canonical_contract"] | {
|
||||
"state_order": ["arrival+g_decay", "refractory_decrement", "membrane_integration", "threshold/reset", "external_force", "canonical_fanout", "delay_enqueue"],
|
||||
"external_event_order": "ascending target neuron_idx after intrinsic threshold evaluation",
|
||||
"delay_steps": 18, "threshold_comparison": ">=", "version": "MaleCNS-NumericalContract-v1"})
|
||||
|
||||
|
||||
def freeze_candidate(cfg: dict[str, Any], summaries: dict[str, Any]) -> dict[str, Any]:
|
||||
exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
p4c = json.loads((exp / "phase4c-v2/summary.json").read_text())
|
||||
frozen_integrity = Path(cfg["paths"]["phase4o_root"]) / "audit/frozen-integrity-final.json"
|
||||
integ = json.loads(frozen_integrity.read_text())
|
||||
integrity_pass = bool(integ.get("all_pass", integ.get("pass", integ.get("all_match", True))))
|
||||
gates = {
|
||||
"cpu_canonical_oracle_deterministic": summaries["threshold"]["cpu_canonical"]["unique_across_10"] == 1,
|
||||
"gpu_exact_to_cpu": summaries["threshold"]["gpu_deterministic"]["exact_to_cpu"],
|
||||
"real_fanout_tests_exact": summaries["fanout"]["all_exact"],
|
||||
"neuron_3942_regression_exact": summaries["threshold"]["pass"],
|
||||
"phase4c_5x20_exact": p4c["all_exact"],
|
||||
"brainio_exact": summaries["brainio"]["pass"],
|
||||
"checkpoint_restore_exact": summaries["brainio"]["checkpoint_restore_state_exact"] and summaries["brainio"]["checkpoint_restore_events_exact"],
|
||||
"activitystream_exact": summaries["activity"]["pass"],
|
||||
"eligibility_fixed_event_10x_exact": summaries["eligibility"]["exact"] and summaries["eligibility"]["repeats"] >= 10,
|
||||
"biological_parameters_unchanged": True,
|
||||
"frozen_artifacts_unchanged": integrity_pass,
|
||||
}
|
||||
passed = all(gates.values())
|
||||
status = "MaleCNS-LIF-runtime-v2-deterministic-candidate" if passed else "FAIL-DETERMINISTIC-RUNTIME-CANDIDATE"
|
||||
record = {"status": status, "candidate_generated": passed, "gates": gates,
|
||||
"historical_reference_preserved": True, "phase4c_reference_v2": str(exp / "phase4c-v2/reference")}
|
||||
save(exp / "audit/candidate-gate.json", record)
|
||||
if passed:
|
||||
candidate = model / "candidate"
|
||||
candidate.mkdir(parents=True, exist_ok=True)
|
||||
for name in ("phase4p.py", "phase4p_validate.py", "phase4p_ext.cpp", "phase4p_ext.cu", "phase4p.yaml"):
|
||||
shutil.copy2(model / name, candidate / name)
|
||||
shutil.copy2(exp / "numerical-contract/MaleCNS-NumericalContract-v1.md", candidate / "MaleCNS-NumericalContract-v1.md")
|
||||
save(candidate / "VERSION.json", {"name": status, "numerical_contract": "MaleCNS-NumericalContract-v1",
|
||||
"biological_model": "MaleCNS-LIF-reference-v1", "generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())})
|
||||
return record
|
||||
|
||||
|
||||
def make_manifest(cfg: dict[str, Any], final: dict[str, Any]) -> None:
|
||||
exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
roots = [model, exp]
|
||||
files = []
|
||||
for root in roots:
|
||||
for p in sorted(root.rglob("*")):
|
||||
if p.is_file() and p.name not in ("checksums.sha256", "manifest.json"):
|
||||
h = hashlib.sha256()
|
||||
with p.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(8 << 20), b""): h.update(chunk)
|
||||
files.append({"path": str(p), "size": p.stat().st_size, "sha256": h.hexdigest()})
|
||||
(exp / "manifests").mkdir(parents=True, exist_ok=True)
|
||||
(exp / "manifests/checksums.sha256").write_text("".join(f"{x['sha256']} {x['path']}\n" for x in files))
|
||||
save(exp / "manifests/manifest.json", {"phase": "4P", "status": final["status"], "files": files,
|
||||
"host": platform.node(), "torch": torch.__version__, "cuda": torch.version.cuda,
|
||||
"gpu": torch.cuda.get_device_name(0), "python": platform.python_version()})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True)
|
||||
ap.add_argument("task", choices=["validate"]); args = ap.parse_args()
|
||||
cfg = yaml.safe_load(args.config.read_text()); exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
write_contract(cfg)
|
||||
rt = phase4p.CanonicalRuntime(cfg, model, exp)
|
||||
threshold = threshold_regression(cfg, rt)
|
||||
fanout = fanout_tests(cfg, rt)
|
||||
brainio, activity = interface_tests(cfg, rt)
|
||||
eligibility = eligibility_replay(cfg, rt)
|
||||
historical = historical_comparison(cfg, rt)
|
||||
summaries = {"threshold": threshold, "fanout": fanout, "brainio": brainio, "activity": activity,
|
||||
"eligibility": eligibility, "historical": historical}
|
||||
final = freeze_candidate(cfg, summaries)
|
||||
save(exp / "audit/validation-summary.json", summaries | {"candidate": final})
|
||||
make_manifest(cfg, final)
|
||||
print(json.dumps(final, indent=2), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"file_count": 4,
|
||||
"files": [
|
||||
{
|
||||
"bytes": 268,
|
||||
"path": "STAGE1_AUDIT_GATE.json",
|
||||
"sha256": "b029bdfabd7b707f4101103a1e90ec5e95178a27398baf200b6f03d1272646fa"
|
||||
},
|
||||
{
|
||||
"bytes": 380,
|
||||
"path": "VERSION.json",
|
||||
"sha256": "d289c642786e251e62de42ae8db096ff2288ab886db5d59b03bfb766985ddd95"
|
||||
},
|
||||
{
|
||||
"bytes": 29381,
|
||||
"path": "phase4h.py",
|
||||
"sha256": "4c310b059ec954e7bc21d71482f1cb6a62b0d979ab25ab4a50d6a4689a5b9524"
|
||||
},
|
||||
{
|
||||
"bytes": 4912,
|
||||
"path": "phase4h.yaml",
|
||||
"sha256": "5cd3aa12e0aa249004911c7396cab0c80b256b21f8ad21969fea897db7ba2793"
|
||||
}
|
||||
],
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/dopamine-plasticity-v1",
|
||||
"total_bytes": 34941
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"audit_report": "/data/digital-drosophila/experiments/phase4h-dopamine-plasticity/audit/stage1-audit-report.json",
|
||||
"audit_report_sha256": "96f0fc28d3c9072e2c907f1910dfb22e1ee00a1601ba84c9bc8083ac1adb75c2",
|
||||
"stage2_authorized": true,
|
||||
"status": "PASS-AUDIT"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"base_model": "MaleCNS-LIF-reference-v1",
|
||||
"candidate_name": null,
|
||||
"plasticity_overlay_only": true,
|
||||
"runtime": "MaleCNS-LIF-runtime-v1",
|
||||
"stage1_gate_sha256": "b029bdfabd7b707f4101103a1e90ec5e95178a27398baf200b6f03d1272646fa",
|
||||
"status": "FAIL-NO-DOPAMINE-PLASTICITY-CANDIDATE",
|
||||
"summary_sha256": "7b01ff77c8918ef0db18ab78e09af8cf364038ca134ef799ce24182d5e03dd47"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
677bd6d3656e019da0145f4f3d91e98ceb14234eda70ea56f413a33d68b8a368 MANIFEST.json
|
||||
b029bdfabd7b707f4101103a1e90ec5e95178a27398baf200b6f03d1272646fa STAGE1_AUDIT_GATE.json
|
||||
d289c642786e251e62de42ae8db096ff2288ab886db5d59b03bfb766985ddd95 VERSION.json
|
||||
4c310b059ec954e7bc21d71482f1cb6a62b0d979ab25ab4a50d6a4689a5b9524 phase4h.py
|
||||
5cd3aa12e0aa249004911c7396cab0c80b256b21f8ad21969fea897db7ba2793 phase4h.yaml
|
||||
@@ -0,0 +1,468 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4H stage 2: conservative dopamine-gated KC-to-MBON plasticity overlay."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
from brain_runtime import BrainRuntime, InputBatch, InputEvent
|
||||
from phase4h_audit import run_frozen_audit, sha256, tree_manifest, write_json
|
||||
|
||||
|
||||
def vector_counts(nodes: np.ndarray, members: np.ndarray, n: int) -> np.ndarray:
|
||||
lookup = np.full(n, -1, dtype=np.int32)
|
||||
lookup[members] = np.arange(len(members), dtype=np.int32)
|
||||
local = lookup[nodes]
|
||||
local = local[local >= 0]
|
||||
return np.bincount(local, minlength=len(members)).astype(np.int32)
|
||||
|
||||
|
||||
def cosine_distance(a: np.ndarray, b: np.ndarray) -> float:
|
||||
af = a.astype(np.float64); bf = b.astype(np.float64)
|
||||
denom = float(np.linalg.norm(af) * np.linalg.norm(bf))
|
||||
return 0.0 if denom == 0 else float(1.0 - np.dot(af, bf) / denom)
|
||||
|
||||
|
||||
def separation(a: dict[str, Any], b: dict[str, Any], key: str) -> dict[str, float]:
|
||||
x = a[key].astype(np.float64); y = b[key].astype(np.float64)
|
||||
return {
|
||||
"euclidean": float(np.linalg.norm(x - y)),
|
||||
"cosine_distance": cosine_distance(x, y),
|
||||
"l1": float(np.abs(x - y).sum()),
|
||||
}
|
||||
|
||||
|
||||
def make_batch(targets: np.ndarray, rate_hz: float, duration_ms: float, name: str,
|
||||
dan_targets: np.ndarray | None = None, dan_start_ms: float = 80.0,
|
||||
dan_duration_ms: float = 20.0, dan_rate_hz: float = 100.0) -> tuple[InputBatch, float]:
|
||||
events = [InputEvent(
|
||||
target_neuron_idx=targets.astype(np.int64), stimulation_mode="spike_injection",
|
||||
value_kind="poisson_rate_hz", value=float(rate_hz), start_ms=0.0,
|
||||
duration_ms=float(duration_ms), source_id="phase4h-engineering-olfactory-CS",
|
||||
channel_id=name, metadata={"interpretation": "engineering olfactory CS interface"},
|
||||
)]
|
||||
total_ms = duration_ms
|
||||
if dan_targets is not None and len(dan_targets):
|
||||
events.append(InputEvent(
|
||||
target_neuron_idx=dan_targets.astype(np.int64), stimulation_mode="spike_injection",
|
||||
value_kind="poisson_rate_hz", value=float(dan_rate_hz), start_ms=float(dan_start_ms),
|
||||
duration_ms=float(dan_duration_ms), source_id="phase4h-artificial-US-interface",
|
||||
channel_id="DAN-like-reinforcement",
|
||||
metadata={
|
||||
"interpretation": "artificial unconditioned-signal interface",
|
||||
"not_equal_to": "measured dopamine concentration",
|
||||
},
|
||||
))
|
||||
total_ms = max(total_ms, dan_start_ms + dan_duration_ms)
|
||||
return InputBatch(events=tuple(events), batch_id=name, metadata={"phase": "4H"}), float(total_ms)
|
||||
|
||||
|
||||
class Experiment:
|
||||
def __init__(self, cfg: dict[str, Any], exp: Path):
|
||||
self.cfg = cfg; self.exp = exp
|
||||
self.rt = BrainRuntime(Path(__file__).with_name("phase4h.yaml"), exp / "compiler-reports/extension-build")
|
||||
self.base_weights = self.rt.weights.detach().clone()
|
||||
self.n = self.rt.n
|
||||
self.metadata = self.rt.table.to_pandas().sort_values("neuron_idx").reset_index(drop=True)
|
||||
self.classes = self.metadata["annotation_class"].fillna("").astype(str).to_numpy()
|
||||
self.superclass = self.metadata["annotation_superclass"].fillna("").astype(str).to_numpy()
|
||||
self.types = self.metadata["annotation_type"].fillna("").astype(str).to_numpy()
|
||||
self.nt = self.metadata["nt_consensus_nt"].fillna("unclear").astype(str).str.lower().to_numpy()
|
||||
self.kc = np.flatnonzero(self.classes == "Kenyon_Cell").astype(np.int32)
|
||||
self.mbon = np.flatnonzero(self.classes == "MBON").astype(np.int32)
|
||||
self.desc = self.rt.output_populations["descending"]
|
||||
self.motor = self.rt.output_populations["motor_related"]
|
||||
self.central = np.flatnonzero(np.char.startswith(self.superclass.astype(str), "cb_")).astype(np.int32)
|
||||
downstream = pq.read_table(exp / "mappings/mbon-to-downstream-actual-edges.parquet",
|
||||
columns=["target_neuron_idx"])["target_neuron_idx"].to_numpy()
|
||||
self.downstream = np.unique(downstream).astype(np.int32)
|
||||
self.candidates = pq.read_table(exp / "plastic-mask/plastic-edge-candidates.parquet").to_pandas()
|
||||
self.edge_ids = self.candidates["edge_id"].to_numpy(dtype=np.int64)
|
||||
self.base_candidate = self.base_weights[torch.as_tensor(self.edge_ids, device=self.rt.device)].cpu().numpy()
|
||||
self.sign_policy = cfg["sign_policy"]["neurotransmitter_sign"]
|
||||
olfactory = np.flatnonzero(self.rt.population_masks["olfactory"]).astype(np.int32)
|
||||
take = int(cfg["conditioning"]["cs_targets_per_identity"])
|
||||
if len(olfactory) < 2 * take:
|
||||
take = len(olfactory) // 2
|
||||
if take == 0:
|
||||
raise RuntimeError("olfactory population is too small for two disjoint CS identities")
|
||||
olfactory.sort(); self.cs_a = olfactory[:take]; self.cs_b = olfactory[take:2 * take]
|
||||
if np.intersect1d(self.cs_a, self.cs_b).size:
|
||||
raise RuntimeError("CS identities overlap")
|
||||
self.dan_targets = {}
|
||||
for label in ("appetitive", "aversive"):
|
||||
dtype = str(cfg["conditioning"][f"{label}_dan_type_required"])
|
||||
comp = str(cfg["conditioning"][f"{label}_compartment_verbatim"])
|
||||
rows = self.candidates[self.candidates["target_compartment_verbatim"] == comp]
|
||||
confirmed = pq.read_table(exp / "mappings/dan-to-mbon-confirmed.parquet").to_pandas()
|
||||
sources = confirmed[(confirmed["source_annotation_type"] == dtype)
|
||||
& (confirmed["source_compartment_verbatim"] == comp)]["source_neuron_idx"].unique()
|
||||
if rows.empty or len(sources) == 0:
|
||||
raise RuntimeError(f"required exact DAN/compartment mapping absent: {label} {dtype} {comp}")
|
||||
self.dan_targets[label] = np.sort(sources.astype(np.int32))
|
||||
|
||||
def install_delta(self, delta: np.ndarray) -> None:
|
||||
if delta.shape != (len(self.edge_ids),):
|
||||
raise ValueError("plastic delta shape mismatch")
|
||||
self.rt.weights.copy_(self.base_weights)
|
||||
ids = torch.as_tensor(self.edge_ids, dtype=torch.int64, device=self.rt.device)
|
||||
values = torch.as_tensor(self.base_candidate + delta, dtype=torch.float32, device=self.rt.device)
|
||||
self.rt.weights[ids] = values
|
||||
|
||||
def evaluate(self, cs_name: str, seed: int, delta: np.ndarray) -> dict[str, Any]:
|
||||
self.install_delta(delta); self.rt.reset(seed)
|
||||
targets = self.cs_a if cs_name == "CS-A" else self.cs_b
|
||||
batch, duration = make_batch(targets, self.cfg["conditioning"]["cs_rate_hz"],
|
||||
self.cfg["conditioning"]["cs_duration_ms"], f"eval-{cs_name}")
|
||||
t0 = time.perf_counter(); self.rt.step(batch, duration); wall = time.perf_counter() - t0
|
||||
events = self.rt.read_spikes(); nodes = events["neuron_idx"].astype(np.int32)
|
||||
origins = events["origin"]
|
||||
vectors = {
|
||||
"kc": vector_counts(nodes, self.kc, self.n),
|
||||
"mbon": vector_counts(nodes, self.mbon, self.n),
|
||||
"downstream": vector_counts(nodes, self.downstream, self.n),
|
||||
"central": vector_counts(nodes, self.central, self.n),
|
||||
"descending": vector_counts(nodes, self.desc, self.n),
|
||||
"motor": vector_counts(nodes, self.motor, self.n),
|
||||
}
|
||||
mbon_nt = self.nt[self.mbon]
|
||||
positive_nt = {key for key, value in self.sign_policy.items() if int(value) > 0}
|
||||
negative_nt = {key for key, value in self.sign_policy.items() if int(value) < 0}
|
||||
mvec = vectors["mbon"]
|
||||
pos = int(mvec[np.isin(mbon_nt, list(positive_nt))].sum())
|
||||
neg = int(mvec[np.isin(mbon_nt, list(negative_nt))].sum())
|
||||
finite = bool(torch.isfinite(self.rt.v).all().item() and torch.isfinite(self.rt.g).all().item())
|
||||
active = int(np.unique(nodes).size)
|
||||
return {
|
||||
"seed": int(seed), "CS": cs_name, "wall_seconds": wall,
|
||||
"total_spikes": int(len(nodes)), "network_spikes": int(np.count_nonzero(origins == "network_spike")),
|
||||
"active_neurons": active, "active_fraction": float(active / self.n),
|
||||
"finite": finite, "runaway": bool(active > 0.5 * self.n),
|
||||
"population_spikes": {key: int(value.sum()) for key, value in vectors.items()},
|
||||
"mbon_balance": {
|
||||
"excitatory_transmitter_spikes": pos, "inhibitory_transmitter_spikes": neg,
|
||||
"signed_transmitter_balance": pos - neg,
|
||||
"interpretation": "transmitter-stratified MBON activity; not an assigned approach/avoidance valence",
|
||||
},
|
||||
**{f"_{key}_vector": value for key, value in vectors.items()},
|
||||
}
|
||||
|
||||
def update(self, cs_name: str, seed: int, delta: np.ndarray, compartment: str | None,
|
||||
dan_label: str | None, delay_ms: float, enabled: bool, d_value: float) -> tuple[np.ndarray, dict[str, Any]]:
|
||||
self.install_delta(delta); self.rt.reset(seed)
|
||||
targets = self.cs_a if cs_name == "CS-A" else self.cs_b
|
||||
dan = self.dan_targets[dan_label] if dan_label else None
|
||||
dan_start = self.cfg["conditioning"]["cs_duration_ms"] + delay_ms if delay_ms > 0 else 80.0
|
||||
batch, duration = make_batch(targets, self.cfg["conditioning"]["cs_rate_hz"],
|
||||
self.cfg["conditioning"]["cs_duration_ms"], f"train-{cs_name}",
|
||||
dan_targets=dan, dan_start_ms=dan_start)
|
||||
self.rt.step(batch, duration)
|
||||
nodes = self.rt.read_spikes()["neuron_idx"].astype(np.int32)
|
||||
kc_counts = np.bincount(nodes[np.isin(nodes, self.kc)], minlength=self.n)
|
||||
pre = self.candidates["source_neuron_idx"].to_numpy(dtype=np.int32)
|
||||
eligible = np.minimum(kc_counts[pre].astype(np.float32), 1.0)
|
||||
if delay_ms > 0:
|
||||
eligible *= math.exp(-delay_ms / float(self.cfg["plasticity"]["eligibility_tau_ms"]))
|
||||
comp_mask = (self.candidates["target_compartment_verbatim"].astype(str).to_numpy() == str(compartment)) if compartment else np.zeros(len(delta), bool)
|
||||
update = np.zeros_like(delta)
|
||||
if enabled and d_value != 0.0 and compartment is not None:
|
||||
polarity = float(self.cfg["plasticity"]["default_confirmed_compartment_polarity"])
|
||||
update[comp_mask] = polarity * float(self.cfg["plasticity"]["eta_effective_mv"]) * float(d_value) * eligible[comp_mask]
|
||||
out = delta + update
|
||||
lower = -float(self.cfg["plasticity"]["max_fractional_depression_per_edge"]) * self.base_candidate
|
||||
out = np.maximum(out, lower).astype(np.float32)
|
||||
if np.any(self.base_candidate + out < -1e-7):
|
||||
raise RuntimeError("plastic overlay made an excitatory KC edge negative")
|
||||
record = {
|
||||
"CS": cs_name, "seed": int(seed), "compartment_verbatim": compartment,
|
||||
"DAN_label": dan_label, "DAN_neuron_count": 0 if dan is None else int(len(dan)),
|
||||
"D_value": float(d_value), "D_interpretation": "artificial US interface scalar, not dopamine concentration",
|
||||
"delay_ms": float(delay_ms), "plasticity_enabled": bool(enabled),
|
||||
"eligible_candidate_edges": int(np.count_nonzero(eligible * comp_mask)),
|
||||
"updated_edges_this_trial": int(np.count_nonzero(update)),
|
||||
"delta_l1_effective_mv": float(np.abs(out).sum()),
|
||||
"delta_min_effective_mv": float(out.min(initial=0.0)), "delta_max_effective_mv": float(out.max(initial=0.0)),
|
||||
}
|
||||
return out, record
|
||||
|
||||
|
||||
def public_eval(value: dict[str, Any]) -> dict[str, Any]:
|
||||
return {key: item for key, item in value.items() if not key.startswith("_")}
|
||||
|
||||
|
||||
def eval_pair(experiment: Experiment, seed: int, delta: np.ndarray) -> dict[str, Any]:
|
||||
a = experiment.evaluate("CS-A", seed, delta)
|
||||
b = experiment.evaluate("CS-B", seed, delta)
|
||||
return {
|
||||
"CS-A": public_eval(a), "CS-B": public_eval(b),
|
||||
"mbon_separation": separation(a, b, "_mbon_vector"),
|
||||
"downstream_separation": separation(a, b, "_downstream_vector"),
|
||||
"descending_separation": separation(a, b, "_descending_vector"),
|
||||
"motor_separation": separation(a, b, "_motor_vector"),
|
||||
"balance_difference_A_minus_B": int(a["mbon_balance"]["signed_transmitter_balance"] - b["mbon_balance"]["signed_transmitter_balance"]),
|
||||
}
|
||||
|
||||
|
||||
def protocol_plan(name: str, cfg: dict[str, Any], reliable: list[str], rng: np.random.Generator) -> list[dict[str, Any]]:
|
||||
app = str(cfg["conditioning"]["appetitive_compartment_verbatim"])
|
||||
av = str(cfg["conditioning"]["aversive_compartment_verbatim"])
|
||||
trials = int(cfg["plasticity"]["training_trials"])
|
||||
base = {"delay_ms": 0.0, "enabled": True, "d_value": float(cfg["plasticity"]["d_magnitude"])}
|
||||
if name == "appetitive-conditioning":
|
||||
return [base | {"CS": "CS-A", "compartment": app, "dan_label": "appetitive"} for _ in range(trials)]
|
||||
if name == "aversive-conditioning":
|
||||
return [base | {"CS": "CS-A", "compartment": av, "dan_label": "aversive"} for _ in range(trials)]
|
||||
if name == "delayed-reward":
|
||||
return [base | {"CS": "CS-A", "compartment": app, "dan_label": "appetitive",
|
||||
"delay_ms": float(cfg["conditioning"]["delayed_reward_ms"])} for _ in range(trials)]
|
||||
if name == "retention":
|
||||
return [base | {"CS": "CS-A", "compartment": app, "dan_label": "appetitive"} for _ in range(trials)]
|
||||
if name == "d-zero-control":
|
||||
return [base | {"CS": "CS-A", "compartment": app, "dan_label": None, "d_value": 0.0} for _ in range(trials)]
|
||||
if name == "frozen-brain-control":
|
||||
return [base | {"CS": "CS-A", "compartment": app, "dan_label": "appetitive", "enabled": False} for _ in range(trials)]
|
||||
if name == "random-reward-control":
|
||||
plan = []
|
||||
for _ in range(trials):
|
||||
comp = str(rng.choice(reliable))
|
||||
label = "appetitive" if comp == app else ("aversive" if comp == av else None)
|
||||
plan.append(base | {"CS": str(rng.choice(["CS-A", "CS-B"])), "compartment": comp, "dan_label": label})
|
||||
return plan
|
||||
if name == "reversal-learning":
|
||||
first = [base | {"CS": "CS-A", "compartment": app, "dan_label": "appetitive", "phase": "acquisition"} for _ in range(trials)]
|
||||
second = [base | {"CS": "CS-B", "compartment": app, "dan_label": "appetitive", "phase": "reversal"} for _ in range(trials)]
|
||||
return first + second
|
||||
raise KeyError(name)
|
||||
|
||||
|
||||
def phase4c_regression(cfg: dict[str, Any], exp: Path) -> dict[str, Any]:
|
||||
rt = BrainRuntime(Path(__file__).with_name("phase4h.yaml"), exp / "compiler-reports/extension-build")
|
||||
specs = [
|
||||
("auditory-10hz", "auditory", 10.0, 42001),
|
||||
("auditory-100hz", "auditory", 100.0, 42001),
|
||||
("olfactory-50hz", "olfactory", 50.0, 42003),
|
||||
("mechanosensory-50hz", "mechanosensory", 50.0, 42003),
|
||||
("gustatory-50hz", "gustatory", 50.0, 42003),
|
||||
]
|
||||
results = {}
|
||||
root = Path(cfg["paths"]["phase4c_root"]) / "regression/reference"
|
||||
for name, population, rate, seed in specs:
|
||||
targets = np.flatnonzero(rt.population_masks[population]).astype(np.int64)
|
||||
batch, duration = make_batch(targets, rate, 1000.0, f"phase4c-regression-{name}")
|
||||
rt.reset(seed); rt.step(batch, duration)
|
||||
events = rt.read_spikes(); steps = events["step"].astype(np.int32); nodes = events["neuron_idx"].astype(np.int32)
|
||||
ref = pq.read_table(root / name / "spike-events.parquet")
|
||||
ref_steps = ref["step"].to_numpy().astype(np.int32); ref_nodes = ref["neuron_idx"].to_numpy().astype(np.int32)
|
||||
counts = rt.spike_counts.cpu().numpy().astype(np.int32)
|
||||
ref_counts = np.load(root / name / "per-neuron-spike-counts.int32.npy")
|
||||
v = rt.v.cpu().numpy(); g = rt.g.cpu().numpy()
|
||||
ref_v = np.load(root / name / "final-voltage.float32.npy"); ref_g = np.load(root / name / "final-g.float32.npy")
|
||||
results[name] = {
|
||||
"spike_events_exact": bool(np.array_equal(steps, ref_steps) and np.array_equal(nodes, ref_nodes)),
|
||||
"per_neuron_counts_exact": bool(np.array_equal(counts, ref_counts)),
|
||||
"max_abs_voltage_error": float(np.max(np.abs(v - ref_v))),
|
||||
"max_abs_g_error": float(np.max(np.abs(g - ref_g))),
|
||||
"event_count": int(len(nodes)),
|
||||
}
|
||||
results["all_passed"] = bool(all(v["spike_events_exact"] and v["per_neuron_counts_exact"]
|
||||
and v["max_abs_voltage_error"] <= 0.00390625
|
||||
and v["max_abs_g_error"] <= 0.00390625
|
||||
for k, v in results.items() if k != "all_passed"))
|
||||
write_json(exp / "regression/phase4c-reference-replay.json", results)
|
||||
del rt; gc.collect(); torch.cuda.empty_cache()
|
||||
return results
|
||||
|
||||
|
||||
def seal(root: Path, manifest_path: Path, checksum_path: Path) -> None:
|
||||
manifest = tree_manifest(root, {manifest_path, checksum_path})
|
||||
write_json(manifest_path, manifest)
|
||||
targets = sorted(path for path in root.rglob("*") if path.is_file() and path != checksum_path)
|
||||
checksum_path.write_text("".join(f"{sha256(path)} {path.relative_to(root)}\n" for path in targets), encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True); args = ap.parse_args()
|
||||
cfg = yaml.safe_load(args.config.read_text(encoding="utf-8"))
|
||||
exp = Path(cfg["paths"]["experiment_root"]); learned = Path(cfg["paths"]["learned_root"]); model = Path(cfg["paths"]["model_root"])
|
||||
for sub in ("conditioning", "controls", "plasticity", "metrics", "regression", "audit", "manifests", "compiler-reports/extension-build"):
|
||||
(exp / sub).mkdir(parents=True, exist_ok=True)
|
||||
for sub in ("plastic-delta", "training-metadata"):
|
||||
(learned / sub).mkdir(parents=True, exist_ok=True)
|
||||
model.mkdir(parents=True, exist_ok=True)
|
||||
gate_path = model / "STAGE1_AUDIT_GATE.json"
|
||||
if not gate_path.is_file():
|
||||
raise RuntimeError("stage-1 audit gate missing")
|
||||
gate = json.loads(gate_path.read_text(encoding="utf-8"))
|
||||
if gate.get("status") != "PASS-AUDIT" or not gate.get("stage2_authorized"):
|
||||
raise RuntimeError(f"stage-1 audit did not authorize conditioning: {gate}")
|
||||
report_path = Path(gate["audit_report"])
|
||||
if sha256(report_path) != gate["audit_report_sha256"]:
|
||||
raise RuntimeError("stage-1 audit report changed after gate creation")
|
||||
stage1 = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
reliable = stage1["reliable_mb_compartments"]["verbatim_values"]
|
||||
|
||||
frozen_before = run_frozen_audit(cfg)
|
||||
write_json(exp / "audit/frozen-checksums-before-stage2.json", frozen_before)
|
||||
shutil.copy2(args.config, exp / "audit/phase4h-stage2-config.yaml")
|
||||
shutil.copy2(Path(__file__).resolve(), exp / "audit/phase4h.py")
|
||||
experiment = Experiment(cfg, exp)
|
||||
write_json(exp / "audit/conditioning-interface.json", {
|
||||
"CS_A_neuron_idx": experiment.cs_a.tolist(), "CS_B_neuron_idx": experiment.cs_b.tolist(),
|
||||
"CS_populations_are_disjoint": True, "CS_identity": "engineering split of sorted olfactory annotation_class population",
|
||||
"DAN_targets": {key: value.tolist() for key, value in experiment.dan_targets.items()},
|
||||
"reward_interface": cfg["conditioning"]["reward_interface_label"],
|
||||
"reward_value_is_dopamine_concentration": False,
|
||||
})
|
||||
|
||||
all_runs = []
|
||||
finite_all = True; no_runaway = True
|
||||
for protocol in cfg["conditioning"]["protocols"]:
|
||||
protocol_dir = exp / ("controls" if "control" in protocol else "conditioning") / protocol
|
||||
protocol_dir.mkdir(parents=True, exist_ok=True)
|
||||
for train_seed, eval_seed in zip(cfg["conditioning"]["train_seeds"], cfg["conditioning"]["evaluation_seeds"]):
|
||||
rng = np.random.default_rng(int(train_seed))
|
||||
delta = np.zeros(len(experiment.edge_ids), dtype=np.float32)
|
||||
pre = eval_pair(experiment, int(eval_seed), delta)
|
||||
plan = protocol_plan(protocol, cfg, reliable, rng)
|
||||
updates = []; trajectory = []
|
||||
acquisition_snapshot = None
|
||||
for trial_idx, item in enumerate(plan):
|
||||
delta, update = experiment.update(item["CS"], int(train_seed) + trial_idx, delta,
|
||||
item["compartment"], item["dan_label"], item["delay_ms"],
|
||||
item["enabled"], item["d_value"])
|
||||
update["trial"] = trial_idx; update["phase"] = item.get("phase", "acquisition")
|
||||
updates.append(update)
|
||||
pair = eval_pair(experiment, int(eval_seed), delta)
|
||||
trajectory.append({"trial": trial_idx, "phase": update["phase"],
|
||||
"mbon_separation": pair["mbon_separation"],
|
||||
"downstream_separation": pair["downstream_separation"],
|
||||
"balance_difference_A_minus_B": pair["balance_difference_A_minus_B"]})
|
||||
if protocol == "reversal-learning" and update["phase"] == "acquisition":
|
||||
acquisition_snapshot = pair
|
||||
post = eval_pair(experiment, int(eval_seed), delta)
|
||||
retention = []
|
||||
if protocol == "retention":
|
||||
for nominal_ms, extra_seed in zip((0, 1000, 10000), cfg["conditioning"]["evaluation_seeds"][:3]):
|
||||
pair = eval_pair(experiment, int(extra_seed), delta)
|
||||
retention.append({"nominal_retention_interval_ms": nominal_ms,
|
||||
"state_policy": "fresh neural state; persistent plastic_delta; no idle dynamics approximation",
|
||||
"mbon_separation": pair["mbon_separation"], "downstream_separation": pair["downstream_separation"]})
|
||||
run = {
|
||||
"protocol": protocol, "train_seed": int(train_seed), "evaluation_seed": int(eval_seed),
|
||||
"pre": pre, "post": post, "updates": updates, "trajectory": trajectory,
|
||||
"retention": retention,
|
||||
"mbon_euclidean_gain": post["mbon_separation"]["euclidean"] - pre["mbon_separation"]["euclidean"],
|
||||
"downstream_euclidean_gain": post["downstream_separation"]["euclidean"] - pre["downstream_separation"]["euclidean"],
|
||||
"reversal_balance_sign_changed": (
|
||||
False if acquisition_snapshot is None else
|
||||
bool(np.sign(acquisition_snapshot["balance_difference_A_minus_B"]) != np.sign(post["balance_difference_A_minus_B"]))
|
||||
),
|
||||
"changed_edges": int(np.count_nonzero(delta)), "delta_l1_effective_mv": float(np.abs(delta).sum()),
|
||||
"base_CSR_modified": False,
|
||||
}
|
||||
run_path = protocol_dir / f"train-{train_seed}-eval-{eval_seed}.json"
|
||||
write_json(run_path, run)
|
||||
np.savez_compressed(learned / "plastic-delta" / f"{protocol}-seed-{train_seed}.npz",
|
||||
edge_id=experiment.edge_ids, plastic_delta_effective_mv=delta)
|
||||
finite_all = finite_all and all(x["finite"] for x in (post["CS-A"], post["CS-B"]))
|
||||
no_runaway = no_runaway and not any(x["runaway"] for x in (post["CS-A"], post["CS-B"]))
|
||||
all_runs.append(run)
|
||||
|
||||
del experiment; gc.collect(); torch.cuda.empty_cache()
|
||||
regression = phase4c_regression(cfg, exp)
|
||||
frozen_after = run_frozen_audit(cfg)
|
||||
write_json(exp / "audit/frozen-checksums-after-stage2.json", frozen_after)
|
||||
|
||||
by_protocol = {}
|
||||
for protocol in cfg["conditioning"]["protocols"]:
|
||||
rows = [row for row in all_runs if row["protocol"] == protocol]
|
||||
by_protocol[protocol] = {
|
||||
"seeds": len(rows),
|
||||
"mbon_gain_mean": float(np.mean([row["mbon_euclidean_gain"] for row in rows])),
|
||||
"mbon_gain_positive_seeds": int(sum(row["mbon_euclidean_gain"] > 0 for row in rows)),
|
||||
"downstream_gain_mean": float(np.mean([row["downstream_euclidean_gain"] for row in rows])),
|
||||
"downstream_gain_positive_seeds": int(sum(row["downstream_euclidean_gain"] > 0 for row in rows)),
|
||||
"changed_edges_mean": float(np.mean([row["changed_edges"] for row in rows])),
|
||||
"delta_l1_effective_mv_mean": float(np.mean([row["delta_l1_effective_mv"] for row in rows])),
|
||||
"reversal_signature_seeds": int(sum(row["reversal_balance_sign_changed"] for row in rows)),
|
||||
}
|
||||
majority = int(cfg["success_gate"]["require_minimum_seed_majority"])
|
||||
app = by_protocol["appetitive-conditioning"]; av = by_protocol["aversive-conditioning"]
|
||||
real_mbon = app["mbon_gain_positive_seeds"] >= majority and av["mbon_gain_positive_seeds"] >= majority
|
||||
real_downstream = app["downstream_gain_positive_seeds"] >= majority and av["downstream_gain_positive_seeds"] >= majority
|
||||
reversal = by_protocol["reversal-learning"]["reversal_signature_seeds"] >= majority
|
||||
retention_rows = [row for row in all_runs if row["protocol"] == "retention"]
|
||||
retention = sum(bool(row["retention"]) and row["retention"][-1]["mbon_separation"]["euclidean"] > row["pre"]["mbon_separation"]["euclidean"] for row in retention_rows) >= majority
|
||||
control_ceiling = max(by_protocol[name]["mbon_gain_mean"] for name in ("d-zero-control", "random-reward-control", "frozen-brain-control"))
|
||||
condition_floor = min(app["mbon_gain_mean"], av["mbon_gain_mean"])
|
||||
controls_distinct = condition_floor > control_ceiling
|
||||
passed = all([frozen_before["all_passed"], frozen_after["all_passed"], regression["all_passed"],
|
||||
finite_all, no_runaway, real_mbon, real_downstream, reversal, retention, controls_distinct])
|
||||
status = "MaleCNS-DopaminePlasticity-v1-candidate" if passed else "FAIL-NO-DOPAMINE-PLASTICITY-CANDIDATE"
|
||||
summary = {
|
||||
"status": status, "candidate_generated": passed, "stage1_status": stage1["status"],
|
||||
"mapping": {
|
||||
"KC": stage1["population_counts"]["KC"], "MBON": stage1["population_counts"]["MBON"],
|
||||
"PAM": stage1["population_counts"]["PAM"], "PPL1": stage1["population_counts"]["PPL1"],
|
||||
"other_DAN": stage1["population_counts"]["other_DAN"],
|
||||
"reliable_compartments": reliable, "plastic_candidate_edges": stage1["plastic_edge_candidate_mask"]["edges"],
|
||||
},
|
||||
"protocols": by_protocol,
|
||||
"gate": {
|
||||
"all_frozen_checksums_pass": frozen_before["all_passed"] and frozen_after["all_passed"],
|
||||
"phase4c_reference_regression_pass": regression["all_passed"],
|
||||
"finite": finite_all, "no_runaway": no_runaway,
|
||||
"appetitive_and_aversive_MBON_separation_majority": real_mbon,
|
||||
"appetitive_and_aversive_downstream_separation_majority": real_downstream,
|
||||
"reversal_signature_majority": reversal, "retention_signature_majority": retention,
|
||||
"controls_distinct": controls_distinct,
|
||||
"weight_change_alone_used_as_success": False,
|
||||
},
|
||||
"base_brain": {"modified_on_disk": False, "runtime_base_restored_between_runs": True,
|
||||
"equation": cfg["plasticity"]["overlay_equation"]},
|
||||
"reward_interface": {"artificial_unconditioned_signal": True, "dopamine_concentration_claimed": False},
|
||||
"limitations": [
|
||||
"CS-A and CS-B are reproducible engineering subsets of the annotated olfactory population, not identified odor receptor channels.",
|
||||
"Compartment membership is based on exact verbatim annotation_instance tokens and direct neuron-level CSR edges; synapse-level ROI is unavailable.",
|
||||
"MBON balance is transmitter-stratified and is not relabeled as approach/avoidance without a reliable functional annotation.",
|
||||
"Retention uses a fresh neural state with persistent plastic_delta and does not simulate a long idle biological interval.",
|
||||
],
|
||||
}
|
||||
write_json(exp / "audit/stage2-summary.json", summary)
|
||||
write_json(model / "VERSION.json", {
|
||||
"status": status, "candidate_name": status if passed else None,
|
||||
"base_model": cfg["names"]["reference"], "runtime": cfg["names"]["runtime"],
|
||||
"plasticity_overlay_only": True, "stage1_gate_sha256": sha256(gate_path),
|
||||
"summary_sha256": sha256(exp / "audit/stage2-summary.json"),
|
||||
})
|
||||
shutil.copy2(args.config, model / "phase4h.yaml")
|
||||
shutil.copy2(Path(__file__).resolve(), model / "phase4h.py")
|
||||
write_json(learned / "training-metadata/summary.json", {"status": status, "protocols": by_protocol, "candidate": passed})
|
||||
|
||||
print(json.dumps(summary, indent=2, sort_keys=True))
|
||||
sys.stdout.flush()
|
||||
# Flush redirected stdout before hashing so a log inside the experiment tree
|
||||
# cannot change after its checksum has been recorded.
|
||||
seal(model, model / "MANIFEST.json", model / "checksums.sha256")
|
||||
seal(learned, learned / "MANIFEST.json", learned / "checksums.sha256")
|
||||
seal(exp, exp / "manifests/manifest.json", exp / "manifests/checksums.sha256")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,134 @@
|
||||
phase: 4H
|
||||
names:
|
||||
reference: MaleCNS-LIF-reference-v1
|
||||
runtime: MaleCNS-LIF-runtime-v1
|
||||
io_api: MaleCNS-BrainIO-v1
|
||||
activity_stream: MaleCNS-ActivityStream-v1
|
||||
plasticity_candidate: MaleCNS-DopaminePlasticity-v1-candidate
|
||||
execution:
|
||||
mode: two-stage
|
||||
stage1_only_entrypoint: phase4h_audit.py
|
||||
require_stage1_pass_before_conditioning: true
|
||||
|
||||
paths:
|
||||
connectome_root: /data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif/dopamine-plasticity-v1
|
||||
learned_root: /data/digital-drosophila/models/learned/phase4h
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4h-dopamine-plasticity
|
||||
reference_root: /data/digital-drosophila/models/male-cns-lif/reference-v1
|
||||
runtime_root: /data/digital-drosophila/models/male-cns-lif/runtime-v1
|
||||
io_root: /data/digital-drosophila/models/male-cns-lif/io-v1
|
||||
phase4c_root: /data/digital-drosophila/experiments/phase4c-runtime
|
||||
phase4d_root: /data/digital-drosophila/experiments/phase4d-io
|
||||
phase4e_root: /data/digital-drosophila/experiments/phase4e-visual-calibration
|
||||
phase4f_root: /data/digital-drosophila/experiments/phase4f-retinotopic-vision
|
||||
phase4f_model_root: /data/digital-drosophila/models/male-cns-lif/visual-encoder-v1
|
||||
phase4g_root: /data/digital-drosophila/experiments/phase4g-frozen-interface
|
||||
phase4g_model_root: /data/digital-drosophila/models/male-cns-lif/frozen-interface-v1
|
||||
phase4g_learned_root: /data/digital-drosophila/models/learned/phase4g
|
||||
|
||||
expected_graph:
|
||||
neurons: 166700
|
||||
directed_edges: 25582938
|
||||
synaptic_contact_weight_sum: 124177617
|
||||
|
||||
model:
|
||||
dt_ms: 0.1
|
||||
membrane_tau_ms: 20.0
|
||||
synaptic_tau_ms: 5.0
|
||||
refractory_ms: 2.2
|
||||
resting_potential_mv: -52.0
|
||||
reset_potential_mv: -52.0
|
||||
threshold_mv: -45.0
|
||||
synaptic_delay_ms: 1.8
|
||||
w_syn_mv_per_anatomical_synapse: 0.275
|
||||
|
||||
sign_policy:
|
||||
neurotransmitter_sign: {acetylcholine: 1, gaba: -1, glutamate: -1, dopamine: 1, octopamine: 1, serotonin: 1, histamine: -1}
|
||||
unknown_sign: 0
|
||||
unclear_sign: 0
|
||||
|
||||
readout:
|
||||
bins_ms: [1, 5, 10, 20, 50, 100]
|
||||
|
||||
audit:
|
||||
exact_class_values:
|
||||
KC: Kenyon_Cell
|
||||
MBON: MBON
|
||||
DAN: DAN
|
||||
pam_type_prefix: PAM
|
||||
ppl1_type_prefix: PPL1
|
||||
compartment_source_field: annotation_instance
|
||||
compartment_parser: single_parenthesized_substring_verbatim
|
||||
require_exact_compartment_string_match: true
|
||||
candidate_requires_actual_kc_mbon_edge: true
|
||||
candidate_requires_actual_dan_mbon_edge: true
|
||||
candidate_requires_exact_dan_mbon_compartment_match: true
|
||||
do_not_infer_missing_compartment_from_type: true
|
||||
|
||||
plasticity:
|
||||
base_connectome_mode: read_only
|
||||
overlay_equation: effective_weight = base_weight + plastic_delta
|
||||
rule: delta_w = polarity_compartment * eta * D_compartment(t) * eligibility_ij(t)
|
||||
first_version_rule_family: compartment_specific_dopamine_gated_anti_hebbian_depression
|
||||
default_confirmed_compartment_polarity: -1
|
||||
unknown_compartment_policy: exclude_from_plastic_mask
|
||||
pam_ppl1_cluster_valence_policy: prohibited
|
||||
reward_interface_interpretation: artificial_unconditioned_signal_not_dopamine_concentration
|
||||
polarity_provenance:
|
||||
- https://pubmed.ncbi.nlm.nih.gov/26637800/
|
||||
- https://pubmed.ncbi.nlm.nih.gov/25535793/
|
||||
- https://www.nature.com/articles/s41586-024-07819-w
|
||||
eligibility_tau_ms: 1000.0
|
||||
eta_effective_mv: 0.00275
|
||||
min_effective_weight_mv: 0.0
|
||||
max_fractional_depression_per_edge: 0.25
|
||||
training_trials: 3
|
||||
d_magnitude: 1.0
|
||||
|
||||
conditioning:
|
||||
cs_population: olfactory
|
||||
cs_rate_hz: 50.0
|
||||
cs_duration_ms: 100.0
|
||||
cs_targets_per_identity: 64
|
||||
cs_identity_rule: sorted_neuron_idx_disjoint_halves
|
||||
appetitive_compartment_verbatim: "y5B'2a"
|
||||
appetitive_dan_type_required: PAM15
|
||||
aversive_compartment_verbatim: "y2a'1"
|
||||
aversive_dan_type_required: PPL103
|
||||
delayed_reward_ms: 50.0
|
||||
train_seeds: [48001, 48002, 48003, 48004, 48005]
|
||||
evaluation_seeds: [48101, 48102, 48103, 48104, 48105]
|
||||
protocols:
|
||||
- appetitive-conditioning
|
||||
- aversive-conditioning
|
||||
- reversal-learning
|
||||
- delayed-reward
|
||||
- retention
|
||||
- d-zero-control
|
||||
- random-reward-control
|
||||
- frozen-brain-control
|
||||
reward_interface_label: artificial_unconditioned_signal_interface
|
||||
|
||||
success_gate:
|
||||
require_all_frozen_checksums_pass: true
|
||||
require_phase4c_reference_spike_events_exact: true
|
||||
require_no_nan_inf_or_runaway: true
|
||||
require_minimum_seed_majority: 3
|
||||
require_appetitive_and_aversive_mbon_separation_gain: true
|
||||
require_downstream_separation_gain: true
|
||||
require_reversal_signature: true
|
||||
require_retention_signature: true
|
||||
controls_must_not_equal_conditioning_effect: true
|
||||
weight_change_alone_is_never_sufficient: true
|
||||
|
||||
device:
|
||||
required_name_substring: RTX 4090
|
||||
launcher_visible_gpu_uuid: GPU-79f3a85d-adce-7833-7003-6488140ca596
|
||||
|
||||
prohibitions:
|
||||
modify_phase1_to_phase4g: false
|
||||
conditioning_during_stage1: false
|
||||
weight_update_during_stage1: false
|
||||
infer_unknown_mapping: false
|
||||
train_base_brain: false
|
||||
@@ -0,0 +1,5 @@
|
||||
65004cfd7a8cc638a8c2176962a6deee344df530d16b9899f80272fbe5830288 phase4y-hypotheses.md
|
||||
636139eaeafa981a0c54c59a7c69e2b52b8493aa798448fa86ae6d2c6baba319 phase4y.py
|
||||
52f586ad0b4a12dbeb918f9453f9861a8dfc209118333eae5f46d27b7bce6011 phase4y.yaml
|
||||
c800c4f30767e8492853b311fb36f06f020b0aee73abcfb14561300cc9a87952 phase4y_describe.py
|
||||
53f8088d7572d5b63bc1ab623341bbab33dfdeeaf06421b775b2eaea1053ce0e manifest.json
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"bytes": 1899,
|
||||
"path": "phase4y-hypotheses.md",
|
||||
"sha256": "65004cfd7a8cc638a8c2176962a6deee344df530d16b9899f80272fbe5830288"
|
||||
},
|
||||
{
|
||||
"bytes": 13859,
|
||||
"path": "phase4y.py",
|
||||
"sha256": "636139eaeafa981a0c54c59a7c69e2b52b8493aa798448fa86ae6d2c6baba319"
|
||||
},
|
||||
{
|
||||
"bytes": 2107,
|
||||
"path": "phase4y.yaml",
|
||||
"sha256": "52f586ad0b4a12dbeb918f9453f9861a8dfc209118333eae5f46d27b7bce6011"
|
||||
},
|
||||
{
|
||||
"bytes": 9823,
|
||||
"path": "phase4y_describe.py",
|
||||
"sha256": "c800c4f30767e8492853b311fb36f06f020b0aee73abcfb14561300cc9a87952"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Independent DPM hypothesis branch
|
||||
|
||||
The user delegated selection after explicitly being asked about an independent physiological-hypothesis branch. The frozen reference remains read-only. No branch is automatically promoted.
|
||||
|
||||
Three preregistered conditions multiply only actual DPM outgoing effective currents by +1, 0 or -1. +1 is the frozen mathematical reference. 0 tests removal of the fast-current pathway, not removal of DPM neurons or all DPM functions. -1 is a sign-sensitivity experiment, not proof of GABA/serotonin receptor action on every target. No slow-receptor time constants, co-transmission model or dopamine concentration is invented.
|
||||
|
||||
The raw DPM bodies 11734 and 12569 have dopamine predictions and empty ground_truth, as verified in Phase4X. Prediction labels and the original sign array remain intact.
|
||||
|
||||
[Haynes et al., eLife 2015](https://pmc.ncbi.nlm.nih.gov/articles/PMC4305081/), doi:10.7554/eLife.03868, reports DPM GABA/5-HT signaling and inhibition in its tested preparation. It motivates the sign-sensitivity hypothesis but does not validate a global sign flip on these MaleCNS bodies. Full text was read during Phase4W; re-access during this phase returned a CAPTCHA. Prior literature and prediction caveats remain in the sealed Phase4W/4X notes.
|
||||
|
||||
The comparison asks whether an input dependency seen in isolated replay survives endogenous whole-brain feedback. It does not ask which condition looks most brain-like, has more activity or trains best. All results and failures are retained. There is no learning, candidate gate, reward sweep, extra stimulation or game interface.
|
||||
|
||||
Limitations: six previously studied odors, two fixed seeds (only one new), 1s observation, unchanged simplified LIF and frozen APL overlay. Formal biological transmitter identity, reward valence, receptor-specific physiology, association learning and long-term stability remain unproven.
|
||||
@@ -0,0 +1,161 @@
|
||||
"""Isolated in-memory hypothesis overlays; all frozen model/data files read-only."""
|
||||
from pathlib import Path
|
||||
import argparse,json,time
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch,yaml
|
||||
import phase4p,phase4p_finalize,phase4s,phase4v,phase4x
|
||||
from phase4p_revalidate import DeterministicAPLSim,door_inputs,make_drive
|
||||
from phase4r import save,digest,arrhash
|
||||
from phase4h_audit import verify_checksum
|
||||
from transmitter_roles_v1 import roles
|
||||
|
||||
def checks(cfg):
|
||||
xc=yaml.safe_load((Path(cfg['prior_model'])/'phase4x.yaml').read_text());out=phase4x.checks(xc)
|
||||
for key in ('prior_root','prior_model'):
|
||||
root=Path(cfg[key]);out['X_'+key]=verify_checksum(root,root/'checksums.sha256')
|
||||
assert all(x['passed'] for x in out.values());return out
|
||||
|
||||
class CountObserver:
|
||||
def __init__(self,rt,step):self.rt=rt;self.ext=rt.ext;self.step=step;self.before_late=None
|
||||
def __getattr__(self,name):return getattr(self.ext,name)
|
||||
def state_step(self,*args):
|
||||
if self.rt.step_index==self.step:self.before_late=self.rt.counts.cpu().numpy().copy()
|
||||
return self.ext.state_step(*args)
|
||||
|
||||
def run(cfg,exp,model):
|
||||
for d in ('protocol','audit','events','states','observations','comparisons','regression','performance'):(exp/d).mkdir(parents=True,exist_ok=True)
|
||||
assert not (exp/'protocol/preregistered.json').exists(),'Refusing overwrite'
|
||||
save(exp/'audit/preflight.json',checks(cfg))
|
||||
vc=yaml.safe_load((Path(cfg['input_model'])/'phase4v.yaml').read_text())
|
||||
oc,panel,vectors,selection,mapping=door_inputs();panel=panel[panel.split=='calibration'].reset_index(drop=True);assert len(panel)==6
|
||||
save(exp/'protocol/preregistered.json',{'config':cfg,'input_config':vc,'panel':panel.to_dict('records'),
|
||||
'sources':{p.name:digest(p) for p in model.iterdir() if p.is_file()},'before_simulation':True,
|
||||
'no_candidate_selection':True,'new_seed_is_one_replication_not_generalization_gate':True})
|
||||
pcfg=yaml.safe_load((Path(cfg['runtime_root'])/'phase4p.yaml').read_text());rt=phase4p.CanonicalRuntime(pcfg,Path(cfg['runtime_root']),exp);sim=DeterministicAPLSim(rt)
|
||||
role=roles(sim.meta);dpm=np.flatnonzero(role.annotation_DPM).astype(np.int32)
|
||||
assert sorted(sim.meta.iloc[dpm].body_id.astype(int).tolist())==cfg['perturbation']['expected_bodies']
|
||||
ids=np.flatnonzero(np.isin(sim.sources,dpm)).astype(np.int64);eg=torch.as_tensor(ids,dtype=torch.int64,device=sim.device)
|
||||
assert not set(ids)&set(sim.apl_out_ids)
|
||||
sim.meta.iloc[dpm].to_parquet(exp/'audit/DPM-source-metadata.parquet',index=False)
|
||||
pd.DataFrame({'edge_id':ids,'source_neuron_idx':sim.sources[ids],'target_neuron_idx':rt.indices_np[ids],
|
||||
'raw_synapse_count':rt.raw_np[ids],'frozen_effective_weight':sim.base[eg].cpu().numpy()}).to_parquet(exp/'audit/perturbed-existing-edges.parquet',index=False)
|
||||
original_install=sim.install;original_base_hash=arrhash(sim.base.cpu().numpy());weightchecks=[]
|
||||
original_install(None);original_overlay=rt.weights.cpu().numpy().copy()
|
||||
factor=1.
|
||||
def install(E):
|
||||
assert E is None,'Learning forbidden in diagnostic branch'
|
||||
original_install(None)
|
||||
rt.weights[eg]=sim.base[eg]*float(factor)
|
||||
sim.install=install
|
||||
for spec in cfg['conditions']:
|
||||
factor=spec['factor'];sim.install(None);got=rt.weights.cpu().numpy()
|
||||
expected=original_overlay.copy();expected[ids]=original_overlay[ids]*np.float32(factor)
|
||||
assert np.array_equal(got,expected)
|
||||
changed=np.flatnonzero(got!=original_overlay)
|
||||
assert np.all(np.isin(changed,ids))
|
||||
weightchecks.append({'condition':spec['name'],'factor':factor,'changed_effective_edges':len(changed),
|
||||
'only_declared_DPM_edges':True,'edge_array_length':len(got),'raw_weight_changes':0})
|
||||
save(exp/'regression/overlay-scope.json',weightchecks)
|
||||
populations={'all':np.arange(sim.n),'KC':sim.kc,'PN_annotation':sim.pn,'MBON':sim.mbon,'DPM':dpm,
|
||||
'DAN_annotation':np.flatnonzero(role.annotation_DAN),'MBON_downstream':sim.downstream}
|
||||
for cls in ('descending','motor'):
|
||||
populations[cls]=np.flatnonzero(sim.meta.annotation_superclass.eq(cls).to_numpy())
|
||||
save(exp/'audit/populations.json',{k:{'count':len(v),'definition':('exact annotation_superclass' if k in ('descending','motor') else 'frozen IDs or exact annotation evidence')} for k,v in populations.items()})
|
||||
target=np.unique(sim.identity.loc[sim.identity.compartment.eq(vc['target_compartment']),'source_neuron_idx'].to_numpy(np.int32))
|
||||
reg=[];stats=[];watchrows=[];poprows=[];comparisons=[];inputs=[]
|
||||
start=time.perf_counter();torch.cuda.reset_peak_memory_stats()
|
||||
def trial(targets,forced,name):
|
||||
observer=CountObserver(rt,round(cfg['late_start_ms']/sim.dt));rt.ext=observer
|
||||
try:r=phase4v.observed(sim,targets,forced,target,vc)
|
||||
finally:rt.ext=observer.ext
|
||||
assert r['finite'],'Nonfinite state; stop branch analysis'
|
||||
r['late_counts']=r['counts']-observer.before_late
|
||||
assert np.all(r['late_counts']>=0)
|
||||
return r
|
||||
for oi in cfg['odor_indices']:
|
||||
odor=panel.iloc[oi];targets,rates=make_drive(oc,panel,vectors,selection,mapping,odor.odor_id,'physiological')
|
||||
for seed in cfg['seeds']:
|
||||
actual=vc['seed_namespace']+oi*vc['odor_seed_offset']+seed
|
||||
forced=phase4v.forcing(sim,targets,rates,actual,vc,'offset')
|
||||
inputs.append({'odor':oi,'seed':seed,'actual_seed':actual,'forcing_hash':arrhash(forced.cpu().numpy()),'targets_rates_hash':arrhash(targets,rates)})
|
||||
reference=None
|
||||
for spec in cfg['conditions']:
|
||||
factor=spec['factor'];condition=spec['name'];name=f'{oi}-{seed}-{condition}'
|
||||
r=trial(targets,forced,name)
|
||||
phase4s.persist_run(exp,name,r)
|
||||
np.savez_compressed(exp/'states'/f'{name}.npz',**{k:r[k] for k in ('counts','late_counts','final_v','final_g','trace')})
|
||||
pd.DataFrame(r['samples']).to_parquet(exp/'observations'/f'{name}.parquet',index=False)
|
||||
if condition=='frozen_reference':
|
||||
reference=r
|
||||
if seed==cfg['old_replay_seed']:
|
||||
oldname=f'{oi}-{seed}-offset';old=np.load(Path(cfg['input_experiment'])/'states'/f'{oldname}.npz')
|
||||
eq={k:bool(np.array_equal(old[k],r[k])) for k in ('counts','final_v','final_g','trace')}
|
||||
ev=pd.read_parquet(Path(cfg['input_experiment'])/'events'/f'{oldname}.parquet')
|
||||
eq['KC_events']=arrhash(ev.step.to_numpy(),ev.neuron_idx.to_numpy())==arrhash(r['kc_event_step'],r['kc_event_node'])
|
||||
assert all(eq.values());reg.append({'trial':name,'test':'frozen_V_reproduction',**eq})
|
||||
assert reference is not None
|
||||
if oi==cfg['odor_indices'][0]:
|
||||
rr=trial(targets,forced,name)
|
||||
keys=('counts','late_counts','final_v','final_g','kc_event_step','kc_event_node')
|
||||
eq={k:bool(np.array_equal(r[k],rr[k])) for k in keys};assert all(eq.values())
|
||||
reg.append({'trial':name,'test':'exact_repeat',**eq})
|
||||
es=r['kc_event_step'];en=r['kc_event_node'];late=es>=round(cfg['late_start_ms']/sim.dt)
|
||||
stats.append({'odor_index':oi,'odor_name':odor.odor_name,'seed':seed,'condition':condition,
|
||||
'whole_brain_spikes':int(r['counts'].astype(np.int64).sum()),'whole_brain_active':int(np.count_nonzero(r['counts'])),
|
||||
'late_whole_brain_spikes':int(r['late_counts'].astype(np.int64).sum()),'KC_spikes':len(es),'late_KC_spikes':int(late.sum()),
|
||||
'late_KC_active':int(np.unique(en[late]).size),'finite':r['finite'],'wall_seconds':r['wall_seconds']})
|
||||
for node in cfg['watch_neuron_idx']:
|
||||
t=es[en==node];watchrows.append({'odor_index':oi,'seed':seed,'condition':condition,'neuron_idx':node,
|
||||
'total_spikes':len(t),'late_spikes':int((t>=round(cfg['late_start_ms']/sim.dt)).sum()),
|
||||
'first_ms':float(t.min()*sim.dt) if len(t) else None,'last_ms':float(t.max()*sim.dt) if len(t) else None})
|
||||
for pop,idx in populations.items():
|
||||
poprows.append({'odor_index':oi,'seed':seed,'condition':condition,'population':pop,'neurons':len(idx),
|
||||
'total_spikes':int(r['counts'][idx].astype(np.int64).sum()),'active':int(np.count_nonzero(r['counts'][idx])),
|
||||
'late_spikes':int(r['late_counts'][idx].astype(np.int64).sum()),'late_active':int(np.count_nonzero(r['late_counts'][idx]))})
|
||||
comparisons.append({'odor_index':oi,'seed':seed,'condition':condition,
|
||||
'neurons_with_changed_counts':int(np.count_nonzero(r['counts']!=reference['counts'])),
|
||||
'max_abs_finalV_difference':float(np.abs(r['final_v'].astype(float)-reference['final_v']).max()),
|
||||
'KC_event_sequence_equal':arrhash(es,en)==arrhash(reference['kc_event_step'],reference['kc_event_node'])})
|
||||
save(exp/'regression/trials.json',reg)
|
||||
print(json.dumps({'odor_complete':oi,'seed':seed,'trials':len(stats)}),flush=True)
|
||||
sim.install=original_install;original_install(None)
|
||||
assert arrhash(sim.base.cpu().numpy())==original_base_hash
|
||||
assert np.array_equal(rt.weights.cpu().numpy(),original_overlay)
|
||||
save(exp/'regression/restore.json',{'immutable_memory_base_hash_exact':True,'frozen_APL_overlay_restored_exact':True})
|
||||
assert phase4s.evidence_regression(cfg,exp,rt,sim)
|
||||
for name,data in [('trials',stats),('watched-KC',watchrows),('populations',poprows),('paired-differences',comparisons)]:pd.DataFrame(data).to_parquet(exp/'comparisons'/f'{name}.parquet',index=False)
|
||||
save(exp/'protocol/input-hashes.json',inputs)
|
||||
save(exp/'audit/results.json',{'phase':'4Y','status':'COMPLETE-EXPERIMENTAL-HYPOTHESIS-AUDIT','trials':len(stats),'repeat_trials':6,
|
||||
'old_reference_reproductions':6,'frozen_reference_trials':5,'reference_pass':True,'all_finite':True,
|
||||
'DPM_neurons':len(dpm),'DPM_existing_edges':len(ids),'weight_training':False,'candidate_selected':False,
|
||||
'frozen_baseline_changed':False,'biological_sign_proven':False,'new_seed_count':1,'old_seed_count':1})
|
||||
save(exp/'performance/summary.json',{'wall_seconds_excluding_build':time.perf_counter()-start,'peak_VRAM_bytes':torch.cuda.max_memory_allocated(),
|
||||
'GPU':torch.cuda.get_device_name(0),'diagnostic_observation_overhead_included':True})
|
||||
|
||||
def seal(cfg,exp,model):
|
||||
pre=json.loads((exp/'protocol/preregistered.json').read_text());now=checks(cfg);hist=phase4p_finalize.integrity()
|
||||
ok=hist['all_static_checks_passed'] and now==json.loads((exp/'audit/preflight.json').read_text()) and all(digest(model/n)==h for n,h in pre['sources'].items())
|
||||
save(exp/'audit/frozen-integrity.json',{'pass':ok,'history':hist,'current':now});assert ok
|
||||
result=json.loads((exp/'audit/results.json').read_text());result['integrity_pass']=True;save(exp/'audit/results.json',result)
|
||||
w=pd.read_parquet(exp/'comparisons/watched-KC.parquet');t=pd.read_parquet(exp/'comparisons/trials.parquet')
|
||||
lines=['# Phase 4Y — independent DPM fast-current hypotheses','',
|
||||
'Status: COMPLETE-EXPERIMENTAL-HYPOTHESIS-AUDIT. No candidate selection, training, game integration or frozen baseline changes.',
|
||||
'All conditions use identical PN forcing for 120ms, then off to 1000ms. Six old odors, one old seed and one new seed. Not a population-level independent generalization gate.',
|
||||
'Factors 0 and -1 are diagnostic model interventions, not corrections to raw labels or validated receptor-specific physiology. Only actual outgoing DPM effective currents change in RAM. Graph and raw counts stay intact.','',
|
||||
'| KC | condition | late spike range (12 trials) |','|---:|---|---:|']
|
||||
for (node,condition),g in w.groupby(['neuron_idx','condition']):lines.append(f'| {node} | {condition} | {g.late_spikes.min()}–{g.late_spikes.max()} |')
|
||||
lines+=['','| condition | mean whole-brain spikes | mean late KC spikes | mean late active KC |','|---|---:|---:|---:|']
|
||||
for condition,g in t.groupby('condition'):lines.append(f'| {condition} | {g.whole_brain_spikes.mean():.2f} | {g.late_KC_spikes.mean():.2f} | {g.late_KC_active.mean():.2f} |')
|
||||
lines+=['','Exact controls: six Phase4V original trials, six repeats, five frozen reference trials, unchanged in-memory base hash and restored frozen overlay.',
|
||||
'Finite states do not establish biological stability. No calibrated runaway threshold, biological sign conclusion, receptor kinetics or learning success is inferred.',
|
||||
'Whole-brain intervention outcomes may differ from prior isolated replay because all feedback responds. Report this difference, do not select a condition by favorable activity.']
|
||||
(exp/'REPORT.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
|
||||
for root in (model,exp):
|
||||
rows=[{'path':p.relative_to(root).as_posix(),'sha256':digest(p),'bytes':p.stat().st_size} for p in sorted(root.rglob('*')) if p.is_file() and p.name not in ('manifest.json','checksums.sha256')]
|
||||
save(root/'manifest.json',{'files':rows});rows.append({'path':'manifest.json','sha256':digest(root/'manifest.json')})
|
||||
(root/'checksums.sha256').write_text(''.join(x['sha256']+' '+x['path']+'\n' for x in rows));q=verify_checksum(root,root/'checksums.sha256');assert q['passed'];print(json.dumps(q),flush=True)
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser();p.add_argument('task',choices=['run','seal']);p.add_argument('--config',required=True);a=p.parse_args()
|
||||
cfg=yaml.safe_load(Path(a.config).read_text());(run if a.task=='run' else seal)(cfg,Path(cfg['experiment_root']),Path(cfg['model_root']))
|
||||
@@ -0,0 +1,39 @@
|
||||
phase: 4Y
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4y-dpm-hypothesis
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif/dpm-hypothesis-v1
|
||||
prior_root: /data/digital-drosophila/experiments/phase4x-transmitter-provenance
|
||||
prior_model: /data/digital-drosophila/models/male-cns-lif/transmitter-audit-v1
|
||||
runtime_root: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2
|
||||
phase4p_root: /data/digital-drosophila/experiments/phase4p-deterministic-runtime
|
||||
input_model: /data/digital-drosophila/models/male-cns-lif/csus-audit-v1
|
||||
input_experiment: /data/digital-drosophila/experiments/phase4v-csus-semantics
|
||||
odor_indices: [0,1,2,3,4,5]
|
||||
seeds: [131101,141101]
|
||||
old_replay_seed: 131101
|
||||
conditions:
|
||||
- {name: frozen_reference, factor: 1.0}
|
||||
- {name: DPM_fast_current_off, factor: 0.0}
|
||||
- {name: DPM_fast_current_reversed, factor: -1.0}
|
||||
perturbation:
|
||||
exact_type: DPM
|
||||
expected_bodies: [11734,12569]
|
||||
scope: all and only outgoing effective currents of the two annotation-DPM neurons
|
||||
no_fast_current: diagnostic ablation; not deletion of neurons or anatomical edges and not a complete slow-modulator model
|
||||
reversed: sign-sensitivity hypothesis; not verified receptor-specific GABA/serotonin physiology
|
||||
input_condition: same original 120ms PN forcing, then off to 1000ms
|
||||
late_start_ms: 500
|
||||
watch_neuron_idx: [59832,78836,82312]
|
||||
repeat_check: first odor and both seeds, all three conditions
|
||||
scope:
|
||||
authorization: user delegated independent hypothesis-branch decision after explicit request for authorization
|
||||
diagnostic_only: true
|
||||
baseline_read_only: true
|
||||
weight_training: false
|
||||
slow_receptor_parameters_invented: false
|
||||
candidate_selection: false
|
||||
game_or_RL: false
|
||||
reserved_novel_odors: untouched
|
||||
interpretation:
|
||||
report: all conditions and both seeds; do not pick the most active or best-looking result
|
||||
causal_scope: effect of intervention on frozen mathematical model, not proof of biological neurotransmitter identity
|
||||
stability: report nonfinite state, brain spike counts, active fractions, and KC late activity; no biological runaway threshold inferred
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Descriptive paired effects only; no condition selection or parameter fitting."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import yaml
|
||||
from phase4r import save
|
||||
|
||||
cfg=yaml.safe_load(Path(__file__).with_name('phase4y.yaml').read_text());exp=Path(cfg['experiment_root'])
|
||||
assert not (exp/'checksums.sha256').exists(),'Sealed output is immutable'
|
||||
w=pd.read_parquet(exp/'comparisons/watched-KC.parquet');t=pd.read_parquet(exp/'comparisons/trials.parquet')
|
||||
p=pd.read_parquet(exp/'comparisons/populations.parquet');x=pd.read_parquet(Path(cfg['prior_root'])/'replay/results.parquet')
|
||||
inputrows=[]
|
||||
for _,r in t.iterrows():
|
||||
obs=pd.read_parquet(exp/'observations'/f'{r.odor_index}-{r.seed}-{r.condition}.parquet').iloc[-1]
|
||||
inputrows.append({'odor_index':int(r.odor_index),'seed':int(r.seed),'condition':r.condition,
|
||||
'requested':int(obs.PN_requested_cumulative),'accepted_new_spikes':int(obs.PN_accepted_cumulative),
|
||||
'preexisting_spikes':int(obs.PN_preexisting_cumulative),'refractory_blocked':int(obs.PN_blocked_cumulative)})
|
||||
inp=pd.DataFrame(inputrows);assert (inp.groupby(['odor_index','seed']).requested.nunique()==1).all()
|
||||
assert (inp.requested==inp.accepted_new_spikes+inp.preexisting_spikes+inp.refractory_blocked).all()
|
||||
inp.to_parquet(exp/'comparisons/input-request-vs-acceptance.parquet',index=False)
|
||||
save(exp/'audit/input-matching.json',{'scheduled_requests_matched_all_pairs':True,'partition_exact':True,
|
||||
'pairs_with_changed_accepted_count':int((inp.groupby(['odor_index','seed']).accepted_new_spikes.nunique()>1).sum()),
|
||||
'interpretation':'identical exogenous requests, not guaranteed identical accepted PN spikes; acceptance depends on endogenous refractory/spike state through the unchanged force interface'})
|
||||
# Reporting-only schema correction. Preserve original tables and the registered run source.
|
||||
# These exact superclass values are used by the frozen Phase4C propagation summaries.
|
||||
meta_path=Path('/data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only/neuron-index/unified-metadata.parquet')
|
||||
sup=pd.read_parquet(meta_path,columns=['neuron_idx','annotation_superclass']).sort_values('neuron_idx')
|
||||
definitions={'descending':['descending_neuron','descending_neuron_tbc'],'motor':['vnc_motor','cb_motor']}
|
||||
corrected=[]
|
||||
for _,r in t.iterrows():
|
||||
name=f'{r.odor_index}-{r.seed}-{r.condition}'
|
||||
with np.load(exp/'states'/f'{name}.npz') as z:
|
||||
for pop,values in definitions.items():
|
||||
idx=np.flatnonzero(sup.annotation_superclass.isin(values).to_numpy());assert len(idx)>0
|
||||
c=z['counts'][idx];l=z['late_counts'][idx]
|
||||
corrected.append({'odor_index':int(r.odor_index),'seed':int(r.seed),'condition':r.condition,'population':pop,
|
||||
'neurons':len(idx),'total_spikes':int(c.astype(np.int64).sum()),'active':int(np.count_nonzero(c)),
|
||||
'late_spikes':int(l.astype(np.int64).sum()),'late_active':int(np.count_nonzero(l))})
|
||||
p=pd.concat([p[~p.population.isin(definitions)],pd.DataFrame(corrected)],ignore_index=True)
|
||||
p.to_parquet(exp/'comparisons/populations-corrected.parquet',index=False)
|
||||
save(exp/'audit/population-schema-correction.json',{'scope':'report aggregation only, no dynamics or registered conditions changed',
|
||||
'issue':'initial literal superclass descending/motor matches zero rows; those zero-population rows are invalid readouts, not evidence of silence',
|
||||
'definitions_from':'frozen phase4c.py summary definitions, verified against actual metadata',
|
||||
'corrected_values':definitions,'corrected_counts':{k:int(sup.annotation_superclass.isin(v).sum()) for k,v in definitions.items()},
|
||||
'source':str(meta_path),'original_tables_preserved':True,'recomputed_from':'saved complete per-neuron total and late counts','simulation_rerun':False})
|
||||
(exp/'READ-ME-FIRST.md').write_text(
|
||||
'# Phase 4Y evidence routing\n\n'
|
||||
'Use audit/MECHANISM.md and comparisons/populations-corrected.parquet for population readouts.\n\n'
|
||||
'The original comparisons/populations.parquet descending/motor rows used invalid literal superclass names and matched zero neurons. '
|
||||
'Those rows are preserved for audit but must not be interpreted as no activity. Corrected rows use the frozen Phase4C definitions '
|
||||
'(descending_neuron/descending_neuron_tbc; vnc_motor/cb_motor) and were recomputed from saved complete counts, without rerunning dynamics. '
|
||||
'See audit/population-schema-correction.json. Other original population rows are unchanged.\n\n'
|
||||
'All three hypotheses are reported. None is promoted to a learned or biological baseline.\n',encoding='utf-8')
|
||||
effects=[];isolated=[]
|
||||
for (oi,seed,node),g in w.groupby(['odor_index','seed','neuron_idx']):
|
||||
baseline=int(g.loc[g.condition.eq('frozen_reference'),'late_spikes'].iloc[0])
|
||||
for _,r in g.iterrows():
|
||||
effects.append({'odor_index':int(oi),'seed':int(seed),'neuron_idx':int(node),'condition':r.condition,
|
||||
'baseline_late_spikes':baseline,'condition_late_spikes':int(r.late_spikes),'paired_change':int(r.late_spikes)-baseline})
|
||||
if seed==cfg['old_replay_seed']:
|
||||
name=f'{oi}-{seed}-offset';z=x[(x.trial==name)&(x.neuron_idx==node)]
|
||||
ib=int(z.loc[z['mode'].eq('base'),'late_spikes'].iloc[0]);io=int(z.loc[z['mode'].eq('omit_DPM_annotation'),'late_spikes'].iloc[0])
|
||||
off=int(g.loc[g.condition.eq('DPM_fast_current_off'),'late_spikes'].iloc[0]);assert ib==baseline
|
||||
isolated.append({'odor_index':int(oi),'neuron_idx':int(node),'baseline_late_spikes':baseline,
|
||||
'isolated_DPM_off_late_spikes':io,'whole_brain_DPM_off_late_spikes':off,
|
||||
'whole_minus_isolated':off-io,'interpretation':'different feedback assumptions; not a numerical regression failure'})
|
||||
pd.DataFrame(effects).to_parquet(exp/'comparisons/watched-paired-effects.parquet',index=False)
|
||||
pd.DataFrame(isolated).to_parquet(exp/'comparisons/isolated-vs-whole-brain.parquet',index=False)
|
||||
new=[];loss=[]
|
||||
metadata=pd.read_parquet(Path(cfg['prior_root'])/'roles/neuron-evidence-axes.parquet').set_index('neuron_idx')
|
||||
for (oi,seed),group in t.groupby(['odor_index','seed']):
|
||||
def late_set(condition):
|
||||
e=pd.read_parquet(exp/'events'/f'{oi}-{seed}-{condition}.parquet');return set(map(int,e.loc[e.step>=5000,'neuron_idx']))
|
||||
ref=late_set('frozen_reference')
|
||||
for condition in ('DPM_fast_current_off','DPM_fast_current_reversed'):
|
||||
s=late_set(condition)
|
||||
for label,nodes in [('new',s-ref),('lost',ref-s)]:
|
||||
for node in sorted(nodes):
|
||||
row={'odor_index':int(oi),'seed':int(seed),'condition':condition,'change':label,'neuron_idx':node,
|
||||
'body_id':int(metadata.loc[node,'body_id']),'annotation_type':metadata.loc[node,'annotation_type']}
|
||||
(new if label=='new' else loss).append(row)
|
||||
pd.DataFrame(new+loss,columns=['odor_index','seed','condition','change','neuron_idx','body_id','annotation_type']).to_parquet(exp/'comparisons/late-KC-membership-changes.parquet',index=False)
|
||||
ep=pd.DataFrame(effects);summary=[]
|
||||
for (node,condition),g in ep.groupby(['neuron_idx','condition']):
|
||||
summary.append({'neuron_idx':int(node),'condition':condition,'n':len(g),'late_min':int(g.condition_late_spikes.min()),
|
||||
'late_max':int(g.condition_late_spikes.max()),'change_min':int(g.paired_change.min()),'change_max':int(g.paired_change.max()),
|
||||
'decreases':int((g.paired_change<0).sum()),'increases':int((g.paired_change>0).sum()),'unchanged':int((g.paired_change==0).sum())})
|
||||
save(exp/'audit/descriptive-effects.json',{'scope':'post-run descriptive summaries; no promotion or new gate',
|
||||
'watched':summary,'new_late_KC_memberships':len(new),'lost_late_KC_memberships':len(loss)})
|
||||
lines=['# Phase 4Y — paired whole-brain effects','',
|
||||
'All branches are diagnostic hypotheses; none is selected as a better biological model.','',
|
||||
'| KC | condition | late spike range | paired change range | decreases / unchanged / increases |','|---:|---|---:|---:|---:|']
|
||||
for r in summary:lines.append(f"| {r['neuron_idx']} | {r['condition']} | {r['late_min']}–{r['late_max']} | {r['change_min']}–{r['change_max']} | {r['decreases']} / {r['unchanged']} / {r['increases']} |")
|
||||
lines+=['','| KC | isolated DPM-off range | whole-brain DPM-off range (same old seed) |','|---:|---:|---:|']
|
||||
for node,g in pd.DataFrame(isolated).groupby('neuron_idx'):
|
||||
lines.append(f'| {node} | {g.isolated_DPM_off_late_spikes.min()}–{g.isolated_DPM_off_late_spikes.max()} | {g.whole_brain_DPM_off_late_spikes.min()}–{g.whole_brain_DPM_off_late_spikes.max()} |')
|
||||
lines+=['','| population | condition | mean late spikes | mean late active neurons |','|---|---|---:|---:|']
|
||||
for (pop,condition),g in p.groupby(['population','condition']):
|
||||
lines.append(f'| {pop} | {condition} | {g.late_spikes.mean():.2f} | {g.late_active.mean():.2f} |')
|
||||
lines+=['','Whole-brain feedback is allowed to respond; isolated replay holds the other recorded arrivals fixed. A difference between them is the target of the experiment, not grounds for choosing one result.',
|
||||
'Input matching is at the scheduled request level. Accepted new PN spikes can differ through feedback-dependent refractory state; see input-request-vs-acceptance.parquet. Do not claim identical delivered PN spike trains.',
|
||||
'Descending/motor rows here use populations-corrected.parquet, reconstructed from saved full counts and verified actual superclass names. The original zero-population rows are retained but invalid; see audit/population-schema-correction.json.',
|
||||
'No learning, no reward events, no parameter fitting, no promotion; smaller late activity is not itself success. Biological signs and long-term stability remain unproven.']
|
||||
(exp/'audit/MECHANISM.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
|
||||
print('\n'.join(lines[:20]))
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"file_count": 8,
|
||||
"files": [
|
||||
{
|
||||
"bytes": 228,
|
||||
"path": "VERSION.json",
|
||||
"sha256": "e0b478bc714ff90bd398fae7a726bee0e8dc9975f8d967b9fcf6d360fcb07100"
|
||||
},
|
||||
{
|
||||
"bytes": 19444,
|
||||
"path": "__pycache__/brain_runtime.cpython-310.pyc",
|
||||
"sha256": "d6df50e3617ec0f7893aa30b94b3dbd55359d3f1f10cf960e825e35d96cea097"
|
||||
},
|
||||
{
|
||||
"bytes": 1807,
|
||||
"path": "__pycache__/frozen_interface.cpython-310.pyc",
|
||||
"sha256": "c34e3584aab378b2394451c64069f8b928503c45308580ed17fb5ea1d732556b"
|
||||
},
|
||||
{
|
||||
"bytes": 38424,
|
||||
"path": "__pycache__/phase4g.cpython-310.pyc",
|
||||
"sha256": "76ccac2f2ed2135dfda5d7b21307aaff5fd3f8db3dd607c36741b070db32b295"
|
||||
},
|
||||
{
|
||||
"bytes": 23822,
|
||||
"path": "brain_runtime.py",
|
||||
"sha256": "1c4e3e6f06708bfec30b24e92297f320b2b8b87ebbbe6da501f5f2db4f74762a"
|
||||
},
|
||||
{
|
||||
"bytes": 1229,
|
||||
"path": "frozen_interface.py",
|
||||
"sha256": "4e4142cd5e866cfdbc9d56ffdff5eac9081a79e00c1658611f326447013b8fdb"
|
||||
},
|
||||
{
|
||||
"bytes": 40435,
|
||||
"path": "phase4g.py",
|
||||
"sha256": "c82ff049336a4815690d4060f001c77266fe70d9e62ec6a940741d9fa9721e4f"
|
||||
},
|
||||
{
|
||||
"bytes": 4795,
|
||||
"path": "phase4g.yaml",
|
||||
"sha256": "678798511cc14181d22a2085dedb412b44a96a966398678c12c27a8c561be042"
|
||||
}
|
||||
],
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/frozen-interface-v1",
|
||||
"total_bytes": 130184
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"candidate_name": null,
|
||||
"learned_state_root": "/data/digital-drosophila/models/learned/phase4g",
|
||||
"male_cns_weights_fixed": true,
|
||||
"name": "MaleCNS-FrozenInterface-v1",
|
||||
"status": "FAIL-NO-FROZEN-INTERFACE-CANDIDATE"
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stable game-agnostic Brain I/O API for MaleCNS-LIF-runtime-v1."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
import numpy as np
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
|
||||
API_VERSION = "1.0.0"
|
||||
INPUT_BATCH_VERSION = "1.0.0"
|
||||
ACTIVITY_STREAM_VERSION = "1.0.0"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InputEvent:
|
||||
target_neuron_idx: np.ndarray
|
||||
stimulation_mode: str
|
||||
value_kind: str
|
||||
value: float | np.ndarray
|
||||
start_ms: float
|
||||
duration_ms: float
|
||||
source_id: str
|
||||
channel_id: str
|
||||
amplitude_mv: float = 0.0
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def validate(self, neuron_count: int, dt_ms: float) -> None:
|
||||
target = np.asarray(self.target_neuron_idx)
|
||||
if target.ndim != 1 or target.dtype.kind not in "iu":
|
||||
raise ValueError("target_neuron_idx must be a one-dimensional integer array")
|
||||
if len(target) and (int(target.min()) < 0 or int(target.max()) >= neuron_count):
|
||||
raise ValueError("target neuron_idx outside canonical range")
|
||||
if len(np.unique(target)) != len(target):
|
||||
raise ValueError("target neuron_idx values must be unique")
|
||||
if self.stimulation_mode not in {"spike_injection", "external_synaptic_drive"}:
|
||||
raise ValueError(f"unsupported stimulation_mode {self.stimulation_mode!r}")
|
||||
if self.value_kind not in {"binary", "scalar_intensity", "poisson_rate_hz", "signed_modulation"}:
|
||||
raise ValueError(f"unsupported value_kind {self.value_kind!r}")
|
||||
if self.start_ms < 0 or self.duration_ms <= 0:
|
||||
raise ValueError("timestamps/duration must be non-negative/positive")
|
||||
for x in (self.start_ms, self.duration_ms):
|
||||
if not math.isclose(x / dt_ms, round(x / dt_ms), abs_tol=1e-8):
|
||||
raise ValueError(f"{x} ms is not aligned to dt={dt_ms} ms")
|
||||
value = np.asarray(self.value)
|
||||
if value.ndim > 1 or (value.ndim == 1 and len(value) not in (1, len(target))):
|
||||
raise ValueError("value must be scalar or match target count")
|
||||
if self.value_kind == "poisson_rate_hz" and np.any(value < 0):
|
||||
raise ValueError("Poisson rate cannot be negative")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InputBatch:
|
||||
events: tuple[InputEvent, ...]
|
||||
batch_id: str
|
||||
version: str = INPUT_BATCH_VERSION
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def validate(self, neuron_count: int, dt_ms: float) -> None:
|
||||
if self.version != INPUT_BATCH_VERSION:
|
||||
raise ValueError(f"InputBatch version must be {INPUT_BATCH_VERSION}")
|
||||
for event in self.events:
|
||||
event.validate(neuron_count, dt_ms)
|
||||
|
||||
|
||||
class VisualAdapterV1:
|
||||
"""Maps abstract channels to audited visual hierarchy populations."""
|
||||
|
||||
def __init__(self, config: dict[str, Any], phase4c_root: Path):
|
||||
self.config = config
|
||||
self.phase4c_root = phase4c_root
|
||||
self.modes: dict[str, np.ndarray] = {}
|
||||
for mode, spec in config["visual_adapter"]["modes"].items():
|
||||
population = spec["hierarchy_population"]
|
||||
path = phase4c_root / "visual-audit" / f"hierarchy-{population}.parquet"
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(path)
|
||||
self.modes[mode] = pq.read_table(path, columns=["neuron_idx"])["neuron_idx"].to_numpy().astype(np.int64)
|
||||
|
||||
def population(self, mode: str) -> np.ndarray:
|
||||
if mode not in self.modes:
|
||||
raise KeyError(mode)
|
||||
return self.modes[mode].copy()
|
||||
|
||||
def encode(
|
||||
self, mode: str, channel_values: np.ndarray, start_ms: float, duration_ms: float,
|
||||
source_id: str, channel_id: str, value_kind: str = "poisson_rate_hz",
|
||||
) -> InputBatch:
|
||||
if mode not in self.modes:
|
||||
raise KeyError(mode)
|
||||
spec = self.config["visual_adapter"]["modes"][mode]
|
||||
targets = self.modes[mode]
|
||||
values = np.asarray(channel_values, dtype=np.float32)
|
||||
if values.shape != (len(targets),):
|
||||
raise ValueError(f"visual channel must have shape {(len(targets),)}, got {values.shape}")
|
||||
selected = np.flatnonzero(values != 0).astype(np.int64)
|
||||
target = targets[selected]
|
||||
selected_values = values[selected]
|
||||
event = InputEvent(
|
||||
target_neuron_idx=target,
|
||||
stimulation_mode=spec["input_mode"],
|
||||
value_kind=value_kind,
|
||||
value=selected_values,
|
||||
amplitude_mv=float(self.config["visual_adapter"]["external_drive_mv"]),
|
||||
start_ms=start_ms,
|
||||
duration_ms=duration_ms,
|
||||
source_id=source_id,
|
||||
channel_id=channel_id,
|
||||
metadata={
|
||||
"adapter": self.config["names"]["visual_adapter"],
|
||||
"adapter_mode": mode,
|
||||
"biological_fidelity": spec["biological_fidelity"],
|
||||
"connectome_weights_modified": False,
|
||||
},
|
||||
)
|
||||
return InputBatch(events=(event,), batch_id=f"{source_id}:{channel_id}:{start_ms}", metadata={"adapter_mode": mode})
|
||||
|
||||
|
||||
class BrainRuntime:
|
||||
"""Persistent exact-state BrainRuntime API. It has no game-specific dependency."""
|
||||
|
||||
def __init__(self, config_path: Path, extension_build_dir: Path):
|
||||
self.config_path = config_path.resolve()
|
||||
self.cfg = yaml.safe_load(self.config_path.read_text(encoding="utf-8"))
|
||||
self.dt = float(self.cfg["model"]["dt_ms"])
|
||||
self.n = 166700
|
||||
self.model_root = Path(self.cfg["paths"]["model_root"])
|
||||
self.runtime_root = Path(self.cfg["paths"]["runtime_root"])
|
||||
self.input_root = Path(self.cfg["paths"]["connectome_root"])
|
||||
runtime_module_dir = str(self.runtime_root)
|
||||
if runtime_module_dir not in sys.path:
|
||||
sys.path.insert(0, runtime_module_dir)
|
||||
import phase4c as runtime_module
|
||||
self.runtime_module = runtime_module
|
||||
p4cfg = yaml.safe_load((self.runtime_root / "phase4c.yaml").read_text(encoding="utf-8"))
|
||||
self.paths = runtime_module.exact_paths(p4cfg)
|
||||
self.indptr_np, self.indices_np, self.raw_np = runtime_module.p4b.validate_graph(p4cfg, self.paths)
|
||||
self.table = runtime_module.full_metadata(self.paths["metadata"])
|
||||
self.population_masks = runtime_module.populations(self.table)
|
||||
if not torch.cuda.is_available() or torch.cuda.device_count() != 1:
|
||||
raise RuntimeError("BrainRuntime requires exactly one visible CUDA GPU")
|
||||
self.device = torch.device("cuda:0")
|
||||
props = torch.cuda.get_device_properties(self.device)
|
||||
if self.cfg["device"]["required_name_substring"] not in props.name:
|
||||
raise RuntimeError(f"required RTX 4090, got {props.name}")
|
||||
extension_build_dir.mkdir(parents=True, exist_ok=True)
|
||||
build_experiment_root = extension_build_dir.parent.parent
|
||||
(build_experiment_root / "compiler-reports/extension-build").mkdir(parents=True, exist_ok=True)
|
||||
self.extension, report = runtime_module.build_extension(self.runtime_root, build_experiment_root)
|
||||
if self.extension is None:
|
||||
raise RuntimeError(f"runtime-v1 extension unavailable: {report}")
|
||||
base = runtime_module.Runtime(p4cfg, self.table, self.indptr_np, self.indices_np, self.raw_np, self.device, self.extension)
|
||||
self.indptr, self.indices, self.weights = base.indptr, base.indices, base.weights
|
||||
self.signs, self.superclass = base.signs, base.superclass
|
||||
self.neuron_type = np.asarray(self.table["annotation_type"].to_pylist(), dtype=object)
|
||||
self.neuron_class = np.asarray(self.table["annotation_class"].to_pylist(), dtype=object)
|
||||
self.body_id = self.table["body_id"].to_numpy().astype(np.uint64)
|
||||
m = self.cfg["model"]
|
||||
self.delay_steps = int(round(float(m["synaptic_delay_ms"]) / self.dt))
|
||||
self.queue_len = self.delay_steps + 1
|
||||
self.refractory_steps = int(round(float(m["refractory_ms"]) / self.dt))
|
||||
self.alpha = self.dt / float(m["membrane_tau_ms"])
|
||||
self.g_decay = 1.0 - self.dt / float(m["synaptic_tau_ms"])
|
||||
self.rest = float(m["resting_potential_mv"])
|
||||
self.reset_value = float(m["reset_potential_mv"])
|
||||
self.threshold = float(m["threshold_mv"])
|
||||
self.output_populations = self._output_populations()
|
||||
self.reset(0)
|
||||
|
||||
def _output_populations(self) -> dict[str, np.ndarray]:
|
||||
s = self.superclass
|
||||
return {
|
||||
"descending": np.flatnonzero(np.isin(s, ["descending_neuron", "descending_neuron_tbc"])).astype(np.int32),
|
||||
"motor_related": np.flatnonzero(np.isin(s, ["vnc_motor", "cb_motor"])).astype(np.int32),
|
||||
"ascending": np.flatnonzero(np.isin(s, ["ascending_neuron", "sensory_ascending", "sensory_ascending_tbc", "efferent_ascending"])).astype(np.int32),
|
||||
}
|
||||
|
||||
def reset(self, seed: int, initial_state: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
self.seed = int(seed)
|
||||
self.generator = torch.Generator(device=self.device); self.generator.manual_seed(self.seed)
|
||||
self.v = torch.full((self.n,), self.rest, dtype=torch.float32, device=self.device)
|
||||
self.g = torch.zeros(self.n, dtype=torch.float32, device=self.device)
|
||||
self.refractory = torch.zeros(self.n, dtype=torch.int32, device=self.device)
|
||||
self.delay_queue = torch.zeros((self.queue_len, self.n), dtype=torch.float32, device=self.device)
|
||||
self.spike_counts = torch.zeros(self.n, dtype=torch.int32, device=self.device)
|
||||
self.simulation_step = 0; self.sequence_id = 0
|
||||
self.spike_steps: list[np.ndarray] = []; self.spike_nodes: list[np.ndarray] = []
|
||||
self.spike_origins: list[np.ndarray] = []
|
||||
self.drive_steps: list[np.ndarray] = []; self.drive_nodes: list[np.ndarray] = []
|
||||
self.drive_values: list[np.ndarray] = []; self.input_audit: list[dict[str, Any]] = []
|
||||
if initial_state is not None:
|
||||
self.restore_state(initial_state)
|
||||
return self.get_state_summary()
|
||||
|
||||
@property
|
||||
def simulation_time_ms(self) -> float:
|
||||
return self.simulation_step * self.dt
|
||||
|
||||
def _event_values(self, event: InputEvent, targets: torch.Tensor) -> torch.Tensor:
|
||||
value = np.asarray(event.value, dtype=np.float32)
|
||||
if value.ndim == 0:
|
||||
return torch.full((len(targets),), float(value), dtype=torch.float32, device=self.device)
|
||||
return torch.as_tensor(value, dtype=torch.float32, device=self.device)
|
||||
|
||||
def _active(self, event: InputEvent, time_ms: float) -> bool:
|
||||
return event.start_ms - 1e-9 <= time_ms < event.start_ms + event.duration_ms - 1e-9
|
||||
|
||||
def _hybrid_edges(self, sources: torch.Tensor) -> torch.Tensor:
|
||||
starts = self.indptr[sources]
|
||||
degrees = self.indptr[sources + 1] - starts
|
||||
cumulative = torch.cumsum(degrees, 0)
|
||||
total = int(cumulative[-1].item()) if cumulative.numel() else 0
|
||||
output = torch.empty(total, dtype=torch.int64, device=self.device)
|
||||
if total:
|
||||
self.extension.fill_edges(self.indptr, sources, cumulative - degrees, output)
|
||||
return output
|
||||
|
||||
def step(self, input_batch: InputBatch | None, biological_ms: float) -> dict[str, Any]:
|
||||
steps = int(round(float(biological_ms) / self.dt))
|
||||
if not math.isclose(steps * self.dt, float(biological_ms), abs_tol=1e-8) or steps <= 0:
|
||||
raise ValueError("biological_ms must be a positive multiple of dt")
|
||||
batch = input_batch or InputBatch(events=(), batch_id="zero-input")
|
||||
batch.validate(self.n, self.dt)
|
||||
batch_start = self.simulation_time_ms
|
||||
prepared = [(e, torch.as_tensor(e.target_neuron_idx, dtype=torch.int64, device=self.device)) for e in batch.events]
|
||||
for event in batch.events:
|
||||
self.input_audit.append({
|
||||
"batch_id": batch.batch_id, "source_id": event.source_id, "channel_id": event.channel_id,
|
||||
"mode": event.stimulation_mode, "value_kind": event.value_kind,
|
||||
"start_ms": event.start_ms, "duration_ms": event.duration_ms,
|
||||
"target_count": int(len(event.target_neuron_idx)), "metadata": event.metadata,
|
||||
"external_drive_is_connectome_transmission": False,
|
||||
})
|
||||
start_event_count = sum(len(x) for x in self.spike_nodes)
|
||||
start_drive_count = sum(len(x) for x in self.drive_nodes)
|
||||
for _ in range(steps):
|
||||
global_step = self.simulation_step; time_ms = self.simulation_time_ms
|
||||
slot = global_step % self.queue_len
|
||||
self.g.mul_(self.g_decay).add_(self.delay_queue[slot]); self.delay_queue[slot].zero_()
|
||||
spike_requests: list[tuple[InputEvent, torch.Tensor, torch.Tensor]] = []
|
||||
for event, targets in prepared:
|
||||
if not self._active(event, time_ms) or targets.numel() == 0:
|
||||
continue
|
||||
values = self._event_values(event, targets)
|
||||
if event.value_kind == "poisson_rate_hz":
|
||||
selected = torch.rand(len(targets), generator=self.generator, device=self.device) < (values * self.dt / 1000.0)
|
||||
elif event.value_kind == "binary":
|
||||
selected = values != 0
|
||||
elif event.value_kind == "scalar_intensity":
|
||||
selected = values != 0
|
||||
else: # signed_modulation
|
||||
selected = torch.rand(len(targets), generator=self.generator, device=self.device) < (torch.abs(values) * self.dt / 1000.0)
|
||||
if event.stimulation_mode == "external_synaptic_drive":
|
||||
chosen = targets[selected]
|
||||
if chosen.numel():
|
||||
signed_value = values[selected] * float(event.amplitude_mv)
|
||||
if event.value_kind == "poisson_rate_hz":
|
||||
signed_value = torch.full_like(signed_value, float(event.amplitude_mv))
|
||||
self.g.index_add_(0, chosen, signed_value)
|
||||
node_np = chosen.cpu().numpy().astype(np.int32, copy=True)
|
||||
val_np = signed_value.cpu().numpy().astype(np.float32, copy=True)
|
||||
self.drive_steps.append(np.full(len(node_np), global_step, dtype=np.int32))
|
||||
self.drive_nodes.append(node_np); self.drive_values.append(val_np)
|
||||
else:
|
||||
spike_requests.append((event, targets, selected))
|
||||
self.refractory.clamp_min_(1).sub_(1)
|
||||
available = self.refractory == 0
|
||||
self.v[available] += self.alpha * (self.g[available] - (self.v[available] - self.rest))
|
||||
self.v[~available] = self.reset_value
|
||||
natural = available & (self.v >= self.threshold)
|
||||
spikes = natural.clone()
|
||||
injected = torch.zeros(self.n, dtype=torch.bool, device=self.device)
|
||||
for _event, targets, selected in spike_requests:
|
||||
accepted = selected & available[targets]
|
||||
chosen = targets[accepted]
|
||||
if chosen.numel():
|
||||
spikes[chosen] = True; injected[chosen] = ~natural[chosen]
|
||||
sources = torch.nonzero(spikes, as_tuple=False).flatten()
|
||||
self.spike_counts.add_(spikes.to(torch.int32))
|
||||
if sources.numel():
|
||||
self.v[sources] = self.reset_value; self.refractory[sources] = self.refractory_steps
|
||||
edge_ids = self._hybrid_edges(sources)
|
||||
if edge_ids.numel():
|
||||
self.delay_queue[(global_step + self.delay_steps) % self.queue_len].scatter_add_(0, self.indices[edge_ids].long(), self.weights[edge_ids])
|
||||
src_np = sources.cpu().numpy().astype(np.int32, copy=True)
|
||||
origin_np = np.where(injected[sources].cpu().numpy(), "external_spike_injection", "network_spike")
|
||||
self.spike_steps.append(np.full(len(src_np), global_step, dtype=np.int32))
|
||||
self.spike_nodes.append(src_np); self.spike_origins.append(origin_np)
|
||||
self.simulation_step += 1
|
||||
torch.cuda.synchronize()
|
||||
self.sequence_id += 1
|
||||
return {
|
||||
"api_version": API_VERSION, "sequence_id": self.sequence_id,
|
||||
"batch_id": batch.batch_id, "start_time_ms": batch_start,
|
||||
"end_time_ms": self.simulation_time_ms, "biological_ms": biological_ms,
|
||||
"new_spikes": sum(len(x) for x in self.spike_nodes) - start_event_count,
|
||||
"new_external_drive_events": sum(len(x) for x in self.drive_nodes) - start_drive_count,
|
||||
}
|
||||
|
||||
def _events(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
steps = np.concatenate(self.spike_steps) if self.spike_steps else np.empty(0, np.int32)
|
||||
nodes = np.concatenate(self.spike_nodes) if self.spike_nodes else np.empty(0, np.int32)
|
||||
origins = np.concatenate(self.spike_origins) if self.spike_origins else np.empty(0, object)
|
||||
return steps, nodes, origins
|
||||
|
||||
def read_spikes(self, window_ms: float | None = None) -> dict[str, Any]:
|
||||
steps, nodes, origins = self._events()
|
||||
if window_ms is not None:
|
||||
threshold = self.simulation_step - int(round(window_ms / self.dt))
|
||||
mask = steps >= threshold; steps, nodes, origins = steps[mask], nodes[mask], origins[mask]
|
||||
return {
|
||||
"simulation_time_ms": self.simulation_time_ms,
|
||||
"step": steps, "timestamp_ms": steps.astype(np.float64) * self.dt,
|
||||
"neuron_idx": nodes, "origin": origins,
|
||||
}
|
||||
|
||||
def read_population(self, population_id: str, window_ms: float) -> dict[str, Any]:
|
||||
if population_id not in self.output_populations:
|
||||
raise KeyError(population_id)
|
||||
members = self.output_populations[population_id]
|
||||
events = self.read_spikes(window_ms)
|
||||
mask = np.isin(events["neuron_idx"], members)
|
||||
nodes = events["neuron_idx"][mask]
|
||||
counts = np.zeros(len(members), dtype=np.int32)
|
||||
if len(nodes):
|
||||
lookup = np.full(self.n, -1, dtype=np.int32); lookup[members] = np.arange(len(members), dtype=np.int32)
|
||||
np.add.at(counts, lookup[nodes], 1)
|
||||
seconds = window_ms / 1000.0
|
||||
return {
|
||||
"simulation_time_ms": self.simulation_time_ms, "population_id": population_id,
|
||||
"neuron_idx": members.copy(), "spike_counts": counts,
|
||||
"firing_rate_hz": counts.astype(np.float32) / seconds,
|
||||
"active_mask": counts > 0, "aggregate_rate_hz": float(counts.sum() / (len(members) * seconds)),
|
||||
"total_spikes": int(counts.sum()), "active_neurons": int(np.count_nonzero(counts)),
|
||||
}
|
||||
|
||||
def read_output(self, output_spec: dict[str, Any], window_ms: float) -> dict[str, Any]:
|
||||
population = output_spec.get("population", "descending")
|
||||
aggregation = output_spec.get("aggregation", "per-neuron")
|
||||
base = self.read_population(population, window_ms)
|
||||
members = base["neuron_idx"]; counts = base["spike_counts"]
|
||||
if aggregation == "per-neuron":
|
||||
return base
|
||||
if aggregation not in {"per-type", "per-class"}:
|
||||
raise ValueError("aggregation must be per-neuron, per-type, or per-class")
|
||||
labels = self.neuron_type if aggregation == "per-type" else self.neuron_class
|
||||
result: dict[str, dict[str, float | int]] = {}
|
||||
for label in sorted({"<null>" if labels[i] is None else str(labels[i]) for i in members}):
|
||||
mask = np.asarray([("<null>" if labels[i] is None else str(labels[i])) == label for i in members])
|
||||
result[label] = {"neuron_count": int(mask.sum()), "spike_count": int(counts[mask].sum()), "mean_rate_hz": float(base["firing_rate_hz"][mask].mean())}
|
||||
return {"simulation_time_ms": self.simulation_time_ms, "population_id": population, "aggregation": aggregation, "features": result}
|
||||
|
||||
def fixed_temporal_bins(self, population_id: str, bin_ms: float, window_ms: float) -> dict[str, Any]:
|
||||
if bin_ms not in self.cfg["readout"]["bins_ms"]:
|
||||
raise ValueError("unsupported fixed bin")
|
||||
ev = self.read_spikes(window_ms); members = self.output_populations[population_id]
|
||||
mask = np.isin(ev["neuron_idx"], members); times = ev["timestamp_ms"][mask]
|
||||
start = self.simulation_time_ms - window_ms
|
||||
edges = np.arange(start, self.simulation_time_ms + bin_ms + 1e-9, bin_ms)
|
||||
counts, _ = np.histogram(times, bins=edges)
|
||||
return {"simulation_time_ms": self.simulation_time_ms, "population_id": population_id, "bin_ms": bin_ms, "bin_start_ms": edges[:-1], "spike_counts": counts.astype(np.int32)}
|
||||
|
||||
def get_state_summary(self) -> dict[str, Any]:
|
||||
return {
|
||||
"api_version": API_VERSION, "runtime": self.cfg["names"]["runtime"],
|
||||
"simulation_step": self.simulation_step, "simulation_time_ms": self.simulation_time_ms,
|
||||
"sequence_id": self.sequence_id, "seed": self.seed,
|
||||
"neurons": self.n, "edges": int(self.indices.numel()),
|
||||
"active_neurons_total": int(torch.count_nonzero(self.spike_counts).item()),
|
||||
"spikes_total": int(self.spike_counts.sum().item()),
|
||||
"external_drive_events_total": sum(len(x) for x in self.drive_nodes),
|
||||
}
|
||||
|
||||
def checkpoint_state(self) -> dict[str, Any]:
|
||||
return {
|
||||
"api_version": API_VERSION, "seed": self.seed, "simulation_step": self.simulation_step,
|
||||
"sequence_id": self.sequence_id, "generator_state": self.generator.get_state().cpu(),
|
||||
"v": self.v.clone(), "g": self.g.clone(), "refractory": self.refractory.clone(),
|
||||
"delay_queue": self.delay_queue.clone(), "spike_counts": self.spike_counts.clone(),
|
||||
"spike_steps": copy.deepcopy(self.spike_steps), "spike_nodes": copy.deepcopy(self.spike_nodes),
|
||||
"spike_origins": copy.deepcopy(self.spike_origins), "drive_steps": copy.deepcopy(self.drive_steps),
|
||||
"drive_nodes": copy.deepcopy(self.drive_nodes), "drive_values": copy.deepcopy(self.drive_values),
|
||||
"input_audit": copy.deepcopy(self.input_audit),
|
||||
}
|
||||
|
||||
def restore_state(self, state: dict[str, Any]) -> dict[str, Any]:
|
||||
if state.get("api_version") != API_VERSION:
|
||||
raise ValueError("checkpoint API version mismatch")
|
||||
self.seed = int(state["seed"]); self.simulation_step = int(state["simulation_step"]); self.sequence_id = int(state["sequence_id"])
|
||||
self.generator.set_state(state["generator_state"].cpu())
|
||||
for name in ("v", "g", "refractory", "delay_queue", "spike_counts"):
|
||||
setattr(self, name, state[name].clone().to(self.device))
|
||||
for name in ("spike_steps", "spike_nodes", "spike_origins", "drive_steps", "drive_nodes", "drive_values", "input_audit"):
|
||||
setattr(self, name, copy.deepcopy(state[name]))
|
||||
return self.get_state_summary()
|
||||
|
||||
def activity_packet(self, window_ms: float, input_metadata: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ev = self.read_spikes(window_ms)
|
||||
aggregates = {name: {k: v for k, v in self.read_population(name, window_ms).items() if k in ("active_neurons", "total_spikes", "aggregate_rate_hz")} for name in self.output_populations}
|
||||
return {
|
||||
"contract_version": ACTIVITY_STREAM_VERSION, "simulation_time_ms": self.simulation_time_ms,
|
||||
"sequence_id": self.sequence_id, "window_ms": window_ms,
|
||||
"spike_neuron_idx": ev["neuron_idx"].tolist(), "spike_timestamp_ms": ev["timestamp_ms"].tolist(),
|
||||
"population_aggregates": aggregates, "active_neuron_count": int(len(np.unique(ev["neuron_idx"]))),
|
||||
"input_channel_metadata": input_metadata or {}, "representation": "event-only",
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
138af9d746caacd5d65d3d5091e98754a704b768fe3aacf0123cf3c250910500 MANIFEST.json
|
||||
e0b478bc714ff90bd398fae7a726bee0e8dc9975f8d967b9fcf6d360fcb07100 VERSION.json
|
||||
d6df50e3617ec0f7893aa30b94b3dbd55359d3f1f10cf960e825e35d96cea097 __pycache__/brain_runtime.cpython-310.pyc
|
||||
c34e3584aab378b2394451c64069f8b928503c45308580ed17fb5ea1d732556b __pycache__/frozen_interface.cpython-310.pyc
|
||||
76ccac2f2ed2135dfda5d7b21307aaff5fd3f8db3dd607c36741b070db32b295 __pycache__/phase4g.cpython-310.pyc
|
||||
1c4e3e6f06708bfec30b24e92297f320b2b8b87ebbbe6da501f5f2db4f74762a brain_runtime.py
|
||||
4e4142cd5e866cfdbc9d56ffdff5eac9081a79e00c1658611f326447013b8fdb frozen_interface.py
|
||||
c82ff049336a4815690d4060f001c77266fe70d9e62ec6a940741d9fa9721e4f phase4g.py
|
||||
678798511cc14181d22a2085dedb412b44a96a966398678c12c27a8c561be042 phase4g.yaml
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Game-agnostic abstract action-logit API for Phase 4G learned adapters."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AbstractActionOutput:
|
||||
action_logits: np.ndarray
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
value = np.asarray(self.action_logits, dtype=np.float32)
|
||||
if value.shape != (4,) or not np.isfinite(value).all():
|
||||
raise ValueError("action_logits must be finite float32[4]")
|
||||
object.__setattr__(self, "action_logits", value)
|
||||
|
||||
def deterministic_argmax(self) -> int:
|
||||
return int(np.argmax(self.action_logits))
|
||||
|
||||
def sample(self, seed: int, temperature: float = 1.0, epsilon: float = 0.0) -> int:
|
||||
if temperature <= 0 or not 0 <= epsilon <= 1:
|
||||
raise ValueError("temperature must be positive and epsilon in [0,1]")
|
||||
rng = np.random.default_rng(seed)
|
||||
if rng.random() < epsilon:
|
||||
return int(rng.integers(0, 4))
|
||||
z = self.action_logits.astype(np.float64) / temperature
|
||||
z -= z.max(); p = np.exp(z); p /= p.sum()
|
||||
return int(rng.choice(4, p=p))
|
||||
|
||||
|
||||
ACTION_NAMES = ("ACTION_0", "ACTION_1", "ACTION_2", "ACTION_3")
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4G: learned interfaces around an immutable MaleCNS-LIF brain core."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse, gc, hashlib, json, math, platform, shutil, subprocess, sys, time
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import yaml
|
||||
from scipy.spatial import cKDTree
|
||||
|
||||
from brain_runtime import BrainRuntime, InputBatch, InputEvent
|
||||
from frozen_interface import AbstractActionOutput
|
||||
|
||||
|
||||
def np_json(x: Any) -> Any:
|
||||
if isinstance(x, np.generic): return x.item()
|
||||
if isinstance(x, np.ndarray): return x.tolist()
|
||||
if isinstance(x, Path): return str(x)
|
||||
raise TypeError(type(x).__name__)
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True, default=np_json)+"\n", encoding="utf-8")
|
||||
|
||||
|
||||
def sha256(path: Path, chunk: int=8<<20) -> str:
|
||||
h=hashlib.sha256()
|
||||
with path.open("rb") as f:
|
||||
while data:=f.read(chunk): h.update(data)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def verify_checksum(root: Path, control: Path) -> dict[str,Any]:
|
||||
checked=0; failures=[]
|
||||
for line in control.read_text().splitlines():
|
||||
if not line.strip(): continue
|
||||
expected,rel=line.split(None,1); path=root/rel.strip().lstrip("*"); checked+=1
|
||||
if not path.is_file() or sha256(path)!=expected: failures.append(str(path))
|
||||
return {"control":str(control),"checked":checked,"failures":failures,"passed":not failures}
|
||||
|
||||
|
||||
def frozen_audit(cfg:dict[str,Any])->tuple[list[Path],dict[str,str],dict[str,Any]]:
|
||||
p=cfg["paths"]
|
||||
controls=[Path(p["reference_root"])/"checksums.sha256",Path(p["runtime_root"])/"checksums.sha256",
|
||||
Path(p["io_root"])/"checksums.sha256",Path(p["phase4c_root"])/"manifests/checksums.sha256",
|
||||
Path(p["phase4d_root"])/"manifests/checksums.sha256",Path(p["phase4e_root"])/"manifests/checksums.sha256",
|
||||
Path(p["phase4f_root"])/"manifests/checksums.sha256",Path(p["phase4f_model_root"])/"checksums.sha256"]
|
||||
if missing:=[str(x) for x in controls if not x.is_file()]: raise FileNotFoundError(missing)
|
||||
checks=[]
|
||||
for x in controls:
|
||||
root=x.parent.parent if x.parent.name=="manifests" else x.parent; checks.append(verify_checksum(root,x))
|
||||
if not all(x["passed"] for x in checks): raise RuntimeError(checks)
|
||||
before={str(x):sha256(x) for x in controls}
|
||||
return controls,before,{"all_passed":True,"controls":checks}
|
||||
|
||||
|
||||
def stable_seed(base:int,text:str)->int:
|
||||
return base+int.from_bytes(hashlib.sha256(text.encode()).digest()[:4],"little")%1_000_000
|
||||
|
||||
|
||||
def make_samples(cfg:dict[str,Any])->pd.DataFrame:
|
||||
rows=[]
|
||||
for split,key in (("train","train_stimulus_seeds"),("held-out","evaluation_stimulus_seeds")):
|
||||
for seed in cfg["dataset"][key]:
|
||||
rng=np.random.default_rng(int(seed))
|
||||
if split=="train":
|
||||
offset=rng.uniform(-.08,.08); speed=rng.uniform(.85,1.15); size=rng.uniform(.9,1.1); curve=rng.uniform(-.08,.08); phase=rng.uniform(-.05,.05)
|
||||
else:
|
||||
sign=-1 if int(seed)%2 else 1
|
||||
offset=sign*rng.uniform(.16,.26); speed=rng.choice([rng.uniform(.62,.78),rng.uniform(1.25,1.42)])
|
||||
size=rng.choice([rng.uniform(.68,.82),rng.uniform(1.22,1.40)]); curve=sign*rng.uniform(.16,.26); phase=sign*rng.uniform(.10,.20)
|
||||
for pattern in cfg["dataset"]["patterns"]:
|
||||
rows.append({"sample_id":len(rows),"split":split,"stimulus_seed":int(seed),"pattern":pattern,
|
||||
"position_offset":offset,"speed_scale":speed,"size_scale":size,"trajectory_curve":curve,"temporal_phase":phase})
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def frames_variant(pattern:str,x:np.ndarray,y:np.ndarray,segments:int,v:dict[str,float])->np.ndarray:
|
||||
out=[]; off=v["position_offset"]
|
||||
for i in range(segments):
|
||||
z=np.clip((i/max(segments-1,1)+v["temporal_phase"])*v["speed_scale"],0,1)
|
||||
s0=.22*v["size_scale"]
|
||||
if pattern=="full-field": f=np.ones_like(x)
|
||||
elif pattern in {"left-spot","right-spot","upper-spot","lower-spot"}:
|
||||
loc={"left-spot":(-.65+off,0),"right-spot":(.65+off,0),"upper-spot":(off,.65),"lower-spot":(off,-.65)}[pattern]
|
||||
f=np.exp(-((x-loc[0])**2+(y-loc[1])**2)/(2*s0*s0))
|
||||
elif pattern in {"left-to-right","right-to-left","up-to-down","down-to-up"}:
|
||||
p=-.8+1.6*z; p=-p if pattern in {"right-to-left","down-to-up"} else p; bend=v["trajectory_curve"]*math.sin(math.pi*z)
|
||||
cx,cy=((p,bend+off) if pattern in {"left-to-right","right-to-left"} else (bend+off,-p))
|
||||
f=np.exp(-((x-cx)**2+(y-cy)**2)/(2*(.18*v["size_scale"])**2))
|
||||
elif pattern=="looming":
|
||||
s=(.08+.48*z)*v["size_scale"]; f=np.exp(-((x-off)**2+y*y)/(2*s*s))
|
||||
elif pattern=="sparse-moving-object":
|
||||
cx=-.75+1.5*z; cy=.55-1.1*z+v["trajectory_curve"]*math.sin(math.pi*z)+off
|
||||
f=np.exp(-((x-cx)**2+(y-cy)**2)/(2*(.10*v["size_scale"])**2))
|
||||
elif pattern=="static-object": f=np.exp(-((x+.25-off)**2+(y-.20)**2)/(2*(.10*v["size_scale"])**2))
|
||||
else: raise KeyError(pattern)
|
||||
out.append(f.astype(np.float32))
|
||||
return np.asarray(out)
|
||||
|
||||
|
||||
def channels(frames:np.ndarray,coords:np.ndarray)->dict[str,np.ndarray]:
|
||||
prev=np.vstack([np.zeros((1,frames.shape[1]),np.float32),frames[:-1]]); delta=frames-prev
|
||||
nbr=cKDTree(coords).query(coords,k=min(7,len(coords)))[1][:,1:]
|
||||
dx=np.abs(coords[:,None,0]-coords[nbr,0]); dy=np.abs(coords[:,None,1]-coords[nbr,1]); hs=[];vs=[]
|
||||
for f in frames:
|
||||
d=np.abs(f[:,None]-f[nbr]); hs.append((d*dx).sum(1)/np.maximum(dx.sum(1),1e-8)); vs.append((d*dy).sum(1)/np.maximum(dy.sum(1),1e-8))
|
||||
return {"luminance":frames,"on":np.maximum(delta,0),"off":np.maximum(-delta,0),"temporal":np.abs(delta),"horizontal":np.asarray(hs),"vertical":np.asarray(vs)}
|
||||
|
||||
|
||||
def hex_grid(count:int)->tuple[np.ndarray,np.ndarray]:
|
||||
e=0
|
||||
while 1+3*e*(e+1)<count:e+=1
|
||||
u=[];v=[]
|
||||
for q in range(-e,e+1):
|
||||
for r in range(max(-e,-e-q),min(e,e-q)+1):u.append(q);v.append(r)
|
||||
u=np.asarray(u);v=np.asarray(v);x=1.5*v;y=-np.sqrt(3)*(u+v/2);x/=max(abs(x).max(),1);y/=max(abs(y).max(),1)
|
||||
return x,y
|
||||
|
||||
|
||||
def encode(cfg:dict[str,Any],route:str,sample:pd.Series,pool:pd.DataFrame,fly_xy:tuple[np.ndarray,np.ndarray])->tuple[InputBatch,np.ndarray,float,dict[str,float]]:
|
||||
t0=time.perf_counter(); seg=int(cfg["dataset"]["temporal_segments"]);k=int(cfg["dataset"]["targets_per_segment"])
|
||||
xy=pool[["retinotopic_x","retinotopic_y"]].to_numpy(float); var={x:float(sample[x]) for x in ("position_offset","speed_scale","size_scale","trajectory_curve","temporal_phase")}
|
||||
if route=="route-a": c=channels(frames_variant(sample.pattern,xy[:,0],xy[:,1],seg,var),xy)
|
||||
else:
|
||||
fx,fy=fly_xy; fxy=np.c_[fx,fy]; fc=channels(frames_variant(sample.pattern,fx,fy,seg,var),fxy); near=cKDTree(fxy).query(xy,k=1)[1]
|
||||
c={name:value[:,near] for name,value in fc.items()}
|
||||
score=c["luminance"]+c["on"]+c["off"]+.5*c["temporal"]+c["horizontal"]+c["vertical"]
|
||||
dt=100/seg;events=[];union=[]
|
||||
for i in range(seg):
|
||||
order=np.lexsort((pool.neuron_idx.to_numpy(),-score[i])); target=np.sort(pool.neuron_idx.to_numpy(np.int64)[order[:k]]);union.append(target)
|
||||
events.append(InputEvent(target,"external_synaptic_drive","poisson_rate_hz",float(cfg["dataset"]["poisson_rate_hz"]),i*dt,dt,
|
||||
"phase4g-learnable-encoder",f"{route}:{sample.pattern}:{i}",float(cfg["dataset"]["external_drive_mv"]),
|
||||
{"route":route,"pattern":sample.pattern,"adapter_only":True,"brain_state_direct_write":False}))
|
||||
descriptor={"mean_luminance":float(c["luminance"].mean()),"on":float(c["on"].mean()),"off":float(c["off"].mean()),
|
||||
"temporal":float(c["temporal"].mean()),"horizontal":float(c["horizontal"].mean()),"vertical":float(c["vertical"].mean()),**var}
|
||||
return InputBatch(tuple(events),f"phase4g:{route}:{sample.sample_id}"),np.unique(np.concatenate(union)),time.perf_counter()-t0,descriptor
|
||||
|
||||
|
||||
def groups_and_maps(cfg:dict[str,Any],rt:BrainRuntime,bins:int=128)->tuple[dict[str,np.ndarray],dict[str,np.ndarray]]:
|
||||
root=Path(cfg["paths"]["phase4c_root"])/"visual-audit";s=np.asarray(rt.superclass,object)
|
||||
groups={"visual-projection":np.flatnonzero(np.isin(s,["visual_projection","visual_projection_tbc"])).astype(np.int32),
|
||||
"central-downstream":pq.read_table(root/"hierarchy-central_brain.parquet",columns=["neuron_idx"])["neuron_idx"].to_numpy().astype(np.int32),
|
||||
"descending":rt.output_populations["descending"],"motor-related":rt.output_populations["motor_related"]}
|
||||
maps={}
|
||||
for name,members in groups.items():
|
||||
a=np.full(rt.n,-1,np.int16);a[members]=((members.astype(np.int64)*2654435761)%bins).astype(np.int16);maps[name]=a
|
||||
return groups,maps
|
||||
|
||||
|
||||
def graph_controls(cfg:dict[str,Any],rt:BrainRuntime,exp:Path)->dict[str,np.ndarray]:
|
||||
out=exp/"random-connectome";out.mkdir(parents=True,exist_ok=True);seed=int(cfg["controls"]["graph_seed"]);rng=np.random.default_rng(seed)
|
||||
real=np.asarray(rt.indices_np,dtype=np.int32); shuffled_path=out/"shuffled-indices.int32.npy"; degree_path=out/"degree-preserving-indices.int32.npy"
|
||||
if not shuffled_path.is_file(): np.save(shuffled_path,rng.integers(0,rt.n,size=len(real),dtype=np.int32))
|
||||
if not degree_path.is_file():
|
||||
degree=real.copy();rng.shuffle(degree);np.save(degree_path,degree)
|
||||
shuffled=np.load(shuffled_path,mmap_mode="r");degree=np.load(degree_path,mmap_mode="r")
|
||||
real_in=np.bincount(real,minlength=rt.n);degree_in=np.bincount(degree,minlength=rt.n)
|
||||
audit={"edge_count":len(real),"neurons":rt.n,"seed":seed,"real_indices_sha256":sha256(Path(cfg["paths"]["connectome_root"])/"gpu/csr-indices.int32.npy"),
|
||||
"shuffled":{"sha256":sha256(shuffled_path),"edge_count_equal":len(shuffled)==len(real),"source_out_degree_exact":True,"target_in_degree_preserved":False},
|
||||
"degree_preserving":{"sha256":sha256(degree_path),"edge_count_equal":len(degree)==len(real),"source_out_degree_exact":True,
|
||||
"target_in_degree_exact":bool(np.array_equal(real_in,degree_in))},"weights_changed":False,"brain_artifacts_written":False}
|
||||
write_json(out/"audit.json",audit);return {"real-malecns":real,"shuffled-edges":shuffled,"degree-preserving":degree}
|
||||
|
||||
|
||||
def hash_counts(nodes:np.ndarray,map_:np.ndarray,bins:int=128)->np.ndarray:
|
||||
x=map_[nodes];x=x[x>=0];return np.bincount(x,minlength=bins).astype(np.float32)
|
||||
|
||||
|
||||
def bypass_features(descriptors:list[dict[str,float]],route:str)->np.ndarray:
|
||||
names=sorted(descriptors[0]);x=np.asarray([[d[n] for n in names] for d in descriptors],np.float32)
|
||||
rng=np.random.default_rng(47901 if route=="route-a" else 47902);w=rng.normal(size=(x.shape[1],128)).astype(np.float32);b=rng.uniform(0,2*np.pi,128).astype(np.float32)
|
||||
return np.sqrt(2/128)*np.cos(x@w+b)
|
||||
|
||||
|
||||
def run_dataset(cfg:dict[str,Any],rt:BrainRuntime,samples:pd.DataFrame,pool:pd.DataFrame,exp:Path)->tuple[dict[str,Any],list[dict[str,Any]]]:
|
||||
controls=graph_controls(cfg,rt,exp);groups,maps=groups_and_maps(cfg,rt)
|
||||
official_coords=pq.read_table(Path(cfg["paths"]["phase4f_root"])/"retinotopy/visual-retinotopy.parquet",columns=["official_hex1","official_hex2","coordinate_reliability"]).to_pandas()
|
||||
unique_cols=int(official_coords.loc[official_coords.coordinate_reliability=="official-direct",["official_hex1","official_hex2"]].drop_duplicates().shape[0]);fly=hex_grid(unique_cols)
|
||||
windows=list(map(int,cfg["dataset"]["biological_windows_ms"]));routes=("route-a","route-b");features={};records=[];descriptors={r:[] for r in routes}
|
||||
cached=[exp/"datasets"/f"{graph}-{route}.npz" for graph in controls for route in routes]
|
||||
cached += [exp/"bypass"/f"brain-bypass-{route}.npz" for route in routes]
|
||||
record_path=exp/"datasets/generation-records.json"
|
||||
if record_path.is_file() and all(path.is_file() for path in cached):
|
||||
for graph in controls:
|
||||
features[graph]={}
|
||||
for route in routes:
|
||||
z=np.load(exp/"datasets"/f"{graph}-{route}.npz");features[graph][route]={k:z[k] for k in z.files if k.startswith("w")}
|
||||
features["brain-bypass"]={}
|
||||
for route in routes:
|
||||
z=np.load(exp/"bypass"/f"brain-bypass-{route}.npz");features["brain-bypass"][route]={k:z[k] for k in z.files}
|
||||
return features,json.loads(record_path.read_text())
|
||||
for graph,indices in controls.items():
|
||||
rt.indices_np=np.asarray(indices);rt.indices=torch.as_tensor(np.asarray(indices),dtype=torch.int32,device=rt.device);features[graph]={}
|
||||
for route in routes:
|
||||
store={f"w{w}_{g}":np.zeros((len(samples),128),np.float32) for w in windows for g in groups};enc_times=np.zeros(len(samples));brain_times=np.zeros((len(samples),len(windows)));drive=np.zeros((len(samples),len(windows)),np.int32);amps=np.zeros((len(samples),len(windows)),np.float64)
|
||||
local_desc=[]
|
||||
for row in samples.itertuples(index=False):
|
||||
batch,targets,enc_time,desc=encode(cfg,route,pd.Series(row._asdict()),pool,fly);enc_times[row.sample_id]=enc_time
|
||||
if graph=="real-malecns": descriptors[route].append(desc)
|
||||
rt.reset(int(row.stimulus_seed));last=0
|
||||
for wi,w in enumerate(windows):
|
||||
t0=time.perf_counter();rt.step(batch,float(w-last));brain_times[row.sample_id,wi]=time.perf_counter()-t0;last=w
|
||||
ev=rt.read_spikes();mask=ev["timestamp_ms"]<w+1e-9;nodes=ev["neuron_idx"][mask]
|
||||
for g in groups:store[f"w{w}_{g}"][row.sample_id]=hash_counts(nodes,maps[g])
|
||||
drive_mask=np.concatenate(rt.drive_steps)<int(round(w/rt.dt)) if rt.drive_steps else np.empty(0,bool)
|
||||
drive[row.sample_id,wi]=int(np.count_nonzero(drive_mask))
|
||||
vals=np.concatenate(rt.drive_values) if rt.drive_values else np.empty(0,np.float32);amps[row.sample_id,wi]=float(vals[drive_mask].sum()) if len(vals) else 0
|
||||
ev=rt.read_spikes();active=len(np.unique(ev["neuron_idx"]));rate=len(ev["neuron_idx"])/(rt.n*.1);finite=bool(torch.isfinite(rt.v).all() and torch.isfinite(rt.g).all())
|
||||
records.append({"graph":graph,"route":route,"sample_id":row.sample_id,"pattern":row.pattern,"split":row.split,"stimulus_seed":row.stimulus_seed,
|
||||
"encoder_wall_seconds":enc_time,"brain_wall_seconds_100ms":float(brain_times[row.sample_id].sum()),"active_neurons":active,"mean_rate_hz":rate,
|
||||
"runaway":active/rt.n>=.8 or rate>=100,"finite":finite})
|
||||
store["encoder_wall_seconds"]=enc_times;store["brain_increment_wall_seconds"]=brain_times;store["external_event_counts"]=drive;store["external_amplitudes_mv"]=amps
|
||||
path=exp/"datasets"/f"{graph}-{route}.npz";np.savez_compressed(path,**store);features[graph][route]={k:v for k,v in store.items() if k.startswith("w")}
|
||||
# Bypass uses identical external visual descriptors but no MaleCNS execution.
|
||||
features["brain-bypass"]={}
|
||||
for route in routes:
|
||||
base=bypass_features(descriptors[route],route);features["brain-bypass"][route]={f"w{w}_{g}":base.copy() for w in windows for g in groups}
|
||||
np.savez_compressed(exp/"bypass"/f"brain-bypass-{route}.npz",**features["brain-bypass"][route])
|
||||
write_json(exp/"datasets/generation-records.json",records)
|
||||
# Exact matched-energy audit by graph, route, split seed and window.
|
||||
matched={}
|
||||
for graph in controls:
|
||||
matched[graph]={}
|
||||
for route in routes:
|
||||
z=np.load(exp/"datasets"/f"{graph}-{route}.npz");entry={}
|
||||
for split,seed in samples[["split","stimulus_seed"]].drop_duplicates().itertuples(index=False):
|
||||
ids=samples.index[(samples.split==split)&(samples.stimulus_seed==seed)].to_numpy();entry[f"{split}-{seed}"]={}
|
||||
for wi,w in enumerate(windows):
|
||||
counts=z["external_event_counts"][ids,wi];amps=z["external_amplitudes_mv"][ids,wi]
|
||||
entry[f"{split}-{seed}"][str(w)]={"event_counts":sorted(set(map(int,counts))),"amplitude_mv":sorted(set(map(float,amps))),"matched":len(set(counts))==1 and len(set(amps))==1}
|
||||
matched[graph][route]=entry
|
||||
write_json(exp/"audit/matched-input.json",matched)
|
||||
return features,records
|
||||
|
||||
|
||||
def reduce_bins(x:np.ndarray,bins:int)->np.ndarray:
|
||||
if bins==128:return x.astype(np.float32)
|
||||
out=np.zeros((len(x),bins),np.float32)
|
||||
for i in range(128):out[:,i%bins]+=x[:,i]
|
||||
return out
|
||||
|
||||
|
||||
class Decoder(nn.Module):
|
||||
def __init__(self,d:int,capacity:str,hidden:int=16):
|
||||
super().__init__();self.net=nn.Linear(d,4) if capacity=="Tiny" else nn.Sequential(nn.Linear(d,hidden),nn.ReLU(),nn.Linear(hidden,4))
|
||||
def forward(self,x):return self.net(x)
|
||||
|
||||
|
||||
def fit_model(x:np.ndarray,y:np.ndarray,test_x:np.ndarray,test_y:np.ndarray,capacity:str,classes:int,seed:int,cfg:dict[str,Any])->tuple[dict[str,Any],dict[str,Any]]:
|
||||
torch.manual_seed(seed);mean=x.mean(0);std=x.std(0);std[std<1e-6]=1
|
||||
xt=torch.tensor((x-mean)/std);yt=torch.tensor(y,dtype=torch.long);xx=torch.tensor((test_x-mean)/std)
|
||||
hidden=int(cfg["training"]["capacities"][capacity].get("hidden",16));m=Decoder(x.shape[1],capacity,hidden);opt=torch.optim.Adam(m.parameters(),lr=float(cfg["training"]["learning_rate"]));t0=time.perf_counter()
|
||||
for _ in range(int(cfg["training"]["epochs"])):
|
||||
opt.zero_grad();loss=nn.functional.cross_entropy(m(xt)[:,:classes],yt);loss.backward();opt.step()
|
||||
train_wall=time.perf_counter()-t0
|
||||
with torch.no_grad():
|
||||
tr=m(xt)[:,:classes].numpy();te=m(xx)[:,:classes].numpy();pred=te.argmax(1);acc=float(np.mean(pred==test_y));train_acc=float(np.mean(tr.argmax(1)==y))
|
||||
params=sum(p.numel() for p in m.parameters());flops=(2*x.shape[1]*4 if capacity=="Tiny" else 2*x.shape[1]*hidden+2*hidden*4)
|
||||
state={k:v.detach().cpu() for k,v in m.state_dict().items()}
|
||||
bundle={"state_dict":state,"mean":mean,"std":std,"capacity":capacity,"input_dim":x.shape[1],"hidden":hidden,"classes":classes,"optimizer_state":opt.state_dict()}
|
||||
result={"accuracy":acc,"train_accuracy":train_acc,"predictions":pred.tolist(),"truth":test_y.tolist(),"confusion":np.bincount(test_y*classes+pred,minlength=classes*classes).reshape(classes,classes).tolist(),
|
||||
"train_wall_seconds":train_wall,"trainable_parameters":params,"approx_flops_per_inference":flops}
|
||||
return bundle,result
|
||||
|
||||
|
||||
def predict(bundle:dict[str,Any],x:np.ndarray)->np.ndarray:
|
||||
m=Decoder(bundle["input_dim"],bundle["capacity"],bundle["hidden"]);m.load_state_dict(bundle["state_dict"]);m.eval()
|
||||
with torch.no_grad():return m(torch.tensor((x-bundle["mean"])/bundle["std"]))[:,:bundle["classes"]].numpy()
|
||||
|
||||
|
||||
def task_data(samples:pd.DataFrame,spec:dict[str,Any])->tuple[np.ndarray,np.ndarray,np.ndarray,np.ndarray]:
|
||||
label=dict(zip(spec["patterns"],spec["labels"]));sel=samples.pattern.isin(spec["patterns"]);ids=samples.index[sel].to_numpy();y=np.asarray([label[x] for x in samples.loc[sel,"pattern"]],np.int64)
|
||||
train=samples.loc[sel,"split"].to_numpy()=="train";return ids[train],y[train],ids[~train],y[~train]
|
||||
|
||||
|
||||
def selected_matrix(a:np.ndarray,b:np.ndarray,samples:pd.DataFrame,choice:dict[str,str])->np.ndarray:
|
||||
return np.asarray([a[i] if choice.get(samples.iloc[i].pattern,"route-a")=="route-a" else b[i] for i in range(len(samples))],np.float32)
|
||||
|
||||
|
||||
def train_experiments(cfg:dict[str,Any],features:dict[str,Any],samples:pd.DataFrame,exp:Path)->tuple[dict[str,Any],dict[str,Any]]:
|
||||
capacities=cfg["training"]["capacities"];seeds=list(map(int,cfg["training"]["model_seeds"]));windows=list(map(int,cfg["dataset"]["biological_windows_ms"]));readouts=["visual-projection","central-downstream","descending","motor-related"]
|
||||
A={};selected_configs={}
|
||||
for graph in cfg["controls"]["graphs"]:
|
||||
A[graph]={}
|
||||
for route in ("route-a","route-b"):
|
||||
A[graph][route]={}
|
||||
for cap,cspec in capacities.items():
|
||||
A[graph][route][cap]={}
|
||||
for task,spec in cfg["dataset"]["tasks"].items():
|
||||
tr,ytr,te,yte=task_data(samples,spec);pilot_train=tr[samples.iloc[tr].stimulus_seed.to_numpy()!=cfg["dataset"]["train_stimulus_seeds"][-1]];pilot_y=ytr[samples.iloc[tr].stimulus_seed.to_numpy()!=cfg["dataset"]["train_stimulus_seeds"][-1]]
|
||||
val=tr[samples.iloc[tr].stimulus_seed.to_numpy()==cfg["dataset"]["train_stimulus_seeds"][-1]];val_y=ytr[samples.iloc[tr].stimulus_seed.to_numpy()==cfg["dataset"]["train_stimulus_seeds"][-1]]
|
||||
pilot=[]
|
||||
for w in windows:
|
||||
for read in readouts:
|
||||
x=reduce_bins(features[graph][route][f"w{w}_{read}"],int(cspec["feature_bins"]));_,r=fit_model(x[pilot_train],pilot_y,x[val],val_y,cap,int(spec["classes"]),seeds[0],cfg);pilot.append((r["accuracy"],w,read))
|
||||
readout_priority={"descending":3,"motor-related":2,"central-downstream":1,"visual-projection":0}
|
||||
_,w,read=max(pilot,key=lambda z:(z[0],readout_priority[z[2]],-z[1]));x=reduce_bins(features[graph][route][f"w{w}_{read}"],int(cspec["feature_bins"]));runs=[]
|
||||
for seed in seeds:
|
||||
_,r=fit_model(x[tr],ytr,x[te],yte,cap,int(spec["classes"]),seed,cfg);r["seed"]=seed;runs.append(r)
|
||||
result={"selected_window_ms":w,"selected_readout":read,"held_out_accuracy_mean":float(np.mean([z["accuracy"] for z in runs])),"held_out_accuracy_std":float(np.std([z["accuracy"] for z in runs])),"runs":runs,"chance":1/int(spec["classes"])}
|
||||
A[graph][route][cap][task]=result
|
||||
if graph=="real-malecns":selected_configs[(cap,task,route)]=(w,read)
|
||||
write_json(exp/"training/experiment-a-decoder-only.json",A)
|
||||
B={};C={};best_bundles={}
|
||||
for graph in cfg["controls"]["graphs"]:
|
||||
B[graph]={};C[graph]={}
|
||||
for cap,cspec in capacities.items():
|
||||
B[graph][cap]={};C[graph][cap]={}
|
||||
for task,spec in cfg["dataset"]["tasks"].items():
|
||||
# A common anatomy/readout configuration selected on real Route A training-validation only.
|
||||
w,read=selected_configs[(cap,task,"route-a")];a=reduce_bins(features[graph]["route-a"][f"w{w}_{read}"],int(cspec["feature_bins"]));b=reduce_bins(features[graph]["route-b"][f"w{w}_{read}"],int(cspec["feature_bins"]));tr,ytr,te,yte=task_data(samples,spec)
|
||||
bruns=[];cruns=[]
|
||||
for seed in seeds:
|
||||
fixed,base=fit_model(a[tr],ytr,a[te],yte,cap,int(spec["classes"]),seed,cfg)
|
||||
if cap=="Tiny":
|
||||
la=nn.functional.cross_entropy(torch.tensor(predict(fixed,a[tr])),torch.tensor(ytr)).item();lb=nn.functional.cross_entropy(torch.tensor(predict(fixed,b[tr])),torch.tensor(ytr)).item();choice={p:("route-a" if la<=lb else "route-b") for p in spec["patterns"]}
|
||||
else:
|
||||
choice={}
|
||||
for p in spec["patterns"]:
|
||||
ids=tr[samples.iloc[tr].pattern.to_numpy()==p];yy=ytr[samples.iloc[tr].pattern.to_numpy()==p]
|
||||
la=nn.functional.cross_entropy(torch.tensor(predict(fixed,a[ids])),torch.tensor(yy)).item();lb=nn.functional.cross_entropy(torch.tensor(predict(fixed,b[ids])),torch.tensor(yy)).item();choice[p]="route-a" if la<=lb else "route-b"
|
||||
xm=selected_matrix(a,b,samples,choice);logits=predict(fixed,xm[te]);pred=logits.argmax(1);bruns.append({"seed":seed,"accuracy":float(np.mean(pred==yte)),"route_choice":choice,"decoder_parameters":base["trainable_parameters"],"encoder_parameters":int(cspec["encoder_parameters"])})
|
||||
# Coordinate descent: update only route-selection adapter and decoder; brain feature bank is immutable.
|
||||
cchoice={p:"route-a" for p in spec["patterns"]};joint=None
|
||||
for _ in range(3):
|
||||
xm=selected_matrix(a,b,samples,cchoice);joint,_=fit_model(xm[tr],ytr,xm[te],yte,cap,int(spec["classes"]),seed,cfg)
|
||||
if cap=="Tiny":
|
||||
choices=[]
|
||||
for route,xr in (("route-a",a),("route-b",b)):
|
||||
loss=nn.functional.cross_entropy(torch.tensor(predict(joint,xr[tr])),torch.tensor(ytr)).item();choices.append((loss,route))
|
||||
cchoice={p:min(choices)[1] for p in spec["patterns"]}
|
||||
else:
|
||||
for p in spec["patterns"]:
|
||||
ids=tr[samples.iloc[tr].pattern.to_numpy()==p];yy=ytr[samples.iloc[tr].pattern.to_numpy()==p]
|
||||
la=nn.functional.cross_entropy(torch.tensor(predict(joint,a[ids])),torch.tensor(yy)).item();lb=nn.functional.cross_entropy(torch.tensor(predict(joint,b[ids])),torch.tensor(yy)).item();cchoice[p]="route-a" if la<=lb else "route-b"
|
||||
xm=selected_matrix(a,b,samples,cchoice);joint,jr=fit_model(xm[tr],ytr,xm[te],yte,cap,int(spec["classes"]),seed,cfg);jr.update({"seed":seed,"route_choice":cchoice,"encoder_parameters":int(cspec["encoder_parameters"])});cruns.append(jr)
|
||||
best_bundles[(graph,cap,task,seed)]={"bundle":joint,"choice":cchoice,"window":w,"readout":read,"accuracy":jr["accuracy"]}
|
||||
B[graph][cap][task]={"window_ms":w,"readout":read,"held_out_accuracy_mean":float(np.mean([z["accuracy"] for z in bruns])),"held_out_accuracy_std":float(np.std([z["accuracy"] for z in bruns])),"runs":bruns,"chance":1/int(spec["classes"])}
|
||||
C[graph][cap][task]={"window_ms":w,"readout":read,"held_out_accuracy_mean":float(np.mean([z["accuracy"] for z in cruns])),"held_out_accuracy_std":float(np.std([z["accuracy"] for z in cruns])),"runs":cruns,"chance":1/int(spec["classes"])}
|
||||
write_json(exp/"training/experiment-b-encoder-only.json",B);write_json(exp/"training/experiment-c-joint.json",C)
|
||||
return {"experiment_a":A,"experiment_b":B,"experiment_c":C},best_bundles
|
||||
|
||||
|
||||
def pair_metrics(a:np.ndarray,b:np.ndarray)->dict[str,float]:
|
||||
an=np.linalg.norm(a);bn=np.linalg.norm(b);cos=1-(float(a@b/(an*bn)) if an and bn else (1 if an==bn else 0));aa=a>0;bb=b>0;u=np.count_nonzero(aa|bb)
|
||||
corr=0 if np.array_equal(a,b) else (1-float(np.corrcoef(a,b)[0,1]) if np.std(a) and np.std(b) else 1)
|
||||
return {"cosine_distance":cos,"euclidean_distance":float(np.linalg.norm(a-b)),"correlation_distance":corr,"jaccard_distance":1-float(np.count_nonzero(aa&bb)/u) if u else 0}
|
||||
|
||||
|
||||
def separability_audit(cfg:dict[str,Any],features:dict[str,Any],samples:pd.DataFrame,exp:Path)->dict[str,Any]:
|
||||
out={};windows=list(map(int,cfg["dataset"]["biological_windows_ms"]));pairs=[("left-spot","right-spot"),("upper-spot","lower-spot"),("left-to-right","right-to-left"),("looming","left-to-right"),("sparse-moving-object","static-object")]
|
||||
for read in ("visual-projection","central-downstream","descending","motor-related"):
|
||||
out[read]={}
|
||||
for w in windows:
|
||||
x=features["real-malecns"]["route-a"][f"w{w}_{read}"];entry={}
|
||||
for a,b in pairs:
|
||||
xa=x[(samples.pattern==a)&(samples.split=="held-out")];xb=x[(samples.pattern==b)&(samples.split=="held-out")]
|
||||
between=[pair_metrics(i,j) for i in xa for j in xb];within=[pair_metrics(z[i],z[j]) for z in (xa,xb) for i in range(len(z)) for j in range(i+1,len(z))]
|
||||
entry[f"{a}__{b}"]={"between_mean":{k:float(np.mean([q[k] for q in between])) for k in between[0]},"within_mean":{k:float(np.mean([q[k] for q in within])) for k in within[0]}}
|
||||
out[read][str(w)]=entry
|
||||
write_json(exp/"separability/summary.json",out);return out
|
||||
|
||||
|
||||
def robustness(cfg:dict[str,Any],features:dict[str,Any],samples:pd.DataFrame,bundles:dict[Any,Any],training:dict[str,Any],exp:Path)->dict[str,Any]:
|
||||
task=cfg["robustness"]["task"];spec=cfg["dataset"]["tasks"][task];tr,ytr,te,yte=task_data(samples,spec);fraction=float(cfg["robustness"]["perturbation_fraction"]);out={}
|
||||
for graph in cfg["controls"]["graphs"]:
|
||||
cap="Small";seed=int(cfg["training"]["model_seeds"][0]);item=bundles[(graph,cap,task,seed)];w=item["window"];read=item["readout"]
|
||||
a=reduce_bins(features[graph]["route-a"][f"w{w}_{read}"],128);b=reduce_bins(features[graph]["route-b"][f"w{w}_{read}"],128);x=selected_matrix(a,b,samples,item["choice"]);base=x[te];rng=np.random.default_rng(47400);conditions={}
|
||||
for condition in cfg["robustness"]["conditions"]:
|
||||
z=base.copy()
|
||||
if condition=="input-noise":z+=rng.normal(0,fraction*max(float(z.std()),1e-3),z.shape)
|
||||
elif condition=="event-dropout":z=rng.binomial(np.maximum(np.rint(z),0).astype(np.int32),1-fraction).astype(np.float32)
|
||||
elif condition=="neuron-dropout":z[:,rng.choice(z.shape[1],int(z.shape[1]*fraction),replace=False)]=0
|
||||
elif condition=="adapter-noise":z*=rng.normal(1,fraction,z.shape)
|
||||
elif condition=="partial-occlusion":z[:,:int(z.shape[1]*fraction)]=0
|
||||
elif condition=="temporal-jitter":
|
||||
available=list(map(int,cfg["dataset"]["biological_windows_ms"]));alt=max([q for q in available if q<w],default=w)
|
||||
aa=reduce_bins(features[graph]["route-a"][f"w{alt}_{read}"],128);bb=reduce_bins(features[graph]["route-b"][f"w{alt}_{read}"],128);z=selected_matrix(aa,bb,samples,item["choice"])[te]
|
||||
pred=predict(item["bundle"],z).argmax(1);conditions[condition]=float(np.mean(pred==yte))
|
||||
conditions["clean"]=float(np.mean(predict(item["bundle"],base).argmax(1)==yte));out[graph]={"window_ms":w,"readout":read,"accuracy":conditions,
|
||||
"method_note":"feature-domain stress of frozen neural readout; temporal jitter uses adjacent earlier biological window; no brain retraining"}
|
||||
write_json(exp/"robustness/summary.json",out);return out
|
||||
|
||||
|
||||
def regression(cfg:dict[str,Any],rt:BrainRuntime,exp:Path)->dict[str,Any]:
|
||||
sys.path.insert(0,cfg["paths"]["io_root"]);import phase4d as p4d
|
||||
trials=p4d.regression(exp,cfg,rt);api=p4d.api_tests(exp,rt);stream=p4d.stream_contract(exp,rt)
|
||||
result={"phase4c_trials":all(x["passed"] for x in trials.values()),"phase4d_brainio":api["all_passed"],"activity_stream":stream["replay_exact"],
|
||||
"phase4e_checksum":True,"phase4f_checksum":True,"all_passed":all(x["passed"] for x in trials.values()) and api["all_passed"] and stream["replay_exact"]}
|
||||
write_json(exp/"regression/summary.json",result);return result
|
||||
|
||||
|
||||
def tree_manifest(root:Path,output:Path,excludes:set[Path])->dict[str,Any]:
|
||||
files=[]
|
||||
for p in sorted(root.rglob("*")):
|
||||
if p.is_file() and p not in excludes:files.append({"path":str(p.relative_to(root)),"bytes":p.stat().st_size,"sha256":sha256(p)})
|
||||
value={"root":str(root),"file_count":len(files),"total_bytes":sum(x["bytes"] for x in files),"files":files};write_json(output,value);return value
|
||||
|
||||
|
||||
def main()->None:
|
||||
ap=argparse.ArgumentParser();ap.add_argument("--config",type=Path,required=True);args=ap.parse_args();cfg=yaml.safe_load(args.config.read_text());exp=Path(cfg["paths"]["experiment_root"]);learned=Path(cfg["paths"]["learned_root"]);interface=Path(cfg["paths"]["interface_root"])
|
||||
for sub in ("datasets","encoder","decoder","training","controls","random-connectome","bypass","separability","robustness","generalization","latency","regression","audit","manifests","compiler-reports/extension-build"):(exp/sub).mkdir(parents=True,exist_ok=True)
|
||||
for sub in ("encoder","decoder","checkpoints","optimizer-state","training-metadata"):(learned/sub).mkdir(parents=True,exist_ok=True)
|
||||
torch.set_num_threads(1);torch.set_num_interop_threads(1)
|
||||
controls,before,frozen=frozen_audit(cfg);write_json(exp/"audit/frozen-before.json",frozen);shutil.copy2(args.config,exp/"training/phase4g.yaml")
|
||||
samples=make_samples(cfg);pq.write_table(pa.Table.from_pandas(samples,preserve_index=False),exp/"datasets/sample-index.parquet",compression="zstd");write_json(exp/"generalization/held-out-policy.json",{"train_ranges":"small position/speed/size/trajectory/time variations","held_out_ranges":"disjoint larger position, unseen speed/size, curved trajectory and temporal phase","train_test_overlap":False})
|
||||
sys.path.insert(0,cfg["paths"]["model_root"]);rt=BrainRuntime(args.config,exp/"compiler-reports/extension-build");pool=pq.read_table(Path(cfg["paths"]["phase4f_root"])/"retinotopy/visual-projection-entry-pool.parquet").to_pandas()
|
||||
features,records=run_dataset(cfg,rt,samples,pool,exp);del rt;gc.collect();torch.cuda.empty_cache()
|
||||
training,bundles=train_experiments(cfg,features,samples,exp);sep=separability_audit(cfg,features,samples,exp);rob=robustness(cfg,features,samples,bundles,training,exp)
|
||||
# Select the formal best real joint result by cross-seed mean, never by a single best run.
|
||||
candidates=[]
|
||||
for cap,tasks in training["experiment_c"]["real-malecns"].items():
|
||||
for task,v in tasks.items():candidates.append((v["held_out_accuracy_mean"],cap,task,v))
|
||||
_,best_cap,best_task,best=max(candidates,key=lambda z:z[0]);mean_accuracy=best["held_out_accuracy_mean"]
|
||||
best_seed=min(cfg["training"]["model_seeds"],key=lambda s:abs(bundles[("real-malecns",best_cap,best_task,int(s))]["accuracy"]-mean_accuracy));best_bundle=bundles[("real-malecns",best_cap,best_task,int(best_seed))]
|
||||
ckpt={"decoder":best_bundle["bundle"],"encoder_route_choice":best_bundle["choice"],"window_ms":best_bundle["window"],"readout":best_bundle["readout"],"task":best_task,"capacity":best_cap,"brain_parameters_in_optimizer":False}
|
||||
torch.save(ckpt,learned/"checkpoints/best-frozen-interface.pt");torch.save(best_bundle["bundle"]["optimizer_state"],learned/"optimizer-state/best-decoder-optimizer.pt")
|
||||
write_json(learned/"training-metadata/best.json",{k:v for k,v in ckpt.items() if k not in {"decoder"}})
|
||||
# Reload checkpoint and exercise abstract action API.
|
||||
loaded=torch.load(learned/"checkpoints/best-frozen-interface.pt",weights_only=False);spec=cfg["dataset"]["tasks"][best_task];tr,ytr,te,yte=task_data(samples,spec);w=loaded["window_ms"];read=loaded["readout"];a=reduce_bins(features["real-malecns"]["route-a"][f"w{w}_{read}"],int(cfg["training"]["capacities"][best_cap]["feature_bins"]));b=reduce_bins(features["real-malecns"]["route-b"][f"w{w}_{read}"],int(cfg["training"]["capacities"][best_cap]["feature_bins"]));x=selected_matrix(a,b,samples,loaded["encoder_route_choice"]);logit=predict(loaded["decoder"],x[te[:1]])[0];padded=np.full(4,-1e9,np.float32);padded[:len(logit)]=logit;api=AbstractActionOutput(padded);api_audit={"checkpoint_reload_prediction":api.deterministic_argmax(),"stochastic_sample":api.sample(47500,temperature=.8),"epsilon_sample":api.sample(47501,epsilon=.1),"abstract_only":True,"game_semantics_assigned":False};write_json(exp/"audit/abstract-action-api.json",api_audit)
|
||||
# Latency audit includes real measured encoder/brain times and decoder microbenchmark.
|
||||
real_records=[x for x in records if x["graph"]=="real-malecns"];decoder_t0=time.perf_counter()
|
||||
for _ in range(1000):predict(loaded["decoder"],x[te[:1]])
|
||||
decoder_s=(time.perf_counter()-decoder_t0)/1000;latency={}
|
||||
for w in cfg["dataset"]["biological_windows_ms"]:
|
||||
# Brain increments were measured separately; cumulative fractions are directly available from saved NPZ.
|
||||
z=np.load(exp/"datasets/real-malecns-route-a.npz");wi=list(cfg["dataset"]["biological_windows_ms"]).index(w);brain=float(z["brain_increment_wall_seconds"][:,:wi+1].sum(1).mean());enc=float(z["encoder_wall_seconds"].mean());total=enc+brain+decoder_s
|
||||
latency[str(w)]={"encoder_seconds":enc,"brain_seconds":brain,"decoder_seconds":decoder_s,"total_seconds":total,"samples_per_hour":3600/total}
|
||||
write_json(exp/"latency/summary.json",latency)
|
||||
# Fresh frozen runtime regression.
|
||||
reg_rt=BrainRuntime(args.config,exp/"compiler-reports/extension-build");reg=regression(cfg,reg_rt,exp);del reg_rt;gc.collect();torch.cuda.empty_cache()
|
||||
after={str(x):sha256(x) for x in controls};unchanged=before==after
|
||||
# Formal gate.
|
||||
chance=1/int(cfg["dataset"]["tasks"][best_task]["classes"]);runs=best["runs"];margin=float(cfg["success_gate"]["stable_accuracy_margin_over_chance"]);success_seeds=sum(r["accuracy"]>=chance+margin for r in runs)
|
||||
downstream=best["readout"] in {"descending","motor-related"};matched=json.loads((exp/"audit/matched-input.json").read_text());matched_pass=all(vv["matched"] for g in matched.values() for r in g.values() for s in r.values() for vv in s.values())
|
||||
stable=success_seeds>=int(cfg["success_gate"]["successful_model_seed_majority"]);nonrun=all(not x["runaway"] and x["finite"] for x in records)
|
||||
control_means={g:training["experiment_c"][g][best_cap][best_task]["held_out_accuracy_mean"] for g in cfg["controls"]["graphs"]};control_difference=any(abs(control_means["real-malecns"]-v)>=.05 for g,v in control_means.items() if g!="real-malecns")
|
||||
bypass_too_easy=any(training["experiment_c"]["brain-bypass"][cap][task]["held_out_accuracy_mean"]>=float(cfg["success_gate"]["bypass_too_easy_accuracy"]) for cap in cfg["training"]["capacities"] for task in cfg["dataset"]["tasks"])
|
||||
passed=all([unchanged,reg["all_passed"],best["held_out_accuracy_mean"]>chance,stable,downstream,matched_pass,nonrun,control_difference])
|
||||
status="PASS-CANDIDATE" if passed else "FAIL-NO-FROZEN-INTERFACE-CANDIDATE"
|
||||
summary={"status":status,"candidate_generated":passed,"best":{"capacity":best_cap,"task":best_task,"window_ms":best["window_ms"],"readout":best["readout"],"held_out_accuracy_mean":best["held_out_accuracy_mean"],"held_out_accuracy_std":best["held_out_accuracy_std"],"chance":chance,"successful_seeds":success_seeds},
|
||||
"control_means":control_means,"control_difference":control_difference,"matched_input_pass":matched_pass,"no_runaway_nan_inf":nonrun,"frozen_brain_unchanged":unchanged,"regression":reg,"checkpoint_reload_pass":True,"bypass_too_easy":bypass_too_easy,
|
||||
"task_too_easy_for_brain_contribution_analysis":bypass_too_easy,"trainable_encoder_parameters":cfg["training"]["capacities"][best_cap]["encoder_parameters"],"trainable_decoder_parameters":best["runs"][0]["trainable_parameters"],"surrogate_note":"discrete route-selection coordinate descent is an engineering optimization mechanism, not a biological claim","prohibitions_respected":cfg["prohibitions"]}
|
||||
write_json(exp/"audit/summary.json",summary);interface.mkdir(parents=True,exist_ok=True)
|
||||
for src in (Path(__file__).resolve(),args.config.resolve(),Path(__file__).with_name("frozen_interface.py"),Path(__file__).with_name("brain_runtime.py")):
|
||||
dst=interface/src.name
|
||||
if src.resolve()!=dst.resolve():shutil.copy2(src,dst)
|
||||
write_json(interface/"VERSION.json",{"name":cfg["names"]["interface"],"status":status,"candidate_name":cfg["names"]["candidate"] if passed else None,"male_cns_weights_fixed":True,"learned_state_root":str(learned)})
|
||||
im=tree_manifest(interface,interface/"MANIFEST.json",{interface/"MANIFEST.json",interface/"checksums.sha256"});targets=sorted(p for p in interface.rglob("*") if p.is_file() and p.name!="checksums.sha256");(interface/"checksums.sha256").write_text("".join(f"{sha256(p)} {p.relative_to(interface)}\n" for p in targets))
|
||||
lm=tree_manifest(learned,learned/"MANIFEST.json",{learned/"MANIFEST.json",learned/"checksums.sha256"});targets=sorted(p for p in learned.rglob("*") if p.is_file() and p.name!="checksums.sha256");(learned/"checksums.sha256").write_text("".join(f"{sha256(p)} {p.relative_to(learned)}\n" for p in targets))
|
||||
em=tree_manifest(exp,exp/"manifests/manifest.json",{exp/"manifests/manifest.json",exp/"manifests/checksums.sha256"});targets=sorted(p for p in exp.rglob("*") if p.is_file() and p!=exp/"manifests/checksums.sha256");(exp/"manifests/checksums.sha256").write_text("".join(f"{sha256(p)} {p.relative_to(exp)}\n" for p in targets))
|
||||
print(json.dumps({"status":status,"best":summary["best"],"controls":control_means,"matched":matched_pass,"regression":reg["all_passed"],"frozen":unchanged},indent=2))
|
||||
|
||||
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,114 @@
|
||||
phase: 4G
|
||||
names:
|
||||
reference: MaleCNS-LIF-reference-v1
|
||||
runtime: MaleCNS-LIF-runtime-v1
|
||||
io_api: MaleCNS-BrainIO-v1
|
||||
activity_stream: MaleCNS-ActivityStream-v1
|
||||
interface: MaleCNS-FrozenInterface-v1
|
||||
candidate: MaleCNS-FrozenInterface-v1-candidate
|
||||
|
||||
paths:
|
||||
connectome_root: /data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif
|
||||
reference_root: /data/digital-drosophila/models/male-cns-lif/reference-v1
|
||||
runtime_root: /data/digital-drosophila/models/male-cns-lif/runtime-v1
|
||||
io_root: /data/digital-drosophila/models/male-cns-lif/io-v1
|
||||
phase4c_root: /data/digital-drosophila/experiments/phase4c-runtime
|
||||
phase4d_root: /data/digital-drosophila/experiments/phase4d-io
|
||||
phase4e_root: /data/digital-drosophila/experiments/phase4e-visual-calibration
|
||||
phase4f_root: /data/digital-drosophila/experiments/phase4f-retinotopic-vision
|
||||
phase4f_model_root: /data/digital-drosophila/models/male-cns-lif/visual-encoder-v1
|
||||
interface_root: /data/digital-drosophila/models/male-cns-lif/frozen-interface-v1
|
||||
learned_root: /data/digital-drosophila/models/learned/phase4g
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4g-frozen-interface
|
||||
|
||||
device:
|
||||
required_name_substring: RTX 4090
|
||||
launcher_visible_gpu_uuid: GPU-79f3a85d-adce-7833-7003-6488140ca596
|
||||
|
||||
model:
|
||||
dt_ms: 0.1
|
||||
membrane_tau_ms: 20.0
|
||||
synaptic_tau_ms: 5.0
|
||||
refractory_ms: 2.2
|
||||
resting_potential_mv: -52.0
|
||||
reset_potential_mv: -52.0
|
||||
threshold_mv: -45.0
|
||||
synaptic_delay_ms: 1.8
|
||||
w_syn_mv_per_anatomical_synapse: 0.275
|
||||
|
||||
sign_policy:
|
||||
neurotransmitter_sign: {acetylcholine: 1, gaba: -1, glutamate: -1, dopamine: 1, octopamine: 1, serotonin: 1, histamine: -1}
|
||||
unknown_sign: 0
|
||||
unclear_sign: 0
|
||||
|
||||
dataset:
|
||||
patterns: [left-spot, right-spot, upper-spot, lower-spot, left-to-right, right-to-left, up-to-down, down-to-up, looming, sparse-moving-object, static-object, full-field]
|
||||
train_stimulus_seeds: [47001, 47002, 47003, 47004, 47005]
|
||||
evaluation_stimulus_seeds: [47101, 47102, 47103]
|
||||
biological_windows_ms: [10, 20, 30, 50, 100]
|
||||
temporal_segments: 5
|
||||
targets_per_segment: 460
|
||||
poisson_rate_hz: 50.0
|
||||
external_drive_mv: 8.8
|
||||
tasks:
|
||||
left-right: {patterns: [left-spot, right-spot], labels: [0, 1], classes: 2}
|
||||
up-down: {patterns: [upper-spot, lower-spot], labels: [0, 1], classes: 2}
|
||||
moving-static: {patterns: [sparse-moving-object, static-object], labels: [0, 1], classes: 2}
|
||||
looming-translation: {patterns: [looming, left-to-right], labels: [0, 1], classes: 2}
|
||||
spatial-4way: {patterns: [left-spot, right-spot, upper-spot, lower-spot], labels: [0, 1, 2, 3], classes: 4}
|
||||
motion-4way: {patterns: [left-to-right, right-to-left, up-to-down, down-to-up], labels: [0, 1, 2, 3], classes: 4}
|
||||
|
||||
training:
|
||||
model_seeds: [47201, 47202, 47203, 47204, 47205]
|
||||
evaluation_seeds: [47101, 47102, 47103]
|
||||
epochs: 120
|
||||
learning_rate: 0.02
|
||||
capacities:
|
||||
Tiny: {feature_bins: 32, decoder: linear, encoder_parameters: 2}
|
||||
Small: {feature_bins: 128, decoder: mlp, hidden: 16, encoder_parameters: 24}
|
||||
abstract_actions: 4
|
||||
|
||||
controls:
|
||||
graph_seed: 47300
|
||||
graphs: [real-malecns, shuffled-edges, degree-preserving, brain-bypass]
|
||||
shuffled_definition: independent uniform target per existing CSR edge slot; source degrees and edge count preserved
|
||||
degree_preserving_definition: global permutation of original target array; exact source out-degree and target in-degree preserved
|
||||
|
||||
robustness:
|
||||
task: spatial-4way
|
||||
conditions: [input-noise, event-dropout, neuron-dropout, adapter-noise, partial-occlusion, temporal-jitter]
|
||||
perturbation_fraction: 0.15
|
||||
|
||||
readout:
|
||||
bins_ms: [1, 5, 10, 20, 50]
|
||||
|
||||
regression:
|
||||
float_absolute_tolerance: 0.00390625
|
||||
reference_trials:
|
||||
- {name: auditory-10hz, population: auditory, rate_hz: 10, seed: 42001, duration_ms: 1000.0}
|
||||
- {name: auditory-100hz, population: auditory, rate_hz: 100, seed: 42001, duration_ms: 1000.0}
|
||||
- {name: olfactory-50hz, population: olfactory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
- {name: mechanosensory-50hz, population: mechanosensory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
- {name: gustatory-50hz, population: gustatory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
|
||||
success_gate:
|
||||
stable_accuracy_margin_over_chance: 0.10
|
||||
successful_model_seed_majority: 3
|
||||
require_descending_or_motor_information: true
|
||||
require_control_difference: true
|
||||
bypass_too_easy_accuracy: 0.95
|
||||
|
||||
prohibitions:
|
||||
brain_backpropagation: false
|
||||
connectome_changes: false
|
||||
transmitter_sign_changes: false
|
||||
brain_gain_changes: false
|
||||
internal_weight_changes: false
|
||||
lif_parameter_changes: false
|
||||
game_rom: false
|
||||
stable_retro: false
|
||||
reinforcement_learning: false
|
||||
dopamine_stdp: false
|
||||
web_server: false
|
||||
three_d_frontend: false
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"artifact": "io-v1",
|
||||
"files": [
|
||||
{
|
||||
"bytes": 1564,
|
||||
"path": "VERSION.json",
|
||||
"sha256": "2ad17a93bf22339db56ae3869af9a2a7c26ca7359d8923c693d71dde63927812"
|
||||
},
|
||||
{
|
||||
"bytes": 23822,
|
||||
"path": "brain_runtime.py",
|
||||
"sha256": "1c4e3e6f06708bfec30b24e92297f320b2b8b87ebbbe6da501f5f2db4f74762a"
|
||||
},
|
||||
{
|
||||
"bytes": 34442,
|
||||
"path": "phase4d.py",
|
||||
"sha256": "9ed8e4246252bdfc590c88e2993b7e105f278c6c4943e1c688f387c1cf54aeb7"
|
||||
},
|
||||
{
|
||||
"bytes": 3175,
|
||||
"path": "phase4d.yaml",
|
||||
"sha256": "e94301362e630e59fa248f883ae09d369dce720bce970d0d2f47ef6a6ed36a2a"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"activity_stream_version": "1.0.0",
|
||||
"api_methods": [
|
||||
"reset",
|
||||
"step",
|
||||
"read_spikes",
|
||||
"read_population",
|
||||
"read_output",
|
||||
"get_state_summary",
|
||||
"checkpoint_state",
|
||||
"restore_state"
|
||||
],
|
||||
"api_tests": {
|
||||
"all_passed": true,
|
||||
"checkpoint_restore_spikes_exact": true,
|
||||
"checkpoint_restore_voltage_exact": true,
|
||||
"continuous_step_does_not_reset_clock": true,
|
||||
"fixed_temporal_bins_tested": {
|
||||
"1": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"10": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"20": [
|
||||
1,
|
||||
0
|
||||
],
|
||||
"5": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0
|
||||
]
|
||||
},
|
||||
"per_class_aggregation_labels": 1,
|
||||
"per_neuron_readout_shape": 1316,
|
||||
"per_type_aggregation_labels": 481,
|
||||
"readout_modes_passed": true,
|
||||
"reset_same_seed_continuous_chunk_spikes_exact": true,
|
||||
"single_vs_chunk_voltage_max_abs_error": 0.0,
|
||||
"timestamps_present": true
|
||||
},
|
||||
"api_version": "1.0.0",
|
||||
"continuous_gpu_state": true,
|
||||
"game_agnostic": true,
|
||||
"input_batch_version": "1.0.0",
|
||||
"name": "MaleCNS-BrainIO-v1",
|
||||
"prohibitions": {
|
||||
"connectome_or_sign_changes": false,
|
||||
"controller_buttons": false,
|
||||
"rom_access": false,
|
||||
"stable_retro": false,
|
||||
"training": false,
|
||||
"web_server": false
|
||||
},
|
||||
"runtime_dependency": "MaleCNS-LIF-runtime-v1"
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stable game-agnostic Brain I/O API for MaleCNS-LIF-runtime-v1."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
import numpy as np
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
|
||||
API_VERSION = "1.0.0"
|
||||
INPUT_BATCH_VERSION = "1.0.0"
|
||||
ACTIVITY_STREAM_VERSION = "1.0.0"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InputEvent:
|
||||
target_neuron_idx: np.ndarray
|
||||
stimulation_mode: str
|
||||
value_kind: str
|
||||
value: float | np.ndarray
|
||||
start_ms: float
|
||||
duration_ms: float
|
||||
source_id: str
|
||||
channel_id: str
|
||||
amplitude_mv: float = 0.0
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def validate(self, neuron_count: int, dt_ms: float) -> None:
|
||||
target = np.asarray(self.target_neuron_idx)
|
||||
if target.ndim != 1 or target.dtype.kind not in "iu":
|
||||
raise ValueError("target_neuron_idx must be a one-dimensional integer array")
|
||||
if len(target) and (int(target.min()) < 0 or int(target.max()) >= neuron_count):
|
||||
raise ValueError("target neuron_idx outside canonical range")
|
||||
if len(np.unique(target)) != len(target):
|
||||
raise ValueError("target neuron_idx values must be unique")
|
||||
if self.stimulation_mode not in {"spike_injection", "external_synaptic_drive"}:
|
||||
raise ValueError(f"unsupported stimulation_mode {self.stimulation_mode!r}")
|
||||
if self.value_kind not in {"binary", "scalar_intensity", "poisson_rate_hz", "signed_modulation"}:
|
||||
raise ValueError(f"unsupported value_kind {self.value_kind!r}")
|
||||
if self.start_ms < 0 or self.duration_ms <= 0:
|
||||
raise ValueError("timestamps/duration must be non-negative/positive")
|
||||
for x in (self.start_ms, self.duration_ms):
|
||||
if not math.isclose(x / dt_ms, round(x / dt_ms), abs_tol=1e-8):
|
||||
raise ValueError(f"{x} ms is not aligned to dt={dt_ms} ms")
|
||||
value = np.asarray(self.value)
|
||||
if value.ndim > 1 or (value.ndim == 1 and len(value) not in (1, len(target))):
|
||||
raise ValueError("value must be scalar or match target count")
|
||||
if self.value_kind == "poisson_rate_hz" and np.any(value < 0):
|
||||
raise ValueError("Poisson rate cannot be negative")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InputBatch:
|
||||
events: tuple[InputEvent, ...]
|
||||
batch_id: str
|
||||
version: str = INPUT_BATCH_VERSION
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def validate(self, neuron_count: int, dt_ms: float) -> None:
|
||||
if self.version != INPUT_BATCH_VERSION:
|
||||
raise ValueError(f"InputBatch version must be {INPUT_BATCH_VERSION}")
|
||||
for event in self.events:
|
||||
event.validate(neuron_count, dt_ms)
|
||||
|
||||
|
||||
class VisualAdapterV1:
|
||||
"""Maps abstract channels to audited visual hierarchy populations."""
|
||||
|
||||
def __init__(self, config: dict[str, Any], phase4c_root: Path):
|
||||
self.config = config
|
||||
self.phase4c_root = phase4c_root
|
||||
self.modes: dict[str, np.ndarray] = {}
|
||||
for mode, spec in config["visual_adapter"]["modes"].items():
|
||||
population = spec["hierarchy_population"]
|
||||
path = phase4c_root / "visual-audit" / f"hierarchy-{population}.parquet"
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(path)
|
||||
self.modes[mode] = pq.read_table(path, columns=["neuron_idx"])["neuron_idx"].to_numpy().astype(np.int64)
|
||||
|
||||
def population(self, mode: str) -> np.ndarray:
|
||||
if mode not in self.modes:
|
||||
raise KeyError(mode)
|
||||
return self.modes[mode].copy()
|
||||
|
||||
def encode(
|
||||
self, mode: str, channel_values: np.ndarray, start_ms: float, duration_ms: float,
|
||||
source_id: str, channel_id: str, value_kind: str = "poisson_rate_hz",
|
||||
) -> InputBatch:
|
||||
if mode not in self.modes:
|
||||
raise KeyError(mode)
|
||||
spec = self.config["visual_adapter"]["modes"][mode]
|
||||
targets = self.modes[mode]
|
||||
values = np.asarray(channel_values, dtype=np.float32)
|
||||
if values.shape != (len(targets),):
|
||||
raise ValueError(f"visual channel must have shape {(len(targets),)}, got {values.shape}")
|
||||
selected = np.flatnonzero(values != 0).astype(np.int64)
|
||||
target = targets[selected]
|
||||
selected_values = values[selected]
|
||||
event = InputEvent(
|
||||
target_neuron_idx=target,
|
||||
stimulation_mode=spec["input_mode"],
|
||||
value_kind=value_kind,
|
||||
value=selected_values,
|
||||
amplitude_mv=float(self.config["visual_adapter"]["external_drive_mv"]),
|
||||
start_ms=start_ms,
|
||||
duration_ms=duration_ms,
|
||||
source_id=source_id,
|
||||
channel_id=channel_id,
|
||||
metadata={
|
||||
"adapter": self.config["names"]["visual_adapter"],
|
||||
"adapter_mode": mode,
|
||||
"biological_fidelity": spec["biological_fidelity"],
|
||||
"connectome_weights_modified": False,
|
||||
},
|
||||
)
|
||||
return InputBatch(events=(event,), batch_id=f"{source_id}:{channel_id}:{start_ms}", metadata={"adapter_mode": mode})
|
||||
|
||||
|
||||
class BrainRuntime:
|
||||
"""Persistent exact-state BrainRuntime API. It has no game-specific dependency."""
|
||||
|
||||
def __init__(self, config_path: Path, extension_build_dir: Path):
|
||||
self.config_path = config_path.resolve()
|
||||
self.cfg = yaml.safe_load(self.config_path.read_text(encoding="utf-8"))
|
||||
self.dt = float(self.cfg["model"]["dt_ms"])
|
||||
self.n = 166700
|
||||
self.model_root = Path(self.cfg["paths"]["model_root"])
|
||||
self.runtime_root = Path(self.cfg["paths"]["runtime_root"])
|
||||
self.input_root = Path(self.cfg["paths"]["connectome_root"])
|
||||
runtime_module_dir = str(self.runtime_root)
|
||||
if runtime_module_dir not in sys.path:
|
||||
sys.path.insert(0, runtime_module_dir)
|
||||
import phase4c as runtime_module
|
||||
self.runtime_module = runtime_module
|
||||
p4cfg = yaml.safe_load((self.runtime_root / "phase4c.yaml").read_text(encoding="utf-8"))
|
||||
self.paths = runtime_module.exact_paths(p4cfg)
|
||||
self.indptr_np, self.indices_np, self.raw_np = runtime_module.p4b.validate_graph(p4cfg, self.paths)
|
||||
self.table = runtime_module.full_metadata(self.paths["metadata"])
|
||||
self.population_masks = runtime_module.populations(self.table)
|
||||
if not torch.cuda.is_available() or torch.cuda.device_count() != 1:
|
||||
raise RuntimeError("BrainRuntime requires exactly one visible CUDA GPU")
|
||||
self.device = torch.device("cuda:0")
|
||||
props = torch.cuda.get_device_properties(self.device)
|
||||
if self.cfg["device"]["required_name_substring"] not in props.name:
|
||||
raise RuntimeError(f"required RTX 4090, got {props.name}")
|
||||
extension_build_dir.mkdir(parents=True, exist_ok=True)
|
||||
build_experiment_root = extension_build_dir.parent.parent
|
||||
(build_experiment_root / "compiler-reports/extension-build").mkdir(parents=True, exist_ok=True)
|
||||
self.extension, report = runtime_module.build_extension(self.runtime_root, build_experiment_root)
|
||||
if self.extension is None:
|
||||
raise RuntimeError(f"runtime-v1 extension unavailable: {report}")
|
||||
base = runtime_module.Runtime(p4cfg, self.table, self.indptr_np, self.indices_np, self.raw_np, self.device, self.extension)
|
||||
self.indptr, self.indices, self.weights = base.indptr, base.indices, base.weights
|
||||
self.signs, self.superclass = base.signs, base.superclass
|
||||
self.neuron_type = np.asarray(self.table["annotation_type"].to_pylist(), dtype=object)
|
||||
self.neuron_class = np.asarray(self.table["annotation_class"].to_pylist(), dtype=object)
|
||||
self.body_id = self.table["body_id"].to_numpy().astype(np.uint64)
|
||||
m = self.cfg["model"]
|
||||
self.delay_steps = int(round(float(m["synaptic_delay_ms"]) / self.dt))
|
||||
self.queue_len = self.delay_steps + 1
|
||||
self.refractory_steps = int(round(float(m["refractory_ms"]) / self.dt))
|
||||
self.alpha = self.dt / float(m["membrane_tau_ms"])
|
||||
self.g_decay = 1.0 - self.dt / float(m["synaptic_tau_ms"])
|
||||
self.rest = float(m["resting_potential_mv"])
|
||||
self.reset_value = float(m["reset_potential_mv"])
|
||||
self.threshold = float(m["threshold_mv"])
|
||||
self.output_populations = self._output_populations()
|
||||
self.reset(0)
|
||||
|
||||
def _output_populations(self) -> dict[str, np.ndarray]:
|
||||
s = self.superclass
|
||||
return {
|
||||
"descending": np.flatnonzero(np.isin(s, ["descending_neuron", "descending_neuron_tbc"])).astype(np.int32),
|
||||
"motor_related": np.flatnonzero(np.isin(s, ["vnc_motor", "cb_motor"])).astype(np.int32),
|
||||
"ascending": np.flatnonzero(np.isin(s, ["ascending_neuron", "sensory_ascending", "sensory_ascending_tbc", "efferent_ascending"])).astype(np.int32),
|
||||
}
|
||||
|
||||
def reset(self, seed: int, initial_state: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
self.seed = int(seed)
|
||||
self.generator = torch.Generator(device=self.device); self.generator.manual_seed(self.seed)
|
||||
self.v = torch.full((self.n,), self.rest, dtype=torch.float32, device=self.device)
|
||||
self.g = torch.zeros(self.n, dtype=torch.float32, device=self.device)
|
||||
self.refractory = torch.zeros(self.n, dtype=torch.int32, device=self.device)
|
||||
self.delay_queue = torch.zeros((self.queue_len, self.n), dtype=torch.float32, device=self.device)
|
||||
self.spike_counts = torch.zeros(self.n, dtype=torch.int32, device=self.device)
|
||||
self.simulation_step = 0; self.sequence_id = 0
|
||||
self.spike_steps: list[np.ndarray] = []; self.spike_nodes: list[np.ndarray] = []
|
||||
self.spike_origins: list[np.ndarray] = []
|
||||
self.drive_steps: list[np.ndarray] = []; self.drive_nodes: list[np.ndarray] = []
|
||||
self.drive_values: list[np.ndarray] = []; self.input_audit: list[dict[str, Any]] = []
|
||||
if initial_state is not None:
|
||||
self.restore_state(initial_state)
|
||||
return self.get_state_summary()
|
||||
|
||||
@property
|
||||
def simulation_time_ms(self) -> float:
|
||||
return self.simulation_step * self.dt
|
||||
|
||||
def _event_values(self, event: InputEvent, targets: torch.Tensor) -> torch.Tensor:
|
||||
value = np.asarray(event.value, dtype=np.float32)
|
||||
if value.ndim == 0:
|
||||
return torch.full((len(targets),), float(value), dtype=torch.float32, device=self.device)
|
||||
return torch.as_tensor(value, dtype=torch.float32, device=self.device)
|
||||
|
||||
def _active(self, event: InputEvent, time_ms: float) -> bool:
|
||||
return event.start_ms - 1e-9 <= time_ms < event.start_ms + event.duration_ms - 1e-9
|
||||
|
||||
def _hybrid_edges(self, sources: torch.Tensor) -> torch.Tensor:
|
||||
starts = self.indptr[sources]
|
||||
degrees = self.indptr[sources + 1] - starts
|
||||
cumulative = torch.cumsum(degrees, 0)
|
||||
total = int(cumulative[-1].item()) if cumulative.numel() else 0
|
||||
output = torch.empty(total, dtype=torch.int64, device=self.device)
|
||||
if total:
|
||||
self.extension.fill_edges(self.indptr, sources, cumulative - degrees, output)
|
||||
return output
|
||||
|
||||
def step(self, input_batch: InputBatch | None, biological_ms: float) -> dict[str, Any]:
|
||||
steps = int(round(float(biological_ms) / self.dt))
|
||||
if not math.isclose(steps * self.dt, float(biological_ms), abs_tol=1e-8) or steps <= 0:
|
||||
raise ValueError("biological_ms must be a positive multiple of dt")
|
||||
batch = input_batch or InputBatch(events=(), batch_id="zero-input")
|
||||
batch.validate(self.n, self.dt)
|
||||
batch_start = self.simulation_time_ms
|
||||
prepared = [(e, torch.as_tensor(e.target_neuron_idx, dtype=torch.int64, device=self.device)) for e in batch.events]
|
||||
for event in batch.events:
|
||||
self.input_audit.append({
|
||||
"batch_id": batch.batch_id, "source_id": event.source_id, "channel_id": event.channel_id,
|
||||
"mode": event.stimulation_mode, "value_kind": event.value_kind,
|
||||
"start_ms": event.start_ms, "duration_ms": event.duration_ms,
|
||||
"target_count": int(len(event.target_neuron_idx)), "metadata": event.metadata,
|
||||
"external_drive_is_connectome_transmission": False,
|
||||
})
|
||||
start_event_count = sum(len(x) for x in self.spike_nodes)
|
||||
start_drive_count = sum(len(x) for x in self.drive_nodes)
|
||||
for _ in range(steps):
|
||||
global_step = self.simulation_step; time_ms = self.simulation_time_ms
|
||||
slot = global_step % self.queue_len
|
||||
self.g.mul_(self.g_decay).add_(self.delay_queue[slot]); self.delay_queue[slot].zero_()
|
||||
spike_requests: list[tuple[InputEvent, torch.Tensor, torch.Tensor]] = []
|
||||
for event, targets in prepared:
|
||||
if not self._active(event, time_ms) or targets.numel() == 0:
|
||||
continue
|
||||
values = self._event_values(event, targets)
|
||||
if event.value_kind == "poisson_rate_hz":
|
||||
selected = torch.rand(len(targets), generator=self.generator, device=self.device) < (values * self.dt / 1000.0)
|
||||
elif event.value_kind == "binary":
|
||||
selected = values != 0
|
||||
elif event.value_kind == "scalar_intensity":
|
||||
selected = values != 0
|
||||
else: # signed_modulation
|
||||
selected = torch.rand(len(targets), generator=self.generator, device=self.device) < (torch.abs(values) * self.dt / 1000.0)
|
||||
if event.stimulation_mode == "external_synaptic_drive":
|
||||
chosen = targets[selected]
|
||||
if chosen.numel():
|
||||
signed_value = values[selected] * float(event.amplitude_mv)
|
||||
if event.value_kind == "poisson_rate_hz":
|
||||
signed_value = torch.full_like(signed_value, float(event.amplitude_mv))
|
||||
self.g.index_add_(0, chosen, signed_value)
|
||||
node_np = chosen.cpu().numpy().astype(np.int32, copy=True)
|
||||
val_np = signed_value.cpu().numpy().astype(np.float32, copy=True)
|
||||
self.drive_steps.append(np.full(len(node_np), global_step, dtype=np.int32))
|
||||
self.drive_nodes.append(node_np); self.drive_values.append(val_np)
|
||||
else:
|
||||
spike_requests.append((event, targets, selected))
|
||||
self.refractory.clamp_min_(1).sub_(1)
|
||||
available = self.refractory == 0
|
||||
self.v[available] += self.alpha * (self.g[available] - (self.v[available] - self.rest))
|
||||
self.v[~available] = self.reset_value
|
||||
natural = available & (self.v >= self.threshold)
|
||||
spikes = natural.clone()
|
||||
injected = torch.zeros(self.n, dtype=torch.bool, device=self.device)
|
||||
for _event, targets, selected in spike_requests:
|
||||
accepted = selected & available[targets]
|
||||
chosen = targets[accepted]
|
||||
if chosen.numel():
|
||||
spikes[chosen] = True; injected[chosen] = ~natural[chosen]
|
||||
sources = torch.nonzero(spikes, as_tuple=False).flatten()
|
||||
self.spike_counts.add_(spikes.to(torch.int32))
|
||||
if sources.numel():
|
||||
self.v[sources] = self.reset_value; self.refractory[sources] = self.refractory_steps
|
||||
edge_ids = self._hybrid_edges(sources)
|
||||
if edge_ids.numel():
|
||||
self.delay_queue[(global_step + self.delay_steps) % self.queue_len].scatter_add_(0, self.indices[edge_ids].long(), self.weights[edge_ids])
|
||||
src_np = sources.cpu().numpy().astype(np.int32, copy=True)
|
||||
origin_np = np.where(injected[sources].cpu().numpy(), "external_spike_injection", "network_spike")
|
||||
self.spike_steps.append(np.full(len(src_np), global_step, dtype=np.int32))
|
||||
self.spike_nodes.append(src_np); self.spike_origins.append(origin_np)
|
||||
self.simulation_step += 1
|
||||
torch.cuda.synchronize()
|
||||
self.sequence_id += 1
|
||||
return {
|
||||
"api_version": API_VERSION, "sequence_id": self.sequence_id,
|
||||
"batch_id": batch.batch_id, "start_time_ms": batch_start,
|
||||
"end_time_ms": self.simulation_time_ms, "biological_ms": biological_ms,
|
||||
"new_spikes": sum(len(x) for x in self.spike_nodes) - start_event_count,
|
||||
"new_external_drive_events": sum(len(x) for x in self.drive_nodes) - start_drive_count,
|
||||
}
|
||||
|
||||
def _events(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
steps = np.concatenate(self.spike_steps) if self.spike_steps else np.empty(0, np.int32)
|
||||
nodes = np.concatenate(self.spike_nodes) if self.spike_nodes else np.empty(0, np.int32)
|
||||
origins = np.concatenate(self.spike_origins) if self.spike_origins else np.empty(0, object)
|
||||
return steps, nodes, origins
|
||||
|
||||
def read_spikes(self, window_ms: float | None = None) -> dict[str, Any]:
|
||||
steps, nodes, origins = self._events()
|
||||
if window_ms is not None:
|
||||
threshold = self.simulation_step - int(round(window_ms / self.dt))
|
||||
mask = steps >= threshold; steps, nodes, origins = steps[mask], nodes[mask], origins[mask]
|
||||
return {
|
||||
"simulation_time_ms": self.simulation_time_ms,
|
||||
"step": steps, "timestamp_ms": steps.astype(np.float64) * self.dt,
|
||||
"neuron_idx": nodes, "origin": origins,
|
||||
}
|
||||
|
||||
def read_population(self, population_id: str, window_ms: float) -> dict[str, Any]:
|
||||
if population_id not in self.output_populations:
|
||||
raise KeyError(population_id)
|
||||
members = self.output_populations[population_id]
|
||||
events = self.read_spikes(window_ms)
|
||||
mask = np.isin(events["neuron_idx"], members)
|
||||
nodes = events["neuron_idx"][mask]
|
||||
counts = np.zeros(len(members), dtype=np.int32)
|
||||
if len(nodes):
|
||||
lookup = np.full(self.n, -1, dtype=np.int32); lookup[members] = np.arange(len(members), dtype=np.int32)
|
||||
np.add.at(counts, lookup[nodes], 1)
|
||||
seconds = window_ms / 1000.0
|
||||
return {
|
||||
"simulation_time_ms": self.simulation_time_ms, "population_id": population_id,
|
||||
"neuron_idx": members.copy(), "spike_counts": counts,
|
||||
"firing_rate_hz": counts.astype(np.float32) / seconds,
|
||||
"active_mask": counts > 0, "aggregate_rate_hz": float(counts.sum() / (len(members) * seconds)),
|
||||
"total_spikes": int(counts.sum()), "active_neurons": int(np.count_nonzero(counts)),
|
||||
}
|
||||
|
||||
def read_output(self, output_spec: dict[str, Any], window_ms: float) -> dict[str, Any]:
|
||||
population = output_spec.get("population", "descending")
|
||||
aggregation = output_spec.get("aggregation", "per-neuron")
|
||||
base = self.read_population(population, window_ms)
|
||||
members = base["neuron_idx"]; counts = base["spike_counts"]
|
||||
if aggregation == "per-neuron":
|
||||
return base
|
||||
if aggregation not in {"per-type", "per-class"}:
|
||||
raise ValueError("aggregation must be per-neuron, per-type, or per-class")
|
||||
labels = self.neuron_type if aggregation == "per-type" else self.neuron_class
|
||||
result: dict[str, dict[str, float | int]] = {}
|
||||
for label in sorted({"<null>" if labels[i] is None else str(labels[i]) for i in members}):
|
||||
mask = np.asarray([("<null>" if labels[i] is None else str(labels[i])) == label for i in members])
|
||||
result[label] = {"neuron_count": int(mask.sum()), "spike_count": int(counts[mask].sum()), "mean_rate_hz": float(base["firing_rate_hz"][mask].mean())}
|
||||
return {"simulation_time_ms": self.simulation_time_ms, "population_id": population, "aggregation": aggregation, "features": result}
|
||||
|
||||
def fixed_temporal_bins(self, population_id: str, bin_ms: float, window_ms: float) -> dict[str, Any]:
|
||||
if bin_ms not in self.cfg["readout"]["bins_ms"]:
|
||||
raise ValueError("unsupported fixed bin")
|
||||
ev = self.read_spikes(window_ms); members = self.output_populations[population_id]
|
||||
mask = np.isin(ev["neuron_idx"], members); times = ev["timestamp_ms"][mask]
|
||||
start = self.simulation_time_ms - window_ms
|
||||
edges = np.arange(start, self.simulation_time_ms + bin_ms + 1e-9, bin_ms)
|
||||
counts, _ = np.histogram(times, bins=edges)
|
||||
return {"simulation_time_ms": self.simulation_time_ms, "population_id": population_id, "bin_ms": bin_ms, "bin_start_ms": edges[:-1], "spike_counts": counts.astype(np.int32)}
|
||||
|
||||
def get_state_summary(self) -> dict[str, Any]:
|
||||
return {
|
||||
"api_version": API_VERSION, "runtime": self.cfg["names"]["runtime"],
|
||||
"simulation_step": self.simulation_step, "simulation_time_ms": self.simulation_time_ms,
|
||||
"sequence_id": self.sequence_id, "seed": self.seed,
|
||||
"neurons": self.n, "edges": int(self.indices.numel()),
|
||||
"active_neurons_total": int(torch.count_nonzero(self.spike_counts).item()),
|
||||
"spikes_total": int(self.spike_counts.sum().item()),
|
||||
"external_drive_events_total": sum(len(x) for x in self.drive_nodes),
|
||||
}
|
||||
|
||||
def checkpoint_state(self) -> dict[str, Any]:
|
||||
return {
|
||||
"api_version": API_VERSION, "seed": self.seed, "simulation_step": self.simulation_step,
|
||||
"sequence_id": self.sequence_id, "generator_state": self.generator.get_state().cpu(),
|
||||
"v": self.v.clone(), "g": self.g.clone(), "refractory": self.refractory.clone(),
|
||||
"delay_queue": self.delay_queue.clone(), "spike_counts": self.spike_counts.clone(),
|
||||
"spike_steps": copy.deepcopy(self.spike_steps), "spike_nodes": copy.deepcopy(self.spike_nodes),
|
||||
"spike_origins": copy.deepcopy(self.spike_origins), "drive_steps": copy.deepcopy(self.drive_steps),
|
||||
"drive_nodes": copy.deepcopy(self.drive_nodes), "drive_values": copy.deepcopy(self.drive_values),
|
||||
"input_audit": copy.deepcopy(self.input_audit),
|
||||
}
|
||||
|
||||
def restore_state(self, state: dict[str, Any]) -> dict[str, Any]:
|
||||
if state.get("api_version") != API_VERSION:
|
||||
raise ValueError("checkpoint API version mismatch")
|
||||
self.seed = int(state["seed"]); self.simulation_step = int(state["simulation_step"]); self.sequence_id = int(state["sequence_id"])
|
||||
self.generator.set_state(state["generator_state"].cpu())
|
||||
for name in ("v", "g", "refractory", "delay_queue", "spike_counts"):
|
||||
setattr(self, name, state[name].clone().to(self.device))
|
||||
for name in ("spike_steps", "spike_nodes", "spike_origins", "drive_steps", "drive_nodes", "drive_values", "input_audit"):
|
||||
setattr(self, name, copy.deepcopy(state[name]))
|
||||
return self.get_state_summary()
|
||||
|
||||
def activity_packet(self, window_ms: float, input_metadata: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
ev = self.read_spikes(window_ms)
|
||||
aggregates = {name: {k: v for k, v in self.read_population(name, window_ms).items() if k in ("active_neurons", "total_spikes", "aggregate_rate_hz")} for name in self.output_populations}
|
||||
return {
|
||||
"contract_version": ACTIVITY_STREAM_VERSION, "simulation_time_ms": self.simulation_time_ms,
|
||||
"sequence_id": self.sequence_id, "window_ms": window_ms,
|
||||
"spike_neuron_idx": ev["neuron_idx"].tolist(), "spike_timestamp_ms": ev["timestamp_ms"].tolist(),
|
||||
"population_aggregates": aggregates, "active_neuron_count": int(len(np.unique(ev["neuron_idx"]))),
|
||||
"input_channel_metadata": input_metadata or {}, "representation": "event-only",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
2ad17a93bf22339db56ae3869af9a2a7c26ca7359d8923c693d71dde63927812 VERSION.json
|
||||
1c4e3e6f06708bfec30b24e92297f320b2b8b87ebbbe6da501f5f2db4f74762a brain_runtime.py
|
||||
9ed8e4246252bdfc590c88e2993b7e105f278c6c4943e1c688f387c1cf54aeb7 phase4d.py
|
||||
e94301362e630e59fa248f883ae09d369dce720bce970d0d2f47ef6a6ed36a2a phase4d.yaml
|
||||
4eb6390bbfcea3d03c70215cc2492ae233d90ea0d08f5fb3376d2cdcf92a7f52 MANIFEST.json
|
||||
@@ -0,0 +1,535 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4D orchestration: stable Brain I/O API, timing, readout and regression."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import time
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
from brain_runtime import (
|
||||
ACTIVITY_STREAM_VERSION, API_VERSION, INPUT_BATCH_VERSION,
|
||||
BrainRuntime, InputBatch, InputEvent, VisualAdapterV1,
|
||||
)
|
||||
|
||||
|
||||
def sha256(path: Path, chunk: int = 8 << 20) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as f:
|
||||
while data := f.read(chunk): h.update(data)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def write_json(path: Path, value: Any) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def np_json(value: Any) -> Any:
|
||||
if isinstance(value, np.ndarray): return value.tolist()
|
||||
if isinstance(value, np.generic): return value.item()
|
||||
raise TypeError(type(value).__name__)
|
||||
|
||||
|
||||
def metadata_populations(runtime: BrainRuntime) -> dict[str, np.ndarray]:
|
||||
return {k: v.copy() for k, v in runtime.output_populations.items()}
|
||||
|
||||
|
||||
def freeze_populations(exp: Path, runtime: BrainRuntime) -> dict[str, Any]:
|
||||
expressions = {
|
||||
"descending": "annotation_superclass in {'descending_neuron','descending_neuron_tbc'}",
|
||||
"motor_related": "annotation_superclass in {'vnc_motor','cb_motor'}",
|
||||
"ascending": "annotation_superclass in {'ascending_neuron','sensory_ascending','sensory_ascending_tbc','efferent_ascending'}",
|
||||
}
|
||||
nt = runtime.table["neurotransmitter"].to_pylist()
|
||||
result: dict[str, Any] = {}
|
||||
for name, nodes in metadata_populations(runtime).items():
|
||||
table = pa.table({
|
||||
"neuron_idx": pa.array(nodes, type=pa.int32()),
|
||||
"body_id": pa.array(runtime.body_id[nodes], type=pa.uint64()),
|
||||
"superclass": pa.array([runtime.superclass[i] for i in nodes], type=pa.string()),
|
||||
"type": pa.array([runtime.neuron_type[i] for i in nodes], type=pa.string()),
|
||||
"class": pa.array([runtime.neuron_class[i] for i in nodes], type=pa.string()),
|
||||
"neurotransmitter": pa.array([nt[i] for i in nodes], type=pa.string()),
|
||||
})
|
||||
pq.write_table(table, exp / "populations" / f"{name}.parquet", compression="zstd")
|
||||
nts = Counter("<null>" if nt[i] is None else str(nt[i]) for i in nodes)
|
||||
result[name] = {
|
||||
"selection_expression": expressions[name], "count": int(len(nodes)),
|
||||
"neurotransmitter_statistics": dict(sorted(nts.items())),
|
||||
"membership_file": str(exp / "populations" / f"{name}.parquet"),
|
||||
}
|
||||
write_json(exp / "populations/summary.json", result)
|
||||
return result
|
||||
|
||||
|
||||
def audit_visual_sides(exp: Path, runtime: BrainRuntime, adapter: VisualAdapterV1) -> dict[str, Any]:
|
||||
values = runtime.table["annotation_rootSide"].to_pylist() if "annotation_rootSide" in runtime.table.column_names else [None] * runtime.n
|
||||
result: dict[str, Any] = {}
|
||||
for mode in ("engineering-first-order", "engineering-second-order"):
|
||||
nodes = adapter.population(mode); c = Counter("<null>" if values[i] is None else str(values[i]) for i in nodes)
|
||||
coverage = 1.0 - c.get("<null>", 0) / len(nodes)
|
||||
reliable = coverage >= 0.90 and c.get("L", 0) > 0 and c.get("R", 0) > 0
|
||||
result[mode] = {"field": "annotation_rootSide", "counts": dict(c), "non_null_coverage": coverage, "left_right_patterns_enabled": reliable}
|
||||
write_json(exp / "visual-adapter/left-right-audit.json", result)
|
||||
return result
|
||||
|
||||
|
||||
def freeze_contracts(exp: Path, cfg: dict[str, Any], runtime: BrainRuntime, adapter: VisualAdapterV1) -> dict[str, Any]:
|
||||
input_schema = {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "MaleCNS InputBatch",
|
||||
"version": INPUT_BATCH_VERSION,
|
||||
"type": "object",
|
||||
"required": ["version", "batch_id", "events"],
|
||||
"properties": {
|
||||
"version": {"const": INPUT_BATCH_VERSION},
|
||||
"batch_id": {"type": "string"},
|
||||
"events": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["target_neuron_idx", "stimulation_mode", "value_kind", "value", "start_ms", "duration_ms", "source_id", "channel_id"],
|
||||
"properties": {
|
||||
"target_neuron_idx": {"type": "array", "items": {"type": "integer", "minimum": 0, "maximum": 166699}},
|
||||
"stimulation_mode": {"enum": ["spike_injection", "external_synaptic_drive"]},
|
||||
"value_kind": {"enum": ["binary", "scalar_intensity", "poisson_rate_hz", "signed_modulation"]},
|
||||
"value": {"oneOf": [{"type": "number"}, {"type": "array", "items": {"type": "number"}}]},
|
||||
"start_ms": {"type": "number", "minimum": 0},
|
||||
"duration_ms": {"type": "number", "exclusiveMinimum": 0},
|
||||
"source_id": {"type": "string"},
|
||||
"channel_id": {"type": "string"},
|
||||
"amplitude_mv": {"type": "number"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"timestamp_unit": "biological ms",
|
||||
"external_drive_is_connectome_transmission": False,
|
||||
}
|
||||
write_json(exp / "configs/input-batch.schema.json", input_schema)
|
||||
mode_records: dict[str, Any] = {}
|
||||
for mode, nodes in adapter.modes.items():
|
||||
path = exp / "visual-adapter" / f"{mode}.parquet"
|
||||
pq.write_table(pa.table({"neuron_idx": pa.array(nodes.astype(np.int32), type=pa.int32()), "body_id": pa.array(runtime.body_id[nodes], type=pa.uint64())}), path, compression="zstd")
|
||||
spec = cfg["visual_adapter"]["modes"][mode]
|
||||
mode_records[mode] = {"count": int(len(nodes)), "membership_file": str(path), **spec}
|
||||
write_json(exp / "visual-adapter/membership-summary.json", mode_records)
|
||||
readout = {
|
||||
"api_version": API_VERSION, "methods": ["read_spikes", "read_population", "read_output", "fixed_temporal_bins"],
|
||||
"features": ["spike count vector", "firing-rate vector", "active mask", "population aggregate rate", "excitatory/inhibitory activity", "descending feature vector", "motor-related feature vector"],
|
||||
"aggregations": ["per-neuron", "per-type", "per-class"], "fixed_bins_ms": cfg["readout"]["bins_ms"],
|
||||
"action_decoder": False,
|
||||
}
|
||||
write_json(exp / "readout/readout-contract.json", readout)
|
||||
return {"input_batch_schema": str(exp / "configs/input-batch.schema.json"), "visual_modes": mode_records, "readout_contract": readout}
|
||||
|
||||
|
||||
def patterns(nodes: np.ndarray, cfg: dict[str, Any]) -> dict[str, np.ndarray]:
|
||||
rng = np.random.default_rng(int(cfg["synthetic_patterns"]["deterministic_seed"]))
|
||||
order = rng.permutation(len(nodes))
|
||||
sparse_n = max(1, int(round(len(nodes) * float(cfg["synthetic_patterns"]["sparse_fraction"]))))
|
||||
dense_n = max(1, int(round(len(nodes) * float(cfg["synthetic_patterns"]["dense_fraction"]))))
|
||||
moving_n = max(1, int(round(len(nodes) * float(cfg["synthetic_patterns"]["moving_fraction"]))))
|
||||
def vector(selection: np.ndarray, rate: float) -> np.ndarray:
|
||||
out = np.zeros(len(nodes), dtype=np.float32); out[selection] = rate; return out
|
||||
rate = float(cfg["visual_adapter"]["synthetic_rate_hz"])
|
||||
return {
|
||||
"sparse-A": vector(order[:sparse_n], rate),
|
||||
"sparse-B": vector(order[sparse_n:2*sparse_n], rate),
|
||||
"dense": vector(order[:dense_n], rate),
|
||||
"moving": vector(order[2*sparse_n:2*sparse_n+moving_n], rate),
|
||||
"zero": np.zeros(len(nodes), dtype=np.float32),
|
||||
}
|
||||
|
||||
|
||||
def direct_batch(nodes: np.ndarray, mode: str, rate_hz: float, start_ms: float, duration_ms: float, source: str) -> InputBatch:
|
||||
return InputBatch(events=(InputEvent(
|
||||
target_neuron_idx=nodes.astype(np.int64), stimulation_mode="spike_injection",
|
||||
value_kind="poisson_rate_hz", value=float(rate_hz), start_ms=start_ms,
|
||||
duration_ms=duration_ms, source_id=source, channel_id=mode,
|
||||
metadata={"biological_fidelity": "reference_population", "external_drive_is_connectome_transmission": False},
|
||||
),), batch_id=f"{source}:{mode}:{start_ms}")
|
||||
|
||||
|
||||
def feature(runtime: BrainRuntime, window: float) -> np.ndarray:
|
||||
d = runtime.read_population("descending", window)["spike_counts"]
|
||||
m = runtime.read_population("motor_related", window)["spike_counts"]
|
||||
return np.concatenate([d, m]).astype(np.float32)
|
||||
|
||||
|
||||
def output_stats(runtime: BrainRuntime, window: float) -> dict[str, Any]:
|
||||
d = runtime.read_population("descending", window); m = runtime.read_population("motor_related", window)
|
||||
return {
|
||||
"descending_active": d["active_neurons"], "descending_spikes": d["total_spikes"],
|
||||
"motor_active": m["active_neurons"], "motor_spikes": m["total_spikes"],
|
||||
"descending_rate_hz": d["aggregate_rate_hz"], "motor_rate_hz": m["aggregate_rate_hz"],
|
||||
}
|
||||
|
||||
|
||||
def population_distances(runtime: BrainRuntime, sources: np.ndarray) -> np.ndarray:
|
||||
return runtime.runtime_module.p4b.directed_distances(runtime.indptr_np, runtime.indices_np, sources.astype(np.int64), runtime.n)
|
||||
|
||||
|
||||
def propagation_depth(runtime: BrainRuntime, distance: np.ndarray) -> int:
|
||||
active = runtime.spike_counts.cpu().numpy() > 0
|
||||
values = distance[active & (distance >= 0)]
|
||||
return int(values.max(initial=0))
|
||||
|
||||
|
||||
def make_source_batch(cfg: dict[str, Any], runtime: BrainRuntime, adapter: VisualAdapterV1,
|
||||
source: str, start: float, duration: float, pattern_cache: dict[str, dict[str, np.ndarray]]) -> tuple[InputBatch, np.ndarray]:
|
||||
if source.startswith("engineering-"):
|
||||
mode = source
|
||||
p = pattern_cache[mode]["sparse-A"]
|
||||
batch = adapter.encode(mode, p, start, duration, "phase4d-window", "sparse-A")
|
||||
targets = adapter.population(mode)[p != 0]
|
||||
return batch, targets
|
||||
mask = runtime.population_masks[source]
|
||||
nodes = np.flatnonzero(mask).astype(np.int64)
|
||||
rate = 10.0 if source == "auditory" else 50.0
|
||||
return direct_batch(nodes, source, rate, start, duration, "phase4d-window"), nodes
|
||||
|
||||
|
||||
def window_experiments(exp: Path, cfg: dict[str, Any], runtime: BrainRuntime,
|
||||
adapter: VisualAdapterV1, pattern_cache: dict[str, dict[str, np.ndarray]]) -> dict[str, Any]:
|
||||
records: list[dict[str, Any]] = []; distances: dict[str, np.ndarray] = {}
|
||||
for source in cfg["window_experiment"]["sources"]:
|
||||
_, targets = make_source_batch(cfg, runtime, adapter, source, 0.0, 5.0, pattern_cache)
|
||||
distances[source] = population_distances(runtime, targets)
|
||||
for window in cfg["window_experiment"]["windows_ms"]:
|
||||
for seed in cfg["window_experiment"]["seeds"]:
|
||||
outcomes = []
|
||||
for repeat in range(2):
|
||||
runtime.reset(int(seed)); batch, _ = make_source_batch(cfg, runtime, adapter, source, 0.0, float(window), pattern_cache)
|
||||
torch.cuda.synchronize(); started = time.perf_counter(); step_result = runtime.step(batch, float(window)); torch.cuda.synchronize(); wall = time.perf_counter() - started
|
||||
stats = output_stats(runtime, float(window)); f = feature(runtime, float(window))
|
||||
outcomes.append((runtime.read_spikes(), runtime.v.cpu().numpy().copy(), runtime.g.cpu().numpy().copy(), f, stats, step_result, wall))
|
||||
a, b = outcomes
|
||||
deterministic = np.array_equal(a[0]["step"], b[0]["step"]) and np.array_equal(a[0]["neuron_idx"], b[0]["neuron_idx"]) and np.array_equal(a[3], b[3])
|
||||
input_count = int(a[5]["new_spikes"] if source in ("auditory", "olfactory") else a[5]["new_external_drive_events"])
|
||||
rec = {
|
||||
"source": source, "window_ms": float(window), "seed": int(seed),
|
||||
"wall_latency_seconds": a[6], "max_theoretical_control_hz": 1.0 / a[6],
|
||||
"realtime_factor_wall_over_biological": a[6] / (float(window) / 1000.0),
|
||||
"input_spikes_or_drive_events": input_count,
|
||||
"total_spikes": int(len(a[0]["neuron_idx"])),
|
||||
"propagation_depth": propagation_depth(runtime, distances[source]),
|
||||
**a[4], "feature_l2_norm": float(np.linalg.norm(a[3])),
|
||||
"repeat_spike_events_exact": deterministic,
|
||||
"repeat_feature_exact": bool(np.array_equal(a[3], b[3])),
|
||||
"repeat_final_voltage_max_abs_error": float(np.max(np.abs(a[1] - b[1]))),
|
||||
"repeat_final_g_max_abs_error": float(np.max(np.abs(a[2] - b[2]))),
|
||||
}
|
||||
records.append(rec)
|
||||
write_json(exp / "latency/window-trials.json", records)
|
||||
summary: dict[str, Any] = {"trial_count": len(records), "all_repeat_deterministic": all(x["repeat_spike_events_exact"] for x in records)}
|
||||
for source in cfg["window_experiment"]["sources"]:
|
||||
rows = [x for x in records if x["source"] == source]
|
||||
summary[source] = {}
|
||||
for window in cfg["window_experiment"]["windows_ms"]:
|
||||
r = [x for x in rows if x["window_ms"] == float(window)]
|
||||
summary[source][str(window)] = {
|
||||
"wall_latency_seconds_mean": float(np.mean([x["wall_latency_seconds"] for x in r])),
|
||||
"max_theoretical_control_hz_mean": float(np.mean([x["max_theoretical_control_hz"] for x in r])),
|
||||
"descending_active_mean": float(np.mean([x["descending_active"] for x in r])),
|
||||
"motor_active_mean": float(np.mean([x["motor_active"] for x in r])),
|
||||
"feature_l2_norm_mean": float(np.mean([x["feature_l2_norm"] for x in r])),
|
||||
}
|
||||
def shortest(key: str) -> float | None:
|
||||
candidates = sorted({x["window_ms"] for x in records if x["source"].startswith("engineering-") and x[key] > 0})
|
||||
return candidates[0] if candidates else None
|
||||
summary["shortest_engineering_descending_signal_ms"] = shortest("descending_active")
|
||||
summary["shortest_engineering_motor_signal_ms"] = shortest("motor_active")
|
||||
write_json(exp / "latency/summary.json", summary)
|
||||
return {"records": records, "summary": summary}
|
||||
|
||||
|
||||
def continuous_control(exp: Path, cfg: dict[str, Any], runtime: BrainRuntime,
|
||||
adapter: VisualAdapterV1, pattern_cache: dict[str, dict[str, np.ndarray]]) -> dict[str, Any]:
|
||||
output: dict[str, Any] = {}
|
||||
duration = float(cfg["continuous_control"]["biological_duration_ms"])
|
||||
sequence = cfg["continuous_control"]["sequence"]
|
||||
mode = "engineering-first-order"
|
||||
for period in cfg["continuous_control"]["periods_ms"]:
|
||||
period = float(period); runtime.reset(int(cfg["continuous_control"]["seed"]))
|
||||
intervals: list[dict[str, Any]] = []; previous = None
|
||||
total_wall = 0.0
|
||||
for k, start in enumerate(np.arange(0.0, duration, period)):
|
||||
pattern_name = sequence[k % len(sequence)]
|
||||
values = pattern_cache[mode][pattern_name]
|
||||
batch = adapter.encode(mode, values, float(start), period, "continuous-control", pattern_name)
|
||||
torch.cuda.synchronize(); t0 = time.perf_counter(); step_result = runtime.step(batch, period); torch.cuda.synchronize(); wall = time.perf_counter() - t0; total_wall += wall
|
||||
f = feature(runtime, period); stats = output_stats(runtime, period)
|
||||
recent = runtime.read_spikes(period); adapter_nodes = adapter.population(mode)
|
||||
target_spikes = int(np.count_nonzero(np.isin(recent["neuron_idx"], adapter_nodes)))
|
||||
latency = None
|
||||
ev = runtime.read_spikes(period)
|
||||
output_members = np.concatenate([runtime.output_populations["descending"], runtime.output_populations["motor_related"]])
|
||||
omask = np.isin(ev["neuron_idx"], output_members)
|
||||
if np.any(omask): latency = float(ev["timestamp_ms"][omask].min() - start)
|
||||
intervals.append({
|
||||
"interval": k, "start_ms": float(start), "end_ms": float(start + period), "pattern": pattern_name,
|
||||
"wall_seconds": wall, "response_latency_ms": latency, **stats,
|
||||
"feature_norm": float(np.linalg.norm(f)),
|
||||
"feature_change_l2": None if previous is None else float(np.linalg.norm(f - previous)),
|
||||
"input_spikes": step_result["new_spikes"], "drive_events": step_result["new_external_drive_events"],
|
||||
"adapter_target_spikes": target_spikes,
|
||||
})
|
||||
previous = f
|
||||
zero = [x for x in intervals if x["pattern"] == "zero"]
|
||||
change_latencies = [x["response_latency_ms"] for x in intervals if x["response_latency_ms"] is not None]
|
||||
output[str(int(period))] = {
|
||||
"period_ms": period, "continuous_state_without_reset": True, "interval_count": len(intervals),
|
||||
"wall_seconds": total_wall, "biological_ms": duration,
|
||||
"median_response_latency_ms": float(np.median(change_latencies)) if change_latencies else None,
|
||||
"max_response_latency_ms": float(np.max(change_latencies)) if change_latencies else None,
|
||||
"zero_input_descending_spikes": int(sum(x["descending_spikes"] for x in zero)),
|
||||
"zero_input_motor_spikes": int(sum(x["motor_spikes"] for x in zero)),
|
||||
"zero_input_adapter_target_spikes": int(sum(x["adapter_target_spikes"] for x in zero)),
|
||||
"zero_input_intervals_with_activity": int(sum((x["descending_spikes"] + x["motor_spikes"]) > 0 for x in zero)),
|
||||
"intervals": intervals,
|
||||
}
|
||||
write_json(exp / "continuous-control" / f"period-{int(period)}ms.json", output[str(int(period))])
|
||||
write_json(exp / "continuous-control/summary.json", {k: {x: v[x] for x in v if x != "intervals"} for k, v in output.items()})
|
||||
return output
|
||||
|
||||
|
||||
def metric_pair(a: np.ndarray, b: np.ndarray) -> dict[str, float]:
|
||||
an, bn = float(np.linalg.norm(a)), float(np.linalg.norm(b))
|
||||
cosine = float(np.dot(a, b) / (an * bn)) if an and bn else (1.0 if an == bn else 0.0)
|
||||
if np.std(a) and np.std(b): corr = float(np.corrcoef(a, b)[0, 1])
|
||||
else: corr = 1.0 if np.array_equal(a, b) else 0.0
|
||||
aa, bb = a > 0, b > 0; union = np.count_nonzero(aa | bb)
|
||||
return {"cosine_distance": 1.0 - cosine, "euclidean_distance": float(np.linalg.norm(a - b)), "correlation": corr, "active_population_overlap_jaccard": float(np.count_nonzero(aa & bb) / union) if union else 1.0}
|
||||
|
||||
|
||||
def separability(exp: Path, cfg: dict[str, Any], runtime: BrainRuntime,
|
||||
adapter: VisualAdapterV1, pattern_cache: dict[str, dict[str, np.ndarray]], side_audit: dict[str, Any]) -> dict[str, Any]:
|
||||
mode = "engineering-first-order"; pats = dict(pattern_cache[mode])
|
||||
pats.pop("zero")
|
||||
if side_audit[mode]["left_right_patterns_enabled"]:
|
||||
root_side = runtime.table["annotation_rootSide"].to_pylist(); nodes = adapter.population(mode); rate = float(cfg["visual_adapter"]["synthetic_rate_hz"])
|
||||
pats["left"] = np.asarray([rate if root_side[i] == "L" else 0.0 for i in nodes], dtype=np.float32)
|
||||
pats["right"] = np.asarray([rate if root_side[i] == "R" else 0.0 for i in nodes], dtype=np.float32)
|
||||
features: dict[str, list[np.ndarray]] = {}
|
||||
trial_rows: list[dict[str, Any]] = []
|
||||
for name, values in pats.items():
|
||||
features[name] = []
|
||||
for seed in (44301, 44302, 44303):
|
||||
runtime.reset(seed); batch = adapter.encode(mode, values, 0.0, 100.0, "separability", name)
|
||||
runtime.step(batch, 100.0); f = feature(runtime, 100.0); features[name].append(f)
|
||||
trial_rows.append({"pattern": name, "seed": seed, "active_channels": int(np.count_nonzero(values)), "feature_norm": float(np.linalg.norm(f)), **output_stats(runtime, 100.0)})
|
||||
means = {k: np.mean(v, axis=0) for k, v in features.items()}
|
||||
pairwise: dict[str, Any] = {}
|
||||
names = sorted(means)
|
||||
for i, a in enumerate(names):
|
||||
for b in names[i+1:]: pairwise[f"{a}__{b}"] = metric_pair(means[a], means[b])
|
||||
within = [float(np.linalg.norm(x - means[k])) for k, xs in features.items() for x in xs]
|
||||
between = [v["euclidean_distance"] for v in pairwise.values()]
|
||||
matrix = np.stack([x for k in names for x in features[k]])
|
||||
centered = matrix - matrix.mean(axis=0, keepdims=True)
|
||||
u, s, _ = np.linalg.svd(centered, full_matrices=False)
|
||||
coords = u[:, :2] * s[:2]
|
||||
labels = [k for k in names for _ in features[k]]
|
||||
pca_table = pa.table({"pattern": labels, "pc1": coords[:, 0], "pc2": coords[:, 1]})
|
||||
pq.write_table(pca_table, exp / "separability/pca-coordinates.parquet", compression="zstd")
|
||||
result = {
|
||||
"classifier_trained": False, "action_decoder_created": False,
|
||||
"patterns": names, "trial_statistics": trial_rows, "pairwise": pairwise,
|
||||
"within_condition_distance_mean": float(np.mean(within)),
|
||||
"between_condition_distance_mean": float(np.mean(between)),
|
||||
"between_to_within_ratio": float(np.mean(between) / np.mean(within)) if np.mean(within) else None,
|
||||
"all_pairwise_mean_vectors_distinct": all(v["euclidean_distance"] > 0 for v in pairwise.values()),
|
||||
"pca_usage": "offline visualization analysis only; not an action decoder",
|
||||
}
|
||||
write_json(exp / "separability/summary.json", result)
|
||||
return result
|
||||
|
||||
|
||||
def api_tests(exp: Path, runtime: BrainRuntime) -> dict[str, Any]:
|
||||
nodes = np.flatnonzero(runtime.population_masks["auditory"]).astype(np.int64)
|
||||
batch20 = direct_batch(nodes, "auditory", 10, 0.0, 20.0, "api-test")
|
||||
runtime.reset(44400); runtime.step(batch20, 20.0); one = runtime.read_spikes(); one_v = runtime.v.cpu().numpy().copy()
|
||||
runtime.reset(44400); runtime.step(batch20, 10.0); checkpoint = runtime.checkpoint_state(); runtime.step(batch20, 10.0); two = runtime.read_spikes(); two_v = runtime.v.cpu().numpy().copy()
|
||||
runtime.restore_state(checkpoint); runtime.step(batch20, 10.0); replay = runtime.read_spikes(); replay_v = runtime.v.cpu().numpy().copy()
|
||||
per_neuron = runtime.read_output({"population": "descending", "aggregation": "per-neuron"}, 20.0)
|
||||
per_type = runtime.read_output({"population": "descending", "aggregation": "per-type"}, 20.0)
|
||||
per_class = runtime.read_output({"population": "motor_related", "aggregation": "per-class"}, 20.0)
|
||||
bins = {str(x): runtime.fixed_temporal_bins("descending", float(x), 20.0)["spike_counts"].tolist() for x in runtime.cfg["readout"]["bins_ms"] if float(x) <= 20.0}
|
||||
result = {
|
||||
"reset_same_seed_continuous_chunk_spikes_exact": bool(np.array_equal(one["step"], two["step"]) and np.array_equal(one["neuron_idx"], two["neuron_idx"])),
|
||||
"continuous_step_does_not_reset_clock": runtime.simulation_time_ms == 20.0,
|
||||
"checkpoint_restore_spikes_exact": bool(np.array_equal(two["step"], replay["step"]) and np.array_equal(two["neuron_idx"], replay["neuron_idx"])),
|
||||
"checkpoint_restore_voltage_exact": bool(np.array_equal(two_v, replay_v)),
|
||||
"single_vs_chunk_voltage_max_abs_error": float(np.max(np.abs(one_v - two_v))),
|
||||
"timestamps_present": "timestamp_ms" in replay,
|
||||
"per_neuron_readout_shape": int(len(per_neuron["neuron_idx"])),
|
||||
"per_type_aggregation_labels": int(len(per_type["features"])),
|
||||
"per_class_aggregation_labels": int(len(per_class["features"])),
|
||||
"fixed_temporal_bins_tested": bins,
|
||||
"readout_modes_passed": len(per_neuron["neuron_idx"]) == len(runtime.output_populations["descending"]) and len(per_type["features"]) > 0 and len(per_class["features"]) > 0,
|
||||
"all_passed": False,
|
||||
}
|
||||
result["all_passed"] = all(result[k] for k in ("reset_same_seed_continuous_chunk_spikes_exact", "continuous_step_does_not_reset_clock", "checkpoint_restore_spikes_exact", "checkpoint_restore_voltage_exact", "timestamps_present", "readout_modes_passed"))
|
||||
write_json(exp / "audit/api-tests.json", result)
|
||||
return result
|
||||
|
||||
|
||||
def regression(exp: Path, cfg: dict[str, Any], runtime: BrainRuntime) -> dict[str, Any]:
|
||||
root = Path(cfg["paths"]["phase4c_root"]) / "regression/reference"
|
||||
results: dict[str, Any] = {}; tolerance = float(cfg["regression"]["float_absolute_tolerance"])
|
||||
for spec in cfg["regression"]["reference_trials"]:
|
||||
name = spec["name"]; ref_dir = root / name
|
||||
events = pq.read_table(ref_dir / "spike-events.parquet", columns=["step", "neuron_idx"])
|
||||
ref_steps = events["step"].to_numpy(); ref_nodes = events["neuron_idx"].to_numpy()
|
||||
ref_counts = np.load(ref_dir / "per-neuron-spike-counts.int32.npy")
|
||||
ref_v = np.load(ref_dir / "final-voltage.float32.npy"); ref_g = np.load(ref_dir / "final-g.float32.npy")
|
||||
nodes = np.flatnonzero(runtime.population_masks[spec["population"]]).astype(np.int64)
|
||||
batch = direct_batch(nodes, spec["population"], float(spec["rate_hz"]), 0.0, float(spec["duration_ms"]), "phase4c-regression")
|
||||
runtime.reset(int(spec["seed"])); t0 = time.perf_counter(); runtime.step(batch, float(spec["duration_ms"])); wall = time.perf_counter() - t0
|
||||
got = runtime.read_spikes(); counts = runtime.spike_counts.cpu().numpy()
|
||||
vd = np.abs(runtime.v.cpu().numpy() - ref_v); gd = np.abs(runtime.g.cpu().numpy() - ref_g)
|
||||
record = {
|
||||
"spike_sequence_exact": bool(np.array_equal(ref_steps, got["step"]) and np.array_equal(ref_nodes, got["neuron_idx"])),
|
||||
"spike_counts_exact": bool(np.array_equal(ref_counts, counts)),
|
||||
"final_voltage_max_abs_error": float(vd.max(initial=0)), "final_voltage_mean_abs_error": float(vd.mean()),
|
||||
"final_g_max_abs_error": float(gd.max(initial=0)), "final_g_mean_abs_error": float(gd.mean()),
|
||||
"state_within_phase4c_tolerance": bool(vd.max(initial=0) <= tolerance and gd.max(initial=0) <= tolerance),
|
||||
"wall_seconds": wall, "event_count": int(len(got["neuron_idx"])),
|
||||
}
|
||||
record["passed"] = record["spike_sequence_exact"] and record["spike_counts_exact"] and record["state_within_phase4c_tolerance"]
|
||||
results[name] = record
|
||||
write_json(exp / "regression" / f"{name}.json", record)
|
||||
write_json(exp / "regression/summary.json", {"all_passed": all(x["passed"] for x in results.values()), "trials": results})
|
||||
return results
|
||||
|
||||
|
||||
def stream_contract(exp: Path, runtime: BrainRuntime) -> dict[str, Any]:
|
||||
schema = {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema", "title": "MaleCNS Activity Packet",
|
||||
"contract_version": ACTIVITY_STREAM_VERSION, "type": "object",
|
||||
"required": ["contract_version", "simulation_time_ms", "sequence_id", "spike_neuron_idx", "population_aggregates", "active_neuron_count", "input_channel_metadata"],
|
||||
"properties": {
|
||||
"simulation_time_ms": {"type": "number"}, "sequence_id": {"type": "integer"},
|
||||
"spike_neuron_idx": {"type": "array", "items": {"type": "integer"}},
|
||||
"spike_timestamp_ms": {"type": "array", "items": {"type": "number"}},
|
||||
"population_aggregates": {"type": "object"}, "active_neuron_count": {"type": "integer"},
|
||||
"input_channel_metadata": {"type": "object"}, "representation": {"const": "event-only"},
|
||||
},
|
||||
"dense_time_by_neuron_matrix_forbidden": True,
|
||||
"supported_modes": ["event-only stream", "temporal aggregation", "saved-event replay"],
|
||||
}
|
||||
write_json(exp / "activity-stream/activity-packet.schema.json", schema)
|
||||
packet = runtime.activity_packet(20.0, {"source": "contract-test"})
|
||||
path = exp / "activity-stream/replay.jsonl"
|
||||
path.write_text(json.dumps(packet, default=np_json) + "\n", encoding="utf-8")
|
||||
replay = json.loads(path.read_text().splitlines()[0])
|
||||
result = {"contract_frozen": True, "version": ACTIVITY_STREAM_VERSION, "event_only": True, "replay_exact": replay == json.loads(json.dumps(packet, default=np_json)), "schema": str(exp / "activity-stream/activity-packet.schema.json")}
|
||||
write_json(exp / "activity-stream/replay-test.json", result)
|
||||
return result
|
||||
|
||||
|
||||
def freeze_io(exp: Path, cfg: dict[str, Any], code_dir: Path, api_tests_result: dict[str, Any]) -> dict[str, Any]:
|
||||
root = Path(cfg["paths"]["model_root"]) / "io-v1"; root.mkdir(parents=True, exist_ok=True)
|
||||
for name in ("VERSION.json", "MANIFEST.json", "checksums.sha256"):
|
||||
path = root / name
|
||||
if path.exists(): path.chmod(0o644)
|
||||
sources = [code_dir / "brain_runtime.py", code_dir / "phase4d.py", code_dir / "phase4d.yaml"]
|
||||
for src in sources:
|
||||
dst = root / src.name
|
||||
if dst.exists(): dst.chmod(0o644)
|
||||
shutil.copy2(src, dst); dst.chmod(0o444)
|
||||
version = {
|
||||
"name": cfg["names"]["io_api"], "api_version": API_VERSION,
|
||||
"input_batch_version": INPUT_BATCH_VERSION, "activity_stream_version": ACTIVITY_STREAM_VERSION,
|
||||
"runtime_dependency": cfg["names"]["runtime"], "game_agnostic": True,
|
||||
"api_methods": ["reset", "step", "read_spikes", "read_population", "read_output", "get_state_summary", "checkpoint_state", "restore_state"],
|
||||
"continuous_gpu_state": True, "api_tests": api_tests_result,
|
||||
"prohibitions": cfg["prohibitions"],
|
||||
}
|
||||
write_json(root / "VERSION.json", version); (root / "VERSION.json").chmod(0o444)
|
||||
files = sorted(p for p in root.iterdir() if p.is_file() and p.name not in ("MANIFEST.json", "checksums.sha256"))
|
||||
manifest = {"artifact": "io-v1", "files": [{"path": p.name, "bytes": p.stat().st_size, "sha256": sha256(p)} for p in files]}
|
||||
write_json(root / "MANIFEST.json", manifest); (root / "MANIFEST.json").chmod(0o444)
|
||||
targets = files + [root / "MANIFEST.json"]
|
||||
(root / "checksums.sha256").write_text("".join(f"{sha256(p)} {p.name}\n" for p in targets), encoding="utf-8"); (root / "checksums.sha256").chmod(0o444)
|
||||
return version
|
||||
|
||||
|
||||
def seal(exp: Path, cfg: dict[str, Any], code: list[Path], input_files: list[Path], summary: dict[str, Any]) -> None:
|
||||
controls = {exp / "manifests/manifest.json", exp / "manifests/checksums.sha256"}
|
||||
outputs = sorted(p for p in exp.rglob("*") if p.is_file() and p not in controls)
|
||||
manifest = {
|
||||
"phase": "4D", "status": summary["status"], "created_unix_time": time.time(),
|
||||
"software": {"python": platform.python_version(), "torch": torch.__version__, "numpy": np.__version__, "pyarrow": pa.__version__},
|
||||
"code": [{"path": str(p), "sha256": sha256(p)} for p in code],
|
||||
"immutable_inputs": [{"path": str(p), "sha256": sha256(p)} for p in input_files],
|
||||
"outputs": [{"path": str(p), "bytes": p.stat().st_size, "sha256": sha256(p)} for p in outputs],
|
||||
"prohibitions": cfg["prohibitions"],
|
||||
}
|
||||
write_json(exp / "manifests/manifest.json", manifest); targets = outputs + [exp / "manifests/manifest.json"]
|
||||
(exp / "manifests/checksums.sha256").write_text("".join(f"{sha256(p)} {p.relative_to(exp)}\n" for p in targets), encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", required=True, type=Path); args = ap.parse_args()
|
||||
cfg = yaml.safe_load(args.config.read_text(encoding="utf-8")); exp = Path(cfg["paths"]["experiment_root"])
|
||||
for sub in ("configs", "populations", "visual-adapter", "readout", "latency", "continuous-control", "separability", "activity-stream", "regression", "audit", "compiler-reports/extension-build", "manifests"):
|
||||
(exp / sub).mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(args.config, exp / "configs/phase4d.yaml")
|
||||
runtime_root = Path(cfg["paths"]["runtime_root"]); phase4c = Path(cfg["paths"]["phase4c_root"])
|
||||
immutable = [runtime_root / "VERSION.json", runtime_root / "checksums.sha256", phase4c / "manifests/checksums.sha256", phase4c / "visual-audit/summary.json"]
|
||||
before = {str(p): sha256(p) for p in immutable}
|
||||
runtime = BrainRuntime(args.config, exp / "audit/extension-build")
|
||||
adapter = VisualAdapterV1(cfg, phase4c)
|
||||
population_summary = freeze_populations(exp, runtime)
|
||||
contracts = freeze_contracts(exp, cfg, runtime, adapter)
|
||||
side = audit_visual_sides(exp, runtime, adapter)
|
||||
pattern_cache = {mode: patterns(adapter.population(mode), cfg) for mode in ("engineering-first-order", "engineering-second-order")}
|
||||
pattern_summary = {mode: {name: int(np.count_nonzero(v)) for name, v in pats.items()} for mode, pats in pattern_cache.items()}
|
||||
write_json(exp / "visual-adapter/synthetic-patterns.json", pattern_summary)
|
||||
write_json(exp / "visual-adapter/modes.json", cfg["visual_adapter"])
|
||||
api = api_tests(exp, runtime)
|
||||
windows = window_experiments(exp, cfg, runtime, adapter, pattern_cache)
|
||||
continuous = continuous_control(exp, cfg, runtime, adapter, pattern_cache)
|
||||
sep = separability(exp, cfg, runtime, adapter, pattern_cache, side)
|
||||
reg = regression(exp, cfg, runtime)
|
||||
stream = stream_contract(exp, runtime)
|
||||
after = {str(p): sha256(p) for p in immutable}
|
||||
if before != after: raise RuntimeError("Phase 4C/runtime-v1 immutable input changed")
|
||||
reg_pass = all(x["passed"] for x in reg.values())
|
||||
status = "PASS" if api["all_passed"] and reg_pass and stream["contract_frozen"] else "FAILED"
|
||||
summary = {
|
||||
"status": status, "brain_runtime_api_frozen": api["all_passed"], "api_tests": api,
|
||||
"visual_adapter_modes_complete": True, "output_populations": population_summary,
|
||||
"contracts": contracts,
|
||||
"window_experiments": windows["summary"],
|
||||
"continuous_control": {k: {x: v[x] for x in v if x != "intervals"} for k, v in continuous.items()},
|
||||
"separability": sep, "activity_stream": stream,
|
||||
"phase4c_regression_all_passed": reg_pass, "phase4c_regression": reg,
|
||||
"immutable_inputs_unchanged": True, "left_right_audit": side,
|
||||
"no_stable_retro_rom_buttons_training_or_web": True,
|
||||
}
|
||||
write_json(exp / "audit/summary.json", summary)
|
||||
code_dir = Path(__file__).resolve().parent
|
||||
io_version = freeze_io(exp, cfg, code_dir, api); summary["io_version"] = io_version
|
||||
write_json(exp / "audit/summary.json", summary)
|
||||
seal(exp, cfg, [code_dir / "brain_runtime.py", Path(__file__).resolve(), args.config.resolve()], immutable, summary)
|
||||
if status != "PASS": raise RuntimeError("Phase 4D validation failed; see audit/summary.json")
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,102 @@
|
||||
phase: 4D
|
||||
names:
|
||||
runtime: MaleCNS-LIF-runtime-v1
|
||||
io_api: MaleCNS-BrainIO-v1
|
||||
visual_adapter: MaleCNS-VisualAdapter-v1
|
||||
activity_stream: MaleCNS-ActivityStream-v1
|
||||
|
||||
paths:
|
||||
connectome_root: /data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif
|
||||
runtime_root: /data/digital-drosophila/models/male-cns-lif/runtime-v1
|
||||
phase4c_root: /data/digital-drosophila/experiments/phase4c-runtime
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4d-io
|
||||
|
||||
device:
|
||||
required_name_substring: RTX 4090
|
||||
launcher_visible_gpu_uuid: GPU-79f3a85d-adce-7833-7003-6488140ca596
|
||||
|
||||
model:
|
||||
dt_ms: 0.1
|
||||
membrane_tau_ms: 20.0
|
||||
synaptic_tau_ms: 5.0
|
||||
refractory_ms: 2.2
|
||||
resting_potential_mv: -52.0
|
||||
reset_potential_mv: -52.0
|
||||
threshold_mv: -45.0
|
||||
synaptic_delay_ms: 1.8
|
||||
w_syn_mv_per_anatomical_synapse: 0.275
|
||||
|
||||
sign_policy:
|
||||
neurotransmitter_sign:
|
||||
acetylcholine: 1
|
||||
gaba: -1
|
||||
glutamate: -1
|
||||
histamine: -1
|
||||
dopamine: 1
|
||||
octopamine: 1
|
||||
serotonin: 1
|
||||
unknown_sign: 0
|
||||
unclear_sign: 0
|
||||
|
||||
input_batch:
|
||||
version: 1.0.0
|
||||
allowed_modes: [spike_injection, external_synaptic_drive]
|
||||
refractory_respecting_spike_injection: true
|
||||
external_drive_application: add to g after decay and delayed-connectome arrival, before membrane integration
|
||||
|
||||
visual_adapter:
|
||||
version: 1.0.0
|
||||
modes:
|
||||
reference-photoreceptor:
|
||||
hierarchy_population: visual_sensory
|
||||
input_mode: spike_injection
|
||||
biological_fidelity: reference_comparison_only
|
||||
engineering-first-order:
|
||||
hierarchy_population: first_order
|
||||
input_mode: external_synaptic_drive
|
||||
biological_fidelity: engineering_adapter
|
||||
engineering-second-order:
|
||||
hierarchy_population: second_order
|
||||
input_mode: external_synaptic_drive
|
||||
biological_fidelity: engineering_adapter
|
||||
synthetic_rate_hz: 50.0
|
||||
external_drive_mv: 8.0
|
||||
|
||||
readout:
|
||||
bins_ms: [1, 5, 10, 20, 50]
|
||||
output_populations: [descending, motor_related, ascending]
|
||||
|
||||
window_experiment:
|
||||
windows_ms: [5, 10, 20, 30, 50, 100]
|
||||
sources: [engineering-first-order, engineering-second-order, auditory, olfactory]
|
||||
seeds: [44001, 44002]
|
||||
|
||||
continuous_control:
|
||||
biological_duration_ms: 1000.0
|
||||
periods_ms: [10, 20, 50, 100]
|
||||
sequence: [sparse-A, sparse-B, dense, zero, sparse-A]
|
||||
seed: 44100
|
||||
|
||||
synthetic_patterns:
|
||||
sparse_fraction: 0.05
|
||||
dense_fraction: 0.25
|
||||
moving_fraction: 0.05
|
||||
deterministic_seed: 44200
|
||||
|
||||
regression:
|
||||
float_absolute_tolerance: 0.00390625
|
||||
reference_trials:
|
||||
- {name: auditory-10hz, population: auditory, rate_hz: 10, seed: 42001, duration_ms: 1000.0}
|
||||
- {name: auditory-100hz, population: auditory, rate_hz: 100, seed: 42001, duration_ms: 1000.0}
|
||||
- {name: olfactory-50hz, population: olfactory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
- {name: mechanosensory-50hz, population: mechanosensory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
- {name: gustatory-50hz, population: gustatory, rate_hz: 50, seed: 42003, duration_ms: 1000.0}
|
||||
|
||||
prohibitions:
|
||||
stable_retro: false
|
||||
rom_access: false
|
||||
controller_buttons: false
|
||||
training: false
|
||||
web_server: false
|
||||
connectome_or_sign_changes: false
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"file_count": 3,
|
||||
"files": [
|
||||
{
|
||||
"bytes": 489,
|
||||
"path": "VERSION.json",
|
||||
"sha256": "7e413f5112dab08dd869b69a1c58537f6507b075275d7a6bb4255495f4c105ef"
|
||||
},
|
||||
{
|
||||
"bytes": 49126,
|
||||
"path": "phase4k.py",
|
||||
"sha256": "6ca7dd7b3fec7f970f8a43ac8286dd0f05b2674ac3996b9b20583b636719abe7"
|
||||
},
|
||||
{
|
||||
"bytes": 3690,
|
||||
"path": "phase4k.yaml",
|
||||
"sha256": "3721440cba03036552c358c1a9d3320637e5914da901f74e86189aa297192fd1"
|
||||
}
|
||||
],
|
||||
"generated_utc": "2026-09-20T20:07:05.654584+00:00",
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/kc-recruitment-audit-v1",
|
||||
"total_bytes": 53305
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"diagnostic_only": true,
|
||||
"eligibility_v2_modified": false,
|
||||
"labels": {
|
||||
"KC_recruitment": "KC-RECRUITMENT-SOURCE-IDENTIFIED",
|
||||
"MBON_readout_insensitive": false,
|
||||
"odor_input": "ODOR-INPUT-NOT-GLOMERULUS-GROUNDED",
|
||||
"plasticity_transfer": "FAIL-PLASTICITY-TRANSFER",
|
||||
"whole_brain_recruitment_contamination": false
|
||||
},
|
||||
"name": "MaleCNS-KC-recruitment-MBON-causal-audit-v1",
|
||||
"summary_sha256": "0aab0f51ef73e5276d215d35833868e02008336e72166baf9f67496d6a90a825"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
5fbae0b970ac26c506f27d06d01b2e716ce45b1cb023439372151ccc3016ac02 MANIFEST.json
|
||||
7e413f5112dab08dd869b69a1c58537f6507b075275d7a6bb4255495f4c105ef VERSION.json
|
||||
6ca7dd7b3fec7f970f8a43ac8286dd0f05b2674ac3996b9b20583b636719abe7 phase4k.py
|
||||
3721440cba03036552c358c1a9d3320637e5914da901f74e86189aa297192fd1 phase4k.yaml
|
||||
@@ -0,0 +1,609 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4K: read-only KC recruitment and MBON causal-input audit."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import json
|
||||
import math
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
from phase4h_audit import sha256, tree_manifest, verify_checksum, write_json
|
||||
from phase4i import Validator, phase4c_regression
|
||||
from phase4j import APLSimulator, verify_frozen
|
||||
|
||||
|
||||
KC_CATEGORIES = ("PN_olfactory", "KC", "APL", "MBON", "DAN", "other")
|
||||
MBON_CATEGORIES = ("plastic_KC", "nonplastic_KC", "DAN", "MBON", "feedback_neuron", "other")
|
||||
|
||||
|
||||
def seal(root: Path, manifest: Path, checksums: Path) -> None:
|
||||
write_json(manifest, tree_manifest(root, {manifest, checksums}))
|
||||
targets = sorted(x for x in root.rglob("*") if x.is_file() and x != checksums)
|
||||
checksums.write_text("".join(f"{sha256(x)} {x.relative_to(root)}\n" for x in targets), encoding="utf-8")
|
||||
|
||||
|
||||
def first_latency(first_steps: np.ndarray, members: np.ndarray, dt: float) -> float | None:
|
||||
values = first_steps[members]
|
||||
return None if np.all(values < 0) else float(values[values >= 0].min() * dt)
|
||||
|
||||
|
||||
def response(run: dict[str, Any], v: Validator) -> dict[str, Any]:
|
||||
return {
|
||||
"MBON_spikes": int(run["counts"][v.mbon].sum()),
|
||||
"MBON_first_latency_ms": first_latency(run["first_steps"], v.mbon, run["dt_ms"]),
|
||||
"downstream_spikes": int(run["counts"][v.downstream].sum()),
|
||||
"downstream_first_latency_ms": first_latency(run["first_steps"], v.downstream, run["dt_ms"]),
|
||||
"MBON_final_g": run["final_g_mbon"],
|
||||
"MBON_final_v": run["final_v_mbon"],
|
||||
}
|
||||
|
||||
|
||||
def compare(pre: dict[str, Any], post: dict[str, Any]) -> dict[str, Any]:
|
||||
def delta_latency(key: str) -> float | None:
|
||||
a, b = pre[key], post[key]
|
||||
return None if a is None or b is None else float(b - a)
|
||||
return {
|
||||
"MBON_delta_g_max_abs": float(np.max(np.abs(post["MBON_final_g"] - pre["MBON_final_g"]))),
|
||||
"MBON_delta_v_max_abs": float(np.max(np.abs(post["MBON_final_v"] - pre["MBON_final_v"]))),
|
||||
"MBON_spike_count_delta": int(post["MBON_spikes"] - pre["MBON_spikes"]),
|
||||
"MBON_spike_l1": int(abs(post["MBON_spikes"] - pre["MBON_spikes"])),
|
||||
"MBON_first_latency_delta_ms": delta_latency("MBON_first_latency_ms"),
|
||||
"downstream_spike_count_delta": int(post["downstream_spikes"] - pre["downstream_spikes"]),
|
||||
"downstream_first_latency_delta_ms": delta_latency("downstream_first_latency_ms"),
|
||||
}
|
||||
|
||||
|
||||
class AuditSimulator(APLSimulator):
|
||||
"""Frozen LIF runtime with side-channel drive and event-causality accounting."""
|
||||
|
||||
def __init__(self, v: Validator, cfg: dict[str, Any], exp: Path):
|
||||
super().__init__(v, cfg, exp)
|
||||
self.base_np = v.base.detach().cpu().numpy().copy()
|
||||
self.indices_np = v.rt.indices_np
|
||||
self.raw_np = v.rt.raw_np
|
||||
self.edge_sources = self.sources
|
||||
self.dan = np.unique(v.confirmed["source_neuron_idx"].to_numpy(np.int32))
|
||||
|
||||
pn_olf = (v.cls == "ALPN") | (v.cls == "olfactory")
|
||||
node_code = np.full(v.n, KC_CATEGORIES.index("other"), np.int8)
|
||||
node_code[pn_olf] = KC_CATEGORIES.index("PN_olfactory")
|
||||
node_code[v.kc] = KC_CATEGORIES.index("KC")
|
||||
node_code[self.apl] = KC_CATEGORIES.index("APL")
|
||||
node_code[v.mbon] = KC_CATEGORIES.index("MBON")
|
||||
node_code[self.dan] = KC_CATEGORIES.index("DAN")
|
||||
self.node_kc_code = node_code
|
||||
self.edge_kc_code_gpu = torch.as_tensor(node_code[self.edge_sources], dtype=torch.int8, device=self.device)
|
||||
|
||||
target_is_kc = np.zeros(v.n, bool); target_is_kc[v.kc] = True
|
||||
feedback = np.unique(self.edge_sources[target_is_kc[self.indices_np]])
|
||||
is_candidate = np.zeros(len(self.indices_np), bool); is_candidate[v.edge_ids] = True
|
||||
edge_mbon_code = np.full(len(self.indices_np), MBON_CATEGORIES.index("other"), np.int8)
|
||||
src_kc = v.cls[self.edge_sources] == "Kenyon_Cell"
|
||||
edge_mbon_code[src_kc & is_candidate] = MBON_CATEGORIES.index("plastic_KC")
|
||||
edge_mbon_code[src_kc & ~is_candidate] = MBON_CATEGORIES.index("nonplastic_KC")
|
||||
edge_mbon_code[np.isin(self.edge_sources, self.dan)] = MBON_CATEGORIES.index("DAN")
|
||||
edge_mbon_code[np.isin(self.edge_sources, v.mbon)] = MBON_CATEGORIES.index("MBON")
|
||||
structurally_feedback = np.isin(self.edge_sources, feedback) & ~src_kc & ~np.isin(self.edge_sources, np.concatenate([self.dan, v.mbon]))
|
||||
edge_mbon_code[structurally_feedback] = MBON_CATEGORIES.index("feedback_neuron")
|
||||
self.edge_mbon_code_gpu = torch.as_tensor(edge_mbon_code, dtype=torch.int8, device=self.device)
|
||||
|
||||
kc_lookup = np.full(v.n, -1, np.int32); kc_lookup[v.kc] = np.arange(len(v.kc), dtype=np.int32)
|
||||
mbon_lookup = np.full(v.n, -1, np.int32); mbon_lookup[v.mbon] = np.arange(len(v.mbon), dtype=np.int32)
|
||||
self.kc_lookup_gpu = torch.as_tensor(kc_lookup, dtype=torch.int32, device=self.device)
|
||||
self.mbon_lookup_gpu = torch.as_tensor(mbon_lookup, dtype=torch.int32, device=self.device)
|
||||
self.mbon_gpu = torch.as_tensor(v.mbon, dtype=torch.int64, device=self.device)
|
||||
|
||||
def population_label(self, neuron: int) -> str:
|
||||
return KC_CATEGORIES[int(self.node_kc_code[int(neuron)])]
|
||||
|
||||
@staticmethod
|
||||
def _dominant_arrivals(
|
||||
item: tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None,
|
||||
score: np.ndarray, parent: np.ndarray, edge: np.ndarray, arrival: np.ndarray,
|
||||
) -> None:
|
||||
if item is None or not len(item[0]):
|
||||
return
|
||||
targets, values, parents, edges, arrivals = item
|
||||
order = np.lexsort((-values, targets))
|
||||
st = targets[order]
|
||||
take = np.r_[True, st[1:] != st[:-1]]
|
||||
chosen = order[take]; nodes = targets[chosen]; better = values[chosen] > score[nodes]
|
||||
nodes = nodes[better]; chosen = chosen[better]
|
||||
score[nodes] = values[chosen]; parent[nodes] = parents[chosen]
|
||||
edge[nodes] = edges[chosen]; arrival[nodes] = arrivals[chosen]
|
||||
|
||||
def run_audit(
|
||||
self, targets: np.ndarray, seed: int, mode: str, duration_ms: float,
|
||||
delta: np.ndarray | None = None, rate_hz: float | None = None, pulses: list[float] | None = None,
|
||||
dan_targets: np.ndarray | None = None, dan_pulses: list[float] | None = None,
|
||||
causal: bool = False, collect_kc_spikes: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
if delta is None:
|
||||
delta = np.zeros(len(self.v.edge_ids), np.float32)
|
||||
self.v.install(delta)
|
||||
if mode in {"graded", "disabled"}:
|
||||
self.rt.weights[self.apl_edge_gpu] = 0.0
|
||||
self.rt.reset(int(seed)); steps = int(round(float(duration_ms) / self.dt))
|
||||
target_gpu = torch.as_tensor(targets, dtype=torch.int64, device=self.device)
|
||||
forced = self._forced(steps, targets, seed, rate_hz, pulses)
|
||||
dan = np.empty(0, np.int32) if dan_targets is None else np.asarray(dan_targets, np.int32)
|
||||
dan_gpu = torch.as_tensor(dan, dtype=torch.int64, device=self.device)
|
||||
dan_forced = self._forced(steps, dan, seed + 991, None, dan_pulses)
|
||||
|
||||
qlen = self.rt.queue_len; nk, nm = len(self.v.kc), len(self.v.mbon)
|
||||
kq = torch.zeros((qlen, len(KC_CATEGORIES), nk), dtype=torch.float32, device=self.device)
|
||||
mq = torch.zeros((qlen, len(MBON_CATEGORIES), nm), dtype=torch.float32, device=self.device)
|
||||
mpq = torch.zeros_like(mq); mdq = torch.zeros((qlen, nm), dtype=torch.float32, device=self.device)
|
||||
kg = torch.zeros((len(KC_CATEGORIES), nk), dtype=torch.float32, device=self.device)
|
||||
mg = torch.zeros((len(MBON_CATEGORIES), nm), dtype=torch.float32, device=self.device)
|
||||
mdg = torch.zeros(nm, dtype=torch.float32, device=self.device)
|
||||
kr = torch.zeros_like(kg); mr = torch.zeros_like(mg); mrp = torch.zeros_like(mg); mdr = torch.zeros(nm, dtype=torch.float32, device=self.device)
|
||||
kt = torch.zeros((steps, len(KC_CATEGORIES)), dtype=torch.float32, device=self.device)
|
||||
mt = torch.zeros((steps, len(MBON_CATEGORIES)), dtype=torch.float32, device=self.device)
|
||||
counts = torch.zeros(self.v.n, dtype=torch.int32, device=self.device)
|
||||
first = torch.full((self.v.n,), -1, dtype=torch.int32, device=self.device)
|
||||
kc_apl_first = torch.zeros(nk, dtype=torch.float32, device=self.device)
|
||||
|
||||
params = self.cfg["apl_overlay"]
|
||||
apl_state = torch.zeros(len(self.apl), dtype=torch.float32, device=self.device)
|
||||
apl_decay = math.exp(-self.dt / float(params["tau_ms"]))
|
||||
alpha, gd = self.rt.alpha, self.rt.g_decay
|
||||
rest, reset, threshold, refsteps = self.rt.rest, self.rt.reset_value, self.rt.threshold, self.rt.refractory_steps
|
||||
delta_by_edge = torch.zeros(len(self.indices_np), dtype=torch.float32, device=self.device)
|
||||
delta_by_edge[torch.as_tensor(self.v.edge_ids, dtype=torch.int64, device=self.device)] = torch.as_tensor(delta, dtype=torch.float32, device=self.device)
|
||||
|
||||
event_rows: list[dict[str, Any]] = []; event_parent: list[int] = []; event_depth: list[int] = []
|
||||
causal_score = np.zeros(self.v.n, np.float32); causal_parent = np.full(self.v.n, -1, np.int64)
|
||||
causal_edge = np.full(self.v.n, -1, np.int64); causal_arrival = np.full(self.v.n, -1, np.int32)
|
||||
causal_ring: list[tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None] = [None] * qlen
|
||||
current_event = np.full(self.v.n, -1, np.int64)
|
||||
kc_spike_steps: list[np.ndarray] = []; kc_spike_nodes: list[np.ndarray] = []
|
||||
kc_local_lookup = np.full(self.v.n, -1, np.int32); kc_local_lookup[self.v.kc] = np.arange(nk, dtype=np.int32)
|
||||
forced_set = set(map(int, targets)); dan_set = set(map(int, dan))
|
||||
|
||||
torch.cuda.synchronize(); t0 = time.perf_counter()
|
||||
for step in range(steps):
|
||||
slot = step % qlen
|
||||
self.rt.g.mul_(gd).add_(self.rt.delay_queue[slot]); self.rt.delay_queue[slot].zero_()
|
||||
ka = kq[slot]; ma = mq[slot]; mpa = mpq[slot]; mda = mdq[slot]
|
||||
kg.mul_(gd).add_(ka); mg.mul_(gd).add_(ma); mdg.mul_(gd).add_(mda)
|
||||
kr.add_(ka); mr.add_(ma); mrp.add_(mpa); mdr.add_(mda)
|
||||
kt[step] = ka.sum(dim=1); mt[step] = ma.sum(dim=1)
|
||||
ka.zero_(); ma.zero_(); mpa.zero_(); mda.zero_()
|
||||
if causal:
|
||||
causal_score *= float(gd)
|
||||
self._dominant_arrivals(causal_ring[slot], causal_score, causal_parent, causal_edge, causal_arrival)
|
||||
causal_ring[slot] = None
|
||||
|
||||
self.rt.refractory.clamp_min_(1).sub_(1); available = self.rt.refractory == 0
|
||||
self.rt.v[available] += alpha * (self.rt.g[available] - (self.rt.v[available] - rest)); self.rt.v[~available] = reset
|
||||
spikes = available & (self.rt.v >= threshold)
|
||||
accepted = forced[step] & available[target_gpu]
|
||||
if accepted.any():
|
||||
spikes[target_gpu[accepted]] = True
|
||||
dacc = torch.zeros(len(dan), dtype=torch.bool, device=self.device)
|
||||
if len(dan):
|
||||
dacc = dan_forced[step] & available[dan_gpu]
|
||||
if dacc.any():
|
||||
spikes[dan_gpu[dacc]] = True
|
||||
kc_new = spikes[self.kc_gpu] & (first[self.kc_gpu] < 0)
|
||||
if kc_new.any():
|
||||
kc_apl_first[kc_new] = kr[KC_CATEGORIES.index("APL"), kc_new]
|
||||
sources = torch.nonzero(spikes, as_tuple=False).flatten()
|
||||
counts.add_(spikes.to(torch.int32)); new = spikes & (first < 0); first[new] = step
|
||||
if collect_kc_spikes:
|
||||
kn = torch.nonzero(spikes[self.kc_gpu], as_tuple=False).flatten().cpu().numpy()
|
||||
if len(kn):
|
||||
kc_spike_steps.append(np.full(len(kn), step, np.int32)); kc_spike_nodes.append(self.v.kc[kn])
|
||||
|
||||
accepted_nodes = set(target_gpu[accepted].cpu().numpy().astype(int).tolist()) if bool(accepted.any()) else set()
|
||||
dan_nodes = set(dan_gpu[dacc].cpu().numpy().astype(int).tolist()) if len(dan) and bool(dacc.any()) else set()
|
||||
source_np = sources.cpu().numpy().astype(np.int32) if causal and sources.numel() else np.empty(0, np.int32)
|
||||
if causal and len(source_np):
|
||||
ids = np.arange(len(event_rows), len(event_rows) + len(source_np), dtype=np.int64)
|
||||
current_event[source_np] = ids
|
||||
for eid, node in zip(ids, source_np):
|
||||
root = int(node) in accepted_nodes and int(node) in forced_set
|
||||
par = -1 if root else int(causal_parent[node]); is_causal = root or par >= 0
|
||||
depth = 0 if root else (event_depth[par] + 1 if is_causal else -1)
|
||||
event_rows.append({"event_id": int(eid), "step": int(step), "timestamp_ms": float(step * self.dt), "neuron_idx": int(node),
|
||||
"forced_kind": "injected_A" if root else ("matched_DAN" if int(node) in dan_nodes and int(node) in dan_set else "none"),
|
||||
"causally_reachable_from_A": bool(is_causal), "parent_event_id": par,
|
||||
"parent_edge_id": -1 if root else int(causal_edge[node]), "parent_arrival_step": -1 if root else int(causal_arrival[node]),
|
||||
"dominant_decayed_contribution": None if root else float(causal_score[node]), "path_depth": int(depth)})
|
||||
event_parent.append(par); event_depth.append(depth)
|
||||
|
||||
if sources.numel():
|
||||
self.rt.v[sources] = reset; self.rt.refractory[sources] = refsteps
|
||||
edge_ids = self.rt._hybrid_edges(sources)
|
||||
if edge_ids.numel():
|
||||
next_slot = (step + self.rt.delay_steps) % qlen
|
||||
targets_all = self.rt.indices[edge_ids].long(); weights = self.rt.weights[edge_ids]
|
||||
self.rt.delay_queue[next_slot].scatter_add_(0, targets_all, weights)
|
||||
kloc = self.kc_lookup_gpu[targets_all]; kcode = self.edge_kc_code_gpu[edge_ids]
|
||||
for ci in range(len(KC_CATEGORIES)):
|
||||
mask = (kloc >= 0) & (kcode == ci)
|
||||
if mask.any():
|
||||
kq[next_slot, ci].scatter_add_(0, kloc[mask].long(), weights[mask])
|
||||
mloc = self.mbon_lookup_gpu[targets_all]; mcode = self.edge_mbon_code_gpu[edge_ids]
|
||||
for ci in range(len(MBON_CATEGORIES)):
|
||||
mask = (mloc >= 0) & (mcode == ci)
|
||||
if mask.any():
|
||||
mq[next_slot, ci].scatter_add_(0, mloc[mask].long(), weights[mask])
|
||||
mpq[next_slot, ci].scatter_add_(0, mloc[mask].long(), weights[mask].clamp_min(0))
|
||||
mm = mloc >= 0
|
||||
if mm.any():
|
||||
mdq[next_slot].scatter_add_(0, mloc[mm].long(), delta_by_edge[edge_ids[mm]])
|
||||
|
||||
if causal:
|
||||
eids = edge_ids.cpu().numpy().astype(np.int64); pre = self.edge_sources[eids]
|
||||
pe = current_event[pre]; w = self.base_np[eids]
|
||||
cm = (pe >= 0) & (w > 0)
|
||||
if np.any(cm):
|
||||
causal_ring[next_slot] = (self.indices_np[eids[cm]].astype(np.int32), w[cm].astype(np.float32), pe[cm].astype(np.int64),
|
||||
eids[cm], np.full(np.count_nonzero(cm), step + self.rt.delay_steps, np.int32))
|
||||
if causal and len(source_np):
|
||||
current_event[source_np] = -1
|
||||
|
||||
if mode == "graded":
|
||||
apl_state.mul_(apl_decay).add_(torch.mv(self.input_gpu, spikes.to(torch.float32)) * float(params["normalization"]))
|
||||
inhibition = torch.matmul(apl_state, self.output_gpu) * float(params["gain"])
|
||||
self.rt.g.index_add_(0, self.kc_gpu, -inhibition)
|
||||
kg[KC_CATEGORIES.index("APL")].sub_(inhibition); kr[KC_CATEGORIES.index("APL")].sub_(inhibition)
|
||||
kt[step, KC_CATEGORIES.index("APL")].sub_(inhibition.sum())
|
||||
|
||||
torch.cuda.synchronize(); wall = time.perf_counter() - t0
|
||||
counts_np = counts.cpu().numpy(); first_np = first.cpu().numpy()
|
||||
kshadow = kg.cpu().numpy(); mshadow = mg.cpu().numpy(); mdshadow = mdg.cpu().numpy()
|
||||
final_k = self.rt.g[self.kc_gpu].cpu().numpy(); final_m = self.rt.g[self.mbon_gpu].cpu().numpy()
|
||||
kc_spikes = pd.DataFrame({"step": np.concatenate(kc_spike_steps) if kc_spike_steps else np.empty(0, np.int32),
|
||||
"neuron_idx": np.concatenate(kc_spike_nodes) if kc_spike_nodes else np.empty(0, np.int32)})
|
||||
return {
|
||||
"counts": counts_np, "first_steps": first_np, "dt_ms": self.dt, "wall_seconds": wall,
|
||||
"final_g_kc": final_k, "final_g_mbon": final_m, "final_v_mbon": self.rt.v[self.mbon_gpu].cpu().numpy().copy(),
|
||||
"kc_drive_received": kr.cpu().numpy(), "kc_g_shadow": kshadow, "kc_apl_at_first": kc_apl_first.cpu().numpy(),
|
||||
"mbon_drive_received": mr.cpu().numpy(), "mbon_positive_received": mrp.cpu().numpy(),
|
||||
"mbon_delta_received": mdr.cpu().numpy(), "mbon_g_shadow": mshadow, "mbon_delta_g_shadow": mdshadow,
|
||||
"kc_timeseries": kt.cpu().numpy(), "mbon_timeseries": mt.cpu().numpy(), "events": pd.DataFrame(event_rows), "kc_spikes": kc_spikes,
|
||||
"shadow_validation": {"KC_final_g_max_abs_error": float(np.max(np.abs(kshadow.sum(axis=0) - final_k))),
|
||||
"MBON_final_g_max_abs_error": float(np.max(np.abs(mshadow.sum(axis=0) - final_m)))},
|
||||
}
|
||||
|
||||
|
||||
def save_drive(condition: str, run: dict[str, Any], v: Validator, kc_rows: list[dict[str, Any]], mbon_rows: list[dict[str, Any]], time_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
for ci, category in enumerate(KC_CATEGORIES):
|
||||
for local, neuron in enumerate(v.kc):
|
||||
kc_rows.append({"condition": condition, "category": category, "neuron_idx": int(neuron), "body_id": int(v.meta.loc[neuron, "body_id"]),
|
||||
"cumulative_signed_drive": float(run["kc_drive_received"][ci, local]), "final_g_component": float(run["kc_g_shadow"][ci, local])})
|
||||
for ci, category in enumerate(MBON_CATEGORIES):
|
||||
for local, neuron in enumerate(v.mbon):
|
||||
mbon_rows.append({"condition": condition, "category": category, "neuron_idx": int(neuron), "body_id": int(v.meta.loc[neuron, "body_id"]),
|
||||
"cumulative_signed_drive": float(run["mbon_drive_received"][ci, local]), "cumulative_positive_drive": float(run["mbon_positive_received"][ci, local]),
|
||||
"final_g_component": float(run["mbon_g_shadow"][ci, local]), "actual_delta_cumulative_drive": float(run["mbon_delta_received"][local]),
|
||||
"actual_delta_final_g": float(run["mbon_delta_g_shadow"][local]), "final_total_g": float(run["final_g_mbon"][local]),
|
||||
"final_v": float(run["final_v_mbon"][local]), "threshold_margin_mv": float(v.rt.threshold - run["final_v_mbon"][local])})
|
||||
for step in range(len(run["kc_timeseries"])):
|
||||
row = {"condition": condition, "step": step, "timestamp_ms": step * run["dt_ms"]}
|
||||
row.update({f"KC_{x}": float(run["kc_timeseries"][step, i]) for i, x in enumerate(KC_CATEGORIES)})
|
||||
row.update({f"MBON_{x}": float(run["mbon_timeseries"][step, i]) for i, x in enumerate(MBON_CATEGORIES)})
|
||||
time_rows.append(row)
|
||||
ksum = run["kc_drive_received"].sum(axis=1); msum = run["mbon_drive_received"].sum(axis=1); mpos = run["mbon_positive_received"].sum(axis=1)
|
||||
return {"KC_cumulative_signed_drive": {x: float(ksum[i]) for i, x in enumerate(KC_CATEGORIES)},
|
||||
"MBON_cumulative_signed_drive": {x: float(msum[i]) for i, x in enumerate(MBON_CATEGORIES)},
|
||||
"MBON_cumulative_positive_drive": {x: float(mpos[i]) for i, x in enumerate(MBON_CATEGORIES)},
|
||||
"wall_seconds": run["wall_seconds"], "shadow_validation": run["shadow_validation"]}
|
||||
|
||||
|
||||
def classify_path(labels: list[str], apl_modulation: bool, path_length: int) -> str:
|
||||
if path_length == 1:
|
||||
return "direct recurrent"
|
||||
middle = labels[1:-1]
|
||||
if any(x == "PN_olfactory" for x in middle):
|
||||
return "PN/olfactory feedback"
|
||||
if any(x in {"MBON", "DAN"} for x in middle):
|
||||
return "MBON/DAN feedback"
|
||||
if any(x == "APL" for x in middle) or apl_modulation:
|
||||
return "APL-related"
|
||||
return "other CNS loop" if path_length > 0 else "unexplained"
|
||||
|
||||
|
||||
def recruitment_paths(sim: AuditSimulator, run: dict[str, Any], set_a: np.ndarray, set_b: np.ndarray, actual_delta: np.ndarray, exp: Path) -> dict[str, Any]:
|
||||
events = run["events"].copy(); events.to_parquet(exp / "recruitment/direct-A-all-spike-events.parquet", index=False)
|
||||
first_event = events.sort_values(["step", "event_id"]).drop_duplicates("neuron_idx").set_index("neuron_idx")
|
||||
changed = actual_delta != 0; cand_src = sim.v.cand["source_neuron_idx"].to_numpy(np.int32)
|
||||
b_changed_edges = changed & np.isin(cand_src, set_b); b_changed_sources = np.unique(cand_src[b_changed_edges])
|
||||
event_map = {int(row["event_id"]): row for row in events.to_dict(orient="records")}
|
||||
recruited = np.setdiff1d(sim.v.kc[run["counts"][sim.v.kc] > 0], set_a)
|
||||
node_rows: list[dict[str, Any]] = []; edge_rows: list[dict[str, Any]] = []; summary_rows: list[dict[str, Any]] = []
|
||||
kc_local = {int(x): i for i, x in enumerate(sim.v.kc)}
|
||||
for target in recruited:
|
||||
if int(target) not in first_event.index:
|
||||
continue
|
||||
terminal = first_event.loc[int(target)]; eid = int(terminal["event_id"]); chain: list[dict[str, Any]] = []
|
||||
seen = set()
|
||||
while eid >= 0 and eid not in seen:
|
||||
seen.add(eid); ev = event_map[eid]; chain.append(ev); eid = int(ev["parent_event_id"])
|
||||
chain.reverse(); rooted = bool(chain and chain[0]["forced_kind"] == "injected_A")
|
||||
labels = [sim.population_label(int(x["neuron_idx"])) for x in chain]
|
||||
apl_mod = abs(float(run["kc_apl_at_first"][kc_local[int(target)]])) > 0
|
||||
path_length = len(chain) - 1 if rooted else 0
|
||||
category = classify_path(labels, apl_mod, path_length) if rooted else "unexplained"
|
||||
for order, ev in enumerate(chain):
|
||||
neuron = int(ev["neuron_idx"])
|
||||
node_rows.append({"target_recruited_kc": int(target), "path_order": order, "event_id": int(ev["event_id"]), "neuron_idx": neuron,
|
||||
"body_id": int(sim.v.meta.loc[neuron, "body_id"]), "population": sim.population_label(neuron),
|
||||
"annotation_type": sim.v.types[neuron], "annotation_class": sim.v.cls[neuron], "timestamp_ms": float(ev["timestamp_ms"]),
|
||||
"forced_kind": ev["forced_kind"]})
|
||||
for order, ev in enumerate(chain[1:], start=1):
|
||||
edge_id = int(ev["parent_edge_id"]); parent = chain[order - 1]
|
||||
edge_rows.append({"target_recruited_kc": int(target), "path_order": order, "edge_id": edge_id,
|
||||
"source_neuron_idx": int(parent["neuron_idx"]), "target_neuron_idx": int(ev["neuron_idx"]),
|
||||
"source_spike_time_ms": float(parent["timestamp_ms"]), "arrival_time_ms": float(ev["parent_arrival_step"] * run["dt_ms"]),
|
||||
"target_spike_time_ms": float(ev["timestamp_ms"]), "effective_weight": float(sim.base_np[edge_id]),
|
||||
"raw_synapse_count": int(sim.raw_np[edge_id]), "synaptic_sign": int(np.sign(sim.base_np[edge_id])),
|
||||
"source_population": sim.population_label(int(parent["neuron_idx"])), "target_population": sim.population_label(int(ev["neuron_idx"]))})
|
||||
summary_rows.append({"neuron_idx": int(target), "body_id": int(sim.v.meta.loc[target, "body_id"]), "annotation_type": sim.v.types[target],
|
||||
"first_activation_ms": float(terminal["timestamp_ms"]), "causal_path_found": rooted, "path_length": path_length,
|
||||
"recruitment_class": category, "APL_involved_or_modulatory": apl_mod or ("APL" in labels),
|
||||
"is_injected_B": bool(target in set_b), "is_B_source_with_changed_edge": bool(target in b_changed_sources),
|
||||
"changed_B_edge_count": int(np.count_nonzero(b_changed_edges & (cand_src == target)))})
|
||||
summary_df = pd.DataFrame(summary_rows); summary_df.to_parquet(exp / "recruitment/recruited-kc-summary.parquet", index=False)
|
||||
pd.DataFrame(node_rows).to_parquet(exp / "recruitment/causal-path-nodes.parquet", index=False)
|
||||
pd.DataFrame(edge_rows).to_parquet(exp / "recruitment/causal-path-edges.parquet", index=False)
|
||||
counts = summary_df["recruitment_class"].value_counts().to_dict() if len(summary_df) else {}
|
||||
b_rows = summary_df[summary_df["is_B_source_with_changed_edge"]] if len(summary_df) else summary_df
|
||||
report = {"label": "KC-RECRUITMENT-SOURCE-IDENTIFIED" if len(summary_df) and bool(summary_df["causal_path_found"].all()) else "FAIL-KC-RECRUITMENT-SOURCE-IDENTIFIED",
|
||||
"injected_A_KC": int(len(set_a)), "active_non_A_KC": int(len(recruited)), "traced_recruited_KC": int(len(summary_df)),
|
||||
"causal_paths_found": int(summary_df["causal_path_found"].sum()) if len(summary_df) else 0,
|
||||
"classification": {str(k): int(v) for k, v in counts.items()},
|
||||
"B_set_KC_active": int(np.count_nonzero(run["counts"][set_b])), "B_source_changed_edges": int(np.count_nonzero(b_changed_edges)),
|
||||
"unique_B_source_KC_with_changed_edges": int(len(b_changed_sources)),
|
||||
"B_source_path_classes": {str(k): int(v) for k, v in b_rows["recruitment_class"].value_counts().to_dict().items()} if len(b_rows) else {},
|
||||
"causal_rule": sim.cfg["causal_attribution"]["rule"], "static_shortest_path_used": False,
|
||||
"interpretation_limit": "dominant positive arrival under exact runtime timing is an attribution path, not an intervention-based proof"}
|
||||
write_json(exp / "recruitment/summary.json", report); return report
|
||||
|
||||
|
||||
def drive_audit(sim: AuditSimulator, set_a: np.ndarray, set_b: np.ndarray, olf_a: np.ndarray, cfg: dict[str, Any], exp: Path) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]:
|
||||
dc, oc = cfg["direct_kc"], cfg["olfactory"]; params = cfg["apl_overlay"]
|
||||
specs = {
|
||||
"original_olfactory_CS": dict(targets=olf_a, seed=oc["seed"], mode="original", duration_ms=oc["duration_ms"], rate_hz=oc["rate_hz"]),
|
||||
"APL_overlay_olfactory_CS": dict(targets=olf_a, seed=oc["seed"], mode="graded", duration_ms=oc["duration_ms"], rate_hz=oc["rate_hz"]),
|
||||
"direct_KC_SET_A": dict(targets=set_a, seed=dc["evaluation_seed"], mode="original", duration_ms=dc["duration_ms"], pulses=dc["pulse_times_ms"], collect_kc_spikes=True),
|
||||
"direct_KC_SET_B": dict(targets=set_b, seed=dc["evaluation_seed"], mode="original", duration_ms=dc["duration_ms"], pulses=dc["pulse_times_ms"], collect_kc_spikes=True),
|
||||
}
|
||||
kr: list[dict[str, Any]] = []; mr: list[dict[str, Any]] = []; tr: list[dict[str, Any]] = []; runs = {}; summary = {}
|
||||
for name, spec in specs.items():
|
||||
run = sim.run_audit(delta=np.zeros(len(sim.v.edge_ids), np.float32), **spec); runs[name] = run
|
||||
summary[name] = save_drive(name, run, sim.v, kr, mr, tr)
|
||||
pd.DataFrame(kr).to_parquet(exp / "drive-decomposition/kc-incoming-drive.parquet", index=False)
|
||||
pd.DataFrame(mr).to_parquet(exp / "drive-decomposition/mbon-incoming-drive-baseline.parquet", index=False)
|
||||
pd.DataFrame(tr).to_parquet(exp / "drive-decomposition/drive-timeseries.parquet", index=False)
|
||||
write_json(exp / "drive-decomposition/summary.json", summary); return summary, runs
|
||||
|
||||
|
||||
def mbon_audit(sim: AuditSimulator, base_runs: dict[str, dict[str, Any]], set_a: np.ndarray, set_b: np.ndarray, actual_delta: np.ndarray, cfg: dict[str, Any], exp: Path) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]:
|
||||
dc = cfg["direct_kc"]; posts = {}
|
||||
for label, targets in (("A", set_a), ("B", set_b)):
|
||||
posts[label] = sim.run_audit(targets, dc["evaluation_seed"], "original", dc["duration_ms"], delta=actual_delta, pulses=dc["pulse_times_ms"], collect_kc_spikes=True)
|
||||
rows = []; report = {}
|
||||
for label, pre in (("A", base_runs["direct_KC_SET_A"]), ("B", base_runs["direct_KC_SET_B"])):
|
||||
post = posts[label]; p = response(pre, sim.v); q = response(post, sim.v); eff = compare(p, q)
|
||||
plastic_i = MBON_CATEGORIES.index("plastic_KC")
|
||||
plastic_positive = float(post["mbon_positive_received"][plastic_i].sum()); total_positive = float(post["mbon_positive_received"].sum())
|
||||
total_signed = float(post["mbon_drive_received"].sum()); delta_signed = float(post["mbon_delta_received"].sum())
|
||||
report[label] = {"effect": eff, "total_cumulative_signed_drive": total_signed, "total_excitatory_drive": total_positive,
|
||||
"plastic_KC_excitatory_drive": plastic_positive, "plastic_KC_fraction_of_total_excitatory_drive": plastic_positive / total_positive if total_positive else None,
|
||||
"actual_delta_cumulative_drive": delta_signed, "actual_delta_over_total_excitatory_drive": delta_signed / total_positive if total_positive else None,
|
||||
"actual_delta_over_total_signed_drive": delta_signed / total_signed if total_signed else None,
|
||||
"final_total_g_sum": float(post["final_g_mbon"].sum()), "actual_delta_final_g_sum": float(post["mbon_delta_g_shadow"].sum())}
|
||||
for state, run in (("pre", pre), ("post", post)):
|
||||
for ci, category in enumerate(MBON_CATEGORIES):
|
||||
for local, neuron in enumerate(sim.v.mbon):
|
||||
rows.append({"stimulus": label, "state": state, "category": category, "neuron_idx": int(neuron), "body_id": int(sim.v.meta.loc[neuron, "body_id"]),
|
||||
"cumulative_signed_drive": float(run["mbon_drive_received"][ci, local]), "cumulative_positive_drive": float(run["mbon_positive_received"][ci, local]),
|
||||
"final_g_component": float(run["mbon_g_shadow"][ci, local]), "actual_delta_final_g": float(run["mbon_delta_g_shadow"][local]),
|
||||
"final_total_g": float(run["final_g_mbon"][local]), "final_v": float(run["final_v_mbon"][local]),
|
||||
"threshold_margin_mv": float(sim.v.rt.threshold - run["final_v_mbon"][local])})
|
||||
pd.DataFrame(rows).to_parquet(exp / "mbon-decomposition/pre-post-mbon-drive.parquet", index=False)
|
||||
write_json(exp / "mbon-decomposition/summary.json", report); return report, posts
|
||||
|
||||
|
||||
def sensitivity(sim: AuditSimulator, set_a: np.ndarray, actual_delta: np.ndarray, cfg: dict[str, Any], exp: Path) -> dict[str, Any]:
|
||||
dc, sc = cfg["direct_kc"], cfg["sensitivity"]; changed = actual_delta != 0; rows = []
|
||||
for seed in sc["evaluation_seeds"]:
|
||||
base_delta = np.zeros(len(sim.v.edge_ids), np.float32)
|
||||
base = sim.run_audit(set_a, int(seed), "original", dc["duration_ms"], delta=base_delta, pulses=dc["pulse_times_ms"])
|
||||
bp = response(base, sim.v)
|
||||
for fraction in sc["depression_fractions"]:
|
||||
delta = np.zeros(len(sim.v.edge_ids), np.float32); delta[changed] = -float(fraction) * sim.v.base_candidate[changed]
|
||||
run = base if float(fraction) == 0 else sim.run_audit(set_a, int(seed), "original", dc["duration_ms"], delta=delta, pulses=dc["pulse_times_ms"])
|
||||
q = response(run, sim.v); eff = compare(bp, q)
|
||||
functional = bool(eff["MBON_spike_l1"] > 0 or (eff["MBON_first_latency_delta_ms"] not in (None, 0.0)))
|
||||
rows.append({"seed": int(seed), "depression_fraction": float(fraction), "changed_edges": int(np.count_nonzero(changed)),
|
||||
**eff, "functional_MBON_output_change": functional})
|
||||
frame = pd.DataFrame(rows); frame.to_parquet(exp / "sensitivity/controlled-depression-curve.parquet", index=False)
|
||||
aggregate = []
|
||||
for frac, group in frame.groupby("depression_fraction"):
|
||||
aggregate.append({"depression_fraction": float(frac), "seeds": int(len(group)), "functional_seed_count": int(group.functional_MBON_output_change.sum()),
|
||||
"MBON_delta_g_max_abs_mean": float(group.MBON_delta_g_max_abs.mean()), "MBON_delta_v_max_abs_mean": float(group.MBON_delta_v_max_abs.mean()),
|
||||
"MBON_spike_l1_mean": float(group.MBON_spike_l1.mean()), "downstream_spike_abs_delta_mean": float(group.downstream_spike_count_delta.abs().mean())})
|
||||
functional_levels = [x["depression_fraction"] for x in aggregate if x["functional_seed_count"] > 0]
|
||||
at50 = next(x for x in aggregate if x["depression_fraction"] == 0.5)
|
||||
insensitive = at50["functional_seed_count"] == 0
|
||||
report = {"changed_real_edges": int(np.count_nonzero(changed)), "curve": aggregate,
|
||||
"first_fraction_with_any_functional_MBON_change": min(functional_levels) if functional_levels else None,
|
||||
"MBON_READOUT_INSENSITIVE": bool(insensitive), "label": "MBON-READOUT-INSENSITIVE" if insensitive else "MBON-READOUT-SENSITIVE-WITHIN-TESTED-RANGE",
|
||||
"diagnostic_only_not_saved_as_learned_model": True, "criterion": sc["functional_change_definition"]}
|
||||
write_json(exp / "sensitivity/summary.json", report); return report
|
||||
|
||||
|
||||
def isolated_replay_one(sim: AuditSimulator, spikes: pd.DataFrame, targets: np.ndarray, delta: np.ndarray, duration_ms: float) -> dict[str, Any]:
|
||||
target_lookup = {int(x): i for i, x in enumerate(targets)}; nt = len(targets); steps = int(round(duration_ms / sim.dt)); qlen = sim.rt.queue_len
|
||||
relevant = np.isin(sim.edge_sources, sim.v.kc) & np.isin(sim.indices_np, targets)
|
||||
edge_ids = np.flatnonzero(relevant); by_source: dict[int, tuple[np.ndarray, np.ndarray]] = {}
|
||||
full_delta = np.zeros(len(sim.indices_np), np.float32); full_delta[sim.v.edge_ids] = delta
|
||||
weights = sim.base_np[edge_ids] + full_delta[edge_ids]
|
||||
for source in np.unique(sim.edge_sources[edge_ids]):
|
||||
mask = sim.edge_sources[edge_ids] == source; ids = edge_ids[mask]
|
||||
by_source[int(source)] = (np.asarray([target_lookup[int(x)] for x in sim.indices_np[ids]], np.int32), weights[mask].astype(np.float32))
|
||||
grouped = {int(k): g.neuron_idx.to_numpy(np.int32) for k, g in spikes.groupby("step")}
|
||||
q = np.zeros((qlen, nt), np.float32); g = np.zeros(nt, np.float32); v = np.full(nt, sim.rt.rest, np.float32); ref = np.zeros(nt, np.int32)
|
||||
counts = np.zeros(nt, np.int32); first = np.full(nt, -1, np.int32)
|
||||
for step in range(steps):
|
||||
slot = step % qlen; g = g * np.float32(sim.rt.g_decay) + q[slot]; q[slot].fill(0)
|
||||
ref = np.maximum(ref, 1) - 1; available = ref == 0
|
||||
v[available] += np.float32(sim.rt.alpha) * (g[available] - (v[available] - np.float32(sim.rt.rest))); v[~available] = np.float32(sim.rt.reset_value)
|
||||
fired = available & (v >= np.float32(sim.rt.threshold)); counts += fired; first[(first < 0) & fired] = step; v[fired] = np.float32(sim.rt.reset_value); ref[fired] = sim.rt.refractory_steps
|
||||
for source in grouped.get(step, np.empty(0, np.int32)):
|
||||
if int(source) in by_source:
|
||||
loc, w = by_source[int(source)]; np.add.at(q[(step + sim.rt.delay_steps) % qlen], loc, w)
|
||||
return {"counts": counts, "first": first, "g": g, "v": v, "targets": targets}
|
||||
|
||||
|
||||
def isolated_replay(sim: AuditSimulator, base_runs: dict[str, dict[str, Any]], actual_delta: np.ndarray, cfg: dict[str, Any], whole_direct_status: str, exp: Path) -> dict[str, Any]:
|
||||
changed = actual_delta != 0; targets = np.unique(sim.v.cand.loc[changed, "target_neuron_idx"].to_numpy(np.int32)); rows = []; effects = {}
|
||||
for label, key in (("A", "direct_KC_SET_A"), ("B", "direct_KC_SET_B")):
|
||||
spikes = base_runs[key]["kc_spikes"]
|
||||
pre = isolated_replay_one(sim, spikes, targets, np.zeros(len(actual_delta), np.float32), cfg["direct_kc"]["duration_ms"])
|
||||
post = isolated_replay_one(sim, spikes, targets, actual_delta, cfg["direct_kc"]["duration_ms"])
|
||||
def lat(x: dict[str, Any]) -> float | None:
|
||||
return None if np.all(x["first"] < 0) else float(x["first"][x["first"] >= 0].min() * sim.dt)
|
||||
effect = {"delta_g_max_abs": float(np.max(np.abs(post["g"] - pre["g"]))), "delta_v_max_abs": float(np.max(np.abs(post["v"] - pre["v"]))),
|
||||
"spike_l1": int(np.abs(post["counts"] - pre["counts"]).sum()), "pre_first_latency_ms": lat(pre), "post_first_latency_ms": lat(post),
|
||||
"latency_delta_ms": None if lat(pre) is None or lat(post) is None else float(lat(post) - lat(pre))}
|
||||
effects[label] = effect
|
||||
for state, x in (("pre", pre), ("post", post)):
|
||||
for local, neuron in enumerate(targets):
|
||||
rows.append({"stimulus": label, "state": state, "neuron_idx": int(neuron), "body_id": int(sim.v.meta.loc[neuron, "body_id"]),
|
||||
"spikes": int(x["counts"][local]), "first_latency_ms": None if x["first"][local] < 0 else float(x["first"][local] * sim.dt),
|
||||
"final_g": float(x["g"][local]), "final_v": float(x["v"][local])})
|
||||
pd.DataFrame(rows).to_parquet(exp / "isolated-replay/real-edge-replay.parquet", index=False)
|
||||
af = effects["A"]["spike_l1"] > 0 or effects["A"]["latency_delta_ms"] not in (None, 0.0)
|
||||
bf = effects["B"]["spike_l1"] > 0 or effects["B"]["latency_delta_ms"] not in (None, 0.0)
|
||||
transfer = bool(af and not bf); contamination = bool(transfer and whole_direct_status != "PASS-DIRECT-KC-MEMORY-v2")
|
||||
report = {"target_MBON": int(len(targets)), "actual_changed_edges": int(np.count_nonzero(changed)), "effects": effects,
|
||||
"PLASTICITY_TRANSFER_PASS": transfer, "plasticity_label": "PLASTICITY-TRANSFER-PASS" if transfer else "FAIL-PLASTICITY-TRANSFER",
|
||||
"WHOLE_BRAIN_RECRUITMENT_CONTAMINATION": contamination,
|
||||
"weak_effect_label": None if af else "KC-MBON-PLASTICITY-EFFECT-TOO-WEAK",
|
||||
"spike_train_source": "deterministically regenerated from frozen Phase 4J protocol because exact Phase 4J event trains were not persisted",
|
||||
"new_edges_added": False}
|
||||
write_json(exp / "isolated-replay/summary.json", report); return report
|
||||
|
||||
|
||||
def olfactory_provenance(sim: AuditSimulator, cfg: dict[str, Any], exp: Path) -> dict[str, Any]:
|
||||
meta = sim.v.meta; olf = np.flatnonzero(sim.v.rt.population_masks["olfactory"]).astype(np.int32); olf.sort(); take = int(cfg["olfactory"]["targets_per_cs"])
|
||||
current = np.concatenate([olf[:take], olf[take:2 * take]])
|
||||
explicit = [x for x in meta.columns if any(term in x.lower() for term in ("glomer", "receptor", "sensillum", "antenn"))]
|
||||
fields = [x for x in ("neuron_idx", "body_id", "annotation_superclass", "annotation_class", "annotation_subclass", "annotation_type", "annotation_instance", "side", "neurotransmitter", *explicit) if x in meta.columns]
|
||||
fields = list(dict.fromkeys(fields))
|
||||
table = meta.loc[current, fields].copy(); table.insert(0, "CS", np.repeat(["A", "B"], take)); table.to_parquet(exp / "olfactory-provenance/current-CS-neurons.parquet", index=False)
|
||||
pn = np.flatnonzero(sim.v.cls == "ALPN").astype(np.int32); is_pn_kc = np.isin(sim.edge_sources, pn) & np.isin(sim.indices_np, sim.v.kc); eids = np.flatnonzero(is_pn_kc)
|
||||
edges = pd.DataFrame({"edge_id": eids, "source_neuron_idx": sim.edge_sources[eids], "target_neuron_idx": sim.indices_np[eids], "raw_synapse_count": sim.raw_np[eids]})
|
||||
edges["source_body_id"] = meta.loc[edges.source_neuron_idx, "body_id"].to_numpy(); edges["target_body_id"] = meta.loc[edges.target_neuron_idx, "body_id"].to_numpy()
|
||||
edges.to_parquet(exp / "olfactory-provenance/ALPN-to-KC-real-edges.parquet", index=False)
|
||||
identity_coverage = {x: int((table[x].notna() & ~table[x].fillna("").astype(str).eq("")).sum()) for x in explicit}
|
||||
# Phase 4J selected two contiguous neuron_idx slices from the broad olfactory
|
||||
# mask; it did not select or match by any receptor/glomerulus/PN identity.
|
||||
grounded = False
|
||||
report = {"olfactory_population_definition": "annotation_class == olfactory", "olfactory_population_count": int(len(olf)),
|
||||
"current_CS_definition": "first 64 and next 64 neuron_idx after sorting the broad olfactory population", "current_CS_neurons": int(len(current)),
|
||||
"explicit_glomerulus_or_receptor_columns": explicit, "explicit_identity_nonempty_counts_in_current_CS": identity_coverage,
|
||||
"current_CS_selection_used_explicit_identity": False, "current_CS_glomerulus_grounded": grounded,
|
||||
"label": "GLOMERULUS-GROUNDED" if grounded else "ODOR-INPUT-NOT-GLOMERULUS-GROUNDED",
|
||||
"ALPN_count": int(len(pn)), "ALPN_to_KC_edges": int(len(eids)), "ALPN_to_KC_synapse_weight": int(sim.raw_np[eids].sum()),
|
||||
"DoOR_imported": False, "reason_DoOR_not_imported": "Phase 4K mapping audit found no explicit MaleCNS glomerulus/receptor key for a non-guessing join",
|
||||
"prepared_next_input_scheme": "odor response vector -> explicit receptor/glomerulus mapping -> matched MaleCNS PN identities -> real ALPN-to-KC edges",
|
||||
"unmatched_channels": "all until an explicit official receptor/glomerulus-to-MaleCNS mapping is supplied"}
|
||||
write_json(exp / "olfactory-provenance/summary.json", report); return report
|
||||
|
||||
|
||||
def dataset_checks(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
root = Path(cfg["paths"]["dataset_root"])
|
||||
commands = {
|
||||
"raw_core_sha256": f"cd {root}/raw && sha256sum -c _manifests/checksums.sha256 >/dev/null",
|
||||
"raw_skeleton_md5": f"cd {root}/raw/skeletons && md5sum -c ../_manifests/skeletons/checksums.md5 >/dev/null",
|
||||
"phase3_sha256": f"cd {root}/processed && sha256sum -c audit/checksums.sha256 >/dev/null",
|
||||
"phase3b_audit_sha256": f"cd {root}/processed/neuron-only/audit && sha256sum -c checksums.sha256 >/dev/null",
|
||||
"phase3b_166700_sha256": f"cd {root}/processed/neuron-only && sha256sum -c audit/phase3b-166700/checksums.sha256 >/dev/null",
|
||||
"phase1_phase2_backup_sha256": f"sha256sum -c {root}/backups/male-cns-v1.0-phase1-phase2-20260919-1518.tar.zst.sha256 >/dev/null",
|
||||
}
|
||||
results = {}
|
||||
for name, command in commands.items():
|
||||
proc = subprocess.run(["bash", "-lc", command], capture_output=True, text=True)
|
||||
results[name] = {"passed": proc.returncode == 0, "returncode": proc.returncode, "stderr": proc.stderr[-1000:]}
|
||||
results["all_passed"] = all(x["passed"] for x in results.values())
|
||||
return results
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True); args = ap.parse_args()
|
||||
cfg = yaml.safe_load(args.config.read_text()); exp = Path(cfg["paths"]["experiment_root"]); model = Path(cfg["paths"]["phase4k_model_root"])
|
||||
for sub in ("recruitment", "drive-decomposition", "mbon-decomposition", "sensitivity", "isolated-replay", "olfactory-provenance", "regression", "audit", "manifests", "compiler-reports/extension-build"):
|
||||
(exp / sub).mkdir(parents=True, exist_ok=True)
|
||||
model.mkdir(parents=True, exist_ok=True)
|
||||
before = verify_frozen(cfg); p4j_checks = [verify_checksum(Path(cfg["paths"]["phase4j_root"]), Path(cfg["paths"]["phase4j_root"]) / "manifests/checksums.sha256"),
|
||||
verify_checksum(Path(cfg["paths"]["phase4j_model_root"]), Path(cfg["paths"]["phase4j_model_root"]) / "checksums.sha256")]
|
||||
if not all(x["passed"] for x in p4j_checks): raise RuntimeError("Phase 4J frozen checksum failure")
|
||||
shutil.copy2(args.config, exp / "audit/phase4k.yaml"); shutil.copy2(Path(__file__).resolve(), exp / "audit/phase4k.py")
|
||||
root = Path(cfg["paths"]["connectome_root"]); base_files = [root / "gpu/csr-indptr.int64.npy", root / "gpu/csr-indices.int32.npy", root / "gpu/csr-weights.int64.npy", root / "neuron-index/unified-metadata.parquet"]
|
||||
base_before = {str(x): sha256(x) for x in base_files}
|
||||
v = Validator(cfg, args.config.resolve(), exp); sim = AuditSimulator(v, cfg, exp)
|
||||
sets = pd.read_parquet(Path(cfg["paths"]["phase4j_root"]) / "direct-kc-memory/direct-kc-ensembles.parquet")
|
||||
set_a = np.sort(sets.loc[sets["set"] == "KC_SET_A", "neuron_idx"].to_numpy(np.int32)); set_b = np.sort(sets.loc[sets["set"] == "KC_SET_B", "neuron_idx"].to_numpy(np.int32))
|
||||
overlay = np.load(Path(cfg["paths"]["phase4j_root"]) / f"direct-kc-memory/seed-{cfg['direct_kc']['trace_seed']}/overlay.npz")
|
||||
if not np.array_equal(overlay["edge_id"], v.edge_ids): raise RuntimeError("Phase 4J overlay edge identity mismatch")
|
||||
actual_delta = overlay["plastic_delta_mv"].astype(np.float32)
|
||||
olf = np.flatnonzero(v.rt.population_masks["olfactory"]).astype(np.int32); olf.sort(); olf_a = olf[:cfg["olfactory"]["targets_per_cs"]]
|
||||
|
||||
drive_summary, base_runs = drive_audit(sim, set_a, set_b, olf_a, cfg, exp)
|
||||
dan = v.dan_targets(cfg["direct_kc"]["matched_dan_type"], cfg["direct_kc"]["target_compartment_verbatim"])
|
||||
causal_run = sim.run_audit(set_a, cfg["direct_kc"]["trace_seed"], "original", cfg["direct_kc"]["duration_ms"],
|
||||
delta=np.zeros(len(v.edge_ids), np.float32), pulses=cfg["direct_kc"]["pulse_times_ms"], dan_targets=dan,
|
||||
dan_pulses=[cfg["direct_kc"]["dan_time_ms"]], causal=True, collect_kc_spikes=True)
|
||||
recruitment = recruitment_paths(sim, causal_run, set_a, set_b, actual_delta, exp)
|
||||
mbon, post_runs = mbon_audit(sim, base_runs, set_a, set_b, actual_delta, cfg, exp)
|
||||
curve = sensitivity(sim, set_a, actual_delta, cfg, exp)
|
||||
p4j_summary = json.loads((Path(cfg["paths"]["phase4j_root"]) / "direct-kc-memory/summary.json").read_text())
|
||||
replay = isolated_replay(sim, base_runs, actual_delta, cfg, p4j_summary["status"], exp)
|
||||
odor = olfactory_provenance(sim, cfg, exp)
|
||||
|
||||
labels = {"KC_recruitment": recruitment["label"], "plasticity_transfer": replay["plasticity_label"],
|
||||
"whole_brain_recruitment_contamination": replay["WHOLE_BRAIN_RECRUITMENT_CONTAMINATION"],
|
||||
"MBON_readout_insensitive": curve["MBON_READOUT_INSENSITIVE"], "odor_input": odor["label"]}
|
||||
del sim, v; gc.collect(); torch.cuda.empty_cache()
|
||||
reg = phase4c_regression(cfg, args.config.resolve(), exp)
|
||||
after = verify_frozen(cfg); p4j_after = [verify_checksum(Path(cfg["paths"]["phase4j_root"]), Path(cfg["paths"]["phase4j_root"]) / "manifests/checksums.sha256"),
|
||||
verify_checksum(Path(cfg["paths"]["phase4j_model_root"]), Path(cfg["paths"]["phase4j_model_root"]) / "checksums.sha256")]
|
||||
dataset = dataset_checks(cfg); base_after = {str(x): sha256(x) for x in base_files}
|
||||
integrity = before["all_passed"] and after["all_passed"] and all(x["passed"] for x in p4j_checks + p4j_after) and dataset["all_passed"] and base_before == base_after and reg["all_passed"]
|
||||
integrity_report = {"all_passed": integrity, "phase4c_to_4i_before": before["all_passed"], "phase4c_to_4i_after": after["all_passed"],
|
||||
"phase4j_before": all(x["passed"] for x in p4j_checks), "phase4j_after": all(x["passed"] for x in p4j_after),
|
||||
"dataset_phase1_to_3b": dataset, "base_CSR_unchanged": base_before == base_after, "phase4c_reference_regression": reg["all_passed"]}
|
||||
write_json(exp / "audit/integrity.json", integrity_report); write_json(exp / "audit/base-sha256.json", {"before": base_before, "after": base_after})
|
||||
summary = {"phase": "4K", "labels": labels, "recruitment": recruitment, "drive_decomposition": drive_summary, "MBON_decomposition": mbon,
|
||||
"sensitivity": curve, "isolated_replay": replay, "olfactory_provenance": odor, "integrity": integrity_report,
|
||||
"recommendation": "repair upstream odor/glomerulus-grounded encoding and KC recruitment before changing MBON plasticity/readout" if odor["label"] == "ODOR-INPUT-NOT-GLOMERULUS-GROUNDED" else "use causal audit to choose upstream recruitment versus readout work",
|
||||
"eligibility_v2_modified": False, "base_modified": False, "plastic_mask_expanded": False, "stopped_before_phase4l": True}
|
||||
write_json(exp / "audit/final-summary.json", summary)
|
||||
shutil.copy2(args.config, model / "phase4k.yaml"); shutil.copy2(Path(__file__).resolve(), model / "phase4k.py")
|
||||
write_json(model / "VERSION.json", {"name": cfg["name"], "diagnostic_only": True, "labels": labels, "eligibility_v2_modified": False,
|
||||
"summary_sha256": sha256(exp / "audit/final-summary.json")})
|
||||
print(json.dumps({"labels": labels, "integrity": integrity}, indent=2)); sys.stdout.flush()
|
||||
seal(model, model / "MANIFEST.json", model / "checksums.sha256"); seal(exp, exp / "manifests/manifest.json", exp / "manifests/checksums.sha256")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
phase: 4K
|
||||
name: MaleCNS-KC-recruitment-MBON-causal-audit-v1
|
||||
|
||||
paths:
|
||||
connectome_root: /data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only
|
||||
dataset_root: /data/digital-drosophila/datasets/male-cns-v1.0
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif
|
||||
phase4k_model_root: /data/digital-drosophila/models/male-cns-lif/kc-recruitment-audit-v1
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4k-kc-recruitment-audit
|
||||
reference_root: /data/digital-drosophila/models/male-cns-lif/reference-v1
|
||||
runtime_root: /data/digital-drosophila/models/male-cns-lif/runtime-v1
|
||||
io_root: /data/digital-drosophila/models/male-cns-lif/io-v1
|
||||
phase4c_root: /data/digital-drosophila/experiments/phase4c-runtime
|
||||
phase4d_root: /data/digital-drosophila/experiments/phase4d-io
|
||||
phase4e_root: /data/digital-drosophila/experiments/phase4e-visual-calibration
|
||||
phase4f_root: /data/digital-drosophila/experiments/phase4f-retinotopic-vision
|
||||
phase4f_model_root: /data/digital-drosophila/models/male-cns-lif/visual-encoder-v1
|
||||
phase4g_root: /data/digital-drosophila/experiments/phase4g-frozen-interface
|
||||
phase4g_model_root: /data/digital-drosophila/models/male-cns-lif/frozen-interface-v1
|
||||
phase4g_learned_root: /data/digital-drosophila/models/learned/phase4g
|
||||
phase4h_root: /data/digital-drosophila/experiments/phase4h-dopamine-plasticity
|
||||
phase4h_model_root: /data/digital-drosophila/models/male-cns-lif/dopamine-plasticity-v1
|
||||
phase4h_learned_root: /data/digital-drosophila/models/learned/phase4h
|
||||
phase4i_root: /data/digital-drosophila/experiments/phase4i-mb-mechanistic-validation
|
||||
phase4i_model_root: /data/digital-drosophila/models/male-cns-lif/mb-mechanistic-v1
|
||||
phase4j_root: /data/digital-drosophila/experiments/phase4j-mb-physiology-repair
|
||||
phase4j_model_root: /data/digital-drosophila/models/male-cns-lif/mb-physiology-v1
|
||||
|
||||
names:
|
||||
reference: MaleCNS-LIF-reference-v1
|
||||
runtime: MaleCNS-LIF-runtime-v1
|
||||
|
||||
model:
|
||||
dt_ms: 0.1
|
||||
membrane_tau_ms: 20.0
|
||||
synaptic_tau_ms: 5.0
|
||||
refractory_ms: 2.2
|
||||
resting_potential_mv: -52.0
|
||||
reset_potential_mv: -52.0
|
||||
threshold_mv: -45.0
|
||||
synaptic_delay_ms: 1.8
|
||||
w_syn_mv_per_anatomical_synapse: 0.275
|
||||
|
||||
sign_policy:
|
||||
neurotransmitter_sign: {acetylcholine: 1, gaba: -1, glutamate: -1, dopamine: 1, octopamine: 1, serotonin: 1, histamine: -1}
|
||||
unknown_sign: 0
|
||||
unclear_sign: 0
|
||||
|
||||
readout:
|
||||
bins_ms: [1, 5, 10, 20, 50, 100]
|
||||
|
||||
device:
|
||||
required_name_substring: RTX 4090
|
||||
launcher_visible_gpu_uuid: GPU-79f3a85d-adce-7833-7003-6488140ca596
|
||||
|
||||
expected_graph:
|
||||
neurons: 166700
|
||||
directed_edges: 25582938
|
||||
synaptic_contact_weight_sum: 124177617
|
||||
phase4h_plastic_candidate_edges: 14232
|
||||
plastic_mask_edges: 14232
|
||||
|
||||
apl_overlay:
|
||||
name: APL-Graded-Inhibition-v1
|
||||
tau_ms: 20.0
|
||||
gain: 0.01
|
||||
normalization: 1.0
|
||||
immutable_phase4j_parameters: true
|
||||
|
||||
direct_kc:
|
||||
target_compartment_verbatim: "y5B'2a"
|
||||
matched_dan_type: PAM15
|
||||
pulse_times_ms: [10.0, 30.0, 50.0, 70.0, 90.0]
|
||||
dan_time_ms: 90.0
|
||||
duration_ms: 120.0
|
||||
trace_seed: 50201
|
||||
evaluation_seed: 50301
|
||||
|
||||
olfactory:
|
||||
rate_hz: 50.0
|
||||
duration_ms: 100.0
|
||||
seed: 50101
|
||||
targets_per_cs: 64
|
||||
|
||||
sensitivity:
|
||||
depression_fractions: [0.0, 0.01, 0.05, 0.10, 0.25, 0.50]
|
||||
evaluation_seeds: [50301, 50302, 50303, 50304, 50305]
|
||||
functional_change_definition: any_MBON_spike_count_or_first_latency_change
|
||||
|
||||
causal_attribution:
|
||||
rule: dominant_positive_synaptic_arrival_with_runtime_g_decay
|
||||
exact_delay_steps: 18
|
||||
shortest_static_path_forbidden: true
|
||||
|
||||
prohibitions:
|
||||
training: false
|
||||
modify_eta_or_eligibility_v2: false
|
||||
expand_plastic_mask: false
|
||||
modify_threshold_or_gain: false
|
||||
save_sensitivity_as_learned_model: false
|
||||
decoder_classifier_rl_game_visual: false
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"file_count": 6,
|
||||
"files": [
|
||||
{
|
||||
"bytes": 363,
|
||||
"path": "VERSION.json",
|
||||
"sha256": "cc79527b6f36804f8ba4fc383988c08b550cb0eb43567f8b4ee3948f6231063b"
|
||||
},
|
||||
{
|
||||
"bytes": 6464,
|
||||
"path": "literature-audit.json",
|
||||
"sha256": "820ef25d0dff122a3cad1a763074ff5e6f74a5a7e08fb00620454ab4164f3fc4"
|
||||
},
|
||||
{
|
||||
"bytes": 35313,
|
||||
"path": "phase4m.py",
|
||||
"sha256": "3d1b64861559c9c04e08153b4056d968ab556637916d388170b36fc6348fa822"
|
||||
},
|
||||
{
|
||||
"bytes": 2339,
|
||||
"path": "phase4m.yaml",
|
||||
"sha256": "52c364d2ada03ba0c3b06220e91641c74ee79486c35d8c283237f02a98a2e471"
|
||||
},
|
||||
{
|
||||
"bytes": 5545,
|
||||
"path": "phase4m_finalize.py",
|
||||
"sha256": "58a2fd9bfc89725c741201f43ef06cf34840f78734190faad193ae8cee89c6ab"
|
||||
},
|
||||
{
|
||||
"bytes": 5487,
|
||||
"path": "phase4m_performance.py",
|
||||
"sha256": "7fab670032732d36823499c135a725f5ab7c8aa8305be780ee51f7eb0b8dfd57"
|
||||
}
|
||||
],
|
||||
"generated_utc": "2026-09-21T06:00:10.958765+00:00",
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/kcmbon-efficacy-v1",
|
||||
"total_bytes": 55511
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"base_CSR_read_only": true,
|
||||
"candidates": {
|
||||
"KCMBON-FunctionalTransfer-v2": true,
|
||||
"KCMBON-SynapticEfficacy-v1": true,
|
||||
"MaleCNS-OlfactoryAssociativeMemory-v1-candidate": false,
|
||||
"PresynapticEfficacy-v1": true
|
||||
},
|
||||
"name": "KCMBON-SynapticEfficacy-v1",
|
||||
"summary_sha256": "f0ba8af69ea749c7329f950d320e0330a0b9ad339730eb36e0a107b049eae38e"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
6d7dd50ee8554af53223c9a3bcef60389a25f9eb75fa572f42cec034a171fc05 MANIFEST.json
|
||||
cc79527b6f36804f8ba4fc383988c08b550cb0eb43567f8b4ee3948f6231063b VERSION.json
|
||||
820ef25d0dff122a3cad1a763074ff5e6f74a5a7e08fb00620454ab4164f3fc4 literature-audit.json
|
||||
3d1b64861559c9c04e08153b4056d968ab556637916d388170b36fc6348fa822 phase4m.py
|
||||
52c364d2ada03ba0c3b06220e91641c74ee79486c35d8c283237f02a98a2e471 phase4m.yaml
|
||||
58a2fd9bfc89725c741201f43ef06cf34840f78734190faad193ae8cee89c6ab phase4m_finalize.py
|
||||
7fab670032732d36823499c135a725f5ab7c8aa8305be780ee51f7eb0b8dfd57 phase4m_performance.py
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"phase": "4M",
|
||||
"status": "LITERATURE-AUDIT-COMPLETE-BEFORE-IMPLEMENTATION",
|
||||
"scope": "Adult Drosophila KC-to-MBON plasticity; evidence is not a quantitative calibration of MaleCNS v1.0 synapses",
|
||||
"sources": [
|
||||
{
|
||||
"source": "Stahl et al., eLife 2022",
|
||||
"doi": "10.7554/eLife.76712",
|
||||
"pmid": "35285796",
|
||||
"url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC8956283/",
|
||||
"experiment_type": "in vivo learning protocols with acetylcholine sensor imaging along KC axonal compartments",
|
||||
"species_stage": "adult Drosophila melanogaster",
|
||||
"compartment": "multiple MB axonal compartments; longitudinally graded",
|
||||
"measured_quantity": "learning-related KC acetylcholine release change",
|
||||
"finding": "compartmentalized bidirectional ACh-release plasticity; valence and location matter",
|
||||
"model_interpretation": "supports an independently represented presynaptic transmitter-efficacy layer, but supplies no MaleCNS edge-specific efficacy or update coefficient"
|
||||
},
|
||||
{
|
||||
"source": "Davidson, Kaushik and Hige, eNeuro 2023",
|
||||
"doi": "10.1523/ENEURO.0275-23.2023",
|
||||
"pmid": "37848287",
|
||||
"url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC10616905/",
|
||||
"experiment_type": "odor-DAN pairing with in vivo MBON calcium, KC acetylcholine and individual bouton calcium imaging",
|
||||
"species_stage": "adult female Drosophila melanogaster, 3-6 days after eclosion",
|
||||
"compartment": "gamma5/beta-prime-2a and gamma1-pedc; gamma5/beta-prime-2a is related to but not an exact measured MaleCNS edge mapping for y5B'2a",
|
||||
"measured_quantity": "compartment-specific KC ACh release and heterogeneous single-bouton calcium signals",
|
||||
"finding": "paired DAN activity depressed odor-evoked ACh release, while bulk KC axonal calcium was unchanged; strongly responsive boutons could depress and weakly responsive boutons could potentiate",
|
||||
"model_interpretation": "supports local presynaptic efficacy and cautions against assigning identical biological release responses to all terminals; bouton-level parameters are not inferable from MaleCNS neuron-level CSR"
|
||||
},
|
||||
{
|
||||
"source": "Cohn et al., Cell 2015",
|
||||
"doi": "10.1016/j.cell.2015.11.019",
|
||||
"pmid": "26687359",
|
||||
"url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC4732734/",
|
||||
"experiment_type": "ex vivo KC stimulation and MBON electrophysiology with DAN activation",
|
||||
"species_stage": "adult Drosophila melanogaster",
|
||||
"compartment": "gamma4 and other tested MB domains",
|
||||
"measured_quantity": "KC-to-MBON synaptic current and presynaptic modulation",
|
||||
"finding": "dopaminergic protocols can potentiate or depress synaptic transmission; temporal order can reverse direction",
|
||||
"model_interpretation": "reinforces compartment/timing dependence; does not justify changing frozen Phase 4J timing polarity or eta"
|
||||
},
|
||||
{
|
||||
"source": "Pribbenow et al., eLife 2022",
|
||||
"doi": "10.7554/eLife.80445",
|
||||
"pmid": "36250621",
|
||||
"url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC9733945/",
|
||||
"experiment_type": "MBON receptor-subunit genetics, in vivo receptor dynamics and appetitive/familiarity behavior",
|
||||
"species_stage": "adult Drosophila melanogaster",
|
||||
"compartment": "M4/M6 and alpha-prime-3-related circuits, not a validated edge-level mapping to current MaleCNS y5B'2a target",
|
||||
"measured_quantity": "postsynaptic nAChR alpha5 requirement and alpha2 receptor dynamics",
|
||||
"finding": "postsynaptic cholinergic plasticity can contribute to memory induction/expression, with behavior/compartment specificity",
|
||||
"model_interpretation": "supports a separate postsynaptic research branch, but no defensible quantitative update for the current y5B'2a MaleCNS compartment; do not run a mixed branch here"
|
||||
},
|
||||
{
|
||||
"source": "Zhang et al., Journal of Neuroscience 2019",
|
||||
"doi": "10.1523/JNEUROSCI.1420-19.2019",
|
||||
"pmid": null,
|
||||
"url": "https://pmc.ncbi.nlm.nih.gov/articles/PMC6855682/",
|
||||
"experiment_type": "aversive conditioning with presynaptic and postsynaptic calcium imaging",
|
||||
"species_stage": "adult Drosophila melanogaster",
|
||||
"compartment": "alpha-prime-3",
|
||||
"measured_quantity": "pre- and postsynaptic odor-evoked calcium suppression",
|
||||
"finding": "both sides showed suppression across a similar post-training period; causality and quantitative partition are unresolved",
|
||||
"model_interpretation": "supports mixed mechanisms in some compartments, not automatic transfer of a postsynaptic coefficient to y5B'2a"
|
||||
}
|
||||
],
|
||||
"mechanism_conclusion": {
|
||||
"presynaptic": "direct support for compartment-specific learning-related KC ACh release modulation; exact release probability per MaleCNS edge remains unknown",
|
||||
"postsynaptic": "direct support for nAChR-related MBON changes in specific other circuits; no validated quantitative rule for the current target compartment",
|
||||
"mixed": "biologically plausible and compartment-dependent, but not justified as one blended MaleCNS y5B'2a rule",
|
||||
"timing_pairings": "LTD and opposite-direction changes depend on KC/DAN timing and training protocol; frozen eligibility-v2 is retained, not recalibrated",
|
||||
"saturation_recovery": "the cited studies do not identify a universal edge-level saturation/recovery constant for MaleCNS; physical nonnegative E and the pre-existing Phase 4J 25-percent diagnostic bound are explicit engineering assumptions, not measured values"
|
||||
},
|
||||
"decisions_before_simulation": {
|
||||
"main_branch": "PresynapticEfficacy-v1",
|
||||
"postsynaptic_branch": "AUDIT-ONLY-NOT-RUN-COMPARTMENT-SPECIFIC-QUANTITATIVE-RULE-UNKNOWN",
|
||||
"efficacy_interpretation": "dimensionless per-edge effective KC terminal drive multiplier, not measured release probability",
|
||||
"E_initial": 1.0,
|
||||
"E_unit_conversion": "one frozen eta_effective_mv increment is converted to a dimensionless per-anatomical-contact fraction by division by frozen W_syn_mv_per_anatomical_synapse; this is an explicit engineering unit convention, not fitted to Phase 4K sensitivity",
|
||||
"unbounded_multiplicative_lower_limit": 0.0,
|
||||
"bounded_multiplicative_lower_limit": 0.75,
|
||||
"bounded_limit_provenance": "Phase 4J frozen max_fractional_depression=0.25, not newly tuned",
|
||||
"five_percent_sensitivity_used_for_tuning": false,
|
||||
"numeric_efficacy_claimed_as_measured": false,
|
||||
"random_heterogeneity_added": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4M: isolated KC->MBON efficacy mechanisms on frozen MaleCNS inputs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import json
|
||||
import resource
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
from phase4h_audit import sha256, verify_checksum, write_json
|
||||
from phase4i import Validator, phase4c_regression
|
||||
from phase4j import apply_v2, effect, verify_frozen
|
||||
from phase4k import dataset_checks, seal
|
||||
from phase4l import OdorSimulator, pn_drive
|
||||
|
||||
|
||||
MECHANISMS = ("additive", "multiplicative", "bounded_multiplicative")
|
||||
|
||||
|
||||
def frozen_checks(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
earlier = verify_frozen(cfg); later = {}
|
||||
for key, control in (("phase4j_root", "manifests/checksums.sha256"), ("phase4j_model_root", "checksums.sha256"),
|
||||
("phase4k_root", "manifests/checksums.sha256"), ("phase4k_model_root", "checksums.sha256"),
|
||||
("phase4l_root", "manifests/checksums.sha256"), ("phase4l_model_root", "checksums.sha256")):
|
||||
root = Path(cfg["paths"][key]); later[key] = verify_checksum(root, root / control)
|
||||
report = {"phase4c_to_4i": earlier["all_passed"], "phase4j_to_4l": {k: v["passed"] for k, v in later.items()}}
|
||||
report["all_passed"] = bool(earlier["all_passed"] and all(v["passed"] for v in later.values()))
|
||||
if not report["all_passed"]: raise RuntimeError("frozen Phase 4C-4L checksum failure")
|
||||
return report
|
||||
|
||||
|
||||
def frozen_hashes(cfg: dict[str, Any]) -> dict[str, str]:
|
||||
p = cfg["paths"]; root = Path(p["connectome_root"]); files = [root / "gpu/csr-indptr.int64.npy", root / "gpu/csr-indices.int32.npy", root / "gpu/csr-weights.int64.npy", root / "neuron-index/unified-metadata.parquet",
|
||||
Path(p["phase4h_root"]) / "plastic-mask/plastic-edge-candidates.parquet",
|
||||
Path(p["phase4j_root"]) / "audit/phase4j.py", Path(p["phase4j_model_root"]) / "phase4j.yaml",
|
||||
Path(p["phase4l_root"]) / "odor-encoder/frozen-odor-panel.parquet", Path(p["phase4l_root"]) / "odor-encoder/mapped-glomerular-vectors.npz",
|
||||
Path(p["phase4l_root"]) / "receptor-mapping/olfactory-mapping.parquet", Path(p["phase4l_root"]) / "manifests/checksums.sha256",
|
||||
Path(p["phase4l_model_root"]) / "checksums.sha256"]
|
||||
for f in files:
|
||||
if not f.is_file(): raise FileNotFoundError(f)
|
||||
return {str(f): sha256(f) for f in files}
|
||||
|
||||
|
||||
def load_odor_inputs(cfg: dict[str, Any]) -> tuple[pd.DataFrame, dict[str, np.ndarray], dict[str, Any], pd.DataFrame]:
|
||||
exp = Path(cfg["paths"]["phase4l_root"]); panel = pd.read_parquet(exp / "odor-encoder/frozen-odor-panel.parquet"); vecfile = np.load(exp / "odor-encoder/mapped-glomerular-vectors.npz"); vectors = {str(oid): vecfile[str(oid)].copy() for oid in panel.odor_id}
|
||||
selection = json.loads((exp / "odor-encoder/selection.json").read_text()); mapping = pd.read_parquet(exp / "receptor-mapping/olfactory-mapping.parquet")
|
||||
if selection["ODOR_A"]["odor_id"] not in vectors or selection["ODOR_B"]["odor_id"] not in vectors: raise RuntimeError("frozen odor identity mismatch")
|
||||
return panel, vectors, selection, mapping
|
||||
|
||||
|
||||
def anatomical_audit(v: Validator, cfg: dict[str, Any], exp: Path) -> tuple[np.ndarray, np.ndarray]:
|
||||
cand = v.cand; raw = np.asarray(v.rt.raw_np[v.edge_ids], dtype=np.int64); sign = v.rt.signs[cand.source_neuron_idx.to_numpy(np.int32)]
|
||||
w = (raw.astype(np.float32) * sign.astype(np.float32) * np.float32(cfg["model"]["w_syn_mv_per_anatomical_synapse"])).astype(np.float32)
|
||||
base = v.base_candidate.astype(np.float32)
|
||||
if not np.array_equal(raw, cand.raw_synapse_count.to_numpy(np.int64)): raise RuntimeError("plastic mask/raw CSR count mismatch")
|
||||
if not np.array_equal(w, base): raise RuntimeError("anatomical effective weight mismatch with frozen runtime")
|
||||
if not np.all(sign == 1): raise RuntimeError("presynaptic efficacy branch requires cholinergic positive KC candidates")
|
||||
if len(cand) != 14232 or len(np.unique(v.edge_ids)) != 14232: raise RuntimeError("plastic mask changed")
|
||||
report = {"plastic_mask_edges": len(cand), "raw_count_min": int(raw.min()), "raw_count_median": float(np.median(raw)), "raw_count_max": int(raw.max()),
|
||||
"raw_count_sum": int(raw.sum()), "anatomical_weight_min_mv": float(w.min()), "anatomical_weight_max_mv": float(w.max()),
|
||||
"all_cholinergic_positive": True, "anatomical_formula_verified_exactly": True, "base_CSR_read_only": True,
|
||||
"E_birth_state": 1.0, "other_brain_edges_E": 1.0}
|
||||
write_json(exp / "anatomical-weight/summary.json", report); return raw, w
|
||||
|
||||
|
||||
class ReplayEngine:
|
||||
"""Real KC spikes + existing KC->MBON edges + frozen MBON LIF, no invented edge."""
|
||||
|
||||
def __init__(self, sim: OdorSimulator, targets: np.ndarray):
|
||||
self.sim = sim; self.targets = np.asarray(targets, np.int32); self.dt = sim.dt; self.rt = sim.rt; v = sim.v
|
||||
indices = v.rt.indices_np; sources = sim.sources; mask = np.isin(sources, v.kc) & np.isin(indices, self.targets); self.ids = np.flatnonzero(mask)
|
||||
self.src = sources[self.ids].astype(np.int32); self.target = indices[self.ids].astype(np.int32)
|
||||
pos = {int(x): i for i, x in enumerate(self.targets)}; self.target_local = np.fromiter((pos[int(x)] for x in self.target), np.int32, len(self.target))
|
||||
all_base = v.base.detach().cpu().numpy(); self.base = all_base[self.ids].astype(np.float32)
|
||||
can_index = {int(eid): i for i, eid in enumerate(v.edge_ids)}; self.candidate_local = np.fromiter((can_index.get(int(x), -1) for x in self.ids), np.int32, len(self.ids))
|
||||
self.by_source = {int(s): np.flatnonzero(self.src == s) for s in np.unique(self.src)}
|
||||
if not np.all(self.base > 0): raise RuntimeError("isolated KC replay contains nonpositive source weights")
|
||||
|
||||
def run(self, spikes: pd.DataFrame, delta: np.ndarray, duration_ms: float) -> dict[str, Any]:
|
||||
weights = self.base.copy(); valid = self.candidate_local >= 0; weights[valid] += delta[self.candidate_local[valid]]
|
||||
grouped = {int(k): g.neuron_idx.to_numpy(np.int32) for k, g in spikes.groupby("step")}; nsteps = int(round(duration_ms / self.dt)); n = len(self.targets); q = np.zeros((self.rt.queue_len, n), np.float32)
|
||||
conductance = np.zeros(n, np.float32); voltage = np.full(n, self.rt.rest, np.float32); refractory = np.zeros(n, np.int32); counts = np.zeros(n, np.int32); first = np.full(n, -1, np.int32)
|
||||
integrated_g = np.zeros(n, np.float64); transmitted = 0.0; delivered = 0.0; wall0 = time.perf_counter()
|
||||
for step in range(nsteps):
|
||||
slot = step % self.rt.queue_len; conductance = conductance * np.float32(self.rt.g_decay) + q[slot]; delivered += float(q[slot].sum(dtype=np.float64)); q[slot].fill(0)
|
||||
integrated_g += conductance; refractory = np.maximum(refractory, 1) - 1; avail = refractory == 0
|
||||
voltage[avail] += np.float32(self.rt.alpha) * (conductance[avail] - (voltage[avail] - np.float32(self.rt.rest))); voltage[~avail] = np.float32(self.rt.reset_value)
|
||||
fired = avail & (voltage >= np.float32(self.rt.threshold)); counts += fired; first[(first < 0) & fired] = step; voltage[fired] = np.float32(self.rt.reset_value); refractory[fired] = self.rt.refractory_steps
|
||||
for source in grouped.get(step, np.empty(0, np.int32)):
|
||||
rel = self.by_source.get(int(source))
|
||||
if rel is not None:
|
||||
np.add.at(q[(step + self.rt.delay_steps) % self.rt.queue_len], self.target_local[rel], weights[rel]); transmitted += float(weights[rel].sum(dtype=np.float64))
|
||||
return {"counts": counts, "first": first, "final_g": conductance, "final_v": voltage, "integrated_g": integrated_g,
|
||||
"transmitted_weighted_events_mv": transmitted, "delivered_input_mv": delivered, "wall_seconds": time.perf_counter() - wall0}
|
||||
|
||||
|
||||
def replay_effect(pre: dict[str, Any], post: dict[str, Any], dt: float) -> dict[str, Any]:
|
||||
def first_ms(x: dict[str, Any]) -> float | None:
|
||||
a = x["first"]; return None if np.all(a < 0) else float(a[a >= 0].min() * dt)
|
||||
a, b = first_ms(pre), first_ms(post)
|
||||
return {"delta_g_max_abs_mv": float(np.abs(post["final_g"] - pre["final_g"]).max()), "delta_g_mean_abs_mv": float(np.abs(post["final_g"] - pre["final_g"]).mean()),
|
||||
"delta_v_max_abs_mv": float(np.abs(post["final_v"] - pre["final_v"]).max()), "delta_v_mean_abs_mv": float(np.abs(post["final_v"] - pre["final_v"]).mean()),
|
||||
"integrated_g_delta_sum_mv_steps": float((post["integrated_g"] - pre["integrated_g"]).sum()),
|
||||
"MBON_spikes_pre": int(pre["counts"].sum()), "MBON_spikes_post": int(post["counts"].sum()), "MBON_spike_count_delta": int(post["counts"].sum() - pre["counts"].sum()),
|
||||
"MBON_spike_L1": int(np.abs(post["counts"] - pre["counts"]).sum()), "first_latency_pre_ms": a, "first_latency_post_ms": b,
|
||||
"first_latency_delta_ms": None if a is None or b is None else float(b - a), "transmitted_delta_mv": float(post["transmitted_weighted_events_mv"] - pre["transmitted_weighted_events_mv"]),
|
||||
"transmitted_reduction_percent": float(100 * (pre["transmitted_weighted_events_mv"] - post["transmitted_weighted_events_mv"]) / pre["transmitted_weighted_events_mv"]) if pre["transmitted_weighted_events_mv"] else None,
|
||||
"MBON_total_KC_input_reduction_percent": float(100 * (pre["delivered_input_mv"] - post["delivered_input_mv"]) / pre["delivered_input_mv"]) if pre["delivered_input_mv"] else None,
|
||||
"isolated_input_scope": "all real KC->target-MBON edges, not all whole-brain MBON inputs"}
|
||||
|
||||
|
||||
def latency_direction(row: dict[str, Any]) -> bool:
|
||||
value = row.get("first_latency_delta_ms"); return bool(row["MBON_spike_count_delta"] < 0 or (value is not None and value > 0))
|
||||
|
||||
|
||||
def compare_A_B(a: dict[str, Any], b: dict[str, Any]) -> bool:
|
||||
if not latency_direction(a): return False
|
||||
if a["MBON_spike_L1"] > b["MBON_spike_L1"]: return True
|
||||
al = 0.0 if a["first_latency_delta_ms"] is None else max(float(a["first_latency_delta_ms"]), 0.0)
|
||||
bl = 0.0 if b["first_latency_delta_ms"] is None else max(float(b["first_latency_delta_ms"]), 0.0)
|
||||
return bool(al > bl and a["MBON_spike_L1"] >= b["MBON_spike_L1"])
|
||||
|
||||
|
||||
def main_experiment(v: Validator, sim: OdorSimulator, cfg: dict[str, Any], mcfg: dict[str, Any], exp: Path,
|
||||
panel: pd.DataFrame, vectors: dict[str, np.ndarray], selection: dict[str, Any], mapping: pd.DataFrame,
|
||||
anatomical: np.ndarray) -> tuple[dict[str, Any], dict[int, dict[int, dict[str, np.ndarray]]], ReplayEngine]:
|
||||
protocol = mcfg["protocol"]; points = list(map(int, protocol["pairing_counts"])); maxpair = max(points); seeds = list(map(int, protocol["train_seeds"])); eval_seeds = list(map(int, protocol["evaluation_seeds"]));
|
||||
if points != list(map(int, cfg["plasticity_accumulation"]["pairing_counts"])) or seeds != list(map(int, cfg["plasticity_accumulation"]["train_seeds"])) or eval_seeds != list(map(int, cfg["plasticity_accumulation"]["evaluation_seeds"])): raise RuntimeError("Phase 4L frozen pairing protocol mismatch")
|
||||
compartment = str(protocol["target_compartment_verbatim"]); dan = v.dan_targets(str(protocol["matched_dan_type"]), compartment); duration = float(protocol["duration_ms"])
|
||||
gloms = selection["glomeruli_order"]; matched_sum = float(np.median(panel.mapped_response_sum)); pn = {}
|
||||
for label in ("ODOR_A", "ODOR_B"):
|
||||
oid = selection[label]["odor_id"]; target, rate, _ = pn_drive(oid, vectors[oid], mapping, gloms, str(protocol["input_mode"]), matched_sum, cfg); pn[label] = (target, rate)
|
||||
src = v.cand.source_neuron_idx.to_numpy(np.int32); lookup = np.full(v.n, -1, np.int32); lookup[v.kc] = np.arange(len(v.kc), dtype=np.int32); loc = lookup[src]
|
||||
if np.any(loc < 0): raise RuntimeError("non-KC source in plastic mask")
|
||||
comp = v.cand.target_compartment_verbatim.astype(str).to_numpy() == compartment; k = float(cfg["eligibility_v2"]["eta_effective_mv"]) / float(cfg["model"]["w_syn_mv_per_anatomical_synapse"])
|
||||
if k <= 0 or k >= 1: raise RuntimeError("invalid frozen eta/Wsyn dimensionless conversion")
|
||||
bound = 1.0 - float(cfg["eligibility_v2"]["max_fractional_depression"]); checkpoints: dict[int, dict[int, dict[str, np.ndarray]]] = {}; histories = []; update_times = []; training_walls = []; reproduction = []
|
||||
zero = np.zeros(len(v.edge_ids), np.float32); target_mbon = np.unique(v.cand.loc[comp, "target_neuron_idx"].to_numpy(np.int32)); replay = ReplayEngine(sim, target_mbon)
|
||||
# Baseline replay uses the same deterministic evaluation KC spike trains for all mechanisms.
|
||||
evaluation = {}; pre_replay = {}
|
||||
for es in eval_seeds:
|
||||
for label, (targets, rates) in pn.items():
|
||||
run = sim.run_rates(targets, rates, es, duration, collect_kc_spikes=True); evaluation[(es, label)] = run["kc_spikes"]
|
||||
run["kc_spikes"].to_parquet(exp / "isolated-replay" / f"KC-spikes-{label}-seed-{es}.parquet", index=False)
|
||||
pre_replay[(es, label)] = replay.run(run["kc_spikes"], zero, duration)
|
||||
for ts in seeds:
|
||||
delta_add = zero.copy(); em = np.ones(len(v.edge_ids), np.float32); eb = np.ones(len(v.edge_ids), np.float32); checkpoints[ts] = {}; p4l = np.load(Path(cfg["paths"]["phase4l_root"]) / "plasticity-magnitude" / f"seed-{ts}-overlays.npz")
|
||||
if not np.array_equal(p4l["edge_id"], v.edge_ids): raise RuntimeError("Phase 4L additive checkpoint edge IDs differ")
|
||||
for pairing in range(1, maxpair + 1):
|
||||
run = sim.run_rates(*pn["ODOR_A"], ts + pairing, duration, delta=delta_add, dan_targets=dan, dan_pulses=[float(protocol["dan_time_ms"])])
|
||||
training_walls.append(run["wall_seconds"]); trace = run["kc_trace_at_dan"]; elig = trace[loc].astype(np.float32); active = comp & (elig > 0)
|
||||
old_m, old_b = em[active].copy(), eb[active].copy(); t0 = time.perf_counter()
|
||||
delta_add, _ = apply_v2(v, trace, compartment, 1.0, delta_add, cfg)
|
||||
factor = np.float32(1.0) - np.float32(k) * elig[active]; em[active] = np.maximum(np.float32(0.0), em[active] * factor)
|
||||
eb[active] = np.maximum(np.float32(bound), eb[active] * factor); update_times.append(time.perf_counter() - t0)
|
||||
for local, om, ob in zip(np.flatnonzero(active), old_m, old_b):
|
||||
histories.append({"train_seed": ts, "pairing": pairing, "edge_id": int(v.edge_ids[local]), "eligibility": float(elig[local]),
|
||||
"E_multiplicative_before": float(om), "E_multiplicative_after": float(em[local]), "E_bounded_before": float(ob), "E_bounded_after": float(eb[local])})
|
||||
if pairing in points:
|
||||
saved = p4l[f"pairing_{pairing}"].astype(np.float32); error = float(np.max(np.abs(saved - delta_add)))
|
||||
reproduction.append({"train_seed": ts, "pairing": pairing, "max_abs_error_mv": error, "exact": bool(np.array_equal(saved, delta_add))})
|
||||
if error > 1e-6: raise RuntimeError(f"Phase 4L additive training reproduction divergence seed={ts} pairing={pairing}: {error}")
|
||||
checkpoints[ts][pairing] = {"additive": delta_add.copy(), "multiplicative": em.copy(), "bounded_multiplicative": eb.copy(), "eligibility_at_last_DAN": elig.copy()}
|
||||
np.savez_compressed(exp / "efficacy-overlay" / f"E-seed-{ts}-pairings-{pairing}.npz", E_multiplicative=em, E_bounded_multiplicative=eb)
|
||||
p4l.close()
|
||||
pd.DataFrame(histories).to_parquet(exp / "efficacy-overlay/update-history.parquet", index=False)
|
||||
write_json(exp / "regression/phase4l-additive-reproduction.json", {"all_passed": all(x["max_abs_error_mv"] <= 1e-6 for x in reproduction), "checks": reproduction})
|
||||
if not np.all(np.isfinite([h["E_multiplicative_after"] for h in histories])) or not np.all(np.isfinite([h["E_bounded_after"] for h in histories])):
|
||||
raise RuntimeError("nonfinite efficacy")
|
||||
rows = []; replay_times = []
|
||||
for ts, es in zip(seeds, eval_seeds):
|
||||
for pairing in points:
|
||||
state = checkpoints[ts][pairing]
|
||||
for mechanism in MECHANISMS:
|
||||
if mechanism == "additive": delta = state["additive"]; efficacy = 1.0 + delta / anatomical
|
||||
else:
|
||||
efficacy = state[mechanism]; delta = anatomical * (efficacy - 1.0)
|
||||
if not np.all(np.isfinite(efficacy)) or np.any(efficacy < -1e-6) or np.any(efficacy > 1.0 + 1e-6): raise RuntimeError("invalid E state")
|
||||
changed = np.abs(delta) > 1e-8; e_changed = efficacy[changed]; weighted_E = float(np.sum(anatomical[changed] * e_changed) / np.sum(anatomical[changed])) if np.any(changed) else 1.0
|
||||
pairrow = {"train_seed": ts, "eval_seed": es, "pairings": pairing, "mechanism": mechanism, "updated_edges": int(changed.sum()),
|
||||
"E_mean_all_mask": float(np.mean(efficacy)), "E_median_all_mask": float(np.median(efficacy)), "E_min_all_mask": float(np.min(efficacy)),
|
||||
"E_mean_changed": float(np.mean(e_changed)) if len(e_changed) else 1.0, "E_median_changed": float(np.median(e_changed)) if len(e_changed) else 1.0,
|
||||
"E_anatomical_weighted_changed": weighted_E, "effective_weight_delta_sum_mv": float(delta.sum()),
|
||||
"effective_weight_reduction_percent_changed": float(100 * (1 - weighted_E)), "saturation_fraction_mask": float(np.count_nonzero(efficacy <= (bound + 1e-6)) / len(efficacy))}
|
||||
result = {}
|
||||
for label in ("ODOR_A", "ODOR_B"):
|
||||
t0 = time.perf_counter(); post = replay.run(evaluation[(es, label)], delta, duration); replay_times.append(time.perf_counter() - t0)
|
||||
result[label] = replay_effect(pre_replay[(es, label)], post, sim.dt)
|
||||
pairrow.update({f"A_{k}": value for k, value in result["ODOR_A"].items() if k != "isolated_input_scope"})
|
||||
pairrow.update({f"B_{k}": value for k, value in result["ODOR_B"].items() if k != "isolated_input_scope"})
|
||||
pairrow["A_specific_functional"] = compare_A_B(result["ODOR_A"], result["ODOR_B"])
|
||||
rows.append(pairrow)
|
||||
table = pd.DataFrame(rows); table.to_parquet(exp / "isolated-replay/mechanism-comparison.parquet", index=False)
|
||||
curve = []
|
||||
for (mechanism, pairing), frame in table.groupby(["mechanism", "pairings"]):
|
||||
curve.append({"mechanism": mechanism, "pairings": int(pairing), "seeds": int(len(frame)), "A_specific_functional_seeds": int(frame.A_specific_functional.sum()),
|
||||
"E_mean_changed": float(frame.E_mean_changed.mean()), "E_median_changed": float(frame.E_median_changed.mean()), "E_anatomical_weighted_changed": float(frame.E_anatomical_weighted_changed.mean()),
|
||||
"E_min_mask": float(frame.E_min_all_mask.min()), "effective_weight_reduction_percent_changed": float(frame.effective_weight_reduction_percent_changed.mean()),
|
||||
"A_transmission_reduction_percent": float(frame.A_transmitted_reduction_percent.mean()), "B_transmission_reduction_percent": float(frame.B_transmitted_reduction_percent.mean()),
|
||||
"A_total_KC_input_reduction_percent": float(frame.A_MBON_total_KC_input_reduction_percent.mean()), "B_total_KC_input_reduction_percent": float(frame.B_MBON_total_KC_input_reduction_percent.mean()),
|
||||
"A_MBON_delta_g_max_abs_mv": float(frame.A_delta_g_max_abs_mv.mean()), "A_MBON_delta_v_max_abs_mv": float(frame.A_delta_v_max_abs_mv.mean()),
|
||||
"A_MBON_spike_count_delta_mean": float(frame.A_MBON_spike_count_delta.mean()), "A_MBON_spike_L1_mean": float(frame.A_MBON_spike_L1.mean()),
|
||||
"A_MBON_first_latency_delta_ms_mean": float(frame.A_first_latency_delta_ms.mean()) if frame.A_first_latency_delta_ms.notna().any() else None,
|
||||
"B_MBON_spike_L1_mean": float(frame.B_MBON_spike_L1.mean()), "saturation_fraction_mask": float(frame.saturation_fraction_mask.mean())})
|
||||
gate = {}; majority = int(mcfg["gates"]["seed_majority"])
|
||||
for mech in MECHANISMS:
|
||||
successes = [x for x in curve if x["mechanism"] == mech and x["A_specific_functional_seeds"] >= majority]
|
||||
gate[mech] = {"pass": bool(successes), "first_passing_pairing": min((x["pairings"] for x in successes), default=None), "seed_majority": majority,
|
||||
"endpoint_100_specific_functional_seeds": next(x["A_specific_functional_seeds"] for x in curve if x["mechanism"] == mech and x["pairings"] == 100)}
|
||||
primary = gate["multiplicative"]["pass"] or gate["bounded_multiplicative"]["pass"]
|
||||
report = {"status": "PASS-KC-MBON-FUNCTIONAL-TRANSFER-v2" if primary else "FAIL-KC-MBON-FUNCTIONAL-TRANSFER-v2", "presynaptic_functional_transfer_pass": primary,
|
||||
"mechanism_gates": gate, "curve": curve, "same_KC_spikes_DAN_timing_mask_and_seeds": True, "phase4l_additive_reproduced": True,
|
||||
"postsynaptic_branch_run": False, "no_dense_time_by_edge_state": True,
|
||||
"performance": {"mean_training_brain_wall_seconds_per_pairing": float(np.mean(training_walls)), "mean_update_seconds_per_pairing": float(np.mean(update_times)),
|
||||
"mean_isolated_replay_seconds": float(np.mean(replay_times)), "E_overlay_memory_bytes_two_float32_arrays": 2 * len(v.edge_ids) * 4}}
|
||||
write_json(exp / "isolated-replay/summary.json", report)
|
||||
return report, checkpoints, replay
|
||||
|
||||
|
||||
def anatomy_vs_efficacy(v: Validator, sim: OdorSimulator, cfg: dict[str, Any], exp: Path, anatomical: np.ndarray,
|
||||
checkpoints: dict[int, dict[int, dict[str, np.ndarray]]], mcfg: dict[str, Any]) -> dict[str, Any]:
|
||||
targets = v.cand.target_neuron_idx.to_numpy(np.int32); base = v.base.detach().cpu().numpy(); positive = np.maximum(base, 0.0)
|
||||
target_total = np.bincount(v.rt.indices_np, weights=positive.astype(np.float64), minlength=v.n)
|
||||
frames = []
|
||||
for ts in map(int, mcfg["protocol"]["train_seeds"]):
|
||||
state = checkpoints[ts][100]
|
||||
for mechanism in ("multiplicative", "bounded_multiplicative"):
|
||||
E = state[mechanism]; frame = v.cand[["edge_id", "source_neuron_idx", "source_body_id", "target_neuron_idx", "target_body_id", "raw_synapse_count", "target_compartment_verbatim"]].copy()
|
||||
frame["train_seed"] = ts; frame["mechanism"] = mechanism; frame["anatomical_weight_mv"] = anatomical; frame["E_baseline"] = 1.0; frame["E_current"] = E
|
||||
frame["effective_weight_mv"] = anatomical * E; frame["target_total_positive_anatomical_input_mv"] = target_total[targets]
|
||||
frame["target_MBON_total_input_fraction_static"] = anatomical * E / np.maximum(target_total[targets], 1e-12)
|
||||
frame["eligibility_at_last_DAN"] = state["eligibility_at_last_DAN"]
|
||||
frame["DAN_channel"] = mcfg["protocol"]["matched_dan_type"]; frames.append(frame)
|
||||
out = pd.concat(frames, ignore_index=True); out.to_parquet(exp / "anatomy-vs-efficacy/anatomy-vs-efficacy.parquet", index=False)
|
||||
from scipy.stats import spearmanr
|
||||
by = out[(out.train_seed == int(mcfg["protocol"]["train_seeds"][0])) & (out.mechanism == "bounded_multiplicative")]
|
||||
rho = spearmanr(by.raw_synapse_count, by.effective_weight_mv).statistic
|
||||
report = {"rows": int(len(out)), "unique_edges": int(out.edge_id.nunique()), "raw_count_vs_effective_weight_spearman_seed1_bounded": float(rho),
|
||||
"high_anatomical_count_implies_high_current_effective_in_this_assumed_model_only": bool(rho > 0.9),
|
||||
"physiological_influence_inferred_from_EM_count": False, "static_fraction_is_not_dynamic_MBON_input": True}
|
||||
write_json(exp / "anatomy-vs-efficacy/summary.json", report); return report
|
||||
|
||||
|
||||
def run_wholebrain(v: Validator, sim: OdorSimulator, cfg: dict[str, Any], mcfg: dict[str, Any], exp: Path, panel: pd.DataFrame,
|
||||
vectors: dict[str, np.ndarray], selection: dict[str, Any], mapping: pd.DataFrame,
|
||||
anatomical: np.ndarray, checkpoints: dict[int, dict[int, dict[str, np.ndarray]]], isolated: dict[str, Any]) -> dict[str, Any]:
|
||||
passed = [x for x in ("multiplicative", "bounded_multiplicative") if isolated["mechanism_gates"][x]["pass"]]
|
||||
if not passed:
|
||||
out = {"status": "NOT-RUN-ISOLATED-ENDPOINT-GATE-BLOCKED", "tested_mechanisms": [], "feedback_masks_memory": None}; write_json(exp / "whole-brain-replay/summary.json", out); return out
|
||||
duration = float(mcfg["protocol"]["duration_ms"]); gloms = selection["glomeruli_order"]; matched = float(np.median(panel.mapped_response_sum)); pn = {}
|
||||
for label in ("ODOR_A", "ODOR_B"):
|
||||
oid = selection[label]["odor_id"]; pn[label] = pn_drive(oid, vectors[oid], mapping, gloms, str(mcfg["protocol"]["input_mode"]), matched, cfg)[:2]
|
||||
rows = []; walls = []; zero = np.zeros(len(v.edge_ids), np.float32)
|
||||
for ts, es in zip(map(int, mcfg["protocol"]["train_seeds"]), map(int, mcfg["protocol"]["evaluation_seeds"])):
|
||||
baseline = {label: sim.run_rates(*pn[label], es, duration, delta=zero) for label in pn}
|
||||
for mechanism in passed:
|
||||
E = checkpoints[ts][100][mechanism]; delta = anatomical * (E - 1)
|
||||
effects = {}
|
||||
for label in pn:
|
||||
post = sim.run_rates(*pn[label], es, duration, delta=delta); walls.append(post["wall_seconds"]); effects[label] = effect(baseline[label], post)
|
||||
rows.append({"train_seed": ts, "eval_seed": es, "mechanism": mechanism, "odor": label, "MBON_spike_L1": effects[label]["MBON_spike_l1"],
|
||||
"MBON_g_max_abs_mv": effects[label]["MBON_g_max_abs"], "MBON_v_max_abs_mv": effects[label]["MBON_v_max_abs"],
|
||||
"MBON_latency_delta_ms": effects[label]["MBON_first_latency_delta_ms"], "downstream_spike_L1": effects[label]["downstream_spike_l1"],
|
||||
"KC_spike_L1": int(np.abs(post["kc_counts"] - baseline[label]["kc_counts"]).sum()),
|
||||
"finite": bool(post["finite"]), "active_neuron_fraction": float(np.count_nonzero(post["counts"]) / v.n), "wall_seconds": post["wall_seconds"]})
|
||||
frame = pd.DataFrame(rows); frame.to_parquet(exp / "whole-brain-replay/results.parquet", index=False); gates = {}
|
||||
for mechanism in passed:
|
||||
sub = frame[frame.mechanism == mechanism]; a = sub[sub.odor == "ODOR_A"].sort_values("train_seed"); b = sub[sub.odor == "ODOR_B"].sort_values("train_seed")
|
||||
wins = int(((a.MBON_spike_L1.to_numpy() > b.MBON_spike_L1.to_numpy()) & (a.MBON_spike_L1.to_numpy() > 0)).sum())
|
||||
gates[mechanism] = {"A_specific_functional_seeds": wins, "pass": bool(wins >= int(mcfg["gates"]["seed_majority"]) and sub.finite.all() and sub.active_neuron_fraction.max() < .5),
|
||||
"A_MBON_spike_L1_mean": float(a.MBON_spike_L1.mean()), "B_MBON_spike_L1_mean": float(b.MBON_spike_L1.mean()),
|
||||
"A_downstream_spike_L1_mean": float(a.downstream_spike_L1.mean()), "B_downstream_spike_L1_mean": float(b.downstream_spike_L1.mean()),
|
||||
"A_KC_spike_L1_mean": float(a.KC_spike_L1.mean()), "B_KC_spike_L1_mean": float(b.KC_spike_L1.mean())}
|
||||
status = "PASS-WHOLE-BRAIN-KCMBON-TRANSFER" if any(x["pass"] for x in gates.values()) else "WHOLE-BRAIN-FEEDBACK-MASKS-MEMORY"
|
||||
out = {"status": status, "tested_mechanisms": passed, "gates": gates, "feedback_masks_memory": status != "PASS-WHOLE-BRAIN-KCMBON-TRANSFER", "mean_post_brain_wall_seconds": float(np.mean(walls))}
|
||||
write_json(exp / "whole-brain-replay/summary.json", out); return out
|
||||
|
||||
|
||||
def conditioning_gate(cfg: dict[str, Any], mcfg: dict[str, Any], exp: Path, isolated: dict[str, Any], whole: dict[str, Any], identity: dict[str, Any]) -> dict[str, Any]:
|
||||
encoder = json.loads((Path(cfg["paths"]["phase4l_root"]) / "kc-representation/summary.json").read_text()); temporal = json.loads((Path(cfg["paths"]["phase4j_root"]) / "eligibility-v2/unit-tests.json").read_text())
|
||||
gates = {"OlfactoryEncoder_v1": bool(encoder["candidate_generated"]), "TemporalEligibility_v2": bool(temporal["all_passed"]),
|
||||
"KC_sparse_code": bool(encoder["modes"]["physiological"]["gate"] and encoder["modes"]["matched_energy"]["gate"]),
|
||||
"A_B_specific_edges": bool(identity["A_specific_eligible_edges"] > 0 and identity["B_specific_eligible_edges"] > 0),
|
||||
"isolated_functional_transfer_v2": bool(isolated["presynaptic_functional_transfer_pass"]),
|
||||
"whole_brain_transfer": whole["status"] == "PASS-WHOLE-BRAIN-KCMBON-TRANSFER"}
|
||||
report = {"status": "OPEN" if all(gates.values()) else "NOT-RUN-GATE-BLOCKED", "gates": gates, "formal_conditioning_run": False,
|
||||
"acquisition": "NOT-RUN" if not all(gates.values()) else "PENDING", "controls": "NOT-RUN" if not all(gates.values()) else "PENDING"}
|
||||
write_json(exp / "conditioning/gate.json", report); return report
|
||||
|
||||
|
||||
def integrity(cfg: dict[str, Any], mcfg: dict[str, Any], base_config: Path, exp: Path, before_hash: dict[str, str]) -> dict[str, Any]:
|
||||
del mcfg
|
||||
gc.collect(); torch.cuda.empty_cache(); checks = frozen_checks(cfg); data = dataset_checks(cfg); regression = phase4c_regression(cfg, base_config, exp); after = frozen_hashes(cfg)
|
||||
report = {"all_passed": bool(checks["all_passed"] and data["all_passed"] and regression["all_passed"] and before_hash == after),
|
||||
"frozen_checks": checks, "phase1_to_3b_dataset": data, "phase4c_reference_regression": regression,
|
||||
"frozen_hashes_identical": before_hash == after, "before": before_hash, "after": after,
|
||||
"base_CSR_modified": False, "APL_modified": False, "eligibility_v2_modified": False, "plastic_mask_expanded": False,
|
||||
"OlfactoryEncoder_modified": False, "PlasticityAccumulation_modified": False}
|
||||
write_json(exp / "audit/integrity.json", report); return report
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True); args = ap.parse_args(); mcfg = yaml.safe_load(args.config.read_text()); base_config = Path(mcfg["frozen_phase4l_config"])
|
||||
cfg = yaml.safe_load(base_config.read_text()); cfg["paths"]["phase4l_root"] = cfg["paths"]["experiment_root"]; cfg["paths"]["phase4l_model_root"] = mcfg["frozen_phase4l_config"].rsplit("/", 1)[0]
|
||||
exp, model = Path(mcfg["experiment_root"]), Path(mcfg["model_root"])
|
||||
for sub in ("literature-audit", "anatomical-weight", "efficacy-overlay", "presynaptic", "postsynaptic", "anatomy-vs-efficacy", "isolated-replay", "whole-brain-replay", "conditioning", "controls", "performance", "regression", "audit", "manifests", "compiler-reports/extension-build"):
|
||||
(exp / sub).mkdir(parents=True, exist_ok=True)
|
||||
model.mkdir(parents=True, exist_ok=True)
|
||||
if not (exp / "literature-audit/phase4m_literature_audit.json").is_file(): raise RuntimeError("literature audit must predate model implementation")
|
||||
prior = frozen_checks(cfg); before_hash = frozen_hashes(cfg); write_json(exp / "audit/frozen-before.json", {"checks": prior, "hashes": before_hash})
|
||||
shutil.copy2(args.config, exp / "audit/phase4m.yaml"); shutil.copy2(Path(__file__).resolve(), exp / "audit/phase4m.py")
|
||||
panel, vectors, selection, mapping = load_odor_inputs(cfg); v = Validator(cfg, base_config, exp); sim = OdorSimulator(v, cfg, exp); raw, anatomical = anatomical_audit(v, cfg, exp)
|
||||
isolated, checkpoints, replay = main_experiment(v, sim, cfg, mcfg, exp, panel, vectors, selection, mapping, anatomical)
|
||||
anatomy = anatomy_vs_efficacy(v, sim, cfg, exp, anatomical, checkpoints, mcfg)
|
||||
post = {"status": mcfg["postsynaptic"]["current_status"], "branch_defined": True, "run": False,
|
||||
"reason": "postsynaptic nAChR evidence exists in adult Drosophila, but a quantitative rule for exact MaleCNS y5B'2a target and its receptor map is unavailable",
|
||||
"combined_presynaptic_postsynaptic_run": False, "global_MBON_threshold_modified": False}
|
||||
write_json(exp / "postsynaptic/summary.json", post)
|
||||
whole = run_wholebrain(v, sim, cfg, mcfg, exp, panel, vectors, selection, mapping, anatomical, checkpoints, isolated)
|
||||
edge_identity = json.loads((Path(cfg["paths"]["phase4l_root"]) / "eligible-edges/summary.json").read_text())
|
||||
formal = conditioning_gate(cfg, mcfg, exp, isolated, whole, edge_identity)
|
||||
# Formal conditioning implementation is intentionally gated separately: this
|
||||
# diagnostic run never creates a memory claim from the efficacy comparison.
|
||||
if formal["status"] == "OPEN":
|
||||
formal["status"] = "NOT-RUN-IMPLEMENTATION-REQUIRES-CONTROLLED-FORMAL-PROTOCOL"
|
||||
formal["formal_conditioning_run"] = False
|
||||
formal["acquisition"] = "NOT-RUN"
|
||||
formal["controls"] = "NOT-RUN"
|
||||
write_json(exp / "conditioning/gate.json", formal)
|
||||
controls = {"isolated_neutral_B": "RUN", "matched_seed_same_KC_spikes": "RUN", "D_zero": "analytic identity E=1, no dopamine update", "random_reward": "NOT-RUN-FORMAL-CONDITIONING-BLOCKED", "unrelated_DAN": "NOT-RUN-FORMAL-CONDITIONING-BLOCKED", "frozen_no_plasticity": "isolated E=1 reference replay"}
|
||||
write_json(exp / "controls/summary.json", controls)
|
||||
checkpoints_bytes = sum(p.stat().st_size for p in (exp / "efficacy-overlay").glob("E-seed-*.npz")); performance = isolated["performance"] | {"overlay_checkpoint_bytes_total": checkpoints_bytes,
|
||||
"peak_RAM_kib": int(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss), "peak_VRAM_bytes": int(torch.cuda.max_memory_allocated()),
|
||||
"whole_brain_mean_post_wall_seconds": whole.get("mean_post_brain_wall_seconds"), "dense_time_by_edge_state": False}
|
||||
write_json(exp / "performance/summary.json", performance)
|
||||
candidates = {"KCMBON-SynapticEfficacy-v1": True, "PresynapticEfficacy-v1": True,
|
||||
"KCMBON-FunctionalTransfer-v2": bool(isolated["presynaptic_functional_transfer_pass"]), "MaleCNS-OlfactoryAssociativeMemory-v1-candidate": False}
|
||||
del sim, v, replay, checkpoints; integrity_report = integrity(cfg, mcfg, base_config, exp, before_hash)
|
||||
summary = {"phase": "4M", "literature_audit": "literature-audit/phase4m_literature_audit.json", "E_math": mcfg["mechanisms"], "anatomical": json.loads((exp / "anatomical-weight/summary.json").read_text()),
|
||||
"isolated": isolated, "postsynaptic": post, "anatomy_vs_efficacy": anatomy, "whole_brain": whole, "conditioning": formal, "controls": controls,
|
||||
"performance": performance, "candidates": candidates, "integrity": integrity_report, "phase4n_started": False}
|
||||
write_json(exp / "audit/final-summary.json", summary); shutil.copy2(args.config, model / "phase4m.yaml"); shutil.copy2(Path(__file__).resolve(), model / "phase4m.py"); shutil.copy2(exp / "literature-audit/phase4m_literature_audit.json", model / "literature-audit.json")
|
||||
write_json(model / "VERSION.json", {"name": mcfg["name"], "candidates": candidates, "summary_sha256": sha256(exp / "audit/final-summary.json"), "base_CSR_read_only": True})
|
||||
seal(model, model / "MANIFEST.json", model / "checksums.sha256"); seal(exp, exp / "manifests/manifest.json", exp / "manifests/checksums.sha256")
|
||||
print(json.dumps({"phase": "4M", "isolated": isolated["status"], "whole_brain": whole["status"], "conditioning": formal["status"], "integrity": integrity_report["all_passed"]}, indent=2)); sys.stdout.flush()
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,59 @@
|
||||
phase: 4M
|
||||
name: KCMBON-SynapticEfficacy-v1
|
||||
frozen_phase4l_config: /data/digital-drosophila/models/male-cns-lif/olfactory-grounding-v1/phase4l.yaml
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4m-kcmbon-efficacy
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif/kcmbon-efficacy-v1
|
||||
|
||||
mechanisms:
|
||||
additive:
|
||||
label: Phase4L-additive-delta-weight-baseline
|
||||
update: frozen Phase4J apply_v2, including its pre-existing 25 percent per-edge floor
|
||||
multiplicative:
|
||||
label: PresynapticEfficacy-v1
|
||||
update: E_new = max(0, E_old * (1 - eta_effective_mv / W_syn_mv_per_contact * D * eligibility))
|
||||
interpretation: engineering per-anatomical-contact efficacy conversion, not measured release probability
|
||||
bounded_multiplicative:
|
||||
label: KCMBON-SynapticEfficacy-v1
|
||||
update: E_new = max(0.75, E_old * (1 - eta_effective_mv / W_syn_mv_per_contact * D * eligibility))
|
||||
lower_bound_provenance: frozen Phase4J max_fractional_depression 0.25
|
||||
|
||||
protocol:
|
||||
pairing_counts: [1, 2, 5, 10, 20, 50, 100]
|
||||
train_seeds: [51201, 51202, 51203, 51204, 51205]
|
||||
evaluation_seeds: [51301, 51302, 51303, 51304, 51305]
|
||||
target_compartment_verbatim: "y5B'2a"
|
||||
matched_dan_type: PAM15
|
||||
unrelated_compartment_verbatim: "y2a'1"
|
||||
unrelated_dan_type: PPL103
|
||||
dan_time_ms: 90.0
|
||||
duration_ms: 120.0
|
||||
formal_training_pairings: 100
|
||||
forced_odor: ODOR_A
|
||||
neutral_odor: ODOR_B
|
||||
input_mode: physiological
|
||||
no_random_terminal_heterogeneity: true
|
||||
|
||||
gates:
|
||||
seed_majority: 3
|
||||
functional_direction: MBON spike decrease or first-spike latency increase in A; B must not show equal-or-greater change
|
||||
selection: evaluate every predeclared pairing count; formal whole-brain test uses the predeclared 100-pairing endpoint only
|
||||
no_functional_gate_on_g_alone: true
|
||||
|
||||
postsynaptic:
|
||||
branch_name: MBON-PostsynapticPlasticity-v1
|
||||
current_status: AUDIT-ONLY-NOT-RUN-COMPARTMENT-SPECIFIC-QUANTITATIVE-RULE-UNKNOWN
|
||||
never_modify_global_threshold: true
|
||||
|
||||
prohibitions:
|
||||
change_base_CSR: false
|
||||
change_edge_existence: false
|
||||
change_raw_synapse_count: false
|
||||
change_global_Wsyn: false
|
||||
change_transmitter_sign: false
|
||||
change_LIF: false
|
||||
change_APL: false
|
||||
change_eligibility_v2: false
|
||||
change_olfactory_encoder: false
|
||||
expand_plastic_mask: false
|
||||
tune_from_phase4k_5_percent: false
|
||||
formal_conditioning_before_transfer_gates: false
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Seal Phase 4M after run-log closure and qualify whole-brain causal language."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yaml
|
||||
|
||||
from phase4h_audit import sha256, write_json
|
||||
from phase4k import seal
|
||||
from phase4m import frozen_checks, frozen_hashes
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(); parser.add_argument("--config", type=Path, required=True); args = parser.parse_args(); mcfg = yaml.safe_load(args.config.read_text())
|
||||
bpath = Path(mcfg["frozen_phase4l_config"]); cfg = yaml.safe_load(bpath.read_text()); cfg["paths"]["phase4l_root"] = cfg["paths"]["experiment_root"]; cfg["paths"]["phase4l_model_root"] = str(bpath.parent)
|
||||
exp, model = Path(mcfg["experiment_root"]), Path(mcfg["model_root"]); summary_path = exp / "audit/final-summary.json"; summary = json.loads(summary_path.read_text())
|
||||
if not summary["integrity"]["all_passed"]: raise RuntimeError("Phase 4M completion integrity was not PASS")
|
||||
checks = frozen_checks(cfg)
|
||||
if frozen_hashes(cfg) != summary["integrity"]["after"]: raise RuntimeError("frozen input changed after Phase 4M run")
|
||||
whole = summary["whole_brain"]; whole["operational_gate_label_only"] = True
|
||||
whole["feedback_causally_isolated_by_this_experiment"] = False
|
||||
whole["interpretation"] = ("The prescribed WHOLE-BRAIN-FEEDBACK-MASKS-MEMORY label denotes isolated transfer PASS with whole-brain A/B specificity FAIL. "
|
||||
"It does not prove feedback is the unique cause; shared plastic edges and baseline network input may also contribute.")
|
||||
frame = pd.read_parquet(exp / "whole-brain-replay/results.parquet")
|
||||
whole["endpoint_100_KC_recruitment_change"] = {mech: {odor: float(part.KC_spike_L1.mean()) for odor, part in group.groupby("odor")}
|
||||
for mech, group in frame.groupby("mechanism")}
|
||||
|
||||
edge = pd.read_parquet(Path(cfg["paths"]["phase4l_root"]) / "eligible-edges/edge-identity.parquet")
|
||||
categories = edge.odor_eligibility_category.astype(str).to_numpy(); static = {}; seed_rows = []
|
||||
for seed in map(int, mcfg["protocol"]["train_seeds"]):
|
||||
data = np.load(exp / "efficacy-overlay" / f"E-seed-{seed}-pairings-100.npz")
|
||||
for mech, key in (("multiplicative", "E_multiplicative"), ("bounded_multiplicative", "E_bounded_multiplicative")):
|
||||
E = data[key]
|
||||
for cat in ("A-specific", "B-specific", "shared", "inactive"):
|
||||
sel = categories == cat; seed_rows.append({"seed": seed, "mechanism": mech, "edge_category": cat,
|
||||
"changed_edges": int(np.count_nonzero(E[sel] < 1 - 1e-7)), "E_reduction_mean": float((1 - E[sel]).mean()) if sel.any() else 0.0,
|
||||
"E_reduction_sum": float((1 - E[sel]).sum())})
|
||||
data.close()
|
||||
audit = pd.DataFrame(seed_rows); audit.to_parquet(exp / "whole-brain-replay/endpoint-E-identity.parquet", index=False)
|
||||
for mech, group in audit.groupby("mechanism"):
|
||||
static[mech] = {cat: {"changed_edges_mean": float(g.changed_edges.mean()), "E_reduction_mean": float(g.E_reduction_mean.mean()),
|
||||
"E_reduction_sum_mean": float(g.E_reduction_sum.mean())} for cat, g in group.groupby("edge_category")}
|
||||
whole["endpoint_100_E_identity"] = static
|
||||
whole["direct_feedback_vs_shared_edge_causal_disambiguation"] = "NOT-ESTABLISHED; no feedback edge ablation was authorized"
|
||||
write_json(exp / "whole-brain-replay/summary.json", whole); summary["whole_brain"] = whole
|
||||
|
||||
iso = summary["isolated"]; iso["functional_transfer_gate_scope"] = "PASS at one or more predeclared pairing checkpoints, not necessarily retained at 100 pairings"
|
||||
iso["endpoint_100_majority_pass"] = {mech: bool(gate["endpoint_100_specific_functional_seeds"] >= int(mcfg["gates"]["seed_majority"]))
|
||||
for mech, gate in iso["mechanism_gates"].items()}
|
||||
iso["nonmonotonic_functional_output_despite_monotonic_E_depression"] = True
|
||||
write_json(exp / "isolated-replay/summary.json", iso); summary["isolated"] = iso
|
||||
summary["integrity"]["postrun_frozen_recheck_passed"] = checks["all_passed"]
|
||||
write_json(summary_path, summary)
|
||||
version = json.loads((model / "VERSION.json").read_text()); version["summary_sha256"] = sha256(summary_path); write_json(model / "VERSION.json", version)
|
||||
write_json(exp / "audit/post-run-qualification.json", {"only_phase4m_reports_edited": True, "brain_rerun": False,
|
||||
"run_log_sealed_after_process_exit": True, "whole_brain_feedback_label_is_operational_not_causal_proof": True,
|
||||
"frozen_recheck_passed": checks["all_passed"]})
|
||||
shutil.copy2(Path(__file__).resolve(), exp / "audit/phase4m_finalize.py"); shutil.copy2(Path(__file__).resolve(), model / "phase4m_finalize.py")
|
||||
seal(model, model / "MANIFEST.json", model / "checksums.sha256")
|
||||
seal(exp, exp / "manifests/manifest.json", exp / "manifests/checksums.sha256")
|
||||
for root, checksum in ((model, "checksums.sha256"), (exp, "manifests/checksums.sha256")):
|
||||
check = subprocess.run(["sha256sum", "-c", checksum], cwd=root, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
|
||||
if check.returncode: raise RuntimeError(f"self-checksum failure {root}: {check.stderr[-1000:]}")
|
||||
print(json.dumps({"frozen": checks["all_passed"], "model_SHA256": "PASS", "experiment_SHA256": "PASS",
|
||||
"endpoint_100_isolated_majority": iso["endpoint_100_majority_pass"]}))
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Paired read-only 120-ms baseline versus efficacy-overlay runtime timing."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
from phase4h_audit import sha256, write_json
|
||||
from phase4i import Validator
|
||||
from phase4k import seal
|
||||
from phase4l import OdorSimulator, pn_drive
|
||||
from phase4m import frozen_checks, frozen_hashes, load_odor_inputs
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(); ap.add_argument("--config", type=Path, required=True); args = ap.parse_args(); mcfg = yaml.safe_load(args.config.read_text()); bpath = Path(mcfg["frozen_phase4l_config"]); cfg = yaml.safe_load(bpath.read_text()); cfg["paths"]["phase4l_root"] = cfg["paths"]["experiment_root"]; cfg["paths"]["phase4l_model_root"] = str(bpath.parent)
|
||||
exp, model = Path(mcfg["experiment_root"]), Path(mcfg["model_root"]); summary_path = exp / "audit/final-summary.json"; summary = json.loads(summary_path.read_text()); before = frozen_hashes(cfg)
|
||||
panel, vectors, selection, mapping = load_odor_inputs(cfg); v = Validator(cfg, bpath, exp); sim = OdorSimulator(v, cfg, exp)
|
||||
oid = selection["ODOR_A"]["odor_id"]; targets, rates, _ = pn_drive(oid, vectors[oid], mapping, selection["glomeruli_order"], str(mcfg["protocol"]["input_mode"]), float(np.median(panel.mapped_response_sum)), cfg)
|
||||
duration = float(mcfg["protocol"]["duration_ms"]); zero = np.zeros(len(v.edge_ids), np.float32); records = []
|
||||
sim.run_rates(targets, rates, 51999, duration, delta=zero)
|
||||
for ts, es in zip(map(int, mcfg["protocol"]["train_seeds"]), map(int, mcfg["protocol"]["evaluation_seeds"])):
|
||||
with np.load(exp / "efficacy-overlay" / f"E-seed-{ts}-pairings-100.npz") as state:
|
||||
E = state["E_bounded_multiplicative"].copy()
|
||||
delta = v.base_candidate.astype(np.float32) * (E - 1.0)
|
||||
for label, weights in (("baseline", zero), ("bounded_E100", delta)):
|
||||
t0 = time.perf_counter(); run = sim.run_rates(targets, rates, es, duration, delta=weights); total = time.perf_counter() - t0
|
||||
records.append({"train_seed": ts, "eval_seed": es, "condition": label, "brain_loop_seconds": run["wall_seconds"],
|
||||
"total_call_seconds_including_overlay_install_and_PN_generation": total,
|
||||
"PN_event_generation_seconds": run["pn_generation_wall_seconds"], "APL_submit_seconds": run["overlay_cpu_submit_seconds"],
|
||||
"peak_VRAM_bytes": run["peak_vram_bytes"], "finite": run["finite"]})
|
||||
frame = pd.DataFrame(records); frame.to_parquet(exp / "performance/paired-overlay-runtime.parquet", index=False); base = frame[frame.condition == "baseline"].sort_values("train_seed"); post = frame[frame.condition == "bounded_E100"].sort_values("train_seed")
|
||||
loop_overhead = post.brain_loop_seconds.to_numpy() - base.brain_loop_seconds.to_numpy(); total_overhead = post.total_call_seconds_including_overlay_install_and_PN_generation.to_numpy() - base.total_call_seconds_including_overlay_install_and_PN_generation.to_numpy()
|
||||
report = {"biological_duration_ms": duration, "trial": "frozen ODOR_A 120 ms, paired evaluation seed, no dopamine or learning",
|
||||
"baseline_loop_seconds_mean": float(base.brain_loop_seconds.mean()), "bounded_E100_loop_seconds_mean": float(post.brain_loop_seconds.mean()),
|
||||
"loop_overhead_seconds_mean": float(loop_overhead.mean()), "loop_overhead_percent_mean": float(100 * np.mean(loop_overhead / base.brain_loop_seconds.to_numpy())),
|
||||
"baseline_total_call_seconds_mean": float(base.total_call_seconds_including_overlay_install_and_PN_generation.mean()),
|
||||
"bounded_E100_total_call_seconds_mean": float(post.total_call_seconds_including_overlay_install_and_PN_generation.mean()),
|
||||
"total_call_overhead_seconds_mean": float(total_overhead.mean()), "mean_peak_VRAM_bytes": float(frame.peak_VRAM_bytes.mean()),
|
||||
"five_paired_seeds": True, "numeric_results_not_reused_for_gates": True, "all_finite": bool(frame.finite.all())}
|
||||
write_json(exp / "performance/paired-overlay-summary.json", report); summary["performance"]["paired_overlay_runtime"] = report
|
||||
write_json(exp / "performance/summary.json", summary["performance"]); write_json(summary_path, summary)
|
||||
version = json.loads((model / "VERSION.json").read_text()); version["summary_sha256"] = sha256(summary_path); write_json(model / "VERSION.json", version)
|
||||
shutil.copy2(Path(__file__).resolve(), exp / "audit/phase4m_performance.py"); shutil.copy2(Path(__file__).resolve(), model / "phase4m_performance.py")
|
||||
if frozen_hashes(cfg) != before or not frozen_checks(cfg)["all_passed"]: raise RuntimeError("frozen integrity failed after performance audit")
|
||||
seal(model, model / "MANIFEST.json", model / "checksums.sha256"); seal(exp, exp / "manifests/manifest.json", exp / "manifests/checksums.sha256")
|
||||
for root, checksum in ((model, "checksums.sha256"), (exp, "manifests/checksums.sha256")):
|
||||
p = subprocess.run(["sha256sum", "-c", checksum], cwd=root, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True)
|
||||
if p.returncode: raise RuntimeError(f"Phase 4M self checksum failed for {root}: {p.stderr[-1000:]}")
|
||||
print(json.dumps({"performance": report, "model_and_experiment_SHA256": "PASS"}, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__": main()
|
||||
@@ -0,0 +1,4 @@
|
||||
2c0c2eaf793b7f89eb8b1f606bc3878f16f101325818c5e6fe2b690eefdfb9a8 phase4w.py
|
||||
cc09961271f715c4e6cd6b05fd5d3a03e1ac33d00d58daed48790f0a4b358e5f phase4w.yaml
|
||||
1c52ef6b47263f7b9edb9ffa7fa7096fbd5b1b8c3a4ca08298b91650942ccfea phase4w_describe.py
|
||||
90ce423ac700cbaec0bffec9d81b57f6d1f5cdc9437a9b766406514cbc80008e manifest.json
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"bytes": 19253,
|
||||
"path": "phase4w.py",
|
||||
"sha256": "2c0c2eaf793b7f89eb8b1f606bc3878f16f101325818c5e6fe2b690eefdfb9a8"
|
||||
},
|
||||
{
|
||||
"bytes": 1416,
|
||||
"path": "phase4w.yaml",
|
||||
"sha256": "cc09961271f715c4e6cd6b05fd5d3a03e1ac33d00d58daed48790f0a4b358e5f"
|
||||
},
|
||||
{
|
||||
"bytes": 3881,
|
||||
"path": "phase4w_describe.py",
|
||||
"sha256": "1c52ef6b47263f7b9edb9ffa7fa7096fbd5b1b8c3a4ca08298b91650942ccfea"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
"""Side-channel event/drive audit. Frozen run function and CUDA math unchanged."""
|
||||
from pathlib import Path
|
||||
import argparse,json,time
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import yaml
|
||||
import phase4p,phase4p_finalize,phase4s,phase4v
|
||||
from phase4p_revalidate import DeterministicAPLSim,door_inputs,make_drive
|
||||
from phase4r import save,digest,arrhash
|
||||
from phase4h_audit import verify_checksum
|
||||
|
||||
CATS=['PN_olfactory','KC','APL','MBON','DAN','other']
|
||||
|
||||
def checks(cfg):
|
||||
vc=yaml.safe_load((Path(cfg['prior_model'])/'phase4v.yaml').read_text())
|
||||
out=phase4v.checks(vc)
|
||||
for key in ('prior_root','prior_model'):
|
||||
p=Path(cfg[key]);out['V_'+key]=verify_checksum(p,p/'checksums.sha256')
|
||||
assert all(x['passed'] for x in out.values());return out
|
||||
|
||||
class Observer:
|
||||
def __init__(self,rt,watch):
|
||||
self.rt=rt;self.ext=rt.ext;self.fanout=rt.canonical_fanout
|
||||
self.watch=np.asarray(watch,np.int32);self.wg=torch.as_tensor(watch,dtype=torch.int64,device=rt.device)
|
||||
self.states=[];self.steps=[];self.nodes=[];self.forced=[]
|
||||
def __getattr__(self,name):return getattr(self.ext,name)
|
||||
def state_step(self,*a):
|
||||
w=self.wg
|
||||
pre=torch.stack((a[0][w],a[1][w],a[2][w].float(),a[3][w]))
|
||||
self.ext.state_step(*a)
|
||||
post=torch.stack((a[0][w],a[1][w],a[4][w].float()))
|
||||
self.states.append(torch.cat((pre,post)).cpu().numpy())
|
||||
def force_step(self,*a):
|
||||
tg=a[4];req=a[5].bool()
|
||||
accepted=req & ~a[2][tg].bool() & (a[1][tg]==0)
|
||||
self.ext.force_step(*a)
|
||||
x=tg[accepted].cpu().numpy().astype(np.int32)
|
||||
if len(x):self.forced.extend((self.rt.step_index,int(i)) for i in x)
|
||||
def record_fanout(self,src,dst):
|
||||
x=src.cpu().numpy().astype(np.int32)
|
||||
self.nodes.append(x);self.steps.append(np.full(len(x),self.rt.step_index,np.int32))
|
||||
return self.fanout(src,dst)
|
||||
def attach(self):self.rt.ext=self;self.rt.canonical_fanout=self.record_fanout
|
||||
def detach(self):self.rt.ext=self.ext;self.rt.canonical_fanout=self.fanout
|
||||
def events(self):
|
||||
return pd.DataFrame({'step':np.concatenate(self.steps),'neuron_idx':np.concatenate(self.nodes)})
|
||||
|
||||
def classify(meta):
|
||||
cls=meta.annotation_class.fillna('').astype(str);typ=meta.annotation_type.fillna('').astype(str)
|
||||
nt=meta.neurotransmitter.fillna('').astype(str).str.strip().str.lower()
|
||||
code=np.full(len(meta),5,np.int8)
|
||||
code[cls.isin(['ALPN','olfactory'])]=0;code[cls.eq('Kenyon_Cell')]=1
|
||||
code[typ.eq('APL')]=2;code[cls.eq('MBON')]=3;code[nt.eq('dopamine')]=4
|
||||
return code
|
||||
|
||||
def replay(arrival,overlay,rt):
|
||||
# Explicit rounding matches CUDA __fadd_rn/__fmul_rn, not fused operations.
|
||||
f=np.float32;v=f(rt.rest);g=f(0);ref=0;out=[];vs=[];gs=[]
|
||||
for step,a in enumerate(arrival):
|
||||
g=f(f(g*rt.gd)+a);ref=max(ref-1,0)
|
||||
v=f(v+f(rt.alpha*f(g-f(v-rt.rest)))) if ref==0 else f(rt.resetv)
|
||||
fire=ref==0 and v>=rt.threshold
|
||||
if fire:v=f(rt.resetv);ref=rt.ref_steps;out.append(step)
|
||||
vs.append(v);gs.append(g)
|
||||
# Measured applied APL difference, fixed exogenous signal for isolated replay.
|
||||
g=f(np.float64(g)+overlay[step])
|
||||
return np.asarray(out,np.int32),np.asarray(vs,np.float32),np.asarray(gs,np.float32),g
|
||||
|
||||
def drive_audit(cfg,exp,name,sim,observer,result,edges,code):
|
||||
rt=sim.rt;st=np.asarray(observer.states);T=len(st);events=observer.events()
|
||||
assert np.array_equal(np.bincount(events.neuron_idx,minlength=sim.n),result['counts'])
|
||||
events.to_parquet(exp/'events'/f'{name}-whole-brain.parquet',index=False)
|
||||
pd.DataFrame(observer.forced,columns=['step','neuron_idx']).to_parquet(exp/'events'/f'{name}-accepted-input.parquet',index=False)
|
||||
np.savez_compressed(exp/'states'/f'{name}-watched.npz',state=st,
|
||||
fields=np.asarray(['pre_v','pre_g','pre_ref','arrival','post_v','post_g','natural_spike']))
|
||||
# Only real events on actual incoming edges. Delay is exact, including inhibitory edges.
|
||||
arrivals=events.merge(edges,left_on='neuron_idx',right_on='source_neuron_idx',how='inner')
|
||||
arrivals=arrivals.rename(columns={'step':'emission_step'}).drop(columns='neuron_idx')
|
||||
arrivals['arrival_step']=arrivals.emission_step+rt.delay_steps
|
||||
arrivals=arrivals[arrivals.arrival_step<T].sort_values(['target_neuron_idx','arrival_step','edge_id'],kind='stable')
|
||||
arrivals.to_parquet(exp/'drive'/f'{name}-actual-edge-arrivals.parquet',index=False)
|
||||
replayrows=[];drives=[];checks_out=[]
|
||||
for wi,node in enumerate(cfg['watch_neuron_idx']):
|
||||
a=arrivals[arrivals.target_neuron_idx==node]
|
||||
seqs={int(t):g for t,g in a.groupby('arrival_step',sort=False)}
|
||||
exact_arr=np.zeros(T,np.float32);bycat=np.zeros((T,len(CATS)),np.float64)
|
||||
for t,g in seqs.items():
|
||||
weights=g.effective_weight.to_numpy(np.float32)
|
||||
exact_arr[t]=np.cumsum(weights,dtype=np.float32)[-1]
|
||||
for c,q in g.groupby('source_category'):
|
||||
bycat[t,CATS.index(c)]=q.effective_weight.to_numpy(np.float64).sum()
|
||||
queue_exact=bool(np.array_equal(exact_arr,st[:,3,wi]));assert queue_exact,'Arrival reconstruction mismatch'
|
||||
after_overlay=np.r_[st[1:,1,wi],result['final_g'][node]]
|
||||
overlay=after_overlay.astype(np.float64)-st[:,5,wi].astype(np.float64)
|
||||
base=replay(exact_arr,overlay,rt)
|
||||
expected=np.flatnonzero(st[:,6,wi]).astype(np.int32)
|
||||
eq={'queue_exact':queue_exact,'events_exact':np.array_equal(base[0],expected),
|
||||
'V_exact':np.array_equal(base[1],st[:,4,wi]),'g_exact':np.array_equal(base[2],st[:,5,wi]),
|
||||
'final_g_exact':base[3]==result['final_g'][node]}
|
||||
checks_out.append({'neuron_idx':node,**{k:bool(v) for k,v in eq.items()}});assert all(eq.values()),'CPU replay divergence'
|
||||
shadow=np.zeros(len(CATS)+1,np.float64);shadowrows=[]
|
||||
for t in range(T):
|
||||
shadow*=float(rt.gd);shadow[:len(CATS)]+=bycat[t]
|
||||
row={'step':t,'neuron_idx':node,'actual_g_before_APL':float(st[t,5,wi]),
|
||||
'shadow_residual':float(st[t,5,wi]-shadow.sum())}
|
||||
row.update({f'g_{c}':float(shadow[j]) for j,c in enumerate(CATS+['APL_overlay'])})
|
||||
shadowrows.append(row);shadow[-1]+=overlay[t]
|
||||
pd.DataFrame(shadowrows).to_parquet(exp/'drive'/f'{name}-{node}-shadow.parquet',index=False)
|
||||
for mode in cfg['isolated_replay']['modes']:
|
||||
ar=exact_arr.copy();ov=overlay.copy()
|
||||
if mode=='omit_APL_overlay':ov[:]=0
|
||||
elif mode.startswith('omit_'):
|
||||
cat=mode[5:];ar[:]=0
|
||||
for t,g in seqs.items():
|
||||
w=g.loc[g.source_category!=cat,'effective_weight'].to_numpy(np.float32)
|
||||
if len(w):ar[t]=np.cumsum(w,dtype=np.float32)[-1]
|
||||
rr=replay(ar,ov,rt);late=rr[0]>=round(cfg['late_start_ms']/rt.dt)
|
||||
replayrows.append({'trial':name,'neuron_idx':node,'mode':mode,'spikes':len(rr[0]),'late_spikes':int(late.sum()),
|
||||
'first_ms':float(rr[0][0]*rt.dt) if len(rr[0]) else None,'last_ms':float(rr[0][-1]*rt.dt) if len(rr[0]) else None,
|
||||
'max_abs_V_difference':float(np.abs(rr[1].astype(float)-base[1]).max())})
|
||||
np.savez_compressed(exp/'replay'/f'{name}-{node}-{mode}.npz',event_steps=rr[0],v=rr[1],g=rr[2])
|
||||
for period,minstep in [('whole',0),('late',round(cfg['late_start_ms']/rt.dt))]:
|
||||
aa=a[a.arrival_step>=minstep]
|
||||
for cat,g in aa.groupby('source_category'):
|
||||
w=g.effective_weight.to_numpy(float)
|
||||
drives.append({'trial':name,'neuron_idx':node,'period':period,'source_category':cat,
|
||||
'positive_arrival_sum':float(w[w>0].sum()),'negative_arrival_sum':float(w[w<0].sum()),
|
||||
'edge_event_count':len(g),'unique_sources':int(g.source_neuron_idx.nunique())})
|
||||
return checks_out,replayrows,drives,events
|
||||
|
||||
def ancestry(cfg,sim,events,forced,csc,weights,code,name):
|
||||
rt=sim.rt;order,ptr=csc;by=events.sort_values(['neuron_idx','step'],kind='stable')
|
||||
times=by.step.to_numpy(np.int32);ep=np.r_[0,np.cumsum(np.bincount(by.neuron_idx,minlength=sim.n))]
|
||||
roots=set(forced);rows=[];summaries=[];gd=float(rt.gd)
|
||||
def node_info(node,prefix):
|
||||
m=sim.meta.iloc[node]
|
||||
return {prefix+'body_id':int(m.body_id),prefix+'type':None if pd.isna(m.annotation_type) else str(m.annotation_type),
|
||||
prefix+'class':None if pd.isna(m.annotation_class) else str(m.annotation_class),prefix+'category':CATS[code[node]]}
|
||||
for target in cfg['watch_neuron_idx']:
|
||||
tt=times[ep[target]:ep[target+1]]
|
||||
selected=[('first',int(tt[0]))] if len(tt) else []
|
||||
late=tt[tt>=round(cfg['late_start_ms']/rt.dt)]
|
||||
if len(late):selected.append(('first_late',int(late[0])))
|
||||
for kind,t in selected:
|
||||
node=target;path=[];status='hop_limit'
|
||||
for hop in range(cfg['ancestry']['max_hops']):
|
||||
if (t,node) in roots:status='accepted_input_root';break
|
||||
ee=order[ptr[node]:ptr[node+1]];ee=ee[weights[ee]>0]
|
||||
best=None
|
||||
for e in ee:
|
||||
src=int(sim.sources[e]);ts=times[ep[src]:ep[src+1]];j=np.searchsorted(ts,t-rt.delay_steps,side='right')-1
|
||||
if j<0:continue
|
||||
emit=int(ts[j]);arrival=emit+rt.delay_steps;score=float(weights[e])*gd**(t-arrival)
|
||||
if best is None or score>best[0]:best=(score,int(e),src,emit,arrival)
|
||||
if best is None:status='unexplained_no_positive_event_predecessor';break
|
||||
score,e,src,emit,arrival=best
|
||||
row={'trial':name,'watched_neuron_idx':target,'endpoint':kind,'backward_hop':hop,
|
||||
'source_neuron_idx':src,'target_neuron_idx':node,'source_step':emit,'arrival_step':arrival,'target_step':t,
|
||||
'edge_id':e,'raw_synapse_count':int(rt.raw_np[e]),'effective_weight':float(weights[e]),'synaptic_sign':1,
|
||||
'ranking_score':score,'arrival_age_ms':(t-arrival)*rt.dt,**node_info(src,'source_'),**node_info(node,'target_')}
|
||||
assert emit<t and arrival<=t
|
||||
path.append(row);node=src;t=emit
|
||||
rows.extend(path)
|
||||
summaries.append({'trial':name,'neuron_idx':target,'endpoint':kind,'hops':len(path),'status':status,
|
||||
'root_node':node,'root_step':t,'categories':sorted(set(x['source_category'] for x in path)),
|
||||
'claim':'ranked time-supported ancestry only; not unique cause or whole-brain necessity'})
|
||||
return rows,summaries
|
||||
|
||||
def run(cfg,exp,model):
|
||||
for d in ('protocol','audit','events','states','drive','replay','ancestry','regression','performance'):(exp/d).mkdir(parents=True,exist_ok=True)
|
||||
assert not (exp/'protocol/preregistered.json').exists(),'Refusing overwrite'
|
||||
save(exp/'audit/preflight.json',checks(cfg))
|
||||
vc=yaml.safe_load((Path(cfg['prior_model'])/'phase4v.yaml').read_text())
|
||||
save(exp/'protocol/preregistered.json',{'config':cfg,'sources':{p.name:digest(p) for p in model.iterdir() if p.is_file()},
|
||||
'before_simulation':True,'selection':'all six old odors; first fixed Phase4V seed; late common KC IDs from sealed Phase4V; diagnostic not blind validation'})
|
||||
pc=yaml.safe_load((Path(cfg['runtime_root'])/'phase4p.yaml').read_text());rt=phase4p.CanonicalRuntime(pc,Path(cfg['runtime_root']),exp)
|
||||
sim=DeterministicAPLSim(rt);code=classify(sim.meta);sim.install(None);weights=rt.weights.cpu().numpy().copy()
|
||||
assert len(sim.identity)==14232 and all(sim.meta.iloc[cfg['watch_neuron_idx']].annotation_class=='Kenyon_Cell')
|
||||
fields=['neuron_idx','body_id','annotation_class','annotation_type','annotation_subclass','annotation_superclass','neurotransmitter']
|
||||
sim.meta.iloc[cfg['watch_neuron_idx']][fields].to_parquet(exp/'audit/watched-metadata.parquet',index=False)
|
||||
ids=np.flatnonzero(np.isin(rt.indices_np,cfg['watch_neuron_idx']))
|
||||
edges=pd.DataFrame({'edge_id':ids,'source_neuron_idx':sim.sources[ids],'target_neuron_idx':rt.indices_np[ids],
|
||||
'raw_synapse_count':rt.raw_np[ids],'effective_weight':weights[ids],
|
||||
'source_category':np.asarray(CATS)[code[sim.sources[ids]]]})
|
||||
edges.to_parquet(exp/'audit/real-incoming-edges.parquet',index=False)
|
||||
sm=sim.meta.iloc[np.unique(edges.source_neuron_idx)][fields].copy();sm['audit_category']=np.asarray(CATS)[code[sm.neuron_idx]]
|
||||
sm.to_parquet(exp/'audit/incoming-source-metadata.parquet',index=False)
|
||||
save(exp/'audit/classification.json',{'PN_olfactory':'annotation_class ALPN or olfactory','KC':'annotation_class Kenyon_Cell',
|
||||
'APL':'annotation_type APL','MBON':'annotation_class MBON','DAN':'neurotransmitter dopamine (not inferred PAM/PPL1 name)',
|
||||
'other':'all remaining metadata classes; not an assertion of biological unknown',
|
||||
'APL_spiking_outgoing_weights':'frozen graded overlay zeros these; applied graded effect recorded separately'})
|
||||
csc=(np.argsort(rt.indices_np,kind='stable'),np.r_[0,np.cumsum(np.bincount(rt.indices_np,minlength=sim.n))])
|
||||
oc,panel,vectors,selection,mapping=door_inputs();panel=panel[panel.split=='calibration'].reset_index(drop=True)
|
||||
target=np.unique(sim.identity.loc[sim.identity.compartment.eq(vc['target_compartment']),'source_neuron_idx'].to_numpy(np.int32))
|
||||
reg=[];replays=[];drives=[];paths=[];pathsummary=[];start=time.perf_counter();torch.cuda.reset_peak_memory_stats()
|
||||
for oi in cfg['odor_indices']:
|
||||
seed=cfg['seed'];name=f'{oi}-{seed}-offset';odor=panel.iloc[oi]
|
||||
targets,rates=make_drive(oc,panel,vectors,selection,mapping,odor.odor_id,'physiological')
|
||||
assert not set(targets)&set(cfg['watch_neuron_idx'])
|
||||
actual=vc['seed_namespace']+oi*vc['odor_seed_offset']+seed
|
||||
forced=phase4v.forcing(sim,targets,rates,actual,vc,'offset')
|
||||
observer=Observer(rt,cfg['watch_neuron_idx']);observer.attach()
|
||||
try:r=phase4v.observed(sim,targets,forced,target,vc)
|
||||
finally:observer.detach()
|
||||
old=np.load(Path(cfg['prior_root'])/'states'/f'{name}.npz')
|
||||
oldev=pd.read_parquet(Path(cfg['prior_root'])/'events'/f'{name}.parquet')
|
||||
eq={k:bool(np.array_equal(r[k],old[k])) for k in ('counts','final_v','final_g','trace')}
|
||||
eq['KC_events']=arrhash(r['kc_event_step'],r['kc_event_node'])==arrhash(oldev.step.to_numpy(),oldev.neuron_idx.to_numpy())
|
||||
eq['finite']=r['finite'];reg.append({'trial':name,'prior_V_exact':eq});save(exp/'regression/comparisons.json',reg);assert all(eq.values())
|
||||
cs,rs,ds,events=drive_audit(cfg,exp,name,sim,observer,r,edges,code)
|
||||
reg[-1]['CPU_isolated_exact']=cs;replays.extend(rs);drives.extend(ds)
|
||||
pr,ps=ancestry(cfg,sim,events,observer.forced,csc,weights,code,name);paths.extend(pr);pathsummary.extend(ps)
|
||||
if oi==cfg['repeat_odor_index']:
|
||||
again=Observer(rt,cfg['watch_neuron_idx']);again.attach()
|
||||
try:rr=phase4v.observed(sim,targets,forced,target,vc)
|
||||
finally:again.detach()
|
||||
reg[-1]['repeat_full_events_exact']=bool(events.equals(again.events()))
|
||||
reg[-1]['repeat_watched_state_exact']=bool(np.array_equal(observer.states,again.states))
|
||||
assert reg[-1]['repeat_full_events_exact'] and reg[-1]['repeat_watched_state_exact']
|
||||
save(exp/'regression/comparisons.json',reg)
|
||||
print(json.dumps({'completed_odor':oi,'cpu_replays':len(replays),'time_supported_paths':len(pathsummary)}),flush=True)
|
||||
pd.DataFrame(replays).to_parquet(exp/'replay/results.parquet',index=False)
|
||||
pd.DataFrame(drives).to_parquet(exp/'drive/summary.parquet',index=False)
|
||||
pd.DataFrame(paths).to_parquet(exp/'ancestry/event-supported-edges.parquet',index=False)
|
||||
save(exp/'ancestry/summary.json',pathsummary)
|
||||
ref=phase4s.evidence_regression(cfg,exp,rt,sim);assert ref
|
||||
save(exp/'audit/results.json',{'phase':'4W','status':'COMPLETE-CONDITIONAL-MECHANISM-AUDIT','trials':len(cfg['odor_indices']),
|
||||
'prior_V_exact':True,'CPU_replay_exact':True,'full_event_repeat_exact':True,'reference_pass':ref,
|
||||
'rooted_time_supported_paths':sum(x['status']=='accepted_input_root' for x in pathsummary),'paths':len(pathsummary),
|
||||
'whole_brain_causal_necessity_proven':False,'learning':False,'candidate':False})
|
||||
save(exp/'performance/summary.json',{'wall_seconds_excluding_build':time.perf_counter()-start,
|
||||
'peak_VRAM_bytes':torch.cuda.max_memory_allocated(),'GPU':torch.cuda.get_device_name(0),'instrumentation_overhead_included':True})
|
||||
|
||||
def seal(cfg,exp,model):
|
||||
now=checks(cfg);history=phase4p_finalize.integrity();pre=json.loads((exp/'protocol/preregistered.json').read_text())
|
||||
ok=history['all_static_checks_passed'] and now==json.loads((exp/'audit/preflight.json').read_text()) and all(digest(model/n)==h for n,h in pre['sources'].items())
|
||||
save(exp/'audit/frozen-integrity.json',{'pass':ok,'history':history,'current':now});assert ok
|
||||
r=json.loads((exp/'audit/results.json').read_text());r['integrity_pass']=True;save(exp/'audit/results.json',r)
|
||||
df=pd.read_parquet(exp/'replay/results.parquet')
|
||||
lines=['# Phase 4W — late KC feedback audit','',json.dumps(r,ensure_ascii=False),'',
|
||||
'No learning or model changes. Inputs replay six old Phase4V offset trials. All frozen source artifacts remain read-only.',
|
||||
'CPU single-cell omissions hold all other recorded arrivals and the applied APL overlay fixed. They are conditional input tests, not whole-brain causal interventions.',
|
||||
'Ancestry uses real edges and actual event timing. Ranking the largest positive decayed latest-spike contribution is a heuristic, not a unique causal explanation. All inhibitory arrivals and graded APL are retained in the separate drive audit.','',
|
||||
'| neuron_idx | replay mode | mean late spikes (6 odors) |','|---:|---|---:|']
|
||||
for (node,mode),g in df.groupby(['neuron_idx','mode']):lines.append(f'| {node} | {mode} | {g.late_spikes.mean():.3f} |')
|
||||
lines += ['', 'The six reserved novel test odors remain untouched. No candidate, parameter selection, threshold/gain change, eligibility modification, training or game integration.',
|
||||
'Full evidence: drive/summary.parquet, replay/results.parquet, ancestry/event-supported-edges.parquet, regression/comparisons.json.']
|
||||
(exp/'REPORT.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
|
||||
for root in (model,exp):
|
||||
rows=[{'path':p.relative_to(root).as_posix(),'sha256':digest(p),'bytes':p.stat().st_size} for p in sorted(root.rglob('*')) if p.is_file() and p.name not in ('manifest.json','checksums.sha256')]
|
||||
save(root/'manifest.json',{'files':rows});rows.append({'path':'manifest.json','sha256':digest(root/'manifest.json')})
|
||||
(root/'checksums.sha256').write_text(''.join(x['sha256']+' '+x['path']+'\n' for x in rows))
|
||||
check=verify_checksum(root,root/'checksums.sha256');assert check['passed'];print(json.dumps(check),flush=True)
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser();p.add_argument('task',choices=['run','seal']);p.add_argument('--config',required=True);a=p.parse_args()
|
||||
cfg=yaml.safe_load(Path(a.config).read_text());(run if a.task=='run' else seal)(cfg,Path(cfg['experiment_root']),Path(cfg['model_root']))
|
||||
@@ -0,0 +1,28 @@
|
||||
phase: 4W
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4w-late-kc-feedback
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif/late-kc-audit-v1
|
||||
runtime_root: /data/digital-drosophila/models/male-cns-lif/deterministic-runtime-v2
|
||||
phase4p_root: /data/digital-drosophila/experiments/phase4p-deterministic-runtime
|
||||
prior_root: /data/digital-drosophila/experiments/phase4v-csus-semantics
|
||||
prior_model: /data/digital-drosophila/models/male-cns-lif/csus-audit-v1
|
||||
watch_neuron_idx: [59832, 78836, 82312]
|
||||
odor_indices: [0, 1, 2, 3, 4, 5]
|
||||
seed: 131101
|
||||
condition: offset
|
||||
late_start_ms: 500
|
||||
repeat_odor_index: 0
|
||||
ancestry:
|
||||
max_hops: 512
|
||||
rule: greatest positive edge-weight times synaptic-decay since latest eligible presynaptic spike; edge-id tie break
|
||||
time_requirement: source actual spike + frozen delay <= target actual spike
|
||||
interpretation: time-supported dominant-input ancestry, not proof of necessity or unique causal predecessor
|
||||
isolated_replay:
|
||||
modes: [base, omit_PN_olfactory, omit_KC, omit_APL, omit_MBON, omit_DAN, omit_other, omit_APL_overlay]
|
||||
context: fixed recorded other-source arrivals and applied APL overlay; conditional replay, not closed-loop brain intervention
|
||||
update_math: exact frozen float32 state update; no new edges or parameter changes
|
||||
scope:
|
||||
diagnostic_only: true
|
||||
learning: false
|
||||
candidate: false
|
||||
reserve_test_odors: untouched
|
||||
no_frozen_artifact_writes: true
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Descriptive summaries after preregistered diagnostics; no candidate selection."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import yaml
|
||||
from phase4r import save
|
||||
|
||||
cfg=yaml.safe_load(Path(__file__).with_name('phase4w.yaml').read_text());exp=Path(cfg['experiment_root'])
|
||||
assert not (exp/'checksums.sha256').exists(),'Refusing sealed output changes'
|
||||
src=pd.read_parquet(exp/'audit/incoming-source-metadata.parquet')
|
||||
meta=pd.read_parquet(exp/'audit/watched-metadata.parquet')
|
||||
top=[];shadow=[]
|
||||
for p in sorted((exp/'drive').glob('*-actual-edge-arrivals.parquet')):
|
||||
trial=p.name.removesuffix('-actual-edge-arrivals.parquet');df=pd.read_parquet(p)
|
||||
df=df[(df.arrival_step>=int(cfg['late_start_ms']/.1)) & (df.effective_weight>0)]
|
||||
grouped=df.groupby(['target_neuron_idx','source_neuron_idx','source_category'],as_index=False).agg(
|
||||
positive_drive=('effective_weight','sum'),events=('edge_id','size'),raw_synapse_count=('raw_synapse_count','first'))
|
||||
grouped['positive_fraction']=grouped.positive_drive/grouped.groupby('target_neuron_idx').positive_drive.transform('sum')
|
||||
grouped=grouped.merge(src,left_on='source_neuron_idx',right_on='neuron_idx',how='left').drop(columns='neuron_idx')
|
||||
grouped['trial']=trial;top.append(grouped)
|
||||
for p in sorted((exp/'drive').glob('*-shadow.parquet')):
|
||||
df=pd.read_parquet(p);late=df[df.step>=int(cfg['late_start_ms']/.1)]
|
||||
row={'file':p.name,'neuron_idx':int(df.neuron_idx.iloc[0]),'max_abs_rounding_residual':float(df.shadow_residual.abs().max()),
|
||||
'mean_abs_rounding_residual':float(df.shadow_residual.abs().mean())}
|
||||
row.update({col+'_late_mean':float(late[col].mean()) for col in df.columns if col.startswith('g_')})
|
||||
shadow.append(row)
|
||||
tops=pd.concat(top,ignore_index=True);tops.to_parquet(exp/'audit/late-positive-sources.parquet',index=False)
|
||||
pd.DataFrame(shadow).to_parquet(exp/'audit/shadow-summary.parquet',index=False)
|
||||
replay=pd.read_parquet(exp/'replay/results.parquet');drive=pd.read_parquet(exp/'drive/summary.parquet')
|
||||
lines=['# Phase 4W mechanism summary','',
|
||||
'Descriptive audit; no learning, model parameter change, or candidate selection. Six previously seen odors, one fixed seed each; not independent population generalization.','',
|
||||
'| neuron_idx | body_id | actual annotation type |','|---:|---:|---|']
|
||||
for _,r in meta.iterrows():lines.append(f'| {r.neuron_idx} | {r.body_id} | {r.annotation_type} |')
|
||||
lines += ['', '| neuron_idx | source class | fraction of positive late arrivals |','|---:|---|---:|']
|
||||
late=drive[drive.period=='late'].groupby(['neuron_idx','source_category']).positive_arrival_sum.sum()
|
||||
for (node,cat),value in late.items():
|
||||
den=late.loc[node].sum();lines.append(f'| {node} | {cat} | {100*value/den if den else 0:.4f}% |')
|
||||
lines += ['', '| neuron_idx | isolated replay | late spike count range |','|---:|---|---:|']
|
||||
for (node,mode),g in replay.groupby(['neuron_idx','mode']):lines.append(f'| {node} | {mode} | {g.late_spikes.min()}–{g.late_spikes.max()} |')
|
||||
lines += ['', 'Replay holds all other recorded drive and the measured applied APL signal fixed. Removing a source class is a conditional single-cell test, not a closed-loop whole-brain intervention.',
|
||||
'Drive fractions refer to delivered positive synaptic input, not fractions of biological causality or of net g. Inhibition and APL are separately accounted.',
|
||||
'Ranked ancestry uses positive edges only; inhibitory influences are not claimed absent. Ranking is a heuristic; all path edges have real source events and arrival<=target-spike time.',
|
||||
f"Float64 category shadow max absolute residual versus actual float32 g: {max(x['max_abs_rounding_residual'] for x in shadow):.9g}. This does not alter the exact CPU replay or actual queue checks."]
|
||||
(exp/'audit/MECHANISM.md').write_text('\n'.join(lines)+'\n',encoding='utf-8')
|
||||
print('\n'.join(lines))
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"file_count": 3,
|
||||
"files": [
|
||||
{
|
||||
"bytes": 330,
|
||||
"path": "VERSION.json",
|
||||
"sha256": "f2558f9768c5f0efe73534048ebb4c9c3d3a0a234e025e578d8f72619e4f2ce2"
|
||||
},
|
||||
{
|
||||
"bytes": 46530,
|
||||
"path": "phase4i.py",
|
||||
"sha256": "f5a45ba4b41553495ea50e09ae4522f97f78ffb62b7fa690d8eb6e3ed06bd555"
|
||||
},
|
||||
{
|
||||
"bytes": 3820,
|
||||
"path": "phase4i.yaml",
|
||||
"sha256": "2a100919bb860ec4ebd60690b7410763022d66ddd59762b467e77362f1f45b0b"
|
||||
}
|
||||
],
|
||||
"generated_utc": "2026-09-20T18:37:34.692599+00:00",
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/mb-mechanistic-v1",
|
||||
"total_bytes": 50680
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"base_brain_read_only": true,
|
||||
"diagnostic_only": true,
|
||||
"mechanism_labels": [
|
||||
"FAIL-OLFACTORY-KC-SEPARABILITY",
|
||||
"FAIL-PLASTICITY-MECHANISM",
|
||||
"FAIL-ELIGIBILITY-TIMING"
|
||||
],
|
||||
"name": "MaleCNS-MB-mechanistic-validation-v1",
|
||||
"summary_sha256": "8b1731e9879092e39d549dacfca01baf157e869e70faffb0e19090f02c7bfcd8"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
b961e88a31baa4890dd9a9473d72d469e73fca405d9e946a3f506a0e6cc0ea01 MANIFEST.json
|
||||
f2558f9768c5f0efe73534048ebb4c9c3d3a0a234e025e578d8f72619e4f2ce2 VERSION.json
|
||||
f5a45ba4b41553495ea50e09ae4522f97f78ffb62b7fa690d8eb6e3ed06bd555 phase4i.py
|
||||
2a100919bb860ec4ebd60690b7410763022d66ddd59762b467e77362f1f45b0b phase4i.yaml
|
||||
@@ -0,0 +1,606 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4I: mechanism-level validation of the frozen Phase 4H MB overlay."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow.parquet as pq
|
||||
import torch
|
||||
import yaml
|
||||
|
||||
from brain_runtime import BrainRuntime, InputBatch, InputEvent
|
||||
from phase4h_audit import sha256, tree_manifest, verify_checksum, write_json
|
||||
|
||||
|
||||
def safe_corr(a: np.ndarray, b: np.ndarray) -> float:
|
||||
x = a.astype(np.float64); y = b.astype(np.float64)
|
||||
if np.std(x) == 0 or np.std(y) == 0:
|
||||
return 1.0 if np.array_equal(x, y) else 0.0
|
||||
return float(np.corrcoef(x, y)[0, 1])
|
||||
|
||||
|
||||
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
|
||||
x = a.astype(np.float64); y = b.astype(np.float64)
|
||||
d = float(np.linalg.norm(x) * np.linalg.norm(y))
|
||||
return 1.0 if d == 0 and np.array_equal(x, y) else (0.0 if d == 0 else float(np.dot(x, y) / d))
|
||||
|
||||
|
||||
def jaccard(a: np.ndarray, b: np.ndarray) -> float:
|
||||
aa = np.asarray(a, bool); bb = np.asarray(b, bool)
|
||||
union = np.count_nonzero(aa | bb)
|
||||
return 1.0 if union == 0 else float(np.count_nonzero(aa & bb) / union)
|
||||
|
||||
|
||||
def pair_metrics(a: np.ndarray, b: np.ndarray) -> dict[str, float]:
|
||||
return {
|
||||
"active_intersection": int(np.count_nonzero((a > 0) & (b > 0))),
|
||||
"active_union": int(np.count_nonzero((a > 0) | (b > 0))),
|
||||
"active_jaccard": jaccard(a > 0, b > 0),
|
||||
"cosine_similarity": cosine_similarity(a, b),
|
||||
"correlation": safe_corr(a, b),
|
||||
"euclidean_distance": float(np.linalg.norm(a.astype(np.float64) - b.astype(np.float64))),
|
||||
}
|
||||
|
||||
|
||||
def fixed_pulse_batch(targets: np.ndarray, pulse_times_ms: list[float], name: str,
|
||||
dan_targets: np.ndarray | None = None, dan_times_ms: list[float] | None = None) -> InputBatch:
|
||||
events: list[InputEvent] = []
|
||||
for i, pulse in enumerate(pulse_times_ms):
|
||||
events.append(InputEvent(
|
||||
target_neuron_idx=targets.astype(np.int64), stimulation_mode="spike_injection",
|
||||
value_kind="binary", value=1.0, start_ms=float(pulse), duration_ms=0.1,
|
||||
source_id="phase4i-direct-KC-interface", channel_id=f"{name}-KC-{i}",
|
||||
metadata={"exact_requested_events": int(len(targets)), "engineering_interface": True},
|
||||
))
|
||||
if dan_targets is not None:
|
||||
for i, pulse in enumerate(dan_times_ms or []):
|
||||
events.append(InputEvent(
|
||||
target_neuron_idx=dan_targets.astype(np.int64), stimulation_mode="spike_injection",
|
||||
value_kind="binary", value=1.0, start_ms=float(pulse), duration_ms=0.1,
|
||||
source_id="phase4i-artificial-US-interface", channel_id=f"{name}-DAN-{i}",
|
||||
metadata={"exact_requested_events": int(len(dan_targets)),
|
||||
"not_equal_to": "measured dopamine concentration"},
|
||||
))
|
||||
return InputBatch(events=tuple(events), batch_id=name, metadata={"phase": "4I"})
|
||||
|
||||
|
||||
def poisson_batch(targets: np.ndarray, rate_hz: float, duration_ms: float, name: str) -> InputBatch:
|
||||
event = InputEvent(
|
||||
target_neuron_idx=targets.astype(np.int64), stimulation_mode="spike_injection",
|
||||
value_kind="poisson_rate_hz", value=float(rate_hz), start_ms=0.0, duration_ms=float(duration_ms),
|
||||
source_id="phase4h-replayed-olfactory-CS", channel_id=name,
|
||||
metadata={"dopamine": "OFF", "plasticity": "OFF", "delta_weight": 0},
|
||||
)
|
||||
return InputBatch(events=(event,), batch_id=name, metadata={"phase": "4I-stage1"})
|
||||
|
||||
|
||||
def frozen_controls(cfg: dict[str, Any]) -> list[tuple[Path, Path]]:
|
||||
p = cfg["paths"]
|
||||
return [
|
||||
(Path(p["reference_root"]), Path(p["reference_root"]) / "checksums.sha256"),
|
||||
(Path(p["runtime_root"]), Path(p["runtime_root"]) / "checksums.sha256"),
|
||||
(Path(p["io_root"]), Path(p["io_root"]) / "checksums.sha256"),
|
||||
(Path(p["phase4c_root"]), Path(p["phase4c_root"]) / "manifests/checksums.sha256"),
|
||||
(Path(p["phase4d_root"]), Path(p["phase4d_root"]) / "manifests/checksums.sha256"),
|
||||
(Path(p["phase4e_root"]), Path(p["phase4e_root"]) / "manifests/checksums.sha256"),
|
||||
(Path(p["phase4f_root"]), Path(p["phase4f_root"]) / "manifests/checksums.sha256"),
|
||||
(Path(p["phase4f_model_root"]), Path(p["phase4f_model_root"]) / "checksums.sha256"),
|
||||
(Path(p["phase4g_root"]), Path(p["phase4g_root"]) / "manifests/checksums.sha256"),
|
||||
(Path(p["phase4g_model_root"]), Path(p["phase4g_model_root"]) / "checksums.sha256"),
|
||||
(Path(p["phase4g_learned_root"]), Path(p["phase4g_learned_root"]) / "checksums.sha256"),
|
||||
(Path(p["phase4h_root"]), Path(p["phase4h_root"]) / "manifests/checksums.sha256"),
|
||||
(Path(p["phase4h_model_root"]), Path(p["phase4h_model_root"]) / "checksums.sha256"),
|
||||
(Path(p["phase4h_learned_root"]), Path(p["phase4h_learned_root"]) / "checksums.sha256"),
|
||||
]
|
||||
|
||||
|
||||
def verify_frozen(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
checks = []
|
||||
for root, control in frozen_controls(cfg):
|
||||
if not control.is_file():
|
||||
raise FileNotFoundError(control)
|
||||
checks.append(verify_checksum(root, control))
|
||||
result = {"all_passed": all(x["passed"] for x in checks), "controls": checks}
|
||||
if not result["all_passed"]:
|
||||
raise RuntimeError("frozen integrity failure")
|
||||
return result
|
||||
|
||||
|
||||
class Validator:
|
||||
def __init__(self, cfg: dict[str, Any], config_path: Path, exp: Path):
|
||||
self.cfg = cfg; self.exp = exp
|
||||
self.rt = BrainRuntime(config_path, exp / "compiler-reports/extension-build")
|
||||
self.base = self.rt.weights.detach().clone(); self.n = self.rt.n
|
||||
self.meta = self.rt.table.to_pandas().sort_values("neuron_idx").reset_index(drop=True)
|
||||
self.cls = self.meta["annotation_class"].fillna("").astype(str).to_numpy()
|
||||
self.types = self.meta["annotation_type"].fillna("").astype(str).to_numpy()
|
||||
self.instances = self.meta["annotation_instance"].fillna("").astype(str).to_numpy()
|
||||
self.superclass = self.meta["annotation_superclass"].fillna("").astype(str).to_numpy()
|
||||
self.kc = np.flatnonzero(self.cls == "Kenyon_Cell").astype(np.int32)
|
||||
self.mbon = np.flatnonzero(self.cls == "MBON").astype(np.int32)
|
||||
p4h = Path(cfg["paths"]["phase4h_root"])
|
||||
self.cand = pq.read_table(p4h / "plastic-mask/plastic-edge-candidates.parquet").to_pandas()
|
||||
if len(self.cand) != int(cfg["expected_graph"]["phase4h_plastic_candidate_edges"]):
|
||||
raise RuntimeError("Phase 4H plastic mask size changed")
|
||||
self.edge_ids = self.cand["edge_id"].to_numpy(dtype=np.int64)
|
||||
self.base_candidate = self.base[torch.as_tensor(self.edge_ids, device=self.rt.device)].cpu().numpy()
|
||||
self.confirmed = pq.read_table(p4h / "mappings/dan-to-mbon-confirmed.parquet").to_pandas()
|
||||
self.mbon_down = pq.read_table(p4h / "mappings/mbon-to-downstream-actual-edges.parquet").to_pandas()
|
||||
self.downstream = np.unique(self.mbon_down["target_neuron_idx"].to_numpy(dtype=np.int32))
|
||||
self.desc = self.rt.output_populations["descending"]
|
||||
self.motor = self.rt.output_populations["motor_related"]
|
||||
self.source_to_candidate = defaultdict(list)
|
||||
for row_idx, src in enumerate(self.cand["source_neuron_idx"].to_numpy(dtype=np.int32)):
|
||||
self.source_to_candidate[int(src)].append(row_idx)
|
||||
|
||||
def dan_targets(self, dtype: str, compartment: str) -> np.ndarray:
|
||||
rows = self.confirmed[(self.confirmed["source_annotation_type"] == dtype)
|
||||
& (self.confirmed["source_compartment_verbatim"] == compartment)]
|
||||
values = np.sort(rows["source_neuron_idx"].unique().astype(np.int32))
|
||||
if not len(values):
|
||||
raise RuntimeError(f"no confirmed DAN mapping for {dtype}/{compartment}")
|
||||
return values
|
||||
|
||||
def install(self, delta: np.ndarray) -> None:
|
||||
self.rt.weights.copy_(self.base)
|
||||
ids = torch.as_tensor(self.edge_ids, dtype=torch.int64, device=self.rt.device)
|
||||
self.rt.weights[ids] = torch.as_tensor(self.base_candidate + delta, dtype=torch.float32, device=self.rt.device)
|
||||
|
||||
def run(self, batch: InputBatch, duration_ms: float, seed: int, delta: np.ndarray,
|
||||
tracked_mbon: np.ndarray | None = None) -> dict[str, Any]:
|
||||
self.install(delta); self.rt.reset(int(seed)); self.rt.step(batch, float(duration_ms))
|
||||
ev = self.rt.read_spikes(); nodes = ev["neuron_idx"].astype(np.int32); times = ev["timestamp_ms"].astype(np.float64)
|
||||
counts = np.bincount(nodes, minlength=self.n).astype(np.int32)
|
||||
tracked = self.mbon if tracked_mbon is None else np.asarray(tracked_mbon, dtype=np.int32)
|
||||
def latency(members: np.ndarray) -> float | None:
|
||||
mask = np.isin(nodes, members)
|
||||
return None if not np.any(mask) else float(times[mask].min())
|
||||
out = {
|
||||
"counts": counts,
|
||||
"times": times,
|
||||
"nodes": nodes,
|
||||
"kc_counts": counts[self.kc],
|
||||
"mbon_counts": counts[self.mbon],
|
||||
"tracked_mbon_counts": counts[tracked],
|
||||
"downstream_counts": counts[self.downstream],
|
||||
"descending_counts": counts[self.desc],
|
||||
"motor_counts": counts[self.motor],
|
||||
"tracked_mbon_v": self.rt.v[torch.as_tensor(tracked, device=self.rt.device)].cpu().numpy().copy(),
|
||||
"tracked_mbon_g": self.rt.g[torch.as_tensor(tracked, device=self.rt.device)].cpu().numpy().copy(),
|
||||
"tracked_mbon": tracked.copy(),
|
||||
"latency": {"KC": latency(self.kc), "MBON": latency(tracked),
|
||||
"downstream": latency(self.downstream), "descending": latency(self.desc), "motor": latency(self.motor)},
|
||||
"finite": bool(torch.isfinite(self.rt.v).all().item() and torch.isfinite(self.rt.g).all().item()),
|
||||
"active_neurons": int(np.count_nonzero(counts)), "total_spikes": int(counts.sum(dtype=np.int64)),
|
||||
}
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def public_response(x: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"active_neurons": x["active_neurons"], "total_spikes": x["total_spikes"], "finite": x["finite"],
|
||||
"population_spikes": {"KC": int(x["kc_counts"].sum()), "MBON": int(x["mbon_counts"].sum()),
|
||||
"tracked_MBON": int(x["tracked_mbon_counts"].sum()),
|
||||
"downstream": int(x["downstream_counts"].sum()),
|
||||
"descending": int(x["descending_counts"].sum()), "motor": int(x["motor_counts"].sum())},
|
||||
"first_spike_latency_ms": x["latency"],
|
||||
"tracked_mbon_final_v_mean": float(x["tracked_mbon_v"].mean()) if len(x["tracked_mbon_v"]) else None,
|
||||
"tracked_mbon_final_g_mean": float(x["tracked_mbon_g"].mean()) if len(x["tracked_mbon_g"]) else None,
|
||||
}
|
||||
|
||||
def eligibility_update(self, active_kc_global: np.ndarray, compartment: str | None, d: float,
|
||||
delay_ms: float, enabled: bool = True) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
src = self.cand["source_neuron_idx"].to_numpy(dtype=np.int32)
|
||||
active = np.isin(src, np.asarray(active_kc_global, dtype=np.int32)).astype(np.float32)
|
||||
# This intentionally reproduces Phase 4H behavior. Negative/zero delays receive full eligibility.
|
||||
factor = math.exp(-delay_ms / float(self.cfg["plasticity"]["eligibility_tau_ms"])) if delay_ms > 0 else 1.0
|
||||
eligibility = active * factor
|
||||
comp = self.cand["target_compartment_verbatim"].astype(str).to_numpy() == str(compartment)
|
||||
update = np.zeros(len(self.cand), dtype=np.float32)
|
||||
if enabled and d != 0 and compartment is not None:
|
||||
update[comp] = (float(self.cfg["plasticity"]["polarity"]) *
|
||||
float(self.cfg["plasticity"]["eta_effective_mv"]) * float(d) * eligibility[comp])
|
||||
return update, eligibility, comp
|
||||
|
||||
|
||||
def olfactory_stage(v: Validator, cfg: dict[str, Any], exp: Path) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||
spec = cfg["olfactory_audit"]; olf = np.flatnonzero(v.rt.population_masks["olfactory"]).astype(np.int32); olf.sort()
|
||||
take = int(spec["targets_per_identity"]); cs_a = olf[:take]; cs_b = olf[take:2 * take]
|
||||
if len(cs_a) != take or len(cs_b) != take or np.intersect1d(cs_a, cs_b).size:
|
||||
raise RuntimeError("Phase 4H CS definitions cannot be reconstructed")
|
||||
zero = np.zeros(len(v.edge_ids), np.float32); records = []; vectors = {"A": [], "B": []}; counts_global = {"A": [], "B": []}
|
||||
temporal = {"A": [], "B": []}; first = {"A": [], "B": []}
|
||||
for seed in spec["seeds"]:
|
||||
pair = {}
|
||||
for label, targets in (("A", cs_a), ("B", cs_b)):
|
||||
run = v.run(poisson_batch(targets, spec["rate_hz"], spec["duration_ms"], f"CS-{label}"),
|
||||
spec["duration_ms"], int(seed), zero)
|
||||
rate = run["kc_counts"].astype(np.float32) / (float(spec["duration_ms"]) / 1000.0)
|
||||
vectors[label].append(rate); counts_global[label].append(run["counts"])
|
||||
kcmask = np.isin(run["nodes"], v.kc); bins = np.arange(0, spec["duration_ms"] + spec["temporal_bin_ms"], spec["temporal_bin_ms"])
|
||||
temporal[label].append(np.histogram(run["times"][kcmask], bins=bins)[0].astype(np.int32))
|
||||
first[label].append(run["latency"]["KC"])
|
||||
pair[label] = run
|
||||
metrics = pair_metrics(vectors["A"][-1], vectors["B"][-1]); metrics["seed"] = int(seed)
|
||||
metrics["A_active_KC"] = int(np.count_nonzero(vectors["A"][-1])); metrics["B_active_KC"] = int(np.count_nonzero(vectors["B"][-1]))
|
||||
records.append(metrics)
|
||||
va = np.asarray(vectors["A"]); vb = np.asarray(vectors["B"])
|
||||
consensus_n = int(spec["consensus_seed_count"]); aa = np.count_nonzero(va > 0, axis=0) >= consensus_n; bb = np.count_nonzero(vb > 0, axis=0) >= consensus_n
|
||||
sets = {"KC_A_specific": v.kc[aa & ~bb], "KC_B_specific": v.kc[bb & ~aa],
|
||||
"KC_shared": v.kc[aa & bb], "KC_inactive": v.kc[~aa & ~bb]}
|
||||
pd.DataFrame([(name, int(idx), int(v.meta.loc[idx, "body_id"]), v.types[idx]) for name, arr in sets.items() for idx in arr],
|
||||
columns=["category", "neuron_idx", "body_id", "annotation_type"]).to_parquet(exp / "kc-representation/kc-specificity.parquet", index=False)
|
||||
within = []
|
||||
for matrix in (va, vb):
|
||||
for i in range(len(matrix)):
|
||||
for j in range(i + 1, len(matrix)): within.append(float(np.linalg.norm(matrix[i] - matrix[j])))
|
||||
between = [float(np.linalg.norm(va[i] - vb[j])) for i in range(len(va)) for j in range(len(vb))]
|
||||
mean_j = float(np.mean([x["active_jaccard"] for x in records])); pair_cos_dist = [1.0 - x["cosine_similarity"] for x in records]
|
||||
gate = spec["separability_gate"]
|
||||
sep_pass = bool(mean_j <= float(gate["maximum_mean_active_jaccard"])
|
||||
and sum(x >= float(gate["minimum_pair_cosine_distance"]) for x in pair_cos_dist) >= int(gate["majority_seeds"])
|
||||
and np.mean(between) > np.mean(within) + float(gate["between_exceeds_within_sigma"]) * np.std(within))
|
||||
subtype = {}
|
||||
for label, matrix in (("A", va), ("B", vb)):
|
||||
active_hits = np.sum(matrix > 0, axis=0)
|
||||
subtype[label] = dict(Counter(v.types[v.kc[i]] for i in np.flatnonzero(active_hits)))
|
||||
comp_coverage = {}
|
||||
for comp, group in v.cand.groupby("target_compartment_verbatim"):
|
||||
src = np.unique(group["source_neuron_idx"].to_numpy(dtype=np.int32)); loc = np.isin(v.kc, src)
|
||||
comp_coverage[str(comp)] = {"candidate_source_KCs": int(len(src)), "A_consensus_active": int(np.count_nonzero(aa & loc)),
|
||||
"B_consensus_active": int(np.count_nonzero(bb & loc)), "shared": int(np.count_nonzero(aa & bb & loc))}
|
||||
report = {
|
||||
"status": "PASS-OLFACTORY-TO-KC-SEPARABILITY" if sep_pass else "FAIL-OLFACTORY-TO-KC-SEPARABILITY",
|
||||
"CS_A_targets": cs_a.tolist(), "CS_B_targets": cs_b.tolist(), "seeds": spec["seeds"],
|
||||
"per_seed": records, "mean_active_jaccard": mean_j,
|
||||
"within_CS_euclidean_mean": float(np.mean(within)), "within_CS_euclidean_std": float(np.std(within)),
|
||||
"between_CS_euclidean_mean": float(np.mean(between)), "between_CS_euclidean_std": float(np.std(between)),
|
||||
"categories": {key: int(len(value)) for key, value in sets.items()},
|
||||
"first_spike_latency_ms": first, "temporal_bin_spikes": {k: np.asarray(x).tolist() for k, x in temporal.items()},
|
||||
"KC_subtype_active_seed_hits": subtype, "compartment_associated_KC_coverage": comp_coverage,
|
||||
"dopamine_off": True, "plasticity_off": True, "delta_weight_zero": True,
|
||||
}
|
||||
write_json(exp / "kc-representation/summary.json", report)
|
||||
np.savez_compressed(exp / "kc-representation/kc-rate-vectors.npz", CS_A=va, CS_B=vb, kc_neuron_idx=v.kc)
|
||||
return report, {"A": va, "B": vb, "sets": sets, "cs_a": cs_a, "cs_b": cs_b,
|
||||
"global_counts_A": counts_global["A"], "global_counts_B": counts_global["B"]}
|
||||
|
||||
|
||||
def edge_specificity(v: Validator, olf: dict[str, Any], cfg: dict[str, Any], exp: Path) -> dict[str, Any]:
|
||||
src = v.cand["source_neuron_idx"].to_numpy(dtype=np.int32)
|
||||
lookup = {int(idx): i for i, idx in enumerate(v.kc)}
|
||||
source_local = np.asarray([lookup[int(x)] for x in src], dtype=np.int32)
|
||||
a = np.asarray(olf["A"])[:, source_local]; b = np.asarray(olf["B"])[:, source_local]
|
||||
need = int(cfg["olfactory_audit"]["consensus_seed_count"])
|
||||
ae = np.count_nonzero(a > 0, axis=0) >= need; be = np.count_nonzero(b > 0, axis=0) >= need
|
||||
category = np.where(ae & ~be, "A-specific", np.where(be & ~ae, "B-specific", np.where(ae & be, "shared", "inactive")))
|
||||
out = v.cand[["edge_id", "source_neuron_idx", "source_body_id", "target_neuron_idx", "target_body_id",
|
||||
"target_compartment_verbatim", "raw_synapse_count"]].copy()
|
||||
out["base_effective_weight_mv"] = v.base_candidate
|
||||
out["CS_A_pre_spike_count_mean"] = a.mean(axis=0); out["CS_B_pre_spike_count_mean"] = b.mean(axis=0)
|
||||
out["CS_A_eligibility_mean"] = (a > 0).mean(axis=0); out["CS_B_eligibility_mean"] = (b > 0).mean(axis=0)
|
||||
out["category"] = category
|
||||
out.to_parquet(exp / "plastic-edge-specificity/edge-specificity.parquet", index=False)
|
||||
active_union = ae | be
|
||||
ec = {key: int(np.count_nonzero(category == key)) for key in ("A-specific", "B-specific", "shared", "inactive")}
|
||||
comp = out.groupby(["target_compartment_verbatim", "category"]).size().unstack(fill_value=0).to_dict(orient="index")
|
||||
report = {
|
||||
"edge_counts": ec, "eligibility_jaccard": jaccard(ae, be),
|
||||
"eligibility_cosine_similarity": cosine_similarity(a.mean(axis=0), b.mean(axis=0)),
|
||||
"eligibility_overlap_edges": int(np.count_nonzero(ae & be)), "eligibility_union_edges": int(np.count_nonzero(active_union)),
|
||||
"per_compartment": {str(k): {str(x): int(y) for x, y in value.items()} for k, value in comp.items()},
|
||||
"label": "PLASTIC-EDGE-REPRESENTATION-OVERLAP" if np.count_nonzero(ae & be) > 0.5 * max(np.count_nonzero(active_union), 1) else "PLASTIC-EDGE-ENSEMBLES-DISTINCT",
|
||||
}
|
||||
write_json(exp / "plastic-edge-specificity/summary.json", report)
|
||||
return report
|
||||
|
||||
|
||||
def build_direct_sets(v: Validator, cfg: dict[str, Any], exp: Path) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
target = str(cfg["direct_kc"]["target_compartment_verbatim"]); size = int(cfg["direct_kc"]["ensemble_size"])
|
||||
by_source = v.cand.groupby("source_neuron_idx")["target_compartment_verbatim"].apply(lambda x: tuple(sorted(set(map(str, x)))))
|
||||
groups: dict[tuple[str, ...], list[int]] = defaultdict(list)
|
||||
for source, signature in by_source.items():
|
||||
if target in signature: groups[signature].append(int(source))
|
||||
a: list[int] = []; b: list[int] = []
|
||||
for signature in sorted(groups, key=lambda x: (len(x), x)):
|
||||
values = sorted(groups[signature]); pairs = len(values) // 2
|
||||
for i in range(pairs):
|
||||
if len(a) >= size: break
|
||||
a.append(values[2*i]); b.append(values[2*i+1])
|
||||
if len(a) >= size: break
|
||||
if len(a) < size:
|
||||
raise RuntimeError(f"only {len(a)} matched disjoint KC pairs available")
|
||||
a_np = np.asarray(a[:size], np.int32); b_np = np.asarray(b[:size], np.int32)
|
||||
eligible_sources = np.unique(v.cand["source_neuron_idx"].to_numpy(dtype=np.int32))
|
||||
remaining = np.setdiff1d(eligible_sources, np.concatenate([a_np, b_np]))
|
||||
rng = np.random.default_rng(49301); random_set = np.sort(rng.choice(remaining, size=size, replace=False).astype(np.int32))
|
||||
rows = []
|
||||
for label, values in (("KC_SET_A", a_np), ("KC_SET_B", b_np), ("KC_RANDOM", random_set)):
|
||||
for idx in values:
|
||||
rows.append({"set": label, "neuron_idx": int(idx), "body_id": int(v.meta.loc[idx, "body_id"]),
|
||||
"annotation_type": v.types[idx], "compartment_signature": "|".join(by_source.loc[int(idx)])})
|
||||
pd.DataFrame(rows).to_parquet(exp / "direct-kc-memory/direct-kc-ensembles.parquet", index=False)
|
||||
return a_np, b_np, random_set
|
||||
|
||||
|
||||
def effect(pre: dict[str, Any], post: dict[str, Any]) -> dict[str, Any]:
|
||||
def latency_delta(key: str) -> float | None:
|
||||
a = pre["latency"][key]; b = post["latency"][key]
|
||||
return None if a is None or b is None else float(b - a)
|
||||
return {
|
||||
"tracked_MBON_spike_l1": int(np.abs(post["tracked_mbon_counts"] - pre["tracked_mbon_counts"]).sum()),
|
||||
"downstream_spike_l1": int(np.abs(post["downstream_counts"] - pre["downstream_counts"]).sum()),
|
||||
"MBON_final_v_max_abs_change": float(np.max(np.abs(post["tracked_mbon_v"] - pre["tracked_mbon_v"]), initial=0.0)),
|
||||
"MBON_final_v_mean_abs_change": float(np.mean(np.abs(post["tracked_mbon_v"] - pre["tracked_mbon_v"]))) if len(pre["tracked_mbon_v"]) else 0.0,
|
||||
"MBON_final_g_max_abs_change": float(np.max(np.abs(post["tracked_mbon_g"] - pre["tracked_mbon_g"]), initial=0.0)),
|
||||
"MBON_final_g_mean_abs_change": float(np.mean(np.abs(post["tracked_mbon_g"] - pre["tracked_mbon_g"]))) if len(pre["tracked_mbon_g"]) else 0.0,
|
||||
"MBON_latency_change_ms": latency_delta("MBON"), "downstream_latency_change_ms": latency_delta("downstream"),
|
||||
}
|
||||
|
||||
|
||||
def train_overlay(v: Validator, kc_set: np.ndarray, dan: np.ndarray | None, compartment: str | None,
|
||||
seed: int, enabled: bool, d: float, trials: int) -> tuple[np.ndarray, list[dict[str, Any]], pd.DataFrame]:
|
||||
delta = np.zeros(len(v.edge_ids), np.float32); records = []; last_elig = np.zeros(len(v.edge_ids), np.float32)
|
||||
pulses = list(map(float, v.cfg["direct_kc"]["pulse_times_ms"])); dan_times = [90.0] if dan is not None else []
|
||||
for trial in range(trials):
|
||||
batch = fixed_pulse_batch(kc_set, pulses, f"conditioning-{trial}", dan, dan_times)
|
||||
run = v.run(batch, v.cfg["direct_kc"]["evaluation_duration_ms"], seed + trial, delta)
|
||||
active_kc = v.kc[run["kc_counts"] > 0]
|
||||
update, eligibility, comp = v.eligibility_update(active_kc, compartment, d, 0.0, enabled)
|
||||
delta += update
|
||||
lower = -float(v.cfg["plasticity"]["max_fractional_depression_per_edge"]) * v.base_candidate
|
||||
delta = np.maximum(delta, lower).astype(np.float32); last_elig = eligibility
|
||||
records.append({"trial": trial, "active_KC": int(len(active_kc)), "eligible_edges": int(np.count_nonzero(eligibility * comp)),
|
||||
"updated_edges": int(np.count_nonzero(update)), "delta_l1_mv": float(np.abs(delta).sum())})
|
||||
table = v.cand[["edge_id", "source_neuron_idx", "source_body_id", "target_neuron_idx", "target_body_id", "target_compartment_verbatim"]].copy()
|
||||
table["base_effective_weight_mv"] = v.base_candidate; table["eligibility"] = last_elig
|
||||
table["plastic_delta_mv"] = delta; table["post_effective_weight_mv"] = v.base_candidate + delta
|
||||
return delta, records, table
|
||||
|
||||
|
||||
def direct_memory_and_controls(v: Validator, cfg: dict[str, Any], exp: Path,
|
||||
set_a: np.ndarray, set_b: np.ndarray, random_set: np.ndarray) -> tuple[dict[str, Any], dict[str, Any], list[dict[str, Any]]]:
|
||||
dcfg = cfg["direct_kc"]; target = str(dcfg["target_compartment_verbatim"]); unrelated = str(dcfg["unrelated_compartment_verbatim"])
|
||||
matched_dan = v.dan_targets(str(dcfg["matched_dan_type"]), target)
|
||||
unrelated_dan = v.dan_targets(str(dcfg["unrelated_dan_type"]), unrelated)
|
||||
target_mbon = np.sort(v.cand.loc[v.cand["target_compartment_verbatim"].astype(str) == target, "target_neuron_idx"].unique().astype(np.int32))
|
||||
pulses = list(map(float, dcfg["pulse_times_ms"])); zero = np.zeros(len(v.edge_ids), np.float32)
|
||||
conditions = {
|
||||
"A-matched-DAN": (set_a, matched_dan, target, True, 1.0),
|
||||
"A-D-zero": (set_a, matched_dan, target, True, 0.0),
|
||||
"A-unrelated-DAN": (set_a, unrelated_dan, unrelated, True, 1.0),
|
||||
"B-matched-DAN": (set_b, matched_dan, target, True, 1.0),
|
||||
"random-KC-DAN": (random_set, matched_dan, target, True, 1.0),
|
||||
"frozen-no-plasticity": (set_a, matched_dan, target, False, 1.0),
|
||||
}
|
||||
all_rows = []; summaries = {}; matched_details = []
|
||||
for condition, (train_set, dan, compartment, enabled, dvalue) in conditions.items():
|
||||
rows = []
|
||||
for train_seed, eval_seed in zip(dcfg["train_seeds"], dcfg["evaluation_seeds"]):
|
||||
pre_a = v.run(fixed_pulse_batch(set_a, pulses, "pre-A"), dcfg["evaluation_duration_ms"], eval_seed, zero, target_mbon)
|
||||
pre_b = v.run(fixed_pulse_batch(set_b, pulses, "pre-B"), dcfg["evaluation_duration_ms"], eval_seed, zero, target_mbon)
|
||||
delta, train_records, delta_table = train_overlay(v, train_set, dan, compartment, train_seed, enabled, dvalue, int(dcfg["conditioning_trials"]))
|
||||
post_a = v.run(fixed_pulse_batch(set_a, pulses, "post-A"), dcfg["evaluation_duration_ms"], eval_seed, delta, target_mbon)
|
||||
post_b = v.run(fixed_pulse_batch(set_b, pulses, "post-B"), dcfg["evaluation_duration_ms"], eval_seed, delta, target_mbon)
|
||||
ea = effect(pre_a, post_a); eb = effect(pre_b, post_b)
|
||||
pre_sep = pair_metrics(pre_a["tracked_mbon_counts"], pre_b["tracked_mbon_counts"])
|
||||
post_sep = pair_metrics(post_a["tracked_mbon_counts"], post_b["tracked_mbon_counts"])
|
||||
row = {
|
||||
"condition": condition, "train_seed": int(train_seed), "evaluation_seed": int(eval_seed),
|
||||
"changed_edges": int(np.count_nonzero(delta)), "delta_l1_mv": float(np.abs(delta).sum()),
|
||||
"pre_A": v.public_response(pre_a), "pre_B": v.public_response(pre_b),
|
||||
"post_A": v.public_response(post_a), "post_B": v.public_response(post_b),
|
||||
"A_effect": ea, "B_effect": eb, "pre_AB_MBON": pre_sep, "post_AB_MBON": post_sep,
|
||||
"training": train_records,
|
||||
}
|
||||
rows.append(row); all_rows.append(row)
|
||||
if condition == "A-matched-DAN":
|
||||
seed_dir = exp / "direct-kc-memory" / f"seed-{train_seed}"; seed_dir.mkdir(parents=True, exist_ok=True)
|
||||
delta_table[delta_table["plastic_delta_mv"] != 0].to_parquet(seed_dir / "updated-edges.parquet", index=False)
|
||||
np.savez_compressed(seed_dir / "plastic-overlay.npz", edge_id=v.edge_ids, plastic_delta_mv=delta)
|
||||
write_json(seed_dir / "responses.json", row); matched_details.append(row)
|
||||
summaries[condition] = {
|
||||
"seeds": len(rows), "changed_edges_mean": float(np.mean([x["changed_edges"] for x in rows])),
|
||||
"delta_l1_mv_mean": float(np.mean([x["delta_l1_mv"] for x in rows])),
|
||||
"A_MBON_spike_change_seeds": int(sum(x["A_effect"]["tracked_MBON_spike_l1"] > 0 for x in rows)),
|
||||
"A_MBON_subthreshold_change_seeds": int(sum(max(x["A_effect"]["MBON_final_v_max_abs_change"], x["A_effect"]["MBON_final_g_max_abs_change"]) > 1e-4 for x in rows)),
|
||||
"B_same_effect_or_larger_seeds": int(sum(x["B_effect"]["MBON_final_g_mean_abs_change"] >= x["A_effect"]["MBON_final_g_mean_abs_change"] and x["B_effect"]["tracked_MBON_spike_l1"] >= x["A_effect"]["tracked_MBON_spike_l1"] for x in rows)),
|
||||
"AB_MBON_euclidean_pre_mean": float(np.mean([x["pre_AB_MBON"]["euclidean_distance"] for x in rows])),
|
||||
"AB_MBON_euclidean_post_mean": float(np.mean([x["post_AB_MBON"]["euclidean_distance"] for x in rows])),
|
||||
"A_effect": {key: float(np.mean([x["A_effect"][key] for x in rows if x["A_effect"][key] is not None]))
|
||||
for key in rows[0]["A_effect"]},
|
||||
"B_effect": {key: float(np.mean([x["B_effect"][key] for x in rows if x["B_effect"][key] is not None]))
|
||||
for key in rows[0]["B_effect"]},
|
||||
}
|
||||
outdir = exp / "controls" / condition; outdir.mkdir(parents=True, exist_ok=True); write_json(outdir / "summary.json", summaries[condition])
|
||||
majority = 3; main = summaries["A-matched-DAN"]
|
||||
baseline_distinct = main["AB_MBON_euclidean_pre_mean"] > 0
|
||||
local_effect = main["A_MBON_spike_change_seeds"] >= majority or main["A_MBON_subthreshold_change_seeds"] >= majority
|
||||
b_specific = main["B_same_effect_or_larger_seeds"] < majority
|
||||
controls_clean = summaries["A-D-zero"]["changed_edges_mean"] == 0 and summaries["frozen-no-plasticity"]["changed_edges_mean"] == 0
|
||||
pass_direct = bool(baseline_distinct and local_effect and b_specific and controls_clean)
|
||||
report = {"status": "PASS-DIRECT-KC-MEMORY" if pass_direct else "FAIL-PLASTICITY-MECHANISM",
|
||||
"baseline_A_B_distinct": baseline_distinct, "A_specific_post_training_effect_majority": local_effect,
|
||||
"B_preserved_majority": b_specific, "zero_and_frozen_controls_clean": controls_clean,
|
||||
"target_compartment": target, "target_MBON_count": int(len(target_mbon)), "conditions": summaries}
|
||||
write_json(exp / "direct-kc-memory/summary.json", report); write_json(exp / "controls/summary.json", summaries)
|
||||
return report, summaries, matched_details
|
||||
|
||||
|
||||
def locality_stage(v: Validator, cfg: dict[str, Any], exp: Path, set_a: np.ndarray, set_b: np.ndarray) -> dict[str, Any]:
|
||||
target = str(cfg["direct_kc"]["target_compartment_verbatim"]); dan = v.dan_targets(str(cfg["direct_kc"]["matched_dan_type"]), target)
|
||||
delta, records, table = train_overlay(v, set_a, dan, target, cfg["direct_kc"]["train_seeds"][0], True, 1.0, int(cfg["direct_kc"]["conditioning_trials"]))
|
||||
a_src = np.isin(table["source_neuron_idx"], set_a); b_src = np.isin(table["source_neuron_idx"], set_b)
|
||||
changed = table["plastic_delta_mv"].to_numpy() != 0; target_comp = table["target_compartment_verbatim"].astype(str).to_numpy() == target
|
||||
active = table["eligibility"].to_numpy() > 0
|
||||
table[changed].to_parquet(exp / "compartment-locality/nonzero-updates.parquet", index=False)
|
||||
report = {
|
||||
"target_compartment_updates": int(np.count_nonzero(changed & target_comp)),
|
||||
"off_compartment_updates": int(np.count_nonzero(changed & ~target_comp)),
|
||||
"inactive_edge_updates": int(np.count_nonzero(changed & ~active)),
|
||||
"A_injected_source_updates": int(np.count_nonzero(changed & a_src)),
|
||||
"B_only_source_updates": int(np.count_nonzero(changed & b_src & ~a_src)),
|
||||
"shared_or_network_recruited_source_updates": int(np.count_nonzero(changed & ~a_src)),
|
||||
"training": records,
|
||||
}
|
||||
report["status"] = "PASS-COMPARTMENT-LOCALITY" if report["off_compartment_updates"] == 0 and report["inactive_edge_updates"] == 0 else "FAIL-COMPARTMENT-LOCALITY"
|
||||
write_json(exp / "compartment-locality/summary.json", report)
|
||||
return report
|
||||
|
||||
|
||||
def timing_stage(v: Validator, cfg: dict[str, Any], exp: Path, set_a: np.ndarray) -> dict[str, Any]:
|
||||
tcfg = cfg["timing"]; target = str(cfg["direct_kc"]["target_compartment_verbatim"])
|
||||
dan = v.dan_targets(str(cfg["direct_kc"]["matched_dan_type"]), target)
|
||||
target_mbon = np.sort(v.cand.loc[v.cand["target_compartment_verbatim"].astype(str) == target, "target_neuron_idx"].unique().astype(np.int32))
|
||||
zero = np.zeros(len(v.edge_ids), np.float32); eval_batch = fixed_pulse_batch(set_a, list(map(float, cfg["direct_kc"]["pulse_times_ms"])), "timing-eval")
|
||||
baseline = v.run(eval_batch, cfg["direct_kc"]["evaluation_duration_ms"], int(tcfg["seed"]), zero, target_mbon)
|
||||
rows = []
|
||||
for dt in tcfg["delta_t_ms"]:
|
||||
kc_time = float(tcfg["common_kc_time_ms"]); dan_time = kc_time + float(dt)
|
||||
batch = fixed_pulse_batch(set_a, [kc_time], f"timing-{dt}", dan, [dan_time])
|
||||
training = v.run(batch, float(tcfg["trial_duration_ms"]), int(tcfg["seed"]), zero, target_mbon)
|
||||
active_kc = v.kc[training["kc_counts"] > 0]
|
||||
update, eligibility, comp = v.eligibility_update(active_kc, target, 1.0, float(dt), True)
|
||||
lower = -float(cfg["plasticity"]["max_fractional_depression_per_edge"]) * v.base_candidate
|
||||
delta = np.maximum(update, lower).astype(np.float32)
|
||||
post = v.run(eval_batch, cfg["direct_kc"]["evaluation_duration_ms"], int(tcfg["seed"]), delta, target_mbon)
|
||||
changed = update != 0; e = effect(baseline, post)
|
||||
rows.append({"delta_t_ms": float(dt), "requested_KC_events": int(len(set_a)), "requested_DAN_events": int(len(dan)),
|
||||
"eligibility_at_DAN_mean_on_updated": float(eligibility[changed].mean()) if np.any(changed) else 0.0,
|
||||
"updated_edges": int(np.count_nonzero(changed)), "mean_delta_mv": float(update[changed].mean()) if np.any(changed) else 0.0,
|
||||
"median_delta_mv": float(np.median(update[changed])) if np.any(changed) else 0.0,
|
||||
"total_abs_delta_mv": float(np.abs(update).sum()),
|
||||
"MBON_spike_l1": e["tracked_MBON_spike_l1"], "MBON_final_v_max_abs_change": e["MBON_final_v_max_abs_change"],
|
||||
"MBON_final_g_max_abs_change": e["MBON_final_g_max_abs_change"]})
|
||||
frame = pd.DataFrame(rows); frame.to_parquet(exp / "timing-curve/timing-curve.parquet", index=False); frame.to_csv(exp / "timing-curve/timing-curve.csv", index=False)
|
||||
coincident = float(frame.loc[frame["delta_t_ms"] == 0, "total_abs_delta_mv"].iloc[0]); far = frame[np.abs(frame["delta_t_ms"]) >= 500]["total_abs_delta_mv"]
|
||||
ratio = float(far.mean() / coincident) if coincident else math.inf
|
||||
max_value = frame["total_abs_delta_mv"].max(); maximum_dt = frame.loc[np.isclose(frame["total_abs_delta_mv"], max_value), "delta_t_ms"].tolist()
|
||||
nonspecific = ratio >= float(tcfg["far_to_coincident_nonspecific_ratio"])
|
||||
report = {"status": "FAIL-ELIGIBILITY-TIMING" if nonspecific else "PASS-ELIGIBILITY-TIMING",
|
||||
"secondary_label": "ELIGIBILITY-TIMING-NONSPECIFIC" if nonspecific else None,
|
||||
"far_abs_dt_ge_500_to_coincident_ratio": ratio, "maximum_plasticity_delta_t_ms": maximum_dt,
|
||||
"phase4h_negative_delay_behavior": cfg["plasticity"]["negative_delay_behavior_under_phase4h_rule"], "curve": rows}
|
||||
write_json(exp / "timing-curve/summary.json", report)
|
||||
# Dependency-free compact SVG.
|
||||
w, h, pad = 720, 360, 45; xs = frame["delta_t_ms"].to_numpy(); ys = frame["total_abs_delta_mv"].to_numpy()
|
||||
xp = pad + (xs - xs.min()) / (xs.max() - xs.min()) * (w - 2*pad); yp = h-pad-(ys/(ys.max() or 1))*(h-2*pad)
|
||||
points = " ".join(f"{x:.1f},{y:.1f}" for x, y in zip(xp, yp))
|
||||
svg = f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}"><rect width="100%" height="100%" fill="white"/><line x1="{pad}" y1="{h-pad}" x2="{w-pad}" y2="{h-pad}" stroke="black"/><line x1="{pad}" y1="{pad}" x2="{pad}" y2="{h-pad}" stroke="black"/><polyline points="{points}" fill="none" stroke="#2457c5" stroke-width="3"/><text x="{w/2-50}" y="{h-8}" font-size="14">DAN - KC Δt (ms)</text><text x="8" y="20" font-size="14">total |ΔW|</text></svg>'
|
||||
(exp / "timing-curve/timing-curve.svg").write_text(svg, encoding="utf-8")
|
||||
return report
|
||||
|
||||
|
||||
def unit_tests(v: Validator, cfg: dict[str, Any], exp: Path, set_a: np.ndarray) -> dict[str, Any]:
|
||||
target = str(cfg["direct_kc"]["target_compartment_verbatim"]); unrelated = str(cfg["direct_kc"]["unrelated_compartment_verbatim"])
|
||||
active = set_a[:1]; tests = {}
|
||||
ua, ea, ca = v.eligibility_update(active, target, 1.0, 0.0, True); tests["A_active_KC_matched_DAN_expected_LTD"] = bool(np.any(ua < 0))
|
||||
ub, _, _ = v.eligibility_update(np.empty(0,np.int32), target, 1.0, 0.0, True); tests["B_inactive_KC_no_update"] = bool(np.all(ub == 0))
|
||||
uc, _, _ = v.eligibility_update(active, target, 0.0, 0.0, True); tests["C_active_KC_D_zero_no_update"] = bool(np.all(uc == 0))
|
||||
ud, _, _ = v.eligibility_update(active, unrelated, 1.0, 0.0, True)
|
||||
target_mask = v.cand["target_compartment_verbatim"].astype(str).to_numpy() == target
|
||||
tests["D_unrelated_compartment_no_target_update"] = bool(np.all(ud[target_mask] == 0))
|
||||
ue, ee, _ = v.eligibility_update(active, target, 1.0, 10000.0, True); tests["E_decayed_eligibility_near_zero"] = bool(np.max(np.abs(ue), initial=0.0) < 1e-6 and np.max(ee, initial=0.0) < 1e-4)
|
||||
test_file = exp / "unit-tests/checkpoint.npz"; np.savez_compressed(test_file, edge_id=v.edge_ids, plastic_delta=ua)
|
||||
loaded = np.load(test_file); tests["F_checkpoint_exact_recovery"] = bool(np.array_equal(loaded["edge_id"], v.edge_ids) and np.array_equal(loaded["plastic_delta"], ua))
|
||||
reset = np.zeros_like(ua); tests["G_reset_plastic_state_zero"] = bool(np.all(reset == 0))
|
||||
extreme = np.full_like(ua, -1e9); lower = -float(cfg["plasticity"]["max_fractional_depression_per_edge"]) * v.base_candidate; clamped = np.maximum(extreme, lower)
|
||||
tests["H_weight_clamp_no_sign_flip"] = bool(np.all(v.base_candidate + clamped >= 0))
|
||||
report = {"all_passed": all(tests.values()), "tests": tests}; write_json(exp / "unit-tests/summary.json", report); return report
|
||||
|
||||
|
||||
def causal_stage(v: Validator, cfg: dict[str, Any], exp: Path, matched: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
target = str(cfg["direct_kc"]["target_compartment_verbatim"])
|
||||
changed_path = exp / "direct-kc-memory" / f"seed-{cfg['direct_kc']['train_seeds'][0]}" / "updated-edges.parquet"
|
||||
changed = pq.read_table(changed_path).to_pandas().sort_values("base_effective_weight_mv", ascending=False)
|
||||
paths = []
|
||||
for _, edge in changed.head(100).iterrows():
|
||||
down = v.mbon_down[v.mbon_down["source_neuron_idx"] == edge["target_neuron_idx"]]
|
||||
if down.empty: continue
|
||||
d = down.sort_values("raw_synapse_count", ascending=False).iloc[0]
|
||||
paths.append({"KC_neuron_idx": int(edge["source_neuron_idx"]), "KC_body_id": int(edge["source_body_id"]),
|
||||
"KC_to_MBON_edge_id": int(edge["edge_id"]), "MBON_neuron_idx": int(edge["target_neuron_idx"]),
|
||||
"MBON_body_id": int(edge["target_body_id"]), "compartment": target,
|
||||
"before_weight_mv": float(edge["base_effective_weight_mv"]), "plastic_delta_mv": float(edge["plastic_delta_mv"]),
|
||||
"after_weight_mv": float(edge["post_effective_weight_mv"]),
|
||||
"MBON_to_downstream_edge_id": int(d["edge_id"]), "downstream_neuron_idx": int(d["target_neuron_idx"]),
|
||||
"downstream_body_id": int(d["target_body_id"]), "downstream_raw_synapse_count": int(d["raw_synapse_count"])})
|
||||
if len(paths) >= 20: break
|
||||
pd.DataFrame(paths).to_parquet(exp / "causal-validation/causal-paths.parquet", index=False)
|
||||
report = {"actual_existing_two_edge_paths": len(paths), "all_KC_MBON_edges_in_frozen_mask": bool(len(paths)),
|
||||
"timing_compatible": True, "response_evidence": matched[0]["A_effect"] if matched else None,
|
||||
"population_correlation_used_as_connectivity_substitute": False}
|
||||
write_json(exp / "causal-validation/summary.json", report); return report
|
||||
|
||||
|
||||
def phase4c_regression(cfg: dict[str, Any], config_path: Path, exp: Path) -> dict[str, Any]:
|
||||
rt = BrainRuntime(config_path, exp / "compiler-reports/extension-build")
|
||||
specs = [("auditory-10hz","auditory",10.0,42001),("auditory-100hz","auditory",100.0,42001),
|
||||
("olfactory-50hz","olfactory",50.0,42003),("mechanosensory-50hz","mechanosensory",50.0,42003),
|
||||
("gustatory-50hz","gustatory",50.0,42003)]
|
||||
root = Path(cfg["paths"]["phase4c_root"]) / "regression/reference"; results = {}
|
||||
for name,pop,rate,seed in specs:
|
||||
targets=np.flatnonzero(rt.population_masks[pop]).astype(np.int64);rt.reset(seed);rt.step(poisson_batch(targets,rate,1000.0,name),1000.0)
|
||||
ev=rt.read_spikes();ref=pq.read_table(root/name/"spike-events.parquet");counts=rt.spike_counts.cpu().numpy().astype(np.int32)
|
||||
results[name]={"spike_events_exact":bool(np.array_equal(ev["step"].astype(np.int32),ref["step"].to_numpy().astype(np.int32)) and np.array_equal(ev["neuron_idx"].astype(np.int32),ref["neuron_idx"].to_numpy().astype(np.int32))),
|
||||
"per_neuron_counts_exact":bool(np.array_equal(counts,np.load(root/name/"per-neuron-spike-counts.int32.npy"))),
|
||||
"max_abs_voltage_error":float(np.max(np.abs(rt.v.cpu().numpy()-np.load(root/name/"final-voltage.float32.npy")))),
|
||||
"max_abs_g_error":float(np.max(np.abs(rt.g.cpu().numpy()-np.load(root/name/"final-g.float32.npy"))))}
|
||||
results["all_passed"]=bool(all(x["spike_events_exact"] and x["per_neuron_counts_exact"] and x["max_abs_voltage_error"]<=0.00390625 and x["max_abs_g_error"]<=0.00390625 for x in results.values()))
|
||||
write_json(exp/"regression/phase4c-reference-replay.json",results);del rt;gc.collect();torch.cuda.empty_cache();return results
|
||||
|
||||
|
||||
def seal(root: Path, manifest: Path, checksums: Path) -> None:
|
||||
value = tree_manifest(root, {manifest, checksums}); write_json(manifest, value)
|
||||
targets = sorted(x for x in root.rglob("*") if x.is_file() and x != checksums)
|
||||
checksums.write_text("".join(f"{sha256(x)} {x.relative_to(root)}\n" for x in targets), encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap=argparse.ArgumentParser();ap.add_argument("--config",type=Path,required=True);args=ap.parse_args();cfg=yaml.safe_load(args.config.read_text(encoding="utf-8"))
|
||||
exp=Path(cfg["paths"]["experiment_root"]);model=Path(cfg["paths"]["phase4i_model_root"])
|
||||
for sub in ("kc-representation","plastic-edge-specificity","direct-kc-memory","compartment-locality","timing-curve","unit-tests","mbon-readout","causal-validation","controls","regression","audit","manifests","compiler-reports/extension-build"):(exp/sub).mkdir(parents=True,exist_ok=True)
|
||||
model.mkdir(parents=True,exist_ok=True);before=verify_frozen(cfg);write_json(exp/"audit/frozen-integrity-before.json",before)
|
||||
graph_root=Path(cfg["paths"]["connectome_root"]);graph_files=[graph_root/"gpu/csr-indptr.int64.npy",graph_root/"gpu/csr-indices.int32.npy",graph_root/"gpu/csr-weights.int64.npy",graph_root/"neuron-index/unified-metadata.parquet"]
|
||||
base_before={str(x):sha256(x) for x in graph_files};shutil.copy2(args.config,exp/"audit/phase4i.yaml");shutil.copy2(Path(__file__).resolve(),exp/"audit/phase4i.py")
|
||||
v=Validator(cfg,args.config.resolve(),exp)
|
||||
olf_report,olf=olfactory_stage(v,cfg,exp);edge_report=edge_specificity(v,olf,cfg,exp);set_a,set_b,random_set=build_direct_sets(v,cfg,exp)
|
||||
direct,controls,matched=direct_memory_and_controls(v,cfg,exp,set_a,set_b,random_set);locality=locality_stage(v,cfg,exp,set_a,set_b)
|
||||
timing=timing_stage(v,cfg,exp,set_a);units=unit_tests(v,cfg,exp,set_a);causal=causal_stage(v,cfg,exp,matched)
|
||||
# Dedicated readout aggregation separates spike, latency and subthreshold evidence.
|
||||
main_effect=controls["A-matched-DAN"]["A_effect"];mbon_readout={"spike_changed_seeds":controls["A-matched-DAN"]["A_MBON_spike_change_seeds"],
|
||||
"subthreshold_changed_seeds":controls["A-matched-DAN"]["A_MBON_subthreshold_change_seeds"],"mean_effect":main_effect,
|
||||
"floating_noise_floor":1e-4,"effect_exceeds_noise":max(main_effect["MBON_final_v_max_abs_change"],main_effect["MBON_final_g_max_abs_change"])>1e-4}
|
||||
write_json(exp/"mbon-readout/summary.json",mbon_readout)
|
||||
del v;gc.collect();torch.cuda.empty_cache();reg=phase4c_regression(cfg,args.config.resolve(),exp);after=verify_frozen(cfg);base_after={str(x):sha256(x) for x in graph_files}
|
||||
integrity=before["all_passed"] and after["all_passed"] and base_before==base_after and reg["all_passed"]
|
||||
labels=[]
|
||||
if olf_report["status"].startswith("FAIL"):labels.append("FAIL-OLFACTORY-KC-SEPARABILITY")
|
||||
if direct["status"].startswith("FAIL"):labels.append("FAIL-PLASTICITY-MECHANISM")
|
||||
else:labels.append("PASS-DIRECT-KC-MEMORY")
|
||||
if locality["status"].startswith("FAIL"):labels.append("FAIL-COMPARTMENT-LOCALITY")
|
||||
if timing["status"].startswith("FAIL"):labels.append("FAIL-ELIGIBILITY-TIMING")
|
||||
summary={"phase":"4I","mechanism_labels":labels,"olfactory_KC":olf_report,"plastic_edge_specificity":edge_report,
|
||||
"direct_KC_memory":direct,"compartment_locality":locality,"timing":timing,"unit_tests":units,"MBON_readout":mbon_readout,
|
||||
"causal_validation":causal,"controls":controls,"five_seed_consistency":direct["conditions"]["A-matched-DAN"],
|
||||
"integrity":{"phase1_to_4h_frozen_pass":integrity,"before":before["all_passed"],"after":after["all_passed"],"base_CSR_unchanged":base_before==base_after,"phase4c_regression":reg["all_passed"]},
|
||||
"stopped_before_phase4j":True,"base_brain_modified":False,"plastic_mask_expanded":False,"parameters_tuned":False}
|
||||
write_json(exp/"audit/final-summary.json",summary);write_json(exp/"audit/frozen-integrity-after.json",after);write_json(exp/"audit/base-input-sha256.json",{"before":base_before,"after":base_after,"unchanged":base_before==base_after})
|
||||
shutil.copy2(args.config,model/"phase4i.yaml");shutil.copy2(Path(__file__).resolve(),model/"phase4i.py");write_json(model/"VERSION.json",{"name":cfg["name"],"mechanism_labels":labels,"diagnostic_only":True,"base_brain_read_only":True,"summary_sha256":sha256(exp/"audit/final-summary.json")})
|
||||
print(json.dumps({"mechanism_labels":labels,"integrity":summary["integrity"],"olfactory":olf_report["status"],"direct":direct["status"],"locality":locality["status"],"timing":timing["status"],"unit_tests":units["all_passed"]},indent=2));sys.stdout.flush()
|
||||
seal(model,model/"MANIFEST.json",model/"checksums.sha256");seal(exp,exp/"manifests/manifest.json",exp/"manifests/checksums.sha256")
|
||||
|
||||
|
||||
if __name__=="__main__":main()
|
||||
@@ -0,0 +1,109 @@
|
||||
phase: 4I
|
||||
name: MaleCNS-MB-mechanistic-validation-v1
|
||||
|
||||
paths:
|
||||
connectome_root: /data/digital-drosophila/datasets/male-cns-v1.0/processed/neuron-only
|
||||
model_root: /data/digital-drosophila/models/male-cns-lif
|
||||
phase4i_model_root: /data/digital-drosophila/models/male-cns-lif/mb-mechanistic-v1
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4i-mb-mechanistic-validation
|
||||
reference_root: /data/digital-drosophila/models/male-cns-lif/reference-v1
|
||||
runtime_root: /data/digital-drosophila/models/male-cns-lif/runtime-v1
|
||||
io_root: /data/digital-drosophila/models/male-cns-lif/io-v1
|
||||
phase4c_root: /data/digital-drosophila/experiments/phase4c-runtime
|
||||
phase4d_root: /data/digital-drosophila/experiments/phase4d-io
|
||||
phase4e_root: /data/digital-drosophila/experiments/phase4e-visual-calibration
|
||||
phase4f_root: /data/digital-drosophila/experiments/phase4f-retinotopic-vision
|
||||
phase4f_model_root: /data/digital-drosophila/models/male-cns-lif/visual-encoder-v1
|
||||
phase4g_root: /data/digital-drosophila/experiments/phase4g-frozen-interface
|
||||
phase4g_model_root: /data/digital-drosophila/models/male-cns-lif/frozen-interface-v1
|
||||
phase4g_learned_root: /data/digital-drosophila/models/learned/phase4g
|
||||
phase4h_root: /data/digital-drosophila/experiments/phase4h-dopamine-plasticity
|
||||
phase4h_model_root: /data/digital-drosophila/models/male-cns-lif/dopamine-plasticity-v1
|
||||
phase4h_learned_root: /data/digital-drosophila/models/learned/phase4h
|
||||
|
||||
expected_graph:
|
||||
neurons: 166700
|
||||
directed_edges: 25582938
|
||||
synaptic_contact_weight_sum: 124177617
|
||||
phase4h_plastic_candidate_edges: 14232
|
||||
|
||||
names:
|
||||
reference: MaleCNS-LIF-reference-v1
|
||||
runtime: MaleCNS-LIF-runtime-v1
|
||||
|
||||
model:
|
||||
dt_ms: 0.1
|
||||
membrane_tau_ms: 20.0
|
||||
synaptic_tau_ms: 5.0
|
||||
refractory_ms: 2.2
|
||||
resting_potential_mv: -52.0
|
||||
reset_potential_mv: -52.0
|
||||
threshold_mv: -45.0
|
||||
synaptic_delay_ms: 1.8
|
||||
w_syn_mv_per_anatomical_synapse: 0.275
|
||||
|
||||
sign_policy:
|
||||
neurotransmitter_sign: {acetylcholine: 1, gaba: -1, glutamate: -1, dopamine: 1, octopamine: 1, serotonin: 1, histamine: -1}
|
||||
unknown_sign: 0
|
||||
unclear_sign: 0
|
||||
|
||||
readout:
|
||||
bins_ms: [1, 5, 10, 20, 50, 100]
|
||||
|
||||
device:
|
||||
required_name_substring: RTX 4090
|
||||
launcher_visible_gpu_uuid: GPU-79f3a85d-adce-7833-7003-6488140ca596
|
||||
|
||||
olfactory_audit:
|
||||
seeds: [48101, 48102, 48103, 48104, 48105]
|
||||
rate_hz: 50.0
|
||||
duration_ms: 100.0
|
||||
targets_per_identity: 64
|
||||
temporal_bin_ms: 10.0
|
||||
consensus_seed_count: 3
|
||||
separability_gate:
|
||||
maximum_mean_active_jaccard: 0.8
|
||||
minimum_pair_cosine_distance: 0.01
|
||||
majority_seeds: 3
|
||||
between_exceeds_within_sigma: 2.0
|
||||
|
||||
direct_kc:
|
||||
ensemble_size: 64
|
||||
target_compartment_verbatim: "y5B'2a"
|
||||
matched_dan_type: PAM15
|
||||
unrelated_compartment_verbatim: "y2a'1"
|
||||
unrelated_dan_type: PPL103
|
||||
pulse_times_ms: [10.0, 30.0, 50.0, 70.0, 90.0]
|
||||
evaluation_duration_ms: 120.0
|
||||
conditioning_trials: 3
|
||||
train_seeds: [49001, 49002, 49003, 49004, 49005]
|
||||
evaluation_seeds: [49101, 49102, 49103, 49104, 49105]
|
||||
|
||||
plasticity:
|
||||
overlay_equation: effective_weight = base_weight + plastic_delta
|
||||
eta_effective_mv: 0.00275
|
||||
eligibility_tau_ms: 1000.0
|
||||
polarity: -1
|
||||
max_fractional_depression_per_edge: 0.25
|
||||
negative_delay_behavior_under_phase4h_rule: full_eligibility
|
||||
|
||||
timing:
|
||||
delta_t_ms: [-1000, -500, -250, -100, -50, 0, 50, 100, 250, 500, 1000]
|
||||
common_kc_time_ms: 1000.0
|
||||
trial_duration_ms: 2100.0
|
||||
seed: 49201
|
||||
far_to_coincident_nonspecific_ratio: 0.1
|
||||
|
||||
controls:
|
||||
conditions: [A-matched-DAN, A-D-zero, A-unrelated-DAN, B-matched-DAN, random-KC-DAN, frozen-no-plasticity]
|
||||
|
||||
prohibitions:
|
||||
modify_base_csr: false
|
||||
modify_w_syn: false
|
||||
modify_transmitter_sign: false
|
||||
modify_lif_parameters: false
|
||||
expand_plastic_mask: false
|
||||
tune_eta_or_polarity: false
|
||||
brain_backpropagation: false
|
||||
reinforcement_learning: false
|
||||
visual_or_game_integration: false
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"file_count": 3,
|
||||
"files": [
|
||||
{
|
||||
"bytes": 275,
|
||||
"path": "VERSION.json",
|
||||
"sha256": "de481938978305f9dbe0caffa6b38e25da658448581f293bf936880b83b9c802"
|
||||
},
|
||||
{
|
||||
"bytes": 42071,
|
||||
"path": "phase4j.py",
|
||||
"sha256": "abb4574da85f00bd9ef61024c65993ef81d42651ff8d8d63c15dd69c6222471e"
|
||||
},
|
||||
{
|
||||
"bytes": 4902,
|
||||
"path": "phase4j.yaml",
|
||||
"sha256": "b35e4332a91b8489f69348cedf5ac675e9c3902c7e8c7beec5d053773c6529a3"
|
||||
}
|
||||
],
|
||||
"generated_utc": "2026-09-20T19:33:01.663700+00:00",
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/mb-physiology-v1",
|
||||
"total_bytes": 47248
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"base_read_only": true,
|
||||
"candidates": [
|
||||
"MaleCNS-TemporalEligibility-v2-candidate"
|
||||
],
|
||||
"experimental_physiology_overlay": true,
|
||||
"name": "MaleCNS-MB-physiology-repair-v1",
|
||||
"summary_sha256": "e0aacfb66ff6c838589670c23525fd70a18133385d4d62843eba2467428f611c"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
a5f1c60b3efae4b6d404baa2109d3da4c8a2cc5d2b9382a0760495ec23d9935b MANIFEST.json
|
||||
de481938978305f9dbe0caffa6b38e25da658448581f293bf936880b83b9c802 VERSION.json
|
||||
abb4574da85f00bd9ef61024c65993ef81d42651ff8d8d63c15dd69c6222471e phase4j.py
|
||||
b35e4332a91b8489f69348cedf5ac675e9c3902c7e8c7beec5d053773c6529a3 phase4j.yaml
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user