13 min read

Optimal Power Flow (OPF)

GAT provides a four-tier solver hierarchy for optimal power flow, from sub-millisecond economic dispatch to production-grade nonlinear optimization. Each tier offers a different accuracy/speed tradeoff, letting you choose the right tool for each task.

Validated Performance: GAT's IPOPT AC-OPF solver achieves <0.01% gap across all 68 PGLib-OPF benchmark cases, matching reference objectives exactly. See the complete benchmark results for details.

Choosing the Right Solver

                    Speed vs. Accuracy Tradeoff

  Fast  ──────────────────────────────────────────►  Accurate

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚     ED      β”‚   β”‚   DC-OPF    β”‚   β”‚    SOCP     β”‚   β”‚   AC-OPF    β”‚
  β”‚   < 1ms     β”‚   β”‚   ~10ms     β”‚   β”‚   ~100ms    β”‚   β”‚    ~1s      β”‚
  β”‚  No network β”‚   β”‚  LP approx  β”‚   β”‚ Convex relaxβ”‚   β”‚  Full NLP   β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                  β”‚                  β”‚                  β”‚
       β–Ό                  β–Ό                  β–Ό                  β–Ό
  Feasibility      N-1 screening       Production        Final
  checks           Planning studies    dispatch          validation

Quick Reference

TierCommandSpeedAccuracyBest For
1gat opf ed< 1ms~20% gapFeasibility checks, generation scheduling
2gat opf dc~10ms~3-5% gapN-1 screening, transmission planning
3gat opf socp~100ms~1-3% gapProduction dispatch, voltage-aware
4gat opf ac~1s< 0.01% gapFinal validation, full physics

Decision Tree

Use Economic Dispatch when:

  • You need instant results (< 1ms)
  • Network constraints don't matter yet
  • Quick "can we meet demand?" checks

Use DC-OPF when:

  • You're screening thousands of contingencies
  • Planning studies where speed matters
  • Real power flows are sufficient

Use SOCP when:

  • You need voltage information
  • Production dispatch decisions
  • Tight bounds on generation cost

Use AC-OPF when:

  • Final operational validation
  • Full physics accuracy required
  • Regulatory compliance

Architecture Overview (v0.5.6)

GAT provides a unified OpfSolver supporting multiple solution methods:

MethodAccuracySpeedStatusUse Case
EconomicDispatch~20% gapFastestβœ… ImplementedQuick estimates, screening
DcOpf~3-5% gapFastβœ… ImplementedPlanning studies
SocpRelaxation~1-3% gapModerateβœ… ImplementedResearch benchmarking
AcOpf (L-BFGS)~2-3% gapModerateβœ… ImplementedPure Rust deployment
AcOpf (IPOPT)<0.01% gapFastβœ… ValidatedHigh-fidelity analysis

Benchmark Results

The IPOPT backend with analytical Jacobian and Hessian achieves exact agreement with PGLib reference values:

CaseGAT ObjectiveReferenceGap
case14_ieee$2,178.08/hr$2,178.10/hr-0.00%
case118_ieee$97,213.61/hr$97,214.00/hr-0.00%

The SOCP solver has been validated against all 68 PGLib-OPF cases:

MetricValue
Cases Tested68
Convergence Rate100%
Largest System78,484 buses
Median Objective Gap< 1%

β†’ View complete benchmark results

What's New in 0.5.6

  • Constraint scaling / row equilibration for DC-OPF LP conditioning
  • Zero-reactance epsilon handling (1e-6) for bus tie transformers
  • Unit-aware newtype wrappers (Megawatts, Kilovolts, PerUnit) for compile-time unit safety
  • IPOPT timing fix with thread-local intermediate callback for accurate iteration reporting
  • Enhanced benchmark appendix with full PGLib DC-OPF and SOCP results

What's New in 0.5.4

  • Security hardening for TUI command execution β€” command allowlisting and metacharacter blocking.
  • Content Security Policy enabled in Tauri GUI β€” XSS protection for desktop app.
  • Type-safe event dispatcher β€” PathBuf and enums replace stringly-typed parameters.

What's New in 0.5.3

  • Arena-based allocation for Monte Carlo and N-k contingency evaluation β€” reduces heap allocations in hot loops.
  • gat-ui-common crate β€” shared UI components between gat-gui and gat-tui.
  • Hot path optimizations for AC-OPF and power flow computations.
  • Strategy pattern architecture for OPF solver selection with unified dispatcher.
  • Improved CI/CD with Native Solvers workflow for IPOPT validation.

