Skip to content

Commit 783cd4a

Browse files
feat(datamodel): add wirepatterns submodule directory
1 parent a9899c7 commit 783cd4a

1 file changed

Lines changed: 336 additions & 0 deletions

File tree

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
module WirePatterns
2+
3+
# ────────────────────────────────────────────────────────────────────────────
4+
# Public API
5+
# ────────────────────────────────────────────────────────────────────────────
6+
7+
export HexaPattern, make_stranded
8+
export ScreenPattern, make_screened
9+
10+
# ────────────────────────────────────────────────────────────────────────────
11+
# Types
12+
# ────────────────────────────────────────────────────────────────────────────
13+
14+
"""
15+
struct HexaPattern
16+
17+
Result for a single design choice.
18+
19+
Fields:
20+
- `layers::Int` — number of concentric layers (1 = center only).
21+
- `wires::Int` — total number of wires, N(L) = 1 + 3L(L-1).
22+
- `wire_diameter_m::Float64` — strand diameter [m].
23+
- `total_area_m2::Float64` — summed metallic area [m²].
24+
- `awg::String` — AWG label from the table (informative).
25+
"""
26+
struct HexaPattern
27+
layers::Int
28+
wires::Int
29+
wire_diameter_m::Float64
30+
total_area_m2::Float64
31+
awg::String
32+
end
33+
34+
"""
35+
struct ScreenPattern
36+
37+
Screen wires design.
38+
39+
Fields:
40+
- `wires::Int` — number of wires on the wire array (N).
41+
- `wire_diameter_m::Float64` — strand diameter [m].
42+
- `lay_diameter_m::Float64` — laying diameter Dm [m].
43+
- `radius_m::Float64` — wire array centerline radius = (Dm + d)/2 [m].
44+
- `total_area_m2::Float64` — N * π/4 * d^2 [m²].
45+
- `coverage_pct::Float64` — 100 * N*d / (π*Dm*sinα) [%].
46+
- `awg::String` — AWG label from the table (informative).
47+
"""
48+
struct ScreenPattern
49+
wires::Int
50+
wire_diameter_m::Float64
51+
lay_diameter_m::Float64
52+
radius_m::Float64
53+
total_area_m2::Float64
54+
coverage_pct::Float64
55+
awg::String
56+
end
57+
58+
# ────────────────────────────────────────────────────────────────────────────
59+
# Utils
60+
# ────────────────────────────────────────────────────────────────────────────
61+
62+
_wire_area(dw::Real) = (pi/4) * (dw^2) # area of one wire
63+
64+
# ---- AWG exact formulas (solid wire) ----
65+
const _AWG_BASE = 92.0
66+
const _D0_MM = 0.127 # 0.005 in in mm
67+
const _AREA0_MM2 = 0.012668 # (π/4)*0.127^2
68+
const _LN_BASE = log(_AWG_BASE)
69+
70+
awg_to_d_mm(n::Real) = _D0_MM * (_AWG_BASE ^ ((36 - n)/39))
71+
awg_to_area_mm2(n::Real) = _AREA0_MM2 * (_AWG_BASE ^ ((36 - n)/19.5))
72+
73+
d_mm_to_awg(d_mm::Real) = 36 - 39 * (log(d_mm/_D0_MM) / _LN_BASE)
74+
area_mm2_to_awg(A_mm2::Real) = 36 - 19.5 * (log(A_mm2/_AREA0_MM2) / _LN_BASE)
75+
76+
function awg_label(n::Integer)
77+
n == -3 && return "0000 (4/0)"
78+
n == -2 && return "000 (3/0)"
79+
n == -1 && return "00 (2/0)"
80+
n == 0 && return "0 (1/0)"
81+
return string(n)
82+
end
83+
84+
"Generate (label, diameter_m) for AWG n in [nmin, nmax]."
85+
function awg_sizes(nmin::Integer = -3, nmax::Integer = 40)
86+
out = Tuple{String, Float64}[]
87+
@inbounds for n in nmin:nmax
88+
d_m = awg_to_d_mm(n) / 1000.0
89+
push!(out, (awg_label(n), d_m))
90+
end
91+
return out
92+
end
93+
94+
"Apply a compaction/fill factor to solid area to approximate stranded metallic CSA."
95+
stranded_area_mm2(n::Real; fill_factor::Real = 0.94) = fill_factor * awg_to_area_mm2(n)
96+
97+
# ────────────────────────────────────────────────────────────────────────────
98+
# Hexagonal strand patterns
99+
# ────────────────────────────────────────────────────────────────────────────
100+
101+
# ---- wire-count constraints per target area (mm²) ----
102+
const _WIRE_RULES = Tuple{Int, Int, Union{Int, Nothing}}[
103+
(10, 6, 7),
104+
(16, 6, 7),
105+
(25, 6, 7),
106+
(35, 6, 7),
107+
(50, 6, 19),
108+
(70, 12, 19),
109+
(95, 15, 19),
110+
(120, 15, 37),
111+
(150, 15, 37),
112+
(185, 30, 37),
113+
(240, 30, 37),
114+
(300, 30, 61),
115+
(400, 53, 61),
116+
(500, 53, 61),
117+
(630, 53, 91),
118+
(800, 53, 91),
119+
(1000, 53, 91),
120+
]
121+
122+
"""
123+
make_stranded(target_area_m2::Real; nmin::Integer=-3, nmax::Integer=40)
124+
125+
Compute hexagonal-pattern strand layouts that approximate or meet the target metallic cross-section, imposing allowed total-wire ranges by target area.
126+
127+
Inputs:
128+
- `target_area_m2` — target metallic area [m²].
129+
- `nmin`,`nmax` — AWG range to consider (default 4/0 … 40).
130+
131+
Returns:
132+
- `best_match` — within allowed N(L), minimize |A − target|.
133+
- `min_layers` — within allowed N(L) and A ≥ target, minimize layers (tie: smallest excess, then smaller diameter).
134+
Fallback: within allowed, pick largest A < target (tie: smaller L, then smaller diameter).
135+
- `min_diam` — within allowed N(L) and A ≥ target, minimize diameter, then layers, then excess.
136+
Fallback: within allowed, pick smallest diameter with largest A < target (then smallest L).
137+
"""
138+
function make_stranded(target_area_m2::Real; nmin::Integer = -3, nmax::Integer = 40)
139+
@assert target_area_m2 > 0 "Target cross-section must be positive."
140+
@assert nmin <= nmax "nmin must be ≤ nmax."
141+
142+
# ---- hex geometry ----
143+
_hex_N(L::Int) = 1 + 3L*(L - 1) # total wires after L layers
144+
_to_choice((dw, L, N, A, awg)) = HexaPattern(L, N, dw, A, awg)
145+
146+
# Return (minN, maxN::Union{Int,Nothing}) for target in mm²
147+
function _allowed_wires(target_mm2::Real)
148+
for (thr, minN, maxN) in _WIRE_RULES
149+
if target_mm2 <= thr
150+
return (minN, maxN)
151+
end
152+
end
153+
return (53, nothing) # > 1000 mm² -> min 53, no maximum
154+
end
155+
156+
@inline function _allowed_N(N::Int, minN::Int, maxN::Union{Int, Nothing})
157+
maxN === nothing ? (N >= minN) : (N >= minN && N <= maxN)
158+
end
159+
160+
# Allowed wire-count range from target (mm²)
161+
target_mm2 = target_area_m2 * 1e6
162+
minN, maxN = _allowed_wires(target_mm2)
163+
164+
# AWG sizes (label, d_m)
165+
sizes = awg_sizes(nmin, nmax)
166+
@assert !isempty(sizes) "AWG range produced no sizes."
167+
168+
# Build allowed candidates: (dw, L, N, A, awg)
169+
candidates = Vector{Tuple{Float64, Int, Int, Float64, String}}()
170+
for (awg, dw) in sizes
171+
a1 = _wire_area(dw)
172+
@inbounds for L in 1:300
173+
N = _hex_N(L)
174+
if _allowed_N(N, minN, maxN)
175+
A = N * a1
176+
push!(candidates, (dw, L, N, A, awg))
177+
end
178+
if maxN !== nothing && N > maxN
179+
break
180+
end
181+
end
182+
end
183+
@assert !isempty(candidates) "No allowed candidates under the imposed wire-count span."
184+
185+
# ---- best_match: minimize |A - target| (tie: smaller dw, then smaller L) ----
186+
rank_keys = [(abs(A - target_area_m2), dw, L) for (dw, L, N, A, _) in candidates]
187+
best_match = _to_choice(candidates[argmin(rank_keys)])
188+
189+
# Split feasible/infeasible for next selectors
190+
feas = filter(((dw, L, N, A, awg),)->A >= target_area_m2, candidates)
191+
infeas = filter(((dw, L, N, A, awg),)->A < target_area_m2, candidates)
192+
193+
# ---- min_layers ----
194+
if !isempty(feas)
195+
# minimal layers, then minimal excess, then smaller diameter
196+
keys_L = [(L, A - target_area_m2, dw) for (dw, L, N, A, _) in feas]
197+
min_layers = _to_choice(feas[argmin(keys_L)])
198+
else
199+
# fallback: closest from below (largest A), then minimal L, then smaller dw
200+
keys_fb = [(-A, L, dw) for (dw, L, N, A, _) in infeas]
201+
min_layers = _to_choice(infeas[argmin(keys_fb)])
202+
end
203+
204+
# ---- min_diam ----
205+
if !isempty(feas)
206+
# smallest diameter; for it, minimal layers; then smallest excess
207+
sort!(feas, by = x -> (x[1], x[2], x[4] - target_area_m2)) # (dw asc, L asc, excess asc)
208+
min_diam = _to_choice(first(feas))
209+
else
210+
# fallback: smallest diameter with best undershoot; then minimal layers
211+
sort!(infeas, by = x -> (x[1], -(x[4]), x[2])) # (dw asc, A desc, L asc)
212+
min_diam = _to_choice(first(infeas))
213+
end
214+
215+
return (; best_match, min_layers, min_diam)
216+
end
217+
218+
# ────────────────────────────────────────────────────────────────────────────
219+
# Screen (single wire array) patterns
220+
# ────────────────────────────────────────────────────────────────────────────
221+
222+
"""
223+
make_screened(A_req_m2::Real, Dm_m::Real;
224+
alpha_deg::Real=15.0, coverage_min_pct::Real=85.0,
225+
gap_frac::Real=0.0, min_wires::Int=3, extra_span::Int=8,
226+
nmin::Integer=-3, nmax::Integer=40)
227+
228+
Compute screen wire layouts that approximate or meet the target metallic cross-section, imposing:
229+
230+
1) CSA: N * (π/4) * d^2 ≥ A_req_m2
231+
2) Cover: (N*d)/(π*Dm*sinα) * 100 ≥ coverage_min_pct
232+
233+
while enforcing no-overlap wire array geometry (with optional clearance `gap_frac`).
234+
235+
Arguments:
236+
- `A_req_m2` — required metallic cross-section [m²].
237+
- `Dm_m` — laying diameter (screen centerline) [m].
238+
- `alpha_deg` — lay angle α in degrees (default 20°).
239+
- `coverage_min_pct` — required geometric coverage (default 85%).
240+
- `gap_frac` — extra clearance fraction in the no-overlap check (default 0).
241+
- `min_wires` — lower bound on N to avoid degenerate “non-wire-array" cases (default 3; set 6 for stronger symmetry).
242+
- `extra_span` — consider up to this many extra wires above the minimal requirement for better best_match search.
243+
- `nmin`,`nmax` — AWG range to consider (default 4/0 … 40).
244+
245+
Returns:
246+
- `min_wires` — minimal N (≥ min_wires) that satisfies both CSA & coverage & geometry;
247+
tie-break: smaller d, then smaller excess area.
248+
- `min_diam` — smallest d that can satisfy both constraints; for it, minimal feasible N;
249+
tie-break: smaller excess area.
250+
- `best_match`— among all feasible combos, area closest to A_req_m2; tie: smaller N, then smaller d.
251+
"""
252+
function make_screened(A_req_m2::Real, Dm_m::Real;
253+
alpha_deg::Real = 15.0, coverage_min_pct::Real = 85.0,
254+
gap_frac::Real = 0.0, min_wires::Int = 6, extra_span::Int = 8,
255+
nmin::Integer = -3, nmax::Integer = 40,
256+
coverage_max_pct::Real = 100.0, # NEW: cap coverage to a single layer
257+
max_overshoot_pct::Real = 10.0, # NEW: optional cap on A overshoot (∞ to disable)
258+
custom_diameters_m::AbstractVector{<:Real} = Float64[])
259+
260+
@assert 0.0 < coverage_min_pct <= 100.0
261+
@assert coverage_max_pct >= coverage_min_pct
262+
@assert max_overshoot_pct 0
263+
264+
# --- helpers ---
265+
function _max_wires_single_layer(Dm::Real, d::Real; gap_frac::Real = 0.0)
266+
s = d*(1 + gap_frac) / (Dm + d)
267+
if !(0.0 < s < 1.0)
268+
;
269+
return 0;
270+
end
271+
return max(0, floor(Int, pi / asin(s)))
272+
end
273+
_to_choice((N, d, Dm, A, cov, awg)) = ScreenPattern(N, d, Dm, 0.5*(Dm + d), A, cov, awg)
274+
275+
α = deg2rad(alpha_deg)
276+
= sin(α);
277+
@assert> 0
278+
279+
# AWG sizes + optional customs
280+
sizes = awg_sizes(nmin, nmax)
281+
for d in custom_diameters_m
282+
push!(sizes, ("custom($(round(d*1e3; digits=3)) mm)", Float64(d)))
283+
end
284+
@assert !isempty(sizes)
285+
286+
# Build candidates that satisfy BOTH constraints + geometry + coverage upper bound
287+
candidates = Tuple{Int, Float64, Float64, Float64, Float64, String}[] # (N,d,Dm,A,cov,awg)
288+
289+
for (awg, d) in sizes
290+
a1 = _wire_area(d)
291+
N_csa = ceil(Int, A_req_m2 / a1)
292+
N_cov = ceil(Int, (coverage_min_pct/100.0) * (pi*Dm_m*sα) / d)
293+
N_min = max(min_wires, N_csa, N_cov)
294+
N_max = _max_wires_single_layer(Dm_m, d; gap_frac = gap_frac)
295+
if N_max <= 0 || N_min > N_max
296+
continue
297+
end
298+
299+
# Try N from N_min upward but reject coverage > coverage_max_pct and overshoot > max_overshoot_pct
300+
upper = min(N_min + extra_span, N_max)
301+
@inbounds for N in N_min:upper
302+
A = N * a1
303+
cov = 100.0 * (N*d) / (pi*Dm_m*sα)
304+
if cov > coverage_max_pct
305+
break # for fixed d, cov grows linearly with N; larger N will also violate
306+
end
307+
if isfinite(max_overshoot_pct)
308+
if A > A_req_m2 * (1 + max_overshoot_pct/100)
309+
continue
310+
end
311+
end
312+
push!(candidates, (N, d, Dm_m, A, cov, awg))
313+
end
314+
end
315+
316+
@assert !isempty(candidates) "No feasible screen with given CSA, Dm, α, coverage bounds, and geometry."
317+
318+
# --- selectors (tweaked) ---
319+
320+
# 1) min_wires: minimize N; tie → minimize |A−Areq|; then smaller d
321+
keys_minN = [(N, abs(A - A_req_m2), d) for (N, d, _, A, _, _) in candidates]
322+
min_wires = _to_choice(candidates[argmin(keys_minN)])
323+
324+
# 2) min_diam: smallest d; for it, minimal |A−Areq|; then minimal N
325+
sort!(candidates, by = x -> (x[2], abs(x[4] - A_req_m2), x[1])) # (d asc, |ΔA| asc, N asc)
326+
min_diam = _to_choice(first(candidates))
327+
328+
# 3) best_match: closest area to A_req; tie → smaller N, then smaller d
329+
keys_best = [(abs(A - A_req_m2), N, d) for (N, d, _, A, _, _) in candidates]
330+
best_match = _to_choice(candidates[argmin(keys_best)])
331+
332+
return (; min_wires, min_diam, best_match)
333+
end
334+
335+
336+
end # module

0 commit comments

Comments
 (0)