feat(coevolution): PR 4 — configs + driver + opposition injection + pool dispatch - #149
Conversation
…lites
Adds `scripts/campaigns/curate_coevolution_prey_bundles.py` — a
re-runnable data-prep utility that loads each per-seed final elite
LSTMPPO weight checkpoint from a prior single-population source
campaign (typically M3 lamarckian-LSTMPPO-klinotaxis-predator at
`artifacts/logbooks/013/m3_lamarckian_pilot/lamarckian/seed-{42..45}/`)
and emits two JSON bundles consumed by `CoevolutionLoop`:
- `configs/evolution/coevolution_warmstart_prey/seed_{42..45}.json`
feeds `CoevolutionLoop.__init__` (`prey_gen0_seed_path`) → CMA-ES
`x0` (D12 prey warm-start).
- `configs/evolution/coevolution_held_out_prey/seed_{42..45}.json`
feeds the generality probe (`_load_held_out_prey_bundle`).
Both bundles ship the same 4 source genomes (one per source seed) —
the M3 lamarckian campaign GC'd intermediate checkpoints so the
original 8-genome held-out plan reduces to 4. The held-out loader
already samples WITH replacement when `held_out_size > len(bundle)`,
so an oversized config still works with sample repetition; coevolution
YAMLs SHOULD set `held_out_size: 4` to match.
Curation flatten path mirrors the encoder's
`_flatten_components` / `_select_genome_components` so the resulting
`params` is byte-identical to what `MLPPPOEncoder.initial_genome`
would produce on a brain with these weights (49,511 floats for the
canonical LSTMPPO + klinotaxis brain shape; NON_GENOME_COMPONENTS
filtered out).
Tick tasks 7.0a + 7.0b in `tasks.md` and document the size reduction
inline so PR 4 reviewers see the rationale at the task level rather
than buried in the bundle README.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR 4, tasks 7.3 + 7.6 (driver + smoke pilot config + integration
plumbing the smoke pilot needs to run end-to-end).
`scripts/run_coevolution.py` is a thin per-seed driver mirroring
`scripts/run_evolution.py`: argparse for --config / --seed /
--output-dir / --resume / --log-level, loads the YAML, instantiates
`CoevolutionLoop`, and calls `loop.run(resume=...)`. The driver
delegates per-side parallelism to the loop itself (existing
`EvolutionLoop._evaluate_in_worker` machinery); bash wrappers
(tasks 7.4-7.5, follow-up commit) sequence multi-seed campaigns by
invoking this entrypoint once per seed.
`configs/evolution/coevolution_smoke.yml` reduces every knob to a
fast end-to-end validation budget (~60 sec): prey/predator pop=4,
K=2, gen_pairs=1, held_out_size=2, prey learn=10/eval=5, predator
eval=5, predator_gen0_bootstrap=cold_start to skip the ~30s
pretrain. Brain shape mirrors the lamarckian-LSTMPPO source so the
warmstart bundle's flattened weight vector matches the encoder's
genome_dim (49,511 floats).
Opposition wiring (the integration boundary PR 3 task 6.8 deferred):
- Per-evaluation `_evaluate_candidate` decodes opposing-side genomes
to brain instances, saves their weights to a tempfile-managed `.pt`
per call, and patches `sim_config` accordingly:
* Prey training: opposition predator weights flow into
`environment.predators.brain_config.extra["weights_path"]` —
env's `_build_predator_brain` now honours this key and calls
`load_weights` post-construction (one opposition genome per
evaluation; all N predator slots load the same weights, matching
the focal genome's "same brain on every slot" semantic).
* Predator training: opposition prey weights flow into
`multi_agent.agents[i].weights_path` — predator fitness's
`_build_prey_agents` now honours this key and calls
`load_weights` post-construction (one opposition genome per
agent slot; opposition list is replicated across all slots).
- Empty-opposition first-K-block bootstrap: prey side gets random-
init opposition predators (env native behaviour); predator side
spawns a single random-init prey opponent so the multi-agent runner
has at least one slot to evaluate against.
`_evaluate_candidate` also patches `sim_config.evolution` to the
side's `EvolutionConfig` so prey-side `LearnedPerformanceFitness`
sees `learn_episodes_per_eval` / `eval_episodes_per_eval`.
Smoke pilot run validated end-to-end:
- Prey side trains via `LearnedPerformanceFitness` with K=10 train
+ L=5 eval episodes per genome.
- Predator side evaluates frozen-weight via
`PredatorEpisodicKillRate`.
- Both lineage CSVs populated; champion_history.json round-trips;
HoFs accumulate; probe schema written (fitness column NaN per
the deferred probe body — schema + cadence are correct now).
Tick tasks 7.3 + 7.6 in tasks.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR 4, tasks 7.1a + 7.1b + 7.2 + 7.4 + 7.5. Configs: - `coevolution_pilot_arm_a.yml`: D7 arm A (heuristic-imitation pretrain bootstrap), 30 gens (3 K-pair blocks at K=10), prey pop=24 / predator pop=16, prey-side warmstart from `coevolution_warmstart_prey/seed_42.json`. Wall-time target ~3.5-7 hours per design.md D4. - `coevolution_pilot_arm_b.yml`: D7 arm B (cold-start), identical to arm A but `predator_gen0_bootstrap: cold_start` and warmstart from `seed_43.json`. - `coevolution_full.yml`: 50 gens (5 K-pair blocks), pop 24/16, templated `prey_gen0_seed_path` placeholder rewritten per seed by the wrapper. `predator_gen0_bootstrap` defaults to the pretrain arm pending pilot result locking the choice (task 9.4 / 10.1). `held_out_size: 4` matches the curated 4-genome held-out bundle (one per source seed); the loader samples WITH replacement when an oversized config asks for more, so the value is conservative. Bash wrappers (`scripts/campaigns/`): - `phase5_m5_coevolution_pilot.sh`: arm A then arm B sequentially. Output dirs separate the two arms (arm_a/, arm_b/) so the aggregator can compare head-to-head. - `phase5_m5_coevolution_full.sh`: 4 seeds (42-45) sequentially. Per-seed config rewrite via sed substitutes the warmstart bundle path so each seed boots from its own M3-elite anchor (D12). `OUTPUT_ROOT` and `SEEDS` env-var overridable for partial / resume / dry-run scenarios. Tick tasks 7.1a + 7.1b + 7.2 + 7.4 + 7.5 in tasks.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit-driven sync between the OpenSpec change and the merged PR 1-3 + in-flight PR 4 implementation. All findings from a focused divergence audit; openspec validate passes (--strict). Blocking fixes: - proposal.md: driver path was `scripts/campaigns/run_coevolution.py` (incorrect); shipped at `scripts/run_coevolution.py` (sibling of `scripts/run_evolution.py`; campaign-bash wrappers under scripts/campaigns/ invoke it). Updated both occurrences plus the scripts impact list. - co-evolution/spec.md "Composition Over Inheritance": the scenario said opposition injection lands in PR 4; PR 4 has shipped it. Rewrote as an unconditional contract documenting both sides: prey-training patches `environment.predators.brain_config.extra ["weights_path"]` (env's `_build_predator_brain` honours the key); predator-training patches `multi_agent.agents[i].weights_path` (predator fitness's `_build_prey_agents` honours it). `sim_config.evolution` patched to side's per-side EvolutionConfig. Empty-opposition first-K-block bootstrap documented as random-init. - co-evolution/spec.md "Probe Cadence and Output Layout": probe fitness column ships NaN until per-opponent evaluation lands in a follow-up commit. Schema + cadence + non-mutation contract are normative now; the body is non-normative pending the follow-up. - co-evolution/spec.md "Checkpoint File Layout": four-file → five-file (round-2 review fix added top-level `champion_history.json`). Added normative "RNG pickle written LAST + holds canonical k_block_index + cross-file consistency" lines from the implementation. Added `run_seed` to the rng pickle's contents. - co-evolution/spec.md "Held-Out Set Construction" + "Pilot Configuration": held_out_size=8 → 4 to match the shipped bundle (production prey bundle ships 4 distinct genomes — one per source seed; original "8 held-out, 2 per seed" plan reduced because the M3 source GC'd intermediate elite checkpoints). Added an in-spec footnote documenting the bundle constraint + the loader's with-replacement fallback when held_out_size > len(bundle). Should-fix: - proposal.md: configs list updated — `coevolution_pilot.yml` (single file) → `coevolution_pilot_arm_a.yml` + `coevolution_pilot_arm_b.yml` + `coevolution_smoke.yml`. Added the warmstart + held-out bundle JSONs and the curation script. - proposal.md: `config_loader.py` modifications list extended to mention the round-2-review additions (`_validate_evolution_coevolution_exclusive`, `_iter_evolution_configs` helper). - design.md D5: footnote on the bundle size reduction (default-8 schema kept for forward-compat; production YAMLs override to 4). - design.md migration plan: tasks 7.0–7.6 → 7.0a–7.6. Notes: - co-evolution/spec.md "Held-Out Bundle Missing" header dropped the stale "(PR 3 only)" qualifier — the no-op path is still a defensive contract, just no longer PR-specific. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…el_workers honesty + sed verify + warmstart guard) Self-review of the PR 4 branch surfaced two real blockers the smoke pilot didn't catch (smoke pop=4 fits in the schema cap; pilot pop=24 won't): **B1: predator-side opposition list would blow MultiAgentConfig schema cap.** `_validate_population` enforces 2 ≤ len(agents) ≤ 10. With pilot/full prey pop=24, the opposition list (one entry per opposing-side genome) was 24 entries, raising ValidationError on the first predator-side K-block. Fix: cap opposition at 10 entries (preserves HoF mix order at the head of the list, so 70/30 prevalence is roughly maintained); pad with random-init bootstrap entries when the cap is below the schema minimum (empty or single-entry opposition). Smoke pop=4 still fits cleanly. **B2: `parallel_workers: 4` is silently ignored.** `CoevolutionLoop` contains no `multiprocessing.Pool` wiring; the per-side `parallel_workers` field flows through `EvolutionConfig` but is never consumed at the loop level. Removed from all three pilot/full YAMLs with an explanatory comment; revised wall-time targets in the bash wrappers + YAML headers (~14-28 hours/seed pilot under sequential dispatch, ~120-240 hours total for full run, vs. design.md D4's parallel-workers=4 estimate). Pool wiring lands in a follow-up. **S3: bash wrapper now verifies sed substitution.** Silent no-ops would otherwise have all 4 full-run seeds load `seed_42.json` if the placeholder ever drifts. Post-substitution `grep -q` check + clear error message. **S4: strengthened warmstart shape-mismatch error.** Existing guard now points users at `curate_coevolution_prey_bundles.py` to re-curate when brain-config edits invalidate the bundle. **S5: spec note on prey-side opposition diversity simplification.** The "all N predator slots use the same opposition genome" semantic matches the focal-genome's pattern but loses per-slot opposition diversity. Documented as an intentional simplification that the HoF mix exercises at the call level rather than the slot level. Also documented the predator-side opposition cap of 10. Smoke pilot still passes end-to-end; 96 M5 tests pass; openspec validates strict. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…heritance
PR 4 follow-up surfaced by self-review (B2): the M5 pilot/full YAMLs
specified `parallel_workers: 4` but `CoevolutionLoop` had no pool
dispatch; the field was silently ignored. Worse, PR 3 deferred ~100
LoC of "replicated inheritance bookkeeping" per design.md but it
never landed, so the prey side's `inheritance: lamarckian` config
was equally a no-op (children always trained from-scratch).
Both ship together because they share the eval_args plumbing.
**Pool dispatch:**
- `_run_one_k_block` now builds a per-generation `eval_args` batch
(master process), then dispatches via `multiprocessing.Pool.map`
when `training_side.evolution_config.parallel_workers > 1`.
- Reuses `EvolutionLoop._evaluate_in_worker` + `_init_worker` so the
11-tuple worker ABI stays single-source.
- Per-generation `tempfile.TemporaryDirectory` owns the lifetime of
opposition `.pt` files (master process writes; workers read via
`load_weights` on the patched sim_config; tmp dir torn down after
the pool batch completes).
- `candidate_tag` (genome_id) namespaces each child's opposition
files so concurrent workers in the same generation don't collide.
**Per-side Lamarckian inheritance:**
- Added `_SideState.selected_parent_ids: list[str]` (mirrors
`EvolutionLoop._selected_parent_ids`); persisted in the per-side
checkpoint pickle for resume continuity (legacy-safe `.get` for
pre-existing checkpoints).
- Added `_resolve_per_child_inheritance(side, child_idx, gen, gid)`
per side, dispatching on `side.inheritance.kind()`:
* `"none"` → from-scratch.
* `"weights"` (Lamarckian) → resolve parent's saved checkpoint
under the SIDE's `output_dir` (`<run>/{side}/inheritance/...`),
return `(parent_warm_start, child_capture_path, parent_id)`.
- Added `_inheritance_active(side)` / `_inheritance_records_lineage(side)`
/ `_gc_inheritance_dir(side, gen, keep_ids)` per-side analogues
of the `EvolutionLoop` helpers.
- After each generation: call `side.inheritance.select_parents(...)`,
update `side.selected_parent_ids`, and GC stale per-genome elite
checkpoints (keep only the about-to-be-parents on the current gen;
drop the prior gen's elites whose children just ran).
**Configs:**
- Restored `parallel_workers: 4` to `coevolution_pilot_arm_a.yml`,
`_b.yml`, `_full.yml` per-side blocks.
- Revised wall-time targets back to design.md D4 estimates
(~3.5-7h/seed pilot, ~30-60h/seed full at parallel_workers=4).
**Verification:**
- Sequential smoke (`parallel_workers: 1`) and parallel-2 smoke
(`parallel_workers: 2`) produce byte-identical lineage CSVs +
identical `prey/inheritance/gen-001/genome-*.pt` checkpoint
(proves Lamarckian inheritance flows through both dispatch paths).
- 383 M5 tests pass; openspec --strict still validates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundle deduplication: the warmstart and held-out prey bundles ship the same 4 source genomes (one per M3 lamarckian-LSTMPPO source seed — intermediate elite checkpoints were GC'd by the source campaign, so the original "8 held-out + 4 warmstart, all distinct" plan reduced to 4 unique genomes anyway). Collapse them into a single `configs/evolution/coevolution_warmstart_prey/` directory; both loaders (`_load_prey_warmstart` for gen-0 anchor + `_load_held_out_prey_bundle` for the generality probe) read from the same dir. Saves ~5 MB of byte-identical duplication. Changes: - `_DEFAULT_PREY_HELD_OUT_BUNDLE_DIR` redirected from `coevolution_held_out_prey/` to `coevolution_warmstart_prey/`. - `coevolution_held_out_prey/` directory deleted. - `curate_coevolution_prey_bundles.py` rewritten to write one bundle dir (single `bundle_out` path per seed instead of separate warmstart/held-out paths). README rewritten to document the dual role under the unified bundle. - Tests updated: `test_bundle_path_anchored_to_repo_root` now asserts the warmstart dir name; `test_prey_held_out_missing_bundle_no_op` monkey-patches `_PREY_HELD_OUT_BUNDLE_DIR` to a tmp path (since the production warmstart bundle now exists in-tree, the missing-dir branch needs an explicit override to exercise). - OpenSpec proposal/design/spec/tasks all updated to reference the unified bundle path. Git LFS routing: added `configs/**/*.json filter=lfs ...` to `.gitattributes` so the 4 remaining ~1.2 MB bundle JSONs are stored as LFS pointers. Branch-history migration via `git lfs migrate import` follows in a separate operation (rewrites past branch commits to use LFS pointers; force-push to the unpushed feature branch is safe). Smoke pilot still passes end-to-end with the unified bundle layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… drift) Round-2 self-review nits — all should-fix, no blockers: - `coevolution_smoke.yml`: drop duplicate `cma_diagonal: true` lines on prey + predator side blocks (YAML last-key-wins, so it didn't break, but a reader trips on the redundancy). - `coevolution.py`: replace stale `_evaluate_candidate` references in docstrings/comments with the post-pool-rewrite name (`_build_patched_sim_config`). The original `_evaluate_candidate` helper was inlined into `_run_one_k_block` when pool dispatch landed; 5 docstring sites still mentioned the old name. - `coevolution.py`: clarify the Pool worker-boundary comment to note that macOS uses spawn (not fork) by default; the picklability guarantee still holds (spawn requires it), but the original wording misled. Smoke pilot still passes; 38/38 coevolution tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughThis PR refactors CoevolutionLoop for optional worker-parallel evaluation and opposition-weight injection, introduces unified prey warmstart/held-out bundles (Git LFS pointers + README), adds smoke/pilot/full coevolution YAMLs, integrates persisted weight loading into env/fitness paths, provides curation and run tooling (CLI + bash), and updates specs and tests. ChangesCo-evolution Implementation and Campaign Delivery
Sequence DiagramssequenceDiagram
participant Master as Master Process
participant Sampler as Opposition Sampler
participant TempFS as Temp Weight Files
participant Pool as Worker Pool
participant Worker as Worker Eval
Master->>Sampler: sample opposition genomes via RNG
Master->>TempFS: materialize decoded weights (.pt)
Master->>Master: build eval_args and patch sim_config
Master->>Pool: dispatch 11-tuple batches to workers
Pool->>Worker: run _evaluate_in_worker with patched config
Worker-->>Master: return fitness and lineage
Master->>Master: update optimizer, checkpoint, and GC
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
… PLR0911 + format)
Three pre-commit issues surfaced after the rebase + force-push:
- `env.py:1664` `_build_predator_brain` weights_path hook: pyright
rejected `load_weights(brain, ...)` because `brain` is annotated
`MLPPPOPredatorBrain` (a `PredatorBrain` protocol) and `load_weights`
expects the agent-side `Brain` protocol. The `WeightPersistence`
surface is what's actually consumed; `cast("Any", brain)` satisfies
the static checker without taking on a runtime `Brain` import (which
would be a layering violation between env and brain.arch).
- `run_coevolution.py:161` `main()` had 7 returns (1 over PLR0911's
threshold). Each is a distinct early-exit error path (config load,
missing coevolution block, resume-dir resolve, KeyboardInterrupt,
unhandled exception) plus the success path. Flattening into a single
return obscures the failure modes; added a `# noqa: PLR0911` with
rationale.
- `run_coevolution.py:207` ruff format reformatted a 3-arg `logger.info`
call from one line to multi-line. Auto-applied.
Smoke pilot still passes; 38/38 coevolution tests pass; openspec
strict validates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
.gitattributes (1)
12-12: ⚡ Quick winConsider scoping the LFS glob to the coevolution bundle path.
configs/**/*.jsonis very broad and can unintentionally LFS-track routine config JSONs later. If the goal is this bundle, a narrower rule reduces future workflow friction.Suggested change
-configs/**/*.json filter=lfs diff=lfs merge=lfs -text +configs/evolution/coevolution_warmstart_prey/*.json filter=lfs diff=lfs merge=lfs -text🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.gitattributes at line 12, The .gitattributes entry using the broad glob "configs/**/*.json" will LFS-track any matching JSON; narrow that pattern to the coevolution bundle's config path by replacing "configs/**/*.json" with a scoped glob that targets only the bundle (e.g. the coevolution bundle's configs), update the .gitattributes entry accordingly, and after changing the glob ensure tracked files are adjusted so only the intended bundle JSONs remain under LFS.packages/quantum-nematode/quantumnematode/env/env.py (1)
1661-1673: ⚡ Quick winReplace string literal in
cast()with actual type object.Line 1673 uses
cast("Any", brain), but sinceAnyis imported at the top of the scope, the string literal should be replaced with the type object itself. This preserves the static typing value and ensures type checkers can properly reason about the cast.Suggested change
- load_weights(cast("Any", brain), _Path(str(weights_path))) + load_weights(cast(Any, brain), _Path(str(weights_path)))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/quantum-nematode/quantumnematode/env/env.py` around lines 1661 - 1673, The cast call currently uses a string literal "Any" (cast("Any", brain)) which prevents proper static typing; change it to use the imported Any type object (cast(Any, brain)) so type checkers can recognize the cast; update the cast expression near the load_weights invocation that calls load_weights(cast(..., brain), _Path(str(weights_path))) — leave the rest (load_weights, MLPPPOPredatorBrain / PredatorBrain references and the comment) unchanged.configs/evolution/coevolution_full.yml (1)
134-138: ⚡ Quick winUse an explicit seed token in
prey_gen0_seed_pathto force fail-fast substitution.Using a real file (
seed_42.json) as the template can let accidental direct runs silently reuse seed 42 as the warm-start anchor. Prefer a tokenized path so missed substitution fails immediately.Suggested change
- prey_gen0_seed_path: configs/evolution/coevolution_warmstart_prey/seed_42.json + prey_gen0_seed_path: configs/evolution/coevolution_warmstart_prey/seed_{seed}.jsonAnd update the full-run wrapper substitution pattern accordingly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@configs/evolution/coevolution_full.yml` around lines 134 - 138, Replace the concrete warm-start filename in the prey_gen0_seed_path value with an explicit placeholder token (e.g. __PREY_GEN0_SEED__ or {{PREY_GEN0_SEED}}) so missing substitution will fail fast; update the code that performs the wrapper substitution (the full-run wrapper that currently injects the per-seed path) to look for and replace that exact token (matching the new placeholder name) instead of assuming seed_42.json, and ensure any documentation or wrapper substitution pattern references the new token name so runs without substitution error immediately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/quantum-nematode/quantumnematode/evolution/coevolution.py`:
- Around line 1623-1651: The worker may have Lamarckian-trained child weights
saved to child_capture_path but gen_genomes still holds the original CMA
solution vectors; before lineage.record, elite selection, and assigning
training_side.population/hof/champion_history, load the learned params back into
each Genome (or change _evaluate_in_worker to return updated params) and update
gen_genomes[i].params (or replace the Genome) using the checkpoint at
child_capture_path (or returned params) for each child in gen_genomes so
lineage.record, the block_elite_fitness/genome logic, and subsequent
population/hof/champion_history state reflect the post-training weights instead
of stale pre-learning vectors.
- Around line 2063-2078: The current code only checks for presence of
predators_cfg.brain_config but must also verify its kind is "mlpppo_predator"
before injecting weights_path; update the guard around existing_brain_cfg in the
function that patches the predators config (references: predators_cfg,
existing_brain_cfg, brain_config) to raise a ValueError with a clear message if
existing_brain_cfg.kind != "mlpppo_predator", then proceed to build
new_extra/new_brain_cfg/new_predators_cfg and return env_cfg.model_copy(...)
only when the kind matches.
In `@scripts/campaigns/curate_coevolution_prey_bundles.py`:
- Around line 359-391: Before calling curate_seed, remove any existing per-seed
output (bundle_dir / f"seed_{seed}.json") so stale files can't be mixed in, and
on any exception from curate_seed do not skip—log the error and abort by
returning non-zero (e.g., return 1) instead of continuing; update the loop
handling around completed_seeds/curate_seed and the error except block
accordingly so failed seeds cause the script to stop and no stale seed_<N>.json
is left behind.
In `@scripts/run_coevolution.py`:
- Around line 137-149: The resume guard only checks coevolution_rng.pkl but must
validate the full checkpoint set before constructing CoevolutionLoop to avoid
wasting pretrain work; update the resume-path check around resume_path to verify
that resume_path / "prey" / "checkpoint.pkl", resume_path / "predator" /
"checkpoint.pkl", resume_path / "coevolution_state.json" and resume_path /
"coevolution_rng.pkl" all exist, and if any are missing call logger.error with a
clear message listing the missing files and return None so
CoevolutionLoop.__init__ and subsequent work never start before
_load_checkpoint() can succeed.
---
Nitpick comments:
In @.gitattributes:
- Line 12: The .gitattributes entry using the broad glob "configs/**/*.json"
will LFS-track any matching JSON; narrow that pattern to the coevolution
bundle's config path by replacing "configs/**/*.json" with a scoped glob that
targets only the bundle (e.g. the coevolution bundle's configs), update the
.gitattributes entry accordingly, and after changing the glob ensure tracked
files are adjusted so only the intended bundle JSONs remain under LFS.
In `@configs/evolution/coevolution_full.yml`:
- Around line 134-138: Replace the concrete warm-start filename in the
prey_gen0_seed_path value with an explicit placeholder token (e.g.
__PREY_GEN0_SEED__ or {{PREY_GEN0_SEED}}) so missing substitution will fail
fast; update the code that performs the wrapper substitution (the full-run
wrapper that currently injects the per-seed path) to look for and replace that
exact token (matching the new placeholder name) instead of assuming
seed_42.json, and ensure any documentation or wrapper substitution pattern
references the new token name so runs without substitution error immediately.
In `@packages/quantum-nematode/quantumnematode/env/env.py`:
- Around line 1661-1673: The cast call currently uses a string literal "Any"
(cast("Any", brain)) which prevents proper static typing; change it to use the
imported Any type object (cast(Any, brain)) so type checkers can recognize the
cast; update the cast expression near the load_weights invocation that calls
load_weights(cast(..., brain), _Path(str(weights_path))) — leave the rest
(load_weights, MLPPPOPredatorBrain / PredatorBrain references and the comment)
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e71d0740-4c88-4e23-b4f3-83ba393a9f57
📒 Files selected for processing (22)
.gitattributesconfigs/evolution/coevolution_full.ymlconfigs/evolution/coevolution_pilot_arm_a.ymlconfigs/evolution/coevolution_pilot_arm_b.ymlconfigs/evolution/coevolution_smoke.ymlconfigs/evolution/coevolution_warmstart_prey/README.mdconfigs/evolution/coevolution_warmstart_prey/seed_42.jsonconfigs/evolution/coevolution_warmstart_prey/seed_43.jsonconfigs/evolution/coevolution_warmstart_prey/seed_44.jsonconfigs/evolution/coevolution_warmstart_prey/seed_45.jsonopenspec/changes/add-coevolution-arms-race/design.mdopenspec/changes/add-coevolution-arms-race/proposal.mdopenspec/changes/add-coevolution-arms-race/specs/co-evolution/spec.mdopenspec/changes/add-coevolution-arms-race/tasks.mdpackages/quantum-nematode/quantumnematode/env/env.pypackages/quantum-nematode/quantumnematode/evolution/coevolution.pypackages/quantum-nematode/quantumnematode/evolution/predator_fitness.pypackages/quantum-nematode/tests/quantumnematode_tests/evolution/test_coevolution.pyscripts/campaigns/curate_coevolution_prey_bundles.pyscripts/campaigns/phase5_m5_coevolution_full.shscripts/campaigns/phase5_m5_coevolution_pilot.shscripts/run_coevolution.py
| # Post-eval bookkeeping: lineage rows, block elite, | ||
| # optimiser tell, generation advance. | ||
| for genome, fit, inherited_from in zip( | ||
| gen_genomes, | ||
| fitnesses, | ||
| inherited_from_per_child, | ||
| strict=True, | ||
| ): | ||
| training_side.lineage.record( | ||
| genome, | ||
| fitness=fit, | ||
| brain_type=training_side.encoder.brain_name, | ||
| inherited_from=inherited_from, | ||
| ) | ||
| # Strict-greater: first-seen high-tier wins on ties | ||
| # (preserves recency of the original champion in | ||
| # the K-block). | ||
| if fit > block_elite_fitness: | ||
| block_elite_fitness = fit | ||
| block_elite_genome = genome | ||
|
|
||
| # Tell optimiser (CMA-ES minimises; our fitness maximises). | ||
| training_side.optimizer.tell(list(solutions), [-f for f in fitnesses]) | ||
| block_fitnesses.extend(fitnesses) | ||
|
|
||
| # Update side-state population + bookkeeping for next gen. | ||
| training_side.population = gen_genomes | ||
| training_side.prev_generation_ids = [g.genome_id for g in gen_genomes] | ||
| training_side.generation += 1 |
There was a problem hiding this comment.
Propagate Lamarckian child weights back into the generation state.
Under prey-side Lamarckian evaluation, _evaluate_in_worker can train from the parent and write the learned child weights to child_capture_path, but this method keeps the original CMA solution vectors in gen_genomes. population, hof, and champion_history therefore carry stale pre-learning prey genomes, so later predator evaluations decode the wrong opposition even though the inheritance checkpoints contain the learned policies. Rehydrate Genome.params from the captured child checkpoints (or return updated params from the worker) before lineage and elite bookkeeping.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/quantum-nematode/quantumnematode/evolution/coevolution.py` around
lines 1623 - 1651, The worker may have Lamarckian-trained child weights saved to
child_capture_path but gen_genomes still holds the original CMA solution
vectors; before lineage.record, elite selection, and assigning
training_side.population/hof/champion_history, load the learned params back into
each Genome (or change _evaluate_in_worker to return updated params) and update
gen_genomes[i].params (or replace the Genome) using the checkpoint at
child_capture_path (or returned params) for each child in gen_genomes so
lineage.record, the block_elite_fitness/genome logic, and subsequent
population/hof/champion_history state reflect the post-training weights instead
of stale pre-learning vectors.
…+ resume guard) 3 of 4 reviewer findings applied: **F2 (Blocking) — `_build_prey_side_environment_patch` only checked that `brain_config` exists, not that its `kind == 'mlpppo_predator'`.** The env's `_build_predator_brain` dispatcher only honours `extra["weights_path"]` on the learnable predator branch; a YAML with `kind: heuristic` would silently ignore the injected opposition weights and the run would proceed with the default heuristic predator (no opposition signal, flat-zero gradient). Added an explicit kind check that raises `ValueError` with a clear message. **F3 — `curate_coevolution_prey_bundles.py` continued past per-seed failures, leaving partial bundles on disk.** A partial bundle (some seeds curated, others stale or missing from a prior run) silently misleads downstream loaders. Two changes: - Pre-delete the per-seed JSON before calling `curate_seed` so a stale file from a prior run can't get mixed in if `curate_seed` fails mid-write. - Abort with `return 1` on any per-seed failure (FileNotFoundError / ValueError / RuntimeError) instead of continuing. Re-running after fixing the source surfaces the failure loudly. **F4 — `_resolve_session_dir` resume check only validated the rng pickle.** The rng pickle is the canonical "checkpoint complete" signal (written last), but if it's present and any of the per-side pickles or `coevolution_state.json` are missing (torn save, concurrent writer, manual restore from partial backup), we'd pay the `CoevolutionLoop.__init__` cost — including the ~30s heuristic- imitation pretrain on arm A — before `_load_checkpoint` discovers the incomplete set. Now checks all 4 required files up-front; missing files surface in a single error message listing all of them. **F1 (skipped) — reviewer claimed `gen_genomes` carries pre-training params while child_capture_path holds post-training weights, and asked for params reload before lineage/elite tracking.** This matches `EvolutionLoop.run`'s identical pattern (`loop.py:599-606`): genome .params is intentionally the CMA-ES search vector (what the optimiser needs for `tell()` and what `champion_history` records as the search-space coordinate); trained weights flow through the per-genome `.pt` sidecar that next-generation Lamarckian children load via `parent_warm_start`. Reloading params from disk into Genome objects would diverge from the established Lamarckian flow and break CMA-ES `tell()` (the optimiser expects to see the samples it generated, not post-train weights). Smoke pilot still passes; 38/38 coevolution tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
PR 4 of the M5 co-evolution arms race milestone. Ships everything needed to run the smoke + pilot + full campaigns end-to-end:
scripts/campaigns/curate_coevolution_prey_bundles.pyflattens prior-campaign elite.ptcheckpoints into the unified prey reference bundle (configs/evolution/coevolution_warmstart_prey/seed_{42..45}.json). Same 4 source genomes serve both the gen-0 warm-start anchor (per design D12) AND the held-out probe opponents — one bundle, two roles, no duplication.coevolution_smoke.yml(~3 min wall, validates the loop end-to-end),coevolution_pilot_arm_a.yml(heuristic-imitation pretrain),coevolution_pilot_arm_b.yml(cold-start),coevolution_full.yml(4 seeds × 50 gens).scripts/run_coevolution.py— per-seed entrypoint mirroringscripts/run_evolution.py.--config / --seed / --output-dir / --resume / --log-level.phase5_m5_coevolution_pilot.sh(sequential arm A → arm B),phase5_m5_coevolution_full.sh(4 seeds 42–45 with per-seedsed-templated warmstart paths + substitution-fired-correctly check)._build_patched_sim_configdecodes opposing-side genomes, materialises weights to a per-generationtempfile.TemporaryDirectory-managed.pt, and patchessim_config:environment.predators.brain_config.extra["weights_path"](env's_build_predator_brainhonours the key).multi_agent.agents[i].weights_path(predator fitness's_build_prey_agentshonours the key)._run_one_k_blockbuilds a per-generationeval_argsbatch, dispatches viamultiprocessing.Poolwhenparallel_workers > 1. ReusesEvolutionLoop._evaluate_in_worker's 11-tuple ABI. Sequential (parallel_workers: 1) and parallel (parallel_workers: 2) smoke runs produce byte-identical lineage CSVs.selected_parent_ids+_resolve_per_child_inheritanceper side;inherited_fromcolumn in lineage.csv populates correctly;prey/inheritance/gen-XXX/genome-*.ptpersisted with GC of stale gens. Without this, the prey side'sinheritance: lamarckianconfig was a no-op.configs/**/*.jsonrouted through LFS via.gitattributes. Each ~1.2 MB bundle JSON is stored as a ~132-byte LFS pointer in git; LFS objects (~5 MB total) live in the LFS server.Test plan
uv run python scripts/run_coevolution.py --config configs/evolution/coevolution_smoke.yml --seed 42 --output-dir /tmp/coevolution_smokecompletes ~3 min, EXIT=0prey/inheritance/gen-001/genome-*.ptpersisted (Lamarckian inheritance flow)parallel_workers: 2smoke run produces byte-identical lineage toparallel_workers: 1tests/quantumnematode_tests/evolution/test_coevolution.pypassuv run pre-commit run --all-filescleanopenspec validate add-coevolution-arms-race --strictvalidphase5_m5_coevolution_pilot.sh(out of PR 4 scope; PR 6)Reviews completed
3 review passes during the PR cycle:
Deferred / out of scope
_probe_one_opponentreturns NaN — schema + cadence are normative now; per-opponent evaluation lands in a follow-up before PR 6 (pilot run).MultiAgentConfig.agentsschema cap); under pop=24 prey, ~14 opposition genomes are dropped per evaluation. Documented as intentional simplification; rotate per slot in a follow-up if pilot evidence shows insufficient diversity.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Chores
Tests