Skip to content

PDL for parametric AC-OPF: paper-faithful implementation with analytical dual#6

Open
xkhainguyen wants to merge 5 commits into
LearningToOptimize:ar/simple_primal_dualfrom
xkhainguyen:worktree-case57-pdl
Open

PDL for parametric AC-OPF: paper-faithful implementation with analytical dual#6
xkhainguyen wants to merge 5 commits into
LearningToOptimize:ar/simple_primal_dualfrom
xkhainguyen:worktree-case57-pdl

Conversation

@xkhainguyen
Copy link
Copy Markdown

Summary

Implementation of Primal-Dual Learning (PDL) (Park & Van Hentenryck, AAAI 2023) for parametric AC-OPF, with several fixes and extensions over a naive baseline.

Fixes to the base implementation

Fix Effect
Paper-faithful augmented Lagrangian loss (raw g, h from BNK.constraints!) Correct Lagrangian gradient; sign of μᵀg term pushes primal toward feasibility
Clamp inequality multipliers to μ ≥ 0 KKT sign correctness
BoundedOutput sigmoid head max_bound_viol ≡ 0 by construction; no dead gradient zone (vs hardsigmoid)
FixRefBus layer Reference-bus angle fixed architecturally; GPU-safe
Analytical primal loss Eliminates dual tracking gap — see below

Analytical dual correction

The dual network lags the true multipliers by orders of magnitude at high ρ. Instead of using λ̂(θ) directly, we apply one ALM update analytically at every primal gradient step:

μ_eff = clamp(μ̂(θ) + ρ·g(ŷ),  0,  max_dual)
λ_eff = clamp(λ̂(θ) + ρ·h(ŷ), −max_dual, max_dual)

Gradient flows through g(ŷ) and h(ŷ) — not through the dual net — giving the correct ρg² and ρh² penalty terms without tracking lag. The dual network still trains in parallel, providing an improving warm-start base over time.

API

Both options live in ALMMethod (defaults shown):

method = ALMMethod(;
    batch_model         = bm,
    num_equal           = num_equal,
    use_analytical_dual = true,   # recommended; false = use dual net output directly
    use_dual_learning   = true,   # false = freeze dual network (ablation only)
    ρmax = 1e4, max_dual = 1e6, τ = 0.8, α = 2.0,
)
train!(method, trainer, data; K=100, L_primal=2500, L_dual=2500, warmup_epochs=25000)

Results on case57 (5000 held-out test samples)

Config max_eq max_ineq
use_analytical_dual=true, ρmax=1e4, max_dual=1e6 1.165 0.000
use_analytical_dual=false ~1.25 0.000
max_dual=1e4 (any ρ schedule) ~1.80 0.000

Variable bounds and inequalities are satisfied exactly in all runs.

Test plan

  • julia --project=. -e 'using Pkg; Pkg.test()' passes on CPU
  • BNK_TEST_CUDA=1 julia --project=. -e 'using Pkg; Pkg.test()' passes on GPU
  • julia --project=. examples/case57_train.jl runs to completion

…efBus, ρ_eq_scale

Key changes to src/L2OALM.jl:
- Paper-faithful augmented Lagrangian loss (correct g,h from BNK.constraints!)
- Clamp inequality multipliers to μ ≥ 0 (KKT correctness)
- Analytical primal loss: apply ALM dual update per gradient step
  (λ_eff = clamp(λ̂ + ρ·h, −M, M)); eliminates dual tracking gap
- ρ_eq_scale: separate penalty multiplier for equality constraints
- use_penalty_only kwarg for penalty-only (no dual) ablation

Key changes to test/power.jl and examples/case57_train.jl:
- BoundedOutput head: sigmoid variable-bound enforcement (max_bound_viol ≡ 0)
- FixRefBus layer: reference-bus angle fixed to zero architecturally; GPU-safe
- Reduced-space AC-OPF: eliminate branch-flow variables
- Warmup at ρ_max, lr decay, env-var hyperparameter overrides

New: examples/case57_train_twophase.jl — two-phase training (Phase 1 fixed ρ
warm-start to skip degenerate basin; Phase 2 growing ρ with ρ_eq_scale).

Best result: max_eq = 1.165 p.u. on 5000 held-out case57 test samples,
max_ineq = max_bound = 0 (analytical dual, ρ_max=1e4, MAX_DUAL=1e6).
Both flags are now ALMMethod fields (defaults: false, true) instead of
train!/single_train_step! kwargs, keeping all method config in one place.

- use_analytical_dual: apply ALM dual update analytically per gradient step
- use_dual_learning: set false to keep the dual network frozen throughout
  (skips dual training loop and the per-outer-iter deepcopy of dual state)

Remove use_penalty_only from single_train_step! (warmup is handled directly
in train! and was never exposed as a kwarg callers needed to set).
@andrewrosemberg
Copy link
Copy Markdown
Member

Amazing!! Let us know when you are ready for a review

@xkhainguyen
Copy link
Copy Markdown
Author

Hi @andrewrosemberg,

I wanted to mention that this is my personal attempt to reproduce the paper's approach. I tried to fix the branch's implementation and make it work on the 57-bus case. The results are reported above. To be honest, I was hoping for even better results, but it may be a limitation of the approach. Feel free to review and even improve or tune it further.

Many thanks!
Khai

@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

ℹ️ You can also turn on project coverage checks and project coverage reporting on Pull Request comment

Thanks for integrating Codecov - We've got you covered ☂️

Updated the CI workflow to include version 1.12 and removed older versions.
@ivanightingale ivanightingale self-requested a review May 28, 2026 22:15
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