added preset saving and heat transfer
This commit is contained in:
130
app.py
130
app.py
@@ -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)
|
||||
|
||||
@@ -78,6 +78,7 @@ def draw_toroid(
|
||||
output_path: str = "toroid.png",
|
||||
dpi: int = 180,
|
||||
fig_size_mm: float = 180.0,
|
||||
actual_fill: float | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
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_ylabel("mm", fontsize=8)
|
||||
ax.tick_params(labelsize=7)
|
||||
fill_str = f" fill={actual_fill*100:.1f}%" if actual_fill is not None else ""
|
||||
ax.set_title(
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
11
presets/constraints.json
Normal file
11
presets/constraints.json
Normal 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
11
presets/core.json
Normal 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
12
presets/sim.json
Normal 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
35
presets/windings.json
Normal 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"
|
||||
}
|
||||
@@ -285,6 +285,46 @@
|
||||
}
|
||||
#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-table-wrap {
|
||||
overflow-x: auto;
|
||||
@@ -334,7 +374,7 @@
|
||||
/* ---- Responsive: collapse to single column on narrow screens ---- */
|
||||
@media (max-width: 860px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.plots-grid { grid-template-columns: 1fr; }
|
||||
.plots-grid { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -353,6 +393,13 @@
|
||||
<!-- Core Parameters -->
|
||||
<div class="card">
|
||||
<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="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>
|
||||
@@ -372,6 +419,13 @@
|
||||
<!-- Windings -->
|
||||
<div class="card">
|
||||
<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 id="windings-container"></div>
|
||||
<div class="btn-row">
|
||||
@@ -397,6 +451,13 @@
|
||||
<!-- Simulation Parameters -->
|
||||
<div class="card">
|
||||
<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="form-row one">
|
||||
<label class="field">
|
||||
@@ -432,7 +493,14 @@
|
||||
|
||||
<details style="margin: 8px 0">
|
||||
<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">
|
||||
<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>
|
||||
@@ -448,6 +516,18 @@
|
||||
</div>
|
||||
</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 ≈ 5–10 W/m²K">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-primary" id="btn-sim" onclick="runSweep()">Run Simulation</button>
|
||||
</div>
|
||||
@@ -480,10 +560,11 @@
|
||||
<select id="freq-select" onchange="updatePlots()"></select>
|
||||
<span id="plot-status" style="font-size:12px;color:#64748b;margin-left:8px"></span>
|
||||
</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-current"></div>
|
||||
<div class="plot-box" id="plot-power"></div>
|
||||
<div class="plot-box" id="plot-temp"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -513,6 +594,7 @@
|
||||
// ============================================================
|
||||
let windingCount = 0;
|
||||
let sweepData = null; // last successful sweep result
|
||||
let designSurfaceArea = null; // A_surface_m2 from last design run
|
||||
|
||||
// ============================================================
|
||||
// Winding builder
|
||||
@@ -656,8 +738,9 @@ async function runDesign() {
|
||||
return;
|
||||
}
|
||||
setMsg(msg, 'ok', 'Design complete.');
|
||||
designSurfaceArea = data.A_surface_m2 || null;
|
||||
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';
|
||||
} catch(e) {
|
||||
setMsg(msg, 'error', 'Network error: ' + e.message);
|
||||
@@ -676,9 +759,29 @@ function showDrawing(b64) {
|
||||
img.style.display = 'block';
|
||||
}
|
||||
|
||||
function showDesignResults(windings) {
|
||||
function showDesignResults(windings, fillTarget, fillActual, windowArea, surfaceArea) {
|
||||
const cont = document.getElementById('design-results-tables');
|
||||
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
|
||||
? ` | 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>
|
||||
|
|
||||
Target fill: <strong>${tgtPct}%</strong>
|
||||
|
|
||||
Actual fill: <strong style="color:${color}">${pct}%</strong>
|
||||
${surfStr}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
windings.forEach(w => {
|
||||
const feasStr = w.feasible
|
||||
? '<span class="badge-ok">OK</span>'
|
||||
@@ -896,12 +999,39 @@ function updatePlots() {
|
||||
document.getElementById('plot-status').textContent =
|
||||
`${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)
|
||||
// ============================================================
|
||||
// All fields — used for CSV export only
|
||||
const _CSV_COLS = [
|
||||
{ key: 'freq_hz', label: 'freq (Hz)' },
|
||||
{ key: 'Z_load_R', label: 'R (Ω)' },
|
||||
@@ -931,7 +1061,37 @@ const _CSV_COLS = [
|
||||
{ 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 (key === 'met_target') {
|
||||
return val
|
||||
@@ -942,22 +1102,27 @@ function fmtCell(key, val) {
|
||||
return (val * 100).toFixed(2) + '%';
|
||||
}
|
||||
if (typeof val === 'number') {
|
||||
// integers: no decimals; floats: 2 decimal places
|
||||
return Number.isInteger(val) ? val : val.toFixed(2);
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
function updateTable(rows) {
|
||||
function updateTable(rows, T_rise, T_copper) {
|
||||
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 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 =>
|
||||
'<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('');
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user