|
| 1 | +@testitem "ParametricBuilder(SystemBuilderSpec): combinatorics + value integrity" setup = |
| 2 | + [defaults] begin |
| 3 | + # ------------------------------------------------------------------------- |
| 4 | + # Shared setup (mirrors your example, but we toggle `unc` per testset) |
| 5 | + # ------------------------------------------------------------------------- |
| 6 | + using LineCableModels |
| 7 | + using LineCableModels.ParametricBuilder: |
| 8 | + CableBuilder, build, Conductor, Insulator, Material, Earth, SystemBuilder, at, |
| 9 | + make_stranded, make_screened, cardinality |
| 10 | + using LineCableModels.DataModel: trifoil_formation, LineCableSystem, CablePosition |
| 11 | + using Measurements |
| 12 | + |
| 13 | + # deterministic frequency grid |
| 14 | + f = 10.0 .^ range(0, stop = 6, length = 10) |
| 15 | + |
| 16 | + # Materials library |
| 17 | + materials = MaterialsLibrary(add_defaults = true) |
| 18 | + |
| 19 | + # deterministic geometry |
| 20 | + t_sct = 0.3e-3 |
| 21 | + t_sc_in = 0.000768 |
| 22 | + t_ins = 0.0083 |
| 23 | + t_sc_out = 0.000472 |
| 24 | + t_cut = 0.0001 |
| 25 | + w_cut = 10e-3 |
| 26 | + t_wbt = 0.00094 |
| 27 | + t_alt = 0.15e-3 |
| 28 | + t_pet = 0.05e-3 |
| 29 | + t_jac = 0.0034 |
| 30 | + |
| 31 | + # nominal data |
| 32 | + datasheet_info = NominalData( |
| 33 | + designation_code = "NA2XS(FL)2Y", |
| 34 | + U0 = 18.0, U = 30.0, |
| 35 | + conductor_cross_section = 1000.0, screen_cross_section = 35.0, |
| 36 | + resistance = 0.0291, capacitance = 0.39, inductance = 0.3, |
| 37 | + ) |
| 38 | + |
| 39 | + co_w = make_stranded(datasheet_info.conductor_cross_section).best_match |
| 40 | + co_n = co_w.layers |
| 41 | + co_d = co_w.wire_diameter_m |
| 42 | + co_lay = 13.0 |
| 43 | + |
| 44 | + sc_w = make_screened(datasheet_info.screen_cross_section, 55.3).best_match |
| 45 | + sc_n = sc_w.wires |
| 46 | + sc_d = sc_w.wire_diameter_m |
| 47 | + sc_lay = 10.0 |
| 48 | + |
| 49 | + # canonical parts builder (ρ/μ grids attached via `unc` in each testset) |
| 50 | + function make_parts( |
| 51 | + ms_al_uq, |
| 52 | + ms_cu, |
| 53 | + ms_pe, |
| 54 | + ms_xlpe, |
| 55 | + ms_sem1, |
| 56 | + ms_sem2, |
| 57 | + ms_polyacryl; |
| 58 | + unc = nothing, |
| 59 | + ) |
| 60 | + return [ |
| 61 | + # CORE conductors: stranded (central + rings) — uses PB coupling semantics. :contentReference[oaicite:1]{index=1} |
| 62 | + Conductor.Stranded( |
| 63 | + :core; |
| 64 | + layers = co_n, |
| 65 | + d = (co_d, unc), |
| 66 | + n = 6, |
| 67 | + lay = (co_lay, unc), |
| 68 | + mat = ms_al_uq, |
| 69 | + ), |
| 70 | + |
| 71 | + # CORE insulators |
| 72 | + Insulator.Semicon(:core; layers = 1, t = (t_sct, unc), mat = ms_polyacryl), |
| 73 | + Insulator.Semicon(:core; layers = 1, t = (t_sc_in, unc), mat = ms_sem1), |
| 74 | + Insulator.Tubular(:core; layers = 1, t = (t_ins, unc), mat = ms_xlpe), |
| 75 | + Insulator.Semicon(:core; layers = 1, t = t_sc_out, mat = ms_sem2), |
| 76 | + Insulator.Semicon(:core; layers = 1, t = t_sct, mat = ms_polyacryl), |
| 77 | + |
| 78 | + # SHEATH |
| 79 | + Conductor.Wires( |
| 80 | + :sheath; |
| 81 | + layers = 1, |
| 82 | + d = (sc_d, unc), |
| 83 | + n = sc_n, |
| 84 | + lay = (sc_lay, unc), |
| 85 | + mat = ms_cu, |
| 86 | + ), |
| 87 | + Conductor.Strip( |
| 88 | + :sheath; |
| 89 | + layers = 1, |
| 90 | + t = (t_cut, unc), |
| 91 | + w = (w_cut, unc), |
| 92 | + lay = (sc_lay, unc), |
| 93 | + mat = ms_cu, |
| 94 | + ), |
| 95 | + Insulator.Semicon(:sheath; layers = 1, t = t_wbt, mat = ms_polyacryl), |
| 96 | + |
| 97 | + # JACKET |
| 98 | + Conductor.Tubular(:jacket; layers = 1, t = t_alt, mat = ms_al_uq), |
| 99 | + Insulator.Tubular(:jacket; layers = 1, t = t_pet, mat = ms_pe), |
| 100 | + Insulator.Tubular(:jacket; layers = 1, t = t_jac, mat = ms_pe), |
| 101 | + ] |
| 102 | + end |
| 103 | + |
| 104 | + # formation anchors |
| 105 | + x0, y0 = 0.0, -1.0 |
| 106 | + xa, ya, xb, yb, xc, yc = trifoil_formation(x0, y0, 0.05) |
| 107 | + |
| 108 | + # convenience to build SystemBuilder with 3 positions |
| 109 | + function make_spec(cbs; dx = (0.0, nothing), dy = (0.0, nothing), |
| 110 | + length = (1000.0, nothing), |
| 111 | + temperature = (20.0, nothing), earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0)) |
| 112 | + positions = [ |
| 113 | + at( |
| 114 | + x = xa, |
| 115 | + y = ya, |
| 116 | + dx = dx, |
| 117 | + dy = dy, |
| 118 | + phases = (:core=>1, :sheath=>0, :jacket=>0), |
| 119 | + ), |
| 120 | + at( |
| 121 | + x = xb, |
| 122 | + y = yb, |
| 123 | + dx = dx, |
| 124 | + dy = dy, |
| 125 | + phases = (:core=>2, :sheath=>0, :jacket=>0), |
| 126 | + ), |
| 127 | + at( |
| 128 | + x = xc, |
| 129 | + y = yc, |
| 130 | + dx = dx, |
| 131 | + dy = dy, |
| 132 | + phases = (:core=>3, :sheath=>0, :jacket=>0), |
| 133 | + ), |
| 134 | + ] |
| 135 | + return SystemBuilder( |
| 136 | + "trifoil_case", |
| 137 | + cbs, |
| 138 | + positions; |
| 139 | + length = length, |
| 140 | + temperature = temperature, |
| 141 | + earth = earth, |
| 142 | + f = f, |
| 143 | + ) |
| 144 | + end |
| 145 | + |
| 146 | + # # helpers to collect all produced problems (channel consumer). |
| 147 | + # function collect_all(xs) |
| 148 | + # acc = Any[] |
| 149 | + # for x in xs |
| 150 | + # ; |
| 151 | + # push!(acc, x); |
| 152 | + # end |
| 153 | + # return acc |
| 154 | + # end |
| 155 | + |
| 156 | + # ──────────────────────────────────────────────────────────────────────── |
| 157 | + @testset "Baseline: fully deterministic (cardinality=1, value equality)" begin |
| 158 | + unc = nothing |
| 159 | + # materials |
| 160 | + ms_al_uq = Material(materials, "aluminum", rho = unc, mu_r = unc) |
| 161 | + ms_al = Material(materials, "aluminum") |
| 162 | + ms_cu = Material(materials, "copper") |
| 163 | + ms_pe = Material(materials, "pe") |
| 164 | + ms_xlpe = Material(materials, "xlpe") |
| 165 | + ms_sem1 = Material(materials, "semicon1") |
| 166 | + ms_sem2 = Material(materials, "semicon2") |
| 167 | + ms_polyacryl = Material(materials, "polyacrylate") |
| 168 | + |
| 169 | + parts = make_parts(ms_al_uq, ms_cu, ms_pe, ms_xlpe, ms_sem1, ms_sem2, ms_polyacryl; unc) |
| 170 | + cbs = CableBuilder("NA2XS(FL)2Y_1000", parts; nominal = datasheet_info) |
| 171 | + |
| 172 | + # CableBuilder cardinality should be 1 (all scalars). :contentReference[oaicite:2]{index=2} |
| 173 | + @test length(cbs) == 1 |
| 174 | + |
| 175 | + spec = make_spec(cbs; dx = (0.0, nothing), dy = (0.0, nothing), |
| 176 | + length = (1000.0, nothing), temperature = (20.0, nothing), |
| 177 | + earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0)) |
| 178 | + |
| 179 | + # SystemBuilder cardinality is designs × length × positions(dx,dy) × temperature × earth. :contentReference[oaicite:3]{index=3} |
| 180 | + @test length(spec) == 1 |
| 181 | + |
| 182 | + probs = collect(spec) |
| 183 | + @test length(probs) == 1 |
| 184 | + |
| 185 | + prob = probs[1] |
| 186 | + |
| 187 | + # Check system contents: 3 cables, phase mapping intact. :contentReference[oaicite:4]{index=4} |
| 188 | + sys = prob.system |
| 189 | + @test sys.num_cables == 3 |
| 190 | + # access positions |
| 191 | + let cps = sys.cables |
| 192 | + @test length(cps) == 3 |
| 193 | + # coords exact (deterministic) |
| 194 | + @test cps[1].horz == xa && cps[1].vert == ya |
| 195 | + @test cps[2].horz == xb && cps[2].vert == yb |
| 196 | + @test cps[3].horz == xc && cps[3].vert == yc |
| 197 | + # mapping |
| 198 | + @test cps[1].conn[1] == 1 && cps[1].conn[2] == 0 && |
| 199 | + cps[1].conn[3] == 0 |
| 200 | + @test cps[2].conn[1] == 2 |
| 201 | + @test cps[3].conn[1] == 3 |
| 202 | + end |
| 203 | + |
| 204 | + # frequencies are carried through, deterministic preview |
| 205 | + @test prob.frequencies == f |
| 206 | + end |
| 207 | + |
| 208 | + # ──────────────────────────────────────────────────────────────────────── |
| 209 | + @testset "Earth grids & %: ρ×εr×μr×t axes expand correctly" begin |
| 210 | + unc = nothing |
| 211 | + ms_al_uq = Material(materials, "aluminum", rho = unc, mu_r = unc) |
| 212 | + ms_cu = Material(materials, "copper") |
| 213 | + ms_pe = Material(materials, "pe") |
| 214 | + ms_xlpe = Material(materials, "xlpe") |
| 215 | + ms_sem1 = Material(materials, "semicon1") |
| 216 | + ms_sem2 = Material(materials, "semicon2") |
| 217 | + ms_polyacryl = Material(materials, "polyacrylate") |
| 218 | + |
| 219 | + parts = make_parts(ms_al_uq, ms_cu, ms_pe, ms_xlpe, ms_sem1, ms_sem2, ms_polyacryl; unc) |
| 220 | + cbs = CableBuilder("NA2XS(FL)2Y_1000", parts; nominal = datasheet_info) |
| 221 | + @test length(cbs) == 1 |
| 222 | + |
| 223 | + # ρ: (100, 500, 2) with 10% → values [100, 500] each ±10% (as Measurement) → 2 |
| 224 | + # εr: [5, 10] → 2 |
| 225 | + # μr: 1.0 → 1 |
| 226 | + # t: Inf → 1 |
| 227 | + earth = Earth( |
| 228 | + rho = ((100.0, 500.0, 2), (10.0)), |
| 229 | + eps_r = [5.0, 10.0], |
| 230 | + mu_r = 1.0, |
| 231 | + t = Inf, |
| 232 | + ) |
| 233 | + |
| 234 | + spec = make_spec(cbs; earth = earth) |
| 235 | + # total = 1 (designs) × 1 (len) × (1×1)^3 (positions) × 1 (T) × (2×2×1×1) = 4 |
| 236 | + @test length(spec) == 4 |
| 237 | + |
| 238 | + probs = collect(spec) |
| 239 | + @test length(probs) == 4 |
| 240 | + |
| 241 | + # Verify EarthModel inputs carry Measurement when % is present. |
| 242 | + for pr in probs |
| 243 | + em = pr.earth_props |
| 244 | + # Check first layer nominal scalars hold Measurement type for rho (base value) when % applied |
| 245 | + # (Earth layer API stores base_* as T; implementation ensures promotion via resolve_T). |
| 246 | + lay1 = em.layers[1] |
| 247 | + @test lay1.base_rho_g isa Measurement |
| 248 | + @test lay1.base_epsr_g isa Measurement |
| 249 | + @test lay1.base_mur_g isa Measurement |
| 250 | + end |
| 251 | + end |
| 252 | + |
| 253 | + # ──────────────────────────────────────────────────────────────────────── |
| 254 | + @testset "System knobs: length & temperature grids + % expansion" begin |
| 255 | + unc = (0.0, 10.0, 2) # use 0% and 10% (length 2) |
| 256 | + ms_al_uq = Material(materials, "aluminum", rho = nothing, mu_r = nothing) |
| 257 | + ms_cu = Material(materials, "copper") |
| 258 | + ms_pe = Material(materials, "pe") |
| 259 | + ms_xlpe = Material(materials, "xlpe") |
| 260 | + ms_sem1 = Material(materials, "semicon1") |
| 261 | + ms_sem2 = Material(materials, "semicon2") |
| 262 | + ms_polyacryl = Material(materials, "polyacrylate") |
| 263 | + |
| 264 | + parts = make_parts(ms_al_uq, ms_cu, ms_pe, ms_xlpe, ms_sem1, ms_sem2, ms_polyacryl; unc = nothing) |
| 265 | + cbs = CableBuilder("NA2XS(FL)2Y_1000", parts; nominal = datasheet_info) |
| 266 | + @test length(cbs) == 1 |
| 267 | + |
| 268 | + # length: (1000, (0,10,2)) → [1000, 1000±10%] (2 choices) |
| 269 | + # temp: (20, (0,10,2)) → [20, 20±10%] (2 choices) |
| 270 | + spec = make_spec(cbs; |
| 271 | + length = (1000.0, unc), |
| 272 | + temperature = (20.0, unc), |
| 273 | + earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0), |
| 274 | + ) |
| 275 | + |
| 276 | + # total = 1 × 2 × 1 × 2 × 1 = 4 |
| 277 | + @test length(spec) == 4 |
| 278 | + |
| 279 | + probs = collect(spec) |
| 280 | + @test length(probs) == 4 |
| 281 | + |
| 282 | + # Check at least one problem has Measurement length & temperature (promotion path) |
| 283 | + # Pull system line_length via internal field (LineCableSystem constructor stores it as T). :contentReference[oaicite:7]{index=7} |
| 284 | + found_meas = false |
| 285 | + for pr in probs |
| 286 | + sys = pr.system |
| 287 | + if sys.line_length isa Measurement && pr.temperature isa Measurement |
| 288 | + found_meas = true |
| 289 | + break |
| 290 | + end |
| 291 | + end |
| 292 | + @test found_meas |
| 293 | + end |
| 294 | + |
| 295 | + # ──────────────────────────────────────────────────────────────────────── |
| 296 | + @testset "Position axes: displacement grids & anchor % semantics" begin |
| 297 | + ms_al_uq = Material(materials, "aluminum", rho = nothing, mu_r = nothing) |
| 298 | + ms_cu = Material(materials, "copper") |
| 299 | + ms_pe = Material(materials, "pe") |
| 300 | + ms_xlpe = Material(materials, "xlpe") |
| 301 | + ms_sem1 = Material(materials, "semicon1") |
| 302 | + ms_sem2 = Material(materials, "semicon2") |
| 303 | + ms_polyacryl = Material(materials, "polyacrylate") |
| 304 | + |
| 305 | + parts = make_parts(ms_al_uq, ms_cu, ms_pe, ms_xlpe, ms_sem1, ms_sem2, ms_polyacryl; unc = nothing) |
| 306 | + cbs = CableBuilder("NA2XS(FL)2Y_1000", parts; nominal = datasheet_info) |
| 307 | + @test length(cbs) == 1 |
| 308 | + |
| 309 | + # Case A: dx sweep, dy deterministic |
| 310 | + specA = make_spec(cbs; |
| 311 | + dx = (-0.01, 0.01, 3), # => [-0.01, 0.0, 0.01] about anchor |
| 312 | + dy = (0.0, nothing), |
| 313 | + earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0), |
| 314 | + ) |
| 315 | + # total = 1 × 1 × (3×1)^3 × 1 × 1 = 27 |
| 316 | + @test length(specA) == 27 |
| 317 | + |
| 318 | + # Validate actual x coordinates hit the expected triplet on p1 |
| 319 | + xs = Float64[] |
| 320 | + for pr in specA |
| 321 | + push!(xs, pr.system.cables[1].horz - xa) |
| 322 | + end |
| 323 | + @test sort!(unique(round.(xs; digits = 5))) == [-0.01, 0.0, 0.01] |
| 324 | + |
| 325 | + # Case B: anchor % (no displacement sweep) — (nothing, pct) on dx. :contentReference[oaicite:8]{index=8} |
| 326 | + specB = make_spec(cbs; |
| 327 | + dx = (nothing, (0.0, 10.0, 2)), # anchors become [xa, measurement(xa, 10%)] |
| 328 | + dy = (0.0, nothing), |
| 329 | + earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0), |
| 330 | + ) |
| 331 | + # total = 1 × 1 × (2×1)^3 × 1 × 1 = 8 |
| 332 | + @test length(specB) == 8 |
| 333 | + |
| 334 | + # Confirm produced anchors include a Measurement with std(|xa|*10%) |
| 335 | + has_anchor_meas = any( |
| 336 | + begin |
| 337 | + x = pr.system.cables[1].horz |
| 338 | + x isa Measurement && |
| 339 | + isapprox(uncertainty(x), abs(xa)*0.10; atol = eps()) # std = |xa|*10% |
| 340 | + end for pr in specB |
| 341 | + ) |
| 342 | + @test has_anchor_meas |
| 343 | + end |
| 344 | + |
| 345 | + # ──────────────────────────────────────────────────────────────────────── |
| 346 | + @testset "Material % propagates into designs (ρ with % → Measurement in design tree)" begin |
| 347 | + # Attach % to aluminum ρ and μ; all geometry deterministic |
| 348 | + ms_al_uq = Material(materials, "aluminum", rho = (1.0, (0.0, 5.0, 2)), mu_r = (1.0, (0.0, 5.0, 2))) |
| 349 | + ms_cu = Material(materials, "copper") |
| 350 | + ms_pe = Material(materials, "pe") |
| 351 | + ms_xlpe = Material(materials, "xlpe") |
| 352 | + ms_sem1 = Material(materials, "semicon1") |
| 353 | + ms_sem2 = Material(materials, "semicon2") |
| 354 | + ms_polyacryl = Material(materials, "polyacrylate") |
| 355 | + |
| 356 | + parts = make_parts(ms_al_uq, ms_cu, ms_pe, ms_xlpe, ms_sem1, ms_sem2, ms_polyacryl; unc = nothing) |
| 357 | + cbs = CableBuilder("NA2XS(FL)2Y_1000", parts; nominal = datasheet_info) |
| 358 | + |
| 359 | + # designs = 2×2 from ρ(%) × μ(%) on the same MaterialSpec (coupled across parts when equal tuples). :contentReference[oaicite:9]{index=9} |
| 360 | + @test length(cbs) == cardinality(cbs) |
| 361 | + |
| 362 | + # System with deterministic system knobs |
| 363 | + spec = make_spec(cbs; earth = Earth(rho = 100.0, eps_r = 10.0, mu_r = 1.0)) |
| 364 | + probs = collect(spec) |
| 365 | + |
| 366 | + # Pick one problem; inspect the first cable's design tree for Measurement presence. |
| 367 | + pr = probs[1] |
| 368 | + des = pr.system.cables[1].design_data # the concrete CableDesign |
| 369 | + # Assert that somewhere in the conductor effective material we see Measurement (ρ or μ). |
| 370 | + # We traverse last component conductor props or any material-like fields that match ρ/μ semantics. |
| 371 | + found_meas = false |
| 372 | + for comp in des.components |
| 373 | + # effective conductor/insulator props are Materials.Material |
| 374 | + if hasproperty(comp, :conductor_props) |
| 375 | + mp = getfield(comp, :conductor_props) |
| 376 | + if (getfield(mp, :rho) isa Measurement) || |
| 377 | + (getfield(mp, :mu_r) isa Measurement) || |
| 378 | + (getfield(mp, :T0) isa Measurement) || |
| 379 | + (getfield(mp, :alpha) isa Measurement) || |
| 380 | + (getfield(mp, :eps_r) isa Measurement) |
| 381 | + found_meas = true |
| 382 | + break |
| 383 | + end |
| 384 | + end |
| 385 | + end |
| 386 | + @test found_meas |
| 387 | + end |
| 388 | +end |
0 commit comments