Skip to content

Commit 430228d

Browse files
feat(engine): add dataframe module
1 parent ca4d695 commit 430228d

1 file changed

Lines changed: 275 additions & 0 deletions

File tree

src/engine/dataframe.jl

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import DataFrames: DataFrame, metadata!
2+
3+
const _LP_FREQ_COL = :frequency
4+
5+
const _SERIES_DUMMY = SeriesImpedance(zeros(ComplexF64, 1, 1, 1))
6+
const _SHUNT_DUMMY = ShuntAdmittance(zeros(ComplexF64, 1, 1, 1))
7+
8+
_freq_units_label(unit::Symbol) = unit_text(unit, "Hz")
9+
10+
_length_unit(per::Symbol) = per
11+
12+
function _column_name(meta::ComponentMetadata)
13+
component = meta.component
14+
if component in (:resistance, :inductance, :conductance, :capacitance)
15+
return Symbol(meta.symbol)
16+
else
17+
return Symbol(component)
18+
end
19+
end
20+
21+
function _normalize_quantity_units(units)
22+
return normalize_quantity_units(units)
23+
end
24+
25+
function _frequency_vector(obj, freqs)
26+
if freqs === nothing
27+
return float.(collect(axes(obj, 3)))
28+
else
29+
f = collect(freqs)
30+
length(f) == size(obj, 3) ||
31+
Base.error("Frequency vector length does not match object samples")
32+
return float.(f)
33+
end
34+
end
35+
36+
function _frequency_vector(slice::AbstractVector, freqs::AbstractVector)
37+
f = collect(freqs)
38+
length(f) == length(slice) ||
39+
Base.error("Frequency vector length must match slice length")
40+
return float.(f)
41+
end
42+
43+
function _build_dataframe(
44+
slice,
45+
freq_raw::Vector{<:Real},
46+
comps::Vector{ComponentMetadata},
47+
units::Dict{Symbol, Symbol},
48+
length_unit::Symbol,
49+
freq_unit::Symbol,
50+
tol::Real,
51+
)
52+
freq_scale = frequency_scale(freq_unit)
53+
freq_values = freq_raw .* freq_scale
54+
unit_map = Dict{Symbol, String}(
55+
_LP_FREQ_COL => _freq_units_label(freq_unit),
56+
)
57+
df = DataFrame(_LP_FREQ_COL => freq_values)
58+
for meta in comps
59+
q_prefix = resolve_quantity_prefix(meta.quantity, units)
60+
scale = quantity_scale(q_prefix)
61+
l_scale = meta.unit.per_length ? length_scale(length_unit) : 1.0
62+
raw_vals = component_values(meta.component, slice, freq_raw)
63+
col_data = map(raw_vals) do x
64+
_clip_field(x * (scale * l_scale), tol)
65+
end
66+
col_name = _column_name(meta)
67+
df[!, col_name] = col_data
68+
unit_map[col_name] =
69+
composite_unit(q_prefix, meta.unit.symbol, meta.unit.per_length, length_unit)
70+
end
71+
metadata!(df, "units", unit_map, style = :note)
72+
return df
73+
end
74+
75+
function _matrix_dataframes(
76+
obj,
77+
freq_raw::Vector{<:Real},
78+
comps::Vector{ComponentMetadata},
79+
units::Dict{Symbol, Symbol},
80+
length_unit::Symbol,
81+
freq_unit::Symbol,
82+
tol::Real,
83+
)
84+
nx, ny, _ = size(obj.values)
85+
result = Matrix{DataFrame}(undef, nx, ny)
86+
for i in 1:nx, j in 1:ny
87+
slice = @view obj.values[i, j, :]
88+
result[i, j] = _build_dataframe(
89+
slice,
90+
freq_raw,
91+
comps,
92+
units,
93+
length_unit,
94+
freq_unit,
95+
tol,
96+
)
97+
end
98+
return result
99+
end
100+
101+
function _slice_dataframe(
102+
slice::AbstractVector,
103+
kind::Symbol,
104+
freq_raw::Vector{<:Real},
105+
mode::Symbol,
106+
coord::Symbol,
107+
units::Dict{Symbol, Symbol},
108+
length_unit::Symbol,
109+
freq_unit::Symbol,
110+
tol::Real,
111+
)
112+
resolved_kind = _resolve_kind(slice, kind, tol)
113+
comps =
114+
resolved_kind == :series_impedance ?
115+
components_for(_SERIES_DUMMY, mode, coord) :
116+
components_for(_SHUNT_DUMMY, mode, coord)
117+
return _build_dataframe(
118+
slice,
119+
freq_raw,
120+
comps,
121+
units,
122+
length_unit,
123+
freq_unit,
124+
tol,
125+
)
126+
end
127+
128+
"""
129+
DataFrame(Z::SeriesImpedance; freqs=nothing, mode=:RLCG, coord=:cart,
130+
freq_unit=:base, length_unit=:kilo, quantity_units=nothing,
131+
tol=sqrt(eps(Float64)))
132+
133+
Convert the entries of a `SeriesImpedance` object into per-element `DataFrame`s
134+
indexed by frequency. Returns an `n×n` matrix of `DataFrame`s whose rows
135+
correspond to conductor indices.
136+
137+
- `freqs`: explicit frequency vector in Hz. Defaults to `1:length(freq axis)`.
138+
- `mode`: `:RLCG` (default) or `:ZY`. For `:ZY`, `coord` may be `:cart` or `:polar`.
139+
- `length_unit`: metric prefix for per-length units (e.g. `:kilo` ⇒ per km).
140+
- `quantity_units`: optional overrides for the quantity metric prefixes used in each column.
141+
- `tol`: absolute tolerance used to zero-out tiny numerical noise.
142+
"""
143+
function DataFrame(
144+
Z::SeriesImpedance;
145+
freqs = nothing,
146+
mode::Symbol = :RLCG,
147+
coord::Symbol = :cart,
148+
freq_unit::Symbol = :base,
149+
length_unit::Symbol = :kilo,
150+
quantity_units = nothing,
151+
tol::Real = sqrt(eps(Float64)),
152+
)
153+
freq_raw = _frequency_vector(Z, freqs)
154+
units = _normalize_quantity_units(quantity_units)
155+
comps = components_for(Z, mode, coord)
156+
return _matrix_dataframes(
157+
Z,
158+
freq_raw,
159+
comps,
160+
units,
161+
length_unit,
162+
freq_unit,
163+
float(tol),
164+
)
165+
end
166+
167+
"""
168+
DataFrame(Y::ShuntAdmittance; freqs=nothing, mode=:RLCG, coord=:cart,
169+
freq_unit=:base, length_unit=:kilo, quantity_units=nothing,
170+
tol=sqrt(eps(Float64)))
171+
172+
Convert the entries of a `ShuntAdmittance` object into per-element `DataFrame`s
173+
indexed by frequency. Returns an `n×n` matrix of `DataFrame`s.
174+
175+
Keyword arguments mirror those of `DataFrame(::SeriesImpedance)`.
176+
"""
177+
function DataFrame(
178+
Y::ShuntAdmittance;
179+
freqs = nothing,
180+
mode::Symbol = :RLCG,
181+
coord::Symbol = :cart,
182+
freq_unit::Symbol = :base,
183+
length_unit::Symbol = :kilo,
184+
quantity_units = nothing,
185+
tol::Real = sqrt(eps(Float64)),
186+
)
187+
freq_raw = _frequency_vector(Y, freqs)
188+
units = _normalize_quantity_units(quantity_units)
189+
comps = components_for(Y, mode, coord)
190+
return _matrix_dataframes(
191+
Y,
192+
freq_raw,
193+
comps,
194+
units,
195+
length_unit,
196+
freq_unit,
197+
float(tol),
198+
)
199+
end
200+
201+
"""
202+
DataFrame(slice::AbstractVector; freqs, kind=:auto, mode=:RLCG,
203+
coord=:cart, freq_unit=:base, length_unit=:kilo, quantity_units=nothing,
204+
tol=sqrt(eps(Float64)))
205+
206+
Create a frequency-indexed `DataFrame` for a single impedance/admittance element
207+
provided as a vector over frequency samples.
208+
209+
- `freqs`: frequency vector in Hz (must be supplied).
210+
- `kind`: `:series_impedance`, `:shunt_admittance`, or `:auto` (default). `:auto`
211+
attempts to infer the correct type using the magnitude of the real part.
212+
- Remaining keywords follow the array methods.
213+
"""
214+
function DataFrame(
215+
slice::AbstractVector;
216+
freqs::AbstractVector,
217+
kind::Symbol = :auto,
218+
mode::Symbol = :RLCG,
219+
coord::Symbol = :cart,
220+
freq_unit::Symbol = :base,
221+
length_unit::Symbol = :kilo,
222+
quantity_units = nothing,
223+
tol::Real = sqrt(eps(Float64)),
224+
)
225+
freq_raw = _frequency_vector(slice, freqs)
226+
units = _normalize_quantity_units(quantity_units)
227+
kind (:series_impedance, :shunt_admittance, :auto) ||
228+
Base.error("Unsupported kind $(kind). Use :series_impedance, :shunt_admittance, or :auto.")
229+
return _slice_dataframe(
230+
slice,
231+
kind,
232+
freq_raw,
233+
mode,
234+
coord,
235+
units,
236+
length_unit,
237+
freq_unit,
238+
float(tol),
239+
)
240+
end
241+
242+
function _clip_field(x::Real, tol)
243+
isfinite(x) || return x
244+
return _clip(x, tol)
245+
end
246+
247+
function _clip_field(m::Measurements.Measurement, tol)
248+
v = _clip(value(m), tol)
249+
u = _clip(uncertainty(m), tol)
250+
return Measurements.measurement(v, u)
251+
end
252+
253+
_clip_field(x, _) = x
254+
255+
function _resolve_kind(slice, kind::Symbol, tol::Real)
256+
kind != :auto && return kind
257+
max_real = 0.0
258+
max_imag = 0.0
259+
for z in slice
260+
r = real(z)
261+
i = imag(z)
262+
val_r = _scalar_abs(r)
263+
val_i = _scalar_abs(i)
264+
isfinite(val_r) && val_r > max_real && (max_real = val_r)
265+
isfinite(val_i) && val_i > max_imag && (max_imag = val_i)
266+
end
267+
if max_real <= tol && max_imag > tol
268+
return :shunt_admittance
269+
else
270+
return :series_impedance
271+
end
272+
end
273+
274+
_scalar_abs(x::Real) = abs(x)
275+
_scalar_abs(m::Measurements.Measurement) = abs(value(m))

0 commit comments

Comments
 (0)