Phase 86: Engagement & Calibration Optimization¶
Block: 9 (Performance at Scale) Status: Complete Tests: 19 (11 calibration flat dict + 8 observer batching)
Overview¶
Replaces pydantic CalibrationSchema.get() calls in the battle loop with plain dict.get() lookups via a pre-computed flat dict. Batches per-observer modifiers (MOPP, altitude sickness, readiness) once per tick instead of per-engagement.
What Was Built¶
86a: CalibrationSchema Flat Dict (simulation/calibration.py)¶
to_flat_dict(sides)method on CalibrationSchema:- Expands
model_dump()output into a flat dict - Flattens morale sub-object (10 keys:
morale_base_degrade_rate, etc.) - Expands side overrides for each side × suffix/prefix fields
- Strips None values so
dict.get(key, default)works naturally - Called once at scenario load time
86a: Flat Dict Wiring (simulation/scenario.py)¶
cal_flatfield on SimulationContext (Phase 86)- Generated at scenario load time from
calibration.to_flat_dict(side_names) - Regenerated on checkpoint restore (not serialized — derived data)
86a: Battle Loop Migration (simulation/battle.py)¶
_resolve_cal_flat(ctx)helper: returnsctx.cal_flatwhen available, builds on-the-fly for backward compat- 88
cal.get()→cal_flat.get()replacements across all battle loop methods - 12 direct
cal.field→cal_flat.get("field", default)replacements for inconsistent attribute accesses - All
cal = ctx.calibrationassignments replaced withcal_flat = _resolve_cal_flat(ctx) - Fire-zone section preserved (uses separate
_fz_calfromconfig.calibration_overrides)
86b: Observer Modifier Batching (simulation/battle.py)¶
_ObserverModifiersNamedTuple: 7 pre-computed per-observer values (MOPP detection, FOV, fatigue, reload, level; altitude factor; readiness)_DEFAULT_OBS_MODS: neutral instance (all 1.0, level 0)- Batch computation: builds
_observer_modsdict for all active units before the per-side engagement loop - MOPP effects: one
_cbrn_eng.get_mopp_effects()call per unit (was per-weapon per-target) - Altitude sickness: one position check per unit (was duplicated in detection AND engagement)
- Readiness: one
_maint_eng.get_unit_readiness()call per unit (was per-weapon) - Inline replacements: detection range modifiers and crew_skill modifiers now read from
_obslookup
Key Design Decisions¶
dict.get(key, default)overdict[key]: Nullable CalibrationSchema fields produce None values that are stripped from the flat dict. Callers usedict.get(key, default)which returns the default when the key is absent — matchingCalibrationSchema.get()behavior._resolve_cal_flat()fallback: Tests that passSimpleNamespacecontexts don't havecal_flat. The helper builds it on-the-fly from whateverctx.calibrationprovides.- Equipment stress NOT batched: Depends on per-weapon equipment instance, not just the observer. Temperature fetch is already hoisted.
- Fire-zone
calrenamed to_fz_cal: This section reads fromconfig.calibration_overrides, notctx.calibration. Renamed to avoid confusion withcal_flat. - No new
enable_*flags: Both optimizations are transparent — identical results, faster execution.
Files Changed¶
Modified (3 source)¶
stochastic_warfare/simulation/calibration.py—to_flat_dict()methodstochastic_warfare/simulation/scenario.py—cal_flatfield, generation at load + checkpoint restorestochastic_warfare/simulation/battle.py—_resolve_cal_flat(),_ObserverModifiers, 100+ replacements, batch computation
New (2 test + 1 devlog)¶
tests/unit/test_phase86_calibration_flat.py— 11 teststests/unit/test_phase86_observer_batching.py— 8 testsdocs/devlog/phase-86.md
Accepted Limitations¶
- Flat dict regenerated on checkpoint restore (not serialized) — adds ~1ms, avoids stale data risk
- Equipment temperature stress still computed per-weapon per-engagement (weapon-dependent, can't batch per-observer)
cal.get()API preserved on CalibrationSchema for engine.py, scenario_runner.py, and test consumers (35 files) — only battle.py uses flat dict