264 lines
8.3 KiB
Python
264 lines
8.3 KiB
Python
"""
|
||
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)
|