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
impl ReductionFrame
Sourcepub fn new(
n_vars: usize,
n_rows: usize,
fixed_vars: Vec<usize>,
fixed_values: Vec<Number>,
dropped_rows: Vec<usize>,
) -> Self
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?
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
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}Sourcepub fn n_full_vars(&self) -> usize
pub fn n_full_vars(&self) -> usize
Examples found in repository?
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}Sourcepub fn n_full_rows(&self) -> usize
pub fn n_full_rows(&self) -> usize
Examples found in repository?
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}Sourcepub fn n_reduced_vars(&self) -> usize
pub fn n_reduced_vars(&self) -> usize
Examples found in repository?
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
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}Sourcepub fn n_reduced_rows(&self) -> usize
pub fn n_reduced_rows(&self) -> usize
Examples found in repository?
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
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}Sourcepub fn project_x(&self, x_full: &[Number]) -> Vec<Number> ⓘ
pub fn project_x(&self, x_full: &[Number]) -> Vec<Number> ⓘ
Project a full-space x vector into reduced space (drop the
fixed entries).
Sourcepub fn lift_x(&self, x_reduced: &[Number]) -> Vec<Number> ⓘ
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?
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
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}Sourcepub fn project_lambda(&self, lambda_full: &[Number]) -> Vec<Number> ⓘ
pub fn project_lambda(&self, lambda_full: &[Number]) -> Vec<Number> ⓘ
Project a full-space λ vector into reduced space.
Sourcepub fn lift_lambda(&self, lambda_reduced: &[Number]) -> Vec<Number> ⓘ
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?
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
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}Sourcepub fn recover_dropped_multipliers(
&self,
grad_f: &[Number],
jac_full_row_major: &[Number],
lambda_full: &[Number],
) -> Result<Vec<Number>, BlockSolveError>
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 (lengthn_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?
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
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
impl Clone for ReductionFrame
Source§fn clone(&self) -> ReductionFrame
fn clone(&self) -> ReductionFrame
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more