Conversation
|
I think this is good to go now. You were right @samtalki, some of the switching terms were missing... |
|
paging @djturizo as he mentioned he has previously worked out closed form expresions of the implicit function theorem applied to AC OPF in some of his legacy code https://github.com/djturizo/Shortest-Path-OPF |
| vars = unflatten_variables(flatten_variables(sol, prob), idx) | ||
| n, m, k = prob.network.n, prob.network.m, prob.n_gen | ||
|
|
||
| J = spzeros(Float64, kkt_dims(prob), kkt_dims(prob)) |
There was a problem hiding this comment.
Suggestion, non-blocking: this is still assembling a SparseMatrixCSC via many scalar insertions. It is already much faster than the old ForwardDiff path, but the allocation count is still fairly high on larger cases. Longer term, it may be worth assembling (I, J, V) triplets and calling sparse(I, J, V, n, n) once, or caching a fixed sparsity pattern and updating only values.
There was a problem hiding this comment.
@cameronkhanpour from my understanding, I think you are right that this is the best practice for building a SparseMatrixCSC. @klamike can you confirm?
There was a problem hiding this comment.
yep, I should fix this
There was a problem hiding this comment.
What Cameron suggests is the most performant way
| Return branch-flow bases (with `sw = 1`) and local first derivatives with | ||
| respect to `(va_f, va_t, vm_f, vm_t)`. | ||
| """ | ||
| function _branch_flow_primitives(va, vm, sw, constants, l::Int) |
There was a problem hiding this comment.
Suggestion, non-blocking: _branch_flow_primitives computes values, first derivatives, and second derivatives unconditionally. Some callers only need values and first derivatives, for example the parameter Jacobian and JVP/VJP paths. Splitting this into a lighter value/gradient helper plus a Hessian-enabled helper would avoid extra work and make caller intent clearer.
There was a problem hiding this comment.
Generally agree but should avoid over abstracting unless the isolation has a clear use case
There was a problem hiding this comment.
Just split the function in 3, there's no over-abstraction problem
| return nothing | ||
| end | ||
|
|
||
| function _fill_ac_param_jacobian_sw!(Jp::AbstractMatrix, prob::ACOPFProblem, |
There was a problem hiding this comment.
Suggestion, non-blocking: the :sw derivative formulas are duplicated across the full parameter Jacobian, single-column helper, and matrix-free JVP/VJP implementations. Since this is the highest-risk part of the analytical path, it would be easier to maintain if a lower-level helper emitted the contribution for one dK/dsw_l column and the full matrix, column, JVP, and VJP paths reused that logic where practical.
There was a problem hiding this comment.
Agree with this and, more generally, we should try to isolate analytical derivate formula primitives that can be reused throughout the package. I could be wrong but there appears to be a lot of duplicated logic throughout the diff.
|
|
||
| """Pre-extract all AC OPF parameters for ForwardDiff closures.""" | ||
| """Pre-extract all AC OPF parameter/state data needed by the analytical slow path.""" | ||
| function _ac_kkt_context(prob::ACOPFProblem) |
There was a problem hiding this comment.
Suggestion, non-blocking: _ac_kkt_context still extracts several vectors inherited from the previous ForwardDiff closure path. In the analytical JVP/VJP code, most of these are not used. Trimming this context to the values needed by the analytical path would remove some small overhead and make the code easier to audit.
There was a problem hiding this comment.
agree; would be nice to avoid
| @@ -44,37 +44,68 @@ bus-type column modifications matching PowerModels' `calc_basic_jacobian_matrix` | |||
| function calc_power_flow_jacobian(state::ACPowerFlowState; | |||
| bus_types::Union{Vector{Int}, Nothing}=nothing) | |||
| Y_dense = Matrix(state.Y) | |||
There was a problem hiding this comment.
Suggestion, non-blocking: converting state.Y to a dense matrix is simple and fine for the current test cases, but it can become a memory bottleneck for larger sparse networks. If AC PF Jacobian performance matters on larger systems, this could be rewritten to iterate over the sparse nonzeros of Y instead of materializing Matrix(state.Y).
There was a problem hiding this comment.
It would be nice to avoid densifying, would this be a difficult fix? @cameronkhanpour @klamike
There was a problem hiding this comment.
Confused why dp_dva, dp_dvm, dq_dva, etc. are preallocated as sparse arrays right after densifying G and B.
There was a problem hiding this comment.
The whole code logic of this function is dense, although that was already the case before this PR, including the densification of state.Y
There was a problem hiding this comment.
In general converting the code logic to leverage sparsity is something that was brought up in issue #20 so it may be more "proper" to address this in a separate PR linking to that issue, but I'll leave it up to y'all if we want to get started with that here.
samtalki
left a comment
There was a problem hiding this comment.
This is an incredibly valuable contribution that will be a core piece of many downstream applications, thank you @klamike . To summarize my (minor) comments overall:
- It would be helpful to deduplicate some of the repeated physics logic into consistent derivative primitives, and
- there are several places where we may be able to better exploit sparsity and avoid densifying matrices.
| @@ -44,37 +44,68 @@ bus-type column modifications matching PowerModels' `calc_basic_jacobian_matrix` | |||
| function calc_power_flow_jacobian(state::ACPowerFlowState; | |||
| bus_types::Union{Vector{Int}, Nothing}=nothing) | |||
| Y_dense = Matrix(state.Y) | |||
There was a problem hiding this comment.
It would be nice to avoid densifying, would this be a difficult fix? @cameronkhanpour @klamike
| vars = unflatten_variables(flatten_variables(sol, prob), idx) | ||
| n, m, k = prob.network.n, prob.network.m, prob.n_gen | ||
|
|
||
| J = spzeros(Float64, kkt_dims(prob), kkt_dims(prob)) |
There was a problem hiding this comment.
@cameronkhanpour from my understanding, I think you are right that this is the best practice for building a SparseMatrixCSC. @klamike can you confirm?
|
|
||
| """Pre-extract all AC OPF parameters for ForwardDiff closures.""" | ||
| """Pre-extract all AC OPF parameter/state data needed by the analytical slow path.""" | ||
| function _ac_kkt_context(prob::ACOPFProblem) |
There was a problem hiding this comment.
agree; would be nice to avoid
| return ForwardDiff.gradient( | ||
| x -> dot(u, _ac_kkt_call(prob, ctx, ctx.sw, NamedTuple{(kw,)}((x,)), fixed_nt)), | ||
| p0) | ||
| @inbounds for l in 1:prob.network.m |
There was a problem hiding this comment.
Is this logic in this loop duplicated elsewhere?
| return v | ||
| end | ||
|
|
||
| @inbounds for l in 1:prob.network.m |
There was a problem hiding this comment.
Is this logic in this loop duplicated elsewhere?
| return nothing | ||
| end | ||
|
|
||
| function _fill_ac_param_jacobian_sw!(Jp::AbstractMatrix, prob::ACOPFProblem, |
There was a problem hiding this comment.
Agree with this and, more generally, we should try to isolate analytical derivate formula primitives that can be reused throughout the package. I could be wrong but there appears to be a lot of duplicated logic throughout the diff.
| @@ -547,25 +764,16 @@ function kkt(z::AbstractVector, prob::ACOPFProblem, sw::AbstractVector; | |||
|
|
|||
| # Materialize rate_a vector once to avoid repeated isnothing checks | |||
| rate_a = isnothing(fmax) ? T[ref[:branch][l]["rate_a"] for l in 1:m] : fmax | |||
There was a problem hiding this comment.
For rate_a what happens if there is an l such that T[ref[:branch][l]["rate_a"] is NaN, nothing, or otherwise degenerate? Does the PowerModels parser already take care of these?
I also have the analogous question for the cost coefficient vectors.
| _add_symmetric_local_hessian!(J, local_idx, prim.d2q_to_hat, coeff_q_to * sw_l, | ||
| -2 * vars.lam_thermal_to[l] * sw_l^2, prim.dq_to_hat) | ||
|
|
||
| for t in 1:4 |
There was a problem hiding this comment.
wondering if there is any duplicated logic in this file
| Return branch-flow bases (with `sw = 1`) and local first derivatives with | ||
| respect to `(va_f, va_t, vm_f, vm_t)`. | ||
| """ | ||
| function _branch_flow_primitives(va, vm, sw, constants, l::Int) |
There was a problem hiding this comment.
Generally agree but should avoid over abstracting unless the isolation has a clear use case
|
|
||
| """ | ||
| Compute the reduced-space Lagrangian L(va, vm, pg, qg; duals, sw). | ||
| Return branch-flow coefficients in the form |
There was a problem hiding this comment.
The interpretation of "branch flow coefficients" could be more clearly documented.
|
Thanks for the detailed comments @samtalki @cameronkhanpour, I will do some passes next week |
No description provided.