OSCR

A neuroevolution-driven agent-based model of coral larvae settlement.

Code ↔ Paper

31 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 31 matches · 1 of them tie a paragraph to a whole file, not to given lines: a weak match, whose lines are not tinted
  1. [1] § 3. Methods › 3.2. Experiments › 3.2.2. Computational experiments – replications. ↔ coral-larvae-abm/Source/coral_larvae_abm/Public/Agent/LarvaAgent.h, lines 43–100 · score 0.92 · competency onset, inner neuron, forward strength, Trained controllers, genome length, volume
  2. [2] § 3. Methods › 3.2. Experiments › 3.2.2. Computational experiments – replications. ↔ coral-larvae-abm/Source/coral_larvae_abm/Private/SimulationManager.cpp, lines 147–220 · score 0.91 · rest selection rate, descent speed, inner neuron, selection strategy, mutation rate, genome length
  3. [3] § 4. Results › 4.2. Controller analysis and robustness ↔ coral-larvae-abm/Source/coral_larvae_abm/Public/Agent/LarvaAgent.h, lines 43–100 · score 0.85 · Simpler controller baselines, gradient taxis, fixed reactive rule, directional cue, evolved controller, threshold
  4. [4] § 4. Results › 4.2. Controller analysis and robustness ↔ Plotting/09_nn_cue_ablation.py, lines 76–107 · score 0.83 · step sensor traces, realized cue influence, controller intrinsic, variance weighted, synthetic, complemented
  5. [5] § 4. Results › 4.2. Controller analysis and robustness ↔ Plotting/18_weight_distribution.py, lines 1–19 · score 0.78 · sensory channel classes, Weight distribution, connection weights, evolved controllers, elite, pruning
  6. [6] § 3. Methods › 3.1. Model overview ↔ coral-larvae-abm/Source/coral_larvae_abm/Public/EvolutionManager.h, lines 46–93 · score 0.77 · parent genomes, mutation rate, genome length, offspring, Truncation, Elitism
  7. [7] § 3. Methods › 3.2. Experiments › 3.2.2. Computational experiments – replications. ↔ coral-larvae-abm/Source/coral_larvae_abm/Public/Experiments/ResultAnalysisFunctions.h, lines 18–51 · score 0.75 · generalized linear model, substrate area, Clarke, Evans, binomial, GLM
  8. [8] § 3. Methods › 3.2. Experiments › 3.2.2. Computational experiments – replications. ↔ coral-larvae-abm/Source/coral_larvae_abm/Private/Experiments/ResultAnalysisFunctions.cpp, lines 142–211 · score 0.74 · generalized linear model, substrate area, standard errors, binomial, slope, settlement
  9. [9] § 3. Methods › 3.1. Model overview ↔ coral-larvae-abm/Source/coral_larvae_abm/Private/DataGrid/DataGridPreprocessor.cpp, lines 153–218 · score 0.74 · particle motion, seawater, flow, density, calibrated, speed
  10. [10] § 3. Methods › 3.1. Model overview ↔ coral-larvae-abm/Source/coral_larvae_abm/Private/Agent/LarvaAgent.cpp, lines 377–385 · score 0.72 · constant downward behavioral, competency age, geotaxis, bias, swim, gated
  11. [11] § 4. Results ↔ Plotting/10_convergence_reliability.py, lines 57–116 · score 0.72 · E1 biological metric, genetic diversity, reliability, premature, collapse, median
  12. [12] § 4. Results ↔ Plotting/19_manuscript_distribution_figures.py, lines 78–85 · score 0.69 · E2 vertical distribution, bottom row, tube bottom, depth zones, descend, rounds
  13. [13] § 4. Results ↔ Plotting/08_analyze_validation_e3.R, lines 140–195 · score 0.67 · source zone, E3 horizontal, E3 vertical, height zones, farthest, nearest
  14. [14] § 4. Results ↔ Plotting/13_e1_patchsize_figure.py, the whole file · a weak match · score 0.67 · fixed CCA cover, maximum tile area, Correct settlement, genome seeds, patch, RNG
  15. [15] § 4. Results › 4.1. Robustness to environmental perturbations ↔ Plotting/20_sensitivity_figures.py, lines 1–27 · score 0.67 · 1–20 kHz, 0.1–1, frequency band, source distance, zone, larvae
  16. [16] § 3. Methods › 3.1. Model overview ↔ coral-larvae-abm/Source/coral_larvae_abm/Public/SimulationManager.h, lines 24–71 · score 0.66 · mutation rate, fitness functions, operators, Elitism, Stochastic, crossover
  17. [17] § 3. Methods › 3.2. Experiments › 3.2.2. Computational experiments – replications. ↔ coral-larvae-abm/Source/coral_larvae_abm/Public/SimulationManager.h, lines 24–71 · score 0.66 · CCA covered limestone, light cycle, overrides, vertical, tiles, trained
  18. [18] § 4. Results › 4.1. Robustness to environmental perturbations ↔ Plotting/20_sensitivity_figures.py, lines 1–27 · score 0.65 · frequency band, light attenuation, 160 dB, 153 dB, sweep, zone
  19. [19] § 3. Methods › 3.1. Model overview ↔ Plotting/09_nn_cue_ablation.py, lines 109–128 · score 0.65 · acoustic particle motion, internal state, channels, field, setup, weight
  20. [20] § 3. Methods › 3.2. Experiments › 3.2.2. Computational experiments – replications. ↔ Plotting/08_analyze_validation_e3.R, lines 238–287 · score 0.62 · way ANOVA, height zones, distance zones, horizontal, vertical, uniform
  21. [21] § 4. Results ↔ coral-larvae-abm/Source/coral_larvae_abm/Public/SimulationManager.h, lines 194–276 · score 0.61 · sound control, RNG seeds, genome seeds, sound source, V3E, V3C
  22. [22] § 4. Results › 4.2. Controller analysis and robustness ↔ coral-larvae-abm/Source/coral_larvae_abm/Public/Sensors/LarvalSensorBaseComponent.h, lines 22–39 · score 0.59 · Adding Gaussian noise, Sensory noise robustness, validation
  23. [23] § 3. Methods › 3.1. Model overview ↔ coral-larvae-abm/Source/coral_larvae_abm/Private/DataGrid/DataGridPreprocessor.cpp, lines 153–218 · score 0.59 · light intensity, particle motion, wavelength, salinity, pressure, temperature
  24. [24] § 4. Results ↔ Plotting/06_analyze_validation_e1_e2.R, lines 354–392 · score 0.58 · correct settlement rate, E1 settlement, E2 depth, metric, position
  25. [25] § 4. Results ↔ Plotting/08_analyze_validation_e3.R, lines 82–120 · score 0.54 · sound control, V3F, sound source, V3E, V3C, V3B
  26. [26] § 3. Methods › 3.1. Model overview ↔ coral-larvae-abm/Source/coral_larvae_abm/Private/DataGrid/DataGridPreprocessor.cpp, lines 220–277 · score 0.53 · Gaussian decay, CCA concentration, reef cells, positioned, larvae
  27. [27] § 3. Methods › 3.1. Model overview ↔ coral-larvae-abm/Source/coral_larvae_abm/Private/Agent/AgentBrainComponent.cpp, lines 167–188 · score 0.53 · target node, source nodes, neurons, connection, mapping, sensors
  28. [28] § 3. Methods › 3.1. Model overview ↔ coral-larvae-abm/Source/coral_larvae_abm/Private/Agent/LarvaAgent.cpp, lines 103–132 · score 0.53 · rotation angles, forward strength, settlement, larva
  29. [29] § 4. Results ↔ Plotting/10_convergence_reliability.py, lines 57–116 · score 0.53 · biological outcome, E1 correct, metric, tracking, population, depth
  30. [30] § 3. Methods › 3.1. Model overview ↔ coral-larvae-abm/Source/coral_larvae_abm/Public/GenomeAnalysisFunctions.h, lines 46–70 · score 0.52 · light intensity, particle motion, wavelength, pressure, temperature, CCA
  31. [31] § 3. Methods › 3.2. Experiments › 3.2.2. Computational experiments – replications. ↔ Plotting/09_nn_cue_ablation.py, lines 130–139 · score 0.52 · diel, shallow, manipulations, cycle, geometry, temperature

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 659 lines · 28 KB · MIT · 3 matches

  1. #!/usr/bin/env python3
  2. """
  3. NN cue-dominance / ablation analysis for the coral-larvae NE-ABM.
  4. Answers reviewer R1's "open the black box" request functionally, not just
  5. structurally: for each evolved controller we measure how much each sensed cue
  6. (sensor input) changes the controller's action outputs (cue-on vs cue-off),
  7. then aggregate across the successful controllers of each experiment.
  8. This is a faithful offline re-implementation of the C++ runtime:
  9. - gene bit layout .......... GenomeFunctions.h (FGene)
  10. - index renumbering ........ AgentBrainComponent::MakeRenumberedConnectionList
  11. - unused-neuron pruning .... AgentBrainComponent::RemoveUnusedNeurons / CanReachAction
  12. - recurrent forward pass ... NeuralNetFunctions::FeedForward (tanh, prev-step neuron state)
  13. - action activation ........ NeuralNetFunctions::ActivateActions (tanh per action)
  14. - active sensors ........... LarvaAgent adds exactly the sensors wired in the net
  15. (GetSensorClasses), so a genome's wired sensors ARE
  16. its active sensors -> controller-only sensitivity.
  17. It only reads the saved training genome files, so it is independent of the
  18. validation seed issue and does not touch the Unreal simulator (safe to run
  19. while E3 training is in progress).
  20. Outputs (under Plotting/output/):
  21. tables/nn_cue_influence_<exp>.csv per-sensor mean action influence + per-action
  22. tables/nn_cue_influence_ranked.csv combined ranked cue dominance across experiments
  23. tables/nn_structure_summary_<exp>.csv per-controller structural stats (final controllers)
  24. plots/nn_cue_influence_<exp>.png ranked cue-influence bar chart
  25. """
  26. import os
  27. import glob
  28. import math
  29. import numpy as np
  30. # ----------------------------------------------------------------------------
  31. # Constants mirrored from the C++ source
  32. # ----------------------------------------------------------------------------
  33. NUM_SENSORS = 16 # ESensorType::NUM_SENSORS (active sensors before it)
  34. NUM_ACTIONS = 5 # EActionType::NUM_ACTIONS
  35. NUM_NEURONS = 12 # MaxInnerNeurons used in all final training runs
  36. GENOME_LEN = 50
  37. GSENSOR = 1 # bSourceType==1 -> sensor ; ==0 -> neuron (GNeuron)
  38. GACTION = 1 # bTargetType==1 -> action ; ==0 -> neuron
  39. GNEURON = 0
  40. INITIAL_NEURON_OUTPUT = 0.5
  41. WEIGHT_SCALE = 8192.0
  42. SENSOR_NAMES = [
  43. "OSCILLATION", "AGE", "ENERGY", "ALTEROMONAS_BIOFILM", "CCA",
  44. "CCA_FORWARD_BACK", "CCA_UP_DOWN", "CCA_LEFT_RIGHT", "TEMPERATURE",
  45. "PRESSURE", "LIGHT_INTENSITY", "LIGHT_WAVELENGTH",
  46. "PARTICLE_MOTION_FORWARD_BACK", "PARTICLE_MOTION_UP_DOWN",
  47. "PARTICLE_MOTION_LEFT_RIGHT", "PARTICLE_MOTION",
  48. ]
  49. ACTION_NAMES = ["FORWARD", "ROTATE_YAW", "ROTATE_PITCH", "SET_OSC", "SETTLE"]
  50. # ----------------------------------------------------------------------------
  51. # Analysis configuration
  52. # ----------------------------------------------------------------------------
  53. REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  54. EXPERIMENTS = {
  55. # exp -> directory of the FINAL controller set actually used for validation
  56. # (reselected 2026-07-09/10 from the fresh UE5.8 rerun; SUS wins all three).
  57. "E1": os.path.join(REPO, "Training", "e1", "SUS"),
  58. "E2": os.path.join(REPO, "Training", "e2", "SUS"),
  59. "E3": os.path.join(REPO, "Training", "e3", "SUS_H3"),
  60. }
  61. # Final-config genome length per experiment (E3 winner uses the reduced genome = 25).
  62. GENOME_LENS = {"E1": 50, "E2": 50, "E3": 25}
  63. # ----------------------------------------------------------------------------
  64. # AP1 trajectory-based cue analysis (gold standard).
  65. #
  66. # When Content/Evolution/validation_trajectory_<exp>_<scenario>.csv files exist
  67. # (bLogPerStepTrajectory on during the validation rerun; see docs/findings.md
  68. # "Validation rerun infrastructure"), we can measure REALIZED cue influence
  69. # from real per-step sensor traces instead of synthetic on/off sweeps: for
  70. # each wired sensor, how much of the variance in each action output is
  71. # explained by that sensor's actual observed values during validation runs.
  72. # This complements (does not replace) the synthetic-sweep analysis above,
  73. # which measures a controller's structural sensitivity regardless of whether
  74. # that sensor's field is ever actually varying in a given experiment's
  75. # geometry.
  76. #
  77. # CSV schema (SimulationManager::FlushPerStepTrajectoryLog):
  78. # timestamp,experiment,scenario,validation_genome_seed,random_seed,
  79. # agent_index,sim_step,pos_x,pos_y,pos_z,
  80. # sensor_<ShortName> x NUM_SENSORS (enum order == SENSOR_NAMES order),
  81. # action_<ShortName> x NUM_ACTIONS (enum order == ACTION_NAMES order)
  82. # Decimals are DOT-separated (FString::SanitizeFloat), comma-separated columns.
  83. # ----------------------------------------------------------------------------
  84. _EVO = os.path.join(REPO, "coral-larvae-abm", "Content", "Evolution")
  85. TRAJECTORY_GLOB = {
  86. # Realized (variance-weighted) cue influence from real per-step sensor traces of the FINAL
  87. # controller sets only. E2: the winner's (SUS_H2) baseline + perturbation runs (excludes the
  88. # calibration probes SW_*/G5_*/RT_*/CAL and the 1-agent Pop-bug junk file). E1/E3 have no clean
  89. # stride-sampled trajectory yet (would need a dedicated -Traj V1A/V3A run); realized analysis is
  90. # skipped for them and the controller-intrinsic (synthetic-sweep) analysis stands alone.
  91. "E1": os.path.join(_EVO, "validation_trajectory_e1_V1Atraj.csv"),
  92. "E2": os.path.join(_EVO, "validation_trajectory_e2_E2Hb_SUS_H4b.csv"),
  93. "E3": os.path.join(_EVO, "validation_trajectory_e3_V3Atraj.csv"),
  94. }
  95. # ----------------------------------------------------------------------------
  96. # Per-experiment cue classification (AP2).
  97. #
  98. # This is a FACTUAL classification of which sensory channels carry a non-trivial
  99. # environmental field in each experiment's setup, NOT a fabricated variance. A
  100. # sensor whose field does not exist by design (e.g. acoustic particle motion in
  101. # the CCA-only E1 bowl, or light in the dark E3 tubes) is definitionally
  102. # uninformative and is excluded from the cue-dominance ranking. Internal-state
  103. # inputs (oscillator, age, energy) are not environmental cues and are reported
  104. # separately. Realized (variance-weighted) influence from real per-step sensor
  105. # traces is deferred to the trajectory-based analysis on the validation rerun
  106. # (see validation-rerun-plan.md, "AP1").
  107. # ----------------------------------------------------------------------------
  108. INTERNAL_SENSORS = {"OSCILLATION", "AGE", "ENERGY"}
  109. CCA_CUES = {"CCA", "CCA_FORWARD_BACK", "CCA_UP_DOWN", "CCA_LEFT_RIGHT",
  110. "ALTEROMONAS_BIOFILM"}
  111. PM_CUES = {"PARTICLE_MOTION", "PARTICLE_MOTION_FORWARD_BACK",
  112. "PARTICLE_MOTION_UP_DOWN", "PARTICLE_MOTION_LEFT_RIGHT"}
  113. LIGHT_CUES = {"LIGHT_INTENSITY", "LIGHT_WAVELENGTH"}
  114. # primary = strong manipulated/spatially varying field; weak = field defined but
  115. # near-constant in that geometry (kept, but flagged).
  116. EXP_FIELDS = {
  117. "E1": {"primary": set(CCA_CUES),
  118. "weak": {"TEMPERATURE", "PRESSURE"}}, # shallow bowl -> flat T/P
  119. "E2": {"primary": set(CCA_CUES) | set(LIGHT_CUES) | {"TEMPERATURE", "PRESSURE"},
  120. "weak": set()}, # 2.2 m tube + diel cycle
  121. "E3": {"primary": set(PM_CUES),
  122. "weak": {"TEMPERATURE", "PRESSURE"}},
  123. }
  124. def sensor_role(exp, sensor_name):
  125. if sensor_name in INTERNAL_SENSORS:
  126. return "internal"
  127. fields = EXP_FIELDS.get(exp, {"primary": set(), "weak": set()})
  128. if sensor_name in fields["primary"]:
  129. return "environmental_primary"
  130. if sensor_name in fields.get("weak", set()):
  131. return "environmental_weak"
  132. return "inactive_no_field"
  133. TOP_N_PER_SEED = 10 # analyse the top-N (elite / "successful") genomes per seed
  134. T_STEPS = 80 # recurrent steps to reach steady controller state
  135. MC_SAMPLES = 48 # random background input vectors per controller
  136. SEED_RANGE = range(1, 31)
  137. RNG = np.random.default_rng(20260706) # fixed for reproducibility (no Date/rand)
  138. OUT_TABLES = os.path.join(REPO, "Plotting", "output", "tables")
  139. OUT_PLOTS = os.path.join(REPO, "Plotting", "output", "plots")
  140. os.makedirs(OUT_TABLES, exist_ok=True)
  141. os.makedirs(OUT_PLOTS, exist_ok=True)
  142. # ----------------------------------------------------------------------------
  143. # Genome decoding + wiring (faithful to the C++ pipeline)
  144. # ----------------------------------------------------------------------------
  145. def decode_gene(hexstr):
  146. n = int(hexstr, 16) & 0xFFFFFFFF
  147. bf = n & 0xFFFF
  148. w_raw = (n >> 16) & 0xFFFF
  149. weight = (w_raw - 0x10000) if w_raw >= 0x8000 else w_raw
  150. b_src = bf & 0x1
  151. s_idx = (bf >> 1) & 0x7F
  152. b_tgt = (bf >> 8) & 0x1
  153. t_idx = (bf >> 9) & 0x7F
  154. return {
  155. "b_src": b_src, "s_idx": s_idx,
  156. "b_tgt": b_tgt, "t_idx": t_idx,
  157. "w": weight / WEIGHT_SCALE,
  158. }
  159. def renumber(genes):
  160. """MakeRenumberedConnectionList: mod indices by their node-type counts."""
  161. conns = []
  162. for g in genes:
  163. s_idx = g["s_idx"] % (NUM_NEURONS if g["b_src"] == GNEURON else NUM_SENSORS)
  164. t_idx = g["t_idx"] % (NUM_ACTIONS if g["b_tgt"] == GACTION else NUM_NEURONS)
  165. conns.append({"b_src": g["b_src"], "s_idx": s_idx,
  166. "b_tgt": g["b_tgt"], "t_idx": t_idx, "w": g["w"]})
  167. return conns
  168. def can_reach_action(neuron_idx, conns):
  169. """DFS over neuron->neuron/action edges (CanReachAction)."""
  170. stack = [neuron_idx]
  171. visited = set()
  172. while stack:
  173. cur = stack.pop()
  174. if cur in visited:
  175. continue
  176. visited.add(cur)
  177. for c in conns:
  178. if c["b_src"] == GNEURON and c["s_idx"] == cur:
  179. if c["b_tgt"] == GACTION:
  180. return True
  181. if c["b_tgt"] == GNEURON:
  182. stack.append(c["t_idx"])
  183. return False
  184. def prune(conns):
  185. """RemoveUnusedNeurons: drop neurons that cannot reach an action, and the
  186. connections feeding them; repeat until stable."""
  187. conns = list(conns)
  188. while True:
  189. neurons = set()
  190. for c in conns:
  191. if c["b_tgt"] == GNEURON:
  192. neurons.add(c["t_idx"])
  193. if c["b_src"] == GNEURON:
  194. neurons.add(c["s_idx"])
  195. dead = {n for n in neurons if not can_reach_action(n, conns)}
  196. if not dead:
  197. break
  198. # remove connections whose TARGET is a dead neuron (RemoveConnectionsToNeuron)
  199. conns = [c for c in conns
  200. if not (c["b_tgt"] == GNEURON and c["t_idx"] in dead)]
  201. return conns
  202. def build_net(genes):
  203. """CreateWiringForGenome -> compact recurrent net.
  204. Returns (connections, n_neurons, driven_mask, wired_sensors).
  205. Connections use remapped 0..k-1 neuron ids.
  206. """
  207. conns = prune(renumber(genes))
  208. # surviving neurons = neuron ids still present as a source-neuron or target-neuron
  209. surviving = []
  210. seen = set()
  211. for c in conns:
  212. if c["b_src"] == GNEURON and c["s_idx"] not in seen:
  213. seen.add(c["s_idx"]); surviving.append(c["s_idx"])
  214. if c["b_tgt"] == GNEURON and c["t_idx"] not in seen:
  215. seen.add(c["t_idx"]); surviving.append(c["t_idx"])
  216. remap = {old: new for new, old in enumerate(surviving)}
  217. k = len(surviving)
  218. net = []
  219. for c in conns:
  220. s_idx = remap[c["s_idx"]] if c["b_src"] == GNEURON else c["s_idx"]
  221. t_idx = remap[c["t_idx"]] if c["b_tgt"] == GNEURON else c["t_idx"]
  222. net.append((c["b_src"], s_idx, c["b_tgt"], t_idx, c["w"]))
  223. # bDriven: neuron has >=1 non-self input (from sensor or other neuron)
  224. driven = np.zeros(k, dtype=bool)
  225. for (b_src, s_idx, b_tgt, t_idx, w) in net:
  226. if b_tgt == GNEURON:
  227. is_self = (b_src == GNEURON and s_idx == t_idx)
  228. if not is_self:
  229. driven[t_idx] = True
  230. wired_sensors = sorted({s_idx for (b_src, s_idx, _, _, _) in net
  231. if b_src == GSENSOR})
  232. return net, k, driven, wired_sensors
  233. # ----------------------------------------------------------------------------
  234. # Vectorised recurrent forward pass (batch over MC samples)
  235. # ----------------------------------------------------------------------------
  236. def forward_batch(net, k, driven, sensor_in):
  237. """sensor_in: (B, NUM_SENSORS). Returns actions (B, NUM_ACTIONS) after tanh.
  238. Runs T_STEPS recurrent updates with constant inputs, then one action pass
  239. from the steady neuron state. Mirrors FeedForward + ActivateActions.
  240. """
  241. B = sensor_in.shape[0]
  242. neuron_out = np.full((B, k), INITIAL_NEURON_OUTPUT, dtype=np.float64)
  243. for _ in range(T_STEPS):
  244. neuron_acc = np.zeros((B, k), dtype=np.float64)
  245. for (b_src, s_idx, b_tgt, t_idx, w) in net:
  246. if b_tgt != GNEURON:
  247. continue # action targets don't affect neuron state
  248. src = sensor_in[:, s_idx] if b_src == GSENSOR else neuron_out[:, s_idx]
  249. neuron_acc[:, t_idx] += src * w
  250. upd = np.tanh(neuron_acc)
  251. if k:
  252. neuron_out[:, driven] = upd[:, driven]
  253. action_lvl = np.zeros((B, NUM_ACTIONS), dtype=np.float64)
  254. for (b_src, s_idx, b_tgt, t_idx, w) in net:
  255. if b_tgt != GACTION:
  256. continue
  257. src = sensor_in[:, s_idx] if b_src == GSENSOR else neuron_out[:, s_idx]
  258. action_lvl[:, t_idx] += src * w
  259. return np.tanh(action_lvl)
  260. def cue_influence(net, k, driven, wired_sensors):
  261. """For each wired sensor: mean |action(on) - action(off)| over MC random
  262. backgrounds. Returns (n_wired,) x (NUM_ACTIONS) influence matrix keyed by sensor idx."""
  263. infl = {} # sensor_idx -> (NUM_ACTIONS,) mean abs delta
  264. if not wired_sensors:
  265. return infl
  266. # random background: wired sensors ~ U[0,1], others 0
  267. base = np.zeros((MC_SAMPLES, NUM_SENSORS), dtype=np.float64)
  268. base[:, wired_sensors] = RNG.random((MC_SAMPLES, len(wired_sensors)))
  269. for s in wired_sensors:
  270. on = base.copy(); on[:, s] = 1.0
  271. off = base.copy(); off[:, s] = 0.0
  272. a_on = forward_batch(net, k, driven, on)
  273. a_off = forward_batch(net, k, driven, off)
  274. infl[s] = np.abs(a_on - a_off).mean(axis=0) # (NUM_ACTIONS,)
  275. return infl
  276. # ----------------------------------------------------------------------------
  277. # Genome file parsing
  278. # ----------------------------------------------------------------------------
  279. def load_genomes(path, genome_len=GENOME_LEN):
  280. """Returns list of (genes, fitness), genes = list of decoded genes."""
  281. out = []
  282. with open(path, "r") as fh:
  283. lines = [ln.strip() for ln in fh if ln.strip()]
  284. i = 0
  285. while i < len(lines):
  286. gene_line = lines[i]
  287. fit = math.nan
  288. if i + 1 < len(lines) and lines[i + 1].startswith(";"):
  289. parts = lines[i + 1].split()
  290. if len(parts) >= 2:
  291. try:
  292. fit = float(parts[1])
  293. except ValueError:
  294. fit = math.nan
  295. i += 2
  296. else:
  297. i += 1
  298. toks = gene_line.split()
  299. if len(toks) < genome_len:
  300. continue
  301. genes = [decode_gene(t) for t in toks]
  302. out.append((genes, fit))
  303. return out
  304. # ----------------------------------------------------------------------------
  305. # Structural summary (reproduce nn-structure-summary on final controllers)
  306. # ----------------------------------------------------------------------------
  307. def structural_stats(net, k):
  308. n_conn = len(net)
  309. if n_conn == 0:
  310. return dict(active_neurons=0, eff_connections=0, mean_abs_weight=0.0,
  311. s2n=0, s2a=0, n2n=0, n2a=0)
  312. s2n = s2a = n2n = n2a = 0
  313. wsum = 0.0
  314. for (b_src, s_idx, b_tgt, t_idx, w) in net:
  315. wsum += abs(w)
  316. if b_src == GSENSOR and b_tgt == GNEURON: s2n += 1
  317. elif b_src == GSENSOR and b_tgt == GACTION: s2a += 1
  318. elif b_src == GNEURON and b_tgt == GNEURON: n2n += 1
  319. elif b_src == GNEURON and b_tgt == GACTION: n2a += 1
  320. return dict(active_neurons=k, eff_connections=n_conn,
  321. mean_abs_weight=wsum / n_conn,
  322. s2n=s2n, s2a=s2a, n2n=n2n, n2a=n2a)
  323. # ----------------------------------------------------------------------------
  324. # Main analysis per experiment
  325. # ----------------------------------------------------------------------------
  326. def analyse_experiment(exp, directory):
  327. print(f"\n=== {exp} ({directory}) ===")
  328. influence_rows = [] # per (genome) -> per sensor
  329. struct_rows = []
  330. n_controllers = 0
  331. # accumulator: sensor_idx -> list of total influence (summed over actions)
  332. per_sensor_total = {s: [] for s in range(NUM_SENSORS)}
  333. per_sensor_byaction = {s: [] for s in range(NUM_SENSORS)} # list of (NUM_ACTIONS,)
  334. wired_count = {s: 0 for s in range(NUM_SENSORS)}
  335. for seed in SEED_RANGE:
  336. matches = glob.glob(os.path.join(
  337. directory, f"best_genomes_*seed_{seed:02d}.txt"))
  338. if not matches:
  339. print(f" seed {seed:02d}: MISSING")
  340. continue
  341. genomes = load_genomes(matches[0], GENOME_LENS.get(exp, GENOME_LEN))
  342. genomes.sort(key=lambda gf: (-gf[1] if not math.isnan(gf[1]) else 0.0))
  343. elite = genomes[:TOP_N_PER_SEED]
  344. for genes, fit in elite:
  345. net, k, driven, wired = build_net(genes)
  346. n_controllers += 1
  347. st = structural_stats(net, k)
  348. st.update(experiment=exp, seed=seed, fitness=fit)
  349. struct_rows.append(st)
  350. infl = cue_influence(net, k, driven, wired)
  351. for s in wired:
  352. wired_count[s] += 1
  353. for s, vec in infl.items():
  354. per_sensor_total[s].append(float(vec.sum()))
  355. per_sensor_byaction[s].append(vec)
  356. # aggregate cue influence
  357. infl_rows = []
  358. for s in range(NUM_SENSORS):
  359. vals = per_sensor_total[s]
  360. if not vals:
  361. continue
  362. arr = np.array(vals)
  363. by = np.array(per_sensor_byaction[s]) # (n, NUM_ACTIONS)
  364. row = dict(
  365. experiment=exp,
  366. sensor=SENSOR_NAMES[s],
  367. role=sensor_role(exp, SENSOR_NAMES[s]),
  368. n_controllers_wiring=len(vals),
  369. frac_controllers=len(vals) / max(1, n_controllers),
  370. mean_total_influence=float(arr.mean()),
  371. se_total_influence=float(arr.std(ddof=1) / math.sqrt(len(arr))) if len(arr) > 1 else 0.0,
  372. )
  373. for ai, an in enumerate(ACTION_NAMES):
  374. row[f"infl_{an}"] = float(by[:, ai].mean())
  375. infl_rows.append(row)
  376. infl_rows.sort(key=lambda r: -r["mean_total_influence"])
  377. env_rows = [r for r in infl_rows
  378. if r["role"] in ("environmental_primary", "environmental_weak")]
  379. internal_rows = [r for r in infl_rows if r["role"] == "internal"]
  380. # write tables (full table keeps every wired sensor + role; environmental
  381. # table is the headline cue-dominance ranking used for interpretation)
  382. import csv
  383. infl_path = os.path.join(OUT_TABLES, f"nn_cue_influence_{exp}.csv")
  384. if infl_rows:
  385. with open(infl_path, "w", newline="") as fh:
  386. w = csv.DictWriter(fh, fieldnames=list(infl_rows[0].keys()))
  387. w.writeheader(); w.writerows(infl_rows)
  388. env_path = os.path.join(OUT_TABLES, f"nn_cue_influence_{exp}_environmental.csv")
  389. if env_rows:
  390. with open(env_path, "w", newline="") as fh:
  391. w = csv.DictWriter(fh, fieldnames=list(env_rows[0].keys()))
  392. w.writeheader(); w.writerows(env_rows)
  393. struct_path = os.path.join(OUT_TABLES, f"nn_structure_summary_{exp}.csv")
  394. if struct_rows:
  395. with open(struct_path, "w", newline="") as fh:
  396. w = csv.DictWriter(fh, fieldnames=list(struct_rows[0].keys()))
  397. w.writeheader(); w.writerows(struct_rows)
  398. # console ranking (headline = environmental cues only)
  399. print(f" controllers analysed: {n_controllers}")
  400. print(f" ENVIRONMENTAL CUE DOMINANCE (informative fields only):")
  401. print(f" {'sensor':<28}{'role':<24}{'mean_infl':>10}{'frac':>7} top action")
  402. for r in env_rows:
  403. top_act = max(ACTION_NAMES, key=lambda a: r[f"infl_{a}"])
  404. print(f" {r['sensor']:<28}{r['role']:<24}{r['mean_total_influence']:>10.4f}"
  405. f"{r['frac_controllers']:>7.2f} {top_act}")
  406. if internal_rows:
  407. print(f" internal-state inputs (not environmental cues):")
  408. for r in internal_rows:
  409. print(f" {r['sensor']:<26}{r['mean_total_influence']:>10.4f}")
  410. # plot
  411. try:
  412. import matplotlib
  413. matplotlib.use("Agg")
  414. import matplotlib.pyplot as plt
  415. if env_rows:
  416. names = [r["sensor"] for r in env_rows]
  417. vals = [r["mean_total_influence"] for r in env_rows]
  418. ses = [r["se_total_influence"] for r in env_rows]
  419. colors = ["#3b7a9e" if r["role"] == "environmental_primary" else "#b0b0b0"
  420. for r in env_rows]
  421. fig, ax = plt.subplots(figsize=(7, 0.4 * len(names) + 1.6))
  422. ax.barh(range(len(names)), vals, xerr=ses, color=colors)
  423. ax.set_yticks(range(len(names))); ax.set_yticklabels(names, fontsize=8)
  424. ax.invert_yaxis()
  425. ax.set_xlabel("Mean |action change| (cue on vs off), summed over actions")
  426. ax.set_title(f"{exp}: environmental cue influence on evolved controllers\n"
  427. f"(top {TOP_N_PER_SEED}/seed, n={n_controllers})")
  428. from matplotlib.patches import Patch
  429. ax.legend(handles=[Patch(color="#3b7a9e", label="primary field"),
  430. Patch(color="#b0b0b0", label="weak field")],
  431. loc="lower right", fontsize=8)
  432. fig.tight_layout()
  433. fig.savefig(os.path.join(OUT_PLOTS, f"nn_cue_influence_{exp}.png"), dpi=150)
  434. plt.close(fig)
  435. except Exception as e:
  436. print(f" (plot skipped: {e})")
  437. return env_rows
  438. # ----------------------------------------------------------------------------
  439. # Trajectory-based (gold-standard) cue analysis using real per-step sensor
  440. # and action traces from the AP1 validation-rerun logging.
  441. # ----------------------------------------------------------------------------
  442. def load_trajectory_csv(path):
  443. """Parse one validation_trajectory_<exp>_<scenario>.csv (comma-separated,
  444. dot decimals). Returns dict of column name -> list, or None if the file
  445. is empty/malformed."""
  446. import csv as csv_mod
  447. rows = []
  448. with open(path, "r", newline="") as fh:
  449. reader = csv_mod.DictReader(fh)
  450. for row in reader:
  451. rows.append(row)
  452. if not rows:
  453. return None
  454. # Header uses mnemonic short names, not positional indices; recover the
  455. # sensor/action column order directly from the header instead (enum
  456. # order is preserved by the C++ writer, but don't assume the names).
  457. fieldnames = reader.fieldnames or []
  458. sensor_cols = [c for c in fieldnames if c.startswith("sensor_")]
  459. action_cols = [c for c in fieldnames if c.startswith("action_")]
  460. if len(sensor_cols) != NUM_SENSORS or len(action_cols) != NUM_ACTIONS:
  461. print(f" WARNING: {os.path.basename(path)} has {len(sensor_cols)} sensor "
  462. f"cols / {len(action_cols)} action cols, expected {NUM_SENSORS}/{NUM_ACTIONS} "
  463. f"-> skipping")
  464. return None
  465. def to_f(x):
  466. try:
  467. return float(x)
  468. except (TypeError, ValueError):
  469. return math.nan
  470. sensors = np.array([[to_f(r[c]) for c in sensor_cols] for r in rows])
  471. actions = np.array([[to_f(r[c]) for c in action_cols] for r in rows])
  472. return {
  473. "sensor_cols": sensor_cols,
  474. "action_cols": action_cols,
  475. "sensors": sensors, # (n_steps, NUM_SENSORS), header order
  476. "actions": actions, # (n_steps, NUM_ACTIONS), header order
  477. "scenario": rows[0].get("scenario", ""),
  478. "agent_index": [r.get("agent_index") for r in rows],
  479. }
  480. def realized_cue_influence(exp, glob_pattern):
  481. """For each sensor column actually present in the trajectory logs: realized
  482. influence = |corr(sensor, action)| averaged over actions, weighted by the
  483. number of steps contributing (i.e. pooled across all matching files),
  484. using the ACTUAL observed sensor variance (not a synthetic sweep). This
  485. directly answers "does this cue's real variation in the validation run
  486. move the controller's outputs", complementing the structural on/off sweep.
  487. """
  488. files = sorted(glob.glob(glob_pattern))
  489. if not files:
  490. print(f" no trajectory files found for {exp}: {glob_pattern}")
  491. return []
  492. print(f" found {len(files)} trajectory file(s) for {exp}")
  493. pooled_sensors = []
  494. pooled_actions = []
  495. sensor_cols = action_cols = None
  496. for f in files:
  497. parsed = load_trajectory_csv(f)
  498. if parsed is None:
  499. continue
  500. if sensor_cols is None:
  501. sensor_cols, action_cols = parsed["sensor_cols"], parsed["action_cols"]
  502. elif parsed["sensor_cols"] != sensor_cols or parsed["action_cols"] != action_cols:
  503. print(f" WARNING: column order mismatch in {f}, skipping")
  504. continue
  505. pooled_sensors.append(parsed["sensors"])
  506. pooled_actions.append(parsed["actions"])
  507. if not pooled_sensors:
  508. return []
  509. sensors = np.concatenate(pooled_sensors, axis=0)
  510. actions = np.concatenate(pooled_actions, axis=0)
  511. n_steps = sensors.shape[0]
  512. # Map header short names back to SENSOR_NAMES/ACTION_NAMES via the enum
  513. # order the C++ writer uses (SensorShortName/ActionShortName iterate
  514. # ESensorType/EActionType in declaration order == SENSOR_NAMES/ACTION_NAMES).
  515. if len(sensor_cols) != len(SENSOR_NAMES) or len(action_cols) != len(ACTION_NAMES):
  516. print(" WARNING: trajectory column count does not match SENSOR_NAMES/ACTION_NAMES")
  517. rows = []
  518. for s_idx, s_name in enumerate(SENSOR_NAMES):
  519. if s_idx >= sensors.shape[1]:
  520. continue
  521. s_vals = sensors[:, s_idx]
  522. if np.all(np.isnan(s_vals)) or np.nanstd(s_vals) == 0:
  523. continue # constant/missing sensor -> no realized influence, not an error
  524. corrs = []
  525. for a_idx in range(actions.shape[1]):
  526. a_vals = actions[:, a_idx]
  527. mask = ~np.isnan(s_vals) & ~np.isnan(a_vals)
  528. if mask.sum() < 2 or np.nanstd(a_vals[mask]) == 0:
  529. corrs.append(0.0)
  530. continue
  531. c = np.corrcoef(s_vals[mask], a_vals[mask])[0, 1]
  532. corrs.append(0.0 if np.isnan(c) else abs(c))
  533. row = dict(
  534. experiment=exp,
  535. sensor=s_name,
  536. role=sensor_role(exp, s_name),
  537. n_steps=int(n_steps),
  538. observed_sd=float(np.nanstd(s_vals)),
  539. mean_abs_corr=float(np.mean(corrs)),
  540. )
  541. for ai, an in enumerate(ACTION_NAMES):
  542. row[f"abs_corr_{an}"] = corrs[ai] if ai < len(corrs) else float("nan")
  543. rows.append(row)
  544. rows.sort(key=lambda r: -r["mean_abs_corr"])
  545. return rows
  546. def analyse_trajectory(exp, glob_pattern):
  547. print(f"\n=== {exp} trajectory-based (gold-standard) cue analysis ===")
  548. rows = realized_cue_influence(exp, glob_pattern)
  549. if not rows:
  550. return []
  551. import csv as csv_mod
  552. out_path = os.path.join(OUT_TABLES, f"nn_cue_influence_trajectory_{exp}.csv")
  553. with open(out_path, "w", newline="") as fh:
  554. w = csv_mod.DictWriter(fh, fieldnames=list(rows[0].keys()))
  555. w.writeheader(); w.writerows(rows)
  556. print(f" wrote {out_path}")
  557. env_rows = [r for r in rows if r["role"] in ("environmental_primary", "environmental_weak")]
  558. print(f" REALIZED ENVIRONMENTAL CUE INFLUENCE (from actual validation-run sensor traces):")
  559. print(f" {'sensor':<28}{'role':<24}{'mean|corr|':>10}{'observed_sd':>13}")
  560. for r in env_rows:
  561. print(f" {r['sensor']:<28}{r['role']:<24}{r['mean_abs_corr']:>10.4f}{r['observed_sd']:>13.4f}")
  562. return rows
  563. def main():
  564. all_rows = []
  565. for exp, directory in EXPERIMENTS.items():
  566. if not os.path.isdir(directory):
  567. print(f"skip {exp}: {directory} not found")
  568. continue
  569. all_rows.extend(analyse_experiment(exp, directory))
  570. if all_rows:
  571. import csv
  572. combined = os.path.join(OUT_TABLES, "nn_cue_influence_ranked.csv")
  573. with open(combined, "w", newline="") as fh:
  574. w = csv.DictWriter(fh, fieldnames=list(all_rows[0].keys()))
  575. w.writeheader(); w.writerows(all_rows)
  576. print(f"\nwrote {combined}")
  577. # Gold-standard trajectory-based analysis, where AP1 logs are available.
  578. # Additive: does not affect the synthetic-sweep tables/plots above.
  579. for exp, glob_pattern in TRAJECTORY_GLOB.items():
  580. analyse_trajectory(exp, glob_pattern)
  581. if __name__ == "__main__":
  582. main()

