Files
toroid/app.py
2026-02-13 14:20:30 -06:00

264 lines
8.3 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.
"""
Flask web interface for the toroidal transformer designer + simulator.
Routes
------
GET / → main page
POST /api/design → run design_transformer(), return winding info + drawing PNG (base64)
POST /api/sweep → run sweep_operating_points(), return JSON dataset
"""
import base64
import math
import os
import tempfile
from flask import Flask, render_template, request, jsonify
from designer import ToroidCore, WindingSpec, design_transformer
from sim_toroid import (
ToroidSimulator, SimConstraints,
sweep_operating_points, SweepEntry,
)
from draw_toroid import draw_toroid
app = Flask(__name__)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _parse_core(data: dict) -> ToroidCore:
"""Build ToroidCore from request dict."""
Ae = data.get("Ae_mm2")
Ve = data.get("Ve_mm3")
return ToroidCore(
ID_mm=float(data["ID_mm"]),
OD_mm=float(data["OD_mm"]),
height_mm=float(data["height_mm"]),
Ae_mm2=float(Ae) if Ae not in (None, "") else None,
Ve_mm3=float(Ve) if Ve not in (None, "") else None,
Pv_func=None, # not editable in the UI; uses built-in fallback
)
def _parse_windings(data: dict) -> list[WindingSpec]:
"""
Parse winding list from request dict.
Expected format:
windings: [
{ name: "primary", taps: [0, 25, 50], awg: [22, 22] },
{ name: "secondary", taps: [0, 100, 50, 50, 50], awg: [22, 22, 22, 26] },
]
"""
specs = []
for w in data["windings"]:
taps = [int(t) for t in w["taps"]]
awg = [int(a) for a in w["awg"]]
specs.append(WindingSpec(awg=awg, taps=taps, name=str(w.get("name", "winding"))))
return specs
def _drawing_b64(core: ToroidCore, results) -> str:
"""Render the toroid PNG and return it as a base64 data-URI string."""
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
tmp_path = f.name
try:
draw_toroid(core, results, output_path=tmp_path, dpi=150, fig_size_mm=160)
with open(tmp_path, "rb") as f:
png_bytes = f.read()
finally:
try:
os.unlink(tmp_path)
except OSError:
pass
return "data:image/png;base64," + base64.b64encode(png_bytes).decode()
def _winding_result_to_dict(wr) -> dict:
"""Serialise a WindingResult for JSON."""
segs = []
for seg in wr.segments:
segs.append({
"segment_index": seg.segment_index,
"tap_number": seg.segment_index + 1,
"awg": seg.awg,
"turns": seg.turns,
"wire_length_m": round(seg.wire_length_m, 4),
"resistance_mohm": round(seg.resistance_ohm * 1000, 3),
"weight_g": round(seg.weight_g, 3),
"fits": seg.fits,
"layers": [
{
"layer_index": lr.layer_index,
"turns_capacity": lr.turns_capacity,
"turns_used": lr.turns_used,
"L_turn_mm": round(lr.L_turn_mm, 2),
}
for lr in seg.layers
],
})
return {
"name": wr.spec.name,
"total_turns": wr.spec.total_turns,
"feasible": wr.feasible,
"total_wire_length_m": round(wr.total_wire_length_m, 4),
"total_resistance_mohm": round(wr.total_resistance_ohm * 1000, 3),
"total_weight_g": round(wr.total_weight_g, 3),
"segments": segs,
"n_taps": len(wr.spec.taps) - 1,
}
def _sweep_entry_to_dict(e: SweepEntry) -> dict:
"""Convert SweepEntry to a plain dict suitable for JSON."""
d = e.as_dict()
# Replace NaN (not JSON-serialisable) with None
for k, v in d.items():
if isinstance(v, float) and math.isnan(v):
d[k] = None
return d
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@app.route("/")
def index():
return render_template("index.html")
@app.route("/api/design", methods=["POST"])
def api_design():
"""
Run design_transformer() and return winding info + PNG drawing.
Request body (JSON):
{
"ID_mm": 21.5, "OD_mm": 46.5, "height_mm": 22.8,
"Ae_mm2": 142.5, // optional
"Ve_mm3": 15219, // optional
"fill_factor": 0.35,
"windings": [
{ "name": "primary", "taps": [0, 25, 50], "awg": [22, 22] },
{ "name": "secondary", "taps": [0, 100, 50, 50, 50],"awg": [22, 22, 22, 26] }
]
}
"""
try:
data = request.get_json(force=True)
core = _parse_core(data)
specs = _parse_windings(data)
fill_factor = float(data.get("fill_factor", 0.35))
results = design_transformer(core, specs, fill_factor=fill_factor)
drawing_b64 = _drawing_b64(core, results)
windings_info = [_winding_result_to_dict(wr) for wr in results]
return jsonify({
"success": True,
"windings": windings_info,
"drawing": drawing_b64,
})
except Exception as exc:
return jsonify({"success": False, "error": str(exc)}), 400
@app.route("/api/sweep", methods=["POST"])
def api_sweep():
"""
Run sweep_operating_points() over a grid of frequencies × loads.
Request body (JSON):
{
// Same core + windings as /api/design ...
"fill_factor": 0.35,
"frequencies": [256, 870, 3140],
"loads": [[10, 0], [50, 0], [100, 0]], // [R, X] pairs
"target_power_W": 25.0,
"Vp_min": 1.0,
"Vp_max": 50.0,
"Vp_steps": 100,
"power_tol_pct": 2.0,
"constraints": {
"B_max_T": 0.3,
"Vp_max": 50.0,
"Vs_max": 120.0,
"Ip_max": 3.0,
"Is_max": 2.0,
"P_out_max_W": 100.0
}
}
"""
try:
data = request.get_json(force=True)
core = _parse_core(data)
specs = _parse_windings(data)
fill_factor = float(data.get("fill_factor", 0.35))
results = design_transformer(core, specs, fill_factor=fill_factor)
# Expect exactly 2 windings: primary and secondary
if len(results) < 2:
return jsonify({"success": False, "error": "Need at least 2 windings"}), 400
# Use first winding as primary, second as secondary
primary_result = results[0]
secondary_result = results[1]
sim = ToroidSimulator(core=core, primary=primary_result, secondary=secondary_result)
# Constraints
cdata = data.get("constraints", {})
constraints = SimConstraints(
B_max_T=float(cdata.get("B_max_T", 0.3)),
Vp_max=float(cdata.get("Vp_max", float("inf"))),
Vs_max=float(cdata.get("Vs_max", float("inf"))),
Ip_max=float(cdata.get("Ip_max", float("inf"))),
Is_max=float(cdata.get("Is_max", float("inf"))),
P_out_max_W=float(cdata.get("P_out_max_W", float("inf"))),
)
frequencies = [float(f) for f in data.get("frequencies", [256.0])]
loads = [(float(p[0]), float(p[1])) for p in data.get("loads", [[10.0, 0.0]])]
target_power_W = float(data.get("target_power_W", 10.0))
Vp_min = float(data.get("Vp_min", 1.0))
Vp_max_sweep = float(data.get("Vp_max", 50.0))
Vp_steps = int(data.get("Vp_steps", 100))
power_tol_pct = float(data.get("power_tol_pct", 2.0))
entries = sweep_operating_points(
sim=sim,
frequencies=frequencies,
loads=loads,
target_power_W=target_power_W,
constraints=constraints,
Vp_min=Vp_min,
Vp_max=Vp_max_sweep,
Vp_steps=Vp_steps,
power_tol_pct=power_tol_pct,
)
rows = [_sweep_entry_to_dict(e) for e in entries]
return jsonify({
"success": True,
"rows": rows,
"frequencies": frequencies,
"loads": [[r, x] for r, x in loads],
})
except Exception as exc:
import traceback
return jsonify({"success": False, "error": str(exc),
"traceback": traceback.format_exc()}), 400
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000)