added toroid simulator

This commit is contained in:
2026-02-17 11:11:47 -06:00
parent a881a0a381
commit 4df588e09c
6 changed files with 323 additions and 108 deletions

View File

@@ -279,7 +279,7 @@ class WindingResult:
# Internal winding position at end of this winding (for chaining)
_end_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:
# Header: show AWG as single value if uniform, else show range
@@ -372,7 +372,8 @@ def analyse_winding(
fill_factor: float = 0.35,
start_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:
"""
Analyse feasibility and compute wire parameters for a winding.
@@ -389,9 +390,13 @@ def analyse_winding(
_end_layer to continue mid-layer across windings.
start_turns_in_layer : int
Turns already consumed in start_layer from a previous winding.
start_total_turns : int
Cumulative turns already placed (used for fill-factor accounting
across windings).
consumed_area_mm2 : float
Wire cross-section area (mm²) already consumed by previous 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
-------
@@ -415,7 +420,8 @@ def analyse_winding(
max_turns_geometry += cap
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] = []
overall_feasible = True
@@ -424,7 +430,6 @@ def analyse_winding(
# turns have been placed in the current layer already.
current_layer = start_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
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_fits = True
turns_remaining = seg_turns
wire_area = math.pi * (wire.diameter_mm / 2.0) ** 2
while turns_remaining > 0:
# 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
seg_fits = False
overall_feasible = False
# Record overflow as a single "layer" with 0 capacity
seg_layers.append(LayerResult(
layer_index=current_layer,
turns_capacity=0,
@@ -467,8 +472,11 @@ def analyse_winding(
turns_in_current_layer = 0
continue
# Check fill-factor cap
if total_turns_placed >= effective_max:
# Fill-factor check: how many more turns of this gauge fit in budget?
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
overall_feasible = False
seg_layers.append(LayerResult(
@@ -481,9 +489,7 @@ def analyse_winding(
turns_remaining = 0
break
turns_to_place = min(turns_remaining,
available_in_layer,
effective_max - total_turns_placed)
turns_to_place = min(turns_remaining, available_in_layer, turns_by_fill)
wire_len = turns_to_place * L_turn
seg_layers.append(LayerResult(
@@ -497,7 +503,7 @@ def analyse_winding(
seg_wire_length_mm += wire_len
turns_remaining -= 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:
current_layer += 1
@@ -549,7 +555,7 @@ def analyse_winding(
cost_usd=cost_usd,
_end_layer=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] = []
cur_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
budget_area_mm2 = fill_factor * core.window_area_mm2
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(
core=core,
spec=spec,
fill_factor=fill_factor,
start_layer=cur_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)
# Advance shared layer position
cur_layer = result._end_layer
cur_turns_in_layer = result._end_turns_in_layer
# Update consumed area using actual turns placed in this winding
turns_placed = result._end_total_turns - turns_already_equivalent
consumed_area_mm2 = min(budget_area_mm2,
consumed_area_mm2 + turns_placed * wire_area_mm2)
consumed_area_mm2 = result._end_consumed_area_mm2
return results
@@ -633,7 +624,7 @@ if __name__ == "__main__":
)
secondary = WindingSpec(
awg=[22, 22, 22, 26], # uniform gauge
awg=[22, 22, 22, 28], # uniform gauge
taps=[0, 100, 50, 50, 50],
name="secondary",
)