Физика v3: коэффициент заполнения железа + фикс зависаний решателя

ЭКСПЛОЙТ (нашёлся по подозрительным 75-82% КПД на дашборде): модель
умножала ВСЮ индуктивность катушки на (mu_eff-1)*overlap, как будто железо
заполняет всё сечение. Тонкий снаряд (4мм) в толстой катушке (29мм) получал
~50x нефизичной силы бесплатно, и эволюция сгрузилась именно туда.
Фикс: iron_fill_factor = A_снаряда/A_среднего витка (классическое
λ = n*(B_fe*A_fe + B_возд*(A-A_fe))).

ЗАВИСАНИЯ (сервер стоял 35+ мин на одной конфигурации):
- сглаживание сухого трения tanh(v/0.01) вместо разрывного sign(v)
- событие тока удержания ключа (SCR закрывается при I<0.1А после пика) --
  передемпфированный хвост больше не молотит весь 20мс бюджет
- полная формула отражения вихревого контура (ωM)²R2/(R2²+(ωL2)²) вместо
  резистивного приближения (114кОм -> 2.5кОм на вырожденных катушках)
- гард: L/R < 100нс = вне области применимости модели, мгновенный отказ
Бенч 200 геномов: 608с -> 22с, худшая оценка 540с -> 2.5с.

MODEL_VERSION -> gausse-physics-v3. 104 теста.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-08 02:22:29 +05:00
parent 78af686e6d
commit ecd6508f29
8 changed files with 140 additions and 14 deletions

View File

@@ -164,3 +164,59 @@ def test_friction_force_smooth_at_zero_velocity():
assert abs(retarding_force_n(p, 1e-6) + retarding_force_n(p, -1e-6)) < 1e-12
# вдали от нуля выходит на полное трение
assert retarding_force_n(p, 1.0) > 0.9 * p.retard_const_n
def test_iron_fill_factor_punishes_thin_slug_in_fat_coil():
"""Регрессия на эксплойт «КПД 82%»: тонкий снаряд в толстой катушке
не может усиливать весь поток — только долю сечения, которую занимает."""
from gausse.physics.inductance import CoilInductanceModel
base = dict(l_air_h=1e-3, coil_length_m=0.05, slug_length_m=0.02,
mu_eff=40.0, smoothing_width_m=0.001, total_turns=300)
full = CoilInductanceModel(**base, iron_fill_factor=1.0)
thin = CoilInductanceModel(**base, iron_fill_factor=0.02) # ⌀4мм в ⌀29мм
# сила при том же токе в 50 раз меньше — халявы больше нет
f_full = float(full.force_newtons(-0.03, 100.0))
f_thin = float(thin.force_newtons(-0.03, 100.0))
assert f_thin < 0.03 * f_full
def test_degenerate_coil_fails_fast():
"""Регрессия на завис 540с: вырожденная катушка бракуется мгновенно."""
import time
from dataclasses import replace
stage = make_stage()
degenerate = replace(stage, turns_per_layer=2, layers=2) # 4 витка, ~2мм
t0 = time.time()
res = run_stage(-0.06, 3.0, degenerate, make_projectile())
assert time.time() - t0 < 5.0, "вырожденный конфиг должен отваливаться быстро"
# либо честно отбракован гардом, либо честно посчитан быстро — оба исхода ок
if not res.feasible:
assert res.reason
def test_eddy_reflection_bounded_at_high_frequency():
"""Полная формула отражения (R₂²+(ωL₂)² в знаменателе): на высоких ω
заметно ниже старого резистивно-доминированного приближения, на низких —
сходится к нему. Совсем вырожденные катушки добивает гард L/R в run_stage."""
import math as _m
from gausse.physics.constants import MU_0
from gausse.physics.losses import EDDY_COUPLING_FACTOR, eddy_reflected_resistance_ohm
kwargs = dict(slug_radius_m=0.002, slug_length_m=0.03,
slug_resistivity_ohm_m=1.6e-7, mu_eff=259.0,
total_turns=5, coil_length_m=0.002)
def resistive_approx(om):
a = _m.pi * kwargs["slug_radius_m"] ** 2
m = EDDY_COUPLING_FACTOR * MU_0 * kwargs["mu_eff"] * (5 / 0.002) * a
r2 = 2 * _m.pi * kwargs["slug_resistivity_ohm_m"] / kwargs["slug_length_m"]
return (om * m) ** 2 / r2
om_hi, om_lo = 1.25e5, 1e2
r_hi = eddy_reflected_resistance_ohm(**kwargs, char_omega_rad_s=om_hi)
r_lo = eddy_reflected_resistance_ohm(**kwargs, char_omega_rad_s=om_lo)
assert r_hi < 0.5 * resistive_approx(om_hi) # высокие ω: ограничена
assert abs(r_lo - resistive_approx(om_lo)) < 0.05 * resistive_approx(om_lo) # низкие: сходится