Skip to main content

powerio_matrix/matrix/
kkt.rs

1//! Interior point operator assembly for the DC-OPF Newton step.
2//!
3//! The Θ⁻¹ diagonals are central-path state (they change every IPM
4//! iteration), so they are passed in, never derived from the case. Given the
5//! case factors `A`, `b`, `L` and the bus cost `q`, this builds the operators
6//! the reduced Newton system needs:
7//!
8//! ```text
9//! L_eff = A B Θf⁻¹ B Aᵀ + L Dg⁻¹ L = L₁ + L₂ D L₂,   Dg = (Q + Θg⁻¹)⁻¹
10//! ```
11//!
12//! The solver multiplies by `L₁`, solves with `L₂ = L`, and scales by
13//! `D^{±1/2}`, all grounded at the slack bus, so the grounded factors are the
14//! primary output; the dense `L_eff` is optional.
15
16// Dense linear algebra: single-char indices (i, j, k, m, n) are the math
17// notation, and the assembly entry points take the operators as flat inputs.
18#![allow(clippy::many_single_char_names, clippy::too_many_arguments)]
19
20use sprs::CsMat;
21
22use crate::matrix::incidence::{build_flow_map, diagonal};
23use crate::matrix::laplacian::{GroundedIndexMap, build_weighted_laplacian, ground_at};
24use crate::matrix::triplet::CooBuilder;
25use crate::{Error, Result};
26
27/// The grounded operators a step of the EKS solver consumes, plus their
28/// ungrounded forms and the diagonal `D = Dg⁻¹`.
29#[derive(Debug, Clone)]
30pub struct KktOperators {
31    /// `L₁ = A diag(b²·θf⁻¹) Aᵀ`, the reweighted Laplacian (n×n).
32    pub l1: CsMat<f64>,
33    /// `L₂ = L`, the DC Laplacian (n×n).
34    pub l2: CsMat<f64>,
35    /// `D = Dg⁻¹ = (Q + Θg⁻¹)⁻¹`, positive diagonal, length n.
36    pub d: Vec<f64>,
37    /// `L₁` grounded at the slack bus, SPD.
38    pub l1_grounded: CsMat<f64>,
39    /// `L₂` grounded at the slack bus, SPD.
40    pub l2_grounded: CsMat<f64>,
41    /// `L_eff = L₁ + L₂ D L₂` grounded at the slack bus, SPD. Present only
42    /// when requested.
43    pub l_eff_grounded: Option<CsMat<f64>>,
44    pub map: GroundedIndexMap,
45}
46
47/// Assemble the reduced KKT operators from the case factors and the
48/// caller-supplied positive interior point diagonals.
49///
50/// `theta_f_inv` (length m) and `theta_g_inv` (length n) are the barrier
51/// weights Θf⁻¹, Θg⁻¹; `q_bus` (length n) is the bus cost diagonal; `r` is the
52/// slack bus index.
53pub fn assemble_kkt(
54    a: &CsMat<f64>,
55    b: &[f64],
56    l: &CsMat<f64>,
57    q_bus: &[f64],
58    theta_f_inv: &[f64],
59    theta_g_inv: &[f64],
60    r: usize,
61    want_l_eff: bool,
62) -> Result<KktOperators> {
63    let n = l.rows();
64    let m = b.len();
65    check_len("theta_f_inv", theta_f_inv, m)?;
66    check_len("theta_g_inv", theta_g_inv, n)?;
67    check_len("q_bus", q_bus, n)?;
68
69    // L₁ = A diag(b²·θf⁻¹) Aᵀ — fold the diagonal into the edge weights.
70    let w: Vec<f64> = (0..m).map(|k| b[k] * b[k] * theta_f_inv[k]).collect();
71    let l1 = build_weighted_laplacian(a, &w);
72    let l2 = l.clone();
73
74    // D = Dg⁻¹ = 1 / (q + θg⁻¹), positive.
75    let d: Vec<f64> = (0..n).map(|i| 1.0 / (q_bus[i] + theta_g_inv[i])).collect();
76
77    let l1_grounded = ground_at(&l1, r);
78    let l2_grounded = ground_at(&l2, r);
79
80    let l_eff_grounded = if want_l_eff {
81        // L_eff = L₁ + L diag(d) L.
82        let dmat = diagonal(&d);
83        let ld = l * &dmat; // L diag(d)
84        let ldl = &ld * l; // (L diag(d)) L
85        let l_eff = add_csr(&l1, &ldl);
86        Some(ground_at(&l_eff, r))
87    } else {
88        None
89    };
90
91    Ok(KktOperators {
92        l1,
93        l2,
94        d,
95        l1_grounded,
96        l2_grounded,
97        l_eff_grounded,
98        map: GroundedIndexMap::new(n, r),
99    })
100}
101
102/// Assemble the full reduced augmented KKT block (eq. "reduced"): a symmetric
103/// indefinite saddle matrix in `(Δp_g, Δθ, Δf, Δν, Δη, Δρ)` of size
104/// `3n + 2m + 1`. Useful as a single operator for a direct factorization.
105pub fn assemble_reduced_kkt(
106    a: &CsMat<f64>,
107    b: &[f64],
108    l: &CsMat<f64>,
109    q_bus: &[f64],
110    theta_f_inv: &[f64],
111    theta_g_inv: &[f64],
112    r: usize,
113) -> Result<CsMat<f64>> {
114    let n = l.rows();
115    let m = b.len();
116    check_len("theta_f_inv", theta_f_inv, m)?;
117    check_len("theta_g_inv", theta_g_inv, n)?;
118    check_len("q_bus", q_bus, n)?;
119
120    // Block column / row offsets for (p_g, θ, f, ν, η, ρ).
121    let (o_pg, o_th, o_f, o_nu, o_eta, o_rho) = (0, n, 2 * n, 2 * n + m, 3 * n + m, 3 * n + 2 * m);
122    let dim = 3 * n + 2 * m + 1;
123
124    let ab = a * &diagonal(b); // AB = A diag(b), n×m
125    let flow = build_flow_map(a, b); // BAᵀ, m×n
126
127    let mut k =
128        CooBuilder::with_capacity_rect(dim, dim, 4 * l.nnz() + 4 * ab.nnz() + 4 * n + 4 * m);
129
130    // (1,1) Q + Θg⁻¹ and (1,4) −I.
131    for i in 0..n {
132        k.add(o_pg + i, o_pg + i, q_bus[i] + theta_g_inv[i]);
133        k.add(o_pg + i, o_nu + i, -1.0);
134        k.add(o_nu + i, o_pg + i, -1.0); // (4,1) −I
135    }
136    // (2,4) L and its transpose (4,2) L.
137    for (&v, (i, j)) in l {
138        k.add(o_th + i, o_nu + j, v);
139        k.add(o_nu + i, o_th + j, v);
140    }
141    // (2,5) −AB and transpose (5,2) −BAᵀ.
142    for (&v, (i, j)) in &ab {
143        k.add(o_th + i, o_eta + j, -v);
144    }
145    for (&v, (i, j)) in &flow {
146        k.add(o_eta + i, o_th + j, -v);
147    }
148    // (2,6) e_r and (6,2) e_rᵀ.
149    k.add(o_th + r, o_rho, 1.0);
150    k.add(o_rho, o_th + r, 1.0);
151    // (3,3) Θf⁻¹ and (3,5)/(5,3) I.
152    for (kk, &tf) in theta_f_inv.iter().enumerate() {
153        k.add(o_f + kk, o_f + kk, tf);
154        k.add(o_f + kk, o_eta + kk, 1.0);
155        k.add(o_eta + kk, o_f + kk, 1.0);
156    }
157
158    Ok(k.finish_csr())
159}
160
161fn check_len(what: &'static str, v: &[f64], expected: usize) -> Result<()> {
162    if v.len() == expected {
163        Ok(())
164    } else {
165        Err(Error::ShapeMismatch {
166            what,
167            expected,
168            got: v.len(),
169        })
170    }
171}
172
173/// Sparse `A + B` via the COO accumulator (independent of sprs `Add`).
174fn add_csr(a: &CsMat<f64>, b: &CsMat<f64>) -> CsMat<f64> {
175    let mut out = CooBuilder::with_capacity_rect(a.rows(), a.cols(), a.nnz() + b.nnz());
176    for (&v, (i, j)) in a {
177        out.add(i, j, v);
178    }
179    for (&v, (i, j)) in b {
180        out.add(i, j, v);
181    }
182    out.finish_csr()
183}