Skip to main content

ReductionFrame

Struct ReductionFrame 

Source
pub struct ReductionFrame {
    pub fixed_vars: Vec<usize>,
    pub fixed_values: Vec<Number>,
    pub dropped_rows: Vec<usize>,
    pub var_map: Vec<Option<usize>>,
    pub row_map: Vec<Option<usize>>,
}
Expand description

One layer of the postsolve stack. Built once per accepted block elimination by PR 8’s orchestrator.

Fields§

§fixed_vars: Vec<usize>

Inner-variable indices fixed by this layer, in ascending order.

§fixed_values: Vec<Number>

Their values at the block-solve fixed point.

§dropped_rows: Vec<usize>

Inner equality-row indices dropped by this layer, in ascending order. dropped_rows.len() == fixed_vars.len().

§var_map: Vec<Option<usize>>

var_map[i] = Some(reduced_idx) if i survives this layer, None if i is in fixed_vars.

§row_map: Vec<Option<usize>>

Same for rows.

Implementations§

Source§

impl ReductionFrame

Source

pub fn new( n_vars: usize, n_rows: usize, fixed_vars: Vec<usize>, fixed_values: Vec<Number>, dropped_rows: Vec<usize>, ) -> Self

Build a frame from the (sorted) lists of fixed variables / values / dropped rows and the full-space problem shape.

Examples found in repository?
examples/frame_roundtrip.rs (lines 38-44)
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}
More examples
Hide additional examples
examples/pipeline_demo.rs (lines 220-226)
68fn main() {
69    let n_vars = 4; // a, b, c, d
70    let n_rows = 4;
71    // Jacobian sparsity (row-major).
72    let jac_pattern = [
73        (0, 0), // c0: a
74        (1, 1),
75        (1, 2), // c1: b + c
76        (2, 1),
77        (2, 2), // c2: b - c
78        (3, 0),
79        (3, 1),
80        (3, 3), // c3: a + b + d
81    ];
82    let jac_irow: Vec<i32> = jac_pattern.iter().map(|(r, _)| *r as i32).collect();
83    let jac_jcol: Vec<i32> = jac_pattern.iter().map(|(_, c)| *c as i32).collect();
84    let g_l = [1.0, 5.0, -1.0, 4.0];
85    let g_u = [1.0, 5.0, -1.0, 4.0]; // all equalities
86
87    let probe = ProbeView {
88        n_vars,
89        m_rows: n_rows,
90        jac_irow: &jac_irow,
91        jac_jcol: &jac_jcol,
92        jac_values: None,
93        g_l: &g_l,
94        g_u: &g_u,
95        linearity: None,
96        one_based: false,
97        eq_tol: 1e-12,
98        excluded_vars: None,
99        excluded_rows: None,
100    };
101
102    println!("pipeline_demo — PRs 2-7 of pounce#53");
103    println!("======================================");
104
105    // ----- Stage 1: incidence + matching -----
106    let inc = EqualityIncidence::from_probe(&probe);
107    println!(
108        "\n[Stage 1] EqualityIncidence: {} eq rows, {} vars",
109        inc.n_eq_rows(),
110        inc.n_vars
111    );
112    let m = hopcroft_karp(&inc);
113    println!("[Stage 1] Matching: size = {}", m.size);
114
115    // ----- Stage 2: DM partition -----
116    let dm = DulmageMendelsohnPartition::from_matching(&inc, &m);
117    println!("\n[Stage 2] DM partition:");
118    println!("   over rows={:?}, cols={:?}", dm.over_rows, dm.over_cols);
119    println!(
120        "   square rows={:?}, cols={:?}",
121        dm.square_rows, dm.square_cols
122    );
123    println!(
124        "   under rows={:?}, cols={:?}",
125        dm.under_rows, dm.under_cols
126    );
127
128    // ----- Stage 3: components -----
129    let comps = SquareComponents::of_square_part(&inc, &m, &dm);
130    println!(
131        "\n[Stage 3] Components: {} square block(s)",
132        comps.components.len()
133    );
134    for (i, c) in comps.components.iter().enumerate() {
135        println!("   component {i}: rows={:?}, cols={:?}", c.eq_rows, c.cols);
136    }
137
138    // ----- Stage 4: BTF -----
139    println!("\n[Stage 4] BTF (per component):");
140    let ineq = InequalityIncidence::from_probe(&probe);
141
142    // Walk each component's BTF, solve each block, build the frame.
143    let mut all_fixed_vars: Vec<usize> = Vec::new();
144    let mut all_fixed_values: Vec<Number> = Vec::new();
145    let mut all_dropped_rows: Vec<usize> = Vec::new();
146    let mut x_running = vec![0.0; n_vars];
147    // Full A and b for this problem, used both for block extraction
148    // and the final KKT check.
149    let a_full = [
150        [1.0, 0.0, 0.0, 0.0],
151        [0.0, 1.0, 1.0, 0.0],
152        [0.0, 1.0, -1.0, 0.0],
153        [1.0, 1.0, 0.0, 1.0],
154    ];
155    let b_full = [1.0, 5.0, -1.0, 4.0];
156
157    for comp in &comps.components {
158        let btf = BlockTriangularForm::of_component(&inc, &m, comp);
159        for (bi, block) in btf.blocks.iter().enumerate() {
160            let class = classify_block(block, &ineq, &Default::default());
161            println!(
162                "   component cols={:?}, block {bi}: rows={:?}, cols={:?}, class={class:?}",
163                comp.cols, block.eq_rows, block.cols
164            );
165            assert_eq!(class, AuxiliaryCouplingClass::PureEquality);
166
167            // Build a small linear system for this block, splicing in
168            // the values of already-solved variables.
169            let k = block.eq_rows.len();
170            let mut a_block = vec![0.0; k * k];
171            let mut b_block = vec![0.0; k];
172            for (ii, &r) in block.eq_rows.iter().enumerate() {
173                let mut rhs = b_full[r];
174                for j in 0..n_vars {
175                    if let Some(jj) = block.cols.iter().position(|&c| c == j) {
176                        a_block[ii * k + jj] = a_full[r][j];
177                    } else {
178                        rhs -= a_full[r][j] * x_running[j];
179                    }
180                }
181                b_block[ii] = rhs;
182            }
183
184            // Solve.
185            let mut eqs = LinearBlockEqs {
186                a: a_block,
187                b: b_block,
188                n: k,
189            };
190            let mut solver = DampedNewtonSolver;
191            let opt = BlockSolveOptions::default();
192            let out = solver.solve(&vec![0.0; k], &mut eqs, &opt).unwrap();
193
194            // Record the block's solution.
195            for (ii, &c) in block.cols.iter().enumerate() {
196                x_running[c] = out.x[ii];
197                all_fixed_vars.push(c);
198                all_fixed_values.push(out.x[ii]);
199            }
200            for &r in &block.eq_rows {
201                all_dropped_rows.push(r);
202            }
203        }
204    }
205
206    // ----- Stage 5: lift via the reduction frame -----
207    println!("\n[Stage 5] After block solves:");
208    println!("   x_running (a, b, c, d) = {:?}", x_running);
209    // For this demo we declare ALL fixed vars and rows as one
210    // composite frame — in the real PR 8 orchestrator, each
211    // elimination layer becomes its own frame on the stack.
212    // Sort by variable index for the frame's contract.
213    let mut order: Vec<usize> = (0..all_fixed_vars.len()).collect();
214    order.sort_by_key(|&i| all_fixed_vars[i]);
215    let fixed_vars_sorted: Vec<usize> = order.iter().map(|&i| all_fixed_vars[i]).collect();
216    let fixed_values_sorted: Vec<Number> = order.iter().map(|&i| all_fixed_values[i]).collect();
217    let mut dropped_rows_sorted = all_dropped_rows.clone();
218    dropped_rows_sorted.sort_unstable();
219
220    let frame = ReductionFrame::new(
221        n_vars,
222        n_rows,
223        fixed_vars_sorted.clone(),
224        fixed_values_sorted,
225        dropped_rows_sorted.clone(),
226    );
227    println!(
228        "   ReductionFrame: fixed_vars={:?}, dropped_rows={:?}",
229        frame.fixed_vars, frame.dropped_rows
230    );
231    println!(
232        "   reduced shape: {} vars × {} rows",
233        frame.n_reduced_vars(),
234        frame.n_reduced_rows()
235    );
236
237    // For this problem BTF decomposed every row into a block — the
238    // reduced shape is 0×0 and no IPM step is needed. The lift is
239    // trivial: x_full = the values the block solves produced.
240    let x_reduced: Vec<Number> = Vec::new();
241    println!("\n[Stage 6] Reduced problem is empty (presolve solved it all).");
242    let x_full = frame.lift_x(&x_reduced);
243    println!("   lifted x_full = {:?}", x_full);
244
245    // Sanity: should match (1, 2, 3, 1).
246    let expected = [1.0, 2.0, 3.0, 1.0];
247    let max_x_err = x_full
248        .iter()
249        .zip(expected.iter())
250        .map(|(a, b)| (a - b).abs())
251        .fold(0.0_f64, f64::max);
252    println!("   max |x - expected|: {:.3e}", max_x_err);
253    assert!(max_x_err < 1e-9);
254
255    // ----- Stage 7: multiplier recovery -----
256    // Objective: f = (a-1)² + (b-2)² + (c-3)² + d²
257    // ∇f at the optimum (1, 2, 3, 1): (0, 0, 0, 2).
258    let grad_f = [
259        2.0 * (x_full[0] - 1.0),
260        2.0 * (x_full[1] - 2.0),
261        2.0 * (x_full[2] - 3.0),
262        2.0 * x_full[3],
263    ];
264    println!("\n[Stage 7] ∇f at the optimum: {:?}", grad_f);
265
266    // No kept rows → reduced λ is empty.
267    let lambda_reduced: Vec<Number> = Vec::new();
268    let mut lambda_full = frame.lift_lambda(&lambda_reduced);
269    println!("   reduced λ = {:?}", lambda_reduced);
270    println!("   lifted λ (pre-recovery) = {:?}", lambda_full);
271
272    // Build full Jacobian for the recovery LU.
273    let mut jac_full = vec![0.0; n_rows * n_vars];
274    for (r, row) in a_full.iter().enumerate() {
275        for (c, &v) in row.iter().enumerate() {
276            jac_full[r * n_vars + c] = v;
277        }
278    }
279    let lam_dropped = frame
280        .recover_dropped_multipliers(&grad_f, &jac_full, &lambda_full)
281        .unwrap();
282    for (idx, &r) in frame.dropped_rows.iter().enumerate() {
283        lambda_full[r] = lam_dropped[idx];
284    }
285    println!("   recovered λ (full) = {:?}", lambda_full);
286
287    // Final KKT residual at every variable.
288    println!("\n[Stage 8] Full-space KKT residual:");
289    let mut max_kkt: Number = 0.0;
290    for i in 0..n_vars {
291        let mut s = grad_f[i];
292        for r in 0..n_rows {
293            s -= jac_full[r * n_vars + i] * lambda_full[r];
294        }
295        println!("   ∂L/∂x[{i}] = {:.3e}", s);
296        max_kkt = max_kkt.max(s.abs());
297    }
298    println!("   max |KKT residual| = {:.3e}", max_kkt);
299    assert!(max_kkt < 1e-10);
300    println!("\n✓ Pipeline end-to-end correct.");
301}
Source