What's New in 0.5.2

  • Full nonlinear AC-OPF reproduces all 68 PGLib benchmark cases with <0.01% gap using IPOPT backend.
  • Multi-period dispatch with generator ramp constraints for day-ahead scheduling.
  • IPOPT solver backend with analytical Jacobian and Hessian β€” matches commercial solver precision.
  • Native solver plugin system with automatic fallback to pure-Rust solvers.
  • Warm-start options from DC or SOCP solutions for improved convergence.
  • Native piecewise-linear cost support for bid curves.
  • Generator capability curves (Q limits as function of P).
  • Angle difference constraints for stability enforcement.
  • Sparse Y-bus with O(nnz) storage for efficient large-network handling.
  • Robust Y-bus construction with transformer taps, phase shifters, shunts, and Ο€-model line charging.
  • Shunt support for exact power flow agreement with external tools.

Solver Backends

GAT provides a native solver plugin system that automatically selects the best available backend:

BackendTypeBest ForAvailability
L-BFGS (default)Pure RustGeneral AC-OPF, portabilityAlways available
ClarabelPure RustSOCP, LP problemsAlways available
IPOPTNative (C++)Large NLP, high accuracyOptional installation
HiGHSNative (C++)LP/MIP, high performanceOptional installation
CBCNative (C)MIP problemsOptional installation

Installing Native Solvers

Native solvers provide better performance for large networks but require system dependencies:

# Build and install IPOPT wrapper (requires libipopt-dev)
cargo xtask solver build ipopt --install

# List installed native solvers
gat solver list

# Uninstall a native solver
gat solver uninstall ipopt

Architecture Benefits

Native solvers run as isolated subprocesses communicating via Arrow IPC:

  • Crash isolation: Native library issues don't crash the main process
  • Version flexibility: Different solver versions can coexist
  • Portability: Pure-Rust fallbacks always available when native solvers aren't installed

The solver dispatcher automatically selects the best available backend based on problem class (LP, SOCP, NLP, MIP).

Rust API

OpfSolver

use gat_algo::{OpfSolver, OpfMethod, OpfSolution, OpfError};
use gat_core::Network;

// Create solver with method selection
let solver = OpfSolver::new()
    .with_method(OpfMethod::SocpRelaxation)  // or AcOpf
    .with_tolerance(1e-6)
    .with_max_iterations(100);

// Solve
let solution: OpfSolution = solver.solve(&network)?;

println!("Converged: {}", solution.converged);
println!("Objective: ${:.2}/hr", solution.objective_value);
println!("Method: {}", solution.method_used);

OpfMethod Enum

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum OpfMethod {
    /// Merit-order economic dispatch (no network constraints)
    EconomicDispatch,
    /// DC optimal power flow (LP with B-matrix)
    DcOpf,
    /// Second-order cone relaxation of AC-OPF
    #[default]
    SocpRelaxation,
    /// Full nonlinear AC-OPF (penalty-method L-BFGS)
    AcOpf,
}

OpfSolution

pub struct OpfSolution {
    // Status
    pub converged: bool,
    pub method_used: OpfMethod,
    pub iterations: usize,
    pub solve_time_ms: u128,

    // Objective
    pub objective_value: f64,  // Total cost ($/hr)

    // Primal variables
    pub generator_p: HashMap<String, f64>,      // Active power (MW)
    pub generator_q: HashMap<String, f64>,      // Reactive power (MVAr)
    pub bus_voltage_mag: HashMap<String, f64>,  // |V| in p.u.
    pub bus_voltage_ang: HashMap<String, f64>,  // ΞΈ in degrees
    pub branch_p_flow: HashMap<String, f64>,    // MW flow
    pub branch_q_flow: HashMap<String, f64>,    // MVAr flow

    // Dual variables
    pub bus_lmp: HashMap<String, f64>,          // $/MWh at each bus

    // Constraints
    pub binding_constraints: Vec<ConstraintInfo>,
    pub total_losses_mw: f64,
}

Note: Not all fields are populated by all methods. Economic dispatch provides generator outputs, objective, and estimated losses. SOCP and AC-OPF provide full voltage, angle, and LMP data; AC-OPF now backfills LMPs using marginal generator costs after the penalty loop finishes.

SOCP Relaxation (v0.4.0)

The SOCP solver implements the Baran-Wu / Farivar-Low branch-flow model:

Features

