Skip to content

Commit af4f4f7

Browse files
test(parametricbuilder): add unit test scaffolding for ParametricBuilder
1 parent 94540b3 commit af4f4f7

1 file changed

Lines changed: 388 additions & 0 deletions

File tree

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
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

Comments
 (0)