added web front end
This commit is contained in:
398
app.py
398
app.py
@@ -1,197 +1,263 @@
|
||||
"""
|
||||
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 model import TransformerModel
|
||||
from optimizer import TransformerOptimizer
|
||||
|
||||
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__)
|
||||
|
||||
# Default transformer configuration
|
||||
# You can modify this to match your actual transformer
|
||||
def get_default_transformer():
|
||||
|
||||
primary_taps = [0, 75, 75]
|
||||
secondary_taps = [0, 100, 150, 150, 150]
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Resistance per turn for each segment (example values in ohms/turn)
|
||||
# These would be calculated based on wire gauge, length per turn, etc.
|
||||
primary_Rp_per_turn = [
|
||||
0.01,
|
||||
0.01,
|
||||
]
|
||||
|
||||
secondary_Rs_per_turn = [
|
||||
0.004,
|
||||
0.024,
|
||||
0.024,
|
||||
0.024,
|
||||
]
|
||||
|
||||
|
||||
|
||||
tf = TransformerModel(
|
||||
Ae_mm2=354.0,
|
||||
Ve_mm3=43900.0,
|
||||
use_core_loss_model=True,
|
||||
Np_total=150,
|
||||
Ns_total=250,
|
||||
primary_taps=primary_taps,
|
||||
secondary_taps=secondary_taps,
|
||||
primary_Rp_per_turn=primary_Rp_per_turn,
|
||||
secondary_Rs_per_turn=secondary_Rs_per_turn,
|
||||
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
|
||||
)
|
||||
return tf
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
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
|
||||
|
||||
|
||||
@app.route('/manual')
|
||||
def manual():
|
||||
return render_template('manual.html')
|
||||
|
||||
|
||||
@app.route('/api/simulate', methods=['POST'])
|
||||
def simulate():
|
||||
"""Run manual simulation with specified tap and voltage"""
|
||||
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:
|
||||
data = request.json
|
||||
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()
|
||||
|
||||
# Extract parameters
|
||||
primary_tap = int(data.get('primary_tap', 1))
|
||||
secondary_tap = int(data.get('secondary_tap', 1))
|
||||
Vp_rms = float(data.get('Vp_rms', 12))
|
||||
freq_hz = float(data.get('freq_hz', 2000))
|
||||
load_ohms = float(data.get('load_ohms', 100))
|
||||
|
||||
# Create transformer
|
||||
tf = get_default_transformer()
|
||||
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,
|
||||
}
|
||||
|
||||
# Run simulation (core loss is calculated automatically)
|
||||
result = tf.simulate(
|
||||
primary_tap=primary_tap,
|
||||
secondary_tap=secondary_tap,
|
||||
Vp_rms=Vp_rms,
|
||||
freq_hz=freq_hz,
|
||||
load_ohms=load_ohms,
|
||||
core_loss_W=0.0, # Will be calculated by model
|
||||
)
|
||||
|
||||
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,
|
||||
'result': {
|
||||
'primary_tap': result['primary_tap'],
|
||||
'secondary_tap': result['secondary_tap'],
|
||||
'Np_eff': result['Np_eff'],
|
||||
'Ns_eff': result['Ns_eff'],
|
||||
'Vp_rms': round(result['Vp_rms'], 2),
|
||||
'Vs_rms': round(result['Vs_rms'], 2),
|
||||
'Ip_rms': round(result['Ip_rms'], 3),
|
||||
'Is_rms': round(result['Is_rms'], 3),
|
||||
'turns_ratio': round(result['turns_ratio'], 3),
|
||||
'P_out_W': round(result['P_out_W'], 2),
|
||||
'P_in_W': round(result['P_in_W'], 2),
|
||||
'P_cu_W': round(result['P_cu_W'], 2),
|
||||
'P_cu_primary_W': round(result['P_cu_primary_W'], 3),
|
||||
'P_cu_secondary_W': round(result['P_cu_secondary_W'], 3),
|
||||
'P_core_W': round(result['P_core_W'], 2),
|
||||
'efficiency': round(result['efficiency'] * 100, 2),
|
||||
'B_peak_T': round(result['B_peak_T'], 4),
|
||||
}
|
||||
"success": True,
|
||||
"windings": windings_info,
|
||||
"drawing": drawing_b64,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 400
|
||||
except Exception as exc:
|
||||
return jsonify({"success": False, "error": str(exc)}), 400
|
||||
|
||||
|
||||
@app.route('/api/optimize', methods=['POST'])
|
||||
def optimize():
|
||||
@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.json
|
||||
data = request.get_json(force=True)
|
||||
core = _parse_core(data)
|
||||
specs = _parse_windings(data)
|
||||
fill_factor = float(data.get("fill_factor", 0.35))
|
||||
|
||||
# Extract parameters
|
||||
load_ohms = float(data.get('load_ohms', 100))
|
||||
target_power_W = float(data.get('target_power_W', 10))
|
||||
freq_hz = float(data.get('freq_hz', 2000))
|
||||
Vp_min = float(data.get('Vp_min', 5))
|
||||
Vp_max = float(data.get('Vp_max', 36))
|
||||
Vp_step = float(data.get('Vp_step', 0.5))
|
||||
B_max_T = float(data.get('B_max_T', 0.3))
|
||||
Vs_max = float(data.get('Vs_max', 200))
|
||||
Is_max = float(data.get('Is_max', 1.5))
|
||||
power_tolerance_percent = float(data.get('power_tolerance_percent', 2.0))
|
||||
results = design_transformer(core, specs, fill_factor=fill_factor)
|
||||
|
||||
# Create transformer and optimizer
|
||||
tf = get_default_transformer()
|
||||
opt = TransformerOptimizer(tf)
|
||||
# Expect exactly 2 windings: primary and secondary
|
||||
if len(results) < 2:
|
||||
return jsonify({"success": False, "error": "Need at least 2 windings"}), 400
|
||||
|
||||
# Run optimization (core loss is calculated automatically)
|
||||
result = opt.optimize(
|
||||
load_ohms=load_ohms,
|
||||
target_power_W=target_power_W,
|
||||
freq_hz=freq_hz,
|
||||
Vp_min=Vp_min,
|
||||
Vp_max=Vp_max,
|
||||
Vp_step=Vp_step,
|
||||
B_max_T=B_max_T,
|
||||
Vs_max=Vs_max,
|
||||
Is_max=Is_max,
|
||||
core_loss_W=0.0, # Will be calculated by model
|
||||
power_tolerance_percent=power_tolerance_percent,
|
||||
# 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"))),
|
||||
)
|
||||
|
||||
if result:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'result': {
|
||||
'primary_tap': result.primary_tap,
|
||||
'secondary_tap': result.secondary_tap,
|
||||
'Np_eff': result.Np_eff,
|
||||
'Ns_eff': result.Ns_eff,
|
||||
'Vp_rms': round(result.Vp_rms, 2),
|
||||
'Vs_rms': round(result.Vs_rms, 2),
|
||||
'Ip_rms': round(result.Ip_rms, 3),
|
||||
'Is_rms': round(result.Is_rms, 3),
|
||||
'turns_ratio': round(result.turns_ratio, 3),
|
||||
'P_out_W': round(result.P_out_W, 2),
|
||||
'P_in_W': round(result.P_in_W, 2),
|
||||
'P_cu_W': round(result.P_cu_W, 2),
|
||||
'P_cu_primary_W': round(result.P_cu_primary_W, 3),
|
||||
'P_cu_secondary_W': round(result.P_cu_secondary_W, 3),
|
||||
'P_core_W': round(result.P_core_W, 2),
|
||||
'efficiency': round(result.efficiency * 100, 2),
|
||||
'B_peak_T': round(result.B_peak_T, 4),
|
||||
'power_error_percent': round(result.power_error_percent, 2),
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'No valid configuration found within constraints'
|
||||
})
|
||||
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]
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 400
|
||||
"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
|
||||
|
||||
|
||||
@app.route('/api/transformer_info', methods=['GET'])
|
||||
def transformer_info():
|
||||
"""Return transformer configuration information"""
|
||||
tf = get_default_transformer()
|
||||
return jsonify({
|
||||
'primary_taps': tf.primary_taps,
|
||||
'secondary_taps': tf.secondary_taps,
|
||||
'Ae_mm2': tf.Ae_mm2,
|
||||
'Np_total': tf.Np_total,
|
||||
'Ns_total': tf.Ns_total,
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, host="0.0.0.0", port=5000)
|
||||
|
||||
Reference in New Issue
Block a user