Skip to content

Commit b056f84

Browse files
feat(parametricbuilder): add base, cablebuilderspec, materialspec, and systembuilderspec modules
1 parent 175f0e4 commit b056f84

4 files changed

Lines changed: 1074 additions & 0 deletions

File tree

src/parametricbuilder/base.jl

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
Base.IteratorEltype(::Type{CableBuilderSpec}) = Base.HasEltype()
2+
Base.eltype(::Type{CableBuilderSpec}) = DataModel.CableDesign
3+
Base.IteratorSize(::Type{CableBuilderSpec}) = Base.SizeUnknown()
4+
5+
function Base.iterate(cbs::CableBuilderSpec)
6+
ch = iterate_designs(cbs)
7+
try
8+
d = take!(ch)
9+
return (d, ch)
10+
catch
11+
return nothing
12+
end
13+
end
14+
15+
function Base.iterate(::CableBuilderSpec, ch::Channel)
16+
try
17+
d = take!(ch)
18+
return (d, ch)
19+
catch
20+
return nothing
21+
end
22+
end
23+
24+
25+
# how many choices are in a "range-like" thing
26+
_choice_count(x) =
27+
x === nothing ? 1 :
28+
(x isa Tuple && length(x) == 2) ? _choice_count(x[1]) * _choice_count(x[2]) :
29+
(x isa AbstractVector) ? length(x) :
30+
(x isa Tuple && length(x) == 3) ? last(x) : 1
31+
32+
# count choices for a MaterialSpec (rho/eps/mu/T/α product)
33+
_choice_count(ms::MaterialSpec) = length(_make_range(ms))
34+
35+
# args: each entry can be scalar | vector | (lo,hi,n) | (value_spec, pct_spec)
36+
_arg_choice_count(a) =
37+
(a isa Tuple && length(a) == 2) ? (_choice_count(a[1]) * _choice_count(a[2])) :
38+
_choice_count(a)
39+
40+
_args_choice_count(args::Tuple) =
41+
isempty(args) ? 1 : prod(_arg_choice_count(a) for a in args)
42+
43+
function cardinality(cbs::CableBuilderSpec)
44+
comp_names = unique(p.component for p in cbs.parts)
45+
by_comp = Dict{Symbol, Vector{PartSpec}}()
46+
for p in cbs.parts
47+
get!(by_comp, p.component, PartSpec[]) |> v -> push!(v, p)
48+
end
49+
50+
total = 1
51+
for cname in comp_names
52+
ps = by_comp[cname]
53+
cond = [p for p in ps if p.part_type <: DataModel.AbstractConductorPart]
54+
insu = [p for p in ps if p.part_type <: DataModel.AbstractInsulatorPart]
55+
isempty(cond) && error("component '$cname' has no conductors")
56+
isempty(insu) && error("component '$cname' has no insulators")
57+
58+
# first conductor axes
59+
p1c = cond[1]
60+
c_dim = _choice_count(p1c.dim[1]) * _choice_count(p1c.dim[2])
61+
c_args = _args_choice_count(p1c.args)
62+
c_mat = _choice_count(p1c.material)
63+
64+
# uncoupled extras from later conductors (couple when tuples compare equal)
65+
for pc in cond[2:end]
66+
pc_dim_same = (pc.dim == p1c.dim)
67+
pc_args_same = (pc.args == p1c.args)
68+
pc_mat_same = (pc.material == p1c.material)
69+
70+
c_dim *= pc_dim_same ? 1 : (_choice_count(pc.dim[1]) * _choice_count(pc.dim[2]))
71+
c_args *= pc_args_same ? 1 : _args_choice_count(pc.args)
72+
c_mat *= pc_mat_same ? 1 : _choice_count(pc.material)
73+
end
74+
cond_factor = c_dim * c_args * c_mat
75+
76+
# first insulator axes
77+
p1i = insu[1]
78+
i_dim = _choice_count(p1i.dim[1]) * _choice_count(p1i.dim[2])
79+
i_args = _args_choice_count(p1i.args)
80+
i_mat = _choice_count(p1i.material)
81+
82+
for pi in insu[2:end]
83+
pi_dim_same = (pi.dim == p1i.dim)
84+
pi_args_same = (pi.args == p1i.args)
85+
pi_mat_same = (pi.material == p1i.material)
86+
87+
i_dim *= pi_dim_same ? 1 : (_choice_count(pi.dim[1]) * _choice_count(pi.dim[2]))
88+
i_args *= pi_args_same ? 1 : _args_choice_count(pi.args)
89+
i_mat *= pi_mat_same ? 1 : _choice_count(pi.material)
90+
end
91+
insu_factor = i_dim * i_args * i_mat
92+
93+
total *= cond_factor * insu_factor
94+
end
95+
return total
96+
end
97+
98+
Base.length(cbs::CableBuilderSpec) = cardinality(cbs)
99+
100+
function Base.show(io::IO, ::MIME"text/plain", cbs::CableBuilderSpec)
101+
comp_names = unique(p.component for p in cbs.parts)
102+
by_comp = Dict{Symbol, Vector{PartSpec}}()
103+
for p in cbs.parts
104+
get!(by_comp, p.component, PartSpec[]) |> v -> push!(v, p)
105+
end
106+
107+
println(io, "CableBuilderSpec(\"", cbs.cable_id, "\")")
108+
println(io, " components: ", join(string.(comp_names), ", "))
109+
110+
total = 1
111+
for cname in comp_names
112+
ps = by_comp[cname]
113+
cond = [p for p in ps if p.part_type <: DataModel.AbstractConductorPart]
114+
insu = [p for p in ps if p.part_type <: DataModel.AbstractInsulatorPart]
115+
isempty(cond) && error("component '$cname' has no conductors")
116+
isempty(insu) && error("component '$cname' has no insulators")
117+
118+
# conductors: couple to first when tuples compare equal
119+
p1c = cond[1]
120+
c_dim = _choice_count(p1c.dim[1]) * _choice_count(p1c.dim[2])
121+
c_args = _args_choice_count(p1c.args)
122+
c_mat = _choice_count(p1c.material)
123+
for pc in cond[2:end]
124+
c_dim *= (pc.dim == p1c.dim) ? 1 : (_choice_count(pc.dim[1]) * _choice_count(pc.dim[2]))
125+
c_args *= (pc.args == p1c.args) ? 1 : _args_choice_count(pc.args)
126+
c_mat *= (pc.material == p1c.material) ? 1 : _choice_count(pc.material)
127+
end
128+
cond_factor = c_dim * c_args * c_mat
129+
130+
# insulators: same coupling rule vs first insulator
131+
p1i = insu[1]
132+
i_dim = _choice_count(p1i.dim[1]) * _choice_count(p1i.dim[2])
133+
i_args = _args_choice_count(p1i.args)
134+
i_mat = _choice_count(p1i.material)
135+
for pi in insu[2:end]
136+
i_dim *= (pi.dim == p1i.dim) ? 1 : (_choice_count(pi.dim[1]) * _choice_count(pi.dim[2]))
137+
i_args *= (pi.args == p1i.args) ? 1 : _args_choice_count(pi.args)
138+
i_mat *= (pi.material == p1i.material) ? 1 : _choice_count(pi.material)
139+
end
140+
insu_factor = i_dim * i_args * i_mat
141+
142+
fac = cond_factor * insu_factor
143+
total *= fac
144+
print(io, "", cname, ": ")
145+
print(io, "cond(dim=", c_dim, ", args=", c_args, ", mat=", c_mat, "); ")
146+
println(io, "insu(dim=", i_dim, ", args=", i_args, ", mat=", i_mat, ") ⇒ ×", fac)
147+
end
148+
149+
println(io, " cardinality: ", total)
150+
if cbs.nominal !== nothing
151+
println(io, " nominal: ", typeof(cbs.nominal))
152+
end
153+
end
154+
155+
156+
function show_trace(tr::DesignTrace)
157+
println("Design: ", tr.cable_id)
158+
for comp in tr.components
159+
println(" Component: ", comp.name)
160+
for c in comp.choices
161+
mat = c.mat
162+
println(" [", c.role, "] ", c.T,
163+
" layers=", c.layers,
164+
" dim=", c.dim,
165+
" args=", c.args,
166+
" ρ=", mat.rho, " εr=", mat.eps_r, " μr=", mat.mu_r)
167+
end
168+
end
169+
end
170+
171+
Base.show(io::IO, ::MIME"text/plain", tr::DesignTrace) = show_trace(tr)
172+
173+
# == Choice-count helpers (reusing ParametricBuilder counting) ==
174+
_position_choice_count(p::_Pos) = _choice_count(p.dx) * _choice_count(p.dy)
175+
176+
_earth_choice_count(e::EarthSpec) =
177+
_choice_count(e.rho) * _choice_count(e.eps_r) * _choice_count(e.mu_r) *
178+
_choice_count(e.t)
179+
180+
# == Public cardinality API ==
181+
function cardinality(spec::SystemBuilderSpec)
182+
pos_prod =
183+
isempty(spec.positions) ? 0 :
184+
prod(_position_choice_count(p) for p in spec.positions)
185+
pos_prod == 0 && return 0
186+
return cardinality(spec.builder) *
187+
_choice_count(spec.length) *
188+
pos_prod *
189+
_choice_count(spec.temperature) *
190+
_earth_choice_count(spec.earth)
191+
end
192+
193+
Base.length(spec::SystemBuilderSpec) = cardinality(spec)
194+
195+
# == Iterator over fully formed LineParametersProblem (skips overlaps silently) ==
196+
function Base.iterate(spec::SystemBuilderSpec)
197+
ch = iterate_problems(spec)
198+
try
199+
x = take!(ch);
200+
return (x, ch)
201+
catch
202+
return nothing
203+
end
204+
end
205+
206+
function Base.iterate(::SystemBuilderSpec, ch::Channel{LineParametersProblem})
207+
try
208+
x = take!(ch);
209+
return (x, ch)
210+
catch
211+
return nothing
212+
end
213+
end
214+
215+
Base.IteratorEltype(::Type{SystemBuilderSpec}) = Base.HasEltype()
216+
Base.eltype(::Type{SystemBuilderSpec}) = LineParametersProblem
217+
Base.IteratorSize(::Type{SystemBuilderSpec}) = Base.SizeUnknown()
218+
219+
# == Terse pretty printer (because why the hell not?) ==
220+
# show at most `limit` values: "v1, v2, ..., vN (N=total)"
221+
_fmt_vals(vals; limit = 8) = begin
222+
v = collect(vals)
223+
n = length(v)
224+
if n == 0
225+
""
226+
elseif n <= limit
227+
string(join(v, ", "))
228+
else
229+
string(join(v[1:limit], ", "), ", … (N=", n, ")")
230+
end
231+
end
232+
233+
# expand one knob (your (valuespec,pct) grammar) into concrete values
234+
_vals_pair(p) = collect(_expand_pair(p)) # :contentReference[oaicite:2]{index=2}
235+
# axis around anchor (handles (nothing, pct) → uncertain anchor)
236+
_vals_axis(anchor, dspec) = collect(_axis(anchor, dspec)) # :contentReference[oaicite:3]{index=3}
237+
238+
# deterministic freq summary: list if tiny, else min..max (N)
239+
_fmt_freqs(f::AbstractVector) =
240+
length(f) 8 ? join(f, ", ") :
241+
string(first(f), "", last(f), " (N=", length(f), ")")
242+
243+
# stable, human order for phases: core,sheath,jacket first if present, then alphabetical
244+
function _fmt_map(conn::Dict{String, Int})
245+
prio = Dict("core"=>1, "sheath"=>2, "jacket"=>3)
246+
ks = collect(keys(conn))
247+
sort!(ks, by = k -> (get(prio, k, 1000), k))
248+
# FIX: use getindex, not get
249+
vs = getindex.(Ref(conn), ks) # or: map(k -> conn[k], ks)
250+
return join(string.(ks, "=>", vs), ", ")
251+
end
252+
253+
function Base.show(io::IO, ::MIME"text/plain", spec::SystemBuilderSpec)
254+
println(io, "SystemBuilder(\"", spec.system_id, "\")")
255+
println(io, " designs × = ", cardinality(spec.builder)) # we don’t explode details here; PB has its own show. :contentReference[oaicite:4]{index=4}
256+
257+
# positions block
258+
println(io, " positions = ", length(spec.positions))
259+
for (i, p) in enumerate(spec.positions)
260+
dxvals = _vals_axis(p.x0, p.dx)
261+
dyvals = _vals_axis(p.y0, p.dy)
262+
println(io, " • p", i, " x: ", _fmt_vals(dxvals), " y: ", _fmt_vals(dyvals),
263+
" phases {", _fmt_map(p.conn), "}")
264+
end
265+
266+
# system scalars
267+
println(io, " length = ", _fmt_vals(_vals_pair(spec.length)))
268+
println(io, " temp = ", _fmt_vals(_vals_pair(spec.temperature)))
269+
270+
# earth knobs (each axis separately)
271+
println(io, " earth:")
272+
println(io, " ρ = ", _fmt_vals(_vals_pair(spec.earth.rho)))
273+
println(io, " εr = ", _fmt_vals(_vals_pair(spec.earth.eps_r)))
274+
println(io, " μr = ", _fmt_vals(_vals_pair(spec.earth.mu_r)))
275+
println(io, " t = ", _fmt_vals(_vals_pair(spec.earth.t)))
276+
277+
# frequencies (deterministic vector coming from the user/spec)
278+
if hasfield(SystemBuilderSpec, :frequencies) && !isempty(getfield(spec, :frequencies))
279+
f = getfield(spec, :frequencies)
280+
println(io, " f = ", _fmt_freqs(f))
281+
end
282+
283+
println(io, " ⇒ total cardinality (upper bound): ", cardinality(spec))
284+
end

0 commit comments

Comments
 (0)