Fix efficiency metric that could exceed 100% (real bug found by Stage 8 sweep)

A real 5000-run sweep through Docker found the evolutionary optimizer's
best genome reporting efficiency=387%. Root cause: efficiency was
computed as exit_kinetic_energy / capacitor_energy_in, but exit kinetic
energy includes the fixed initial_v_mps launch push (a modeling
assumption, not something paid for by the capacitors) -- for a light
enough projectile, that free energy dominates and the ratio blows past 1.

Fixed by using kinetic_energy_delta (the energy the coils actually added)
instead of absolute exit KE. The per-stage energy identity (energy_in =
remaining_cap + dissipated + kinetic_delta) guarantees kinetic_delta <=
energy_in, so this metric can never exceed 1.0 -- provably, not by luck.
Added a regression test with a light projectile + large initial velocity
reproducing the exact failure mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-06 21:03:30 +05:00
parent 57a83cd4f8
commit 1029f40318
2 changed files with 28 additions and 1 deletions

View File

@@ -109,7 +109,13 @@ def run_coilgun(config: CoilgunConfig) -> CoilgunResult:
entry_x_m, entry_v_mps = result.exit_x_m, result.exit_v_mps
exit_kinetic_energy_j = 0.5 * config.projectile.mass_kg * entry_v_mps**2
efficiency = exit_kinetic_energy_j / total_energy_in_j if total_energy_in_j > 0 else None
# КПД = энергия, добавленная катушками (kinetic_energy_delta), а не вся
# exit-кинетическая энергия -- иначе фиксированный внешний "толчок"
# initial_v_mps (не учтённый в energy_in) может дать КПД > 100%, что
# физически бессмысленно. per-stage энергобаланс (energy_in =
# remaining_cap + dissipated + kinetic_delta) гарантирует
# kinetic_delta <= energy_in, так что этот КПД всегда <= 1.
efficiency = total_kinetic_energy_delta_j / total_energy_in_j if total_energy_in_j > 0 else None
return CoilgunResult(
feasible=True,

View File

@@ -96,6 +96,27 @@ def test_infeasible_stage_stops_chain_with_honest_reason():
assert len(result.stage_outcomes) == 2
def test_efficiency_never_exceeds_one_even_with_large_initial_velocity():
"""Регрессия: КПД считался как exit_KE/energy_in, а не как ДОБАВЛЕННАЯ
катушками энергия/energy_in. Для лёгкого снаряда с большой заданной
initial_v_mps (внешний "толчок", не учтённый в energy_in) это давало
КПД > 100% -- физически бессмысленно, найдено реальным sweep на Этапе 8."""
light_projectile = ProjectileConfig(material=STEEL, diameter_m=0.003, length_m=0.005)
config = CoilgunConfig(
stages=[_stage()],
inter_stage_gaps_m=[],
projectile=light_projectile,
initial_x_m=-0.05,
initial_v_mps=50.0, # заведомо большой "бесплатный" толчок относительно массы снаряда
)
result = run_coilgun(config)
assert result.feasible, result.reason
assert result.efficiency <= 1.0 + 1e-9
# и КПД действительно равен добавленной энергии, а не абсолютной exit-КЭ
expected = result.total_kinetic_energy_delta_j / result.total_energy_in_j
assert result.efficiency == pytest.approx(expected)
def test_mismatched_gap_count_raises():
with pytest.raises(ValueError):
CoilgunConfig(