pub fn n_full_vars(&self) -> usize

Examples found in repository?
examples/frame_roundtrip.rs (line 49)
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}
Source

pub fn n_full_rows(&self) -> usize

Examples found in repository?
examples/frame_roundtrip.rs (line 54)
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}
Source

pub fn n_reduced_vars(&self) -> usize

Examples found in repository?
examples/frame_roundtrip.rs (line 50)
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}
More examples
Hide additional examples
examples/pipeline_demo.rs (line 233)
68fn main() {
69    let n_vars = 4; // a, b, c, d
70    let n_rows = 4;
71    // Jacobian sparsity (row-major).
72    let jac_pattern = [
73        (0, 0), // c0: a
74        (1, 1),
75        (1, 2), // c1: b + c
76        (2, 1),
77        (2, 2), // c2: b - c
78        (3, 0),
79        (3, 1),
80        (3, 3), // c3: a + b + d
81    ];
82    let jac_irow: Vec<i32> = jac_pattern.iter().map(|(r, _)| *r as i32).collect();
83    let jac_jcol: Vec<i32> = jac_pattern.iter().map(|(_, c)| *c as i32).collect();
84    let g_l = [1.0, 5.0, -1.0, 4.0];
85    let g_u = [1.0, 5.0, -1.0, 4.0]; // all equalities
86
87    let probe = ProbeView {
88        n_vars,
89        m_rows: n_rows,
90        jac_irow: &jac_irow,
91        jac_jcol: &jac_jcol,
92        jac_values: None,
93        g_l: &g_l,
94        g_u: &g_u,
95        linearity: None,
96        one_based: false,
97        eq_tol: 1e-12,
98        excluded_vars: None,
99        excluded_rows: None,
100    };
101
102    println!("pipeline_demo — PRs 2-7 of pounce#53");
103    println!("======================================");
104
105    // ----- Stage 1: incidence + matching -----
106    let inc = EqualityIncidence::from_probe(&probe);
107    println!(
108        "\n[Stage 1] EqualityIncidence: {} eq rows, {} vars",
109        inc.n_eq_rows(),
110        inc.n_vars
111    );
112    let m = hopcroft_karp(&inc);
113    println!("[Stage 1] Matching: size = {}", m.size);
114
115    // ----- Stage 2: DM partition -----
116    let dm = DulmageMendelsohnPartition::from_matching(&inc, &m);
117    println!("\n[Stage 2] DM partition:");
118    println!("   over rows={:?}, cols={:?}", dm.over_rows, dm.over_cols);
119    println!(
120        "   square rows={:?}, cols={:?}",
121        dm.square_rows, dm.square_cols
122    );
123    println!(
124        "   under rows={:?}, cols={:?}",
125        dm.under_rows, dm.under_cols
126    );
127
128    // ----- Stage 3: components -----
129    let comps = SquareComponents::of_square_part(&inc, &m, &dm);
130    println!(
131        "\n[Stage 3] Components: {} square block(s)",
132        comps.components.len()
133    );
134    for (i, c) in comps.components.iter().enumerate() {
135        println!("   component {i}: rows={:?}, cols={:?}", c.eq_rows, c.cols);
136    }
137
138    // ----- Stage 4: BTF -----
139    println!("\n[Stage 4] BTF (per component):");
140    let ineq = InequalityIncidence::from_probe(&probe);
141
142    // Walk each component's BTF, solve each block, build the frame.
143    let mut all_fixed_vars: Vec<usize> = Vec::new();
144    let mut all_fixed_values: Vec<Number> = Vec::new();
145    let mut all_dropped_rows: Vec<usize> = Vec::new();
146    let mut x_running = vec![0.0; n_vars];
147    // Full A and b for this problem, used both for block extraction
148    // and the final KKT check.
149    let a_full = [
150        [1.0, 0.0, 0.0, 0.0],
151        [0.0, 1.0, 1.0, 0.0],
152        [0.0, 1.0, -1.0, 0.0],
153        [1.0, 1.0, 0.0, 1.0],
154    ];
155    let b_full = [1.0, 5.0, -1.0, 4.0];
156
157    for comp in &comps.components {
158        let btf = BlockTriangularForm::of_component(&inc, &m, comp);
159        for (bi, block) in btf.blocks.iter().enumerate() {
160            let class = classify_block(block, &ineq, &Default::default());
161            println!(
162                "   component cols={:?}, block {bi}: rows={:?}, cols={:?}, class={class:?}",
163                comp.cols, block.eq_rows, block.cols
164            );
165            assert_eq!(class, AuxiliaryCouplingClass::PureEquality);
166
167            // Build a small linear system for this block, splicing in
168            // the values of already-solved variables.
169            let k = block.eq_rows.len();
170            let mut a_block = vec![0.0; k * k];
171            let mut b_block = vec![0.0; k];
172            for (ii, &r) in block.eq_rows.iter().enumerate() {
173                let mut rhs = b_full[r];
174                for j in 0..n_vars {
175                    if let Some(jj) = block.cols.iter().position(|&c| c == j) {
176                        a_block[ii * k + jj] = a_full[r][j];
177                    } else {
178                        rhs -= a_full[r][j] * x_running[j];
179                    }
180                }
181                b_block[ii] = rhs;
182            }
183
184            // Solve.
185            let mut eqs = LinearBlockEqs {
186                a: a_block,
187                b: b_block,
188                n: k,
189            };
190            let mut solver = DampedNewtonSolver;
191            let opt = BlockSolveOptions::default();
192            let out = solver.solve(&vec![0.0; k], &mut eqs, &opt).unwrap();
193
194            // Record the block's solution.
195            for (ii, &c) in block.cols.iter().enumerate() {
196                x_running[c] = out.x[ii];
197                all_fixed_vars.push(c);
198                all_fixed_values.push(out.x[ii]);
199            }
200            for &r in &block.eq_rows {
201                all_dropped_rows.push(r);
202            }
203        }
204    }
205
206    // ----- Stage 5: lift via the reduction frame -----
207    println!("\n[Stage 5] After block solves:");
208    println!("   x_running (a, b, c, d) = {:?}", x_running);
209    // For this demo we declare ALL fixed vars and rows as one
210    // composite frame — in the real PR 8 orchestrator, each
211    // elimination layer becomes its own frame on the stack.
212    // Sort by variable index for the frame's contract.
213    let mut order: Vec<usize> = (0..all_fixed_vars.len()).collect();
214    order.sort_by_key(|&i| all_fixed_vars[i]);
215    let fixed_vars_sorted: Vec<usize> = order.iter().map(|&i| all_fixed_vars[i]).collect();
216    let fixed_values_sorted: Vec<Number> = order.iter().map(|&i| all_fixed_values[i]).collect();
217    let mut dropped_rows_sorted = all_dropped_rows.clone();
218    dropped_rows_sorted.sort_unstable();
219
220    let frame = ReductionFrame::new(
221        n_vars,
222        n_rows,
223        fixed_vars_sorted.clone(),
224        fixed_values_sorted,
225        dropped_rows_sorted.clone(),
226    );
227    println!(
228        "   ReductionFrame: fixed_vars={:?}, dropped_rows={:?}",
229        frame.fixed_vars, frame.dropped_rows
230    );
231    println!(
232        "   reduced shape: {} vars × {} rows",
233        frame.n_reduced_vars(),
234        frame.n_reduced_rows()
235    );
236
237    // For this problem BTF decomposed every row into a block — the
238    // reduced shape is 0×0 and no IPM step is needed. The lift is
239    // trivial: x_full = the values the block solves produced.
240    let x_reduced: Vec<Number> = Vec::new();
241    println!("\n[Stage 6] Reduced problem is empty (presolve solved it all).");
242    let x_full = frame.lift_x(&x_reduced);
243    println!("   lifted x_full = {:?}", x_full);
244
245    // Sanity: should match (1, 2, 3, 1).
246    let expected = [1.0, 2.0, 3.0, 1.0];
247    let max_x_err = x_full
248        .iter()
249        .zip(expected.iter())
250        .map(|(a, b)| (a - b).abs())
251        .fold(0.0_f64, f64::max);
252    println!("   max |x - expected|: {:.3e}", max_x_err);
253    assert!(max_x_err < 1e-9);
254
255    // ----- Stage 7: multiplier recovery -----
256    // Objective: f = (a-1)² + (b-2)² + (c-3)² + d²
257    // ∇f at the optimum (1, 2, 3, 1): (0, 0, 0, 2).
258    let grad_f = [
259        2.0 * (x_full[0] - 1.0),
260        2.0 * (x_full[1] - 2.0),
261        2.0 * (x_full[2] - 3.0),
262        2.0 * x_full[3],
263    ];
264    println!("\n[Stage 7] ∇f at the optimum: {:?}", grad_f);
265
266    // No kept rows → reduced λ is empty.
267    let lambda_reduced: Vec<Number> = Vec::new();
268    let mut lambda_full = frame.lift_lambda(&lambda_reduced);
269    println!("   reduced λ = {:?}", lambda_reduced);
270    println!("   lifted λ (pre-recovery) = {:?}", lambda_full);
271
272    // Build full Jacobian for the recovery LU.
273    let mut jac_full = vec![0.0; n_rows * n_vars];
274    for (r, row) in a_full.iter().enumerate() {
275        for (c, &v) in row.iter().enumerate() {
276            jac_full[r * n_vars + c] = v;
277        }
278    }
279    let lam_dropped = frame
280        .recover_dropped_multipliers(&grad_f, &jac_full, &lambda_full)
281        .unwrap();
282    for (idx, &r) in frame.dropped_rows.iter().enumerate() {
283        lambda_full[r] = lam_dropped[idx];
284    }
285    println!("   recovered λ (full) = {:?}", lambda_full);
286
287    // Final KKT residual at every variable.
288    println!("\n[Stage 8] Full-space KKT residual:");
289    let mut max_kkt: Number = 0.0;
290    for i in 0..n_vars {
291        let mut s = grad_f[i];
292        for r in 0..n_rows {
293            s -= jac_full[r * n_vars + i] * lambda_full[r];
294        }
295        println!("   ∂L/∂x[{i}] = {:.3e}", s);
296        max_kkt = max_kkt.max(s.abs());
297    }
298    println!("   max |KKT residual| = {:.3e}", max_kkt);
299    assert!(max_kkt < 1e-10);
300    println!("\n✓ Pipeline end-to-end correct.");
301}
Source

pub fn n_reduced_rows(&self) -> usize

Examples found in repository?
examples/frame_roundtrip.rs (line 55)
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}
More examples
Hide additional examples
examples/pipeline_demo.rs (line 234)
68fn main() {
69    let n_vars = 4; // a, b, c, d
70    let n_rows = 4;
71    // Jacobian sparsity (row-major).
72    let jac_pattern = [
73        (0, 0), // c0: a
74        (1, 1),
75        (1, 2), // c1: b + c
76        (2, 1),
77        (2, 2), // c2: b - c
78        (3, 0),
79        (3, 1),
80        (3, 3), // c3: a + b + d
81    ];
82    let jac_irow: Vec<i32> = jac_pattern.iter().map(|(r, _)| *r as i32).collect();
83    let jac_jcol: Vec<i32> = jac_pattern.iter().map(|(_, c)| *c as i32).collect();
84    let g_l = [1.0, 5.0, -1.0, 4.0];
85    let g_u = [1.0, 5.0, -1.0, 4.0]; // all equalities
86
87    let probe = ProbeView {
88        n_vars,
89        m_rows: n_rows,
90        jac_irow: &jac_irow,
91        jac_jcol: &jac_jcol,
92        jac_values: None,
93        g_l: &g_l,
94        g_u: &g_u,
95        linearity: None,
96        one_based: false,
97        eq_tol: 1e-12,
98        excluded_vars: None,
99        excluded_rows: None,
100    };
101
102    println!("pipeline_demo — PRs 2-7 of pounce#53");
103    println!("======================================");
104
105    // ----- Stage 1: incidence + matching -----
106    let inc = EqualityIncidence::from_probe(&probe);
107    println!(
108        "\n[Stage 1] EqualityIncidence: {} eq rows, {} vars",
109        inc.n_eq_rows(),
110        inc.n_vars
111    );
112    let m = hopcroft_karp(&inc);
113    println!("[Stage 1] Matching: size = {}", m.size);
114
115    // ----- Stage 2: DM partition -----
116    let dm = DulmageMendelsohnPartition::from_matching(&inc, &m);
117    println!("\n[Stage 2] DM partition:");
118    println!("   over rows={:?}, cols={:?}", dm.over_rows, dm.over_cols);
119    println!(
120        "   square rows={:?}, cols={:?}",
121        dm.square_rows, dm.square_cols
122    );
123    println!(
124        "   under rows={:?}, cols={:?}",
125        dm.under_rows, dm.under_cols
126    );
127
128    // ----- Stage 3: components -----
129    let comps = SquareComponents::of_square_part(&inc, &m, &dm);
130    println!(
131        "\n[Stage 3] Components: {} square block(s)",
132        comps.components.len()
133    );
134    for (i, c) in comps.components.iter().enumerate() {
135        println!("   component {i}: rows={:?}, cols={:?}", c.eq_rows, c.cols);
136    }
137
138    // ----- Stage 4: BTF -----
139    println!("\n[Stage 4] BTF (per component):");
140    let ineq = InequalityIncidence::from_probe(&probe);
141
142    // Walk each component's BTF, solve each block, build the frame.
143    let mut all_fixed_vars: Vec<usize> = Vec::new();
144    let mut all_fixed_values: Vec<Number> = Vec::new();
145    let mut all_dropped_rows: Vec<usize> = Vec::new();
146    let mut x_running = vec![0.0; n_vars];
147    // Full A and b for this problem, used both for block extraction
148    // and the final KKT check.
149    let a_full = [
150        [1.0, 0.0, 0.0, 0.0],
151        [0.0, 1.0, 1.0, 0.0],
152        [0.0, 1.0, -1.0, 0.0],
153        [1.0, 1.0, 0.0, 1.0],
154    ];
155    let b_full = [1.0, 5.0, -1.0, 4.0];
156
157    for comp in &comps.components {
158        let btf = BlockTriangularForm::of_component(&inc, &m, comp);
159        for (bi, block) in btf.blocks.iter().enumerate() {
160            let class = classify_block(block, &ineq, &Default::default());
161            println!(
162                "   component cols={:?}, block {bi}: rows={:?}, cols={:?}, class={class:?}",
163                comp.cols, block.eq_rows, block.cols
164            );
165            assert_eq!(class, AuxiliaryCouplingClass::PureEquality);
166
167            // Build a small linear system for this block, splicing in
168            // the values of already-solved variables.
169            let k = block.eq_rows.len();
170            let mut a_block = vec![0.0; k * k];
171            let mut b_block = vec![0.0; k];
172            for (ii, &r) in block.eq_rows.iter().enumerate() {
173                let mut rhs = b_full[r];
174                for j in 0..n_vars {
175                    if let Some(jj) = block.cols.iter().position(|&c| c == j) {
176                        a_block[ii * k + jj] = a_full[r][j];
177                    } else {
178                        rhs -= a_full[r][j] * x_running[j];
179                    }
180                }
181                b_block[ii] = rhs;
182            }
183
184            // Solve.
185            let mut eqs = LinearBlockEqs {
186                a: a_block,
187                b: b_block,
188                n: k,
189            };
190            let mut solver = DampedNewtonSolver;
191            let opt = BlockSolveOptions::default();
192            let out = solver.solve(&vec![0.0; k], &mut eqs, &opt).unwrap();
193
194            // Record the block's solution.
195            for (ii, &c) in block.cols.iter().enumerate() {
196                x_running[c] = out.x[ii];
197                all_fixed_vars.push(c);
198                all_fixed_values.push(out.x[ii]);
199            }
200            for &r in &block.eq_rows {
201                all_dropped_rows.push(r);
202            }
203        }
204    }
205
206    // ----- Stage 5: lift via the reduction frame -----
207    println!("\n[Stage 5] After block solves:");
208    println!("   x_running (a, b, c, d) = {:?}", x_running);
209    // For this demo we declare ALL fixed vars and rows as one
210    // composite frame — in the real PR 8 orchestrator, each
211    // elimination layer becomes its own frame on the stack.
212    // Sort by variable index for the frame's contract.
213    let mut order: Vec<usize> = (0..all_fixed_vars.len()).collect();
214    order.sort_by_key(|&i| all_fixed_vars[i]);
215    let fixed_vars_sorted: Vec<usize> = order.iter().map(|&i| all_fixed_vars[i]).collect();
216    let fixed_values_sorted: Vec<Number> = order.iter().map(|&i| all_fixed_values[i]).collect();
217    let mut dropped_rows_sorted = all_dropped_rows.clone();
218    dropped_rows_sorted.sort_unstable();
219
220    let frame = ReductionFrame::new(
221        n_vars,
222        n_rows,
223        fixed_vars_sorted.clone(),
224        fixed_values_sorted,
225        dropped_rows_sorted.clone(),
226    );
227    println!(
228        "   ReductionFrame: fixed_vars={:?}, dropped_rows={:?}",
229        frame.fixed_vars, frame.dropped_rows
230    );
231    println!(
232        "   reduced shape: {} vars × {} rows",
233        frame.n_reduced_vars(),
234        frame.n_reduced_rows()
235    );
236
237    // For this problem BTF decomposed every row into a block — the
238    // reduced shape is 0×0 and no IPM step is needed. The lift is
239    // trivial: x_full = the values the block solves produced.
240    let x_reduced: Vec<Number> = Vec::new();
241    println!("\n[Stage 6] Reduced problem is empty (presolve solved it all).");
242    let x_full = frame.lift_x(&x_reduced);
243    println!("   lifted x_full = {:?}", x_full);
244
245    // Sanity: should match (1, 2, 3, 1).
246    let expected = [1.0, 2.0, 3.0, 1.0];
247    let max_x_err = x_full
248        .iter()
249        .zip(expected.iter())
250        .map(|(a, b)| (a - b).abs())
251        .fold(0.0_f64, f64::max);
252    println!("   max |x - expected|: {:.3e}", max_x_err);
253    assert!(max_x_err < 1e-9);
254
255    // ----- Stage 7: multiplier recovery -----
256    // Objective: f = (a-1)² + (b-2)² + (c-3)² + d²
257    // ∇f at the optimum (1, 2, 3, 1): (0, 0, 0, 2).
258    let grad_f = [
259        2.0 * (x_full[0] - 1.0),
260        2.0 * (x_full[1] - 2.0),
261        2.0 * (x_full[2] - 3.0),
262        2.0 * x_full[3],
263    ];
264    println!("\n[Stage 7] ∇f at the optimum: {:?}", grad_f);
265
266    // No kept rows → reduced λ is empty.
267    let lambda_reduced: Vec<Number> = Vec::new();
268    let mut lambda_full = frame.lift_lambda(&lambda_reduced);
269    println!("   reduced λ = {:?}", lambda_reduced);
270    println!("   lifted λ (pre-recovery) = {:?}", lambda_full);
271
272    // Build full Jacobian for the recovery LU.
273    let mut jac_full = vec![0.0; n_rows * n_vars];
274    for (r, row) in a_full.iter().enumerate() {
275        for (c, &v) in row.iter().enumerate() {
276            jac_full[r * n_vars + c] = v;
277        }
278    }
279    let lam_dropped = frame
280        .recover_dropped_multipliers(&grad_f, &jac_full, &lambda_full)
281        .unwrap();
282    for (idx, &r) in frame.dropped_rows.iter().enumerate() {
283        lambda_full[r] = lam_dropped[idx];
284    }
285    println!("   recovered λ (full) = {:?}", lambda_full);
286
287    // Final KKT residual at every variable.
288    println!("\n[Stage 8] Full-space KKT residual:");
289    let mut max_kkt: Number = 0.0;
290    for i in 0..n_vars {
291        let mut s = grad_f[i];
292        for r in 0..n_rows {
293            s -= jac_full[r * n_vars + i] * lambda_full[r];
294        }
295        println!("   ∂L/∂x[{i}] = {:.3e}", s);
296        max_kkt = max_kkt.max(s.abs());
297    }
298    println!("   max |KKT residual| = {:.3e}", max_kkt);
299    assert!(max_kkt < 1e-10);
300    println!("\n✓ Pipeline end-to-end correct.");
301}
Source

