|
| 1 | +struct eigLM <: AbstractTransformFormulation |
| 2 | + tol::BASE_FLOAT |
| 3 | +end |
| 4 | + |
| 5 | +const LMTOL = 1e-8 # default LM tolerance |
| 6 | + |
| 7 | +# Convenient ctor |
| 8 | +eigLM(; tol::U = U(LMTOL)) where {U <: REALSCALAR} = |
| 9 | + eigLM(BASE_FLOAT(tol)) # explicit downcast to Float64 |
| 10 | + |
| 11 | +get_description( |
| 12 | + ::eigLM, |
| 13 | +) = "Levenberg–Marquardt (frequency-tracked eigen decomposition)" |
| 14 | + |
| 15 | +""" |
| 16 | +$(TYPEDSIGNATURES) |
| 17 | +
|
| 18 | +Apply Levenberg–Marquardt modal decomposition to a frequency-dependent |
| 19 | +[`LineParameters`](@ref) object. Returns the (frequency-tracked) modal |
| 20 | +transformation matrices and a **modal-domain** `LineParameters` holding the |
| 21 | +**modal characteristic** impedance/admittance (diagonal per frequency). |
| 22 | +
|
| 23 | +# Arguments |
| 24 | +
|
| 25 | +- `lp`: Phase-domain line parameters (series `Z`, shunt `Y`, and `f`). |
| 26 | +- `f::eigLM`: Functor with solver tolerance. |
| 27 | +
|
| 28 | +# Returns |
| 29 | +
|
| 30 | +- `Tv`: Transformation matrices `T(•)` as a 3-tensor `n×n×nfreq` (columns are modes). |
| 31 | +- `LineParameters`: Modal-domain characteristic parameters: |
| 32 | + - `Z.values[:,:,k] = Diagonal(Zcₖ)`, where `Zcₖ = sqrt.(diag(Zmₖ))./sqrt.(diag(Ymₖ))`. |
| 33 | + - `Y.values[:,:,k] = Diagonal(Ycₖ)`, with `Ycₖ = 1 ./ Zcₖ`. |
| 34 | +
|
| 35 | +# Notes |
| 36 | +
|
| 37 | +- Columns are **phase→modal** voltage transformation (same convention as your legacy code). |
| 38 | +- Rotation `rot!` is applied per frequency to minimize the imaginary part of each column |
| 39 | + (Gustavsen’s scheme), stabilizing mode identity across the sweep. |
| 40 | +""" |
| 41 | +function (f::eigLM)(lp::LineParameters) |
| 42 | + n, n2, nfreq = size(lp.Z.values) |
| 43 | + n == n2 || throw(DimensionMismatch("Z must be square")) |
| 44 | + size(lp.Y.values) == (n, n, nfreq) || throw(DimensionMismatch("Y must be n×n×nfreq")) |
| 45 | + |
| 46 | + # 1) Deterministic eigen/LM on nominal arrays |
| 47 | + Z_nom = to_nominal(lp.Z.values) |
| 48 | + Y_nom = to_nominal(lp.Y.values) |
| 49 | + f_nom = to_nominal(lp.f) |
| 50 | + Ti, _g_nom = _calc_transformation_matrix_LM(n, Z_nom, Y_nom, f_nom; tol = f.tol) |
| 51 | + _rot_min_imag!(Ti) |
| 52 | + |
| 53 | + # 2) Apply deterministic T to uncertain (or plain) inputs for *physical* outputs |
| 54 | + Zm, Ym, Zc_mod, Yc_mod, Zch, Ych = |
| 55 | + _calc_modal_quantities(Ti, lp.Z.values, lp.Y.values) |
| 56 | + Gdiag = _calc_gamma(Ti, lp.Z.values, lp.Y.values) |
| 57 | + |
| 58 | + # Keep your original return (Ti, modal characteristic) for compatibility, |
| 59 | + # but you now also have Zm, Ym, Zch, Ych, Gdiag available for downstream use. |
| 60 | + return Ti, LineParameters(SeriesImpedance(Zc_mod), ShuntAdmittance(Yc_mod), lp.f), |
| 61 | + LineParameters(SeriesImpedance(Zm), ShuntAdmittance(Ym), lp.f), |
| 62 | + LineParameters(SeriesImpedance(Zch), ShuntAdmittance(Ych), lp.f), Gdiag |
| 63 | +end |
| 64 | + |
| 65 | +#= --------------------------------------------------------------------------- |
| 66 | +Internals |
| 67 | +-----------------------------------------------------------------------------=# |
| 68 | + |
| 69 | +# Propagate γ with uncertainty WITHOUT eigen(): |
| 70 | +# γ̂_k = sqrt.( diag( inv(T_k) * (Y_k*Z_k) * T_k ) ) |
| 71 | +function _calc_gamma( |
| 72 | + Ti::AbstractArray{Tc, 3}, |
| 73 | + Z::AbstractArray{Tu, 3}, |
| 74 | + Y::AbstractArray{Tu, 3}, |
| 75 | +) where {Tc <: Complex, Tu <: COMPLEXSCALAR} |
| 76 | + n, n2, nfreq = size(Ti) |
| 77 | + n == n2 || throw(DimensionMismatch("Ti must be n×n×nfreq")) |
| 78 | + size(Z) == size(Y) == (n, n, nfreq) || throw(DimensionMismatch("Z,Y must be n×n×nfreq")) |
| 79 | + |
| 80 | + # Element type follows uncertain inputs |
| 81 | + Tγ = promote_type(eltype(Z), eltype(Y)) |
| 82 | + Gdiag = zeros(Tγ, n, n, nfreq) # store as diagonal matrices for consistency |
| 83 | + |
| 84 | + Tk = zeros(Tc, n, n) |
| 85 | + invT = zeros(Tc, n, n) |
| 86 | + |
| 87 | + @inbounds for k in 1:nfreq |
| 88 | + Tk .= @view Ti[:, :, k] |
| 89 | + invT .= inv(Tk) |
| 90 | + |
| 91 | + S_k = @view(Y[:, :, k]) * @view(Z[:, :, k]) # Complex{Measurement} ok |
| 92 | + λdiag = diag(invT * S_k * Tk) |
| 93 | + γdiag = sqrt.(λdiag) |
| 94 | + @views Gdiag[:, :, k] .= Diagonal(γdiag) |
| 95 | + end |
| 96 | + return Gdiag |
| 97 | +end |
| 98 | + |
| 99 | +# Frequency-tracked Levenberg–Marquardt eigen solution |
| 100 | +function _calc_transformation_matrix_LM( |
| 101 | + n::Int, |
| 102 | + Z::AbstractArray{T, 3}, |
| 103 | + Y::AbstractArray{T, 3}, |
| 104 | + f::AbstractVector{U}; |
| 105 | + tol::U = LMTOL, |
| 106 | +) where {T <: Complex, U <: Real} |
| 107 | + |
| 108 | + # Constants |
| 109 | + ε0 = U(ε₀) # [F/m] |
| 110 | + μ0 = U(μ₀) |
| 111 | + |
| 112 | + nfreq = size(Z, 3) |
| 113 | + Ti = zeros(T, n, n, nfreq) |
| 114 | + g = zeros(T, n, n, nfreq) # store as diagonalized in n×n×nfreq for convenience |
| 115 | + |
| 116 | + Zk = zeros(T, n, n) |
| 117 | + Yk = zeros(T, n, n) |
| 118 | + |
| 119 | + # k = 1 → plain eigen-decomposition seed |
| 120 | + Zk .= @view Z[:, :, 1] |
| 121 | + Yk .= @view Y[:, :, 1] |
| 122 | + S = Yk * Zk |
| 123 | + E = eigen(S) # S*v = λ*v |
| 124 | + Ti[:, :, 1] .= E.vectors |
| 125 | + g[:, :, 1] .= Diagonal(sqrt.(E.values)) # γ = sqrt(λ) |
| 126 | + |
| 127 | + # k ≥ 2 → LM tracking |
| 128 | + ord_sq = n^2 |
| 129 | + for k in 2:nfreq |
| 130 | + Zk .= @view Z[:, :, k] |
| 131 | + Yk .= @view Y[:, :, k] |
| 132 | + |
| 133 | + S = Yk * Zk |
| 134 | + |
| 135 | + # Normalize as in legacy: (S / norm_val) - I |
| 136 | + ω = 2π * f[k] |
| 137 | + nrm = -(ω^2) * ε0 * μ0 |
| 138 | + S̃ = (S ./ nrm) - I |
| 139 | + |
| 140 | + # Seed from previous step |
| 141 | + Tseed = @view Ti[:, :, k-1] |
| 142 | + gseed = @view g[:, :, k-1] |
| 143 | + λseed = (diag(gseed) .^ 2 ./ nrm) .- 1 # since S̃*T = T*Λ with Λ = λ̃ = (λ/nrm)-1 |
| 144 | + |
| 145 | + # Build real-valued unknown vector: [Re(T); Im(T); Re(λ); Im(λ)] |
| 146 | + x0 = [ |
| 147 | + vec(real(Tseed)); |
| 148 | + vec(imag(Tseed)); |
| 149 | + real(λseed); |
| 150 | + imag(λseed) |
| 151 | + ] |
| 152 | + |
| 153 | + function _residual!( |
| 154 | + F::AbstractVector{<:R}, |
| 155 | + x::AbstractVector{<:R}, |
| 156 | + ) where {R <: Real} |
| 157 | + # Unpack |
| 158 | + Tr = reshape(@view(x[1:ord_sq]), n, n) |
| 159 | + Ti_ = reshape(@view(x[(ord_sq+1):(2*ord_sq)]), n, n) |
| 160 | + |
| 161 | + λr = @view x[(2*ord_sq+1):(2*ord_sq+n)] |
| 162 | + λi = @view x[(2*ord_sq+n+1):(2*ord_sq+2n)] |
| 163 | + |
| 164 | + Λr = Diagonal(λr) |
| 165 | + Λi = Diagonal(λi) |
| 166 | + |
| 167 | + Sr = real(S̃); |
| 168 | + Si = imag(S̃) |
| 169 | + |
| 170 | + # Residual of S̃*T - T*Λ = 0, split into real/imag |
| 171 | + Rr = (Sr*Tr - Si*Ti_) - (Tr*Λr - Ti_*Λi) |
| 172 | + Ri = (Sr*Ti_ + Si*Tr) - (Tr*Λi + Ti_*Λr) |
| 173 | + |
| 174 | + F[1:ord_sq] .= vec(Rr) |
| 175 | + F[(ord_sq+1):(2*ord_sq)] .= vec(Ri) |
| 176 | + |
| 177 | + # Column normalization constraints |
| 178 | + # For each column j: ||t_r||^2 - ||t_i||^2 = 1 and t_r ⋅ t_i = 0 |
| 179 | + c1 = sum(abs2.(Tr), dims = 1) .- sum(abs2.(Ti_), dims = 1) .- 1 |
| 180 | + c2 = sum(Tr .* Ti_, dims = 1) |
| 181 | + idx = 2*ord_sq |
| 182 | + @inbounds for j in 1:n |
| 183 | + F[idx+2j-1] = c1[j] |
| 184 | + F[idx+2j] = c2[j] |
| 185 | + end |
| 186 | + return nothing |
| 187 | + end |
| 188 | + |
| 189 | + sol = nlsolve( |
| 190 | + _residual!, |
| 191 | + x0; |
| 192 | + method = :trust_region, |
| 193 | + autodiff = :forward, |
| 194 | + xtol = tol, |
| 195 | + ftol = tol, |
| 196 | + ) |
| 197 | + |
| 198 | + if !converged(sol) |
| 199 | + @warn "LM solver did not converge at k=$k, using seed eigen-decomposition fallback" |
| 200 | + E = eigen(S) |
| 201 | + Ti[:, :, k] .= E.vectors |
| 202 | + g[:, :, k] .= Diagonal(sqrt.(E.values)) |
| 203 | + continue |
| 204 | + end |
| 205 | + |
| 206 | + x = sol.zero |
| 207 | + Tr = reshape(@view(x[1:ord_sq]), n, n) |
| 208 | + Ti_ = reshape(@view(x[(ord_sq+1):(2*ord_sq)]), n, n) |
| 209 | + T̂ = Tr .+ im .* Ti_ |
| 210 | + |
| 211 | + λr = @view x[(2*ord_sq+1):(2*ord_sq+n)] |
| 212 | + λi = @view x[(2*ord_sq+n+1):(2*ord_sq+2n)] |
| 213 | + λ̃ = λr .+ im .* λi # normalized eigenvalues |
| 214 | + |
| 215 | + # Undo normalization: λ = (λ̃ + 1) * nrm ; γ = sqrt(λ) |
| 216 | + λ = (λ̃ .+ one(eltype(λ̃))) .* nrm |
| 217 | + γ = sqrt.(λ) |
| 218 | + |
| 219 | + Ti[:, :, k] .= T̂ |
| 220 | + g[:, :, k] .= Diagonal(γ) |
| 221 | + end |
| 222 | + |
| 223 | + return Ti, g |
| 224 | +end |
| 225 | + |
| 226 | +# In-place rotation to minimize imag part column-wise (per frequency slice) |
| 227 | +function _rot_min_imag!(Ti::AbstractArray{T, 3}) where {T <: Complex} |
| 228 | + n, n2, nfreq = size(Ti) |
| 229 | + n == n2 || throw(DimensionMismatch("Ti must be n×n×nfreq")) |
| 230 | + tmp = zeros(T, n, n) |
| 231 | + @inbounds for k in 1:nfreq |
| 232 | + tmp .= @view Ti[:, :, k] |
| 233 | + rot!(tmp) # column-wise rotation in-place |
| 234 | + Ti[:, :, k] .= tmp |
| 235 | + end |
| 236 | + return Ti |
| 237 | +end |
| 238 | + |
| 239 | +# Full modal + characteristic + phase back-projection |
| 240 | +# Returns: |
| 241 | +# Zm, Ym :: n×n×nfreq (modal-domain series/shunt matrices) |
| 242 | +# Zc_mod,Yc_mod :: n×n×nfreq (diagonal: per-mode characteristic) |
| 243 | +# Zch, Ych :: n×n×nfreq (phase-domain characteristic back-projected) |
| 244 | +function _calc_modal_quantities( |
| 245 | + Ti::AbstractArray{Tc, 3}, |
| 246 | + Z::AbstractArray{Tu, 3}, |
| 247 | + Y::AbstractArray{Tu, 3}, |
| 248 | +) where {Tc <: Complex, Tu <: COMPLEXSCALAR} |
| 249 | + |
| 250 | + n, n2, nfreq = size(Ti) |
| 251 | + n == n2 || throw(DimensionMismatch("Ti must be n×n×nfreq")) |
| 252 | + size(Z) == size(Y) == (n, n, nfreq) || throw(DimensionMismatch("Z,Y must be n×n×nfreq")) |
| 253 | + |
| 254 | + Tz = promote_type(eltype(Z), eltype(Y)) # keep uncertainties |
| 255 | + Zm = zeros(Tz, n, n, nfreq) |
| 256 | + Ym = zeros(Tz, n, n, nfreq) |
| 257 | + Zc_mod = zeros(Tz, n, n, nfreq) |
| 258 | + Yc_mod = zeros(Tz, n, n, nfreq) |
| 259 | + Zch = zeros(Tz, n, n, nfreq) |
| 260 | + Ych = zeros(Tz, n, n, nfreq) |
| 261 | + |
| 262 | + Tk = zeros(Tc, n, n) |
| 263 | + Zk = zeros(Tz, n, n) |
| 264 | + Yk = zeros(Tz, n, n) |
| 265 | + invT = zeros(Tc, n, n) |
| 266 | + |
| 267 | + @inbounds for k in 1:nfreq |
| 268 | + Tk .= @view Ti[:, :, k] |
| 269 | + invT .= inv(Tk) |
| 270 | + Zk .= @view Z[:, :, k] |
| 271 | + Yk .= @view Y[:, :, k] |
| 272 | + |
| 273 | + # Modal matrices (carry uncertainties) |
| 274 | + @views Zm[:, :, k] .= transpose(Tk) * Zk * Tk |
| 275 | + @views Ym[:, :, k] .= invT * Yk * transpose(invT) |
| 276 | + |
| 277 | + # Characteristic per-mode (diagonal) in modal domain |
| 278 | + zc = sqrt.(diag(@view Zm[:, :, k])) ./ sqrt.(diag(@view Ym[:, :, k])) |
| 279 | + @views Zc_mod[:, :, k] .= Diagonal(zc) |
| 280 | + @views Yc_mod[:, :, k] .= Diagonal(inv.(zc)) |
| 281 | + |
| 282 | + # Phase-domain characteristic back-projection |
| 283 | + @views Zch[:, :, k] .= transpose(invT) * Zc_mod[:, :, k] * invT |
| 284 | + @views Ych[:, :, k] .= Tk * Yc_mod[:, :, k] * transpose(Tk) |
| 285 | + end |
| 286 | + return Zm, Ym, Zc_mod, Yc_mod, Zch, Ych |
| 287 | +end |
| 288 | + |
| 289 | +# column rotation to minimize imag parts |
| 290 | +function rot!(S::AbstractMatrix{T}) where {T <: COMPLEXSCALAR} |
| 291 | + n, m = size(S) |
| 292 | + n == m || throw(DimensionMismatch("Input must be square")) |
| 293 | + @inbounds for j in 1:n |
| 294 | + col = @view S[:, j] |
| 295 | + |
| 296 | + # optimal angle |
| 297 | + num = -2 * sum(real.(col) .* imag.(col)) # real |
| 298 | + den = sum(real.(col) .^ 2 .- imag.(col) .^ 2) # real |
| 299 | + ang = BASE_FLOAT(0.5) * atan(num, den) # real |
| 300 | + |
| 301 | + s1 = cis(ang) |
| 302 | + s2 = cis(ang + BASE_FLOAT(pi/2)) |
| 303 | + |
| 304 | + A = col .* s1 |
| 305 | + B = col .* s2 |
| 306 | + |
| 307 | + # all-real quadratic metrics |
| 308 | + Ar = real.(A); |
| 309 | + Ai = imag.(A) |
| 310 | + Br = real.(B); |
| 311 | + Bi = imag.(B) |
| 312 | + |
| 313 | + aaa1 = sum(Ai .^ 2) |
| 314 | + bbb1 = sum(Ar .* Ai) |
| 315 | + ccc1 = sum(Ar .^ 2) |
| 316 | + err1 = aaa1 * cos(ang)^2 + bbb1 * sin(2*ang) + ccc1 * sin(ang)^2 # real |
| 317 | + |
| 318 | + aaa2 = sum(Bi .^ 2) |
| 319 | + bbb2 = sum(Br .* Bi) |
| 320 | + ccc2 = sum(Br .^ 2) |
| 321 | + err2 = aaa2 * cos(ang)^2 + bbb2 * sin(2*ang) + ccc2 * sin(ang)^2 # real |
| 322 | + |
| 323 | + col .*= (err1 < err2 ? s1 : s2) |
| 324 | + end |
| 325 | + return S |
| 326 | +end |
| 327 | + |
| 328 | + |
| 329 | +# tiny helper: in-place imag (for metric term; avoids repeated allocations) |
| 330 | +@inline function imag!(x::AbstractVector{<:Complex}) |
| 331 | + @inbounds for i in eachindex(x) |
| 332 | + x[i] = imag(x[i]) |
| 333 | + end |
| 334 | + return x |
| 335 | +end |
0 commit comments