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:
@@ -109,7 +109,13 @@ def run_coilgun(config: CoilgunConfig) -> CoilgunResult:
|
|||||||
entry_x_m, entry_v_mps = result.exit_x_m, result.exit_v_mps
|
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
|
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(
|
return CoilgunResult(
|
||||||
feasible=True,
|
feasible=True,
|
||||||
|
|||||||
@@ -96,6 +96,27 @@ def test_infeasible_stage_stops_chain_with_honest_reason():
|
|||||||
assert len(result.stage_outcomes) == 2
|
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():
|
def test_mismatched_gap_count_raises():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
CoilgunConfig(
|
CoilgunConfig(
|
||||||
|
|||||||
Reference in New Issue
Block a user