pub fn project_x(&self, x_full: &[Number]) -> Vec<Number>

Project a full-space x vector into reduced space (drop the fixed entries).

Source

pub fn lift_x(&self, x_reduced: &[Number]) -> Vec<Number>

Lift a reduced x back to full space, splicing the fixed values back into their original positions.

Examples found in repository?
examples/frame_roundtrip.rs (line 64)
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}
More examples
Hide additional examples
examples/pipeline_demo.rs (line 242)
68fn main() {
69    let n_vars = 4; // a, b, c, d
70    let n_rows = 4;
71    // Jacobian sparsity (row-major).
72    let jac_pattern = [
73        (0, 0), // c0: a
74        (1, 1),
75        (1, 2), // c1: b + c
76        (2, 1),
77        (2, 2), // c2: b - c
78        (3, 0),
79        (3, 1),
80        (3, 3), // c3: a + b + d
81    ];
82    let jac_irow: Vec<i32> = jac_pattern.iter().map(|(r, _)| *r as i32).collect();
83    let jac_jcol: Vec<i32> = jac_pattern.iter().map(|(_, c)| *c as i32).collect();
84    let g_l = [1.0, 5.0, -1.0, 4.0];
85    let g_u = [1.0, 5.0, -1.0, 4.0]; // all equalities
86
87    let probe = ProbeView {
88        n_vars,
89        m_rows: n_rows,
90        jac_irow: &jac_irow,
91        jac_jcol: &jac_jcol,
92        jac_values: None,
93        g_l: &g_l,
94        g_u: &g_u,
95        linearity: None,
96        one_based: false,
97        eq_tol: 1e-12,
98        excluded_vars: None,
99        excluded_rows: None,
100    };
101
102    println!("pipeline_demo — PRs 2-7 of pounce#53");
103    println!("======================================");
104
105    // ----- Stage 1: incidence + matching -----
106    let inc = EqualityIncidence::from_probe(&probe);
107    println!(
108        "\n[Stage 1] EqualityIncidence: {} eq rows, {} vars",
109        inc.n_eq_rows(),
110        inc.n_vars
111    );
112    let m = hopcroft_karp(&inc);
113    println!("[Stage 1] Matching: size = {}", m.size);
114
115    // ----- Stage 2: DM partition -----
116    let dm = DulmageMendelsohnPartition::from_matching(&inc, &m);
117    println!("\n[Stage 2] DM partition:");
118    println!("   over rows={:?}, cols={:?}", dm.over_rows, dm.over_cols);
119    println!(
120        "   square rows={:?}, cols={:?}",
121        dm.square_rows, dm.square_cols
122    );
123    println!(
124        "   under rows={:?}, cols={:?}",
125        dm.under_rows, dm.under_cols
126    );
127
128    // ----- Stage 3: components -----
129    let comps = SquareComponents::of_square_part(&inc, &m, &dm);
130    println!(
131        "\n[Stage 3] Components: {} square block(s)",
132        comps.components.len()
133    );
134    for (i, c) in comps.components.iter().enumerate() {
135        println!("   component {i}: rows={:?}, cols={:?}", c.eq_rows, c.cols);
136    }
137
138    // ----- Stage 4: BTF -----
139    println!("\n[Stage 4] BTF (per component):");
140    let ineq = InequalityIncidence::from_probe(&probe);
141
142    // Walk each component's BTF, solve each block, build the frame.
143    let mut all_fixed_vars: Vec<usize> = Vec::new();
144    let mut all_fixed_values: Vec<Number> = Vec::new();
145    let mut all_dropped_rows: Vec<usize> = Vec::new();
146    let mut x_running = vec![0.0; n_vars];
147    // Full A and b for this problem, used both for block extraction
148    // and the final KKT check.
149    let a_full = [
150        [1.0, 0.0, 0.0, 0.0],
151        [0.0, 1.0, 1.0, 0.0],
152        [0.0, 1.0, -1.0, 0.0],
153        [1.0, 1.0, 0.0, 1.0],
154    ];
155    let b_full = [1.0, 5.0, -1.0, 4.0];
156
157    for comp in &comps.components {
158        let btf = BlockTriangularForm::of_component(&inc, &m, comp);
159        for (bi, block) in btf.blocks.iter().enumerate() {
160            let class = classify_block(block, &ineq, &Default::default());
161            println!(
162                "   component cols={:?}, block {bi}: rows={:?}, cols={:?}, class={class:?}",
163                comp.cols, block.eq_rows, block.cols
164            );
165            assert_eq!(class, AuxiliaryCouplingClass::PureEquality);
166
167            // Build a small linear system for this block, splicing in
168            // the values of already-solved variables.
169            let k = block.eq_rows.len();
170            let mut a_block = vec![0.0; k * k];
171            let mut b_block = vec![0.0; k];
172            for (ii, &r) in block.eq_rows.iter().enumerate() {
173                let mut rhs = b_full[r];
174                for j in 0..n_vars {
175                    if let Some(jj) = block.cols.iter().position(|&c| c == j) {
176                        a_block[ii * k + jj] = a_full[r][j];
177                    } else {
178                        rhs -= a_full[r][j] * x_running[j];
179                    }
180                }
181                b_block[ii] = rhs;
182            }
183
184            // Solve.
185            let mut eqs = LinearBlockEqs {
186                a: a_block,
187                b: b_block,
188                n: k,
189            };
190            let mut solver = DampedNewtonSolver;
191            let opt = BlockSolveOptions::default();
192            let out = solver.solve(&vec![0.0; k], &mut eqs, &opt).unwrap();
193
194            // Record the block's solution.
195            for (ii, &c) in block.cols.iter().enumerate() {
196                x_running[c] = out.x[ii];
197                all_fixed_vars.push(c);
198                all_fixed_values.push(out.x[ii]);
199            }
200            for &r in &block.eq_rows {
201                all_dropped_rows.push(r);
202            }
203        }
204    }
205
206    // ----- Stage 5: lift via the reduction frame -----
207    println!("\n[Stage 5] After block solves:");
208    println!("   x_running (a, b, c, d) = {:?}", x_running);
209    // For this demo we declare ALL fixed vars and rows as one
210    // composite frame — in the real PR 8 orchestrator, each
211    // elimination layer becomes its own frame on the stack.
212    // Sort by variable index for the frame's contract.
213    let mut order: Vec<usize> = (0..all_fixed_vars.len()).collect();
214    order.sort_by_key(|&i| all_fixed_vars[i]);
215    let fixed_vars_sorted: Vec<usize> = order.iter().map(|&i| all_fixed_vars[i]).collect();
216    let fixed_values_sorted: Vec<Number> = order.iter().map(|&i| all_fixed_values[i]).collect();
217    let mut dropped_rows_sorted = all_dropped_rows.clone();
218    dropped_rows_sorted.sort_unstable();
219
220    let frame = ReductionFrame::new(
221        n_vars,
222        n_rows,
223        fixed_vars_sorted.clone(),
224        fixed_values_sorted,
225        dropped_rows_sorted.clone(),
226    );
227    println!(
228        "   ReductionFrame: fixed_vars={:?}, dropped_rows={:?}",
229        frame.fixed_vars, frame.dropped_rows
230    );
231    println!(
232        "   reduced shape: {} vars × {} rows",
233        frame.n_reduced_vars(),
234        frame.n_reduced_rows()
235    );
236
237    // For this problem BTF decomposed every row into a block — the
238    // reduced shape is 0×0 and no IPM step is needed. The lift is
239    // trivial: x_full = the values the block solves produced.
240    let x_reduced: Vec<Number> = Vec::new();
241    println!("\n[Stage 6] Reduced problem is empty (presolve solved it all).");
242    let x_full = frame.lift_x(&x_reduced);
243    println!("   lifted x_full = {:?}", x_full);
244
245    // Sanity: should match (1, 2, 3, 1).
246    let expected = [1.0, 2.0, 3.0, 1.0];
247    let max_x_err = x_full
248        .iter()
249        .zip(expected.iter())
250        .map(|(a, b)| (a - b).abs())
251        .fold(0.0_f64, f64::max);
252    println!("   max |x - expected|: {:.3e}", max_x_err);
253    assert!(max_x_err < 1e-9);
254
255    // ----- Stage 7: multiplier recovery -----
256    // Objective: f = (a-1)² + (b-2)² + (c-3)² + d²
257    // ∇f at the optimum (1, 2, 3, 1): (0, 0, 0, 2).
258    let grad_f = [
259        2.0 * (x_full[0] - 1.0),
260        2.0 * (x_full[1] - 2.0),
261        2.0 * (x_full[2] - 3.0),
262        2.0 * x_full[3],
263    ];
264    println!("\n[Stage 7] ∇f at the optimum: {:?}", grad_f);
265
266    // No kept rows → reduced λ is empty.
267    let lambda_reduced: Vec<Number> = Vec::new();
268    let mut lambda_full = frame.lift_lambda(&lambda_reduced);
269    println!("   reduced λ = {:?}", lambda_reduced);
270    println!("   lifted λ (pre-recovery) = {:?}", lambda_full);
271
272    // Build full Jacobian for the recovery LU.
273    let mut jac_full = vec![0.0; n_rows * n_vars];
274    for (r, row) in a_full.iter().enumerate() {
275        for (c, &v) in row.iter().enumerate() {
276            jac_full[r * n_vars + c] = v;
277        }
278    }
279    let lam_dropped = frame
280        .recover_dropped_multipliers(&grad_f, &jac_full, &lambda_full)
281        .unwrap();
282    for (idx, &r) in frame.dropped_rows.iter().enumerate() {
283        lambda_full[r] = lam_dropped[idx];
284    }
285    println!("   recovered λ (full) = {:?}", lambda_full);
286
287    // Final KKT residual at every variable.
288    println!("\n[Stage 8] Full-space KKT residual:");
289    let mut max_kkt: Number = 0.0;
290    for i in 0..n_vars {
291        let mut s = grad_f[i];
292        for r in 0..n_rows {
293            s -= jac_full[r * n_vars + i] * lambda_full[r];
294        }
295        println!("   ∂L/∂x[{i}] = {:.3e}", s);
296        max_kkt = max_kkt.max(s.abs());
297    }
298    println!("   max |KKT residual| = {:.3e}", max_kkt);
299    assert!(max_kkt < 1e-10);
300    println!("\n✓ Pipeline end-to-end correct.");
301}
Source

