Skip to content

Commit 7915047

Browse files
feat(transforms): add eiglevenberg.jl for eigenvalue Levenberg-Marquardt routines
1 parent b741150 commit 7915047

1 file changed

Lines changed: 335 additions & 0 deletions

File tree

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

Comments
 (0)