Skip to content

Apply the angle gate to the matrix-free switching sensitivity - #74

Open
samtalki wants to merge 4 commits into
ck/issue-63-energized-angle-gatesfrom
claude/cameron-pr-71-72-review-if1asi
Open

Apply the angle gate to the matrix-free switching sensitivity#74
samtalki wants to merge 4 commits into
ck/issue-63-energized-angle-gatesfrom
claude/cameron-pr-71-72-review-if1asi

Conversation

@samtalki

Copy link
Copy Markdown
Member

Stacked on #71 — base is ck/issue-63-energized-angle-gates, so this diff is just the two commits on top. GitHub will retarget to main when #71 merges.

Summary

  • Add the gated angle difference terms to the matrix-free ∂K/∂sw in src/sens/vjp_jvp.jl, which Gate DC angle limits by energized branches #71 applied to the materialized Jacobians but not here.
  • Assert both the inline and cached vjp/jvp paths in test_kkt_vjp_jvp.jl, for every (op, param) pair.
  • Add a direct regression under the existing binding-angle network in test_angle_diff_duals.jl.
  • Clarify the fractional-switching wording in the DC OPF math docs; drop a stray blank line.

The gap

calc_kkt_jacobian_switching carries three gated angle terms in ∂K/∂sw: the va block picks up dgate * (γ_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 the nu_bal, nu_flow, and va blocks arising from B and W, so it dropped all three.

Consequence: jvp(prob, op, :sw, v) and vjp(prob, op, :sw, w) 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.

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 :sw

Both kernels previously served :sw and :b through one shared body, selected by passing net.b or net.sw as a bare coeff vector. :b genuinely has no angle-gate terms — #71 establishes that the effective gate is locally constant in b within a fixed energized regime, and zero by convention at the nonsmooth b == 0 boundary. That correct-for-:b omission is exactly what made the shared kernel misleading.

The kernels now take the parameter symbol and derive coeff themselves, so they can tell the two apart, and the gate terms are added on the :sw path only. _angle_difference_gate_dsw is reused rather than re-deriving the b == 0 rule, keeping one 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.

Why CI was green

test_kkt_vjp_jvp.jl called calc_sensitivity immediately before each vjp/jvp comparison, warming prob.cache.dz_dsw so the comparison silently took the cached fast path and never reached the inline kernels. Separately, the binding-angle case in test_angle_diff_duals.jl only checked calc_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_sensitivity never 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 the out = -(∂K/∂p)ᵀ·u convention 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

claude added 4 commits August 12, 2026 02:07
`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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants