diff --git a/docs/src/advanced.md b/docs/src/advanced.md index 04fdef2..10e5031 100644 --- a/docs/src/advanced.md +++ b/docs/src/advanced.md @@ -124,9 +124,13 @@ prob = ACOPFProblem(net; silent=true) exa_prob = ACOPFProblem(net; backend=:exa, silent=true) ``` -## KKT System Access (Qualified) +## KKT System Access -KKT internals are available via qualified access (`PowerDiff.function_name`), not exported: +`kkt_layout` is the primary public entry point for the flattened KKT system. It +returns the dimension and named index ranges together so they always describe +the same layout. `kkt_dims` and `kkt_indices` are conveniences for +callers that need only one part. Lower-level KKT operators remain available via +qualified access (`PowerDiff.function_name`). ```julia using PowerDiff @@ -137,14 +141,16 @@ z = PD.flatten_variables(sol, prob) # Solution → vector vars = PD.unflatten_variables(z, prob) # Vector → named tuple K = PD.kkt(z, prob, d) # KKT residuals J = PD.calc_kkt_jacobian(prob) # Sparse Jacobian dK/dz -dim = PD.kkt_dims(dc_net) # KKT dimension -idx = PD.kkt_indices(dc_net) # Named index ranges +dim, idx = kkt_layout(dc_net) # Dimension and named index ranges # AC OPF — same unified API z = PD.flatten_variables(sol, ac_prob) J = PD.calc_kkt_jacobian(ac_prob) # Sparse analytical Jacobian -dim = PD.kkt_dims(ac_prob) # KKT dimension -idx = PD.kkt_indices(ac_prob) # Named index ranges +dim, idx = kkt_layout(ac_prob) + +# Thin conveniences when only one component is needed +dim = kkt_dims(ac_prob) +idx = kkt_indices(ac_prob) ``` ## LMP Sign Conventions diff --git a/docs/src/api.md b/docs/src/api.md index e885994..0d24952 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -30,11 +30,21 @@ jvp jvp! vjp vjp! -kkt_dims dict_to_vec vec_to_dict ``` +## KKT Layout + +`kkt_layout` is the primary interface for inspecting a DC or AC KKT system. +`kkt_dims` and `kkt_indices` return the individual parts of the same layout. + +```@docs +kkt_layout +kkt_dims +kkt_indices +``` + ## Introspection ```@docs diff --git a/src/PowerDiff.jl b/src/PowerDiff.jl index 13fedd3..797577c 100644 --- a/src/PowerDiff.jl +++ b/src/PowerDiff.jl @@ -101,7 +101,8 @@ export IDMapping export calc_sensitivity, calc_sensitivity_column export Sensitivity, silence export operand_symbols, parameter_symbols -export jvp, vjp, jvp!, vjp!, dict_to_vec, vec_to_dict, kkt_dims +export jvp, vjp, jvp!, vjp!, dict_to_vec, vec_to_dict +export kkt_layout, kkt_dims, kkt_indices export parse_file, parse_matpower, parse_matpower_struct, get_path # DC Power Flow Types diff --git a/src/prob/kkt_ac_opf.jl b/src/prob/kkt_ac_opf.jl index be8a99e..5e46586 100644 --- a/src/prob/kkt_ac_opf.jl +++ b/src/prob/kkt_ac_opf.jl @@ -32,51 +32,12 @@ _ref_bus_indices(prob::ACOPFProblem) = prob.data.ref_bus_keys # ============================================================================= -# Dimension Calculations +# KKT Layout # ============================================================================= -""" - kkt_dims(prob::ACOPFProblem) - -Compute the dimension of the flattened KKT variable vector for AC OPF. +_ac_kkt_dims(n::Int, m::Int, k::Int, n_ref::Int) = 6n + 12m + 6k + n_ref -The KKT system includes: -- Primal: va (n), vm (n), pg (k), qg (k) -- Dual (equality): ν_p_bal (n), ν_q_bal (n), ν_ref_bus (n_ref) -- Dual (inequality): λ_thermal_fr (m), λ_thermal_to (m), - λ_angle_lb (m), λ_angle_ub (m), - μ_vm_lb (n), μ_vm_ub (n), - ρ_pg_lb (k), ρ_pg_ub (k), ρ_qg_lb (k), ρ_qg_ub (k), - σ_p_fr_lb (m), σ_p_fr_ub (m), σ_q_fr_lb (m), σ_q_fr_ub (m), - σ_p_to_lb (m), σ_p_to_ub (m), σ_q_to_lb (m), σ_q_to_ub (m) - -Total: 6n + 12m + 6k + n_ref -""" -function kkt_dims(prob::ACOPFProblem) - n, m, k = prob.network.n, prob.network.m, prob.n_gen - n_ref = length(prob.data.ref_bus_keys) - return 6n + 12m + 6k + n_ref -end - -""" - kkt_indices(n, m, k, n_ref) → NamedTuple - -Compute all KKT variable indices from problem dimensions. -Single source of truth for index calculations. - -# Variable ordering -[va(n), vm(n), pg(k), qg(k), - ν_p_bal(n), ν_q_bal(n), ν_ref_bus(n_ref), - λ_thermal_fr(m), λ_thermal_to(m), λ_angle_lb(m), λ_angle_ub(m), - μ_vm_lb(n), μ_vm_ub(n), - ρ_pg_lb(k), ρ_pg_ub(k), ρ_qg_lb(k), ρ_qg_ub(k), - σ_p_fr_lb(m), σ_p_fr_ub(m), σ_q_fr_lb(m), σ_q_fr_ub(m), - σ_p_to_lb(m), σ_p_to_ub(m), σ_q_to_lb(m), σ_q_to_ub(m)] - -# Returns -NamedTuple with index ranges for each variable block. -""" -function kkt_indices(n::Int, m::Int, k::Int, n_ref::Int) +function _ac_kkt_indices(n::Int, m::Int, k::Int, n_ref::Int) i = 0 # Primal idx_va = (i+1):(i+n); i += n @@ -130,9 +91,34 @@ function kkt_indices(n::Int, m::Int, k::Int, n_ref::Int) ) end -function kkt_indices(prob::ACOPFProblem) +@inline function _ac_kkt_layout(n::Int, m::Int, k::Int, n_ref::Int) + return _ac_kkt_dims(n, m, k, n_ref), _ac_kkt_indices(n, m, k, n_ref) +end + +"""Return the AC KKT index ranges for the supplied bus, branch, generator, and reference-bus counts.""" +kkt_indices(n::Int, m::Int, k::Int, n_ref::Int) = + last(_ac_kkt_layout(n, m, k, n_ref)) + +""" + kkt_layout(network::ACNetwork) → (dim, indices) + kkt_layout(prob::ACOPFProblem) → (dim, indices) + +Return the dimension and named index ranges of the flattened AC KKT variable +vector. + +The layout contains the `va`, `vm`, `pg`, and `qg` primal blocks followed by +the power-balance, reference-bus, thermal-limit, angle-limit, voltage-limit, +generation-limit, and reduced-space flow-bound dual blocks. With `n` buses, +`m` branches, `k` generators, and `n_ref` reference buses, the total dimension +is `6n + 12m + 6k + n_ref`. +""" +function kkt_layout(net::ACNetwork) + return _ac_kkt_layout(net.n, net.m, length(net.gen_bus), length(net.ref_bus_keys)) +end + +function kkt_layout(prob::ACOPFProblem) n_ref = length(prob.data.ref_bus_keys) - kkt_indices(prob.network.n, prob.network.m, prob.n_gen, n_ref) + return _ac_kkt_layout(prob.network.n, prob.network.m, prob.n_gen, n_ref) end # ============================================================================= @@ -143,7 +129,7 @@ end flatten_variables(sol::ACOPFSolution, prob::ACOPFProblem) Flatten solution primal and dual variables into a single vector for KKT evaluation. -Ordering matches `kkt_indices`. +Ordering matches the indices returned by `kkt_layout`. """ function flatten_variables(sol::ACOPFSolution, prob::ACOPFProblem) return vcat( @@ -633,7 +619,7 @@ function calc_kkt_jacobian(prob::ACOPFProblem; sol::Union{ACOPFSolution,Nothing} sol = _ensure_ac_solved!(prob) end - idx = kkt_indices(prob) + dim, idx = kkt_layout(prob) constants = _extract_kkt_constants(prob) prob.cache.kkt_constants = constants cq = _extract_gen_cq(prob) @@ -642,7 +628,6 @@ function calc_kkt_jacobian(prob::ACOPFProblem; sol::Union{ACOPFSolution,Nothing} vars = unflatten_variables(flatten_variables(sol, prob), idx) n, m, k = prob.network.n, prob.network.m, prob.n_gen - dim = kkt_dims(prob) row_idxs = Int[] col_idxs = Int[] vals = Float64[] @@ -1203,9 +1188,9 @@ function calc_kkt_jacobian_param(prob::ACOPFProblem, sol::ACOPFSolution, param:: haskey(_AC_PARAM_EXTRACT, param) || throw(ArgumentError( "Unknown AC OPF parameter: $param. Valid: $(keys(_AC_PARAM_EXTRACT))")) - idx = kkt_indices(prob) + dim, idx = kkt_layout(prob) p0 = _AC_PARAM_EXTRACT[param](prob) - Jp = zeros(Float64, kkt_dims(prob), length(p0)) + Jp = zeros(Float64, dim, length(p0)) constants = _require_kkt_constants(prob) @@ -1359,10 +1344,10 @@ Compute a single analytical column of ∂K/∂param without materializing the fu parameter Jacobian. """ function _calc_ac_kkt_param_column(prob::ACOPFProblem, sol::ACOPFSolution, param::Symbol, col_idx::Int) - idx = kkt_indices(prob) + dim, idx = kkt_layout(prob) vars = sol p0 = _AC_PARAM_EXTRACT[param](prob) - Kcol = zeros(Float64, kkt_dims(prob)) + Kcol = zeros(Float64, dim) constants = _require_kkt_constants(prob) diff --git a/src/prob/kkt_dc_opf.jl b/src/prob/kkt_dc_opf.jl index 68a1ec0..d3cb179 100644 --- a/src/prob/kkt_dc_opf.jl +++ b/src/prob/kkt_dc_opf.jl @@ -268,54 +268,11 @@ function _extract_dz_column(prob::DCOPFProblem, dz_col::Vector{Float64}, op::Sym end # ============================================================================= -# Dimension Calculations +# KKT Layout # ============================================================================= -""" - kkt_dims(prob::DCOPFProblem) - kkt_dims(network::DCNetwork) - -Compute the dimension of the flattened KKT variable vector. - -The KKT system includes: -- Primal: va (n), pg (k), f (m), psh (n) -- Dual (inequality): lam_lb (m), lam_ub (m), gamma_lb (m), gamma_ub (m), rho_lb (k), rho_ub (k), mu_lb (n), mu_ub (n) -- Dual (equality): nu_bal (n), nu_flow (m) -- Reference bus constraints: n_ref - -Total: 5n + 6m + 3k + n_ref -""" -function kkt_dims(prob::DCOPFProblem) - net = getfield(prob, :network) - return _dc_kkt_dims(getfield(net, :n), getfield(net, :m), getfield(net, :k), - getfield(prob, :_n_ref)) -end -kkt_dims(n::Int, m::Int, k::Int) = _dc_kkt_dims(n, m, k, 1) _dc_kkt_dims(n::Int, m::Int, k::Int, n_ref::Int) = 5n + 6m + 3k + n_ref -function kkt_dims(net::DCNetwork) - n = getfield(net, :n) - m = getfield(net, :m) - k = getfield(net, :k) - n_ref = length(_reference_buses(net)) - # va(n) + pg(k) + f(m) + psh(n) + lam_lb(m) + lam_ub(m) + gamma_lb(m) + gamma_ub(m) + rho_lb(k) + rho_ub(k) + mu_lb(n) + mu_ub(n) + nu_bal(n) + nu_flow(m) + ref(n_ref) - return _dc_kkt_dims(n, m, k, n_ref) -end - -""" - kkt_indices(n, m, k) → NamedTuple - -Compute all KKT variable indices from network dimensions. -Single source of truth for index calculations. - -# Variable ordering -[va(n), pg(k), f(m), psh(n), lam_lb(m), lam_ub(m), gamma_lb(m), gamma_ub(m), rho_lb(k), rho_ub(k), mu_lb(n), mu_ub(n), nu_bal(n), nu_flow(m), eta(n_ref)] - -# Returns -NamedTuple with index ranges for each variable block. -""" -kkt_indices(n::Int, m::Int, k::Int) = _dc_kkt_indices(n, m, k, 1) - function _dc_kkt_indices(n::Int, m::Int, k::Int, n_ref::Int) i = 0 idx_θ = (i+1):(i+n); i += n @@ -344,29 +301,43 @@ function _dc_kkt_indices(n::Int, m::Int, k::Int, n_ref::Int) ) end -kkt_indices(net::DCNetwork) = _dc_kkt_indices(net.n, net.m, net.k, length(_reference_buses(net))) - -function kkt_indices(prob::DCOPFProblem) - net = getfield(prob, :network) - return _dc_kkt_indices(getfield(net, :n), getfield(net, :m), getfield(net, :k), - getfield(prob, :_n_ref)) +@inline function _dc_kkt_layout(n::Int, m::Int, k::Int, n_ref::Int) + return _dc_kkt_dims(n, m, k, n_ref), _dc_kkt_indices(n, m, k, n_ref) end -function _dc_kkt_layout(net::DCNetwork) - n_ref = length(_reference_buses(net)) +"""Return the DC KKT dimension for `n` buses, `m` branches, and `k` generators, assuming one reference bus.""" +kkt_dims(n::Int, m::Int, k::Int) = first(_dc_kkt_layout(n, m, k, 1)) + +"""Return the DC KKT index ranges for `n` buses, `m` branches, and `k` generators, assuming one reference bus.""" +kkt_indices(n::Int, m::Int, k::Int) = last(_dc_kkt_layout(n, m, k, 1)) + +""" + kkt_layout(network::DCNetwork) → (dim, indices) + kkt_layout(prob::DCOPFProblem) → (dim, indices) + +Return the dimension and named index ranges of the flattened DC KKT variable +vector. + +The variable ordering is `[va, pg, f, psh, lam_lb, lam_ub, gamma_lb, +gamma_ub, rho_lb, rho_ub, mu_lb, mu_ub, nu_bal, nu_flow, η]`. With `n` +buses, `m` branches, `k` generators, and `n_ref` energized islands, the total +dimension is `5n + 6m + 3k + n_ref`. +""" +function kkt_layout(net::DCNetwork) n = getfield(net, :n) m = getfield(net, :m) k = getfield(net, :k) - return _dc_kkt_dims(n, m, k, n_ref), _dc_kkt_indices(n, m, k, n_ref) + n_ref = length(_reference_buses(net)) + return _dc_kkt_layout(n, m, k, n_ref) end -function _dc_kkt_layout(prob::DCOPFProblem) +function kkt_layout(prob::DCOPFProblem) net = getfield(prob, :network) - n_ref = getfield(prob, :_n_ref) n = getfield(net, :n) m = getfield(net, :m) k = getfield(net, :k) - return _dc_kkt_dims(n, m, k, n_ref), _dc_kkt_indices(n, m, k, n_ref) + n_ref = getfield(prob, :_n_ref) + return _dc_kkt_layout(n, m, k, n_ref) end # ============================================================================= @@ -615,7 +586,7 @@ function calc_kkt_jacobian(net::DCNetwork, d::AbstractVector, prob::DCOPFProblem k = getfield(net, :k) refs = _reference_buses(net) n_ref = length(refs) - dim = _dc_kkt_dims(n, m, k, n_ref) + dim, idx = kkt_layout(prob) A = getfield(net, :A) G_inc = getfield(net, :G_inc) b = getfield(net, :b) @@ -644,8 +615,7 @@ function calc_kkt_jacobian(net::DCNetwork, d::AbstractVector, prob::DCOPFProblem W = Diagonal(-b .* sw) B_mat = sparse(A' * W * A) - # Build Jacobian blocks using centralized index calculation - idx = _dc_kkt_indices(n, m, k, n_ref) + # Build Jacobian blocks using the centralized layout. A_rowptr, A_rowcols, A_rowvals = _sparse_row_storage(A) G_rowptr, G_rowcols, G_rowvals = _sparse_row_storage(G_inc) rowval = Int[] @@ -859,7 +829,7 @@ function calc_kkt_jacobian_demand(net::DCNetwork, d::AbstractVector, sol::DCOPFS n = getfield(net, :n) m = getfield(net, :m) k = getfield(net, :k) - dim, idx = _dc_kkt_layout(net) + dim, idx = kkt_layout(net) mu_ub = getfield(sol, :mu_ub) colptr = Vector{Int}(undef, n + 1) @@ -887,7 +857,7 @@ Compute column `j` of the KKT parameter Jacobian ∂K/∂d. Only 1-2 nonzeros: always `nu_bal[j]`, plus `mu_ub[j]` if `d[j] > 0`. """ function calc_kkt_jacobian_demand_column(net::DCNetwork, d::AbstractVector, sol::DCOPFSolution, j::Int) - dim, idx = _dc_kkt_layout(net) + dim, idx = kkt_layout(net) col = zeros(dim) col[idx.nu_bal[j]] = -1.0 col[idx.mu_ub[j]] = sol.mu_ub[j] * _shed_capacity_derivative(d[j]) @@ -922,7 +892,7 @@ enabling gradient-based optimization for topology control. function calc_kkt_jacobian_switching(prob::DCOPFProblem, sol::DCOPFSolution) net = prob.network n, m, k = net.n, net.m, net.k - dim, idx = _dc_kkt_layout(prob) + dim, idx = kkt_layout(prob) θ = sol.va @@ -986,7 +956,7 @@ Compute column `e` of the KKT parameter Jacobian ∂K/∂sw. """ function calc_kkt_jacobian_switching_column(prob::DCOPFProblem, sol::DCOPFSolution, e::Int) net = prob.network - dim, idx = _dc_kkt_layout(prob) + dim, idx = kkt_layout(prob) col = zeros(dim) A = net.A; b = net.b; θ = sol.va Aθ_e = dot(A[e, :], θ) diff --git a/src/sens/cost.jl b/src/sens/cost.jl index 72aa176..efcf386 100644 --- a/src/sens/cost.jl +++ b/src/sens/cost.jl @@ -37,7 +37,7 @@ function calc_kkt_jacobian_cost_linear(net::DCNetwork) n = getfield(net, :n) m = getfield(net, :m) k = getfield(net, :k) - dim, idx = _dc_kkt_layout(net) + dim, idx = kkt_layout(net) colptr = Vector{Int}(undef, k + 1) rowval = Int[] @@ -61,7 +61,7 @@ end Compute column `j` of ∂K/∂cl. Only 1 nonzero: `pg[j] = 1.0`. """ function calc_kkt_jacobian_cost_linear_column(net::DCNetwork, j::Int) - dim, idx = _dc_kkt_layout(net) + dim, idx = kkt_layout(net) col = zeros(dim) col[idx.pg[j]] = 1.0 return col @@ -92,7 +92,7 @@ function calc_kkt_jacobian_cost_quadratic(prob::DCOPFProblem, sol::DCOPFSolution n = getfield(net, :n) m = getfield(net, :m) k = getfield(net, :k) - dim, idx = _dc_kkt_layout(prob) + dim, idx = kkt_layout(prob) g = getfield(sol, :pg) @@ -120,7 +120,7 @@ end Compute column `j` of ∂K/∂cq. Only 1 nonzero: `pg[j] = 2*g[j]`. """ function calc_kkt_jacobian_cost_quadratic_column(net::DCNetwork, sol::DCOPFSolution, j::Int) - dim, idx = _dc_kkt_layout(net) + dim, idx = kkt_layout(net) col = zeros(dim) col[idx.pg[j]] = 2.0 * sol.pg[j] return col diff --git a/src/sens/flowlimit.jl b/src/sens/flowlimit.jl index e820c84..fd6af75 100644 --- a/src/sens/flowlimit.jl +++ b/src/sens/flowlimit.jl @@ -53,7 +53,7 @@ function calc_kkt_jacobian_flowlimit(prob::DCOPFProblem, sol::DCOPFSolution) n = getfield(net, :n) m = getfield(net, :m) k = getfield(net, :k) - dim, idx = _dc_kkt_layout(prob) + dim, idx = kkt_layout(prob) lambda_lb = getfield(sol, :lam_lb) lambda_ub = getfield(sol, :lam_ub) @@ -83,7 +83,7 @@ end Compute column `e` of ∂K/∂fmax. Only 2 nonzeros: `lam_lb[e]` and `lam_ub[e]`. """ function calc_kkt_jacobian_flowlimit_column(net::DCNetwork, sol::DCOPFSolution, e::Int) - dim, idx = _dc_kkt_layout(net) + dim, idx = kkt_layout(net) col = zeros(dim) col[idx.lam_lb[e]] = sol.lam_lb[e] col[idx.lam_ub[e]] = sol.lam_ub[e] diff --git a/src/sens/susceptance.jl b/src/sens/susceptance.jl index 1fe599e..d4e7618 100644 --- a/src/sens/susceptance.jl +++ b/src/sens/susceptance.jl @@ -51,7 +51,7 @@ Derivatives: function calc_kkt_jacobian_susceptance(prob::DCOPFProblem, sol::DCOPFSolution) net = prob.network n, m, k = net.n, net.m, net.k - dim, idx = _dc_kkt_layout(prob) + dim, idx = kkt_layout(prob) theta = sol.va nu_bal = sol.nu_bal @@ -91,7 +91,7 @@ Compute column `e` of ∂K/∂b. ~6 nonzeros from the incidence structure of bra """ function calc_kkt_jacobian_susceptance_column(prob::DCOPFProblem, sol::DCOPFSolution, e::Int) net = prob.network - dim, idx = _dc_kkt_layout(prob) + dim, idx = kkt_layout(prob) col = zeros(dim) A = net.A; sw = net.sw; θ = sol.va Aθ_e = dot(A[e, :], θ) diff --git a/src/sens/vjp_jvp.jl b/src/sens/vjp_jvp.jl index c643e93..58dd66d 100644 --- a/src/sens/vjp_jvp.jl +++ b/src/sens/vjp_jvp.jl @@ -268,10 +268,10 @@ function _dcopf_vjp(prob::DCOPFProblem, op::Symbol, param::Symbol, adj::Abstract end function _dcopf_jvp(prob::DCOPFProblem, op::Symbol, param::Symbol, tang::AbstractVector) - idx = kkt_indices(prob) + dim, idx = kkt_layout(prob) odim = length(_dc_operand_kkt_rows(idx, op)) out = Vector{Float64}(undef, odim) - work = Vector{Float64}(undef, kkt_dims(prob)) + work = Vector{Float64}(undef, dim) return _dcopf_jvp!(out, prob, op, param, tang, work) end @@ -398,7 +398,7 @@ end # ============================================================================= function _acopf_vjp(prob::ACOPFProblem, op::Symbol, param::Symbol, adj::AbstractVector) - idx = kkt_indices(prob) + dim, idx = kkt_layout(prob) op_rows = _ac_operand_kkt_rows(idx, op) sign = _ac_operand_sign(op) @@ -414,7 +414,7 @@ function _acopf_vjp(prob::ACOPFProblem, op::Symbol, param::Symbol, adj::Abstract kkt_lu = _ensure_ac_kkt_factor!(prob) ctx = _ac_kkt_context(prob) - w = zeros(kkt_dims(prob)) + w = zeros(dim) w[op_rows] .= sign .* adj u = kkt_lu' \ w @@ -425,7 +425,7 @@ function _acopf_vjp(prob::ACOPFProblem, op::Symbol, param::Symbol, adj::Abstract end function _acopf_jvp(prob::ACOPFProblem, op::Symbol, param::Symbol, tang::AbstractVector) - idx = kkt_indices(prob) + dim, idx = kkt_layout(prob) op_rows = _ac_operand_kkt_rows(idx, op) sign = _ac_operand_sign(op) @@ -440,7 +440,7 @@ function _acopf_jvp(prob::ACOPFProblem, op::Symbol, param::Symbol, tang::Abstrac kkt_lu = _ensure_ac_kkt_factor!(prob) ctx = _ac_kkt_context(prob) - v = zeros(kkt_dims(prob)) + v = zeros(dim) _ac_param_jvp!(v, prob, ctx, param, tang) u = kkt_lu \ v diff --git a/src/types/abstract.jl b/src/types/abstract.jl index 81e984b..fe2d04c 100644 --- a/src/types/abstract.jl +++ b/src/types/abstract.jl @@ -91,19 +91,38 @@ appropriate formulation (DC or AC). function calc_kkt_jacobian end """ - kkt_dims(prob::AbstractOPFProblem) → Int + kkt_layout(system) → (dim, indices) + +Return the layout of the flattened KKT system for an OPF network or problem. +`dim` is the total dimension and `indices` is a named tuple containing the +index range of every primal and dual variable block. + +This is the primary entry point for KKT shape information. Use `kkt_dims` or +`kkt_indices` when only one part of the layout is needed. +""" +function kkt_layout end -Total dimension of the KKT system for the given OPF problem. +""" + kkt_dims(system) → Int + +Return the total dimension of the KKT system. This is a convenience equivalent +to `first(kkt_layout(system))`. """ function kkt_dims end """ - kkt_indices(prob::AbstractOPFProblem) → NamedTuple + kkt_indices(system) → NamedTuple -Index ranges for primal and dual variables in the flattened KKT vector. +Return the named index ranges for primal and dual variables in the flattened +KKT vector. This is a convenience equivalent to `last(kkt_layout(system))`. """ function kkt_indices end +@inline kkt_dims(system::Union{AbstractPowerNetwork,AbstractOPFProblem}) = + first(kkt_layout(system)) +@inline kkt_indices(system::Union{AbstractPowerNetwork,AbstractOPFProblem}) = + last(kkt_layout(system)) + """ flatten_variables(sol::AbstractOPFSolution, prob::AbstractOPFProblem) → Vector diff --git a/test/runtests.jl b/test/runtests.jl index da1ab48..265acb1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -23,13 +23,32 @@ using Ipopt using JuMP: MOI, optimizer_with_attributes # Import non-exported KKT functions used by tests -import PowerDiff: kkt, kkt_dims, kkt_indices, calc_kkt_jacobian, +import PowerDiff: kkt, calc_kkt_jacobian, flatten_variables, unflatten_variables PowerModels.silence() include("common.jl") +struct UnsupportedKKTNetwork <: AbstractPowerNetwork end + +@testset "KKT Layout Interface" begin + @test all(name -> name in names(PowerDiff), (:kkt_layout, :kkt_dims, :kkt_indices)) + + # Convenience accessors for third-party network types identify the missing + # primary interface method directly. + err = try + kkt_dims(UnsupportedKKTNetwork()) + nothing + catch caught + caught + end + @test err isa MethodError + if err isa MethodError + @test err.f === kkt_layout + end +end + # ============================================================================= # DC OPF Tests # ============================================================================= @@ -149,8 +168,11 @@ end # KKT dimension: n(θ) + k(g) + m(f) + n(psh) + n(ν_bal) + m(ν_flow) + # 2m(λ_ub/lb) + 2k(ρ_ub/lb) + 2n(μ_ub/lb) + 2m(γ_ub/lb) + 1(η_ref) # = 5n + 6m + 3k + 1 - dim = kkt_dims(dc_net) + dim, idx = kkt_layout(dc_net) @test dim == 5*dc_net.n + 6*dc_net.m + 3*dc_net.k + 1 + @test kkt_layout(prob) == (dim, idx) + @test kkt_dims(dc_net) == dim + @test kkt_indices(dc_net) == idx # Test flatten/unflatten round-trip z = flatten_variables(sol, prob) @@ -165,7 +187,6 @@ end K = kkt(z, prob, d) # Note: complementary slackness won't be exactly zero due to interior point solver # Check stationarity and feasibility conditions using centralized indices - idx = kkt_indices(dc_net) # Primal feasibility should be very tight @test norm(K[idx.nu_bal]) < 1e-4 end @@ -178,7 +199,13 @@ end else ac_prob = ACOPFProblem(net) ac_sol = solve!(ac_prob) + dim, idx = kkt_layout(ac_prob) + @test kkt_layout(ac_prob.network) == (dim, idx) + @test kkt_dims(ac_prob) == dim + @test kkt_indices(ac_prob) == idx + @test last(idx.sig_q_to_ub) == dim z = flatten_variables(ac_sol, ac_prob) + @test length(z) == dim K = kkt(z, ac_prob) # No NaN sentinels survived (validates pre-allocated index assignment is complete) diff --git a/test/test_dc_islands.jl b/test/test_dc_islands.jl index e17483b..7b0d5df 100644 --- a/test/test_dc_islands.jl +++ b/test/test_dc_islands.jl @@ -88,7 +88,8 @@ end solve!(prob) @test reference_buses(net) == [1] - dim_connected = kkt_dims(prob) + dim_connected, idx_connected = kkt_layout(prob) + @test last(idx_connected.η) == dim_connected out = zeros(net.n) jvp!(out, prob, :lmp, :d, ones(net.n)) @test length(prob.cache.work) == dim_connected @@ -102,7 +103,12 @@ end @test length(prob.cons.ref) == 2 sol = solve!(prob) - @test kkt_dims(prob) == dim_connected + 1 + dim_islanded, idx_islanded = kkt_layout(prob) + @test dim_islanded == dim_connected + 1 + @test length(idx_islanded.η) == 2 + @test kkt_layout(net) == (dim_islanded, idx_islanded) + @test kkt_dims(prob) == dim_islanded + @test kkt_indices(prob) == idx_islanded @test sol.va[reference_buses(net)] ≈ zeros(2) atol=1e-8 @test norm(kkt(flatten_variables(sol, prob), prob, d), Inf) < 1e-4