09_nn_cue_ablation.py at commit 2b78a1f, under MIT · at the source

Overview

Authors: Sarah Wolpold1, Christian R. Voolstra2, Sebastian von Mammen1
  1. Department of Computer Science, Julius-Maximilians University, Würzburg, Germany
  2. Department of Biology, University of Konstanz, Konstanz, Germany
Institutions: University of Würzburg (Germany); University of Konstanz (Germany)
Journal: PloS one, volume 21, issue 9, article e0359316
Dates: received 15 December 2025; accepted 11 September 2026; published online 25 September 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pone.0359316 · PMID 42789678 · PMCID PMC13614647 · OpenAlex W7214388760
Open access: gold, a free copy (OpenAlex)
Status: code verified
Methods: Statistics, Graphs, fMRI & imaging
MeSH: Anthozoa*, Biological Evolution*, Models, Biological*, Animals, Coral Reefs, Ecosystem, Larva, Neural Networks, Computer (* major topic)
Journal subjects: Biology and Life Sciences, Developmental Biology, Life Cycles, Larvae, Marine Biology, Coral Reefs, Earth Sciences, Marine and Aquatic Sciences, Reefs, Genetics, Genomics, Neuroscience, Cognitive Science, Cognitive Psychology, Perception, Sensory Perception, Psychology, Social Sciences, Sensory Cues, Corals, Physical Sciences, Physics, Acoustics
Topic: Coral and Marine Ecosystems Studies (Ecology, Environmental Science), according to OpenAlex
Citations: not cited yet (Europe PMC); 35 references in the paper

