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

View File

@@ -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 ≈ 510 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>
@@ -512,7 +593,8 @@
// State
// ============================================================
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
@@ -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
? `&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 => {
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>