Files
gausse/tests/test_realism_v2.py
jze9 1480f4ce1a Физика v4: диффузия поля в снаряд (намагничивается только скин-кольцо)
Эволюция на v3 нашла новый обход: «медленная» катушка (600+ витков, ~1мс
импульс, 36А) давала КПД 30% — на низкой частоте слабы и Доуэлл, и вихревые,
и трение. Неучтённая физика: за импульс поле проникает в сталь лишь на
глубину скин-слоя δ=√(2ρ/(ω·μ0·μr)) ~ доли мм; внутренность снаряда не
намагничивается и силы не даёт. iron_penetration_fraction = 1-(1-δ/a)²
умножает коэффициент заполнения. MODEL_VERSION -> gausse-physics-v4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 03:34:12 +05:00

239 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Физреализм v2: скин/близость (Доуэлл), трение+воздух, импульсные рейтинги ключей."""
import math
import numpy as np
from gausse.components.database import ComponentDatabase
from gausse.components.schema import (
CapacitorSpec,
ProjectileMaterialSpec,
SensorSpec,
SwitchSpec,
WireSpec,
)
from gausse.physics.ac_resistance import dowell_ac_factor, skin_depth_m
from gausse.physics.constants import switch_pulse_limit_a
from gausse.sim.stage import ProjectileConfig, StageConfig, build_stage_physics, run_stage
DB = ComponentDatabase.load()
def make_projectile() -> ProjectileConfig:
steel = ProjectileMaterialSpec(
name="steel", density_kg_m3=7850.0, mu_r=200.0, b_sat_tesla=1.8, price_per_kg=100.0, source="test"
)
return ProjectileConfig(material=steel, diameter_m=0.008, length_m=0.02)
def make_stage() -> StageConfig:
wire = WireSpec(
part_id="w1", material="copper", gauge_mm=0.8, insulation_od_mm=0.85,
resistivity_ohm_m=1.68e-8, max_current_a=10.0, price_per_m=3.0, source="test",
)
capacitor = CapacitorSpec(
part_number="c1", capacitance_uf=100.0, voltage_v=400.0, esr_ohm=0.05, max_current_a=300.0,
price=300.0, source="test",
)
switch = SwitchSpec(
part_number="sw1", kind="MOSFET", max_current_a=500.0, max_voltage_v=500.0,
on_resistance_ohm=0.02, on_voltage_drop_v=None, turn_on_time_ns=50.0, price=50.0, source="test",
)
sensor = SensorSpec(part_number="s1", kind="optical", propagation_delay_ns=500.0, price=20.0, source="test")
return StageConfig(
wire=wire, capacitor=capacitor, switch=switch, sensor=sensor,
tube_od_m=0.01, turns_per_layer=20, layers=4,
sensor_to_coil_distance_m=0.02, charge_voltage_v=350.0,
)
# --- Доуэлл ---
def test_skin_depth_copper_50hz():
# медь на 50 Гц: δ ≈ 9.3 мм (классическое справочное значение)
delta = skin_depth_m(1.68e-8, 2 * math.pi * 50)
assert abs(delta - 9.2e-3) < 0.5e-3
def test_dowell_limits():
# низкая частота -> фактор стремится к 1
assert dowell_ac_factor(0.001, 0.00108, 8, 1.68e-8, 2 * math.pi * 1.0) < 1.001
# больше слоёв -> сильнее эффект близости (монотонность)
omega = 2 * math.pi * 2000
f = [dowell_ac_factor(0.002, 0.00208, m, 1.68e-8, omega) for m in (1, 4, 10)]
assert f[0] < f[1] < f[2]
# всегда >= 1
assert all(x >= 1.0 for x in f)
def test_dowell_series_matches_formula_at_boundary():
# непрерывность на стыке ряда и полной формулы (Δ≈0.25)
omega_lo, omega_hi = None, None
# подберём частоты чуть ниже/выше границы Δ=0.25 для d=1мм
d, rho = 0.001, 1.68e-8
for omega in np.logspace(2, 6, 4000):
delta = skin_depth_m(rho, omega)
big = (math.pi / 4) ** 0.75 * d / delta # porosity~1
if big < 0.25:
omega_lo = omega
elif omega_hi is None:
omega_hi = omega
break
f_lo = dowell_ac_factor(d, d, 6, rho, omega_lo)
f_hi = dowell_ac_factor(d, d, 6, rho, omega_hi)
assert abs(f_lo - f_hi) < 0.01
def test_thick_wire_many_layers_significant_ac_factor():
# толстый провод (2мм) в 10 слоёв на кГц — эффект близости должен быть
# существенным (>1.3), иначе канал потерь не работает
f = dowell_ac_factor(0.002, 0.00208, 10, 1.68e-8, 2 * math.pi * 1500)
assert f > 1.3
# --- трение и воздух ---
def test_stage_with_friction_slower_than_ideal():
"""Выходная скорость с трением/воздухом ниже, чем была бы без них."""
stage = make_stage()
projectile = make_projectile()
res = run_stage(-0.06, 3.0, stage, projectile)
assert res.feasible, res.reason
# подлёт 3 м/с на ~3 см: только трение уже отъедает заметную скорость
phys = build_stage_physics(stage, projectile)
assert phys.circuit_params.retard_const_n > 0
assert phys.circuit_params.drag_coeff_n_per_mps2 > 0
def test_slow_projectile_stalls_before_sensor():
"""Медленный снаряд далеко от датчика останавливается трением — честный отказ."""
stage = make_stage()
projectile = make_projectile()
res = run_stage(-1.0, 0.15, stage, projectile) # 15 см/с за метр до катушки
assert not res.feasible
assert "трением" in res.reason
def test_energy_balance_with_friction():
"""Баланс: E_in = E_ост.конденсатора + E_потерь(включая трение) + ΔE_кин."""
stage = make_stage()
projectile = make_projectile()
res = run_stage(-0.06, 3.0, stage, projectile)
assert res.feasible, res.reason
balance = res.energy_remaining_cap_j + res.energy_dissipated_j + res.kinetic_energy_delta_j
assert abs(balance - res.energy_in_j) < 0.02 * res.energy_in_j
# --- импульсные рейтинги ключей ---
def test_pulse_ratings_loaded_from_datasheets():
by_pn = {s.part_number: s for s in DB.switches}
assert by_pn["IRFP254PBF"].pulse_current_a == 92.0 # IDM из даташита
assert by_pn["BT151-650R"].pulse_current_a == 100.0 # ITSM
assert by_pn["IRG4PC50F"].pulse_current_a == 280.0 # ICM
for s in DB.switches:
limit = switch_pulse_limit_a(s)
assert limit >= s.max_current_a # импульсный >= продолжительного
def test_pulse_limit_fallback_without_datasheet():
from dataclasses import replace
sw = replace(DB.switches[0], pulse_current_a=None)
# без даташита — консервативный множитель по типу ключа
assert switch_pulse_limit_a(sw) == sw.max_current_a * {"SCR": 10.0, "MOSFET": 4.0, "IGBT": 3.0}[sw.kind]
def test_friction_force_smooth_at_zero_velocity():
"""Регрессия на завис решателя: сила трения обязана быть ГЛАДКОЙ в v=0.
Разрывный sign(v) заставлял solve_ivp бесконечно дробить шаг у v≈0
(одна конфигурация считалась 35+ минут на сервере).
"""
from gausse.physics.circuit import StageCircuitParams, retarding_force_n
p = StageCircuitParams(
capacitance_f=1e-4, r_total_ohm=0.1, mass_kg=0.01,
retard_const_n=0.034, drag_coeff_n_per_mps2=2e-5,
)
# непрерывность: около нуля сила ~ 0, нечётная, без скачка
assert abs(retarding_force_n(p, 1e-6)) < 1e-5
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) # низкие: сходится
def test_iron_penetration_fraction_limits():
"""Диффузия поля: тонкий снаряд/медленный импульс — всё сечение; толстый
снаряд на быстром импульсе — только скин-кольцо (эксплойт «30%» закрыт)."""
from gausse.physics.ac_resistance import iron_penetration_fraction
# очень низкая частота: поле успевает пропитать всё сечение
assert iron_penetration_fraction(0.005, 1.4e-7, 500.0, 1.0) == 1.0
# сталь ⌀9.3мм на ω~1350 рад/с (медленная катушка из эксплойта):
# намагничивается лишь внешнее кольцо — заметно меньше половины сечения
frac = iron_penetration_fraction(0.00465, 1.4e-7, 500.0, 1350.0)
assert 0.02 < frac < 0.5
# быстрее импульс -> тоньше кольцо (монотонность по ω)
frac_fast = iron_penetration_fraction(0.00465, 1.4e-7, 500.0, 13500.0)
assert frac_fast < frac