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 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__)
|
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)
|
def _parse_core(data: dict) -> ToroidCore:
|
||||||
# These would be calculated based on wire gauge, length per turn, etc.
|
"""Build ToroidCore from request dict."""
|
||||||
primary_Rp_per_turn = [
|
Ae = data.get("Ae_mm2")
|
||||||
0.01,
|
Ve = data.get("Ve_mm3")
|
||||||
0.01,
|
return ToroidCore(
|
||||||
]
|
ID_mm=float(data["ID_mm"]),
|
||||||
|
OD_mm=float(data["OD_mm"]),
|
||||||
secondary_Rs_per_turn = [
|
height_mm=float(data["height_mm"]),
|
||||||
0.004,
|
Ae_mm2=float(Ae) if Ae not in (None, "") else None,
|
||||||
0.024,
|
Ve_mm3=float(Ve) if Ve not in (None, "") else None,
|
||||||
0.024,
|
Pv_func=None, # not editable in the UI; uses built-in fallback
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
return tf
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
def _parse_windings(data: dict) -> list[WindingSpec]:
|
||||||
def index():
|
"""
|
||||||
return render_template('index.html')
|
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 _drawing_b64(core: ToroidCore, results) -> str:
|
||||||
def manual():
|
"""Render the toroid PNG and return it as a base64 data-URI string."""
|
||||||
return render_template('manual.html')
|
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
|
||||||
|
tmp_path = f.name
|
||||||
|
|
||||||
@app.route('/api/simulate', methods=['POST'])
|
|
||||||
def simulate():
|
|
||||||
"""Run manual simulation with specified tap and voltage"""
|
|
||||||
try:
|
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
|
def _winding_result_to_dict(wr) -> dict:
|
||||||
tf = get_default_transformer()
|
"""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(
|
def _sweep_entry_to_dict(e: SweepEntry) -> dict:
|
||||||
primary_tap=primary_tap,
|
"""Convert SweepEntry to a plain dict suitable for JSON."""
|
||||||
secondary_tap=secondary_tap,
|
d = e.as_dict()
|
||||||
Vp_rms=Vp_rms,
|
# Replace NaN (not JSON-serialisable) with None
|
||||||
freq_hz=freq_hz,
|
for k, v in d.items():
|
||||||
load_ohms=load_ohms,
|
if isinstance(v, float) and math.isnan(v):
|
||||||
core_loss_W=0.0, # Will be calculated by model
|
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({
|
return jsonify({
|
||||||
'success': True,
|
"success": True,
|
||||||
'result': {
|
"windings": windings_info,
|
||||||
'primary_tap': result['primary_tap'],
|
"drawing": drawing_b64,
|
||||||
'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),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as exc:
|
||||||
return jsonify({
|
return jsonify({"success": False, "error": str(exc)}), 400
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
}), 400
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/optimize', methods=['POST'])
|
@app.route("/api/sweep", methods=["POST"])
|
||||||
def optimize():
|
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:
|
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
|
results = design_transformer(core, specs, fill_factor=fill_factor)
|
||||||
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))
|
|
||||||
|
|
||||||
# Create transformer and optimizer
|
# Expect exactly 2 windings: primary and secondary
|
||||||
tf = get_default_transformer()
|
if len(results) < 2:
|
||||||
opt = TransformerOptimizer(tf)
|
return jsonify({"success": False, "error": "Need at least 2 windings"}), 400
|
||||||
|
|
||||||
# Run optimization (core loss is calculated automatically)
|
# Use first winding as primary, second as secondary
|
||||||
result = opt.optimize(
|
primary_result = results[0]
|
||||||
load_ohms=load_ohms,
|
secondary_result = results[1]
|
||||||
target_power_W=target_power_W,
|
|
||||||
freq_hz=freq_hz,
|
sim = ToroidSimulator(core=core, primary=primary_result, secondary=secondary_result)
|
||||||
Vp_min=Vp_min,
|
|
||||||
Vp_max=Vp_max,
|
# Constraints
|
||||||
Vp_step=Vp_step,
|
cdata = data.get("constraints", {})
|
||||||
B_max_T=B_max_T,
|
constraints = SimConstraints(
|
||||||
Vs_max=Vs_max,
|
B_max_T=float(cdata.get("B_max_T", 0.3)),
|
||||||
Is_max=Is_max,
|
Vp_max=float(cdata.get("Vp_max", float("inf"))),
|
||||||
core_loss_W=0.0, # Will be calculated by model
|
Vs_max=float(cdata.get("Vs_max", float("inf"))),
|
||||||
power_tolerance_percent=power_tolerance_percent,
|
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:
|
frequencies = [float(f) for f in data.get("frequencies", [256.0])]
|
||||||
return jsonify({
|
loads = [(float(p[0]), float(p[1])) for p in data.get("loads", [[10.0, 0.0]])]
|
||||||
'success': True,
|
target_power_W = float(data.get("target_power_W", 10.0))
|
||||||
'result': {
|
Vp_min = float(data.get("Vp_min", 1.0))
|
||||||
'primary_tap': result.primary_tap,
|
Vp_max_sweep = float(data.get("Vp_max", 50.0))
|
||||||
'secondary_tap': result.secondary_tap,
|
Vp_steps = int(data.get("Vp_steps", 100))
|
||||||
'Np_eff': result.Np_eff,
|
power_tol_pct = float(data.get("power_tol_pct", 2.0))
|
||||||
'Ns_eff': result.Ns_eff,
|
|
||||||
'Vp_rms': round(result.Vp_rms, 2),
|
entries = sweep_operating_points(
|
||||||
'Vs_rms': round(result.Vs_rms, 2),
|
sim=sim,
|
||||||
'Ip_rms': round(result.Ip_rms, 3),
|
frequencies=frequencies,
|
||||||
'Is_rms': round(result.Is_rms, 3),
|
loads=loads,
|
||||||
'turns_ratio': round(result.turns_ratio, 3),
|
target_power_W=target_power_W,
|
||||||
'P_out_W': round(result.P_out_W, 2),
|
constraints=constraints,
|
||||||
'P_in_W': round(result.P_in_W, 2),
|
Vp_min=Vp_min,
|
||||||
'P_cu_W': round(result.P_cu_W, 2),
|
Vp_max=Vp_max_sweep,
|
||||||
'P_cu_primary_W': round(result.P_cu_primary_W, 3),
|
Vp_steps=Vp_steps,
|
||||||
'P_cu_secondary_W': round(result.P_cu_secondary_W, 3),
|
power_tol_pct=power_tol_pct,
|
||||||
'P_core_W': round(result.P_core_W, 2),
|
)
|
||||||
'efficiency': round(result.efficiency * 100, 2),
|
|
||||||
'B_peak_T': round(result.B_peak_T, 4),
|
rows = [_sweep_entry_to_dict(e) for e in entries]
|
||||||
'power_error_percent': round(result.power_error_percent, 2),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
return jsonify({
|
|
||||||
'success': False,
|
|
||||||
'error': 'No valid configuration found within constraints'
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
"success": True,
|
||||||
'error': str(e)
|
"rows": rows,
|
||||||
}), 400
|
"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'])
|
if __name__ == "__main__":
|
||||||
def transformer_info():
|
app.run(debug=True, host="0.0.0.0", port=5000)
|
||||||
"""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)
|
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ class ToroidCore:
|
|||||||
@property
|
@property
|
||||||
def cross_section_area_mm2(self) -> float:
|
def cross_section_area_mm2(self) -> float:
|
||||||
"""Rectangular cross-section area of the core material."""
|
"""Rectangular cross-section area of the core material."""
|
||||||
|
if self.Ae_mm2 != None:
|
||||||
|
return self.Ae_mm2
|
||||||
|
|
||||||
return ((self.OD_mm - self.ID_mm) / 2.0) * self.height_mm
|
return ((self.OD_mm - self.ID_mm) / 2.0) * self.height_mm
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
159
sim_toroid.py
159
sim_toroid.py
@@ -329,43 +329,34 @@ class ToroidSimulator:
|
|||||||
|
|
||||||
turns_ratio = Ns / Np # a = Ns/Np
|
turns_ratio = Ns / Np # a = Ns/Np
|
||||||
|
|
||||||
# --- Flux density ---------------------------------------------------
|
# --- Circuit solution (exact voltage divider) -----------------------
|
||||||
# The primary voltage applied to the winding minus the resistive drop
|
|
||||||
# sets the flux. We iterate once: compute ideal Ip, correct Vp seen by
|
|
||||||
# core, then recompute.
|
|
||||||
#
|
|
||||||
# Iteration:
|
|
||||||
# Pass 0 (ideal): B from full Vp
|
|
||||||
# Pass 1: B from (Vp - Ip_0 * Rp) where Ip_0 is ideal primary current
|
|
||||||
#
|
|
||||||
Ae_m2 = self._effective_Ae_mm2() * 1e-6
|
Ae_m2 = self._effective_Ae_mm2() * 1e-6
|
||||||
|
|
||||||
def _compute_op(Vp_core: float):
|
# Exact voltage divider: solve for Vp_core analytically.
|
||||||
"""Compute operating point given effective core voltage."""
|
#
|
||||||
B = Vp_core / (4.44 * Np * Ae_m2 * freq_hz)
|
# Circuit: Vp --[Rp]-- Vp_core --[ideal xfmr]-- [Rs + Z_load]
|
||||||
# Ideal open-circuit secondary voltage
|
#
|
||||||
Vs_oc = complex(Vp_core * turns_ratio, 0.0)
|
# Secondary impedance reflected to primary:
|
||||||
# Secondary loop: Vs_oc = Is * (Rs + Z_load)
|
# Z_sec_ref = (Rs + Z_load) / turns_ratio²
|
||||||
Z_sec_total = complex(Rs, 0.0) + Z_load_complex
|
# Voltage divider:
|
||||||
Is_phasor = Vs_oc / Z_sec_total
|
# Vp_core = Vp * Z_sec_ref / (Rp + Z_sec_ref)
|
||||||
# Voltage across load
|
# Then:
|
||||||
Vs_phasor = Is_phasor * Z_load_complex
|
# Is = Vp_core * turns_ratio / (Rs + Z_load)
|
||||||
# Reflect secondary current to primary (ideal transformer)
|
# Ip = Is * turns_ratio (= Vp_core / Z_sec_ref / turns_ratio ... simplified)
|
||||||
Ip_reflected = Is_phasor / turns_ratio # phasor, Amperes
|
|
||||||
return B, Is_phasor, Vs_phasor, Ip_reflected
|
|
||||||
|
|
||||||
# Pass 0: ideal (ignore primary drop for now)
|
Z_sec_total = complex(Rs, 0.0) + Z_load_complex
|
||||||
_, Is0, _, Ip0 = _compute_op(Vp_rms)
|
Z_sec_ref = Z_sec_total / (turns_ratio ** 2) # reflected to primary
|
||||||
Ip0_mag = abs(Ip0)
|
Z_total_primary = complex(Rp, 0.0) + Z_sec_ref
|
||||||
|
|
||||||
# Pass 1: correct for primary winding voltage drop
|
Vp_core_phasor = complex(Vp_rms, 0.0) * Z_sec_ref / Z_total_primary
|
||||||
# Primary drop is Ip * Rp (in phase with current; Rp is real)
|
Vp_core = abs(Vp_core_phasor)
|
||||||
# Vp_core ≈ Vp_rms - Ip0 * Rp (phasor subtraction)
|
|
||||||
# For simplicity treat Ip0 as real-valued magnitude for the correction
|
|
||||||
# (conservative: subtracts in phase with voltage)
|
|
||||||
Vp_core = max(Vp_rms - Ip0_mag * Rp, 0.0)
|
|
||||||
|
|
||||||
B_peak_T, Is_phasor, Vs_phasor, Ip_phasor = _compute_op(Vp_core)
|
B_peak_T = Vp_core / (4.44 * Np * Ae_m2 * freq_hz)
|
||||||
|
|
||||||
|
Vs_oc = Vp_core_phasor * turns_ratio
|
||||||
|
Is_phasor = Vs_oc / Z_sec_total
|
||||||
|
Vs_phasor = Is_phasor * Z_load_complex
|
||||||
|
Ip_phasor = Is_phasor * turns_ratio
|
||||||
|
|
||||||
Is_rms = abs(Is_phasor)
|
Is_rms = abs(Is_phasor)
|
||||||
Vs_rms_out = abs(Vs_phasor)
|
Vs_rms_out = abs(Vs_phasor)
|
||||||
@@ -876,69 +867,69 @@ if __name__ == "__main__":
|
|||||||
constraints = SimConstraints(
|
constraints = SimConstraints(
|
||||||
B_max_T=1.0,
|
B_max_T=1.0,
|
||||||
Vp_max=50.0,
|
Vp_max=50.0,
|
||||||
Vs_max=90.0,
|
Vs_max=120.0,
|
||||||
Ip_max=5.0,
|
Ip_max=3.0,
|
||||||
Is_max=2.0,
|
Is_max=2.0,
|
||||||
P_out_max_W=25.0,
|
P_out_max_W=25.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
print("=== Single operating point (10 ohm resistive load) ===")
|
# print("=== Single operating point (10 ohm resistive load) ===")
|
||||||
result = sim.simulate(
|
# result = sim.simulate(
|
||||||
Vp_rms=12.0,
|
# Vp_rms=12.0,
|
||||||
freq_hz=256.0,
|
# freq_hz=256.0,
|
||||||
primary_tap=2,
|
# primary_tap=2,
|
||||||
secondary_tap=1,
|
# secondary_tap=1,
|
||||||
Z_load=(100.0, 0.0),
|
# Z_load=(100.0, 0.0),
|
||||||
constraints=constraints,
|
# constraints=constraints,
|
||||||
)
|
# )
|
||||||
print(result)
|
# print(result)
|
||||||
print()
|
# print()
|
||||||
|
|
||||||
print("=== Complex load (8 ohm + 1 mH at 50 kHz -> X ~= 314 ohm) ===")
|
# print("=== Complex load (8 ohm + 1 mH at 50 kHz -> X ~= 314 ohm) ===")
|
||||||
X = 2 * math.pi * 50_000.0 * 1e-3
|
# X = 2 * math.pi * 50_000.0 * 1e-3
|
||||||
result2 = sim.simulate(
|
# result2 = sim.simulate(
|
||||||
Vp_rms=24.0,
|
# Vp_rms=24.0,
|
||||||
freq_hz=50_000.0,
|
# freq_hz=50_000.0,
|
||||||
primary_tap=2,
|
# primary_tap=2,
|
||||||
secondary_tap=1,
|
# secondary_tap=1,
|
||||||
Z_load=(8.0, X),
|
# Z_load=(8.0, X),
|
||||||
constraints=constraints,
|
# constraints=constraints,
|
||||||
)
|
# )
|
||||||
print(result2)
|
# print(result2)
|
||||||
print()
|
# print()
|
||||||
|
|
||||||
print("=== Tap sweep ===")
|
# print("=== Tap sweep ===")
|
||||||
all_results = sweep_taps(sim, 24.0, 50_000.0, (10.0, 0.0), constraints)
|
# all_results = sweep_taps(sim, 24.0, 50_000.0, (10.0, 0.0), constraints)
|
||||||
for r in all_results:
|
# for r in all_results:
|
||||||
feasible = "OK" if r.feasible else "VIOL"
|
# feasible = "OK" if r.feasible else "VIOL"
|
||||||
print(
|
# print(
|
||||||
f" P{r.primary_tap}/S{r.secondary_tap} "
|
# f" P{r.primary_tap}/S{r.secondary_tap} "
|
||||||
f"Np={r.Np_eff:3d} Ns={r.Ns_eff:3d} "
|
# f"Np={r.Np_eff:3d} Ns={r.Ns_eff:3d} "
|
||||||
f"Vs={r.Vs_rms:.3f}V Is={r.Is_rms:.4f}A "
|
# f"Vs={r.Vs_rms:.3f}V Is={r.Is_rms:.4f}A "
|
||||||
f"P_out={r.P_out_W:.3f}W eff={r.efficiency*100:.2f}% [{feasible}]"
|
# f"P_out={r.P_out_W:.3f}W eff={r.efficiency*100:.2f}% [{feasible}]"
|
||||||
)
|
# )
|
||||||
|
|
||||||
print()
|
# print()
|
||||||
print("=== Optimize: find best taps + Vp to deliver ~10 W at 256 Hz ===")
|
# print("=== Optimize: find best taps + Vp to deliver ~10 W at 256 Hz ===")
|
||||||
opt = sim.optimize(
|
# opt = sim.optimize(
|
||||||
freq_hz=256.0,
|
# freq_hz=256.0,
|
||||||
Z_load=(10.0, 0.0),
|
# Z_load=(10.0, 0.0),
|
||||||
target_power_W=25.0,
|
# target_power_W=25.0,
|
||||||
constraints=constraints,
|
# constraints=constraints,
|
||||||
Vp_min=1.0,
|
# Vp_min=1.0,
|
||||||
Vp_max=50.0,
|
# Vp_max=50.0,
|
||||||
Vp_steps=200,
|
# Vp_steps=200,
|
||||||
power_tol_pct=2.0,
|
# power_tol_pct=2.0,
|
||||||
)
|
# )
|
||||||
if opt is None:
|
# if opt is None:
|
||||||
print(" No feasible solution found.")
|
# print(" No feasible solution found.")
|
||||||
else:
|
# else:
|
||||||
print(opt)
|
# print(opt)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("=== Operating-point sweep (3 freqs x 3 loads) ===")
|
print("=== Operating-point sweep (3 freqs x 3 loads) ===")
|
||||||
freqs = [256.0, 870.0, 3140.0, 8900.0]
|
freqs = [256.0, 870.0, 3140.0, 8900.0]
|
||||||
loads = [(50.0, 0.0), (100.0, 0.0), (200.0, 0.0), (600.0, 0.0)]
|
loads = [(10.0, 0.0), (50.0, 0.0), (100.0, 0.0), (200.0, 0.0), (600.0, 0.0), (2000.0, 0.0)]
|
||||||
entries = sweep_operating_points(
|
entries = sweep_operating_points(
|
||||||
sim=sim,
|
sim=sim,
|
||||||
frequencies=freqs,
|
frequencies=freqs,
|
||||||
|
|||||||
1034
templates/index.html
Normal file
1034
templates/index.html
Normal file
File diff suppressed because it is too large
Load Diff
562
templates/manual.html
Normal file
562
templates/manual.html
Normal file
@@ -0,0 +1,562 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Manual Simulation - Transformer</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: #667eea;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
border-radius: 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group h3 {
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container, .select-container {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container:last-child, .select-container:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-display {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #d3d3d3;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #667eea;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb:hover {
|
||||||
|
background: #764ba2;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 15px 40px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 25px;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results h2 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
background: white;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card h4 {
|
||||||
|
color: #555;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-unit {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #fee;
|
||||||
|
border: 2px solid #f66;
|
||||||
|
color: #c33;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.efficiency-high {
|
||||||
|
border-left-color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.efficiency-medium {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.efficiency-low {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tap-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🔧 Manual Simulation</h1>
|
||||||
|
<a href="/" class="nav-link">⚡ Go to Optimizer</a>
|
||||||
|
</div>
|
||||||
|
<p class="subtitle">Directly specify tap settings and input voltage to simulate transformer performance</p>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<div class="control-group">
|
||||||
|
<h3>Tap Selection</h3>
|
||||||
|
<div class="select-container">
|
||||||
|
<label>
|
||||||
|
<span>Primary Tap</span>
|
||||||
|
</label>
|
||||||
|
<select id="primary-tap">
|
||||||
|
<option value="1">Tap 1 (50 turns)</option>
|
||||||
|
<option value="2">Tap 2 (100 turns)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="select-container">
|
||||||
|
<label>
|
||||||
|
<span>Secondary Tap</span>
|
||||||
|
</label>
|
||||||
|
<select id="secondary-tap">
|
||||||
|
<option value="1">Tap 1 (100 turns)</option>
|
||||||
|
<option value="2">Tap 2 (150 turns)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3>Input Parameters</h3>
|
||||||
|
<div class="slider-container">
|
||||||
|
<label>
|
||||||
|
<span>Input Voltage</span>
|
||||||
|
<span class="value-display"><span id="vp-value">12</span> V</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" id="vp" min="1" max="100" value="12" step="0.5">
|
||||||
|
</div>
|
||||||
|
<div class="slider-container">
|
||||||
|
<label>
|
||||||
|
<span>Frequency</span>
|
||||||
|
<span class="value-display"><span id="freq-value">2000</span> Hz</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" id="freq" min="100" max="50000" value="2000" step="100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3>Load Conditions</h3>
|
||||||
|
<div class="slider-container">
|
||||||
|
<label>
|
||||||
|
<span>Load Resistance</span>
|
||||||
|
<span class="value-display"><span id="load-value">100</span> Ω</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" id="load" min="5" max="10000" value="100" step="5">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-container">
|
||||||
|
<button id="simulate-btn" onclick="runSimulation()">▶️ Run Simulation</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading" class="loading hidden">
|
||||||
|
Running simulation...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="error" class="error hidden"></div>
|
||||||
|
|
||||||
|
<div id="results" class="results hidden">
|
||||||
|
<h2>Simulation Results</h2>
|
||||||
|
<div class="results-grid">
|
||||||
|
<div class="result-card" id="efficiency-card">
|
||||||
|
<h4>Efficiency</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="efficiency">-</span><span class="result-unit">%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Output Power</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="p-out">-</span><span class="result-unit">W</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Input Power</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="p-in">-</span><span class="result-unit">W</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Primary Turns</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="np-eff">-</span><span class="result-unit">turns</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Secondary Turns</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="ns-eff">-</span><span class="result-unit">turns</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Turns Ratio</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="turns-ratio">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Input Voltage</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="vp-rms">-</span><span class="result-unit">V</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Output Voltage</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="vs-rms">-</span><span class="result-unit">V</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Input Current</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="ip-rms">-</span><span class="result-unit">A</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Output Current</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="is-rms">-</span><span class="result-unit">A</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Flux Density</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="b-peak">-</span><span class="result-unit">T</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Copper Loss (Primary)</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="p-cu-primary">-</span><span class="result-unit">W</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Copper Loss (Secondary)</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="p-cu-secondary">-</span><span class="result-unit">W</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card">
|
||||||
|
<h4>Core Loss</h4>
|
||||||
|
<div class="result-value">
|
||||||
|
<span id="p-core">-</span><span class="result-unit">W</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Update slider value displays
|
||||||
|
const sliders = {
|
||||||
|
'vp': { element: 'vp-value', format: (v) => parseFloat(v).toFixed(1) },
|
||||||
|
'freq': { element: 'freq-value', format: (v) => Math.round(v) },
|
||||||
|
'load': { element: 'load-value', format: (v) => Math.round(v) },
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [id, config] of Object.entries(sliders)) {
|
||||||
|
const slider = document.getElementById(id);
|
||||||
|
const display = document.getElementById(config.element);
|
||||||
|
slider.addEventListener('input', (e) => {
|
||||||
|
display.textContent = config.format(e.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSimulation() {
|
||||||
|
const btn = document.getElementById('simulate-btn');
|
||||||
|
const loading = document.getElementById('loading');
|
||||||
|
const results = document.getElementById('results');
|
||||||
|
const error = document.getElementById('error');
|
||||||
|
|
||||||
|
// Hide previous results/errors
|
||||||
|
results.classList.add('hidden');
|
||||||
|
error.classList.add('hidden');
|
||||||
|
|
||||||
|
// Show loading
|
||||||
|
btn.disabled = true;
|
||||||
|
loading.classList.remove('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/simulate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
primary_tap: parseInt(document.getElementById('primary-tap').value),
|
||||||
|
secondary_tap: parseInt(document.getElementById('secondary-tap').value),
|
||||||
|
Vp_rms: parseFloat(document.getElementById('vp').value),
|
||||||
|
freq_hz: parseFloat(document.getElementById('freq').value),
|
||||||
|
load_ohms: parseFloat(document.getElementById('load').value),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
displayResults(data.result);
|
||||||
|
} else {
|
||||||
|
showError(data.error);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showError('Network error: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
loading.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayResults(result) {
|
||||||
|
document.getElementById('efficiency').textContent = result.efficiency;
|
||||||
|
document.getElementById('p-out').textContent = result.P_out_W;
|
||||||
|
document.getElementById('p-in').textContent = result.P_in_W;
|
||||||
|
document.getElementById('np-eff').textContent = result.Np_eff;
|
||||||
|
document.getElementById('ns-eff').textContent = result.Ns_eff;
|
||||||
|
document.getElementById('turns-ratio').textContent = result.turns_ratio;
|
||||||
|
document.getElementById('vp-rms').textContent = result.Vp_rms;
|
||||||
|
document.getElementById('vs-rms').textContent = result.Vs_rms;
|
||||||
|
document.getElementById('ip-rms').textContent = result.Ip_rms;
|
||||||
|
document.getElementById('is-rms').textContent = result.Is_rms;
|
||||||
|
document.getElementById('b-peak').textContent = result.B_peak_T;
|
||||||
|
document.getElementById('p-cu-primary').textContent = result.P_cu_primary_W;
|
||||||
|
document.getElementById('p-cu-secondary').textContent = result.P_cu_secondary_W;
|
||||||
|
document.getElementById('p-core').textContent = result.P_core_W;
|
||||||
|
|
||||||
|
// Color code efficiency
|
||||||
|
const effCard = document.getElementById('efficiency-card');
|
||||||
|
effCard.classList.remove('efficiency-high', 'efficiency-medium', 'efficiency-low');
|
||||||
|
if (result.efficiency >= 90) {
|
||||||
|
effCard.classList.add('efficiency-high');
|
||||||
|
} else if (result.efficiency >= 80) {
|
||||||
|
effCard.classList.add('efficiency-medium');
|
||||||
|
} else {
|
||||||
|
effCard.classList.add('efficiency-low');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('results').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
const errorDiv = document.getElementById('error');
|
||||||
|
errorDiv.textContent = message;
|
||||||
|
errorDiv.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load transformer info on page load
|
||||||
|
async function loadTransformerInfo() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/transformer_info');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Update tap options based on actual transformer
|
||||||
|
const primarySelect = document.getElementById('primary-tap');
|
||||||
|
const secondarySelect = document.getElementById('secondary-tap');
|
||||||
|
|
||||||
|
primarySelect.innerHTML = '';
|
||||||
|
secondarySelect.innerHTML = '';
|
||||||
|
|
||||||
|
// Primary taps (skip first element which is 0)
|
||||||
|
for (let i = 1; i < data.primary_taps.length; i++) {
|
||||||
|
const turns = data.primary_taps.slice(1, i + 1).reduce((a, b) => a + b, 0);
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = i;
|
||||||
|
option.textContent = `Tap ${i} (${turns} turns)`;
|
||||||
|
primarySelect.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secondary taps
|
||||||
|
for (let i = 1; i < data.secondary_taps.length; i++) {
|
||||||
|
const turns = data.secondary_taps.slice(1, i + 1).reduce((a, b) => a + b, 0);
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = i;
|
||||||
|
option.textContent = `Tap ${i} (${turns} turns)`;
|
||||||
|
secondarySelect.appendChild(option);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load transformer info:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load on page load
|
||||||
|
loadTransformerInfo();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user