|
| 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 | + sα = sin(α); |
| 277 | + @assert sα > 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