Archive MaleCNS research sources, provenance, bibliography and immutable data inventory
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"file_count": 6,
|
||||
"files": [
|
||||
{
|
||||
"bytes": 229,
|
||||
"path": "VERSION.json",
|
||||
"sha256": "d98a4074a0523cb1e767a2fca8df909dad74ba541dd4edf58ebbf34f0e196837"
|
||||
},
|
||||
{
|
||||
"bytes": 19442,
|
||||
"path": "__pycache__/brain_runtime.cpython-310.pyc",
|
||||
"sha256": "d80d8ecf0a35cc90a3f063a2362938f853d98ed64c88ba4c7864f793813b6117"
|
||||
},
|
||||
{
|
||||
"bytes": 23822,
|
||||
"path": "brain_runtime.py",
|
||||
"sha256": "1c4e3e6f06708bfec30b24e92297f320b2b8b87ebbbe6da501f5f2db4f74762a"
|
||||
},
|
||||
{
|
||||
"bytes": 37799,
|
||||
"path": "phase4f.py",
|
||||
"sha256": "5524ac0565b9e2a2f96d7a062109c27fe64365fcaaef44ee7b7ded7d3318fe50"
|
||||
},
|
||||
{
|
||||
"bytes": 4359,
|
||||
"path": "phase4f.yaml",
|
||||
"sha256": "52393d2c9ad019ceb61de9168dc809ec52aec72b3c66f1b9708d22c0808f678d"
|
||||
},
|
||||
{
|
||||
"bytes": 3795,
|
||||
"path": "phase4f_extract_retinotopy.py",
|
||||
"sha256": "6df36ecf4b6aa5565c1f3c10345fe997a2c3eefe88e42e0b194be272ce83e233"
|
||||
}
|
||||
],
|
||||
"root": "/data/digital-drosophila/models/male-cns-lif/visual-encoder-v1",
|
||||
"total_bytes": 89446
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"brain_frozen": true,
|
||||
"candidate_name": null,
|
||||
"connectome_changed": false,
|
||||
"lif_semantics_changed": false,
|
||||
"name": "MaleCNS-VisualEncoder-v1",
|
||||
"status": "FAIL-NO-V1-CANDIDATE",
|
||||
"transmitter_sign_changed": false
|
||||
}
|
||||
@@ -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,7 @@
|
||||
d98a4074a0523cb1e767a2fca8df909dad74ba541dd4edf58ebbf34f0e196837 VERSION.json
|
||||
d80d8ecf0a35cc90a3f063a2362938f853d98ed64c88ba4c7864f793813b6117 __pycache__/brain_runtime.cpython-310.pyc
|
||||
1c4e3e6f06708bfec30b24e92297f320b2b8b87ebbbe6da501f5f2db4f74762a brain_runtime.py
|
||||
5524ac0565b9e2a2f96d7a062109c27fe64365fcaaef44ee7b7ded7d3318fe50 phase4f.py
|
||||
52393d2c9ad019ceb61de9168dc809ec52aec72b3c66f1b9708d22c0808f678d phase4f.yaml
|
||||
6df36ecf4b6aa5565c1f3c10345fe997a2c3eefe88e42e0b194be272ce83e233 phase4f_extract_retinotopy.py
|
||||
407e20fe42cff605ac83993818e22b76d75cb1f7dd1844a7b387e83d02911958 MANIFEST.json
|
||||
@@ -0,0 +1,528 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Phase 4F: official-data retinotopic visual encoding for frozen MaleCNS-LIF."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import gc
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter
|
||||
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
|
||||
from scipy.spatial import cKDTree
|
||||
|
||||
from brain_runtime import BrainRuntime, InputBatch, InputEvent
|
||||
|
||||
|
||||
def np_json(value: Any) -> Any:
|
||||
if isinstance(value, (np.generic,)): return value.item()
|
||||
if isinstance(value, np.ndarray): return value.tolist()
|
||||
if isinstance(value, Path): return str(value)
|
||||
raise TypeError(type(value).__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_file(root: Path, checksum_file: Path) -> dict[str, Any]:
|
||||
checked, failures = 0, []
|
||||
for line in checksum_file.read_text(encoding="utf-8").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 {"checksum_file": str(checksum_file), "checked": checked, "failures": failures, "passed": not failures}
|
||||
|
||||
|
||||
def immutable_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["model_root"]) / "visual-interface-v2/checksums.sha256",
|
||||
]
|
||||
missing = [str(x) for x in controls if not x.is_file()]
|
||||
if missing: raise FileNotFoundError(f"immutable controls missing: {missing}")
|
||||
snapshots = {str(x): sha256(x) for x in controls}
|
||||
checks = []
|
||||
for x in controls:
|
||||
root = x.parent.parent if x.parent.name == "manifests" else x.parent
|
||||
checks.append(verify_checksum_file(root, x))
|
||||
if not all(x["passed"] for x in checks): raise RuntimeError(f"frozen input checksum failure: {checks}")
|
||||
return controls, snapshots, {"all_passed": True, "controls": checks}
|
||||
|
||||
|
||||
def git_commit(path: Path) -> str:
|
||||
return subprocess.check_output(["/usr/bin/git", "-C", str(path), "rev-parse", "HEAD"], text=True).strip()
|
||||
|
||||
|
||||
def external_resource_audit(cfg: dict[str, Any], exp: Path) -> dict[str, Any]:
|
||||
root = Path(cfg["paths"]["external_root"])
|
||||
specs = {
|
||||
"visualpathways": (cfg["official_resources"]["visualpathways_commit"], "GPL-3.0"),
|
||||
"celltype-explorer": (cfg["official_resources"]["celltype_explorer_commit"], "CC-BY-4.0"),
|
||||
"flyvis": (cfg["official_resources"]["flyvis_commit"], "MIT"),
|
||||
}
|
||||
repos = {}
|
||||
for name, (expected, license_id) in specs.items():
|
||||
path = root / name; actual = git_commit(path)
|
||||
if actual != expected: raise RuntimeError(f"{name} commit {actual} != {expected}")
|
||||
license_files = [x for x in path.iterdir() if x.is_file() and x.name.lower().startswith(("license", "copying"))]
|
||||
repos[name] = {"url": {"visualpathways": "https://github.com/reiserlab/visualpathways",
|
||||
"celltype-explorer": "https://github.com/reiserlab/celltype-explorer-drosophila-male-cns",
|
||||
"flyvis": "https://github.com/TuragaLab/flyvis"}[name],
|
||||
"commit": actual, "expected_commit": expected, "license": license_id,
|
||||
"license_files": [str(x) for x in license_files]}
|
||||
source = root / "retinotopy-tbars-body-summary.parquet"
|
||||
source_summary = json.loads((root / "retinotopy-tbars-body-summary.json").read_text())
|
||||
result = {"repositories": repos, "official_tbar_snapshot": {**source_summary, "sha256": sha256(source)},
|
||||
"paper": {"doi": cfg["official_resources"]["paper_doi"], "role": "official visual-pathway definition reference"}}
|
||||
write_json(exp / "audit/external-resource-provenance.json", result)
|
||||
return result
|
||||
|
||||
|
||||
def _hierarchy_labels(cfg: dict[str, Any], n: int) -> np.ndarray:
|
||||
labels = np.full(n, None, dtype=object)
|
||||
root = Path(cfg["paths"]["phase4c_root"]) / "visual-audit"
|
||||
for name in ("visual_sensory", "first_order", "second_order", "higher_visual", "central_brain"):
|
||||
path = root / f"hierarchy-{name}.parquet"
|
||||
if path.is_file():
|
||||
idx = pq.read_table(path, columns=["neuron_idx"])["neuron_idx"].to_numpy().astype(np.int64)
|
||||
labels[idx] = name
|
||||
return labels
|
||||
|
||||
|
||||
def _read_eye_map(path: Path, eye: str) -> pd.DataFrame:
|
||||
workbook = pd.ExcelFile(path)
|
||||
candidates = [name for name in workbook.sheet_names if "column" in name.lower()]
|
||||
if not candidates and len(workbook.sheet_names) == 1:
|
||||
candidates = workbook.sheet_names
|
||||
if len(candidates) != 1:
|
||||
raise RuntimeError(f"cannot uniquely identify curated-column sheet in {path}: {workbook.sheet_names}")
|
||||
data = pd.read_excel(path, sheet_name=candidates[0])
|
||||
id_cols = [x for x in data.columns if x not in {"hex1_id", "hex2_id"}]
|
||||
rows = []
|
||||
for cell_type in id_cols:
|
||||
for body, h1, h2 in zip(data[cell_type], data["hex1_id"], data["hex2_id"]):
|
||||
if pd.notna(body): rows.append((int(body), float(h1), float(h2), eye, str(cell_type)))
|
||||
out = pd.DataFrame(rows, columns=["body_id", "hex1", "hex2", "eye", "eye_map_cell_type"])
|
||||
return out.drop_duplicates(["body_id", "hex1", "hex2", "eye", "eye_map_cell_type"])
|
||||
|
||||
|
||||
def build_retinotopy(cfg: dict[str, Any], runtime: BrainRuntime, exp: Path) -> tuple[pd.DataFrame, dict[str, Any]]:
|
||||
ext = Path(cfg["paths"]["external_root"]); vp_root = ext / "visualpathways"
|
||||
md = runtime.table.to_pandas(); n = len(md)
|
||||
body = md["body_id"].to_numpy(np.uint64); body_to_idx = {int(b): i for i, b in enumerate(body)}
|
||||
h1 = md["annotation_assignedOlHex1"].to_numpy(float); h2 = md["annotation_assignedOlHex2"].to_numpy(float)
|
||||
direct = np.isfinite(h1) & np.isfinite(h2)
|
||||
coord1, coord2 = h1.copy(), h2.copy()
|
||||
kind = np.full(n, None, dtype=object); kind[direct] = "official_annotation_assignedOlHex"
|
||||
provenance = np.full(n, None, dtype=object); provenance[direct] = "MaleCNS v1.0 annotation assignedOlHex1/assignedOlHex2"
|
||||
reliability = np.full(n, None, dtype=object); reliability[direct] = "official-direct"
|
||||
contribution = np.full(n, np.nan); rf_r = np.full(n, np.nan); rf_theta = np.full(n, np.nan); rf_vic = np.full(n, np.nan)
|
||||
|
||||
eye = pd.concat([
|
||||
_read_eye_map(vp_root / "params/ME_columnar-cells_location.xlsx", "R"),
|
||||
_read_eye_map(vp_root / "params/ME_L_columnar-cells_location.xlsx", "L")], ignore_index=True)
|
||||
eye = eye[eye.body_id.isin(body_to_idx)].copy()
|
||||
eye_consistency = []
|
||||
for row in eye.itertuples(index=False):
|
||||
i = body_to_idx[int(row.body_id)]
|
||||
if direct[i]: eye_consistency.append(abs(coord1[i] - row.hex1) < 1e-6 and abs(coord2[i] - row.hex2) < 1e-6)
|
||||
else:
|
||||
coord1[i], coord2[i], kind[i], reliability[i] = row.hex1, row.hex2, "official_visualpathways_curated_column", "official-direct"
|
||||
provenance[i] = f"reiserlab/visualpathways curated column {row.eye} {row.eye_map_cell_type}"
|
||||
|
||||
tbar = pq.read_table(ext / "retinotopy-tbars-body-summary.parquet").to_pandas()
|
||||
tbar_canonical = 0
|
||||
for row in tbar.itertuples(index=False):
|
||||
i = body_to_idx.get(int(row.body_id))
|
||||
if i is None: continue
|
||||
tbar_canonical += 1
|
||||
contribution[i] = row.visual_input_contribution_sum; rf_r[i] = row.tbar_r_weighted_mean
|
||||
rf_theta[i] = row.tbar_theta_weighted_mean; rf_vic[i] = row.tbar_vic_weighted_mean
|
||||
if not np.isfinite(coord1[i]) and np.isfinite(row.tbar_hex1_weighted_mean) and np.isfinite(row.tbar_hex2_weighted_mean):
|
||||
coord1[i], coord2[i] = row.tbar_hex1_weighted_mean, row.tbar_hex2_weighted_mean
|
||||
kind[i], reliability[i] = "official_retinotopy_tbar_weighted_mean", "official-derived-field"
|
||||
provenance[i] = cfg["official_resources"]["retinotopy_tbar_url"]
|
||||
|
||||
# Anatomical receptive-field centroids for projection neurons are derived
|
||||
# only where no official body coordinate exists. They are explicitly
|
||||
# labelled derived and never counted as direct official coordinate coverage.
|
||||
official = np.isfinite(coord1) & np.isfinite(coord2)
|
||||
src = np.flatnonzero(official)
|
||||
deg = runtime.indptr_np[src + 1] - runtime.indptr_np[src]
|
||||
edge_ids = np.concatenate([np.arange(runtime.indptr_np[s], runtime.indptr_np[s + 1]) for s in src])
|
||||
edge_src = np.repeat(src, deg); dst = runtime.indices_np[edge_ids]; w = runtime.raw_np[edge_ids].astype(np.float64)
|
||||
sw = np.bincount(dst, weights=w, minlength=n)
|
||||
sx = np.bincount(dst, weights=w * coord1[edge_src], minlength=n)
|
||||
sy = np.bincount(dst, weights=w * coord2[edge_src], minlength=n)
|
||||
superclass = np.asarray(md["annotation_superclass"].tolist(), dtype=object)
|
||||
vp = np.isin(superclass, ["visual_projection", "visual_projection_tbc"])
|
||||
derived = vp & ~official & (sw > 0)
|
||||
coord1[derived] = sx[derived] / sw[derived]; coord2[derived] = sy[derived] / sw[derived]
|
||||
contribution[derived] = sw[derived]; kind[derived] = "connectome_weighted_centroid_from_official_eye_map"
|
||||
reliability[derived] = "connectome-derived"; provenance[derived] = "official eye-map coordinates + unchanged MaleCNS v1.0 raw synapse-count CSR"
|
||||
|
||||
mapped = np.isfinite(coord1) & np.isfinite(coord2)
|
||||
xmin, xmax = np.nanmin(coord1[mapped]), np.nanmax(coord1[mapped]); ymin, ymax = np.nanmin(coord2[mapped]), np.nanmax(coord2[mapped])
|
||||
x = 2 * (coord1 - xmin) / (xmax - xmin) - 1; y = 2 * (coord2 - ymin) / (ymax - ymin) - 1
|
||||
hierarchy = _hierarchy_labels(cfg, n); hierarchy[vp] = "visual_projection"
|
||||
instance = np.asarray(md["annotation_instance"].tolist(), object)
|
||||
path_class, path_source = np.full(n, None, object), np.full(n, None, object)
|
||||
for filename, field, source_name in (("TableS1_VPN_in_out_summary.csv", "oc_class", "official VPN table"),
|
||||
("TableS3_VCBN_in_out_summary.csv", "cc_class", "official VCBN table")):
|
||||
table = pd.read_csv(vp_root / "results/tables" / filename)
|
||||
lookup = dict(zip(table["instance"].astype(str), table[field]))
|
||||
for i, val in enumerate(instance):
|
||||
if val is not None and str(val) in lookup and pd.notna(lookup[str(val)]):
|
||||
path_class[i], path_source[i] = str(lookup[str(val)]), source_name
|
||||
eye_by_body = eye.groupby("body_id").agg(eye=("eye", lambda z: ";".join(sorted(set(z)))),
|
||||
eye_map_cell_type=("eye_map_cell_type", lambda z: ";".join(sorted(set(z))))).to_dict("index")
|
||||
rows = pd.DataFrame({
|
||||
"body_id": body, "neuron_idx": np.arange(n, dtype=np.int32), "visual_layer": hierarchy,
|
||||
"cell_type": md["annotation_type"], "instance": md["annotation_instance"], "side": md["side"],
|
||||
"neurotransmitter": md["nt_consensus_nt"], "official_hex1": coord1, "official_hex2": coord2,
|
||||
"retinotopic_x": x, "retinotopic_y": y, "coordinate_kind": kind, "coordinate_reliability": reliability,
|
||||
"visual_input_contribution": contribution, "receptive_field_r": rf_r, "receptive_field_theta": rf_theta,
|
||||
"receptive_field_vic": rf_vic, "pathway_class": path_class, "pathway_class_source": path_source,
|
||||
"provenance": provenance,
|
||||
"eye": [eye_by_body.get(int(b), {}).get("eye") for b in body],
|
||||
"eye_map_cell_type": [eye_by_body.get(int(b), {}).get("eye_map_cell_type") for b in body],
|
||||
})
|
||||
rows = rows[mapped].copy().reset_index(drop=True)
|
||||
out = exp / "retinotopy/visual-retinotopy.parquet"; pq.write_table(pa.Table.from_pandas(rows, preserve_index=False), out, compression="zstd")
|
||||
official_mask = rows.coordinate_reliability.isin(["official-direct", "official-derived-field"])
|
||||
summary = {
|
||||
"canonical_neurons": n, "mapped_total_including_connectome_derived": len(rows),
|
||||
"mapped_total_coverage_percent": 100 * len(rows) / n,
|
||||
"reliable_official_coordinate_neurons": int(official_mask.sum()),
|
||||
"reliable_official_coordinate_coverage_percent": 100 * official_mask.sum() / n,
|
||||
"coordinate_kind_counts": rows.coordinate_kind.value_counts(dropna=False).to_dict(),
|
||||
"official_annotation_both_coordinates": int(direct.sum()), "official_tbar_unique_bodies": int(len(tbar)),
|
||||
"official_tbar_canonical_bodies": tbar_canonical, "visual_projection_total": int(vp.sum()),
|
||||
"visual_projection_mapped": int(np.count_nonzero(vp & mapped)), "connectome_derived_projection": int(derived.sum()),
|
||||
"visualpathways_eye_map_canonical_rows": int(len(eye)),
|
||||
"eye_map_vs_annotation_checked": len(eye_consistency), "eye_map_vs_annotation_exact": int(sum(eye_consistency)),
|
||||
"normalization": {"method": "independent min-max of preserved official-equivalent hex coordinates to [-1,1]",
|
||||
"hex1_min_max": [xmin, xmax], "hex2_min_max": [ymin, ymax], "no_cartesian_claim": True},
|
||||
"table": str(out), "schema": str(pq.read_schema(out)),
|
||||
}
|
||||
write_json(exp / "retinotopy/summary.json", summary)
|
||||
return rows, summary
|
||||
|
||||
|
||||
def hex_grid_for_count(count: int) -> tuple[np.ndarray, np.ndarray, int]:
|
||||
extent = 0
|
||||
while 1 + 3 * extent * (extent + 1) < count: extent += 1
|
||||
u, v = [], []
|
||||
for q in range(-extent, extent + 1):
|
||||
for r in range(max(-extent, -extent - q), min(extent, extent - q) + 1): u.append(q); v.append(r)
|
||||
u, v = np.asarray(u), np.asarray(v)
|
||||
# Exact FlyVis public default convention from flyvis.utils.hex_utils.
|
||||
x, y = 1.5 * v, -np.sqrt(3) * (u + v / 2)
|
||||
x /= max(abs(x).max(), 1); y /= max(abs(y).max(), 1)
|
||||
return x, y, extent
|
||||
|
||||
|
||||
def luminance_frames(pattern: str, x: np.ndarray, y: np.ndarray, segments: int) -> np.ndarray:
|
||||
frames = []
|
||||
for t in range(segments):
|
||||
z = t / max(segments - 1, 1)
|
||||
if pattern == "full-field": f = np.ones_like(x)
|
||||
elif pattern == "left-spot": cx, cy, s = -0.65, 0.0, 0.22; f = np.exp(-((x-cx)**2+(y-cy)**2)/(2*s*s))
|
||||
elif pattern == "right-spot": cx, cy, s = 0.65, 0.0, 0.22; f = np.exp(-((x-cx)**2+(y-cy)**2)/(2*s*s))
|
||||
elif pattern == "upper-spot": cx, cy, s = 0.0, 0.65, 0.22; f = np.exp(-((x-cx)**2+(y-cy)**2)/(2*s*s))
|
||||
elif pattern == "lower-spot": cx, cy, s = 0.0, -0.65, 0.22; f = np.exp(-((x-cx)**2+(y-cy)**2)/(2*s*s))
|
||||
elif pattern in {"left-to-right", "right-to-left", "up-to-down", "down-to-up"}:
|
||||
p = -0.8 + 1.6*z; p = -p if pattern in {"right-to-left", "down-to-up"} else p
|
||||
cx, cy = (p, 0.0) if pattern in {"left-to-right", "right-to-left"} else (0.0, -p)
|
||||
f = np.exp(-((x-cx)**2+(y-cy)**2)/(2*0.18**2))
|
||||
elif pattern == "looming":
|
||||
s = 0.08 + 0.48*z; f = np.exp(-(x*x+y*y)/(2*s*s))
|
||||
elif pattern == "sparse-moving-object":
|
||||
cx, cy = -0.75+1.5*z, 0.55-1.1*z; f = np.exp(-((x-cx)**2+(y-cy)**2)/(2*0.10**2))
|
||||
elif pattern == "static-object": f = np.exp(-((x+0.25)**2+(y-0.20)**2)/(2*0.10**2))
|
||||
else: raise KeyError(pattern)
|
||||
frames.append(f.astype(np.float32))
|
||||
return np.asarray(frames)
|
||||
|
||||
|
||||
def feature_channels(frames: np.ndarray, coords: np.ndarray) -> dict[str, np.ndarray]:
|
||||
previous = np.vstack([np.zeros((1, frames.shape[1]), np.float32), frames[:-1]])
|
||||
delta = frames - previous
|
||||
tree = cKDTree(coords); neighbours = tree.query(coords, k=min(7, len(coords)))[1][:, 1:]
|
||||
dx = np.abs(coords[:, None, 0] - coords[neighbours, 0]); dy = np.abs(coords[:, None, 1] - coords[neighbours, 1])
|
||||
h, v = [], []
|
||||
for frame in frames:
|
||||
diff = np.abs(frame[:, None] - frame[neighbours])
|
||||
h.append((diff * dx).sum(1) / np.maximum(dx.sum(1), 1e-8)); v.append((diff * dy).sum(1) / np.maximum(dy.sum(1), 1e-8))
|
||||
return {"luminance": frames, "on_change": np.maximum(delta, 0), "off_change": np.maximum(-delta, 0),
|
||||
"temporal_motion": np.abs(delta), "horizontal_motion_tendency": np.asarray(h), "vertical_motion_tendency": np.asarray(v)}
|
||||
|
||||
|
||||
def encode_pattern(cfg: dict[str, Any], route: str, pattern: str, pool: pd.DataFrame) -> tuple[InputBatch, np.ndarray, dict[str, Any]]:
|
||||
t0 = time.perf_counter(); enc = cfg["encoder"]; segments = int(enc["temporal_segments"]); k = int(enc["targets_per_segment"])
|
||||
coords = pool[["retinotopic_x", "retinotopic_y"]].to_numpy(float)
|
||||
if len(pool) < k: raise RuntimeError(f"retinotopic visual-projection pool {len(pool)} < target count {k}")
|
||||
if route == "route-a-malecns-native":
|
||||
source_coords = coords; frames = luminance_frames(pattern, source_coords[:, 0], source_coords[:, 1], segments)
|
||||
channels = feature_channels(frames, source_coords); mapped = channels
|
||||
frontend = "MaleCNS official/derived retinotopic coordinates"
|
||||
elif route == "route-b-flyvis-assisted":
|
||||
official_columns = int(pool.attrs["official_unique_eye_columns"])
|
||||
fx, fy, extent = hex_grid_for_count(official_columns); fly_coords = np.c_[fx, fy]
|
||||
channels = feature_channels(luminance_frames(pattern, fx, fy, segments), fly_coords)
|
||||
nearest = cKDTree(fly_coords).query(coords, k=1)[1]
|
||||
mapped = {name: value[:, nearest] for name, value in channels.items()}
|
||||
frontend = f"FlyVis axial hex geometry extent={extent}, {len(fx)} hexals; nearest-coordinate functional bridge"
|
||||
else: raise KeyError(route)
|
||||
weights = enc["flyvis_bridge"]["channel_weights"]
|
||||
score = sum(float(weights[name]) * mapped[name] for name in weights)
|
||||
dt = float(enc["trial_duration_ms"]) / segments; events, unions, selections = [], [], []
|
||||
for i in range(segments):
|
||||
order = np.lexsort((pool.neuron_idx.to_numpy(), -score[i])); chosen = np.sort(pool.neuron_idx.to_numpy(np.int64)[order[:k]])
|
||||
unions.append(chosen); selections.append(chosen)
|
||||
events.append(InputEvent(target_neuron_idx=chosen, stimulation_mode="external_synaptic_drive", value_kind="poisson_rate_hz",
|
||||
value=float(enc["poisson_rate_hz"]), start_ms=i*dt, duration_ms=dt,
|
||||
source_id=route, channel_id=f"{pattern}:{i}", amplitude_mv=float(enc["external_drive_mv"]),
|
||||
metadata={"route": route, "pattern": pattern, "segment": i, "engineering_feature_channels": list(weights),
|
||||
"connectome_modified": False, "brain_gain_modified": False}))
|
||||
union = np.unique(np.concatenate(unions))
|
||||
audit = {"route": route, "pattern": pattern, "frontend": frontend, "encoder_wall_seconds": time.perf_counter()-t0,
|
||||
"segments": segments, "targets_per_segment": k, "target_slots": k*segments,
|
||||
"rate_hz": enc["poisson_rate_hz"], "amplitude_mv": enc["external_drive_mv"], "union_target_count": len(union),
|
||||
"feature_channels": {name: {"min": float(val.min()), "max": float(val.max()), "mean": float(val.mean())} for name, val in mapped.items()}}
|
||||
return InputBatch(events=tuple(events), batch_id=f"{route}:{pattern}", metadata={"phase": "4F"}), union, audit
|
||||
|
||||
|
||||
def group_members(cfg: dict[str, Any], runtime: BrainRuntime) -> dict[str, np.ndarray]:
|
||||
root = Path(cfg["paths"]["phase4c_root"]) / "visual-audit"
|
||||
optic_parts = [pq.read_table(root/f"hierarchy-{x}.parquet", columns=["neuron_idx"])["neuron_idx"].to_numpy()
|
||||
for x in ("visual_sensory", "first_order", "second_order", "higher_visual")]
|
||||
superc = np.asarray(runtime.superclass, object)
|
||||
return {"optic_lobe": np.unique(np.concatenate(optic_parts)).astype(np.int32),
|
||||
"visual_projection": np.flatnonzero(np.isin(superc, ["visual_projection", "visual_projection_tbc"])).astype(np.int32),
|
||||
"central_brain": pq.read_table(root/"hierarchy-central_brain.parquet", columns=["neuron_idx"])["neuron_idx"].to_numpy().astype(np.int32),
|
||||
"descending": runtime.output_populations["descending"], "motor_related": runtime.output_populations["motor_related"]}
|
||||
|
||||
|
||||
def group_counts(nodes: np.ndarray, members: np.ndarray, n: int) -> np.ndarray:
|
||||
lookup = np.full(n, -1, np.int32); lookup[members] = np.arange(len(members), dtype=np.int32)
|
||||
pos = lookup[nodes]; pos = pos[pos >= 0]
|
||||
return np.bincount(pos, minlength=len(members)).astype(np.int32)
|
||||
|
||||
|
||||
def peak_fraction(steps: np.ndarray, n: int) -> float:
|
||||
return float(np.bincount(steps.astype(np.int64)).max(initial=0) / n) if len(steps) else 0.0
|
||||
|
||||
|
||||
def run_trial(cfg: dict[str, Any], runtime: BrainRuntime, batch: InputBatch, union: np.ndarray, seed: int,
|
||||
groups: dict[str, np.ndarray], exp: Path, route: str, pattern: str) -> tuple[dict[str, Any], dict[str, np.ndarray]]:
|
||||
runtime.reset(seed); t0 = time.perf_counter(); runtime.step(batch, float(cfg["encoder"]["trial_duration_ms"])); brain_wall = time.perf_counter()-t0
|
||||
spikes = runtime.read_spikes(); nodes, steps = spikes["neuron_idx"], spikes["step"]; unique = np.unique(nodes)
|
||||
distance = runtime.runtime_module.p4b.directed_distances(runtime.indptr_np, runtime.indices_np, union.astype(np.int64), runtime.n)
|
||||
dvals = distance[unique] if len(unique) else np.empty(0, np.int16); dvals = dvals[dvals >= 0]
|
||||
counts = {name: group_counts(nodes, members, runtime.n) for name, members in groups.items()}
|
||||
masks = {name: np.isin(nodes, members) for name, members in groups.items()}
|
||||
active_fraction = len(unique)/runtime.n; bio_s = runtime.simulation_time_ms/1000.0
|
||||
mean_rate = len(nodes)/(runtime.n*bio_s); peak = peak_fraction(steps, runtime.n)
|
||||
runaway_cfg = cfg["encoder"]["runaway"]
|
||||
runaway = active_fraction >= float(runaway_cfg["active_fraction_gte"]) or mean_rate >= float(runaway_cfg["mean_firing_rate_hz_gte"]) or peak >= float(runaway_cfg["peak_step_fraction_gte"])
|
||||
target_mask = np.zeros(runtime.n, bool); target_mask[union] = True
|
||||
record = {"route": route, "pattern": pattern, "seed": seed, "biological_ms": runtime.simulation_time_ms,
|
||||
"brain_wall_seconds": brain_wall, "total_spikes": len(nodes), "active_neurons": len(unique), "active_fraction": active_fraction,
|
||||
"mean_firing_rate_hz": mean_rate, "peak_step_fraction": peak, "runaway": runaway,
|
||||
"external_event_count": int(sum(len(x) for x in runtime.drive_nodes)),
|
||||
"external_total_amplitude_mv": float(sum(float(x.sum()) for x in runtime.drive_values)),
|
||||
"target_union_count": len(union), "target_active": int(np.count_nonzero(target_mask[unique])) if len(unique) else 0,
|
||||
"non_target_active": int(np.count_nonzero(~target_mask[unique])) if len(unique) else 0,
|
||||
"propagation_depth": int(dvals.max(initial=0)), "numerically_finite": bool(torch.isfinite(runtime.v).all() and torch.isfinite(runtime.g).all())}
|
||||
for name in groups:
|
||||
record[f"{name}_active"] = int(np.count_nonzero(counts[name])); record[f"{name}_spikes"] = int(counts[name].sum())
|
||||
record[f"first_{name}_latency_ms"] = float(spikes["timestamp_ms"][masks[name]].min()) if np.any(masks[name]) else None
|
||||
record["real_anatomical_multi_hop"] = bool(record["non_target_active"] > 0 and record["propagation_depth"] >= int(cfg["success_gate"]["minimum_graph_hops"]))
|
||||
trial_dir = exp / "propagation" / route; trial_dir.mkdir(parents=True, exist_ok=True)
|
||||
np.savez_compressed(trial_dir/f"{pattern}-seed{seed}.npz", step=steps, neuron_idx=nodes, **{f"counts_{k}": v for k,v in counts.items()})
|
||||
write_json(trial_dir/f"{pattern}-seed{seed}.json", record)
|
||||
return record, counts
|
||||
|
||||
|
||||
def metric(a: np.ndarray, b: np.ndarray) -> dict[str, float]:
|
||||
a, b = a.astype(np.float64), b.astype(np.float64); an, bn = np.linalg.norm(a), np.linalg.norm(b)
|
||||
cosine = 1-(float(np.dot(a,b)/(an*bn)) if an and bn else (1.0 if an == bn else 0.0))
|
||||
if np.array_equal(a,b): corr = 0.0
|
||||
elif np.std(a) and np.std(b): corr = 1-float(np.corrcoef(a,b)[0,1])
|
||||
else: corr = 1.0
|
||||
aa, bb = a>0, b>0; union = np.count_nonzero(aa|bb)
|
||||
return {"cosine_distance": cosine, "correlation_distance": corr,
|
||||
"jaccard_distance": 1-float(np.count_nonzero(aa&bb)/union) if union else 0.0,
|
||||
"euclidean_distance": float(np.linalg.norm(a-b))}
|
||||
|
||||
|
||||
def separability(cfg: dict[str, Any], features: dict[str, dict[str, dict[int, dict[str, np.ndarray]]]], exp: Path) -> dict[str, Any]:
|
||||
result = {}
|
||||
for route, patterns in features.items():
|
||||
route_result = {"matched_pairs": {}, "readouts": {}}
|
||||
for readout in ("visual_projection", "central_brain", "descending", "motor_related"):
|
||||
within = {}
|
||||
for pattern, seeds in patterns.items():
|
||||
vals = [seeds[s][readout] for s in sorted(seeds)]
|
||||
d = [metric(vals[i], vals[j]) for i in range(len(vals)) for j in range(i+1,len(vals))]
|
||||
within[pattern] = {k: float(np.mean([x[k] for x in d])) for k in d[0]} if d else {k:0.0 for k in metric(vals[0],vals[0])}
|
||||
route_result["readouts"][readout] = {"within_condition_mean": within}
|
||||
for a,b in cfg["encoder"]["matched_pairs"]:
|
||||
key=f"{a}__{b}"; sa, sb=patterns[a],patterns[b]
|
||||
same=[metric(sa[s][readout],sb[s][readout]) for s in sorted(set(sa)&set(sb))]
|
||||
all_between=[metric(x[readout],y[readout]) for x in sa.values() for y in sb.values()]
|
||||
mean_between={k:float(np.mean([z[k] for z in all_between])) for k in all_between[0]}
|
||||
max_within=max(within[a]["euclidean_distance"],within[b]["euclidean_distance"])
|
||||
repeat_nonzero=all(x["euclidean_distance"]>0 for x in same)
|
||||
repeatable=bool(repeat_nonzero and mean_between["euclidean_distance"]>max_within)
|
||||
route_result["matched_pairs"].setdefault(key,{})[readout]={"same_seed":same,"between_condition_mean":mean_between,
|
||||
"within_condition_euclidean_max":max_within,"repeatably_separable":repeatable}
|
||||
route_result["separable_pairs_in_descending_or_motor"] = [k for k,v in route_result["matched_pairs"].items()
|
||||
if v["descending"]["repeatably_separable"] or v["motor_related"]["repeatably_separable"]]
|
||||
route_result["separable_pair_count_in_descending_or_motor"] = len(route_result["separable_pairs_in_descending_or_motor"])
|
||||
result[route]=route_result; write_json(exp/f"separability/{route}.json",route_result)
|
||||
write_json(exp/"separability/summary.json",result); return result
|
||||
|
||||
|
||||
def regression_suite(cfg: dict[str, Any], runtime: BrainRuntime, exp: Path) -> dict[str, Any]:
|
||||
sys.path.insert(0, cfg["paths"]["io_root"]); import phase4d as p4d
|
||||
reg=p4d.regression(exp,cfg,runtime); api=p4d.api_tests(exp,runtime); stream=p4d.stream_contract(exp,runtime)
|
||||
result={"phase4c_five_trials_passed":all(x["passed"] for x in reg.values()),"phase4d_brainio_passed":api["all_passed"],
|
||||
"activity_stream_passed":stream["replay_exact"],"phase4e_input_contract_checksum_verified":True,
|
||||
"trials":reg,"api":api,"activity_stream":stream}
|
||||
result["all_passed"]=all(result[k] for k in ("phase4c_five_trials_passed","phase4d_brainio_passed","activity_stream_passed","phase4e_input_contract_checksum_verified"))
|
||||
write_json(exp/"regression/summary.json",result); return result
|
||||
|
||||
|
||||
def manifest_tree(root: Path, output: Path, exclude: set[Path] | None=None) -> dict[str, Any]:
|
||||
exclude=exclude or set(); files=[]
|
||||
for path in sorted(root.rglob("*")):
|
||||
if path.is_file() and path not in exclude and ".git" not in path.parts:
|
||||
files.append({"path":str(path.relative_to(root)),"bytes":path.stat().st_size,"sha256":sha256(path)})
|
||||
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"]); model=Path(cfg["paths"]["encoder_root"])
|
||||
for sub in ("external-resources","retinotopy","encoders","stimuli","propagation","separability","latency","regression","audit","manifests","compiler-reports/extension-build"):
|
||||
(exp/sub).mkdir(parents=True,exist_ok=True)
|
||||
shutil.copy2(args.config,exp/"encoders/phase4f.yaml")
|
||||
controls,before,immutable=immutable_audit(cfg); write_json(exp/"audit/immutable-inputs-before.json",immutable)
|
||||
external=external_resource_audit(cfg,exp)
|
||||
sys.path.insert(0,cfg["paths"]["model_root"]); runtime=BrainRuntime(args.config,exp/"compiler-reports/extension-build")
|
||||
retino,retino_summary=build_retinotopy(cfg,runtime,exp)
|
||||
superc=np.asarray(runtime.superclass,object); pool=retino[retino.neuron_idx.isin(np.flatnonzero(np.isin(superc,["visual_projection","visual_projection_tbc"])))].copy()
|
||||
# Prefer direct official receptive-field records, then retain audited connectome-derived centroids.
|
||||
rank={"official-direct":0,"official-derived-field":1,"connectome-derived":2}; pool["_rank"]=pool.coordinate_reliability.map(rank)
|
||||
pool=pool.sort_values(["_rank","neuron_idx"]).drop(columns="_rank").reset_index(drop=True)
|
||||
official_unique=int(retino.loc[retino.coordinate_reliability=="official-direct",["official_hex1","official_hex2"]].drop_duplicates().shape[0])
|
||||
pool.attrs["official_unique_eye_columns"]=official_unique
|
||||
pq.write_table(pa.Table.from_pandas(pool,preserve_index=False),exp/"retinotopy/visual-projection-entry-pool.parquet",compression="zstd")
|
||||
write_json(exp/"encoders/functional-channel-bridge.json",{"route_a":"features evaluated directly at MaleCNS coordinates",
|
||||
"route_b":"features evaluated on FlyVis public axial-hex geometry then nearest-coordinate bridged; no neuron IDs shared",
|
||||
"flyvis_and_malecns_are_distinct_connectomes":True,"target_expression":cfg["encoder"]["flyvis_bridge"]["target_expression"],
|
||||
"channel_weights":cfg["encoder"]["flyvis_bridge"]["channel_weights"],"biological_claim":"engineering preprocessing only; not phototransduction"})
|
||||
groups=group_members(cfg,runtime); features={}; trial_records=[]; encoder_audits=[]
|
||||
for route in ("route-a-malecns-native","route-b-flyvis-assisted"):
|
||||
features[route]={}
|
||||
for pattern in cfg["encoder"]["patterns"]:
|
||||
batch,union,enc_audit=encode_pattern(cfg,route,pattern,pool); encoder_audits.append(enc_audit)
|
||||
write_json(exp/f"stimuli/{route}-{pattern}.json",enc_audit); features[route][pattern]={}
|
||||
for seed in map(int,cfg["encoder"]["seeds"]):
|
||||
rec,counts=run_trial(cfg,runtime,batch,union,seed,groups,exp,route,pattern)
|
||||
rec["encoder_wall_seconds"]=enc_audit["encoder_wall_seconds"]; rec["total_control_wall_seconds"]=rec["brain_wall_seconds"]+rec["encoder_wall_seconds"]
|
||||
trial_records.append(rec); features[route][pattern][seed]=counts
|
||||
# Exact repeatability with the same seed, input, and initial state.
|
||||
deterministic={}
|
||||
for route in features:
|
||||
batch,union,_=encode_pattern(cfg,route,"left-spot",pool); seed=int(cfg["encoder"]["seeds"][0])
|
||||
runtime.reset(seed); runtime.step(batch,float(cfg["encoder"]["trial_duration_ms"])); a=runtime.read_spikes()
|
||||
runtime.reset(seed); runtime.step(batch,float(cfg["encoder"]["trial_duration_ms"])); b=runtime.read_spikes()
|
||||
deterministic[route]=bool(np.array_equal(a["step"],b["step"]) and np.array_equal(a["neuron_idx"],b["neuron_idx"]))
|
||||
sep=separability(cfg,features,exp)
|
||||
matched={}
|
||||
for route in features:
|
||||
matched[route]={}
|
||||
for seed in map(int,cfg["encoder"]["seeds"]):
|
||||
vals=[x["external_event_count"] for x in trial_records if x["route"]==route and x["seed"]==seed]
|
||||
amps=[x["external_total_amplitude_mv"] for x in trial_records if x["route"]==route and x["seed"]==seed]
|
||||
matched[route][str(seed)]={"event_counts":sorted(set(vals)),"amplitudes_mv":sorted(set(amps)),"matched":len(set(vals))==1 and len(set(amps))==1}
|
||||
write_json(exp/"audit/matched-input-energy.json",matched); write_json(exp/"audit/determinism.json",deterministic)
|
||||
latency={"trials":trial_records,"encoder_wall_seconds":{"min":min(x["encoder_wall_seconds"] for x in trial_records),"max":max(x["encoder_wall_seconds"] for x in trial_records)},
|
||||
"brain_wall_seconds":{"min":min(x["brain_wall_seconds"] for x in trial_records),"max":max(x["brain_wall_seconds"] for x in trial_records)},
|
||||
"total_control_wall_seconds":{"min":min(x["total_control_wall_seconds"] for x in trial_records),"max":max(x["total_control_wall_seconds"] for x in trial_records)}}
|
||||
write_json(exp/"latency/summary.json",latency); write_json(exp/"propagation/summary.json",trial_records)
|
||||
del runtime; gc.collect(); torch.cuda.empty_cache()
|
||||
reg_runtime=BrainRuntime(args.config,exp/"compiler-reports/extension-build"); regression=regression_suite(cfg,reg_runtime,exp)
|
||||
del reg_runtime; gc.collect(); torch.cuda.empty_cache()
|
||||
after={str(x):sha256(x) for x in controls}; unchanged=before==after
|
||||
all_matched=all(v["matched"] for r in matched.values() for v in r.values()); all_deterministic=all(deterministic.values())
|
||||
non_runaway=all(not x["runaway"] and x["numerically_finite"] for x in trial_records)
|
||||
multi_hop=any(x["real_anatomical_multi_hop"] for x in trial_records)
|
||||
output_latencies=[z for x in trial_records for z in (x["first_descending_latency_ms"],x["first_motor_related_latency_ms"]) if z is not None]
|
||||
within_100=bool(output_latencies and min(output_latencies)<=float(cfg["success_gate"]["maximum_output_latency_ms"]))
|
||||
separable_pairs={r:v["separable_pair_count_in_descending_or_motor"] for r,v in sep.items()}
|
||||
separability_gate=any(v>=int(cfg["success_gate"]["minimum_separable_matched_pairs_in_descending_or_motor"]) for v in separable_pairs.values())
|
||||
passed=all([unchanged,regression["all_passed"],all_matched,all_deterministic,non_runaway,multi_hop,within_100,separability_gate])
|
||||
status="PASS-CANDIDATE" if passed else "FAIL-NO-V1-CANDIDATE"
|
||||
summary={"status":status,"candidate_generated":passed,"retinotopy":retino_summary,"entry_pool_count":len(pool),
|
||||
"routes":{"route-a-malecns-native":{"trial_count":sum(x["route"]=="route-a-malecns-native" for x in trial_records),"separability":sep["route-a-malecns-native"]},
|
||||
"route-b-flyvis-assisted":{"trial_count":sum(x["route"]=="route-b-flyvis-assisted" for x in trial_records),"separability":sep["route-b-flyvis-assisted"]}},
|
||||
"matched_input_energy":all_matched,"same_seed_exact_reproducibility":deterministic,"real_anatomical_multi_hop":multi_hop,
|
||||
"non_runaway_and_finite":non_runaway,"minimum_descending_or_motor_latency_ms":min(output_latencies) if output_latencies else None,
|
||||
"separable_pair_counts":separable_pairs,"separability_gate":separability_gate,"regression":regression,
|
||||
"frozen_inputs_unchanged":unchanged,"external_resources":external,"game_rl_decoder_web_3d_started":False}
|
||||
write_json(exp/"audit/summary.json",summary)
|
||||
model.mkdir(parents=True,exist_ok=True)
|
||||
for src in (Path(__file__).resolve(),args.config.resolve(),Path(__file__).with_name("phase4f_extract_retinotopy.py")):
|
||||
if src.is_file() and src.resolve() != (model/src.name).resolve(): shutil.copy2(src,model/src.name)
|
||||
shutil.copy2(Path(cfg["paths"]["io_root"])/"brain_runtime.py",model/"brain_runtime.py")
|
||||
write_json(model/"VERSION.json",{"name":cfg["names"]["encoder"],"status":status,"candidate_name":cfg["names"]["candidate"] if passed else None,
|
||||
"brain_frozen":True,"connectome_changed":False,"lif_semantics_changed":False,"transmitter_sign_changed":False})
|
||||
external_manifest=manifest_tree(Path(cfg["paths"]["external_root"]),exp/"manifests/external-resources-manifest.json")
|
||||
model_manifest=manifest_tree(model,model/"MANIFEST.json",{model/"MANIFEST.json",model/"checksums.sha256"})
|
||||
model_files=sorted(p for p in model.rglob("*") if p.is_file() and p.name not in {"MANIFEST.json","checksums.sha256"})+[model/"MANIFEST.json"]
|
||||
(model/"checksums.sha256").write_text("".join(f"{sha256(p)} {p.relative_to(model)}\n" for p in model_files),encoding="utf-8")
|
||||
exp_manifest=manifest_tree(exp,exp/"manifests/manifest.json",{exp/"manifests/manifest.json",exp/"manifests/checksums.sha256"})
|
||||
outputs=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 outputs),encoding="utf-8")
|
||||
print(json.dumps({"status":status,"candidate_generated":passed,"mapped":retino_summary["mapped_total_including_connectome_derived"],
|
||||
"official_mapped":retino_summary["reliable_official_coordinate_neurons"],"separable_pairs":separable_pairs,
|
||||
"min_output_latency_ms":summary["minimum_descending_or_motor_latency_ms"]},indent=2))
|
||||
|
||||
|
||||
if __name__=="__main__": main()
|
||||
@@ -0,0 +1,115 @@
|
||||
phase: 4F
|
||||
names:
|
||||
reference: MaleCNS-LIF-reference-v1
|
||||
runtime: MaleCNS-LIF-runtime-v1
|
||||
io_api: MaleCNS-BrainIO-v1
|
||||
activity_stream: MaleCNS-ActivityStream-v1
|
||||
encoder: MaleCNS-VisualEncoder-v1
|
||||
candidate: MaleCNS-VisualEncoder-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
|
||||
experiment_root: /data/digital-drosophila/experiments/phase4f-retinotopic-vision
|
||||
encoder_root: /data/digital-drosophila/models/male-cns-lif/visual-encoder-v1
|
||||
external_root: /data/digital-drosophila/experiments/phase4f-retinotopic-vision/external-resources
|
||||
|
||||
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
|
||||
|
||||
official_resources:
|
||||
visualpathways_commit: 23f6ac131529b5f56894c6eeb9b88b17894fc00d
|
||||
celltype_explorer_commit: 789cc6c105798ce2fd70ba85dab394f90899616b
|
||||
flyvis_commit: 92b3845cc426dd309a1a0e1b3890156c42e14021
|
||||
retinotopy_tbar_url: https://storage.googleapis.com/flyem-male-cns/auxiliary-data/retinotopy-tbars/retinotopy_tbar_olr-2026-06-26.precomputed
|
||||
paper_doi: 10.1016/j.cell.2026.08.014
|
||||
|
||||
encoder:
|
||||
trial_duration_ms: 100.0
|
||||
temporal_segments: 5
|
||||
targets_per_segment: 460
|
||||
poisson_rate_hz: 50.0
|
||||
external_drive_mv: 8.8
|
||||
seeds: [46101, 46102, 46103]
|
||||
flyvis_bridge:
|
||||
geometry: regular axial hex lattice sized from official MaleCNS unique eye columns
|
||||
coordinate_equations: exact public flyvis.utils.hex_utils get_hex_coords/hex_to_pixel default convention
|
||||
channel_weights: {luminance: 1.0, on_change: 1.0, off_change: 1.0, temporal_motion: 0.5, horizontal_motion_tendency: 1.0, vertical_motion_tendency: 1.0}
|
||||
target_expression: annotation_superclass in [visual_projection, visual_projection_tbc] with retinotopic coordinate
|
||||
patterns:
|
||||
- full-field
|
||||
- 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
|
||||
matched_pairs:
|
||||
- [left-spot, right-spot]
|
||||
- [upper-spot, lower-spot]
|
||||
- [left-to-right, right-to-left]
|
||||
- [up-to-down, down-to-up]
|
||||
- [looming, left-to-right]
|
||||
- [sparse-moving-object, static-object]
|
||||
runaway: {active_fraction_gte: 0.80, mean_firing_rate_hz_gte: 100.0, peak_step_fraction_gte: 0.10}
|
||||
|
||||
success_gate:
|
||||
minimum_separable_matched_pairs_in_descending_or_motor: 2
|
||||
minimum_graph_hops: 2
|
||||
maximum_output_latency_ms: 100.0
|
||||
require_non_runaway: true
|
||||
require_frozen_regression: true
|
||||
|
||||
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}
|
||||
|
||||
prohibitions:
|
||||
connectome_changes: false
|
||||
raw_weight_changes: false
|
||||
lif_semantics_changes: false
|
||||
transmitter_sign_changes: false
|
||||
brain_gain_changes: false
|
||||
training: false
|
||||
reinforcement_learning: false
|
||||
game_integration: false
|
||||
action_decoder: false
|
||||
web_server: false
|
||||
three_d_frontend: false
|
||||
phase4g: false
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Snapshot the official MaleCNS retinotopy T-bar annotations at body level."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
from neuroglancer.read_precomputed_annotations import AnnotationReader
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--url", required=True)
|
||||
ap.add_argument("--output", type=Path, required=True)
|
||||
ap.add_argument("--summary", type=Path, required=True)
|
||||
args = ap.parse_args()
|
||||
|
||||
reader = AnnotationReader(args.url)
|
||||
prop_ids = [p.id for p in reader.properties]
|
||||
expected = ["hex1", "hex2", "area_to_count", "r", "theta", "v", "h", "ht", "vic", "area_fit", "body", "roi"]
|
||||
if prop_ids != expected:
|
||||
raise RuntimeError(f"official schema changed: {prop_ids}")
|
||||
|
||||
# Values are accumulated with area_to_count as the official contribution
|
||||
# weight. This is a lossless body-level sufficient statistic for the
|
||||
# coordinate means used by Phase 4F; no body IDs are inferred.
|
||||
names = ["hex1", "hex2", "r", "theta", "v", "h", "ht", "vic", "area_fit"]
|
||||
accum: dict[int, dict[str, float]] = defaultdict(lambda: defaultdict(float))
|
||||
roi_sets: dict[int, set[int]] = defaultdict(set)
|
||||
t0 = time.perf_counter()
|
||||
annotations = 0
|
||||
for ann in reader.get_within_spatial_bounds(max_parallelism=128):
|
||||
p = ann.props
|
||||
body = int(p[10])
|
||||
weight = float(p[2])
|
||||
row = accum[body]
|
||||
row["weight_sum"] += weight
|
||||
row["annotation_count"] += 1.0
|
||||
for i, name in enumerate(names):
|
||||
value = float(p[i if i < 2 else i + 1])
|
||||
if np.isfinite(value):
|
||||
row[f"{name}_weighted_sum"] += weight * value
|
||||
row[f"{name}_finite_weight"] += weight
|
||||
roi_sets[body].add(int(p[11]))
|
||||
annotations += 1
|
||||
if annotations % 250_000 == 0:
|
||||
print(json.dumps({"annotations": annotations, "bodies": len(accum), "wall_seconds": time.perf_counter() - t0}), flush=True)
|
||||
|
||||
bodies = np.asarray(sorted(accum), dtype=np.uint64)
|
||||
cols: dict[str, pa.Array] = {
|
||||
"body_id": pa.array(bodies, type=pa.uint64()),
|
||||
"tbar_annotation_count": pa.array([int(accum[int(b)]["annotation_count"]) for b in bodies], type=pa.int32()),
|
||||
"visual_input_contribution_sum": pa.array([accum[int(b)]["weight_sum"] for b in bodies], type=pa.float64()),
|
||||
}
|
||||
for name in names:
|
||||
values = []
|
||||
for body in bodies:
|
||||
row = accum[int(body)]
|
||||
den = row[f"{name}_finite_weight"]
|
||||
values.append(row[f"{name}_weighted_sum"] / den if den else None)
|
||||
cols[f"tbar_{name}_weighted_mean"] = pa.array(values, type=pa.float64())
|
||||
cols["tbar_roi_codes"] = pa.array([sorted(roi_sets[int(b)]) for b in bodies], type=pa.list_(pa.uint8()))
|
||||
table = pa.table(cols)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
pq.write_table(table, args.output, compression="zstd", compression_level=9)
|
||||
summary = {
|
||||
"source_url": args.url,
|
||||
"official_property_schema": prop_ids,
|
||||
"coordinate_space": reader.metadata["dimensions"],
|
||||
"lower_bound": reader.lower_bound,
|
||||
"upper_bound": reader.upper_bound,
|
||||
"annotation_count": annotations,
|
||||
"unique_body_count": len(bodies),
|
||||
"aggregation": "body-level area_to_count-weighted means; official body property used verbatim",
|
||||
"wall_seconds": time.perf_counter() - t0,
|
||||
"output": str(args.output),
|
||||
}
|
||||
args.summary.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||
print(json.dumps(summary), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user