|
| 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