added preset saving and heat transfer

This commit is contained in:
2026-02-13 15:06:15 -06:00
parent f707c87f7e
commit a881a0a381
7 changed files with 559 additions and 20 deletions

130
app.py
View File

@@ -9,13 +9,14 @@ POST /api/sweep → run sweep_operating_points(), return JSON dataset
"""
import base64
import json
import math
import os
import tempfile
from flask import Flask, render_template, request, jsonify
from designer import ToroidCore, WindingSpec, design_transformer
from designer import ToroidCore, WindingSpec, WireSpec, design_transformer
from sim_toroid import (
ToroidSimulator, SimConstraints,
sweep_operating_points, SweepEntry,
@@ -61,12 +62,30 @@ def _parse_windings(data: dict) -> list[WindingSpec]:
return specs
def _drawing_b64(core: ToroidCore, results) -> str:
def _actual_fill_factor(core: ToroidCore, results) -> float:
"""
Compute actual fill factor = total wire cross-section area placed / window area.
Sums wire area for every turn placed across all windings.
"""
window_area = core.window_area_mm2
if window_area <= 0:
return 0.0
total_wire_area = 0.0
for wr in results:
for seg in wr.segments:
wire = WireSpec.from_awg(seg.awg)
r = wire.diameter_mm / 2.0
total_wire_area += math.pi * r * r * seg.turns
return total_wire_area / window_area
def _drawing_b64(core: ToroidCore, results, actual_fill: float) -> 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)
draw_toroid(core, results, output_path=tmp_path, dpi=150, fig_size_mm=160,
actual_fill=actual_fill)
with open(tmp_path, "rb") as f:
png_bytes = f.read()
finally:
@@ -156,13 +175,28 @@ def api_design():
results = design_transformer(core, specs, fill_factor=fill_factor)
drawing_b64 = _drawing_b64(core, results)
actual_fill = _actual_fill_factor(core, results)
drawing_b64 = _drawing_b64(core, results, actual_fill)
windings_info = [_winding_result_to_dict(wr) for wr in results]
# Thermal surface area of wound toroid (m²):
# outer cylinder + top/bottom annular faces + inner bore cylinder
OD_m = core.OD_mm * 1e-3
ID_m = core.ID_mm * 1e-3
H_m = core.height_mm * 1e-3
A_outer = math.pi * OD_m * H_m
A_faces = 2 * math.pi / 4 * (OD_m**2 - ID_m**2)
A_bore = math.pi * ID_m * H_m
A_surface_m2 = A_outer + A_faces + A_bore
return jsonify({
"success": True,
"windings": windings_info,
"drawing": drawing_b64,
"fill_factor_target": fill_factor,
"fill_factor_actual": round(actual_fill, 4),
"window_area_mm2": round(core.window_area_mm2, 2),
"A_surface_m2": round(A_surface_m2, 6),
})
except Exception as exc:
@@ -259,5 +293,93 @@ def api_sweep():
"traceback": traceback.format_exc()}), 400
# ---------------------------------------------------------------------------
# Preset persistence
# ---------------------------------------------------------------------------
PRESETS_DIR = os.path.join(os.path.dirname(__file__), "presets")
os.makedirs(PRESETS_DIR, exist_ok=True)
# Valid preset categories
_PRESET_TYPES = {"core", "windings", "constraints", "sim"}
def _preset_path(ptype: str) -> str:
return os.path.join(PRESETS_DIR, f"{ptype}.json")
def _load_store(ptype: str) -> dict:
"""Load the JSON store for a preset type; return {} on missing/corrupt."""
path = _preset_path(ptype)
if not os.path.exists(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def _save_store(ptype: str, store: dict) -> None:
path = _preset_path(ptype)
with open(path, "w", encoding="utf-8") as f:
json.dump(store, f, indent=2)
@app.route("/api/presets/<ptype>", methods=["GET"])
def presets_list(ptype: str):
"""Return list of saved preset names and the last-used name."""
if ptype not in _PRESET_TYPES:
return jsonify({"success": False, "error": f"Unknown type '{ptype}'"}), 400
store = _load_store(ptype)
names = [k for k in store if not k.startswith("_")]
last = store.get("_last", None)
return jsonify({"success": True, "names": sorted(names), "last": last})
@app.route("/api/presets/<ptype>/<name>", methods=["GET"])
def presets_load(ptype: str, name: str):
"""Load a named preset and mark it as last-used."""
if ptype not in _PRESET_TYPES:
return jsonify({"success": False, "error": f"Unknown type '{ptype}'"}), 400
store = _load_store(ptype)
if name not in store:
return jsonify({"success": False, "error": f"Preset '{name}' not found"}), 404
store["_last"] = name
_save_store(ptype, store)
return jsonify({"success": True, "name": name, "data": store[name]})
@app.route("/api/presets/<ptype>/<name>", methods=["POST"])
def presets_save(ptype: str, name: str):
"""Save (create or overwrite) a named preset and mark it as last-used."""
if ptype not in _PRESET_TYPES:
return jsonify({"success": False, "error": f"Unknown type '{ptype}'"}), 400
if not name or name.startswith("_"):
return jsonify({"success": False, "error": "Invalid preset name"}), 400
data = request.get_json(force=True)
store = _load_store(ptype)
store[name] = data
store["_last"] = name
_save_store(ptype, store)
return jsonify({"success": True, "name": name})
@app.route("/api/presets/<ptype>/<name>", methods=["DELETE"])
def presets_delete(ptype: str, name: str):
"""Delete a named preset."""
if ptype not in _PRESET_TYPES:
return jsonify({"success": False, "error": f"Unknown type '{ptype}'"}), 400
store = _load_store(ptype)
if name not in store:
return jsonify({"success": False, "error": f"Preset '{name}' not found"}), 404
del store[name]
if store.get("_last") == name:
remaining = [k for k in store if not k.startswith("_")]
store["_last"] = remaining[-1] if remaining else None
_save_store(ptype, store)
return jsonify({"success": True})
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000)