- physics/ac_resistance.py: R_AC обмотки по Доуэллу на частоте импульса 1/sqrt(LC) — для толстого провода во многих слоях потери в разы выше DC - трение о трубку (0.35·m·g) + аэродинамика (0.5·rho·Cd·A·v^2) во всей динамике: CPU ОДУ разряда, подлёт к датчику (с событием остановки), GPU numpy-путь, fused cupy-ядро, аналитический coast GPU-sweep'а - SwitchSpec.pulse_current_a: паспортные ITSM/IDM/ICM из даташитов вместо generic-множителей; отчёт теперь различает превышение продолжительного рейтинга (норма для импульса) и импульсного предела (отбраковка) — фикс вводившего в заблуждение флага switch_current_over_limit - КПД теперь может быть слегка отрицательным (трение съело больше, чем добавила слабая катушка) — это честно - MODEL_VERSION -> gausse-physics-v2 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
116 lines
4.1 KiB
Python
116 lines
4.1 KiB
Python
import json
|
|
import threading
|
|
import urllib.request
|
|
from http.server import ThreadingHTTPServer
|
|
|
|
from gausse.optim.sweep import run_sweep
|
|
from gausse.web.page import PAGE_HTML
|
|
from gausse.web.stats import overview, run_detail
|
|
|
|
|
|
def _make_db(tmp_path):
|
|
db = tmp_path / "runs.sqlite3"
|
|
run_sweep(db, n_runs=60, n_workers=2, seed=11)
|
|
return db
|
|
|
|
|
|
def test_overview_reports_counts_and_top(tmp_path):
|
|
db = _make_db(tmp_path)
|
|
o = overview(db, db.with_suffix(".sqlite3.log"))
|
|
assert o["total"] == 60
|
|
assert o["feasible"] + o["infeasible"] == 60
|
|
assert len(o["efficiency_histogram"]) == 10
|
|
# лог процесса должен существовать и попасть в хвост
|
|
assert len(o["log_tail"]) > 0
|
|
if o["top"]:
|
|
# топ отсортирован по КПД по убыванию
|
|
effs = [t["efficiency"] for t in o["top"]]
|
|
assert effs == sorted(effs, reverse=True)
|
|
|
|
|
|
def test_run_detail_has_full_physics_and_distances(tmp_path):
|
|
db = _make_db(tmp_path)
|
|
o = overview(db, None)
|
|
assert o["top"], "нужен хотя бы один реализуемый прогон"
|
|
d = run_detail(db, o["top"][0]["run_id"])
|
|
assert d is not None
|
|
detail = d["detail"]
|
|
# ключевое, что просил пользователь: расстояния между катушками и позиции
|
|
assert "inter_stage_gaps_m" in detail
|
|
assert "coil_center_positions_m" in detail
|
|
stage0 = detail["stages"][0]
|
|
assert "electrical" in stage0 and "wire_resistance_ac_ohm" in stage0["electrical"]
|
|
# v2: скин/близость — AC-сопротивление обмотки выше DC
|
|
assert stage0["electrical"]["wire_ac_resistance_factor"] >= 1.0
|
|
assert "winding" in stage0 and "total_turns" in stage0["winding"]
|
|
assert "sensor_to_coil_distance_m" in stage0
|
|
|
|
|
|
def test_run_detail_missing_id_returns_none(tmp_path):
|
|
db = _make_db(tmp_path)
|
|
assert run_detail(db, "нет-такого-id") is None
|
|
|
|
|
|
def test_http_server_serves_page_and_api(tmp_path):
|
|
from functools import partial
|
|
|
|
from gausse.web.server import _Handler
|
|
|
|
db = _make_db(tmp_path)
|
|
handler = partial(_Handler, db_path=db, log_path=db.with_suffix(".sqlite3.log"))
|
|
httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
|
port = httpd.server_address[1]
|
|
t = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
t.start()
|
|
try:
|
|
base = f"http://127.0.0.1:{port}"
|
|
page = urllib.request.urlopen(base + "/", timeout=5).read().decode("utf-8")
|
|
assert "<!doctype html>" in page.lower()
|
|
api = json.loads(urllib.request.urlopen(base + "/api/overview", timeout=5).read())
|
|
assert api["total"] == 60
|
|
finally:
|
|
httpd.shutdown()
|
|
httpd.server_close()
|
|
|
|
|
|
def test_page_is_self_contained():
|
|
# никаких внешних ресурсов (CSP-безопасно): ни http-ссылок в src/href, ни CDN
|
|
assert "src=\"http" not in PAGE_HTML
|
|
assert "href=\"http" not in PAGE_HTML
|
|
|
|
|
|
def test_render_plot_png_returns_image_bytes(tmp_path):
|
|
from gausse.web.render import render_plot_png
|
|
from gausse.web.stats import overview
|
|
|
|
db = _make_db(tmp_path)
|
|
o = overview(db, None)
|
|
assert o["top"], "нужен реализуемый прогон для графика"
|
|
run_id = o["top"][0]["run_id"]
|
|
for kind in ("velocity", "field", "current"):
|
|
png = render_plot_png(db, run_id, kind)
|
|
assert png is not None and png[:8] == b"\x89PNG\r\n\x1a\n", f"{kind} не PNG"
|
|
|
|
|
|
def test_render_plot_unknown_kind_returns_none(tmp_path):
|
|
from gausse.web.render import render_plot_png
|
|
|
|
db = _make_db(tmp_path)
|
|
o = _overview_top_id(db)
|
|
assert render_plot_png(db, o, "nonsense") is None
|
|
|
|
|
|
def _overview_top_id(db):
|
|
from gausse.web.stats import overview
|
|
|
|
return overview(db, None)["top"][0]["run_id"]
|
|
|
|
|
|
def test_render_animation_gif_returns_gif_bytes(tmp_path):
|
|
from gausse.web.render import render_animation_gif
|
|
|
|
db = _make_db(tmp_path)
|
|
run_id = _overview_top_id(db)
|
|
gif = render_animation_gif(db, run_id)
|
|
assert gif is not None and gif[:6] in (b"GIF87a", b"GIF89a")
|