Abstract

Coral reefs face significant threats due to climate change and human activities, which require innovative approaches to study and protect these ecosystems. Successful settlement is the bottleneck linking a reef’s free-swimming larvae to the benthic community, ultimately determining the next generation of corals and the long-term resilience of the entire ecosystem. This work introduces an agent-based model (ABM) driven by neuroevolution (NE) to simulate coral larval settlement behavior under various environmental conditions. Inspired by biological processes, the model combines sensory input with a neural network (NN) to guide larval actions, optimizing settlement success through evolutionary algorithms. The model replicates three experimental setups from previous studies, validating it against key metrics such as settlement success, vertical distribution, and orientation to environmental cues. The evolved controllers capture the characteristic cue responses of each experiment: crustose coralline algae (CCA)-driven settlement, the bimodal vertical distribution, and orientation towards reef sound, while also exposing the limits of the simplified environment, as its reduced hydrodynamics. Sensitivity analyses further indicate that these behaviors are driven by different mechanisms across the experiments, illustrating how the model yields interpretable, testable outcomes. This approach provides a basis for integrating adaptive behaviors into coral larval simulations, and future work will focus on refining the model to enhance biological realism and scalability.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 31 matches between paragraphs and lines of code.

sarahkcw/coral-larvae-abm

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 2b78a1f134a205493caf1611aa5953c3ad0b54be, 6 August 2026
Languages: C/C++ (170), R (29), C++ (16), Python (12)
Size: 398 files, 227 scripts
Software Heritage: not archived
Found in: “Data Availability”
Holds: README, license file
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: ggplot2 (24 files), tidyverse (20 files), Matplotlib (9 files), reshape2 (7 files), NumPy (4 files), SciPy (4 files), cowplot (2 files)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
229 files

