""" 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)