Apply the angle gate to the matrix-free switching sensitivity - #74
Open
samtalki wants to merge 4 commits into
Open
Apply the angle gate to the matrix-free switching sensitivity#74samtalki wants to merge 4 commits into
samtalki wants to merge 4 commits into
Conversation
`calc_kkt_jacobian_switching` carries three gated angle difference terms in dK/dsw: the va block picks up dgate * (gamma_ub - gamma_lb) * a_e, and each gamma block picks up its own complementary slackness row. The matrix-free path in `_dc_topo_vjp!`/`_dc_topo_jvp!` covered only the nu_bal, nu_flow, and va blocks arising from B and W, so it dropped all three. `jvp(prob, op, :sw, v)` and `vjp(prob, op, :sw, w)` therefore disagreed with `calc_sensitivity(prob, op, :sw)` whenever an angle limit was binding, and only on the cold-cache path: `_dcopf_vjp!`/`_dcopf_jvp!` short-circuit to a cached dz/dsw matrix multiply when one exists, so the same call returned different answers depending on whether `calc_sensitivity` had run first. Take the parameter symbol rather than a pre-selected coefficient vector so the two kernels can tell :sw from :b, and add the gate terms on the :sw path only. :b correctly has none: the effective gate is locally constant in b within a fixed energized regime and zero by convention at the nonsmooth b == 0 boundary. Reuse `_angle_difference_gate_dsw` so the b == 0 rule keeps a single definition shared with the materialized Jacobian. The va contribution folds into the existing w_va accumulator, so neither path gains a matvec or an allocation. The gap was invisible because `test_kkt_vjp_jvp.jl` called `calc_sensitivity` immediately before each vjp/jvp comparison, warming the cache so the comparison silently exercised the fast path and never reached the inline kernels. Assert both paths against the same expectation there, using a problem instance that `calc_sensitivity` never sees, which covers every (op, param) pair rather than just :sw. Add a direct regression in `test_angle_diff_duals.jl` under the existing binding-angle network, where gamma_ub is nonzero and the two paths diverge without this fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Z1wMj3KNzVgbdFfZtn67m
The angle gate multiplies both sides of the phase angle difference bounds, so on an energized branch the factor cancels and the primal constraint is exactly angmin <= (A*theta)_e <= angmax for every sw_e in (0, 1]. "Retaining fractional switching on energized branches" read as though a partially closed branch relaxes the limit; say instead that fractional switching rescales the angle limit duals. Also remove the blank line left inside the branch loop in `calc_kkt_jacobian_susceptance`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Z1wMj3KNzVgbdFfZtn67m
`_dc_topo_vjp!` evaluated `param === :sw` twice, once to select `coeff` and once for the loop-body guard, and `_dc_topo_jvp!` re-tested the symbol inline while its sibling used a named local. Compute `gated` once in both and derive `coeff` from it, so the two kernels read the same way. No behavior change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Z1wMj3KNzVgbdFfZtn67m
The zero-susceptance testset FD-verified `calc_kkt_jacobian_switching` and its column form on both sides of b == 0, but never exercised the matrix-free path. That path reaches `_angle_difference_gate_dsw` through its own call site, so the two can drift apart at exactly the boundary whose convention this branch documents. Compare `_dc_param_jvp!`/`_dc_param_vjp!` against the materialized column, at b == 0 and at b = +/-1e-3 where the gate derivative is one. These call the inline kernels directly instead of going through vjp/jvp, for two reasons: vjp/jvp resolve the solution from the problem cache and would discard the synthetic gammas the surrounding testset relies on, and the solver canonicalizes gamma to zero on a de-energized branch, which would leave the b == 0 case vacuous either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Z1wMj3KNzVgbdFfZtn67m
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #71 — base is
ck/issue-63-energized-angle-gates, so this diff is just the two commits on top. GitHub will retarget tomainwhen #71 merges.Summary
∂K/∂swinsrc/sens/vjp_jvp.jl, which Gate DC angle limits by energized branches #71 applied to the materialized Jacobians but not here.test_kkt_vjp_jvp.jl, for every(op, param)pair.test_angle_diff_duals.jl.The gap
calc_kkt_jacobian_switchingcarries three gated angle terms in∂K/∂sw: thevablock picks updgate * (γ_ub - γ_lb) * aₑ, and each gamma block picks up its own complementary-slackness row. The matrix-free path in_dc_topo_vjp!/_dc_topo_jvp!covered only thenu_bal,nu_flow, andvablocks arising fromBandW, so it dropped all three.Consequence:
jvp(prob, op, :sw, v)andvjp(prob, op, :sw, w)disagreed withcalc_sensitivity(prob, op, :sw)whenever an angle limit was binding — and only on the cold-cache path._dcopf_vjp!/_dcopf_jvp!short-circuit to a cacheddz/dswmatrix multiply when one exists, so the same call returned different answers depending on whethercalc_sensitivityhad run first.This predates #71. It surfaced while reviewing it, and #71 is the natural place to close it out since its own summary covers the switching parameter Jacobians.
Why only
:swBoth kernels previously served
:swand:bthrough one shared body, selected by passingnet.bornet.swas a barecoeffvector.:bgenuinely has no angle-gate terms — #71 establishes that the effective gate is locally constant inbwithin a fixed energized regime, and zero by convention at the nonsmoothb == 0boundary. That correct-for-:bomission is exactly what made the shared kernel misleading.The kernels now take the parameter symbol and derive
coeffthemselves, so they can tell the two apart, and the gate terms are added on the:swpath only._angle_difference_gate_dswis reused rather than re-deriving theb == 0rule, keeping one definition shared with the materialized Jacobian. Thevacontribution folds into the existingw_vaaccumulator, so neither path gains a matvec or an allocation.Why CI was green
test_kkt_vjp_jvp.jlcalledcalc_sensitivityimmediately before each vjp/jvp comparison, warmingprob.cache.dz_dswso the comparison silently took the cached fast path and never reached the inline kernels. Separately, the binding-angle case intest_angle_diff_duals.jlonly checkedcalc_sensitivity. The two conditions needed to expose this never co-occurred.Both are addressed: the comparison loops now assert the inline path against a problem instance
calc_sensitivitynever sees, in addition to the cached path — covering every parameter, not just:sw.Verification status
Nothing has been executed. There is no Julia toolchain available in the environment this was written in, and CI does not run on branch pushes — only on
pull_request. Opening this PR is what triggers the first run.The signs of each added term were derived by hand against the corresponding entry in
calc_kkt_jacobian_switching, under theout = -(∂K/∂p)ᵀ·uconvention that the existing terms in the same function follow. That is reasoning, not a passing suite. Please read the CI result on this PR as the actual evidence.Once green, the chain is: matrix-free
:sw≡ materialized Jacobian (new tests) ≡ finite differences (#71's existing FD loop in the same testset).Generated by Claude Code