pub fn project_lambda(&self, lambda_full: &[Number]) -> Vec<Number>

Project a full-space λ vector into reduced space.

Source

pub fn lift_lambda(&self, lambda_reduced: &[Number]) -> Vec<Number>

Lift a reduced λ back to full space, with zeros at dropped row indices. (Real values for dropped rows come from Self::recover_dropped_multipliers.)

Examples found in repository?
examples/frame_roundtrip.rs (line 72)
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}
More examples
Hide additional examples
examples/pipeline_demo.rs (line 268)
68fn main() {
69    let n_vars = 4; // a, b, c, d
70    let n_rows = 4;
71    // Jacobian sparsity (row-major).
72    let jac_pattern = [
73        (0, 0), // c0: a
74        (1, 1),
75        (1, 2), // c1: b + c
76        (2, 1),
77        (2, 2), // c2: b - c
78        (3, 0),
79        (3, 1),
80        (3, 3), // c3: a + b + d
81    ];
82    let jac_irow: Vec<i32> = jac_pattern.iter().map(|(r, _)| *r as i32).collect();
83    let jac_jcol: Vec<i32> = jac_pattern.iter().map(|(_, c)| *c as i32).collect();
84    let g_l = [1.0, 5.0, -1.0, 4.0];
85    let g_u = [1.0, 5.0, -1.0, 4.0]; // all equalities
86
87    let probe = ProbeView {
88        n_vars,
89        m_rows: n_rows,
90        jac_irow: &jac_irow,
91        jac_jcol: &jac_jcol,
92        jac_values: None,
93        g_l: &g_l,
94        g_u: &g_u,
95        linearity: None,
96        one_based: false,
97        eq_tol: 1e-12,
98        excluded_vars: None,
99        excluded_rows: None,
100    };
101
102    println!("pipeline_demo — PRs 2-7 of pounce#53");
103    println!("======================================");
104
105    // ----- Stage 1: incidence + matching -----
106    let inc = EqualityIncidence::from_probe(&probe);
107    println!(
108        "\n[Stage 1] EqualityIncidence: {} eq rows, {} vars",
109        inc.n_eq_rows(),
110        inc.n_vars
111    );
112    let m = hopcroft_karp(&inc);
113    println!("[Stage 1] Matching: size = {}", m.size);
114
115    // ----- Stage 2: DM partition -----
116    let dm = DulmageMendelsohnPartition::from_matching(&inc, &m);
117    println!("\n[Stage 2] DM partition:");
118    println!("   over rows={:?}, cols={:?}", dm.over_rows, dm.over_cols);
119    println!(
120        "   square rows={:?}, cols={:?}",
121        dm.square_rows, dm.square_cols
122    );
123    println!(
124        "   under rows={:?}, cols={:?}",
125        dm.under_rows, dm.under_cols
126    );
127
128    // ----- Stage 3: components -----
129    let comps = SquareComponents::of_square_part(&inc, &m, &dm);
130    println!(
131        "\n[Stage 3] Components: {} square block(s)",
132        comps.components.len()
133    );
134    for (i, c) in comps.components.iter().enumerate() {
135        println!("   component {i}: rows={:?}, cols={:?}", c.eq_rows, c.cols);
136    }
137
138    // ----- Stage 4: BTF -----
139    println!("\n[Stage 4] BTF (per component):");
140    let ineq = InequalityIncidence::from_probe(&probe);
141
142    // Walk each component's BTF, solve each block, build the frame.
143    let mut all_fixed_vars: Vec<usize> = Vec::new();
144    let mut all_fixed_values: Vec<Number> = Vec::new();
145    let mut all_dropped_rows: Vec<usize> = Vec::new();
146    let mut x_running = vec![0.0; n_vars];
147    // Full A and b for this problem, used both for block extraction
148    // and the final KKT check.
149    let a_full = [
150        [1.0, 0.0, 0.0, 0.0],
151        [0.0, 1.0, 1.0, 0.0],
152        [0.0, 1.0, -1.0, 0.0],
153        [1.0, 1.0, 0.0, 1.0],
154    ];
155    let b_full = [1.0, 5.0, -1.0, 4.0];
156
157    for comp in &comps.components {
158        let btf = BlockTriangularForm::of_component(&inc, &m, comp);
159        for (bi, block) in btf.blocks.iter().enumerate() {
160            let class = classify_block(block, &ineq, &Default::default());
161            println!(
162                "   component cols={:?}, block {bi}: rows={:?}, cols={:?}, class={class:?}",
163                comp.cols, block.eq_rows, block.cols
164            );
165            assert_eq!(class, AuxiliaryCouplingClass::PureEquality);
166
167            // Build a small linear system for this block, splicing in
168            // the values of already-solved variables.
169            let k = block.eq_rows.len();
170            let mut a_block = vec![0.0; k * k];
171            let mut b_block = vec![0.0; k];
172            for (ii, &r) in block.eq_rows.iter().enumerate() {
173                let mut rhs = b_full[r];
174                for j in 0..n_vars {
175                    if let Some(jj) = block.cols.iter().position(|&c| c == j) {
176                        a_block[ii * k + jj] = a_full[r][j];
177                    } else {
178                        rhs -= a_full[r][j] * x_running[j];
179                    }
180                }
181                b_block[ii] = rhs;
182            }
183
184            // Solve.
185            let mut eqs = LinearBlockEqs {
186                a: a_block,
187                b: b_block,
188                n: k,
189            };
190            let mut solver = DampedNewtonSolver;
191            let opt = BlockSolveOptions::default();
192            let out = solver.solve(&vec![0.0; k], &mut eqs, &opt).unwrap();
193
194            // Record the block's solution.
195            for (ii, &c) in block.cols.iter().enumerate() {
196                x_running[c] = out.x[ii];
197                all_fixed_vars.push(c);
198                all_fixed_values.push(out.x[ii]);
199            }
200            for &r in &block.eq_rows {
201                all_dropped_rows.push(r);
202            }
203        }
204    }
205
206    // ----- Stage 5: lift via the reduction frame -----
207    println!("\n[Stage 5] After block solves:");
208    println!("   x_running (a, b, c, d) = {:?}", x_running);
209    // For this demo we declare ALL fixed vars and rows as one
210    // composite frame — in the real PR 8 orchestrator, each
211    // elimination layer becomes its own frame on the stack.
212    // Sort by variable index for the frame's contract.
213    let mut order: Vec<usize> = (0..all_fixed_vars.len()).collect();
214    order.sort_by_key(|&i| all_fixed_vars[i]);
215    let fixed_vars_sorted: Vec<usize> = order.iter().map(|&i| all_fixed_vars[i]).collect();
216    let fixed_values_sorted: Vec<Number> = order.iter().map(|&i| all_fixed_values[i]).collect();
217    let mut dropped_rows_sorted = all_dropped_rows.clone();
218    dropped_rows_sorted.sort_unstable();
219
220    let frame = ReductionFrame::new(
221        n_vars,
222        n_rows,
223        fixed_vars_sorted.clone(),
224        fixed_values_sorted,
225        dropped_rows_sorted.clone(),
226    );
227    println!(
228        "   ReductionFrame: fixed_vars={:?}, dropped_rows={:?}",
229        frame.fixed_vars, frame.dropped_rows
230    );
231    println!(
232        "   reduced shape: {} vars × {} rows",
233        frame.n_reduced_vars(),
234        frame.n_reduced_rows()
235    );
236
237    // For this problem BTF decomposed every row into a block — the
238    // reduced shape is 0×0 and no IPM step is needed. The lift is
239    // trivial: x_full = the values the block solves produced.
240    let x_reduced: Vec<Number> = Vec::new();
241    println!("\n[Stage 6] Reduced problem is empty (presolve solved it all).");
242    let x_full = frame.lift_x(&x_reduced);
243    println!("   lifted x_full = {:?}", x_full);
244
245    // Sanity: should match (1, 2, 3, 1).
246    let expected = [1.0, 2.0, 3.0, 1.0];
247    let max_x_err = x_full
248        .iter()
249        .zip(expected.iter())
250        .map(|(a, b)| (a - b).abs())
251        .fold(0.0_f64, f64::max);
252    println!("   max |x - expected|: {:.3e}", max_x_err);
253    assert!(max_x_err < 1e-9);
254
255    // ----- Stage 7: multiplier recovery -----
256    // Objective: f = (a-1)² + (b-2)² + (c-3)² + d²
257    // ∇f at the optimum (1, 2, 3, 1): (0, 0, 0, 2).
258    let grad_f = [
259        2.0 * (x_full[0] - 1.0),
260        2.0 * (x_full[1] - 2.0),
261        2.0 * (x_full[2] - 3.0),
262        2.0 * x_full[3],
263    ];
264    println!("\n[Stage 7] ∇f at the optimum: {:?}", grad_f);
265
266    // No kept rows → reduced λ is empty.
267    let lambda_reduced: Vec<Number> = Vec::new();
268    let mut lambda_full = frame.lift_lambda(&lambda_reduced);
269    println!("   reduced λ = {:?}", lambda_reduced);
270    println!("   lifted λ (pre-recovery) = {:?}", lambda_full);
271
272    // Build full Jacobian for the recovery LU.
273    let mut jac_full = vec![0.0; n_rows * n_vars];
274    for (r, row) in a_full.iter().enumerate() {
275        for (c, &v) in row.iter().enumerate() {
276            jac_full[r * n_vars + c] = v;
277        }
278    }
279    let lam_dropped = frame
280        .recover_dropped_multipliers(&grad_f, &jac_full, &lambda_full)
281        .unwrap();
282    for (idx, &r) in frame.dropped_rows.iter().enumerate() {
283        lambda_full[r] = lam_dropped[idx];
284    }
285    println!("   recovered λ (full) = {:?}", lambda_full);
286
287    // Final KKT residual at every variable.
288    println!("\n[Stage 8] Full-space KKT residual:");
289    let mut max_kkt: Number = 0.0;
290    for i in 0..n_vars {
291        let mut s = grad_f[i];
292        for r in 0..n_rows {
293            s -= jac_full[r * n_vars + i] * lambda_full[r];
294        }
295        println!("   ∂L/∂x[{i}] = {:.3e}", s);
296        max_kkt = max_kkt.max(s.abs());
297    }
298    println!("   max |KKT residual| = {:.3e}", max_kkt);
299    assert!(max_kkt < 1e-10);
300    println!("\n✓ Pipeline end-to-end correct.");
301}
Source

