added toroid simulator
This commit is contained in:
79
app.py
79
app.py
@@ -203,6 +203,85 @@ def api_design():
|
|||||||
return jsonify({"success": False, "error": str(exc)}), 400
|
return jsonify({"success": False, "error": str(exc)}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/simulate", methods=["POST"])
|
||||||
|
def api_simulate():
|
||||||
|
"""
|
||||||
|
Compute a single operating point.
|
||||||
|
|
||||||
|
Request body (JSON):
|
||||||
|
{
|
||||||
|
// Same core + windings as /api/design ...
|
||||||
|
"fill_factor": 0.35,
|
||||||
|
"primary_tap": 1,
|
||||||
|
"secondary_tap": 1,
|
||||||
|
"Vp_rms": 12.0,
|
||||||
|
"freq_hz": 1000.0,
|
||||||
|
"R_load": 100.0,
|
||||||
|
"X_load": 0.0,
|
||||||
|
"constraints": { ... } // optional
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
if len(results) < 2:
|
||||||
|
return jsonify({"success": False, "error": "Need at least 2 windings"}), 400
|
||||||
|
|
||||||
|
sim = ToroidSimulator(core=core, primary=results[0], secondary=results[1])
|
||||||
|
|
||||||
|
cdata = data.get("constraints", {})
|
||||||
|
constraints = SimConstraints(
|
||||||
|
B_max_T=float(cdata.get("B_max_T", 0.3)),
|
||||||
|
Vp_max=float(cdata.get("Vp_max", float("inf"))),
|
||||||
|
Vs_max=float(cdata.get("Vs_max", float("inf"))),
|
||||||
|
Ip_max=float(cdata.get("Ip_max", float("inf"))),
|
||||||
|
Is_max=float(cdata.get("Is_max", float("inf"))),
|
||||||
|
P_out_max_W=float(cdata.get("P_out_max_W", float("inf"))),
|
||||||
|
) if cdata else None
|
||||||
|
|
||||||
|
r = sim.simulate(
|
||||||
|
Vp_rms=float(data["Vp_rms"]),
|
||||||
|
freq_hz=float(data["freq_hz"]),
|
||||||
|
primary_tap=int(data["primary_tap"]),
|
||||||
|
secondary_tap=int(data["secondary_tap"]),
|
||||||
|
Z_load=(float(data.get("R_load", 0.0)), float(data.get("X_load", 0.0))),
|
||||||
|
constraints=constraints,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _f(v):
|
||||||
|
return None if (isinstance(v, float) and math.isnan(v)) else v
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"Np_eff": r.Np_eff,
|
||||||
|
"Ns_eff": r.Ns_eff,
|
||||||
|
"turns_ratio": _f(round(r.turns_ratio, 4)),
|
||||||
|
"B_peak_T": _f(round(r.B_peak_T, 4)),
|
||||||
|
"Vp_rms": _f(round(r.Vp_rms_applied, 4)),
|
||||||
|
"Vs_rms": _f(round(r.Vs_rms, 4)),
|
||||||
|
"Ip_rms": _f(round(r.Ip_rms, 4)),
|
||||||
|
"Is_rms": _f(round(r.Is_rms, 4)),
|
||||||
|
"P_out_W": _f(round(r.P_out_W, 4)),
|
||||||
|
"P_cu_W": _f(round(r.P_cu_W, 4)),
|
||||||
|
"P_cu_primary_W": _f(round(r.P_cu_primary_W, 4)),
|
||||||
|
"P_cu_secondary_W": _f(round(r.P_cu_secondary_W, 4)),
|
||||||
|
"P_core_W": _f(round(r.P_core_W, 4)),
|
||||||
|
"P_in_W": _f(round(r.P_in_W, 4)),
|
||||||
|
"efficiency_pct": _f(round(r.efficiency * 100, 2)),
|
||||||
|
"violations": r.violations,
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
import traceback
|
||||||
|
return jsonify({"success": False, "error": str(exc),
|
||||||
|
"traceback": traceback.format_exc()}), 400
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/sweep", methods=["POST"])
|
@app.route("/api/sweep", methods=["POST"])
|
||||||
def api_sweep():
|
def api_sweep():
|
||||||
"""
|
"""
|
||||||
|
|||||||
59
designer.py
59
designer.py
@@ -279,7 +279,7 @@ class WindingResult:
|
|||||||
# Internal winding position at end of this winding (for chaining)
|
# Internal winding position at end of this winding (for chaining)
|
||||||
_end_layer: int = 0
|
_end_layer: int = 0
|
||||||
_end_turns_in_layer: int = 0
|
_end_turns_in_layer: int = 0
|
||||||
_end_total_turns: int = 0
|
_end_consumed_area_mm2: float = 0.0
|
||||||
|
|
||||||
def summary(self) -> str:
|
def summary(self) -> str:
|
||||||
# Header: show AWG as single value if uniform, else show range
|
# Header: show AWG as single value if uniform, else show range
|
||||||
@@ -372,7 +372,8 @@ def analyse_winding(
|
|||||||
fill_factor: float = 0.35,
|
fill_factor: float = 0.35,
|
||||||
start_layer: int = 0,
|
start_layer: int = 0,
|
||||||
start_turns_in_layer: int = 0,
|
start_turns_in_layer: int = 0,
|
||||||
start_total_turns: int = 0,
|
consumed_area_mm2: float = 0.0,
|
||||||
|
budget_area_mm2: Optional[float] = None,
|
||||||
) -> WindingResult:
|
) -> WindingResult:
|
||||||
"""
|
"""
|
||||||
Analyse feasibility and compute wire parameters for a winding.
|
Analyse feasibility and compute wire parameters for a winding.
|
||||||
@@ -389,9 +390,13 @@ def analyse_winding(
|
|||||||
_end_layer to continue mid-layer across windings.
|
_end_layer to continue mid-layer across windings.
|
||||||
start_turns_in_layer : int
|
start_turns_in_layer : int
|
||||||
Turns already consumed in start_layer from a previous winding.
|
Turns already consumed in start_layer from a previous winding.
|
||||||
start_total_turns : int
|
consumed_area_mm2 : float
|
||||||
Cumulative turns already placed (used for fill-factor accounting
|
Wire cross-section area (mm²) already consumed by previous windings.
|
||||||
across windings).
|
Fill-factor budget is tracked in area so mixed gauges are handled
|
||||||
|
correctly.
|
||||||
|
budget_area_mm2 : float or None
|
||||||
|
Total allowed wire area (fill_factor * window_area). Computed from
|
||||||
|
fill_factor if not supplied.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -415,7 +420,8 @@ def analyse_winding(
|
|||||||
max_turns_geometry += cap
|
max_turns_geometry += cap
|
||||||
layer_idx += 1
|
layer_idx += 1
|
||||||
|
|
||||||
effective_max = max_turns_fill # fill factor is the binding constraint
|
if budget_area_mm2 is None:
|
||||||
|
budget_area_mm2 = fill_factor * core.window_area_mm2
|
||||||
|
|
||||||
segments: list[SegmentResult] = []
|
segments: list[SegmentResult] = []
|
||||||
overall_feasible = True
|
overall_feasible = True
|
||||||
@@ -424,7 +430,6 @@ def analyse_winding(
|
|||||||
# turns have been placed in the current layer already.
|
# turns have been placed in the current layer already.
|
||||||
current_layer = start_layer
|
current_layer = start_layer
|
||||||
turns_in_current_layer = start_turns_in_layer
|
turns_in_current_layer = start_turns_in_layer
|
||||||
total_turns_placed = start_total_turns
|
|
||||||
current_layer_wire_diameter: Optional[float] = None # set on first use
|
current_layer_wire_diameter: Optional[float] = None # set on first use
|
||||||
|
|
||||||
for seg_idx, (seg_turns, wire) in enumerate(zip(spec.segment_turns(), seg_wires)):
|
for seg_idx, (seg_turns, wire) in enumerate(zip(spec.segment_turns(), seg_wires)):
|
||||||
@@ -432,6 +437,7 @@ def analyse_winding(
|
|||||||
seg_wire_length_mm = 0.0
|
seg_wire_length_mm = 0.0
|
||||||
seg_fits = True
|
seg_fits = True
|
||||||
turns_remaining = seg_turns
|
turns_remaining = seg_turns
|
||||||
|
wire_area = math.pi * (wire.diameter_mm / 2.0) ** 2
|
||||||
|
|
||||||
while turns_remaining > 0:
|
while turns_remaining > 0:
|
||||||
# If the wire gauge changed from what's already on this layer,
|
# If the wire gauge changed from what's already on this layer,
|
||||||
@@ -449,7 +455,6 @@ def analyse_winding(
|
|||||||
# No more geometry room at all
|
# No more geometry room at all
|
||||||
seg_fits = False
|
seg_fits = False
|
||||||
overall_feasible = False
|
overall_feasible = False
|
||||||
# Record overflow as a single "layer" with 0 capacity
|
|
||||||
seg_layers.append(LayerResult(
|
seg_layers.append(LayerResult(
|
||||||
layer_index=current_layer,
|
layer_index=current_layer,
|
||||||
turns_capacity=0,
|
turns_capacity=0,
|
||||||
@@ -467,8 +472,11 @@ def analyse_winding(
|
|||||||
turns_in_current_layer = 0
|
turns_in_current_layer = 0
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check fill-factor cap
|
# Fill-factor check: how many more turns of this gauge fit in budget?
|
||||||
if total_turns_placed >= effective_max:
|
area_remaining = budget_area_mm2 - consumed_area_mm2
|
||||||
|
turns_by_fill = int(area_remaining / wire_area) if wire_area > 0 else 0
|
||||||
|
|
||||||
|
if turns_by_fill <= 0:
|
||||||
seg_fits = False
|
seg_fits = False
|
||||||
overall_feasible = False
|
overall_feasible = False
|
||||||
seg_layers.append(LayerResult(
|
seg_layers.append(LayerResult(
|
||||||
@@ -481,9 +489,7 @@ def analyse_winding(
|
|||||||
turns_remaining = 0
|
turns_remaining = 0
|
||||||
break
|
break
|
||||||
|
|
||||||
turns_to_place = min(turns_remaining,
|
turns_to_place = min(turns_remaining, available_in_layer, turns_by_fill)
|
||||||
available_in_layer,
|
|
||||||
effective_max - total_turns_placed)
|
|
||||||
wire_len = turns_to_place * L_turn
|
wire_len = turns_to_place * L_turn
|
||||||
|
|
||||||
seg_layers.append(LayerResult(
|
seg_layers.append(LayerResult(
|
||||||
@@ -497,7 +503,7 @@ def analyse_winding(
|
|||||||
seg_wire_length_mm += wire_len
|
seg_wire_length_mm += wire_len
|
||||||
turns_remaining -= turns_to_place
|
turns_remaining -= turns_to_place
|
||||||
turns_in_current_layer += turns_to_place
|
turns_in_current_layer += turns_to_place
|
||||||
total_turns_placed += turns_to_place
|
consumed_area_mm2 += turns_to_place * wire_area
|
||||||
|
|
||||||
if turns_in_current_layer >= cap:
|
if turns_in_current_layer >= cap:
|
||||||
current_layer += 1
|
current_layer += 1
|
||||||
@@ -549,7 +555,7 @@ def analyse_winding(
|
|||||||
cost_usd=cost_usd,
|
cost_usd=cost_usd,
|
||||||
_end_layer=current_layer,
|
_end_layer=current_layer,
|
||||||
_end_turns_in_layer=turns_in_current_layer,
|
_end_turns_in_layer=turns_in_current_layer,
|
||||||
_end_total_turns=total_turns_placed,
|
_end_consumed_area_mm2=consumed_area_mm2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -582,39 +588,24 @@ def design_transformer(
|
|||||||
results: list[WindingResult] = []
|
results: list[WindingResult] = []
|
||||||
cur_layer = 0
|
cur_layer = 0
|
||||||
cur_turns_in_layer = 0
|
cur_turns_in_layer = 0
|
||||||
# Track consumed window area (mm²) across all windings to enforce the
|
|
||||||
# shared fill-factor budget regardless of AWG.
|
|
||||||
consumed_area_mm2 = 0.0
|
consumed_area_mm2 = 0.0
|
||||||
budget_area_mm2 = fill_factor * core.window_area_mm2
|
budget_area_mm2 = fill_factor * core.window_area_mm2
|
||||||
|
|
||||||
for spec in windings:
|
for spec in windings:
|
||||||
# Use the thickest wire in this spec for area-budget accounting
|
|
||||||
seg_wires = [WireSpec.from_awg(a) for a in spec.awg]
|
|
||||||
wire_ref = max(seg_wires, key=lambda w: w.diameter_mm)
|
|
||||||
wire_area_mm2 = math.pi * (wire_ref.diameter_mm / 2.0) ** 2
|
|
||||||
# Express the remaining area budget as an equivalent turn count for
|
|
||||||
# this winding's reference wire gauge, then back-calculate the
|
|
||||||
# synthetic start offset so that analyse_winding's fill-factor
|
|
||||||
# headroom check is correct.
|
|
||||||
turns_already_equivalent = int(consumed_area_mm2 / wire_area_mm2)
|
|
||||||
|
|
||||||
result = analyse_winding(
|
result = analyse_winding(
|
||||||
core=core,
|
core=core,
|
||||||
spec=spec,
|
spec=spec,
|
||||||
fill_factor=fill_factor,
|
fill_factor=fill_factor,
|
||||||
start_layer=cur_layer,
|
start_layer=cur_layer,
|
||||||
start_turns_in_layer=cur_turns_in_layer,
|
start_turns_in_layer=cur_turns_in_layer,
|
||||||
start_total_turns=turns_already_equivalent,
|
consumed_area_mm2=consumed_area_mm2,
|
||||||
|
budget_area_mm2=budget_area_mm2,
|
||||||
)
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
# Advance shared layer position
|
|
||||||
cur_layer = result._end_layer
|
cur_layer = result._end_layer
|
||||||
cur_turns_in_layer = result._end_turns_in_layer
|
cur_turns_in_layer = result._end_turns_in_layer
|
||||||
# Update consumed area using actual turns placed in this winding
|
consumed_area_mm2 = result._end_consumed_area_mm2
|
||||||
turns_placed = result._end_total_turns - turns_already_equivalent
|
|
||||||
consumed_area_mm2 = min(budget_area_mm2,
|
|
||||||
consumed_area_mm2 + turns_placed * wire_area_mm2)
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -633,7 +624,7 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
|
|
||||||
secondary = WindingSpec(
|
secondary = WindingSpec(
|
||||||
awg=[22, 22, 22, 26], # uniform gauge
|
awg=[22, 22, 22, 28], # uniform gauge
|
||||||
taps=[0, 100, 50, 50, 50],
|
taps=[0, 100, 50, 50, 50],
|
||||||
name="secondary",
|
name="secondary",
|
||||||
)
|
)
|
||||||
|
|||||||
156
draw_toroid.py
156
draw_toroid.py
@@ -145,57 +145,86 @@ def draw_toroid(
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
# Build layer_index -> wire_centre_radius map (accounts for mixed gauges)
|
# Drawing geometry.
|
||||||
#
|
#
|
||||||
# Layers are wound in order from the bore wall inward. Each layer
|
# Layer radial positions use the largest wire diameter (_uniform_d) as
|
||||||
# consumes its own wire diameter of radial space. We walk through all
|
# a uniform pitch: visual layer n sits at ID/2 - (n+0.5)*_uniform_d.
|
||||||
# layers across all windings in ascending layer_index order and accumulate
|
# This keeps layer spacing uniform regardless of gauge.
|
||||||
# the actual radial depth so the centre radius is correct regardless of
|
#
|
||||||
# whether wire gauge changes between segments or windings.
|
# Angular packing uses each segment's actual wire diameter so thinner
|
||||||
|
# wires pack more tightly and are drawn to scale.
|
||||||
|
#
|
||||||
|
# designer.py bumps the layer index on a gauge change even when the
|
||||||
|
# current layer still has room. For drawing we replay the packing
|
||||||
|
# using actual wire diameters to determine visual layer assignments,
|
||||||
|
# so a gauge change mid-layer continues on the same visual ring.
|
||||||
|
#
|
||||||
|
# Result: _seg_layer_draw[(w_idx, seg_idx, designer_layer)] =
|
||||||
|
# (visual_layer_index, start_angle)
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
|
||||||
# Collect (layer_index, wire_diameter) for every active layer, all windings
|
# --- Largest wire diameter → uniform radial layer pitch ---
|
||||||
_layer_d: dict[int, float] = {}
|
_all_diameters: list[float] = []
|
||||||
for wr in winding_results:
|
for wr in winding_results:
|
||||||
for seg in wr.segments:
|
for seg in wr.segments:
|
||||||
wire = WireSpec.from_awg(seg.awg)
|
wire = WireSpec.from_awg(seg.awg)
|
||||||
for lr in seg.layers:
|
for lr in seg.layers:
|
||||||
if lr.turns_used > 0 and lr.turns_capacity > 0:
|
if lr.turns_used > 0 and lr.turns_capacity > 0:
|
||||||
# First winding to touch a layer defines its wire gauge
|
_all_diameters.append(wire.diameter_mm)
|
||||||
if lr.layer_index not in _layer_d:
|
|
||||||
_layer_d[lr.layer_index] = wire.diameter_mm
|
|
||||||
|
|
||||||
# Walk layers in order to accumulate radial depth from the bore wall
|
_uniform_d = max(_all_diameters) if _all_diameters else 1.0
|
||||||
layer_centre_r: dict[int, float] = {}
|
|
||||||
depth = 0.0
|
|
||||||
for n in sorted(_layer_d):
|
|
||||||
d = _layer_d[n]
|
|
||||||
layer_centre_r[n] = ID / 2.0 - depth - d / 2.0
|
|
||||||
depth += d
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------
|
# --- Replay packing with actual wire diameters ---
|
||||||
# Pre-compute per-layer start angles (shared across all windings) so
|
# Track arc consumed (radians) on the current visual layer. Each wire
|
||||||
# the total winding arc on each layer is centred at the top (π/2).
|
# of diameter d at ring radius r consumes d/r radians. Advance to the
|
||||||
# -----------------------------------------------------------------------
|
# next ring when the accumulated arc reaches 2π. This handles mixed
|
||||||
_layer_total_used: dict[int, int] = {}
|
# gauges correctly: the total arc never exceeds one full circumference.
|
||||||
for _wr in winding_results:
|
#
|
||||||
for _seg in _wr.segments:
|
# A single designer layer entry may be split across two visual layers
|
||||||
for _lr in _seg.layers:
|
# (when the layer wraps mid-segment), so we store a list of draw calls.
|
||||||
if _lr.turns_used > 0 and _lr.turns_capacity > 0:
|
#
|
||||||
n_ = _lr.layer_index
|
# Key: (w_idx, seg_idx, designer_layer_index) ->
|
||||||
_layer_total_used[n_] = _layer_total_used.get(n_, 0) + _lr.turns_used
|
# list of (vis_layer, start_angle, turns_to_draw)
|
||||||
|
|
||||||
# Step = d/r (touching circles), start at 0, wind continuously.
|
_seg_layer_draw: dict[tuple[int, int, int], list[tuple[int, float, int]]] = {}
|
||||||
# All segments share the same running angle per layer — they just
|
|
||||||
# pick up where the previous segment left off and keep going around.
|
vis_layer = 0
|
||||||
layer_angle_step: dict[int, float] = {}
|
vis_arc = 0.0 # arc consumed so far on current visual layer (radians)
|
||||||
layer_next_angle: dict[int, float] = {}
|
|
||||||
for n_ in _layer_total_used:
|
for w_idx, wr in enumerate(winding_results):
|
||||||
r_ = layer_centre_r.get(n_, 0.0)
|
for seg in wr.segments:
|
||||||
d_ = _layer_d.get(n_, 1.0)
|
wire = WireSpec.from_awg(seg.awg)
|
||||||
if r_ > 0:
|
d_actual = wire.diameter_mm
|
||||||
layer_angle_step[n_] = d_ / r_
|
for lr in seg.layers:
|
||||||
layer_next_angle[n_] = 0.0
|
if lr.turns_used == 0 or lr.turns_capacity == 0:
|
||||||
|
continue
|
||||||
|
turns_left = lr.turns_used
|
||||||
|
key = (w_idx, seg.segment_index, lr.layer_index)
|
||||||
|
while turns_left > 0:
|
||||||
|
r_ = ID / 2.0 - (vis_layer + 0.5) * _uniform_d
|
||||||
|
if r_ <= 0:
|
||||||
|
break # no more radial room
|
||||||
|
step = d_actual / r_ # arc per wire (radians)
|
||||||
|
available = int((2 * math.pi - vis_arc) / step)
|
||||||
|
if available <= 0:
|
||||||
|
vis_layer += 1
|
||||||
|
vis_arc = 0.0
|
||||||
|
continue
|
||||||
|
place = min(turns_left, available)
|
||||||
|
_seg_layer_draw.setdefault(key, []).append(
|
||||||
|
(vis_layer, vis_arc, place)
|
||||||
|
)
|
||||||
|
vis_arc += place * step
|
||||||
|
turns_left -= place
|
||||||
|
if vis_arc >= 2 * math.pi - step * 0.5:
|
||||||
|
vis_layer += 1
|
||||||
|
vis_arc = 0.0
|
||||||
|
|
||||||
|
# --- Visual layer centre radii ---
|
||||||
|
_vis_layers_used = set(v for draws in _seg_layer_draw.values() for v, _, _ in draws)
|
||||||
|
layer_centre_r: dict[int, float] = {
|
||||||
|
n: ID / 2.0 - (n + 0.5) * _uniform_d for n in _vis_layers_used
|
||||||
|
}
|
||||||
|
|
||||||
legend_handles: list[mpatches.Patch] = []
|
legend_handles: list[mpatches.Patch] = []
|
||||||
|
|
||||||
@@ -227,35 +256,28 @@ def draw_toroid(
|
|||||||
if lr.turns_used == 0 or lr.turns_capacity == 0:
|
if lr.turns_used == 0 or lr.turns_capacity == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
n = lr.layer_index
|
key = (w_idx, seg.segment_index, lr.layer_index)
|
||||||
r = layer_centre_r.get(n, 0.0)
|
if key not in _seg_layer_draw:
|
||||||
if r <= 0:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
angle_step = layer_angle_step.get(n, 2.0 * math.pi / max(lr.turns_used, 1))
|
|
||||||
start_angle = layer_next_angle.get(n, 0.0)
|
|
||||||
|
|
||||||
# Draw at true wire radius. Circles are evenly spaced over
|
|
||||||
# 360° so they tile the ring; they may overlap on outer
|
|
||||||
# layers (where arc spacing > wire diameter) or underlap on
|
|
||||||
# very dense inner layers, but the count and colour are correct.
|
|
||||||
draw_r = d / 2.0
|
draw_r = d / 2.0
|
||||||
|
for vis_n, start_angle, n_turns in _seg_layer_draw[key]:
|
||||||
for i in range(lr.turns_used):
|
r = layer_centre_r.get(vis_n, 0.0)
|
||||||
a = start_angle + i * angle_step
|
if r <= 0:
|
||||||
x = r * math.cos(a)
|
continue
|
||||||
y = r * math.sin(a)
|
angle_step = d / r
|
||||||
ax.add_patch(Circle(
|
for i in range(n_turns):
|
||||||
(x, y), draw_r,
|
a = start_angle + i * angle_step
|
||||||
facecolor=seg_color,
|
x = r * math.cos(a)
|
||||||
edgecolor=_WIRE_EDGE_COLOR,
|
y = r * math.sin(a)
|
||||||
linewidth=0.35,
|
ax.add_patch(Circle(
|
||||||
alpha=0.90,
|
(x, y), draw_r,
|
||||||
zorder=10 + n,
|
facecolor=seg_color,
|
||||||
))
|
edgecolor=_WIRE_EDGE_COLOR,
|
||||||
|
linewidth=0.35,
|
||||||
# Advance the angle for the next segment on this layer
|
alpha=0.90,
|
||||||
layer_next_angle[n] = start_angle + lr.turns_used * angle_step
|
zorder=10 + vis_n,
|
||||||
|
))
|
||||||
|
|
||||||
# Segment legend entry
|
# Segment legend entry
|
||||||
awg_tag = f" AWG {seg.awg}" if len(set(awg_list)) > 1 else ""
|
awg_tag = f" AWG {seg.awg}" if len(set(awg_list)) > 1 else ""
|
||||||
@@ -356,7 +378,7 @@ if __name__ == "__main__":
|
|||||||
core = ToroidCore(ID_mm=21.5, OD_mm=46.5, height_mm=22.8)
|
core = ToroidCore(ID_mm=21.5, OD_mm=46.5, height_mm=22.8)
|
||||||
|
|
||||||
primary = WindingSpec(
|
primary = WindingSpec(
|
||||||
awg=[22, 22],
|
awg=[20, 22],
|
||||||
taps=[0, 25, 50],
|
taps=[0, 25, 50],
|
||||||
name="primary",
|
name="primary",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
{
|
{
|
||||||
"points": {
|
"points": {
|
||||||
"frequencies": "256\n870\n3140\n8900\n12000\n16000\n22000\n33000\n45000",
|
"frequencies": "256\n870\n3140\n8900\n12000\n16000\n22000\n33000\n45000",
|
||||||
"loads": "5\n10\n50\n100\n200\n600\n2000",
|
"loads": "10\n50\n100\n200\n600\n2000",
|
||||||
"target_power_W": 25,
|
"target_power_W": 25,
|
||||||
"power_tol_pct": 2,
|
"power_tol_pct": 2,
|
||||||
"Vp_min": 1,
|
"Vp_min": 1,
|
||||||
"Vp_max": 50,
|
"Vp_max": 50,
|
||||||
"Vp_steps": 100
|
"Vp_steps": 100,
|
||||||
|
"T_ambient_C": 25,
|
||||||
|
"h_conv": 6
|
||||||
},
|
},
|
||||||
"_last": "points"
|
"_last": "points"
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,8 @@
|
|||||||
50
|
50
|
||||||
],
|
],
|
||||||
"awg": [
|
"awg": [
|
||||||
22,
|
20,
|
||||||
22
|
20
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
50
|
50
|
||||||
],
|
],
|
||||||
"awg": [
|
"awg": [
|
||||||
|
20,
|
||||||
22,
|
22,
|
||||||
22,
|
28,
|
||||||
22,
|
28
|
||||||
22
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -535,6 +535,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Operating Point -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Operating Point</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-row">
|
||||||
|
<label class="field"><span>Primary tap</span><input type="number" id="op-ptap" value="1" min="1" step="1"></label>
|
||||||
|
<label class="field"><span>Secondary tap</span><input type="number" id="op-stap" value="1" min="1" step="1"></label>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label class="field"><span>Vp RMS (V)</span><input type="number" id="op-vp" value="12" min="0.01" step="0.1"></label>
|
||||||
|
<label class="field"><span>Frequency (Hz)</span><input type="number" id="op-freq" value="1000" min="1" step="1"></label>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label class="field"><span>Load R (Ω)</span><input type="number" id="op-rload" value="100" min="0" step="1"></label>
|
||||||
|
<label class="field"><span>Load X (Ω)</span><input type="number" id="op-xload" value="0" step="1"></label>
|
||||||
|
</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn btn-primary" id="btn-op" onclick="runOperatingPoint()">Simulate</button>
|
||||||
|
</div>
|
||||||
|
<div id="op-msg" class="msg" style="margin-top:8px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /left-col -->
|
</div><!-- /left-col -->
|
||||||
|
|
||||||
|
|
||||||
@@ -552,6 +575,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Operating Point Results -->
|
||||||
|
<div class="card" id="op-results-card" style="display:none">
|
||||||
|
<div class="card-header">Operating Point Results</div>
|
||||||
|
<div class="card-body" style="padding:12px">
|
||||||
|
<div id="op-violations" style="display:none;margin-bottom:10px;padding:8px 12px;background:#fef2f2;border:1px solid #fca5a5;border-radius:6px;color:#b91c1c;font-size:12px"></div>
|
||||||
|
<table class="design-table" style="width:100%">
|
||||||
|
<tbody id="op-results-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Plots -->
|
<!-- Plots -->
|
||||||
<div class="card" id="plots-card" style="display:none">
|
<div class="card" id="plots-card" style="display:none">
|
||||||
<div class="card-header">Simulation Results</div>
|
<div class="card-header">Simulation Results</div>
|
||||||
@@ -834,6 +868,93 @@ function parseLoads() {
|
|||||||
}).filter(p => p[0] > 0 || p[1] !== 0);
|
}).filter(p => p[0] > 0 || p[1] !== 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Operating Point
|
||||||
|
// ============================================================
|
||||||
|
async function runOperatingPoint() {
|
||||||
|
const btn = document.getElementById('btn-op');
|
||||||
|
const msg = document.getElementById('op-msg');
|
||||||
|
const card = document.getElementById('op-results-card');
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
setMsg(msg, 'info', 'Simulating…');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = Object.assign({}, buildPayload(), {
|
||||||
|
primary_tap: parseInt(document.getElementById('op-ptap').value),
|
||||||
|
secondary_tap: parseInt(document.getElementById('op-stap').value),
|
||||||
|
Vp_rms: parseFloat(document.getElementById('op-vp').value),
|
||||||
|
freq_hz: parseFloat(document.getElementById('op-freq').value),
|
||||||
|
R_load: parseFloat(document.getElementById('op-rload').value) || 0,
|
||||||
|
X_load: parseFloat(document.getElementById('op-xload').value) || 0,
|
||||||
|
constraints: collectConstraints(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const resp = await fetch('/api/simulate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
setMsg(msg, 'error', 'Error: ' + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMsg(msg, 'ok', 'Done.');
|
||||||
|
card.style.display = '';
|
||||||
|
|
||||||
|
// Violations banner
|
||||||
|
const vdiv = document.getElementById('op-violations');
|
||||||
|
if (data.violations && data.violations.length) {
|
||||||
|
vdiv.style.display = '';
|
||||||
|
vdiv.textContent = 'Violations: ' + data.violations.join(', ');
|
||||||
|
} else {
|
||||||
|
vdiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
['Turns (Np / Ns)', `${data.Np_eff} / ${data.Ns_eff}`],
|
||||||
|
['Turns ratio (Np:Ns)', data.turns_ratio != null ? data.turns_ratio.toFixed(4) : '—'],
|
||||||
|
['Peak flux density', data.B_peak_T != null ? data.B_peak_T.toFixed(4) + ' T' : '—'],
|
||||||
|
['Primary voltage (Vp)', data.Vp_rms != null ? data.Vp_rms.toFixed(3) + ' V' : '—'],
|
||||||
|
['Secondary voltage (Vs)', data.Vs_rms != null ? data.Vs_rms.toFixed(3) + ' V' : '—'],
|
||||||
|
['Primary current (Ip)', data.Ip_rms != null ? data.Ip_rms.toFixed(4) + ' A' : '—'],
|
||||||
|
['Secondary current (Is)', data.Is_rms != null ? data.Is_rms.toFixed(4) + ' A' : '—'],
|
||||||
|
['Output power', data.P_out_W != null ? data.P_out_W.toFixed(3) + ' W' : '—'],
|
||||||
|
['Copper loss (primary)', data.P_cu_primary_W != null ? data.P_cu_primary_W.toFixed(3) + ' W' : '—'],
|
||||||
|
['Copper loss (secondary)', data.P_cu_secondary_W != null ? data.P_cu_secondary_W.toFixed(3) + ' W' : '—'],
|
||||||
|
['Total copper loss', data.P_cu_W != null ? data.P_cu_W.toFixed(3) + ' W' : '—'],
|
||||||
|
['Core loss', data.P_core_W != null ? data.P_core_W.toFixed(3) + ' W' : '—'],
|
||||||
|
['Input power', data.P_in_W != null ? data.P_in_W.toFixed(3) + ' W' : '—'],
|
||||||
|
['Efficiency', data.efficiency_pct != null ? data.efficiency_pct.toFixed(2) + ' %' : '—'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const tbody = document.getElementById('op-results-body');
|
||||||
|
tbody.innerHTML = rows.map(([label, val]) =>
|
||||||
|
`<tr><td style="font-weight:500;color:#444;width:55%">${label}</td><td>${val}</td></tr>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
setMsg(msg, 'error', 'Error: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: collect constraints object from UI
|
||||||
|
function collectConstraints() {
|
||||||
|
return {
|
||||||
|
B_max_T: parseFloat(document.getElementById('con-B').value) || 0.3,
|
||||||
|
Vp_max: parseFloat(document.getElementById('con-Vp').value) || 1e9,
|
||||||
|
Vs_max: parseFloat(document.getElementById('con-Vs').value) || 1e9,
|
||||||
|
Ip_max: parseFloat(document.getElementById('con-Ip').value) || 1e9,
|
||||||
|
Is_max: parseFloat(document.getElementById('con-Is').value) || 1e9,
|
||||||
|
P_out_max_W:parseFloat(document.getElementById('con-Pout').value) || 1e9,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function runSweep() {
|
async function runSweep() {
|
||||||
const btn = document.getElementById('btn-sim');
|
const btn = document.getElementById('btn-sim');
|
||||||
const msg = document.getElementById('sim-msg');
|
const msg = document.getElementById('sim-msg');
|
||||||
|
|||||||
Reference in New Issue
Block a user