-
Notifications
You must be signed in to change notification settings - Fork 22
LM performance improvements #338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
93b06f6
bfb2d79
fa4ff7a
11efea0
0d134ed
d31d118
9941c5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,7 @@ docs/site/ | |
| *.ipynb | ||
|
|
||
| Manifest.toml | ||
| scripts/benchmarks/*.csv | ||
| .vscode | ||
| *.h5 | ||
| data | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| [deps] | ||
| CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" | ||
| DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" | ||
| PowerFlows = "94fada2c-fd9a-4e89-8d82-81405f5cb4f6" | ||
| PowerSystemCaseBuilder = "f00506e0-b84f-492a-93c2-c0a9afc4364e" | ||
| PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd" |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,157 +1,232 @@ | ||||||||||||||||||||||
| """Driver for the LevenbergMarquardtACPowerFlow method: sets up the data | ||||||||||||||||||||||
| structures (e.g. residual), runs the power flow method via calling `_run_power_flow_method` | ||||||||||||||||||||||
| """Pre-allocated workspace for the Levenberg-Marquardt solver. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Holds the augmented matrix `[J; √λ·I]` with a fixed sparsity pattern, | ||||||||||||||||||||||
| a mapping to update its entries in-place, and a cached QR factorization.""" | ||||||||||||||||||||||
| mutable struct LMWorkspace | ||||||||||||||||||||||
| A::SparseMatrixCSC{Float64, Int64} | ||||||||||||||||||||||
| # PERF: if we're doing dense access, I'd expect a bitmask to be faster. | ||||||||||||||||||||||
| # Or only store λ_diag_indices (sparse) and infer J entries as the complement. | ||||||||||||||||||||||
| # Indices into A.nzval for the J block entries (same order as J.Jv.nzval) | ||||||||||||||||||||||
| j_nzval_indices::Vector{Int} | ||||||||||||||||||||||
| # Indices into A.nzval for the √λ diagonal entries (length n) | ||||||||||||||||||||||
| λ_diag_indices::Vector{Int} | ||||||||||||||||||||||
| # PERF: have not investigated other factorization methods. SPQR does not allow | ||||||||||||||||||||||
| # for in-place updates or re-use of symbolic factorization, but with non | ||||||||||||||||||||||
| # square matrices--J^T J form is more unstable--we don't have a lot of options. | ||||||||||||||||||||||
| # Cached QR factorization | ||||||||||||||||||||||
| F::SparseArrays.SPQR.QRSparse{Float64, Int64} | ||||||||||||||||||||||
| # Preallocated augmented RHS [-Rv; 0] (length m + n); bottom n stay zero. | ||||||||||||||||||||||
| b::Vector{Float64} | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| """Build the augmented matrix `[J; I]` once, recording which `A.nzval` entries | ||||||||||||||||||||||
| correspond to J values vs the λ diagonal.""" | ||||||||||||||||||||||
| function LMWorkspace(Jv::SparseMatrixCSC{Float64, J_INDEX_TYPE}) | ||||||||||||||||||||||
| m, n = size(Jv) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Convert J to Int64 indices for SPQR compatibility, then vcat with identity. | ||||||||||||||||||||||
| Jv64 = SparseMatrixCSC{Float64, Int64}( | ||||||||||||||||||||||
| Jv.m, Jv.n, | ||||||||||||||||||||||
| Vector{Int64}(Jv.colptr), | ||||||||||||||||||||||
| Vector{Int64}(Jv.rowval), | ||||||||||||||||||||||
| copy(Jv.nzval), | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| Iλ = sparse(Int64.(1:n), Int64.(1:n), ones(n), n, n) | ||||||||||||||||||||||
| A = vcat(Jv64, Iλ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # Identify which A.nzval entries come from J vs the diagonal. | ||||||||||||||||||||||
| j_nzval_indices = Vector{Int}(undef, length(Jv.nzval)) | ||||||||||||||||||||||
| λ_diag_indices = Vector{Int}(undef, n) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| j_idx = 0 | ||||||||||||||||||||||
| for col in 1:n | ||||||||||||||||||||||
| for a_idx in A.colptr[col]:(A.colptr[col + 1] - 1) | ||||||||||||||||||||||
| row = A.rowval[a_idx] | ||||||||||||||||||||||
| if row <= m | ||||||||||||||||||||||
| j_idx += 1 | ||||||||||||||||||||||
| j_nzval_indices[j_idx] = a_idx | ||||||||||||||||||||||
| elseif row == m + col | ||||||||||||||||||||||
| λ_diag_indices[col] = a_idx | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| @assert j_idx == length(Jv.nzval) "Expected $(length(Jv.nzval)) J entries, found $j_idx" | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| b = zeros(m + n) | ||||||||||||||||||||||
| F = LinearAlgebra.qr(A) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return LMWorkspace(A, j_nzval_indices, λ_diag_indices, F, b) | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| """Copy current Jacobian values into the augmented matrix.""" | ||||||||||||||||||||||
| function copy_jacobian!(ws::LMWorkspace, Jv::SparseMatrixCSC{Float64, J_INDEX_TYPE}) | ||||||||||||||||||||||
| nzv = Jv.nzval | ||||||||||||||||||||||
| for (i, a_idx) in enumerate(ws.j_nzval_indices) | ||||||||||||||||||||||
| ws.A.nzval[a_idx] = nzv[i] | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| return | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| """Update the √λ diagonal and re-factorize.""" | ||||||||||||||||||||||
| function update_lambda!(ws::LMWorkspace, λ::Float64) | ||||||||||||||||||||||
| sqrtλ = sqrt(λ) | ||||||||||||||||||||||
| for a_idx in ws.λ_diag_indices | ||||||||||||||||||||||
| ws.A.nzval[a_idx] = sqrtλ | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| ws.F = LinearAlgebra.qr(ws.A) | ||||||||||||||||||||||
| return | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| """Driver for the LevenbergMarquardtACPowerFlow method: sets up the data | ||||||||||||||||||||||
| structures (e.g. residual), runs the power flow method via calling `_run_power_flow_method` | ||||||||||||||||||||||
| on them, then handles post-processing (e.g. loss factors).""" | ||||||||||||||||||||||
| function _newton_power_flow( | ||||||||||||||||||||||
| pf::ACPolarPowerFlow{LevenbergMarquardtACPowerFlow}, | ||||||||||||||||||||||
| pf::AbstractACPowerFlow{LevenbergMarquardtACPowerFlow}, | ||||||||||||||||||||||
| data::ACPowerFlowData, | ||||||||||||||||||||||
| time_step::Int64; | ||||||||||||||||||||||
| tol::Float64 = DEFAULT_NR_TOL, | ||||||||||||||||||||||
| maxIterations::Int = DEFAULT_NR_MAX_ITER, | ||||||||||||||||||||||
| validate_voltage_magnitudes::Bool = DEFAULT_VALIDATE_VOLTAGES, | ||||||||||||||||||||||
| vm_validation_range::MinMax = DEFAULT_VALIDATION_RANGE, | ||||||||||||||||||||||
| λ_0::Float64 = DEFAULT_λ_0, | ||||||||||||||||||||||
| maxTestλs::Int = DEFAULT_MAX_TEST_λs, | ||||||||||||||||||||||
| x0::Union{Vector{Float64}, Nothing} = nothing, | ||||||||||||||||||||||
| _ignored..., | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| init_kwargs = if isnothing(x0) | ||||||||||||||||||||||
| (; validate_voltage_magnitudes, vm_validation_range) | ||||||||||||||||||||||
| else | ||||||||||||||||||||||
| (; validate_voltage_magnitudes, vm_validation_range, x0) | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| residual, J, x0 = initialize_power_flow_variables( | ||||||||||||||||||||||
| pf, data, time_step; | ||||||||||||||||||||||
| validate_voltage_magnitudes, vm_validation_range) | ||||||||||||||||||||||
| pf, data, time_step; init_kwargs...) | ||||||||||||||||||||||
| converged = norm(residual.Rv, Inf) < tol | ||||||||||||||||||||||
| i = 0 | ||||||||||||||||||||||
| if !converged | ||||||||||||||||||||||
| ws = LMWorkspace(J.Jv) | ||||||||||||||||||||||
| converged, i = _run_power_flow_method( | ||||||||||||||||||||||
| time_step, | ||||||||||||||||||||||
| x0, | ||||||||||||||||||||||
| residual, | ||||||||||||||||||||||
| J; | ||||||||||||||||||||||
| tol, maxIterations, λ_0, maxTestλs, | ||||||||||||||||||||||
| J, | ||||||||||||||||||||||
| ws; | ||||||||||||||||||||||
| tol, maxIterations, λ_0, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| # x0 was mutated in place to the converged state by _run_power_flow_method | ||||||||||||||||||||||
| # (or is the already-converged initial state if the loop was skipped). | ||||||||||||||||||||||
| _finalize_formulation!(pf, data, x0, residual, time_step) | ||||||||||||||||||||||
| return _finalize_power_flow( | ||||||||||||||||||||||
| converged, i, "LevenbergMarquardtACPowerFlow", residual, data, J.Jv, time_step) | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| function _run_power_flow_method( | ||||||||||||||||||||||
| time_step::Int, | ||||||||||||||||||||||
| x::Vector{Float64}, | ||||||||||||||||||||||
| residual::ACPowerFlowResidual, | ||||||||||||||||||||||
| J::ACPowerFlowJacobian; | ||||||||||||||||||||||
| residual::Union{ACPowerFlowResidual, ACRectangularCIResidual}, | ||||||||||||||||||||||
| J::Union{ACPowerFlowJacobian, ACRectangularCIJacobian}, | ||||||||||||||||||||||
| ws::LMWorkspace; | ||||||||||||||||||||||
| maxIterations::Int = DEFAULT_NR_MAX_ITER, | ||||||||||||||||||||||
| tol::Float64 = DEFAULT_NR_TOL, | ||||||||||||||||||||||
| λ_0::Float64 = DEFAULT_λ_0, | ||||||||||||||||||||||
| maxTestλs::Int = DEFAULT_MAX_TEST_λs, | ||||||||||||||||||||||
| _ignored..., | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| λ::Float64 = λ_0 | ||||||||||||||||||||||
| μ::Float64 = λ_0 | ||||||||||||||||||||||
| λ::Float64 = 0.0 | ||||||||||||||||||||||
| i, converged = 0, false | ||||||||||||||||||||||
| residual(x, time_step) | ||||||||||||||||||||||
| resSize = dot(residual.Rv, residual.Rv) | ||||||||||||||||||||||
| linf = norm(residual.Rv, Inf) | ||||||||||||||||||||||
| @debug "initially: sum of squares $(siground(resSize)), L ∞ norm $(siground(linf)), λ = $λ" | ||||||||||||||||||||||
| while i < maxIterations && !converged && !isnan(λ) | ||||||||||||||||||||||
| λ = update_damping_factor!(x, residual, J, time_step, maxTestλs) | ||||||||||||||||||||||
| converged = !isnan(λ) && norm(residual.Rv, Inf) < tol | ||||||||||||||||||||||
| while i < maxIterations && !converged && isfinite(λ) && μ < DEFAULT_μ_MAX | ||||||||||||||||||||||
| λ, μ = update_damping_factor!(x, residual, J, μ, time_step, ws) | ||||||||||||||||||||||
| converged = isfinite(λ) && norm(residual.Rv, Inf) < tol | ||||||||||||||||||||||
| i += 1 | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| if isnan(λ) | ||||||||||||||||||||||
| @error "λ is NaN" | ||||||||||||||||||||||
| elseif i == maxIterations | ||||||||||||||||||||||
| @error "The LevenbergMarquardtACPowerFlow solver didn't coverge in $maxIterations iterations." | ||||||||||||||||||||||
| if !converged | ||||||||||||||||||||||
| if !isfinite(λ) | ||||||||||||||||||||||
| @error "λ is not finite ($(λ))" | ||||||||||||||||||||||
| elseif μ >= DEFAULT_μ_MAX | ||||||||||||||||||||||
| @error "The LevenbergMarquardtACPowerFlow damping factor μ hit the cap (DEFAULT_μ_MAX=$(DEFAULT_μ_MAX)) after $i iterations; aborting (likely divergence)." | ||||||||||||||||||||||
| elseif i == maxIterations | ||||||||||||||||||||||
| @error "The LevenbergMarquardtACPowerFlow solver didn't coverge in $maxIterations iterations." | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return converged, i | ||||||||||||||||||||||
| end | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| # LM implementation and parameters values largely based upon this paper: | ||||||||||||||||||||||
| # # https://www.sciencedirect.com/science/article/pii/S0142061518336342 | ||||||||||||||||||||||
| # LM implementation based on standard Levenberg-Marquardt method. | ||||||||||||||||||||||
| # See Nocedal & Wright (2006), sections 10.3 and 11.2. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| """Compute one LM trial step. Assumes `residual` and `J` are already evaluated | ||||||||||||||||||||||
| at `x` by the caller. Returns the gain ratio ρ.""" | ||||||||||||||||||||||
| function compute_error( | ||||||||||||||||||||||
| x::Vector{Float64}, | ||||||||||||||||||||||
| residual::ACPowerFlowResidual, | ||||||||||||||||||||||
| J::ACPowerFlowJacobian, | ||||||||||||||||||||||
| residual::Union{ACPowerFlowResidual, ACRectangularCIResidual}, | ||||||||||||||||||||||
| J::Union{ACPowerFlowJacobian, ACRectangularCIJacobian}, | ||||||||||||||||||||||
| λ::Float64, | ||||||||||||||||||||||
| time_step::Int, | ||||||||||||||||||||||
| residualSize::Float64, | ||||||||||||||||||||||
| ws::LMWorkspace, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| residual(x, time_step) # M(x_c) | ||||||||||||||||||||||
| J(time_step) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| n = size(J.Jv, 2) | ||||||||||||||||||||||
| Iλ = sparse(1:n, 1:n, sqrt(λ) .* ones(n), n, n) # less error-prone compared to A = vcat(J.Jv, sqrt(λ) * sparse(LinearAlgebra.I, size(J.Jv))) | ||||||||||||||||||||||
| A = [J.Jv; Iλ] | ||||||||||||||||||||||
| # Update augmented matrix with current J values and λ, then factorize once. | ||||||||||||||||||||||
| copy_jacobian!(ws, J.Jv) | ||||||||||||||||||||||
| update_lambda!(ws, λ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| b_x = vcat(-residual.Rv, zeros(size(J.Jv, 2))) | ||||||||||||||||||||||
| Δx = A \ b_x | ||||||||||||||||||||||
| m = length(residual.Rv) | ||||||||||||||||||||||
| @assert m == length(ws.b) - size(J.Jv, 2) "residual/J size mismatch vs preallocated LM buffer (m=$m, buf=$(length(ws.b)), n=$(size(J.Jv, 2)))" | ||||||||||||||||||||||
| @views ws.b[1:m] .= .-residual.Rv # bottom n entries stay zero from construction | ||||||||||||||||||||||
| Δx = ws.F \ ws.b | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| temp_x = residual.Rv .+ J.Jv * Δx | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| x_trial = x .+ Δx | ||||||||||||||||||||||
| residual(x_trial, time_step) # M(y_c) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| b_y = vcat(-residual.Rv, zeros(size(J.Jv, 2))) | ||||||||||||||||||||||
| Δy = A \ b_y | ||||||||||||||||||||||
| temp_y = residual.Rv .+ J.Jv * Δy | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| newResidualSize_y = dot(residual.Rv, residual.Rv) | ||||||||||||||||||||||
| residual(x_trial .+ Δy, time_step) # M(x_c+Δx+Δy) | ||||||||||||||||||||||
| residual(x_trial, time_step) # M(x_c + Δx) | ||||||||||||||||||||||
| newResidualSize = dot(residual.Rv, residual.Rv) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| b_z = vcat(-residual.Rv, zeros(size(J.Jv, 2))) | ||||||||||||||||||||||
| Δz = A \ b_z | ||||||||||||||||||||||
| temp_z = residual.Rv .+ J.Jv * Δz | ||||||||||||||||||||||
| newResidualSize_z = dot(residual.Rv, residual.Rv) | ||||||||||||||||||||||
| predicted_reduction = residualSize - dot(temp_x, temp_x) | ||||||||||||||||||||||
| actual_reduction = residualSize - newResidualSize | ||||||||||||||||||||||
|
||||||||||||||||||||||
| actual_reduction = residualSize - newResidualSize | |
| actual_reduction = residualSize - newResidualSize | |
| # Guard against zero/negative or non-finite predicted reduction to | |
| # avoid Inf/NaN ρ and accidentally accepting a bad step. | |
| if predicted_reduction <= 0.0 || !isfinite(predicted_reduction) | |
| # Treat as a rejected step: restore residual to match x. | |
| residual(x, time_step) | |
| return 0.0 | |
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The LM loop only checks
!isnan(λ), but with the newμ *= 4updates,μ/λcan overflow toInf(or become non-finite via factorization), and the loop will keep running untilmaxIterations. Consider switching the loop condition and convergence logic toisfinite(λ)(and/or boundingμ), and optionally restoring an explicit “give up after N rejected steps” safeguard similar to the previousmaxTestλsbehavior.