pub fn recover_dropped_multipliers( &self, grad_f: &[Number], jac_full_row_major: &[Number], lambda_full: &[Number], ) -> Result<Vec<Number>, BlockSolveError>

Recover the k = fixed_vars.len() dropped-row multipliers via dense LU on the full-space KKT stationarity equations at the fixed variables. Returns one entry per self.dropped_rows (in the same order).

Assumption: fixed variables are interior to their original bounds at the optimum (so z_l = z_u = 0 for them).

§Inputs
  • grad_f — objective gradient at the full-space optimum (length n_full_vars).
  • jac_full_row_major — dense full-space Jacobian (n_full_rows × n_full_vars) at the optimum.
  • lambda_full — multipliers for kept rows; entries at dropped-row positions are ignored.
§Example
use pounce_presolve::reduction_frame::ReductionFrame;

// 1 var, 1 row, dropped:  c(x) = x - 3 = 0, obj f = 4 x.
// Stationarity:  4 - 1 * λ = 0  →  λ = 4.
let frame = ReductionFrame::new(1, 1, vec![0], vec![3.0], vec![0]);
let grad_f = [4.0];
let jac = [1.0];
let lambda_full = [0.0]; // dropped, ignored
let lam = frame
    .recover_dropped_multipliers(&grad_f, &jac, &lambda_full)
    .unwrap();