FeatureStatus
Squared voltage/current variablesβœ…
Quadratic costs (cβ‚€ + c₁·P + cβ‚‚Β·PΒ²)βœ…
Phase-shifting transformersβœ…
Off-nominal tap ratiosβœ…
Line charging (Ο€-model)βœ…
Thermal limits (S_max)βœ…
Voltage boundsβœ…
LMP extraction from dualsβœ…

Mathematical Formulation

Variables: w_i (squared voltage), β„“_ij (squared current), P_ij, Q_ij (branch flows)

Objective:

minimize Ξ£ (cβ‚€ + c₁·P_g + cβ‚‚Β·P_gΒ²)

Branch-flow constraints:

w_j = w_i - 2(rΒ·P_ij + xΒ·Q_ij) + (rΒ² + xΒ²)Β·β„“_ij
P_ijΒ² + Q_ijΒ² ≀ w_i Β· β„“_ij  (SOC constraint)

Solver: Clarabel interior-point conic solver (15-30 iterations typical)

References

  • Baran & Wu (1989): DOI:10.1109/61.25627
  • Farivar & Low (2013): DOI:10.1109/TPWRS.2013.2255317
  • Gan, Li, Topcu & Low (2015): DOI:10.1109/TAC.2014.2332712

Full AC-OPF (v0.4.0)

The AC-OPF solver uses polar coordinates with a penalty-method L-BFGS optimizer and now ships with a complete ac_nlp pipeline:

Features

FeatureStatus
Polar formulation (V, ΞΈ)βœ…
Y-bus construction (with taps + phase shifts)βœ…
Line charging / Ο€-model supportβœ…
Quadratic costsβœ…
Voltage boundsβœ…
Generator limitsβœ…
Jacobian computationβœ…
L-BFGS penalty optimizerβœ…
Thermal limits (branch flow)βœ…
IPOPT backendβœ… (solver-ipopt feature)

Key components in gat_algo::opf::ac_nlp:

  • ybus.rs: builds the complex admittance matrix with tap ratios, phase shifters, and shunt line charging.
  • sparse_ybus.rs: O(nnz) sparse Y-bus storage for large networks.
  • power_equations.rs: evaluates P/Q injections and full Jacobians (βˆ‚P/βˆ‚ΞΈ, βˆ‚P/βˆ‚V, βˆ‚Q/βˆ‚ΞΈ, βˆ‚Q/βˆ‚V) in polar form.
  • branch_flow.rs: computes branch apparent power flows for thermal limit enforcement.
  • hessian.rs: second-derivative computation for interior-point methods (IPOPT).
  • solver.rs: wraps argmin's L-BFGS optimizer with a penalty ramp until equality constraints reach feasibility.
  • ipopt_solver.rs: full-featured interior-point solver via IPOPT (requires solver-ipopt feature).

Mathematical Formulation

Variables: V_i (voltage magnitude), ΞΈ_i (angle), P_g, Q_g (generator dispatch)

Objective:

minimize Ξ£ (cβ‚€ + c₁·P_g + cβ‚‚Β·P_gΒ²)

Power flow equations:

P_i = Ξ£β±Ό V_iΒ·V_jΒ·(G_ijΒ·cos(ΞΈ_i - ΞΈ_j) + B_ijΒ·sin(ΞΈ_i - ΞΈ_j))
Q_i = Ξ£β±Ό V_iΒ·V_jΒ·(G_ijΒ·sin(ΞΈ_i - ΞΈ_j) - B_ijΒ·cos(ΞΈ_i - ΞΈ_j))

Solver: argmin L-BFGS with iterative penalty method (penalty factor ramps until equality constraints are feasible).

Usage

let solver = OpfSolver::new()
    .with_method(OpfMethod::AcOpf)
    .with_max_iterations(200)
    .with_tolerance(1e-4);

let solution = solver.solve(&network)?;

ADMM Distributed OPF (v0.5.6)

GAT implements distributed OPF using the Alternating Direction Method of Multipliers (ADMM), enabling scalable optimization across partitioned networks.

Key Features

FeatureStatus
Graph partitioning (METIS)βœ…
Parallel subproblem solvingβœ…
Consensus coordinationβœ…
Adaptive penaltyβœ…
Tie-line flow calculationβœ…
GPU accelerationβœ… (optional)

Algorithm

For each iteration k:
  1. x-update: Solve local OPF for each partition (parallel)
  2. z-update: Average boundary voltages (consensus)
  3. Ξ»-update: Update dual variables
  4. Check: Compute primal/dual residuals

