Skip to main content

frame_roundtrip/
frame_roundtrip.rs

1//! Worked example for PR 7 of issue #53 — reduction frame +
2//! postsolve multiplier recovery.
3//!
4//! Full problem (3 variables `b1, b2, y`, 3 constraints):
5//!
6//! ```text
7//!   min   f(b1, b2, y) = 10 b1 + 4 b2 + y²
8//!   s.t.  c0(b):  2 b1 +   b2       - 3 = 0   ← dropped (equality)
9//!         c1(b):    b1 -   b2       + 1 = 0   ← dropped (equality)
10//!         c2(b):    b1 +   b2 +  y  - 5 = 0   ← kept    (equality)
11//! ```
12//!
13//! Block solve fixes `(b1, b2) = (2/3, 5/3)` by satisfying `c0` and
14//! `c1`. The reduced problem has one variable (`y`) and one row
15//! (`c2`), which gives `y* = 8/3`. The IPM then converges to that.
16//!
17//! Postsolve must:
18//! 1. Lift `x` back to full space `(b1*, b2*, y*)`.
19//! 2. Recover λ for the dropped rows by solving the full-space KKT
20//!    stationarity at the fixed variables.
21//! 3. Verify the full-space KKT residual is below tolerance.
22//!
23//! Run with:
24//! ```bash
25//! cargo run -p pounce-presolve --example frame_roundtrip
26//! ```
27
28#![allow(clippy::expect_used, clippy::unwrap_used)]
29
30use pounce_presolve::reduction_frame::ReductionFrame;
31
32fn main() {
33    // Full-space problem shape.
34    let n_vars = 3;
35    let n_rows = 3;
36
37    // Block solve fixed b1, b2.
38    let frame = ReductionFrame::new(
39        n_vars,
40        n_rows,
41        vec![0, 1],                 // fixed_vars: b1, b2
42        vec![2.0 / 3.0, 5.0 / 3.0], // their values
43        vec![0, 1],                 // dropped_rows: c0, c1
44    );
45
46    println!("frame_roundtrip example — PR 7 of pounce#53");
47    println!(
48        "full vars: {}, reduced vars: {}",
49        frame.n_full_vars(),
50        frame.n_reduced_vars()
51    );
52    println!(
53        "full rows: {}, reduced rows: {}",
54        frame.n_full_rows(),
55        frame.n_reduced_rows()
56    );
57
58    // Simulated IPM result on the reduced problem.
59    let y_star = 8.0 / 3.0;
60    let x_reduced = [y_star];
61    let lambda_reduced = [2.0 * y_star]; // stationarity at y: 2y - λ_kept = 0
62
63    // Lift x to full space.
64    let x_full = frame.lift_x(&x_reduced);
65    println!(
66        "lifted x:      [{:.6}, {:.6}, {:.6}]",
67        x_full[0], x_full[1], x_full[2]
68    );
69
70    // Lift λ to full space — zeros at dropped row indices, to be
71    // filled in by multiplier recovery.
72    let mut lambda_full = frame.lift_lambda(&lambda_reduced);
73    println!("lifted λ (pre-recovery): {:?}", lambda_full);
74
75    // Build the full Jacobian at x_full (rows × vars, row-major).
76    let jac_full = [
77        2.0, 1.0, 0.0, // c0: 2 b1 + b2
78        1.0, -1.0, 0.0, // c1: b1 - b2
79        1.0, 1.0, 1.0, // c2: b1 + b2 + y
80    ];
81    // Objective gradient at x_full.
82    let grad_f = [10.0, 4.0, 2.0 * x_full[2]];
83
84    // Recover the dropped multipliers.
85    let lam_dropped = frame
86        .recover_dropped_multipliers(&grad_f, &jac_full, &lambda_full)
87        .expect("non-singular");
88
89    // Fill them into the full λ vector.
90    for (k, &r) in frame.dropped_rows.iter().enumerate() {
91        lambda_full[r] = lam_dropped[k];
92    }
93    println!("lifted λ (post-recovery): {:?}", lambda_full);
94
95    // Verify full-space stationarity at ALL variables.
96    println!("KKT stationarity residual (should be ≈ 0):");
97    let mut max_res = 0.0_f64;
98    for i in 0..n_vars {
99        let mut s = grad_f[i];
100        for r in 0..n_rows {
101            s -= jac_full[r * n_vars + i] * lambda_full[r];
102        }
103        println!("  var {i}: {:.3e}", s);
104        max_res = max_res.max(s.abs());
105    }
106    println!("max |residual|: {:.3e}", max_res);
107    assert!(max_res < 1e-12);
108}