added toroid simulator
This commit is contained in:
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
|
||||
# consumes its own wire diameter of radial space. We walk through all
|
||||
# layers across all windings in ascending layer_index order and accumulate
|
||||
# the actual radial depth so the centre radius is correct regardless of
|
||||
# whether wire gauge changes between segments or windings.
|
||||
# Layer radial positions use the largest wire diameter (_uniform_d) as
|
||||
# a uniform pitch: visual layer n sits at ID/2 - (n+0.5)*_uniform_d.
|
||||
# This keeps layer spacing uniform regardless of gauge.
|
||||
#
|
||||
# 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
|
||||
_layer_d: dict[int, float] = {}
|
||||
# --- Largest wire diameter → uniform radial layer pitch ---
|
||||
_all_diameters: list[float] = []
|
||||
for wr in winding_results:
|
||||
for seg in wr.segments:
|
||||
wire = WireSpec.from_awg(seg.awg)
|
||||
for lr in seg.layers:
|
||||
if lr.turns_used > 0 and lr.turns_capacity > 0:
|
||||
# First winding to touch a layer defines its wire gauge
|
||||
if lr.layer_index not in _layer_d:
|
||||
_layer_d[lr.layer_index] = wire.diameter_mm
|
||||
_all_diameters.append(wire.diameter_mm)
|
||||
|
||||
# Walk layers in order to accumulate radial depth from the bore wall
|
||||
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
|
||||
_uniform_d = max(_all_diameters) if _all_diameters else 1.0
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Pre-compute per-layer start angles (shared across all windings) so
|
||||
# the total winding arc on each layer is centred at the top (π/2).
|
||||
# -----------------------------------------------------------------------
|
||||
_layer_total_used: dict[int, int] = {}
|
||||
for _wr in winding_results:
|
||||
for _seg in _wr.segments:
|
||||
for _lr in _seg.layers:
|
||||
if _lr.turns_used > 0 and _lr.turns_capacity > 0:
|
||||
n_ = _lr.layer_index
|
||||
_layer_total_used[n_] = _layer_total_used.get(n_, 0) + _lr.turns_used
|
||||
# --- Replay packing with actual wire diameters ---
|
||||
# Track arc consumed (radians) on the current visual layer. Each wire
|
||||
# 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
|
||||
# gauges correctly: the total arc never exceeds one full circumference.
|
||||
#
|
||||
# A single designer layer entry may be split across two visual layers
|
||||
# (when the layer wraps mid-segment), so we store a list of draw calls.
|
||||
#
|
||||
# Key: (w_idx, seg_idx, designer_layer_index) ->
|
||||
# list of (vis_layer, start_angle, turns_to_draw)
|
||||
|
||||
# Step = d/r (touching circles), start at 0, wind continuously.
|
||||
# All segments share the same running angle per layer — they just
|
||||
# pick up where the previous segment left off and keep going around.
|
||||
layer_angle_step: dict[int, float] = {}
|
||||
layer_next_angle: dict[int, float] = {}
|
||||
for n_ in _layer_total_used:
|
||||
r_ = layer_centre_r.get(n_, 0.0)
|
||||
d_ = _layer_d.get(n_, 1.0)
|
||||
if r_ > 0:
|
||||
layer_angle_step[n_] = d_ / r_
|
||||
layer_next_angle[n_] = 0.0
|
||||
_seg_layer_draw: dict[tuple[int, int, int], list[tuple[int, float, int]]] = {}
|
||||
|
||||
vis_layer = 0
|
||||
vis_arc = 0.0 # arc consumed so far on current visual layer (radians)
|
||||
|
||||
for w_idx, wr in enumerate(winding_results):
|
||||
for seg in wr.segments:
|
||||
wire = WireSpec.from_awg(seg.awg)
|
||||
d_actual = wire.diameter_mm
|
||||
for lr in seg.layers:
|
||||
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] = []
|
||||
|
||||
@@ -227,35 +256,28 @@ def draw_toroid(
|
||||
if lr.turns_used == 0 or lr.turns_capacity == 0:
|
||||
continue
|
||||
|
||||
n = lr.layer_index
|
||||
r = layer_centre_r.get(n, 0.0)
|
||||
if r <= 0:
|
||||
key = (w_idx, seg.segment_index, lr.layer_index)
|
||||
if key not in _seg_layer_draw:
|
||||
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
|
||||
|
||||
for i in range(lr.turns_used):
|
||||
a = start_angle + i * angle_step
|
||||
x = r * math.cos(a)
|
||||
y = r * math.sin(a)
|
||||
ax.add_patch(Circle(
|
||||
(x, y), draw_r,
|
||||
facecolor=seg_color,
|
||||
edgecolor=_WIRE_EDGE_COLOR,
|
||||
linewidth=0.35,
|
||||
alpha=0.90,
|
||||
zorder=10 + n,
|
||||
))
|
||||
|
||||
# Advance the angle for the next segment on this layer
|
||||
layer_next_angle[n] = start_angle + lr.turns_used * angle_step
|
||||
for vis_n, start_angle, n_turns in _seg_layer_draw[key]:
|
||||
r = layer_centre_r.get(vis_n, 0.0)
|
||||
if r <= 0:
|
||||
continue
|
||||
angle_step = d / r
|
||||
for i in range(n_turns):
|
||||
a = start_angle + i * angle_step
|
||||
x = r * math.cos(a)
|
||||
y = r * math.sin(a)
|
||||
ax.add_patch(Circle(
|
||||
(x, y), draw_r,
|
||||
facecolor=seg_color,
|
||||
edgecolor=_WIRE_EDGE_COLOR,
|
||||
linewidth=0.35,
|
||||
alpha=0.90,
|
||||
zorder=10 + vis_n,
|
||||
))
|
||||
|
||||
# Segment legend entry
|
||||
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)
|
||||
|
||||
primary = WindingSpec(
|
||||
awg=[22, 22],
|
||||
awg=[20, 22],
|
||||
taps=[0, 25, 50],
|
||||
name="primary",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user