GPU Acceleration

Branch power flow calculation can use GPU for large networks:

# Build with GPU support
cargo build -p gat-cli --features gpu

Performance:

  • Large networks (100s-1000s of branches): 2-10x speedup
  • Small networks: Minimal benefit (kernel overhead)
  • Auto-fallback to CPU if GPU unavailable

Usage

use gat_algo::opf::admm::{AdmmOpfSolver, AdmmConfig};

let solver = AdmmOpfSolver::new(AdmmConfig {
    num_partitions: 4,
    penalty: 1.0,
    max_iter: 100,
    use_gpu: true,
    ..Default::default()
});

let result = solver.solve(&network)?;
println!("Objective: ${:.2}/hr", result.objective);
println!("Tie-lines: {}", result.num_tie_lines);

References

  • Boyd, S., et al. (2011). Distributed Optimization via ADMM.
  • Chen, Y., et al. (2025). DPLib: Distributed Power System Benchmark. arXiv:2506.20819.

Generator Cost Models

Generators support polynomial and piecewise-linear cost functions via the CostModel enum:

use gat_core::{Gen, GenId, BusId, CostModel};

// Quadratic cost: $100 + $20/MWh + $0.01/MWΒ²h
let gen = Gen::new(GenId::new(0), "Gen1".into(), BusId::new(0))
    .with_p_limits(10.0, 100.0)    // Pmin=10 MW, Pmax=100 MW
    .with_q_limits(-50.0, 50.0)    // Qmin=-50 MVAr, Qmax=50 MVAr
    .with_cost(CostModel::quadratic(100.0, 20.0, 0.01));

// Linear cost: $50 + $25/MWh
let gen2 = Gen::new(GenId::new(1), "Gen2".into(), BusId::new(1))
    .with_p_limits(0.0, 200.0)
    .with_cost(CostModel::linear(50.0, 25.0));

// Piecewise linear: [(MW, $/hr), ...]
let gen3 = Gen::new(GenId::new(2), "Gen3".into(), BusId::new(2))
    .with_p_limits(0.0, 100.0)
    .with_cost(CostModel::PiecewiseLinear(vec![
        (0.0, 0.0),
        (50.0, 1000.0),
        (100.0, 2500.0),
    ]));

CostModel Methods

impl CostModel {
    /// Evaluate cost at given power output ($/hr)
    pub fn evaluate(&self, p_mw: f64) -> f64;

    /// Get marginal cost at given power ($/MWh)
    pub fn marginal_cost(&self, p_mw: f64) -> f64;

    /// Check if this cost model has actual cost data
    pub fn has_cost(&self) -> bool;
}

CLI Commands

GAT provides four OPF commands matching the solver hierarchy:

# Tier 1: Economic Dispatch (< 1ms)
gat opf ed grid.arrow --out dispatch.parquet

# Tier 2: DC-OPF (~10ms for 118-bus)
gat opf dc grid.arrow --out flows.parquet

# Tier 3: SOCP Relaxation (~100ms for 118-bus)
gat opf socp grid.arrow --out solution.parquet

# Tier 4: AC-OPF (~1s for 118-bus)
gat opf ac grid.arrow --out optimal.parquet

Economic Dispatch (gat opf ed)

Merit-order dispatch ignoring network constraints. Fastest option for "can we meet demand?" checks.

gat opf ed grid.arrow --out dispatch.parquet

Output: Generator dispatch (P) ordered by marginal cost.

DC-OPF (gat opf dc)

Linear approximation with B-matrix flow constraints. Standard for transmission planning.

gat opf dc grid.arrow \
  --out results/dc-opf.parquet \
  [--branch-limits limits.csv]

Features:

  • Real power flow on branches
  • Generator cost minimization
  • Optional branch flow limits
  • LMP extraction from duals

Output: Branch flows, generator dispatch, bus LMPs.

SOCP Relaxation (gat opf socp)

Second-order cone relaxation with voltage magnitude modeling. Production-ready for most applications.

gat opf socp grid.arrow \
  --out results/socp.parquet \
  [--tol 1e-6]

Features:

  • Squared voltage/current variables
  • Quadratic generator costs
  • Transformer tap ratios and phase shifters
  • Thermal limits (apparent power)
  • LMP extraction from duals

Backend: Clarabel (pure Rust, no external dependencies)

Output: Branch flows, voltages, generator dispatch, LMPs.

AC-OPF (gat opf ac)

Full nonlinear AC-OPF with polar formulation. Maximum accuracy for final validation.

