Files
gausse/tests/test_report.py
jze9 7f040c3cf5 Fix two physics bugs the user caught: unbounded field and ignored current limits
1. Iron saturation now in the dynamics (was the "13 Tesla" bug). Flux
   linkage lambda(x,I) = L_air*I + L_iron*overlap(x)*g(I) with
   g(I)=I_sat*tanh(I/I_sat) saturating at the current where iron reaches
   B_sat. Both the circuit back-EMF (dlambda/dx) and the force (coenergy,
   dW'/dx) derive from the SAME lambda, so energy stays conserved (0.025%
   error) AND the field caps at B_sat instead of running to 13 T. Reduces
   to the old 0.5*I^2*dL/dx in the low-current limit. On the config that
   reported 60% efficiency / 11.6 T, it now gives 40% / 1.90 T.

2. Switch surge-current limit is now a hard feasibility constraint: a
   config whose peak discharge current exceeds the switch's pulse rating
   (continuous * surge factor per device kind) is infeasible -- otherwise
   the optimizer "wins" with configs that vaporize their own thyristor
   (e.g. 119 A through a 12 A BT151). Capacitor current stays a warning
   (electrolytics tolerate pulses; DB has continuous not pulse ratings).

Regression tests added for both (field cap near saturation, energy
conservation under strong discharge). 87 tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 01:45:26 +05:00

115 lines
4.1 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.
from gausse.components.database import ComponentDatabase
from gausse.report.animate import animate_run
from gausse.report.bom import build_bom, render_bom_text, total_cost_rub
from gausse.report.plots import (
plot_stage_currents,
plot_stage_fields,
plot_velocity_vs_position,
save_all,
)
from gausse.report.summary import build_summary, render_summary_json, render_summary_text
from gausse.sim.coilgun import CoilgunConfig, run_coilgun
from gausse.sim.stage import ProjectileConfig, StageConfig
DB = ComponentDatabase.load()
def _feasible_config() -> CoilgunConfig:
wire = next(w for w in DB.wires if w.part_id == "cu-petv2-1.0mm")
capacitor = next(c for c in DB.capacitors if c.part_number == "cap-470uf-400v")
# мощный ключ (180А, surge ~720А) — конфигурация с реальным пиковым током
switch = next(s for s in DB.switches if s.part_number == "IRFP4468PBF")
sensor = next(s for s in DB.sensors if s.part_number == "TCST2103")
steel = next(m for m in DB.projectile_materials if m.name == "Ст3 (конструкционная сталь)")
projectile = ProjectileConfig(material=steel, diameter_m=0.008, length_m=0.02)
stage = 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=200.0,
)
return CoilgunConfig(
stages=[stage, stage], inter_stage_gaps_m=[0.05], projectile=projectile,
initial_x_m=-0.05, initial_v_mps=5.0,
)
def test_feasible_fixture_is_actually_feasible():
result = run_coilgun(_feasible_config())
assert result.feasible, result.reason
def test_plots_render_without_error():
config = _feasible_config()
result = run_coilgun(config)
assert result.feasible
fig1 = plot_stage_currents(result)
fig2 = plot_stage_fields(result)
fig3 = plot_velocity_vs_position(result)
for fig in (fig1, fig2, fig3):
assert fig is not None
def test_save_all_writes_nonempty_png_files(tmp_path):
config = _feasible_config()
result = run_coilgun(config)
paths = save_all(result, tmp_path)
assert len(paths) == 3
for p in paths:
from pathlib import Path
assert Path(p).stat().st_size > 0
def test_bom_totals_are_positive_and_match_sum():
config = _feasible_config()
lines = build_bom(config, DB)
assert len(lines) > 0
total = total_cost_rub(lines)
assert total == sum(line.total_price_rub for line in lines)
assert total > 0
text = render_bom_text(lines)
assert "ИТОГО" in text
def test_summary_reports_feasible_run_honestly():
config = _feasible_config()
result = run_coilgun(config)
summary = build_summary(result, config, DB)
assert summary["feasible"] is True
assert summary["efficiency"] == result.efficiency
assert "model_limitations" in summary
assert len(summary["model_limitations"]) > 0
text = render_summary_text(summary)
assert "РЕАЛИЗУЕМО" in text
json_text = render_summary_json(summary)
assert "efficiency" in json_text
def test_summary_reports_infeasible_run_honestly():
import dataclasses
config = _feasible_config()
# индукционный датчик при скорости заведомо ниже порога -> провал ступени 0
weak_sensor = next(s for s in DB.sensors if s.kind == "inductive")
broken_stage = dataclasses.replace(config.stages[0], sensor=weak_sensor)
config = dataclasses.replace(
config, stages=[broken_stage, config.stages[1]], initial_v_mps=2.0
)
result = run_coilgun(config)
summary = build_summary(result, config, DB)
if not result.feasible:
text = render_summary_text(summary)
assert "НЕ РЕАЛИЗУЕМО" in text
assert summary["reason"] is not None
def test_animate_run_produces_nonempty_gif(tmp_path):
config = _feasible_config()
result = run_coilgun(config)
assert result.feasible
out_path = tmp_path / "run.gif"
animate_run(result, config, str(out_path), fps=10)
assert out_path.stat().st_size > 0