The paper's code and data availability statement is in the Data section.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 227 scripts, each with its path and the digest of its content;
  • 31 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data Availability

Yes - all data are fully available without restriction; All datasets generated and analysed in this study are openly available. The per-run and per-agent training and validation exports, together with the evolved genomes, are archived on Zenodo (doi.org/10.5281/zenodo.21381268). The analysis-ready extracts and all code required to reproduce the tables and figures are available in the GitHub repository https://github.com/sarahkcw/coral-larvae-abm. No third-party or access-restricted data were used.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 3 authors, 8 MeSH terms, 27 references.

Cite

This paper

Wolpold, S., Voolstra, C. R., & von Mammen, S. (2026). A neuroevolution-driven agent-based model of coral larvae settlement. PloS one, 21(9), e0359316. https://doi.org/10.1371/journal.pone.0359316

BibTeX

@article{wolpold2026neuroevolution,
author = {Wolpold, Sarah and Voolstra, Christian R. and von Mammen, Sebastian},
title = {{A neuroevolution-driven agent-based model of coral larvae settlement}},
journal = {PloS one},
year = {2026},
month = sep,
volume = {21},
number = {9},
pages = {e0359316},
publisher = {PLOS},
issn = {1932-6203},
doi = {10.1371/journal.pone.0359316},
url = {https://doi.org/10.1371/journal.pone.0359316},
pmid = {42789678},
pmcid = {PMC13614647}
}

RIS

TY - JOUR
AU - Wolpold, Sarah
AU - Voolstra, Christian R.
AU - von Mammen, Sebastian
TI - A neuroevolution-driven agent-based model of coral larvae settlement
T2 - PloS one
J2 - PLoS One
PY - 2026
DA - 2026/09/25
VL - 21
IS - 9
SP - e0359316
SN - 1932-6203
PB - PLOS
DO - 10.1371/journal.pone.0359316
UR - https://doi.org/10.1371/journal.pone.0359316
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pone.0359316",
"type": "article-journal",
"title": "A neuroevolution-driven agent-based model of coral larvae settlement",
"container-title": "PloS one",
"author": [
{
"family": "Wolpold",
"given": "Sarah"
},
{
"family": "Voolstra",
"given": "Christian R."
},
{
"family": "von Mammen",
"given": "Sebastian"
}
],
"container-title-short": "PLoS One",
"volume": "21",
"issue": "9",
"page": "e0359316",
"DOI": "10.1371/journal.pone.0359316",
"PMID": "42789678",
"PMCID": "PMC13614647",
"ISSN": "1932-6203",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pone.0359316",
"language": "en",
"issued": {
"date-parts": [
[
2026,
9,
25
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41592-026-03211-w [code]
Spatial isoform sequencing at single-cell resolution reveals cell-type-specific spatial isoform variability in multiple brain cell types.
Journal: Nature methods
In common: cowplot, reshape2, ggplot2, 4 other tools
[2] doi:10.1038/s41592-026-03194-8 [code]
Beyond benchmarking: an expert-guided consensus approach to spatially aware clustering.
Journal: Nature methods
In common: cowplot, reshape2, ggplot2, 4 other tools
[3] doi:10.1038/s41467-026-76939-w [code]
HIPPIE: a generative model for electrophysiological analysis across species, technologies, and modalities.
Journal: Nature communications
In common: cowplot, reshape2, ggplot2, 4 other tools
[4] doi:10.1002/imt2.70163 [code]
Spatial multi-omics unveils sphingolipid metabolic reprogramming within the retinal pathological niche.
Journal: iMeta
In common: cowplot, reshape2, ggplot2, 4 other tools
[5] doi:10.1038/s41467-026-76675-1 [code]
Long-read proteogenomic atlas of human neuronal differentiation reveals isoform diversity informing neurodevelopmental risk mechanisms.
Journal: Nature communications
In common: cowplot, reshape2, ggplot2, 4 other tools
[6] doi:10.1126/sciadv.aeg3223 [code]
The extreme diversity of retinal amacrine cells has deep evolutionary roots.
Journal: Science advances
In common: cowplot, reshape2, ggplot2, 4 other tools
[7] doi:10.7554/elife.93640 [code]
Sibling chimerism among microglia in marmosets.
Journal: eLife
In common: cowplot, reshape2, ggplot2, 4 other tools
[8] doi:10.1093/bioinformatics/btag592 [code]
Network-based stratification of allele-specific expression reveals patient subgroups in Huntington's disease.
Journal: Bioinformatics (Oxford, England)
In common: cowplot, reshape2, ggplot2, 4 other tools
[9] doi:10.1038/s41467-026-75722-1 [code]
Single-nucleus analysis of the adult human olfactory epithelium uncovers shared neurogenesis programs with the brain.
Journal: Nature communications
In common: cowplot, reshape2, ggplot2, 4 other tools
[10] doi:10.1186/s13073-026-01704-z [code]
Gene expression profiling enables refined parcellation of cortical layers in the heterogeneous human cerebral cortex.
Journal: Genome medicine
In common: cowplot, reshape2, ggplot2, 4 other tools

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.