gat opf ac grid.arrow \
  --out results/ac-opf.parquet \
  [--tol 1e-4] \
  [--max-iter 200] \
  [--warm-start socp]

Options:

  • --tol: convergence tolerance (default 1e-4)
  • --max-iter: maximum iterations (default 200)
  • --warm-start: initialization β€” flat, dc, or socp (recommended)

Backend: IPOPT with analytical Jacobian and Hessian (if installed), otherwise L-BFGS

Output: Full voltage profile (V, ΞΈ), generator P/Q, branch flows, losses, LMPs.

Test Fixtures

test_data/opf provides reusable CSVs for local experiments:

  • costs.csv: sample marginal costs for buses 0 and 1.
  • limits.csv: matching pmin, pmax, and demand entries.
  • branch_limits.csv: tight limits for violation testing.
  • piecewise.csv: two-piece segments for piecewise cost testing.

Troubleshooting

OPF Returns Infeasible

Symptoms: "Problem infeasible" or "No solution found"

Common Causes & Solutions:

  1. Insufficient generation capacity

    gat inspect power-balance grid.arrow
    # Total Pmax must exceed total load + losses
    
  2. Transmission limits too tight

    # Relax limits temporarily to diagnose
    gat opf dc grid.arrow --out test.parquet --ignore-branch-limits
    
  3. Missing or incorrect cost data

    # Verify all generators have costs
    gat inspect generators grid.arrow --format json | \
      jq '.[] | select(.cost == null or .cost == 0)'
    
  4. Network islands

    gat graph islands grid.arrow
    # Each island needs its own slack/generation
    

AC-OPF Does Not Converge

Symptoms: IPOPT reports "Maximum iterations exceeded" or "Restoration failed"

Solutions:

  1. Use SOCP warm-start (recommended)

    gat opf ac grid.arrow --warm-start socp --out results.parquet
    
  2. Increase iterations and relax tolerance

    gat opf ac grid.arrow --max-iter 500 --tol 1e-3 --out results.parquet
    
  3. Check voltage bounds

    # Widen voltage bounds if too restrictive
    gat inspect buses grid.arrow --format json | \
      jq '.[] | {id, vmin, vmax} | select(.vmax - .vmin < 0.1)'
    
  4. Fall back to L-BFGS if IPOPT unavailable

    gat opf ac grid.arrow --solver lbfgs --out results.parquet
    

Large Objective Gap vs Reference

Symptoms: Cost differs significantly from MATPOWER/PowerModels results

Causes & Solutions:

  1. Different cost function interpretation

    # Verify cost units ($/MWh vs $/MW)
    gat inspect generators grid.arrow --format json | jq '.[0].cost'
    
  2. Generator limits not binding correctly

    # Check dispatch is within limits
    gat opf dc grid.arrow --out r.parquet --format json | \
      jq '.generators[] | select(.pg > .pmax or .pg < .pmin)'
    
  3. Loss modeling differences

    • DC-OPF ignores losses (expected ~3-5% gap)
    • Use SOCP or AC-OPF for loss-inclusive comparison

SOCP Relaxation Gap Too Large

Symptoms: SOCP objective significantly lower than AC-OPF

Solutions:

  1. Tighten voltage bounds

    # Narrow voltage range reduces relaxation gap
    gat opf socp grid.arrow --vmin 0.95 --vmax 1.05 --out results.parquet
    
  2. Check for binding constraints

    # Large gaps often indicate loose problem
    gat opf socp grid.arrow --out r.parquet --format json | \
      jq '.binding_constraints | length'
    

Negative LMPs

Symptoms: Some buses show negative locational marginal prices

Interpretation: This is often correct! Negative LMPs occur when:

  • Curtailment is cheaper than transmission
  • Congestion creates "trapped" low-cost generation
  • Renewable must-run constraints

Verification:

# Check which constraints are binding
gat opf dc grid.arrow --out r.parquet --format json | \
  jq '.branches[] | select(.binding == true) | {from, to, flow, limit}'

Memory/Performance Issues

Large systems (>10k buses):

# Use DC-OPF for screening
gat opf dc grid.arrow --threads 8 --out results.parquet

# For AC-OPF, use sparse linear algebra
gat opf ac grid.arrow --solver ipopt-ma57 --out results.parquet

Batch scenarios:

# Process in parallel batches
gat batch opf grid.arrow \
  --scenarios scenarios.yaml \
  --threads 4 \
  --out batch_results/