assert!((lam[0] - 4.0).abs() < 1e-12);
Examples found in repository?
examples/frame_roundtrip.rs (line 86)
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}
More examples
Hide additional examples
examples/pipeline_demo.rs (line 280)
68fn main() {
69    let n_vars = 4; // a, b, c, d
70    let n_rows = 4;
71    // Jacobian sparsity (row-major).
72    let jac_pattern = [
73        (0, 0), // c0: a
74        (1, 1),
75        (1, 2), // c1: b + c
76        (2, 1),
77        (2, 2), // c2: b - c
78        (3, 0),
79        (3, 1),
80        (3, 3), // c3: a + b + d
81    ];
82    let jac_irow: Vec<i32> = jac_pattern.iter().map(|(r, _)| *r as i32).collect();
83    let jac_jcol: Vec<i32> = jac_pattern.iter().map(|(_, c)| *c as i32).collect();
84    let g_l = [1.0, 5.0, -1.0, 4.0];
85    let g_u = [1.0, 5.0, -1.0, 4.0]; // all equalities
86
87    let probe = ProbeView {
88        n_vars,
89        m_rows: n_rows,
90        jac_irow: &jac_irow,
91        jac_jcol: &jac_jcol,
92        jac_values: None,
93        g_l: &g_l,
94        g_u: &g_u,
95        linearity: None,
96        one_based: false,
97        eq_tol: 1e-12,
98        excluded_vars: None,
99        excluded_rows: None,
100    };
101
102    println!("pipeline_demo — PRs 2-7 of pounce#53");
103    println!("======================================");
104
105    // ----- Stage 1: incidence + matching -----
106    let inc = EqualityIncidence::from_probe(&probe);
107    println!(
108        "\n[Stage 1] EqualityIncidence: {} eq rows, {} vars",
109        inc.n_eq_rows(),
110        inc.n_vars
111    );
112    let m = hopcroft_karp(&inc);
113    println!("[Stage 1] Matching: size = {}", m.size);
114
115    // ----- Stage 2: DM partition -----
116    let dm = DulmageMendelsohnPartition::from_matching(&inc, &m);
117    println!("\n[Stage 2] DM partition:");
118    println!("   over rows={:?}, cols={:?}", dm.over_rows, dm.over_cols);
119    println!(
120        "   square rows={:?}, cols={:?}",
121        dm.square_rows, dm.square_cols
122    );
123    println!(
124        "   under rows={:?}, cols={:?}",
125        dm.under_rows, dm.under_cols
126    );
127
128    // ----- Stage 3: components -----
129    let comps = SquareComponents::of_square_part(&inc, &m, &dm);
130    println!(
131        "\n[Stage 3] Components: {} square block(s)",
132        comps.components.len()
133    );
134    for (i, c) in comps.components.iter().enumerate() {
135        println!("   component {i}: rows={:?}, cols={:?}", c.eq_rows, c.cols);
136    }
137
138    // ----- Stage 4: BTF -----
139    println!("\n[Stage 4] BTF (per component):");
140    let ineq = InequalityIncidence::from_probe(&probe);
141
142    // Walk each component's BTF, solve each block, build the frame.
143    let mut all_fixed_vars: Vec<usize> = Vec::new();
144    let mut all_fixed_values: Vec<Number> = Vec::new();
145    let mut all_dropped_rows: Vec<usize> = Vec::new();
146    let mut x_running = vec![0.0; n_vars];
147    // Full A and b for this problem, used both for block extraction
148    // and the final KKT check.
149    let a_full = [
150        [1.0, 0.0, 0.0, 0.0],
151        [0.0, 1.0, 1.0, 0.0],
152        [0.0, 1.0, -1.0, 0.0],
153        [1.0, 1.0, 0.0, 1.0],
154    ];
155    let b_full = [1.0, 5.0, -1.0, 4.0];
156
157    for comp in &comps.components {
158        let btf = BlockTriangularForm::of_component(&inc, &m, comp);
159        for (bi, block) in btf.blocks.iter().enumerate() {
160            let class = classify_block(block, &ineq, &Default::default());
161            println!(
162                "   component cols={:?}, block {bi}: rows={:?}, cols={:?}, class={class:?}",
163                comp.cols, block.eq_rows, block.cols
164            );
165            assert_eq!(class, AuxiliaryCouplingClass::PureEquality);
166
167            // Build a small linear system for this block, splicing in
168            // the values of already-solved variables.
169            let k = block.eq_rows.len();
170            let mut a_block = vec![0.0; k * k];
171            let mut b_block = vec![0.0; k];
172            for (ii, &r) in block.eq_rows.iter().enumerate() {
173                let mut rhs = b_full[r];
174                for j in 0..n_vars {
175                    if let Some(jj) = block.cols.iter().position(|&c| c == j) {
176                        a_block[ii * k + jj] = a_full[r][j];
177                    } else {
178                        rhs -= a_full[r][j] * x_running[j];
179                    }
180                }
181                b_block[ii] = rhs;
182            }
183
184            // Solve.
185            let mut eqs = LinearBlockEqs {
186                a: a_block,
187                b: b_block,
188                n: k,
189            };
190            let mut solver = DampedNewtonSolver;
191            let opt = BlockSolveOptions::default();
192            let out = solver.solve(&vec![0.0; k], &mut eqs, &opt).unwrap();
193
194            // Record the block's solution.
195            for (ii, &c) in block.cols.iter().enumerate() {
196                x_running[c] = out.x[ii];
197                all_fixed_vars.push(c);
198                all_fixed_values.push(out.x[ii]);
199            }
200            for &r in &block.eq_rows {
201                all_dropped_rows.push(r);
202            }
203        }
204    }
205
206    // ----- Stage 5: lift via the reduction frame -----
207    println!("\n[Stage 5] After block solves:");
208    println!("   x_running (a, b, c, d) = {:?}", x_running);
209    // For this demo we declare ALL fixed vars and rows as one
210    // composite frame — in the real PR 8 orchestrator, each
211    // elimination layer becomes its own frame on the stack.
212    // Sort by variable index for the frame's contract.
213    let mut order: Vec<usize> = (0..all_fixed_vars.len()).collect();
214    order.sort_by_key(|&i| all_fixed_vars[i]);
215    let fixed_vars_sorted: Vec<usize> = order.iter().map(|&i| all_fixed_vars[i]).collect();
216    let fixed_values_sorted: Vec<Number> = order.iter().map(|&i| all_fixed_values[i]).collect();
217    let mut dropped_rows_sorted = all_dropped_rows.clone();
218    dropped_rows_sorted.sort_unstable();
219
220    let frame = ReductionFrame::new(
221        n_vars,
222        n_rows,
223        fixed_vars_sorted.clone(),
224        fixed_values_sorted,
225        dropped_rows_sorted.clone(),
226    );
227    println!(
228        "   ReductionFrame: fixed_vars={:?}, dropped_rows={:?}",
229        frame.fixed_vars, frame.dropped_rows
230    );
231    println!(
232        "   reduced shape: {} vars × {} rows",
233        frame.n_reduced_vars(),
234        frame.n_reduced_rows()
235    );
236
237    // For this problem BTF decomposed every row into a block — the
238    // reduced shape is 0×0 and no IPM step is needed. The lift is
239    // trivial: x_full = the values the block solves produced.
240    let x_reduced: Vec<Number> = Vec::new();
241    println!("\n[Stage 6] Reduced problem is empty (presolve solved it all).");
242    let x_full = frame.lift_x(&x_reduced);
243    println!("   lifted x_full = {:?}", x_full);
244
245    // Sanity: should match (1, 2, 3, 1).
246    let expected = [1.0, 2.0, 3.0, 1.0];
247    let max_x_err = x_full
248        .iter()
249        .zip(expected.iter())
250        .map(|(a, b)| (a - b).abs())
251        .fold(0.0_f64, f64::max);
252    println!("   max |x - expected|: {:.3e}", max_x_err);
253    assert!(max_x_err < 1e-9);
254
255    // ----- Stage 7: multiplier recovery -----
256    // Objective: f = (a-1)² + (b-2)² + (c-3)² + d²
257    // ∇f at the optimum (1, 2, 3, 1): (0, 0, 0, 2).
258    let grad_f = [
259        2.0 * (x_full[0] - 1.0),
260        2.0 * (x_full[1] - 2.0),
261        2.0 * (x_full[2] - 3.0),
262        2.0 * x_full[3],
263    ];
264    println!("\n[Stage 7] ∇f at the optimum: {:?}", grad_f);
265
266    // No kept rows → reduced λ is empty.
267    let lambda_reduced: Vec<Number> = Vec::new();
268    let mut lambda_full = frame.lift_lambda(&lambda_reduced);
269    println!("   reduced λ = {:?}", lambda_reduced);
270    println!("   lifted λ (pre-recovery) = {:?}", lambda_full);
271
272    // Build full Jacobian for the recovery LU.
273    let mut jac_full = vec![0.0; n_rows * n_vars];
274    for (r, row) in a_full.iter().enumerate() {
275        for (c, &v) in row.iter().enumerate() {
276            jac_full[r * n_vars + c] = v;
277        }
278    }
279    let lam_dropped = frame
280        .recover_dropped_multipliers(&grad_f, &jac_full, &lambda_full)
281        .unwrap();
282    for (idx, &r) in frame.dropped_rows.iter().enumerate() {
283        lambda_full[r] = lam_dropped[idx];
284    }
285    println!("   recovered λ (full) = {:?}", lambda_full);
286
287    // Final KKT residual at every variable.
288    println!("\n[Stage 8] Full-space KKT residual:");
289    let mut max_kkt: Number = 0.0;
290    for i in 0..n_vars {
291        let mut s = grad_f[i];
292        for r in 0..n_rows {
293            s -= jac_full[r * n_vars + i] * lambda_full[r];
294        }
295        println!("   ∂L/∂x[{i}] = {:.3e}", s);
296        max_kkt = max_kkt.max(s.abs());
297    }
298    println!("   max |KKT residual| = {:.3e}", max_kkt);
299    assert!(max_kkt < 1e-10);
300    println!("\n✓ Pipeline end-to-end correct.");
301}

Trait Implementations§

Source§

impl Clone for ReductionFrame

Source§

fn clone(&self) -> ReductionFrame

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ReductionFrame

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for ReductionFrame

Source§

fn default() -> ReductionFrame

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more