From 1029f4031859b90ed7d86d0e0b050f0e31b0bb60 Mon Sep 17 00:00:00 2001 From: jze9 Date: Mon, 6 Jul 2026 21:03:30 +0500 Subject: [PATCH] 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 --- src/gausse/sim/coilgun.py | 8 +++++++- tests/test_coilgun_chain.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/gausse/sim/coilgun.py b/src/gausse/sim/coilgun.py index dc47a21..b318414 100644 --- a/src/gausse/sim/coilgun.py +++ b/src/gausse/sim/coilgun.py @@ -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, diff --git a/tests/test_coilgun_chain.py b/tests/test_coilgun_chain.py index ac719ef..2ae9d3a 100644 --- a/tests/test_coilgun_chain.py +++ b/tests/test_coilgun_chain.py @@ -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(