Files
gausse/tests/test_report.py
jze9 65d8b633ad Wire up reporting (plots, BOM, summary, animation) and a working CLI (Stage 7)
- report/plots.py: current/field/velocity plots per stage, Agg backend so
  it works headless in Docker/on a server
- report/bom.py: itemized bill of materials with real component prices
- report/summary.py: honest text/JSON summary including a
  "model limitations" section and a saturation warning flag per stage
- report/animate.py: the visualization the user explicitly asked for --
  a GIF of the slug flying through the tube with each coil glowing by its
  instantaneous current, reconstructing the pre-trigger ballistic flight
  segment (not just the stored discharge phase) for a continuous timeline
- cli.py: sweep/evolve/simulate/report subcommands now actually call the
  underlying modules instead of being stubs
- Extended StageResult/StageOutcome with the coil/entry-state fields the
  report layer needed (mu_eff, turns, coil_length, entry_x/v) rather than
  recomputing them by other means

Verified end-to-end through `docker compose run`, not just pytest: a real
sweep (30 runs, 11 feasible) followed by `report --animate` produced a
correct GIF/plots/BOM/summary for the actual best result found (32.8%
efficiency, 982 RUB) -- not a synthetic fixture.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 20:50:32 +05:00

114 lines
4.0 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")
switch = next(s for s in DB.switches if s.part_number == "IRFP250PBF")
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=350.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