Skip to content

M1: Python/NumPy port, golden-tested against Miller's C++ #27

Description

@schmug

Task

Build the Python/NumPy port of the biosim4 simulator, golden-tested against David Miller's C++ — vectorised over the population. This is SPEC §7's M1, with one correction: SPEC's M1 gate is wrong. It reads "golden-output test: matches TS port step-for-step under fixed seed", but the conformance audit (#26) proved the TS port diverges from Miller in 23 verified ways across all 14 modules. Golden-testing against it would faithfully reproduce a broken reference. Miller's C++ is the ground truth. (ISSUES.md §6 half-anticipated this: "the golden test may need to reproduce upstream, not the port.")

Context — why port instead of patch

The full-sweep audit (#26, data/audit/cpp_conformance_audit.md) found 23 distinct new defects: 4 CRITICAL, 10 MAJOR, 8 MINOR. Zero of 14 modules clean. Its recommendation is PORT, argued from three facts:

  1. Patching's usual benefit is void. D3/D4/D10/D11/D12/D21 each change RNG draw counts or drawn values → every published baseline must be re-run under any option. The choice is not "cheap patch vs expensive rewrite"; it is "re-run on a substrate with 23 known defects and an unmeasured defect floor, vs re-run on one with dtypes and golden tests."
  2. The dominant defect class is eliminated by a port, and only by a port. 7 of 23 defects share one root cause: JavaScript has no integer types. The port transliterated C++ arithmetic without modelling C++ types. A typed port makes D3, D4, D6, D9, D12, D15, D20 unrepresentable, not merely fixed. Patching leaves the generator running — bug #4 M0 predictability harness → GATE: FAIL #4's "fix" changed which bit randomBitFlip flips but left the int16_t store semantics that give bit 15 its meaning; D3 is its unfixed half.
  3. The defect floor is NOT established. The single worst defect (D0 — 100% of GENETIC_SIM_FWD similarity scores wrong, live in both challenges, zero tests) survived a 14-module sweep with 28 adversarial skeptics, and was found only by asking "which files did nobody open?" SignalBeacon.ts, BeaconTrace.ts and HandGenome.ts (627 LOC, including the entire SIGNAL_BEACON challenge) still have zero coverage.

SPEC §4.2 independently requires this port: the TS sim's throughput "cannot support the experiment," and "the brain has to be a torch model anyway, so the JS/Python boundary is a problem regardless." Issue #25 is measuring the actual numbers.

Not STOP: the audit's own framing — "the science is sound; the substrate is not."

The six non-negotiables (from the audit, §5)

  1. Port Miller's unit tests as golden tests FIRST. They are already written and sitting in the vendored tree — 389 LOC of free oracles, and the audit's own skeptics used them to find defects:
    • unitTestGridVisitNeighborhood.cpp (35 LOC) — pins D12 in 7 cases
    • unitTestBasicTypes.cpp (312 LOC) — pins D17
    • unitTestConnectNeuralNetWiringFromGenome.cpp (42 LOC) — pins the decode/wiring
  2. Golden-test jaro_winkler_distance against the C++ on generated genome pairs. D0 is proof that an untested comparison function silently returns garbage forever. Reference artifact: data/audit/jw_verify.js (20,000/20,000 pairs diverge).
  3. Adopt Miller's 1-based indexing with a reserved slot 0. Verified in the C++: peeps.cpp:21// Index 0 is reserved, so add one: — and simulator.cpp:149// multithreaded loop: index 0 is reserved, start at 1. The port's 0-based choice is the root of three findings (D10, D11, D21 — including agent 0 is never stepped yet still breeds).
  4. Decide D2 explicitly and document it. Miller's neuron-array construction is itself buggy: he indexes by original key with std::map::operator[] default-insertion, pinning a subset of hidden neurons at a constant 0.5. The TS "fix" for known-bug #2 Seeded PRNG throughout #2 overshot — Miller does not drive them all; 57.5% of default brains differ in driven flags. Bug-for-bug fidelity means deliberately re-introducing frozen neurons. This directly touches the M0 question (how much usable recurrent state an agent has). Either choice is defensible; silence is not.
  5. State the determinism invariant once, globally. Miller-stream identity is unreachable (seeded mulberry32 vs his thread-private Jenkins) — say so explicitly, and audit self-determinism per seed instead. Then cost appendEpochLog's ~1100 undrawn RNG calls/generation (called from inside spawnNewGeneration.cpp:177, before initializeNewGeneration) against that invariant. The audit found every auditor asked only "does my function draw RNG?" — locally correct, globally useless.
  6. Re-run everything. Not optional under any option.

Typed-dtype requirements (the structural fix)

These are what make the dominant defect class unrepresentable:

  • np.int16 connection weights — restores bit 15 as the sign bit (D3: sign currently frozen for life; |w| reaches 8.0 vs Miller's 4.0)
  • makeRandomWeight over the full 0xffff range, not 0xefff (D4: birth weights currently 53.3% negative and capped at +3.4998)
  • 7-bit masked sourceNum/sinkNum gene fields, 0..127 (D9)
  • uint16 grid cells (the 0xffff barrier sentinel)
  • // integer division everywhere the C++ uses int division or (int) casts (D6, D12, D15, D20) — including visitNeighborhood's two (int) truncations and Coord.length()'s (int)sqrt

Scope

Port, vectorised over the population where it pays: basicTypes, Grid (+ createBarrier, visitNeighborhood), Signals, Peeps, Genome (+ decode, mutation, reproduction), GenomeCompare, Individual (+ feedForward, getSensor), Sensor, ExecuteActions, SurvivalCriteria, SpawnNewGeneration, params, and the sim loop (simulator / endOfSimStep / endOfGeneration).

Suggested phasing (each phase independently landable):

  • P1 — foundation + oracles: basicTypes, Grid, Signals + port Miller's 3 unit tests. Highest oracle density; pins D12/D17 immediately.
  • P2 — genome: decode, mutation, reproduction, GenomeCompare + the jaro-winkler golden test. Most CRITICALs live here (D0, D3, D4).
  • P3 — agent: Individual, feedForward, sensors, actions, survival.
  • P4 — loop: Peeps, SpawnNewGeneration, the step/generation loop, throughput.

Reference: the C++ is vendored (davidrmiller/biosim4 @ 45e808c); ~5,055 LOC C++ / ~3,595 LOC TS. Clone it locally rather than fetching per-file (a prior agent run died because WebFetch depends on a permission classifier that was unavailable).

Constraints

  • Do NOT golden-test against the TS port. SPEC §7's M1 gate is provably the wrong target (C++ conformance audit: 23 new defects, 4 critical, 0 clean modules — recommend PORT #26). Miller's C++ is ground truth.
  • Do NOT port the TS port's bugs. Read the C++ for each function; use the TS only to understand slsim's deliberate additions.
  • Preserve slsim's deliberate additions as first-class, and port them too: the seeded PRNG, env-var parameterisation, sensor-stream CSV logging, and the experiment levers (barrier dynamics, killEnable, the SIGNAL_BEACON challenge + beacon trace/scatter, genome injection). Note these are the 627 LOC the audit never reviewed — port them from their intent, and test them.
  • Keep the TS port in-tree as a historical reference; it is no longer the source of truth. Do not delete it.
  • Do NOT build the Brain interface / TransformerBrain / MLPBrain (M3/M4). This is the substrate only.
  • Do NOT change the SPEC §2 gate criterion.

Acceptance criteria

  • Miller's unitTestGridVisitNeighborhood.cpp, unitTestBasicTypes.cpp and unitTestConnectNeuralNetWiringFromGenome.cpp ported and passing against the Python port.
  • jaro_winkler_distance golden-tested against the C++ on generated genome pairs — 0% divergence (vs the TS port's 100%).
  • Each of the 23 audited defects is either structurally unrepresentable (state which dtype/design makes it so) or covered by a test pinning the C++ behaviour. A short table mapping D0–D22 → unrepresentable / tested / deliberately divergent (with rationale).
  • Throughput ≥ 20× the TS port on the same config (ISSUES.md §6), or a documented reason, informed by Throughput spike: is the PoC computationally reachable? (M1/M4 feasibility) #25's measurement.
  • Determinism: same seed → bit-identical output, with the invariant from non-negotiable Enrich the world to flip the M0 gate: barriers + Miller-level mutation #5 stated explicitly in the code and docs.
  • D2 decided and documented (frozen-neuron fidelity: yes or no, and why).
  • RESULTS.md updated to state that every pre-port result is superseded and why.

Out of scope

  • The Brain interface, TransformerBrain, MLPBrain, ES training, lifetime learning (M3/M4/M5).
  • Re-running the M0 corpus on the new port (a follow-up issue — but note acceptance requires the port to make it re-runnable).
  • Patching the TS port further. Fix inverted selection as default + search-strength gate before M1 #24's search-strength question is undermined anyway (its injected target used a weight the search cannot produce — D4).

Reasoning guidance

This is a large, multi-session port; land it in phases, each with its golden tests green. Read the C++ for every function you port — do not translate the TS. The whole reason this issue exists is that transliterating without modelling types produced an open-ended bug generator; the antidote is types first, oracles first, then code. When the C++ itself looks wrong (D2), stop and flag it rather than silently choosing.

Cross-references

Labels

enhancement

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions