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 base64
import json
import math import math
import os import os
import tempfile import tempfile
from flask import Flask, render_template, request, jsonify 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 ( from sim_toroid import (
ToroidSimulator, SimConstraints, ToroidSimulator, SimConstraints,
sweep_operating_points, SweepEntry, sweep_operating_points, SweepEntry,
@@ -61,12 +62,30 @@ def _parse_windings(data: dict) -> list[WindingSpec]:
return specs 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.""" """Render the toroid PNG and return it as a base64 data-URI string."""
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f: with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
tmp_path = f.name tmp_path = f.name
try: 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: with open(tmp_path, "rb") as f:
png_bytes = f.read() png_bytes = f.read()
finally: finally:
@@ -156,13 +175,28 @@ def api_design():
results = design_transformer(core, specs, fill_factor=fill_factor) 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] 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({ return jsonify({
"success": True, "success": True,
"windings": windings_info, "windings": windings_info,
"drawing": drawing_b64, "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: except Exception as exc:
@@ -259,5 +293,93 @@ def api_sweep():
"traceback": traceback.format_exc()}), 400 "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__": if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=5000) app.run(debug=True, host="0.0.0.0", port=5000)

View File

@@ -78,6 +78,7 @@ def draw_toroid(
output_path: str = "toroid.png", output_path: str = "toroid.png",
dpi: int = 180, dpi: int = 180,
fig_size_mm: float = 180.0, fig_size_mm: float = 180.0,
actual_fill: float | None = None,
) -> None: ) -> None:
""" """
Render a to-scale top-down cross-section of the transformer as a PNG. Render a to-scale top-down cross-section of the transformer as a PNG.
@@ -110,9 +111,10 @@ def draw_toroid(
ax.set_xlabel("mm", fontsize=8) ax.set_xlabel("mm", fontsize=8)
ax.set_ylabel("mm", fontsize=8) ax.set_ylabel("mm", fontsize=8)
ax.tick_params(labelsize=7) ax.tick_params(labelsize=7)
fill_str = f" fill={actual_fill*100:.1f}%" if actual_fill is not None else ""
ax.set_title( ax.set_title(
f"Toroid cross-section " f"Toroid cross-section "
f"ID={ID:.1f} mm OD={OD:.1f} mm H={core.height_mm:.1f} mm", f"ID={ID:.1f} mm OD={OD:.1f} mm H={core.height_mm:.1f} mm{fill_str}",
fontsize=9, pad=6, fontsize=9, pad=6,
) )

11
presets/constraints.json Normal file
View File

@@ -0,0 +1,11 @@
{
"C1": {
"B_max_T": 1,
"Vp_max": 50,
"Vs_max": 90,
"Ip_max": 3,
"Is_max": 2,
"P_out_max_W": 100
},
"_last": "C1"
}

11
presets/core.json Normal file
View File

@@ -0,0 +1,11 @@
{
"FT-3KM F4424G": {
"ID_mm": 21.5,
"OD_mm": 46.5,
"height_mm": 22.8,
"fill_factor": 0.35,
"Ae_mm2": 142.5,
"Ve_mm3": 15219
},
"_last": "FT-3KM F4424G"
}

12
presets/sim.json Normal file
View File

@@ -0,0 +1,12 @@
{
"points": {
"frequencies": "256\n870\n3140\n8900\n12000\n16000\n22000\n33000\n45000",
"loads": "5\n10\n50\n100\n200\n600\n2000",
"target_power_W": 25,
"power_tol_pct": 2,
"Vp_min": 1,
"Vp_max": 50,
"Vp_steps": 100
},
"_last": "points"
}

35
presets/windings.json Normal file
View File

@@ -0,0 +1,35 @@
{
"Design 1": {
"windings": [
{
"name": "primary",
"taps": [
0,
25,
50
],
"awg": [
22,
22
]
},
{
"name": "secondary",
"taps": [
0,
100,
50,
50,
50
],
"awg": [
22,
22,
22,
22
]
}
]
},
"_last": "Design 1"
}

View File

@@ -285,6 +285,46 @@
} }
#plots-card { position: relative; } #plots-card { position: relative; }
/* ---- Preset bar ---- */
.preset-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
background: #f1f5f9;
border-bottom: 1px solid #e2e8f0;
flex-wrap: wrap;
}
.preset-bar select {
flex: 1;
min-width: 100px;
max-width: 200px;
font-size: 12px;
padding: 3px 6px;
}
.preset-bar input[type=text] {
flex: 1;
min-width: 80px;
max-width: 160px;
font-size: 12px;
padding: 3px 6px;
}
.btn-preset {
padding: 3px 10px;
font-size: 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
white-space: nowrap;
}
.btn-preset-load { background: #dbeafe; color: #1d4ed8; }
.btn-preset-load:hover { background: #bfdbfe; }
.btn-preset-save { background: #dcfce7; color: #166534; }
.btn-preset-save:hover { background: #bbf7d0; }
.btn-preset-delete { background: #fee2e2; color: #b91c1c; }
.btn-preset-delete:hover { background: #fca5a5; }
/* ---- Sweep results table ---- */ /* ---- Sweep results table ---- */
.sweep-table-wrap { .sweep-table-wrap {
overflow-x: auto; overflow-x: auto;
@@ -334,7 +374,7 @@
/* ---- Responsive: collapse to single column on narrow screens ---- */ /* ---- Responsive: collapse to single column on narrow screens ---- */
@media (max-width: 860px) { @media (max-width: 860px) {
.layout { grid-template-columns: 1fr; } .layout { grid-template-columns: 1fr; }
.plots-grid { grid-template-columns: 1fr; } .plots-grid { grid-template-columns: 1fr !important; }
} }
</style> </style>
</head> </head>
@@ -353,6 +393,13 @@
<!-- Core Parameters --> <!-- Core Parameters -->
<div class="card"> <div class="card">
<div class="card-header">Core Parameters</div> <div class="card-header">Core Parameters</div>
<div class="preset-bar">
<select id="preset-sel-core" title="Saved cores"></select>
<button class="btn-preset btn-preset-load" onclick="presetLoad('core')">Load</button>
<input type="text" id="preset-name-core" placeholder="preset name">
<button class="btn-preset btn-preset-save" onclick="presetSave('core')">Save</button>
<button class="btn-preset btn-preset-delete" onclick="presetDelete('core')">Delete</button>
</div>
<div class="card-body"> <div class="card-body">
<div class="form-row three"> <div class="form-row three">
<label class="field"><span>ID (mm)</span><input type="number" id="core-ID" value="21.5" min="1" step="0.1"></label> <label class="field"><span>ID (mm)</span><input type="number" id="core-ID" value="21.5" min="1" step="0.1"></label>
@@ -372,6 +419,13 @@
<!-- Windings --> <!-- Windings -->
<div class="card"> <div class="card">
<div class="card-header">Windings</div> <div class="card-header">Windings</div>
<div class="preset-bar">
<select id="preset-sel-windings" title="Saved windings"></select>
<button class="btn-preset btn-preset-load" onclick="presetLoad('windings')">Load</button>
<input type="text" id="preset-name-windings" placeholder="preset name">
<button class="btn-preset btn-preset-save" onclick="presetSave('windings')">Save</button>
<button class="btn-preset btn-preset-delete" onclick="presetDelete('windings')">Delete</button>
</div>
<div class="card-body"> <div class="card-body">
<div id="windings-container"></div> <div id="windings-container"></div>
<div class="btn-row"> <div class="btn-row">
@@ -397,6 +451,13 @@
<!-- Simulation Parameters --> <!-- Simulation Parameters -->
<div class="card"> <div class="card">
<div class="card-header">Simulation</div> <div class="card-header">Simulation</div>
<div class="preset-bar">
<select id="preset-sel-sim" title="Saved sim settings"></select>
<button class="btn-preset btn-preset-load" onclick="presetLoad('sim')">Load</button>
<input type="text" id="preset-name-sim" placeholder="preset name">
<button class="btn-preset btn-preset-save" onclick="presetSave('sim')">Save</button>
<button class="btn-preset btn-preset-delete" onclick="presetDelete('sim')">Delete</button>
</div>
<div class="card-body"> <div class="card-body">
<div class="form-row one"> <div class="form-row one">
<label class="field"> <label class="field">
@@ -432,7 +493,14 @@
<details style="margin: 8px 0"> <details style="margin: 8px 0">
<summary style="cursor:pointer; font-size:12px; color:#2d6a9f; font-weight:500">Constraints (expand)</summary> <summary style="cursor:pointer; font-size:12px; color:#2d6a9f; font-weight:500">Constraints (expand)</summary>
<div style="margin-top:8px"> <div class="preset-bar" style="margin:6px 0; padding:4px 0; background:none; border:none">
<select id="preset-sel-constraints" title="Saved constraints"></select>
<button class="btn-preset btn-preset-load" onclick="presetLoad('constraints')">Load</button>
<input type="text" id="preset-name-constraints" placeholder="preset name">
<button class="btn-preset btn-preset-save" onclick="presetSave('constraints')">Save</button>
<button class="btn-preset btn-preset-delete" onclick="presetDelete('constraints')">Delete</button>
</div>
<div style="margin-top:4px">
<div class="form-row"> <div class="form-row">
<label class="field"><span>B max (T)</span><input type="number" id="con-B" value="0.3" min="0.01" step="0.01"></label> <label class="field"><span>B max (T)</span><input type="number" id="con-B" value="0.3" min="0.01" step="0.01"></label>
<label class="field"><span>Vp max (V)</span><input type="number" id="con-Vp" value="50" min="1" step="1"></label> <label class="field"><span>Vp max (V)</span><input type="number" id="con-Vp" value="50" min="1" step="1"></label>
@@ -448,6 +516,18 @@
</div> </div>
</details> </details>
<div class="form-row" style="margin-top:8px">
<label class="field">
<span>Ambient temp (°C)</span>
<input type="number" id="sim-tambient" value="25" min="-40" max="100" step="1">
</label>
<label class="field">
<span>Conv. coeff h (W/m²K)</span>
<input type="number" id="sim-hconv" value="6" min="1" max="100" step="0.5"
title="Still-air natural convection ≈ 510 W/m²K">
</label>
</div>
<div class="btn-row"> <div class="btn-row">
<button class="btn btn-primary" id="btn-sim" onclick="runSweep()">Run Simulation</button> <button class="btn btn-primary" id="btn-sim" onclick="runSweep()">Run Simulation</button>
</div> </div>
@@ -480,10 +560,11 @@
<select id="freq-select" onchange="updatePlots()"></select> <select id="freq-select" onchange="updatePlots()"></select>
<span id="plot-status" style="font-size:12px;color:#64748b;margin-left:8px"></span> <span id="plot-status" style="font-size:12px;color:#64748b;margin-left:8px"></span>
</div> </div>
<div class="plots-grid"> <div class="plots-grid" style="grid-template-columns: 1fr 1fr;">
<div class="plot-box" id="plot-voltage"></div> <div class="plot-box" id="plot-voltage"></div>
<div class="plot-box" id="plot-current"></div> <div class="plot-box" id="plot-current"></div>
<div class="plot-box" id="plot-power"></div> <div class="plot-box" id="plot-power"></div>
<div class="plot-box" id="plot-temp"></div>
</div> </div>
</div> </div>
@@ -512,7 +593,8 @@
// State // State
// ============================================================ // ============================================================
let windingCount = 0; let windingCount = 0;
let sweepData = null; // last successful sweep result let sweepData = null; // last successful sweep result
let designSurfaceArea = null; // A_surface_m2 from last design run
// ============================================================ // ============================================================
// Winding builder // Winding builder
@@ -656,8 +738,9 @@ async function runDesign() {
return; return;
} }
setMsg(msg, 'ok', 'Design complete.'); setMsg(msg, 'ok', 'Design complete.');
designSurfaceArea = data.A_surface_m2 || null;
showDrawing(data.drawing); showDrawing(data.drawing);
showDesignResults(data.windings); showDesignResults(data.windings, data.fill_factor_target, data.fill_factor_actual, data.window_area_mm2, data.A_surface_m2);
resContainer.style.display = 'block'; resContainer.style.display = 'block';
} catch(e) { } catch(e) {
setMsg(msg, 'error', 'Network error: ' + e.message); setMsg(msg, 'error', 'Network error: ' + e.message);
@@ -676,9 +759,29 @@ function showDrawing(b64) {
img.style.display = 'block'; img.style.display = 'block';
} }
function showDesignResults(windings) { function showDesignResults(windings, fillTarget, fillActual, windowArea, surfaceArea) {
const cont = document.getElementById('design-results-tables'); const cont = document.getElementById('design-results-tables');
cont.innerHTML = ''; cont.innerHTML = '';
// Fill factor summary row
if (fillActual != null) {
const pct = (fillActual * 100).toFixed(1);
const tgtPct = fillTarget != null ? (fillTarget * 100).toFixed(0) : '?';
const over = fillActual > fillTarget * 1.005;
const color = over ? '#b91c1c' : '#166534';
const surfStr = surfaceArea != null
? `&nbsp;|&nbsp; Surface area: <strong>${(surfaceArea * 1e4).toFixed(1)} cm²</strong>` : '';
cont.innerHTML += `
<div style="margin-bottom:10px;padding:6px 10px;background:#f1f5f9;border-radius:6px;font-size:12px">
Window area: <strong>${windowArea != null ? windowArea.toFixed(1) : '?'} mm²</strong>
&nbsp;|&nbsp;
Target fill: <strong>${tgtPct}%</strong>
&nbsp;|&nbsp;
Actual fill: <strong style="color:${color}">${pct}%</strong>
${surfStr}
</div>`;
}
windings.forEach(w => { windings.forEach(w => {
const feasStr = w.feasible const feasStr = w.feasible
? '<span class="badge-ok">OK</span>' ? '<span class="badge-ok">OK</span>'
@@ -896,12 +999,39 @@ function updatePlots() {
document.getElementById('plot-status').textContent = document.getElementById('plot-status').textContent =
`${rows.length} points, ${rows.filter(r => r.met_target).length} met target`; `${rows.length} points, ${rows.filter(r => r.met_target).length} met target`;
updateTable(rows); // --- Temperature plot ---
const T_amb = parseFloat(document.getElementById('sim-tambient').value) || 25;
const h_conv = parseFloat(document.getElementById('sim-hconv').value) || 6;
const A_surf = designSurfaceArea; // m² from last design, null if not run yet
const T_rise = rows.map(r => {
if (r.P_in_W == null || r.P_out_W == null || !A_surf) return null;
return (r.P_in_W - r.P_out_W) / (h_conv * A_surf);
});
const T_copper = T_rise.map(dt => dt != null ? T_amb + dt : null);
Plotly.react('plot-temp', [
{ x, y: T_rise, name: 'T rise (°C)', mode: 'lines+markers', type: 'scatter',
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>ΔT=%{y:.1f}°C<br>%{text}<extra></extra>' },
{ x, y: T_copper, name: 'T copper (°C)', mode: 'lines+markers', type: 'scatter',
line: {dash: 'dot'},
text: hoverTap, hovertemplate: 'R=%{x}Ω<br>T=%{y:.1f}°C<br>%{text}<extra></extra>' },
], {
title: { text: `Temperature @ ${freqLabel} (T_amb=${T_amb}°C, h=${h_conv} W/m²K)`, font: {size:12} },
xaxis: { title: 'R load (Ω)', type: 'log' },
yaxis: { title: 'Temperature (°C)' },
margin: {t:40, b:42, l:54, r:10},
legend: {font:{size:11}},
height: plotH,
}, {responsive: true, displayModeBar: false});
updateTable(rows, T_rise, T_copper);
} }
// ============================================================ // ============================================================
// Results table (CSV-equivalent, all rows for selected freq) // Results table (CSV-equivalent, all rows for selected freq)
// ============================================================ // ============================================================
// All fields — used for CSV export only
const _CSV_COLS = [ const _CSV_COLS = [
{ key: 'freq_hz', label: 'freq (Hz)' }, { key: 'freq_hz', label: 'freq (Hz)' },
{ key: 'Z_load_R', label: 'R (Ω)' }, { key: 'Z_load_R', label: 'R (Ω)' },
@@ -931,7 +1061,37 @@ const _CSV_COLS = [
{ key: 'efficiency', label: 'eff' }, { key: 'efficiency', label: 'eff' },
]; ];
function fmtCell(key, val) { // Columns shown in the on-screen table (subset + computed P_loss)
const _TABLE_COLS = [
{ key: 'freq_hz', label: 'freq (Hz)' },
{ key: 'Z_load_R', label: 'R (Ω)' },
{ key: 'Z_load_X', label: 'X (Ω)' },
{ key: 'met_target', label: 'met' },
{ key: 'primary_tap', label: 'P tap' },
{ key: 'secondary_tap', label: 'S tap' },
{ key: 'Vp_rms', label: 'Vp (V)' },
{ key: 'Np_eff', label: 'Np' },
{ key: 'Ns_eff', label: 'Ns' },
{ key: 'B_peak_T', label: 'B (T)' },
{ key: 'Vs_rms', label: 'Vs (V)' },
{ key: 'Ip_rms', label: 'Ip (A)' },
{ key: 'Is_rms', label: 'Is (A)' },
{ key: 'P_in_W', label: 'P_in (W)' },
{ key: 'P_out_W', label: 'P_out (W)' },
{ key: '_P_loss', label: 'P_loss (W)' }, // computed
{ key: 'efficiency', label: 'eff' },
{ key: '_T_rise', label: 'ΔT (°C)' }, // computed from thermal model
{ key: '_T_copper', label: 'T_cu (°C)' }, // computed
];
function fmtCell(key, val, row) {
if (key === '_P_loss') {
const pin = row && row.P_in_W != null ? row.P_in_W : null;
const pout = row && row.P_out_W != null ? row.P_out_W : null;
val = (pin != null && pout != null) ? pin - pout : null;
}
if (key === '_T_rise') { val = row ? row._T_rise : null; }
if (key === '_T_copper') { val = row ? row._T_copper : null; }
if (val === null || val === undefined) return '<span class="cell-null">—</span>'; if (val === null || val === undefined) return '<span class="cell-null">—</span>';
if (key === 'met_target') { if (key === 'met_target') {
return val return val
@@ -942,22 +1102,27 @@ function fmtCell(key, val) {
return (val * 100).toFixed(2) + '%'; return (val * 100).toFixed(2) + '%';
} }
if (typeof val === 'number') { if (typeof val === 'number') {
// integers: no decimals; floats: 2 decimal places
return Number.isInteger(val) ? val : val.toFixed(2); return Number.isInteger(val) ? val : val.toFixed(2);
} }
return val; return val;
} }
function updateTable(rows) { function updateTable(rows, T_rise, T_copper) {
document.getElementById('table-card').style.display = 'block'; document.getElementById('table-card').style.display = 'block';
// Attach computed thermal values directly onto each row object
rows.forEach((r, i) => {
r._T_rise = T_rise ? T_rise[i] : null;
r._T_copper = T_copper ? T_copper[i] : null;
});
const thead = document.getElementById('sweep-table-head'); const thead = document.getElementById('sweep-table-head');
const tbody = document.getElementById('sweep-table-body'); const tbody = document.getElementById('sweep-table-body');
thead.innerHTML = '<tr>' + _CSV_COLS.map(c => `<th>${c.label}</th>`).join('') + '</tr>'; thead.innerHTML = '<tr>' + _TABLE_COLS.map(c => `<th>${c.label}</th>`).join('') + '</tr>';
tbody.innerHTML = rows.map(r => tbody.innerHTML = rows.map(r =>
'<tr>' + _CSV_COLS.map(c => `<td>${fmtCell(c.key, r[c.key])}</td>`).join('') + '</tr>' '<tr>' + _TABLE_COLS.map(c => `<td>${fmtCell(c.key, r[c.key], r)}</td>`).join('') + '</tr>'
).join(''); ).join('');
} }
@@ -1024,10 +1189,191 @@ function calcPlotHeight() {
})(); })();
// ============================================================ // ============================================================
// Initialise with default windings // Preset system
// ============================================================ // ============================================================
addWinding('primary', [25, 50], [22, 22]);
addWinding('secondary', [100, 50, 50, 50], [22, 22, 22, 26]); // Serialise current UI state for each preset type
function presetExtract(ptype) {
if (ptype === 'core') {
const d = {
ID_mm: parseFloat(document.getElementById('core-ID').value),
OD_mm: parseFloat(document.getElementById('core-OD').value),
height_mm: parseFloat(document.getElementById('core-H').value),
fill_factor: parseFloat(document.getElementById('core-fill').value),
};
const ae = document.getElementById('core-Ae').value.trim();
const ve = document.getElementById('core-Ve').value.trim();
if (ae !== '') d.Ae_mm2 = parseFloat(ae);
if (ve !== '') d.Ve_mm3 = parseFloat(ve);
return d;
}
if (ptype === 'windings') {
const windings = [];
document.querySelectorAll('.winding-block').forEach(block => {
const name = block.querySelector('.winding-name').value.trim();
const taps = [0], awg = [];
block.querySelectorAll('.seg-row').forEach(row => {
taps.push(parseInt(row.querySelector('.seg-turns').value));
awg.push(parseInt(row.querySelector('.seg-awg').value));
});
windings.push({ name, taps, awg });
});
return { windings };
}
if (ptype === 'sim') {
return {
frequencies: document.getElementById('sim-freqs').value,
loads: document.getElementById('sim-loads').value,
target_power_W: parseFloat(document.getElementById('sim-target').value),
power_tol_pct: parseFloat(document.getElementById('sim-tol').value),
Vp_min: parseFloat(document.getElementById('sim-vpmin').value),
Vp_max: parseFloat(document.getElementById('sim-vpmax').value),
Vp_steps: parseInt(document.getElementById('sim-vpsteps').value),
T_ambient_C: parseFloat(document.getElementById('sim-tambient').value),
h_conv: parseFloat(document.getElementById('sim-hconv').value),
};
}
if (ptype === 'constraints') {
return {
B_max_T: parseFloat(document.getElementById('con-B').value),
Vp_max: parseFloat(document.getElementById('con-Vp').value),
Vs_max: parseFloat(document.getElementById('con-Vs').value),
Ip_max: parseFloat(document.getElementById('con-Ip').value),
Is_max: parseFloat(document.getElementById('con-Is').value),
P_out_max_W: parseFloat(document.getElementById('con-Pout').value),
};
}
}
// Apply loaded preset data back to the UI
function presetApply(ptype, data) {
if (ptype === 'core') {
document.getElementById('core-ID').value = data.ID_mm ?? '';
document.getElementById('core-OD').value = data.OD_mm ?? '';
document.getElementById('core-H').value = data.height_mm ?? '';
document.getElementById('core-fill').value = data.fill_factor ?? 0.35;
document.getElementById('core-Ae').value = data.Ae_mm2 ?? '';
document.getElementById('core-Ve').value = data.Ve_mm3 ?? '';
}
if (ptype === 'windings') {
const container = document.getElementById('windings-container');
container.innerHTML = '';
windingCount = 0;
(data.windings || []).forEach(w => {
addWinding(w.name, w.taps.slice(1), w.awg);
});
}
if (ptype === 'sim') {
if (data.frequencies !== undefined) document.getElementById('sim-freqs').value = data.frequencies;
if (data.loads !== undefined) document.getElementById('sim-loads').value = data.loads;
if (data.target_power_W !== undefined) document.getElementById('sim-target').value = data.target_power_W;
if (data.power_tol_pct !== undefined) document.getElementById('sim-tol').value = data.power_tol_pct;
if (data.Vp_min !== undefined) document.getElementById('sim-vpmin').value = data.Vp_min;
if (data.Vp_max !== undefined) document.getElementById('sim-vpmax').value = data.Vp_max;
if (data.Vp_steps !== undefined) document.getElementById('sim-vpsteps').value = data.Vp_steps;
if (data.T_ambient_C !== undefined) document.getElementById('sim-tambient').value = data.T_ambient_C;
if (data.h_conv !== undefined) document.getElementById('sim-hconv').value = data.h_conv;
}
if (ptype === 'constraints') {
if (data.B_max_T !== undefined) document.getElementById('con-B').value = data.B_max_T;
if (data.Vp_max !== undefined) document.getElementById('con-Vp').value = data.Vp_max;
if (data.Vs_max !== undefined) document.getElementById('con-Vs').value = data.Vs_max;
if (data.Ip_max !== undefined) document.getElementById('con-Ip').value = data.Ip_max;
if (data.Is_max !== undefined) document.getElementById('con-Is').value = data.Is_max;
if (data.P_out_max_W !== undefined) document.getElementById('con-Pout').value = data.P_out_max_W;
}
}
// Populate a preset dropdown from server
async function presetRefresh(ptype) {
const sel = document.getElementById(`preset-sel-${ptype}`);
if (!sel) return null;
const resp = await fetch(`/api/presets/${ptype}`);
const data = await resp.json();
if (!data.success) return null;
sel.innerHTML = data.names.length === 0
? '<option value="">(no saved presets)</option>'
: data.names.map(n => `<option value="${n}"${n === data.last ? ' selected' : ''}>${n}</option>`).join('');
// Sync name input to selected
const nameEl = document.getElementById(`preset-name-${ptype}`);
if (nameEl && data.last) nameEl.value = data.last;
return data;
}
async function presetLoad(ptype) {
const sel = document.getElementById(`preset-sel-${ptype}`);
const name = sel ? sel.value : '';
if (!name) return;
const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(name)}`);
const data = await resp.json();
if (!data.success) { alert('Load failed: ' + data.error); return; }
presetApply(ptype, data.data);
// Sync name input
const nameEl = document.getElementById(`preset-name-${ptype}`);
if (nameEl) nameEl.value = name;
}
async function presetSave(ptype) {
const nameEl = document.getElementById(`preset-name-${ptype}`);
const name = nameEl ? nameEl.value.trim() : '';
if (!name) { alert('Enter a preset name first.'); return; }
const payload = presetExtract(ptype);
const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(name)}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
});
const data = await resp.json();
if (!data.success) { alert('Save failed: ' + data.error); return; }
await presetRefresh(ptype);
// Select the newly saved name
const sel = document.getElementById(`preset-sel-${ptype}`);
if (sel) sel.value = name;
}
async function presetDelete(ptype) {
const sel = document.getElementById(`preset-sel-${ptype}`);
const name = sel ? sel.value : '';
if (!name || name === '(no saved presets)') return;
if (!confirm(`Delete preset "${name}"?`)) return;
const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(name)}`, { method: 'DELETE' });
const data = await resp.json();
if (!data.success) { alert('Delete failed: ' + data.error); return; }
await presetRefresh(ptype);
}
// On page load: refresh all dropdowns and auto-load last-used preset for each type
async function initPresets() {
for (const ptype of ['core', 'windings', 'sim', 'constraints']) {
const info = await presetRefresh(ptype);
if (info && info.last) {
// Auto-load last used
const resp = await fetch(`/api/presets/${ptype}/${encodeURIComponent(info.last)}`);
const d = await resp.json();
if (d.success) presetApply(ptype, d.data);
}
}
}
// ============================================================
// Initialise: load presets first; fall back to hardcoded defaults
// ============================================================
(async () => {
// initPresets returns after applying last-used presets (if any)
const windingsInfo = await (async () => {
const r = await fetch('/api/presets/windings');
const d = await r.json();
return d.success ? d : null;
})();
// Only add default windings if no saved windings preset exists
if (!windingsInfo || !windingsInfo.last) {
addWinding('primary', [25, 50], [22, 22]);
addWinding('secondary', [100, 50, 50, 50], [22, 22, 22, 26]);
}
await initPresets();
})();
</script> </script>
</body> </body>