1use crate::kkt::aug_system_solver::{AugSysCoeffs, AugSysRhs, AugSysSol, AugSystemSolver};
27use pounce_common::tagged::Tag;
28use pounce_common::timing::TimingStatistics;
29use pounce_common::types::{Index, Number};
30use pounce_linalg::dense_gen_matrix::{DenseGenMatrix, DenseGenMatrixSpace};
31use pounce_linalg::dense_sym_matrix::DenseSymMatrixSpace;
32use pounce_linalg::dense_vector::{DenseVector, DenseVectorSpace};
33use pounce_linalg::diag_matrix::DiagMatrix;
34use pounce_linalg::low_rank_update_sym_matrix::LowRankUpdateSymMatrix;
35use pounce_linalg::multi_vector_matrix::{MultiVectorMatrix, MultiVectorMatrixSpace};
36use pounce_linalg::{Matrix, SymMatrix, Vector};
37use pounce_linsol::ESymSolverStatus;
38use std::rc::Rc;
39
40pub struct LowRankAugSystemSolver {
41 inner: Box<dyn AugSystemSolver>,
43 first_call: bool,
45 num_neg_evals: Index,
47 cache: AugSysCache,
49 factor: Factorization,
51 inner_has_factor: bool,
65 inner_factor_neg_evals: Option<Index>,
70 bypass: Option<Box<dyn AugSystemSolver>>,
80 last_solve_took_bypass: bool,
83}
84
85#[derive(Debug, Clone)]
86pub struct AugSysCache {
87 pub w_tag: Tag,
88 pub w_factor: Number,
89 pub d_x_tag: Tag,
90 pub delta_x: Number,
91 pub d_s_tag: Tag,
92 pub delta_s: Number,
93 pub j_c_tag: Tag,
94 pub d_c_tag: Tag,
95 pub delta_c: Number,
96 pub j_d_tag: Tag,
97 pub d_d_tag: Tag,
98 pub delta_d: Number,
99}
100
101impl Default for AugSysCache {
102 fn default() -> Self {
103 Self {
104 w_tag: Tag::NONE,
105 w_factor: 0.0,
106 d_x_tag: Tag::NONE,
107 delta_x: 0.0,
108 d_s_tag: Tag::NONE,
109 delta_s: 0.0,
110 j_c_tag: Tag::NONE,
111 d_c_tag: Tag::NONE,
112 delta_c: 0.0,
113 j_d_tag: Tag::NONE,
114 d_d_tag: Tag::NONE,
115 delta_d: 0.0,
116 }
117 }
118}
119
120#[derive(Default)]
121struct Factorization {
122 wdiag: Option<Box<DiagMatrix>>,
126 j1: Option<DenseGenMatrix>,
128 j2: Option<DenseGenMatrix>,
130 vtilde1_x: Option<MultiVectorMatrix>,
132 vtilde1_s: Option<MultiVectorMatrix>,
133 vtilde1_c: Option<MultiVectorMatrix>,
134 vtilde1_d: Option<MultiVectorMatrix>,
135 utilde2_x: Option<MultiVectorMatrix>,
137 utilde2_s: Option<MultiVectorMatrix>,
138 utilde2_c: Option<MultiVectorMatrix>,
139 utilde2_d: Option<MultiVectorMatrix>,
140}
141
142impl LowRankAugSystemSolver {
143 pub fn new(inner: Box<dyn AugSystemSolver>) -> Self {
144 Self {
145 inner,
146 first_call: true,
147 num_neg_evals: 0,
148 cache: AugSysCache::default(),
149 factor: Factorization::default(),
150 inner_has_factor: false,
151 inner_factor_neg_evals: None,
152 bypass: None,
153 last_solve_took_bypass: false,
154 }
155 }
156
157 pub fn with_bypass_solver(
193 inner: Box<dyn AugSystemSolver>,
194 bypass: Box<dyn AugSystemSolver>,
195 ) -> Self {
196 Self {
197 bypass: Some(bypass),
198 ..Self::new(inner)
199 }
200 }
201
202 pub fn augmented_system_requires_change(&self, coeffs: &AugSysCoeffs<'_>) -> bool {
205 let cache = &self.cache;
206 let zero_tag: Tag = Tag::NONE;
207
208 let w_changed = match coeffs.w {
209 Some(w) => w.as_tagged().get_tag() != cache.w_tag,
210 None => cache.w_tag != zero_tag,
211 };
212 if w_changed || coeffs.w_factor != cache.w_factor {
213 return true;
214 }
215 let dx_changed = match coeffs.d_x {
216 Some(d) => d.as_tagged().get_tag() != cache.d_x_tag,
217 None => cache.d_x_tag != zero_tag,
218 };
219 if dx_changed || coeffs.delta_x != cache.delta_x {
220 return true;
221 }
222 let ds_changed = match coeffs.d_s {
223 Some(d) => d.as_tagged().get_tag() != cache.d_s_tag,
224 None => cache.d_s_tag != zero_tag,
225 };
226 if ds_changed || coeffs.delta_s != cache.delta_s {
227 return true;
228 }
229 if coeffs.j_c.as_tagged().get_tag() != cache.j_c_tag {
230 return true;
231 }
232 let dc_changed = match coeffs.d_c {
233 Some(d) => d.as_tagged().get_tag() != cache.d_c_tag,
234 None => cache.d_c_tag != zero_tag,
235 };
236 if dc_changed || coeffs.delta_c != cache.delta_c {
237 return true;
238 }
239 if coeffs.j_d.as_tagged().get_tag() != cache.j_d_tag {
240 return true;
241 }
242 let dd_changed = match coeffs.d_d {
243 Some(d) => d.as_tagged().get_tag() != cache.d_d_tag,
244 None => cache.d_d_tag != zero_tag,
245 };
246 if dd_changed || coeffs.delta_d != cache.delta_d {
247 return true;
248 }
249 false
250 }
251
252 fn store_cache(&mut self, coeffs: &AugSysCoeffs<'_>) {
253 let zero_tag = Tag::NONE;
254 self.cache.w_tag = coeffs
255 .w
256 .map(|w| w.as_tagged().get_tag())
257 .unwrap_or(zero_tag);
258 self.cache.w_factor = coeffs.w_factor;
259 self.cache.d_x_tag = coeffs
260 .d_x
261 .map(|d| d.as_tagged().get_tag())
262 .unwrap_or(zero_tag);
263 self.cache.delta_x = coeffs.delta_x;
264 self.cache.d_s_tag = coeffs
265 .d_s
266 .map(|d| d.as_tagged().get_tag())
267 .unwrap_or(zero_tag);
268 self.cache.delta_s = coeffs.delta_s;
269 self.cache.j_c_tag = coeffs.j_c.as_tagged().get_tag();
270 self.cache.d_c_tag = coeffs
271 .d_c
272 .map(|d| d.as_tagged().get_tag())
273 .unwrap_or(zero_tag);
274 self.cache.delta_c = coeffs.delta_c;
275 self.cache.j_d_tag = coeffs.j_d.as_tagged().get_tag();
276 self.cache.d_d_tag = coeffs
277 .d_d
278 .map(|d| d.as_tagged().get_tag())
279 .unwrap_or(zero_tag);
280 self.cache.delta_d = coeffs.delta_d;
281 }
282
283 pub fn first_call(&self) -> bool {
284 self.first_call
285 }
286
287 pub fn cache(&self) -> &AugSysCache {
288 &self.cache
289 }
290
291 fn update_factorization(
298 &mut self,
299 lr_w: &LowRankUpdateSymMatrix,
300 coeffs: &AugSysCoeffs<'_>,
301 proto: &AugSysRhs<'_>,
302 check_neg_evals: bool,
303 num_neg_evals: Index,
304 ) -> ESymSolverStatus {
305 self.inner_has_factor = false;
314 self.inner_factor_neg_evals = None;
315
316 let proto_x = downcast_dense(proto.rhs_x);
317 let proto_s = downcast_dense(proto.rhs_s);
318 let proto_c = downcast_dense(proto.rhs_c);
319 let proto_d = downcast_dense(proto.rhs_d);
320 let space_x = Rc::clone(proto_x.space());
321 let space_s = Rc::clone(proto_s.space());
322 let space_c = Rc::clone(proto_c.space());
323 let space_d = Rc::clone(proto_d.space());
324
325 let b0_dense: DenseVector = if coeffs.w_factor == 1.0 {
329 match lr_w.get_diag() {
330 Some(d) => clone_dense(downcast_dense(d.as_ref())),
331 None => zero_x_for(&space_x, lr_w),
332 }
333 } else {
334 zero_x_for(&space_x, lr_w)
335 };
336
337 let wdiag_diag: Rc<dyn Vector> = match (lr_w.p_lowrank(), lr_w.reduced_diag()) {
338 (Some(p_lm), true) => {
339 let mut fullx = space_x.make_new_dense();
341 p_lm.mult_vector(1.0, &b0_dense, 0.0, &mut fullx);
342 Rc::new(fullx) as Rc<dyn Vector>
343 }
344 _ => Rc::new(clone_dense(&b0_dense)) as Rc<dyn Vector>,
345 };
346 let mut wdiag = Box::new(DiagMatrix::new(space_x.dim()));
347 wdiag.set_diag(wdiag_diag);
348 self.factor.wdiag = Some(wdiag);
349
350 if coeffs.w_factor == 1.0 && lr_w.get_v().is_some() {
352 let v = Rc::clone(lr_w.get_v().unwrap());
353 let n_v = v.n_cols();
354
355 let v_x_space = MultiVectorMatrixSpace::new(n_v, Rc::clone(&space_x));
359 let mut v_x = v_x_space.make_new_multi_vector();
360 for k in 0..n_v {
361 let vk = Rc::clone(v.get_vector(k));
362 let rhs_x_k: Rc<dyn Vector> = match lr_w.p_lowrank() {
363 Some(p_lm) => {
364 let mut fullx = space_x.make_new_dense();
365 p_lm.mult_vector(1.0, vk.as_ref(), 0.0, &mut fullx);
366 Rc::new(fullx) as Rc<dyn Vector>
367 }
368 None => vk,
369 };
370 v_x.set_vector(k, rhs_x_k);
371 }
372
373 let (vt_x, vt_s, vt_c, vt_d) = self.multi_solve_block(
374 &v_x,
375 coeffs,
376 &space_x,
377 &space_s,
378 &space_c,
379 &space_d,
380 check_neg_evals,
381 num_neg_evals,
382 );
383
384 let vt_x = match vt_x {
385 Ok(x) => x,
386 Err(status) => return status,
387 };
388
389 let m1_space = DenseSymMatrixSpace::new(n_v);
391 let mut m1 = m1_space.make_new_dense_sym();
392 m1.fill_identity(1.0);
393 m1.high_rank_update_transpose(1.0, &vt_x, &v_x, 1.0);
394 let j1_space = DenseGenMatrixSpace::new(n_v, n_v);
395 let mut j1 = j1_space.make_new_dense_gen();
396 if !j1.compute_cholesky_factor(&m1) {
397 self.num_neg_evals += 1;
398 return ESymSolverStatus::WrongInertia;
399 }
400 self.factor.vtilde1_x = Some(vt_x);
401 self.factor.vtilde1_s = Some(vt_s);
402 self.factor.vtilde1_c = Some(vt_c);
403 self.factor.vtilde1_d = Some(vt_d);
404 self.factor.j1 = Some(j1);
405 } else {
406 self.factor.vtilde1_x = None;
407 self.factor.vtilde1_s = None;
408 self.factor.vtilde1_c = None;
409 self.factor.vtilde1_d = None;
410 self.factor.j1 = None;
411 }
412
413 if coeffs.w_factor == 1.0 && lr_w.get_u().is_some() {
416 let u = Rc::clone(lr_w.get_u().unwrap());
417 let n_u = u.n_cols();
418
419 let u_x_space = MultiVectorMatrixSpace::new(n_u, Rc::clone(&space_x));
420 let mut u_x = u_x_space.make_new_multi_vector();
421 for k in 0..n_u {
422 let uk = Rc::clone(u.get_vector(k));
423 let rhs_x_k: Rc<dyn Vector> = match lr_w.p_lowrank() {
424 Some(p_lm) => {
425 let mut fullx = space_x.make_new_dense();
426 p_lm.mult_vector(1.0, uk.as_ref(), 0.0, &mut fullx);
427 Rc::new(fullx) as Rc<dyn Vector>
428 }
429 None => uk,
430 };
431 u_x.set_vector(k, rhs_x_k);
432 }
433
434 let (mut ut_x, mut ut_s, mut ut_c, mut ut_d) = match self.multi_solve_block(
435 &u_x,
436 coeffs,
437 &space_x,
438 &space_s,
439 &space_c,
440 &space_d,
441 check_neg_evals,
442 num_neg_evals,
443 ) {
444 (Ok(x), s, c, d) => (x, s, c, d),
445 (Err(status), _, _, _) => return status,
446 };
447
448 if self.factor.vtilde1_x.is_some() {
450 let vt1_x = self.factor.vtilde1_x.as_ref().unwrap();
451 let vt1_s = self.factor.vtilde1_s.as_ref().unwrap();
452 let vt1_c = self.factor.vtilde1_c.as_ref().unwrap();
453 let vt1_d = self.factor.vtilde1_d.as_ref().unwrap();
454 let n_v = vt1_x.n_cols();
455 let c_space = DenseGenMatrixSpace::new(n_v, n_u);
459 let mut c_mat = c_space.make_new_dense_gen();
460 {
461 let cv = c_mat.values_mut();
462 for j in 0..n_u as usize {
463 let uj = u_x.get_vector(j as Index).as_ref();
464 for i in 0..n_v as usize {
465 let vi = vt1_x.get_vector(i as Index).as_ref();
466 cv[i + j * n_v as usize] = vi.dot(uj);
467 }
468 }
469 }
470 self.factor
471 .j1
472 .as_ref()
473 .unwrap()
474 .cholesky_solve_matrix(&mut c_mat);
475 ut_x.add_right_mult_matrix(-1.0, vt1_x, &c_mat, 1.0);
476 ut_s.add_right_mult_matrix(-1.0, vt1_s, &c_mat, 1.0);
477 ut_c.add_right_mult_matrix(-1.0, vt1_c, &c_mat, 1.0);
478 ut_d.add_right_mult_matrix(-1.0, vt1_d, &c_mat, 1.0);
479 }
480
481 let m2_space = DenseSymMatrixSpace::new(n_u);
486 let mut m2 = m2_space.make_new_dense_sym();
487 m2.fill_identity(1.0);
488 m2.high_rank_update_transpose(-1.0, &ut_x, &u_x, 1.0);
489 let j2_space = DenseGenMatrixSpace::new(n_u, n_u);
490 let mut j2 = j2_space.make_new_dense_gen();
491 if !j2.compute_cholesky_factor(&m2) {
492 self.num_neg_evals += 1;
493 return ESymSolverStatus::WrongInertia;
494 }
495 self.factor.utilde2_x = Some(ut_x);
496 self.factor.utilde2_s = Some(ut_s);
497 self.factor.utilde2_c = Some(ut_c);
498 self.factor.utilde2_d = Some(ut_d);
499 self.factor.j2 = Some(j2);
500 } else {
501 self.factor.utilde2_x = None;
502 self.factor.utilde2_s = None;
503 self.factor.utilde2_c = None;
504 self.factor.utilde2_d = None;
505 self.factor.j2 = None;
506 }
507
508 ESymSolverStatus::Success
509 }
510
511 #[allow(clippy::too_many_arguments)]
516 fn multi_solve_block(
517 &mut self,
518 v_x: &MultiVectorMatrix,
519 coeffs: &AugSysCoeffs<'_>,
520 space_x: &Rc<DenseVectorSpace>,
521 space_s: &Rc<DenseVectorSpace>,
522 space_c: &Rc<DenseVectorSpace>,
523 space_d: &Rc<DenseVectorSpace>,
524 check_neg_evals: bool,
525 num_neg_evals: Index,
526 ) -> (
527 Result<MultiVectorMatrix, ESymSolverStatus>,
528 MultiVectorMatrix,
529 MultiVectorMatrix,
530 MultiVectorMatrix,
531 ) {
532 let n_cols = v_x.n_cols();
533 let n_cols_us = n_cols as usize;
534
535 let mut out_x =
537 MultiVectorMatrixSpace::new(n_cols, Rc::clone(space_x)).make_new_multi_vector();
538 let mut out_s =
539 MultiVectorMatrixSpace::new(n_cols, Rc::clone(space_s)).make_new_multi_vector();
540 let mut out_c =
541 MultiVectorMatrixSpace::new(n_cols, Rc::clone(space_c)).make_new_multi_vector();
542 let mut out_d =
543 MultiVectorMatrixSpace::new(n_cols, Rc::clone(space_d)).make_new_multi_vector();
544 out_x.fill_with_new_vectors();
545 out_s.fill_with_new_vectors();
546 out_c.fill_with_new_vectors();
547 out_d.fill_with_new_vectors();
548
549 let mut rhs_s = space_s.make_new_dense();
552 rhs_s.set(0.0);
553 let mut rhs_c = space_c.make_new_dense();
554 rhs_c.set(0.0);
555 let mut rhs_d = space_d.make_new_dense();
556 rhs_d.set(0.0);
557
558 let n_x = space_x.dim() as usize;
584 let n_s = space_s.dim() as usize;
585 let n_c = space_c.dim() as usize;
586 let n_d = space_d.dim() as usize;
587 let dim = n_x + n_s + n_c + n_d;
588
589 if !self.inner_has_factor
609 && n_cols_us > 1
610 && dim > 0
611 && self.inner.multi_solve_matches_single_solve(n_cols_us)
612 {
613 let mut packed = vec![0.0; dim * n_cols_us];
614 let mut packed_ok = true;
615 for k in 0..n_cols_us {
616 let col = &mut packed[k * dim..k * dim + n_x];
617 if !copy_dense_into(v_x.get_vector(k as Index).as_ref(), col) {
618 packed_ok = false;
619 break;
620 }
621 }
624 if packed_ok {
625 let ic = inner_coeffs(&self.factor, coeffs);
626 if let Some(status) = self.inner.try_solve_many_flat(
627 &ic,
628 &mut packed,
629 n_cols_us,
630 check_neg_evals,
631 num_neg_evals,
632 ) {
633 if self.inner.provides_inertia() {
634 self.num_neg_evals = self.inner.number_of_neg_evals();
635 }
636 if status != ESymSolverStatus::Success {
637 self.inner_has_factor = false;
638 self.inner_factor_neg_evals = None;
639 return (Err(status), out_s, out_c, out_d);
640 }
641 self.inner_has_factor = true;
642 self.inner_factor_neg_evals = check_neg_evals.then_some(num_neg_evals);
643 for k in 0..n_cols_us {
644 let col = &packed[k * dim..(k + 1) * dim];
645 let mut sol_x = space_x.make_new_dense();
646 let mut sol_s = space_s.make_new_dense();
647 let mut sol_c = space_c.make_new_dense();
648 let mut sol_d = space_d.make_new_dense();
649 sol_x.set_values(&col[..n_x]);
650 sol_s.set_values(&col[n_x..n_x + n_s]);
651 sol_c.set_values(&col[n_x + n_s..n_x + n_s + n_c]);
652 sol_d.set_values(&col[n_x + n_s + n_c..]);
653 out_x.set_vector(k as Index, Rc::new(sol_x) as Rc<dyn Vector>);
654 out_s.set_vector(k as Index, Rc::new(sol_s) as Rc<dyn Vector>);
655 out_c.set_vector(k as Index, Rc::new(sol_c) as Rc<dyn Vector>);
656 out_d.set_vector(k as Index, Rc::new(sol_d) as Rc<dyn Vector>);
657 }
658 return (Ok(out_x), out_s, out_c, out_d);
659 }
660 }
661 }
662
663 let mut k0 = 0usize;
664 if !self.inner_has_factor && n_cols_us > 0 {
665 let rhs_x_dyn: &dyn Vector = v_x.get_vector(0).as_ref();
666 match self.solve_one_column(
667 rhs_x_dyn,
668 &rhs_s,
669 &rhs_c,
670 &rhs_d,
671 coeffs,
672 space_x,
673 space_s,
674 space_c,
675 space_d,
676 check_neg_evals,
677 num_neg_evals,
678 ) {
679 Ok((sol_x, sol_s, sol_c, sol_d)) => {
680 out_x.set_vector(0, Rc::new(sol_x) as Rc<dyn Vector>);
681 out_s.set_vector(0, Rc::new(sol_s) as Rc<dyn Vector>);
682 out_c.set_vector(0, Rc::new(sol_c) as Rc<dyn Vector>);
683 out_d.set_vector(0, Rc::new(sol_d) as Rc<dyn Vector>);
684 }
685 Err(status) => return (Err(status), out_s, out_c, out_d),
686 }
687 k0 = 1;
688 }
689
690 if k0 < n_cols_us {
696 let nrhs = n_cols_us - k0;
697 if nrhs > 1
707 && dim > 0
708 && dim == self.inner.system_dim() as usize
709 && self.inner.multi_solve_matches_single_solve(nrhs)
710 {
711 let mut packed = vec![0.0; dim * nrhs];
712 let mut packed_ok = true;
713 for (j, k) in (k0..n_cols_us).enumerate() {
714 let col = &mut packed[j * dim..j * dim + n_x];
715 if !copy_dense_into(v_x.get_vector(k as Index).as_ref(), col) {
716 packed_ok = false;
717 break;
718 }
719 }
722 if packed_ok {
723 let ic = inner_coeffs(&self.factor, coeffs);
724 if let Some(status) = self.inner.try_resolve_many_flat(&ic, &mut packed, nrhs) {
725 if self.inner.provides_inertia() {
726 self.num_neg_evals = self.inner.number_of_neg_evals();
727 }
728 if status != ESymSolverStatus::Success {
729 self.inner_has_factor = false;
730 self.inner_factor_neg_evals = None;
731 return (Err(status), out_s, out_c, out_d);
732 }
733 for (j, k) in (k0..n_cols_us).enumerate() {
734 let col = &packed[j * dim..(j + 1) * dim];
735 let mut sol_x = space_x.make_new_dense();
736 let mut sol_s = space_s.make_new_dense();
737 let mut sol_c = space_c.make_new_dense();
738 let mut sol_d = space_d.make_new_dense();
739 sol_x.set_values(&col[..n_x]);
740 sol_s.set_values(&col[n_x..n_x + n_s]);
741 sol_c.set_values(&col[n_x + n_s..n_x + n_s + n_c]);
742 sol_d.set_values(&col[n_x + n_s + n_c..]);
743 out_x.set_vector(k as Index, Rc::new(sol_x) as Rc<dyn Vector>);
744 out_s.set_vector(k as Index, Rc::new(sol_s) as Rc<dyn Vector>);
745 out_c.set_vector(k as Index, Rc::new(sol_c) as Rc<dyn Vector>);
746 out_d.set_vector(k as Index, Rc::new(sol_d) as Rc<dyn Vector>);
747 }
748 return (Ok(out_x), out_s, out_c, out_d);
749 }
750 }
751 }
752 }
753
754 for k in k0..n_cols_us {
756 let rhs_x_dyn: &dyn Vector = v_x.get_vector(k as Index).as_ref();
757 match self.solve_one_column(
758 rhs_x_dyn,
759 &rhs_s,
760 &rhs_c,
761 &rhs_d,
762 coeffs,
763 space_x,
764 space_s,
765 space_c,
766 space_d,
767 check_neg_evals,
768 num_neg_evals,
769 ) {
770 Ok((sol_x, sol_s, sol_c, sol_d)) => {
771 out_x.set_vector(k as Index, Rc::new(sol_x) as Rc<dyn Vector>);
772 out_s.set_vector(k as Index, Rc::new(sol_s) as Rc<dyn Vector>);
773 out_c.set_vector(k as Index, Rc::new(sol_c) as Rc<dyn Vector>);
774 out_d.set_vector(k as Index, Rc::new(sol_d) as Rc<dyn Vector>);
775 }
776 Err(status) => return (Err(status), out_s, out_c, out_d),
777 }
778 }
779 (Ok(out_x), out_s, out_c, out_d)
780 }
781
782 #[allow(clippy::too_many_arguments)]
788 fn solve_one_column(
789 &mut self,
790 rhs_x: &dyn Vector,
791 rhs_s: &DenseVector,
792 rhs_c: &DenseVector,
793 rhs_d: &DenseVector,
794 coeffs: &AugSysCoeffs<'_>,
795 space_x: &Rc<DenseVectorSpace>,
796 space_s: &Rc<DenseVectorSpace>,
797 space_c: &Rc<DenseVectorSpace>,
798 space_d: &Rc<DenseVectorSpace>,
799 check_neg_evals: bool,
800 num_neg_evals: Index,
801 ) -> Result<(DenseVector, DenseVector, DenseVector, DenseVector), ESymSolverStatus> {
802 let inner_rhs = AugSysRhs {
803 rhs_x,
804 rhs_s: rhs_s.as_dyn_vector(),
805 rhs_c: rhs_c.as_dyn_vector(),
806 rhs_d: rhs_d.as_dyn_vector(),
807 };
808 let mut sol_x = space_x.make_new_dense();
810 let mut sol_s = space_s.make_new_dense();
811 let mut sol_c = space_c.make_new_dense();
812 let mut sol_d = space_d.make_new_dense();
813 sol_x.set(0.0);
814 sol_s.set(0.0);
815 sol_c.set(0.0);
816 sol_d.set(0.0);
817 let ic = inner_coeffs(&self.factor, coeffs);
818 let reuse = self.inner_has_factor;
819 let status = {
820 let mut sol = AugSysSol {
821 sol_x: &mut sol_x,
822 sol_s: &mut sol_s,
823 sol_c: &mut sol_c,
824 sol_d: &mut sol_d,
825 };
826 if reuse {
827 self.inner.resolve(&ic, &inner_rhs, &mut sol)
828 } else {
829 self.inner
830 .solve(&ic, &inner_rhs, &mut sol, check_neg_evals, num_neg_evals)
831 }
832 };
833 if self.inner.provides_inertia() {
834 self.num_neg_evals = self.inner.number_of_neg_evals();
835 }
836 if status != ESymSolverStatus::Success {
837 self.inner_has_factor = false;
838 self.inner_factor_neg_evals = None;
839 return Err(status);
840 }
841 if !reuse {
842 self.inner_has_factor = true;
843 self.inner_factor_neg_evals = check_neg_evals.then_some(num_neg_evals);
844 }
845 Ok((sol_x, sol_s, sol_c, sol_d))
846 }
847}
848
849fn copy_dense_into(src: &dyn Vector, dst: &mut [Number]) -> bool {
857 if dst.is_empty() {
858 return true;
859 }
860 let Some(dv) = src.as_any().downcast_ref::<DenseVector>() else {
861 return false;
862 };
863 if dv.dim() as usize != dst.len() {
864 return false;
865 }
866 if dv.is_homogeneous() {
867 let v = dv.scalar();
868 dst.iter_mut().for_each(|x| *x = v);
869 } else {
870 dst.copy_from_slice(dv.values());
871 }
872 true
873}
874
875fn inner_coeffs<'b>(factor: &'b Factorization, coeffs: &AugSysCoeffs<'b>) -> AugSysCoeffs<'b> {
880 let wdiag: &DiagMatrix = factor.wdiag.as_ref().expect("Wdiag unset").as_ref();
881 AugSysCoeffs {
882 w: Some(wdiag as &dyn SymMatrix),
883 w_factor: 1.0,
884 d_x: coeffs.d_x,
885 delta_x: coeffs.delta_x,
886 d_s: coeffs.d_s,
887 delta_s: coeffs.delta_s,
888 j_c: coeffs.j_c,
889 d_c: coeffs.d_c,
890 delta_c: coeffs.delta_c,
891 j_d: coeffs.j_d,
892 d_d: coeffs.d_d,
893 delta_d: coeffs.delta_d,
894 }
895}
896
897fn downcast_dense(v: &dyn Vector) -> &DenseVector {
898 v.as_any()
899 .downcast_ref::<DenseVector>()
900 .expect("LowRankAugSystemSolver currently requires DenseVector RHS/solutions")
901}
902
903fn clone_dense(src: &DenseVector) -> DenseVector {
907 let mut out = src.space().make_new_dense();
908 out.set_values(&src.expanded_values());
909 out
910}
911
912fn zero_x_for(space_x: &Rc<DenseVectorSpace>, lr_w: &LowRankUpdateSymMatrix) -> DenseVector {
913 let _ = lr_w;
918 let mut z = space_x.make_new_dense();
919 z.set(0.0);
920 z
921}
922
923impl AugSystemSolver for LowRankAugSystemSolver {
924 fn provides_inertia(&self) -> bool {
925 self.inner.provides_inertia()
926 }
927
928 fn singularity_certified(&self) -> bool {
929 self.inner.singularity_certified()
930 }
931
932 fn number_of_neg_evals(&self) -> Index {
933 if self.inner.provides_inertia() {
934 self.inner.number_of_neg_evals()
935 } else {
936 self.num_neg_evals
937 }
938 }
939
940 fn increase_quality(&mut self) -> bool {
941 self.inner_has_factor = false;
944 self.inner_factor_neg_evals = None;
945 let inner = self.inner.increase_quality();
949 let bypass = self
950 .bypass
951 .as_deref_mut()
952 .map(|b| b.increase_quality())
953 .unwrap_or(false);
954 inner || bypass
955 }
956
957 fn last_solve_status(&self) -> ESymSolverStatus {
958 match (self.last_solve_took_bypass, self.bypass.as_deref()) {
961 (true, Some(bypass)) => bypass.last_solve_status(),
962 _ => self.inner.last_solve_status(),
963 }
964 }
965
966 fn set_timing_stats(&mut self, timing: Rc<TimingStatistics>) {
967 if let Some(bypass) = self.bypass.as_deref_mut() {
971 bypass.set_timing_stats(Rc::clone(&timing));
972 }
973 self.inner.set_timing_stats(timing);
974 }
975
976 fn set_slack_scaling(&mut self, nx: Index, s_scale: &[Number]) {
977 if let Some(bypass) = self.bypass.as_deref_mut() {
980 bypass.set_slack_scaling(nx, s_scale);
981 }
982 self.inner.set_slack_scaling(nx, s_scale);
983 }
984
985 fn handles_low_rank_w(&self) -> bool {
986 true
987 }
988
989 fn solve(
990 &mut self,
991 coeffs: &AugSysCoeffs<'_>,
992 rhs: &AugSysRhs<'_>,
993 sol: &mut AugSysSol<'_>,
994 check_neg_evals: bool,
995 num_neg_evals: Index,
996 ) -> ESymSolverStatus {
997 let mut check_neg_evals = check_neg_evals;
1000 if !self.inner.provides_inertia() {
1001 check_neg_evals = false;
1002 }
1003
1004 let lr_w_opt = coeffs
1013 .w
1014 .and_then(|w| w.as_any().downcast_ref::<LowRankUpdateSymMatrix>());
1015 let Some(lr_w) = lr_w_opt else {
1016 self.last_solve_took_bypass = true;
1025 let target = match self.bypass.as_deref_mut() {
1026 Some(bypass) => bypass,
1027 None => {
1028 self.inner_has_factor = false;
1033 self.inner_factor_neg_evals = None;
1034 self.inner.as_mut()
1035 }
1036 };
1037 let status = target.solve(coeffs, rhs, sol, check_neg_evals, num_neg_evals);
1038 if target.provides_inertia() {
1039 self.num_neg_evals = target.number_of_neg_evals();
1040 }
1041 return status;
1042 };
1043
1044 self.last_solve_took_bypass = false;
1045
1046 let needs_rebuild = self.first_call || self.augmented_system_requires_change(coeffs);
1047 if needs_rebuild {
1048 let status =
1049 self.update_factorization(lr_w, coeffs, rhs, check_neg_evals, num_neg_evals);
1050 if status != ESymSolverStatus::Success {
1051 return status;
1052 }
1053 self.store_cache(coeffs);
1054 self.first_call = false;
1055 }
1056
1057 let reuse = self.inner_has_factor
1070 && (!check_neg_evals || self.inner_factor_neg_evals == Some(num_neg_evals));
1071 let ic = inner_coeffs(&self.factor, coeffs);
1072 let status = if reuse {
1073 self.inner.resolve(&ic, rhs, sol)
1074 } else {
1075 self.inner
1076 .solve(&ic, rhs, sol, check_neg_evals, num_neg_evals)
1077 };
1078 if self.inner.provides_inertia() {
1079 self.num_neg_evals = self.inner.number_of_neg_evals();
1080 }
1081 if status != ESymSolverStatus::Success {
1082 self.inner_has_factor = false;
1083 self.inner_factor_neg_evals = None;
1084 return status;
1085 }
1086 if !reuse {
1087 self.inner_has_factor = true;
1088 self.inner_factor_neg_evals = check_neg_evals.then_some(num_neg_evals);
1089 }
1090
1091 if self.factor.utilde2_x.is_some() {
1094 self.apply_smw(1.0, true, rhs, sol);
1095 }
1096 if self.factor.vtilde1_x.is_some() {
1097 self.apply_smw(-1.0, false, rhs, sol);
1098 }
1099
1100 ESymSolverStatus::Success
1101 }
1102
1103 fn resolve(
1120 &mut self,
1121 coeffs: &AugSysCoeffs<'_>,
1122 rhs: &AugSysRhs<'_>,
1123 sol: &mut AugSysSol<'_>,
1124 ) -> ESymSolverStatus {
1125 let is_low_rank = coeffs
1135 .w
1136 .and_then(|w| w.as_any().downcast_ref::<LowRankUpdateSymMatrix>())
1137 .is_some();
1138 if !is_low_rank
1139 || self.first_call
1140 || !self.inner_has_factor
1141 || self.augmented_system_requires_change(coeffs)
1142 {
1143 return self.solve(coeffs, rhs, sol, false, 0);
1144 }
1145
1146 let ic = inner_coeffs(&self.factor, coeffs);
1147 let status = self.inner.resolve(&ic, rhs, sol);
1148 if status != ESymSolverStatus::Success {
1149 self.inner_has_factor = false;
1150 self.inner_factor_neg_evals = None;
1151 return status;
1152 }
1153
1154 if self.factor.utilde2_x.is_some() {
1156 self.apply_smw(1.0, true, rhs, sol);
1157 }
1158 if self.factor.vtilde1_x.is_some() {
1159 self.apply_smw(-1.0, false, rhs, sol);
1160 }
1161
1162 ESymSolverStatus::Success
1163 }
1164
1165 }
1179
1180impl LowRankAugSystemSolver {
1181 fn apply_smw(&self, sign: Number, use_u: bool, rhs: &AugSysRhs<'_>, sol: &mut AugSysSol<'_>) {
1187 let (mvx, mvs, mvc, mvd, j) = if use_u {
1188 (
1189 self.factor.utilde2_x.as_ref().unwrap(),
1190 self.factor.utilde2_s.as_ref().unwrap(),
1191 self.factor.utilde2_c.as_ref().unwrap(),
1192 self.factor.utilde2_d.as_ref().unwrap(),
1193 self.factor.j2.as_ref().unwrap(),
1194 )
1195 } else {
1196 (
1197 self.factor.vtilde1_x.as_ref().unwrap(),
1198 self.factor.vtilde1_s.as_ref().unwrap(),
1199 self.factor.vtilde1_c.as_ref().unwrap(),
1200 self.factor.vtilde1_d.as_ref().unwrap(),
1201 self.factor.j1.as_ref().unwrap(),
1202 )
1203 };
1204 let n = mvx.n_cols();
1205 let mut b_vec: Vec<Number> = Vec::with_capacity(n as usize);
1209 for k in 0..n {
1210 let dot = mvx.get_vector(k).dot(rhs.rhs_x)
1211 + mvs.get_vector(k).dot(rhs.rhs_s)
1212 + mvc.get_vector(k).dot(rhs.rhs_c)
1213 + mvd.get_vector(k).dot(rhs.rhs_d);
1214 b_vec.push(dot);
1215 }
1216 let space_b = DenseVectorSpace::new(n);
1217 let mut b = space_b.make_new_dense();
1218 b.set_values(&b_vec);
1219 j.cholesky_solve_vector(&mut b);
1221 mvx.mult_vector(sign, &b, 1.0, sol.sol_x);
1223 mvs.mult_vector(sign, &b, 1.0, sol.sol_s);
1224 mvc.mult_vector(sign, &b, 1.0, sol.sol_c);
1225 mvd.mult_vector(sign, &b, 1.0, sol.sol_d);
1226 }
1227}
1228
1229#[cfg(test)]
1230mod tests {
1231 use super::*;
1232 use pounce_linalg::dense_vector::DenseVectorSpace;
1233 use pounce_linalg::low_rank_update_sym_matrix::LowRankUpdateSymMatrixSpace;
1234 use std::cell::{Cell, RefCell};
1235
1236 struct DiagInner {
1241 calls: Cell<usize>,
1242 }
1243 impl AugSystemSolver for DiagInner {
1244 fn provides_inertia(&self) -> bool {
1245 true
1246 }
1247 fn number_of_neg_evals(&self) -> Index {
1248 0
1249 }
1250 fn increase_quality(&mut self) -> bool {
1251 true
1252 }
1253 fn last_solve_status(&self) -> ESymSolverStatus {
1254 ESymSolverStatus::Success
1255 }
1256 fn solve(
1257 &mut self,
1258 coeffs: &AugSysCoeffs<'_>,
1259 rhs: &AugSysRhs<'_>,
1260 sol: &mut AugSysSol<'_>,
1261 _check_neg_evals: bool,
1262 _num_neg_evals: Index,
1263 ) -> ESymSolverStatus {
1264 self.calls.set(self.calls.get() + 1);
1265 let wdiag = coeffs
1266 .w
1267 .expect("DiagInner requires W")
1268 .as_any()
1269 .downcast_ref::<DiagMatrix>()
1270 .expect("DiagInner requires W to be a DiagMatrix");
1271 let diag_rc = wdiag.get_diag().expect("Wdiag has no diag set").clone();
1272 let diag = downcast_dense(diag_rc.as_ref()).expanded_values();
1273 let rhs_x = downcast_dense(rhs.rhs_x).expanded_values();
1274 let dx_vals: Option<Vec<Number>> =
1275 coeffs.d_x.map(|d| downcast_dense(d).expanded_values());
1276 let mut out = vec![0.0; rhs_x.len()];
1277 for i in 0..rhs_x.len() {
1278 let dx_i = match &dx_vals {
1279 Some(v) => v[i],
1280 None => 0.0,
1281 };
1282 let denom = diag[i] + dx_i + coeffs.delta_x;
1283 out[i] = rhs_x[i] / denom;
1284 }
1285 let sol_x_dv = sol
1286 .sol_x
1287 .as_any_mut()
1288 .downcast_mut::<DenseVector>()
1289 .unwrap();
1290 sol_x_dv.set_values(&out);
1291 ESymSolverStatus::Success
1293 }
1294 }
1295
1296 #[derive(Default)]
1307 struct InnerStats {
1308 factorizations: Cell<usize>,
1309 backsolves: Cell<usize>,
1310 batched_calls: Cell<usize>,
1311 batched_cols: Cell<usize>,
1312 factor_batched_calls: Cell<usize>,
1316 factor_batched_cols: Cell<usize>,
1317 factored_diag: RefCell<Vec<Number>>,
1318 factored_delta_x: Cell<Number>,
1319 }
1320
1321 struct CountingInner {
1322 stats: Rc<InnerStats>,
1323 packed: bool,
1329 }
1330
1331 impl CountingInner {
1332 fn new() -> (Self, Rc<InnerStats>) {
1335 let stats = Rc::new(InnerStats::default());
1336 (
1337 Self {
1338 stats: Rc::clone(&stats),
1339 packed: false,
1340 },
1341 stats,
1342 )
1343 }
1344
1345 fn with_packed_path() -> (Self, Rc<InnerStats>) {
1347 let stats = Rc::new(InnerStats::default());
1348 (
1349 Self {
1350 stats: Rc::clone(&stats),
1351 packed: true,
1352 },
1353 stats,
1354 )
1355 }
1356
1357 fn apply_cached(&self, rhs: &AugSysRhs<'_>, sol: &mut AugSysSol<'_>) {
1359 let rhs_x = downcast_dense(rhs.rhs_x).expanded_values();
1360 let diag = self.stats.factored_diag.borrow();
1361 let delta_x = self.stats.factored_delta_x.get();
1362 let out: Vec<Number> = (0..rhs_x.len())
1363 .map(|i| rhs_x[i] / (diag[i] + delta_x))
1364 .collect();
1365 sol.sol_x
1366 .as_any_mut()
1367 .downcast_mut::<DenseVector>()
1368 .unwrap()
1369 .set_values(&out);
1370 }
1371 }
1372
1373 impl AugSystemSolver for CountingInner {
1374 fn provides_inertia(&self) -> bool {
1375 false
1376 }
1377 fn number_of_neg_evals(&self) -> Index {
1378 0
1379 }
1380 fn increase_quality(&mut self) -> bool {
1381 false
1382 }
1383 fn last_solve_status(&self) -> ESymSolverStatus {
1384 ESymSolverStatus::Success
1385 }
1386 fn solve(
1387 &mut self,
1388 coeffs: &AugSysCoeffs<'_>,
1389 rhs: &AugSysRhs<'_>,
1390 sol: &mut AugSysSol<'_>,
1391 _check_neg_evals: bool,
1392 _num_neg_evals: Index,
1393 ) -> ESymSolverStatus {
1394 self.stats
1395 .factorizations
1396 .set(self.stats.factorizations.get() + 1);
1397 let wdiag = coeffs
1398 .w
1399 .expect("CountingInner requires W")
1400 .as_any()
1401 .downcast_ref::<DiagMatrix>()
1402 .expect("CountingInner requires W to be a DiagMatrix");
1403 let diag_rc = wdiag.get_diag().expect("Wdiag has no diag set").clone();
1404 *self.stats.factored_diag.borrow_mut() =
1405 downcast_dense(diag_rc.as_ref()).expanded_values();
1406 self.stats.factored_delta_x.set(coeffs.delta_x);
1407 self.apply_cached(rhs, sol);
1408 ESymSolverStatus::Success
1409 }
1410 fn resolve(
1411 &mut self,
1412 _coeffs: &AugSysCoeffs<'_>,
1413 rhs: &AugSysRhs<'_>,
1414 sol: &mut AugSysSol<'_>,
1415 ) -> ESymSolverStatus {
1416 assert!(
1417 !self.stats.factored_diag.borrow().is_empty(),
1418 "resolve reached with no cached factor"
1419 );
1420 self.stats.backsolves.set(self.stats.backsolves.get() + 1);
1421 self.apply_cached(rhs, sol);
1422 ESymSolverStatus::Success
1423 }
1424 fn system_dim(&self) -> Index {
1425 if self.packed {
1426 self.stats.factored_diag.borrow().len() as Index
1427 } else {
1428 0
1429 }
1430 }
1431 fn multi_solve_matches_single_solve(&self, _nrhs: usize) -> bool {
1437 self.packed
1438 }
1439
1440 fn try_solve_many_flat(
1445 &mut self,
1446 coeffs: &AugSysCoeffs<'_>,
1447 packed_rhs: &mut [Number],
1448 nrhs: usize,
1449 _check_neg_evals: bool,
1450 _num_neg_evals: Index,
1451 ) -> Option<ESymSolverStatus> {
1452 if !self.packed {
1453 return None;
1454 }
1455 let wdiag = coeffs
1456 .w
1457 .expect("CountingInner requires W")
1458 .as_any()
1459 .downcast_ref::<DiagMatrix>()
1460 .expect("CountingInner requires W to be a DiagMatrix");
1461 let diag_rc = wdiag.get_diag().expect("Wdiag has no diag set").clone();
1462 let diag = downcast_dense(diag_rc.as_ref()).expanded_values();
1463 let dim = diag.len();
1464 if packed_rhs.len() != dim * nrhs {
1465 return None;
1466 }
1467 *self.stats.factored_diag.borrow_mut() = diag.clone();
1468 self.stats.factored_delta_x.set(coeffs.delta_x);
1469 self.stats
1470 .factorizations
1471 .set(self.stats.factorizations.get() + 1);
1472 self.stats
1473 .factor_batched_calls
1474 .set(self.stats.factor_batched_calls.get() + 1);
1475 self.stats
1476 .factor_batched_cols
1477 .set(self.stats.factor_batched_cols.get() + nrhs);
1478 let delta_x = coeffs.delta_x;
1479 for col in packed_rhs.chunks_mut(dim) {
1480 for (i, x) in col.iter_mut().enumerate() {
1481 *x /= diag[i] + delta_x;
1482 }
1483 }
1484 Some(ESymSolverStatus::Success)
1485 }
1486
1487 fn try_resolve_many_flat(
1488 &mut self,
1489 _coeffs: &AugSysCoeffs<'_>,
1490 packed_rhs: &mut [Number],
1491 nrhs: usize,
1492 ) -> Option<ESymSolverStatus> {
1493 if !self.packed {
1494 return None;
1495 }
1496 let diag = self.stats.factored_diag.borrow();
1497 if diag.is_empty() {
1498 return None;
1499 }
1500 let dim = diag.len();
1501 if packed_rhs.len() != dim * nrhs {
1502 return Some(ESymSolverStatus::FatalError);
1503 }
1504 self.stats
1505 .batched_calls
1506 .set(self.stats.batched_calls.get() + 1);
1507 self.stats
1508 .batched_cols
1509 .set(self.stats.batched_cols.get() + nrhs);
1510 let delta_x = self.stats.factored_delta_x.get();
1511 for col in packed_rhs.chunks_mut(dim) {
1512 for (i, x) in col.iter_mut().enumerate() {
1513 *x /= diag[i] + delta_x;
1514 }
1515 }
1516 Some(ESymSolverStatus::Success)
1517 }
1518 }
1519
1520 fn dvec(space: &Rc<DenseVectorSpace>, vals: &[Number]) -> DenseVector {
1521 let mut v = space.make_new_dense();
1522 v.set_values(vals);
1523 v
1524 }
1525
1526 fn dvec_rc(space: &Rc<DenseVectorSpace>, vals: &[Number]) -> Rc<DenseVector> {
1527 Rc::new(dvec(space, vals))
1528 }
1529
1530 #[test]
1531 fn smw_recovers_low_rank_inverse() {
1532 let space_x = DenseVectorSpace::new(1);
1537 let space_zero = DenseVectorSpace::new(0);
1538 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
1539 let mut lr = lr_space.make_new_low_rank();
1540 let b0_rc: Rc<dyn Vector> = dvec_rc(&space_x, &[2.0]);
1541 lr.set_diag(b0_rc);
1542 let v_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
1543 let mut v_mvm = v_space.make_new_multi_vector();
1544 v_mvm.set_vector(0, dvec_rc(&space_x, &[3.0]) as Rc<dyn Vector>);
1545 lr.set_v(Rc::new(v_mvm));
1546 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1547
1548 let mut solver = LowRankAugSystemSolver::new(Box::new(DiagInner {
1549 calls: Cell::new(0),
1550 }));
1551
1552 let j_c_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1554 let j_d_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1555 let j_c = j_c_space.make_new_dense_gen();
1556 let j_d = j_d_space.make_new_dense_gen();
1557
1558 let coeffs = AugSysCoeffs {
1559 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1560 w_factor: 1.0,
1561 d_x: None,
1562 delta_x: 0.0,
1563 d_s: None,
1564 delta_s: 0.0,
1565 j_c: &j_c as &dyn Matrix,
1566 d_c: None,
1567 delta_c: 0.0,
1568 j_d: &j_d as &dyn Matrix,
1569 d_d: None,
1570 delta_d: 0.0,
1571 };
1572
1573 let rhs_x = dvec(&space_x, &[5.0]);
1574 let rhs_s = dvec(&space_zero, &[]);
1575 let rhs_c = dvec(&space_zero, &[]);
1576 let rhs_d = dvec(&space_zero, &[]);
1577 let rhs = AugSysRhs {
1578 rhs_x: &rhs_x,
1579 rhs_s: &rhs_s,
1580 rhs_c: &rhs_c,
1581 rhs_d: &rhs_d,
1582 };
1583 let mut sol_x = dvec(&space_x, &[0.0]);
1584 let mut sol_s = dvec(&space_zero, &[]);
1585 let mut sol_c = dvec(&space_zero, &[]);
1586 let mut sol_d = dvec(&space_zero, &[]);
1587 let mut sol = AugSysSol {
1588 sol_x: &mut sol_x,
1589 sol_s: &mut sol_s,
1590 sol_c: &mut sol_c,
1591 sol_d: &mut sol_d,
1592 };
1593 let status = solver.solve(&coeffs, &rhs, &mut sol, false, 0);
1594 assert_eq!(status, ESymSolverStatus::Success);
1595 let got = sol_x.expanded_values()[0];
1597 let want = 5.0 / 11.0;
1598 assert!((got - want).abs() < 1e-12, "got {} want {}", got, want);
1599 }
1600
1601 #[test]
1602 fn smw_with_u_only_applies_positive_correction() {
1603 let space_x = DenseVectorSpace::new(1);
1606 let space_zero = DenseVectorSpace::new(0);
1607 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
1608 let mut lr = lr_space.make_new_low_rank();
1609 lr.set_diag(dvec_rc(&space_x, &[5.0]));
1610 let u_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
1611 let mut u_mvm = u_space.make_new_multi_vector();
1612 u_mvm.set_vector(0, dvec_rc(&space_x, &[1.5]) as Rc<dyn Vector>);
1613 lr.set_u(Rc::new(u_mvm));
1614 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1615
1616 let mut solver = LowRankAugSystemSolver::new(Box::new(DiagInner {
1617 calls: Cell::new(0),
1618 }));
1619
1620 let j_c_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1621 let j_d_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1622 let j_c = j_c_space.make_new_dense_gen();
1623 let j_d = j_d_space.make_new_dense_gen();
1624
1625 let coeffs = AugSysCoeffs {
1626 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1627 w_factor: 1.0,
1628 d_x: None,
1629 delta_x: 0.0,
1630 d_s: None,
1631 delta_s: 0.0,
1632 j_c: &j_c as &dyn Matrix,
1633 d_c: None,
1634 delta_c: 0.0,
1635 j_d: &j_d as &dyn Matrix,
1636 d_d: None,
1637 delta_d: 0.0,
1638 };
1639
1640 let rhs_x = dvec(&space_x, &[7.0]);
1641 let rhs_s = dvec(&space_zero, &[]);
1642 let rhs_c = dvec(&space_zero, &[]);
1643 let rhs_d = dvec(&space_zero, &[]);
1644 let rhs = AugSysRhs {
1645 rhs_x: &rhs_x,
1646 rhs_s: &rhs_s,
1647 rhs_c: &rhs_c,
1648 rhs_d: &rhs_d,
1649 };
1650 let mut sol_x = dvec(&space_x, &[0.0]);
1651 let mut sol_s = dvec(&space_zero, &[]);
1652 let mut sol_c = dvec(&space_zero, &[]);
1653 let mut sol_d = dvec(&space_zero, &[]);
1654 let mut sol = AugSysSol {
1655 sol_x: &mut sol_x,
1656 sol_s: &mut sol_s,
1657 sol_c: &mut sol_c,
1658 sol_d: &mut sol_d,
1659 };
1660 let status = solver.solve(&coeffs, &rhs, &mut sol, false, 0);
1661 assert_eq!(status, ESymSolverStatus::Success);
1662 let got = sol_x.expanded_values()[0];
1664 let want = 7.0 / 2.75;
1665 assert!((got - want).abs() < 1e-12, "got {} want {}", got, want);
1666 }
1667
1668 #[test]
1669 fn smw_reports_wrong_inertia_on_indefinite_negative_update() {
1670 let space_x = DenseVectorSpace::new(1);
1680 let space_zero = DenseVectorSpace::new(0);
1681 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
1682 let mut lr = lr_space.make_new_low_rank();
1683 lr.set_diag(dvec_rc(&space_x, &[2.0]));
1684 let u_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
1685 let mut u_mvm = u_space.make_new_multi_vector();
1686 u_mvm.set_vector(0, dvec_rc(&space_x, &[2.0]) as Rc<dyn Vector>);
1687 lr.set_u(Rc::new(u_mvm));
1688 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1689
1690 let mut solver = LowRankAugSystemSolver::new(Box::new(DiagInner {
1691 calls: Cell::new(0),
1692 }));
1693
1694 let j_c_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1695 let j_d_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1696 let j_c = j_c_space.make_new_dense_gen();
1697 let j_d = j_d_space.make_new_dense_gen();
1698
1699 let coeffs = AugSysCoeffs {
1700 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1701 w_factor: 1.0,
1702 d_x: None,
1703 delta_x: 0.0,
1704 d_s: None,
1705 delta_s: 0.0,
1706 j_c: &j_c as &dyn Matrix,
1707 d_c: None,
1708 delta_c: 0.0,
1709 j_d: &j_d as &dyn Matrix,
1710 d_d: None,
1711 delta_d: 0.0,
1712 };
1713
1714 let rhs_x = dvec(&space_x, &[1.0]);
1715 let rhs_s = dvec(&space_zero, &[]);
1716 let rhs_c = dvec(&space_zero, &[]);
1717 let rhs_d = dvec(&space_zero, &[]);
1718 let rhs = AugSysRhs {
1719 rhs_x: &rhs_x,
1720 rhs_s: &rhs_s,
1721 rhs_c: &rhs_c,
1722 rhs_d: &rhs_d,
1723 };
1724 let mut sol_x = dvec(&space_x, &[0.0]);
1725 let mut sol_s = dvec(&space_zero, &[]);
1726 let mut sol_c = dvec(&space_zero, &[]);
1727 let mut sol_d = dvec(&space_zero, &[]);
1728 let mut sol = AugSysSol {
1729 sol_x: &mut sol_x,
1730 sol_s: &mut sol_s,
1731 sol_c: &mut sol_c,
1732 sol_d: &mut sol_d,
1733 };
1734 let status = solver.solve(&coeffs, &rhs, &mut sol, false, 0);
1735 assert_eq!(status, ESymSolverStatus::WrongInertia);
1736 }
1737
1738 #[test]
1739 fn smw_with_v_and_u_combines_corrections() {
1740 let space_x = DenseVectorSpace::new(1);
1743 let space_zero = DenseVectorSpace::new(0);
1744 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
1745 let mut lr = lr_space.make_new_low_rank();
1746 lr.set_diag(dvec_rc(&space_x, &[10.0]));
1747 let v_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
1748 let mut v_mvm = v_space.make_new_multi_vector();
1749 v_mvm.set_vector(0, dvec_rc(&space_x, &[2.0]) as Rc<dyn Vector>);
1750 lr.set_v(Rc::new(v_mvm));
1751 let u_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
1752 let mut u_mvm = u_space.make_new_multi_vector();
1753 u_mvm.set_vector(0, dvec_rc(&space_x, &[1.0]) as Rc<dyn Vector>);
1754 lr.set_u(Rc::new(u_mvm));
1755 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1756
1757 let mut solver = LowRankAugSystemSolver::new(Box::new(DiagInner {
1758 calls: Cell::new(0),
1759 }));
1760
1761 let j_c_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1762 let j_d_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1763 let j_c = j_c_space.make_new_dense_gen();
1764 let j_d = j_d_space.make_new_dense_gen();
1765
1766 let coeffs = AugSysCoeffs {
1767 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1768 w_factor: 1.0,
1769 d_x: None,
1770 delta_x: 0.0,
1771 d_s: None,
1772 delta_s: 0.0,
1773 j_c: &j_c as &dyn Matrix,
1774 d_c: None,
1775 delta_c: 0.0,
1776 j_d: &j_d as &dyn Matrix,
1777 d_d: None,
1778 delta_d: 0.0,
1779 };
1780
1781 let rhs_x = dvec(&space_x, &[1.0]);
1782 let rhs_s = dvec(&space_zero, &[]);
1783 let rhs_c = dvec(&space_zero, &[]);
1784 let rhs_d = dvec(&space_zero, &[]);
1785 let rhs = AugSysRhs {
1786 rhs_x: &rhs_x,
1787 rhs_s: &rhs_s,
1788 rhs_c: &rhs_c,
1789 rhs_d: &rhs_d,
1790 };
1791 let mut sol_x = dvec(&space_x, &[0.0]);
1792 let mut sol_s = dvec(&space_zero, &[]);
1793 let mut sol_c = dvec(&space_zero, &[]);
1794 let mut sol_d = dvec(&space_zero, &[]);
1795 let mut sol = AugSysSol {
1796 sol_x: &mut sol_x,
1797 sol_s: &mut sol_s,
1798 sol_c: &mut sol_c,
1799 sol_d: &mut sol_d,
1800 };
1801 let status = solver.solve(&coeffs, &rhs, &mut sol, false, 0);
1802 assert_eq!(status, ESymSolverStatus::Success);
1803 let got = sol_x.expanded_values()[0];
1805 let want = 1.0 / 13.0;
1806 assert!((got - want).abs() < 1e-12, "got {} want {}", got, want);
1807 }
1808
1809 #[test]
1810 fn unchanged_coeffs_skip_rebuild_after_first_call() {
1811 let mut lr_solver = LowRankAugSystemSolver::new(Box::new(DiagInner {
1812 calls: Cell::new(0),
1813 }));
1814 let space_x = DenseVectorSpace::new(1);
1815 let space_zero = DenseVectorSpace::new(0);
1816 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
1817 let mut lr = lr_space.make_new_low_rank();
1818 lr.set_diag(dvec_rc(&space_x, &[2.0]));
1819 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1820 let j_c_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1821 let j_d_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1822 let j_c = j_c_space.make_new_dense_gen();
1823 let j_d = j_d_space.make_new_dense_gen();
1824 let coeffs = AugSysCoeffs {
1825 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1826 w_factor: 1.0,
1827 d_x: None,
1828 delta_x: 0.001,
1829 d_s: None,
1830 delta_s: 0.0,
1831 j_c: &j_c as &dyn Matrix,
1832 d_c: None,
1833 delta_c: 0.0,
1834 j_d: &j_d as &dyn Matrix,
1835 d_d: None,
1836 delta_d: 0.0,
1837 };
1838 let rhs_x = dvec(&space_x, &[1.0]);
1839 let rhs_zero = dvec(&space_zero, &[]);
1840 let rhs = AugSysRhs {
1841 rhs_x: &rhs_x,
1842 rhs_s: &rhs_zero,
1843 rhs_c: &rhs_zero,
1844 rhs_d: &rhs_zero,
1845 };
1846 let mut sol_x = dvec(&space_x, &[0.0]);
1847 let mut sol_z1 = dvec(&space_zero, &[]);
1848 let mut sol_z2 = dvec(&space_zero, &[]);
1849 let mut sol_z3 = dvec(&space_zero, &[]);
1850 {
1851 let mut sol = AugSysSol {
1852 sol_x: &mut sol_x,
1853 sol_s: &mut sol_z1,
1854 sol_c: &mut sol_z2,
1855 sol_d: &mut sol_z3,
1856 };
1857 lr_solver.solve(&coeffs, &rhs, &mut sol, false, 0);
1858 }
1859 assert!(!lr_solver.augmented_system_requires_change(&coeffs));
1861 }
1862
1863 fn empty_jacobians(
1868 n_x: Index,
1869 ) -> (
1870 pounce_linalg::dense_gen_matrix::DenseGenMatrix,
1871 pounce_linalg::dense_gen_matrix::DenseGenMatrix,
1872 ) {
1873 (
1874 pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, n_x).make_new_dense_gen(),
1875 pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, n_x).make_new_dense_gen(),
1876 )
1877 }
1878
1879 #[test]
1880 fn smw_columns_share_one_factorization() {
1881 let space_x = DenseVectorSpace::new(3);
1886 let space_zero = DenseVectorSpace::new(0);
1887 let lr_space = LowRankUpdateSymMatrixSpace::new(3, None, false);
1888 let mut lr = lr_space.make_new_low_rank();
1889 lr.set_diag(dvec_rc(&space_x, &[2.0, 3.0, 4.0]));
1890 let v_space = MultiVectorMatrixSpace::new(3, Rc::clone(&space_x));
1891 let mut v = v_space.make_new_multi_vector();
1892 v.set_vector(0, dvec_rc(&space_x, &[0.5, 0.0, 0.0]));
1893 v.set_vector(1, dvec_rc(&space_x, &[0.0, 0.5, 0.0]));
1894 v.set_vector(2, dvec_rc(&space_x, &[0.0, 0.0, 0.5]));
1895 lr.set_v(Rc::new(v));
1896 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1897 let (j_c, j_d) = empty_jacobians(3);
1898 let delta_x = 0.0;
1899
1900 let (inner, stats) = CountingInner::new();
1901 let mut solver = LowRankAugSystemSolver::new(Box::new(inner));
1902
1903 let coeffs = AugSysCoeffs {
1904 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1905 w_factor: 1.0,
1906 d_x: None,
1907 delta_x,
1908 d_s: None,
1909 delta_s: 0.0,
1910 j_c: &j_c as &dyn Matrix,
1911 d_c: None,
1912 delta_c: 0.0,
1913 j_d: &j_d as &dyn Matrix,
1914 d_d: None,
1915 delta_d: 0.0,
1916 };
1917 let rhs_x = dvec(&space_x, &[1.0, 1.0, 1.0]);
1918 let rhs_zero = dvec(&space_zero, &[]);
1919 let rhs = AugSysRhs {
1920 rhs_x: &rhs_x,
1921 rhs_s: &rhs_zero,
1922 rhs_c: &rhs_zero,
1923 rhs_d: &rhs_zero,
1924 };
1925 let (mut sx, mut z1, mut z2, mut z3) = (
1926 dvec(&space_x, &[0.0, 0.0, 0.0]),
1927 dvec(&space_zero, &[]),
1928 dvec(&space_zero, &[]),
1929 dvec(&space_zero, &[]),
1930 );
1931 {
1932 let mut sol = AugSysSol {
1933 sol_x: &mut sx,
1934 sol_s: &mut z1,
1935 sol_c: &mut z2,
1936 sol_d: &mut z3,
1937 };
1938 assert_eq!(
1939 solver.solve(&coeffs, &rhs, &mut sol, false, 0),
1940 ESymSolverStatus::Success
1941 );
1942 }
1943 assert_eq!(
1944 stats.factorizations.get(),
1945 1,
1946 "three SMW columns plus the diagonal solve must share one factorization"
1947 );
1948 assert_eq!(
1949 stats.backsolves.get(),
1950 3,
1951 "2 remaining V columns + diagonal solve"
1952 );
1953 }
1954
1955 #[test]
1956 fn batched_smw_columns_match_the_per_column_path() {
1957 fn run(packed: bool) -> (Vec<Number>, Rc<InnerStats>) {
1965 let space_x = DenseVectorSpace::new(3);
1966 let space_zero = DenseVectorSpace::new(0);
1967 let lr_space = LowRankUpdateSymMatrixSpace::new(3, None, false);
1968 let mut lr = lr_space.make_new_low_rank();
1969 lr.set_diag(dvec_rc(&space_x, &[2.0, 3.0, 4.0]));
1970 let v_space = MultiVectorMatrixSpace::new(3, Rc::clone(&space_x));
1971 let mut v = v_space.make_new_multi_vector();
1972 v.set_vector(0, dvec_rc(&space_x, &[0.5, 0.25, 0.0]));
1973 v.set_vector(1, dvec_rc(&space_x, &[0.0, 0.5, 0.125]));
1974 v.set_vector(2, dvec_rc(&space_x, &[0.25, 0.0, 0.5]));
1975 lr.set_v(Rc::new(v));
1976 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1977 let (j_c, j_d) = empty_jacobians(3);
1978
1979 let (inner, stats) = if packed {
1980 CountingInner::with_packed_path()
1981 } else {
1982 CountingInner::new()
1983 };
1984 let mut solver = LowRankAugSystemSolver::new(Box::new(inner));
1985 let coeffs = AugSysCoeffs {
1986 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1987 w_factor: 1.0,
1988 d_x: None,
1989 delta_x: 0.0,
1990 d_s: None,
1991 delta_s: 0.0,
1992 j_c: &j_c as &dyn Matrix,
1993 d_c: None,
1994 delta_c: 0.0,
1995 j_d: &j_d as &dyn Matrix,
1996 d_d: None,
1997 delta_d: 0.0,
1998 };
1999 let rhs_x = dvec(&space_x, &[1.0, -2.0, 3.5]);
2000 let rhs_zero = dvec(&space_zero, &[]);
2001 let rhs = AugSysRhs {
2002 rhs_x: &rhs_x,
2003 rhs_s: &rhs_zero,
2004 rhs_c: &rhs_zero,
2005 rhs_d: &rhs_zero,
2006 };
2007 let (mut sx, mut z1, mut z2, mut z3) = (
2008 dvec(&space_x, &[0.0, 0.0, 0.0]),
2009 dvec(&space_zero, &[]),
2010 dvec(&space_zero, &[]),
2011 dvec(&space_zero, &[]),
2012 );
2013 {
2014 let mut sol = AugSysSol {
2015 sol_x: &mut sx,
2016 sol_s: &mut z1,
2017 sol_c: &mut z2,
2018 sol_d: &mut z3,
2019 };
2020 assert_eq!(
2021 solver.solve(&coeffs, &rhs, &mut sol, false, 0),
2022 ESymSolverStatus::Success
2023 );
2024 }
2025 (sx.expanded_values(), stats)
2026 }
2027
2028 let (sol_loop, stats_loop) = run(false);
2029 let (sol_batch, stats_batch) = run(true);
2030
2031 assert_eq!(
2032 stats_loop.batched_calls.get(),
2033 0,
2034 "a mock without the packed path must take the per-column arm"
2035 );
2036 assert!(
2037 stats_batch.factor_batched_calls.get() >= 1,
2038 "the packed mock must actually reach the merged arm — a mock \
2039 whose `system_dim()` is 0 when cold silently would not, and \
2040 then this test would be green about code it never ran"
2041 );
2042 assert_eq!(
2043 stats_batch.factorizations.get(),
2044 1,
2045 "batching must not cost an extra factorization"
2046 );
2047 assert_eq!(
2048 sol_loop, sol_batch,
2049 "batched and per-column arms must agree bit-for-bit"
2050 );
2051
2052 assert_eq!(
2059 stats_batch.factor_batched_calls.get(),
2060 1,
2061 "the cold SMW block must be ONE factorizing multi-RHS call"
2062 );
2063 assert_eq!(
2064 stats_batch.factor_batched_cols.get(),
2065 3,
2066 "all 3 V columns must ride along with the factorization"
2067 );
2068 assert_eq!(
2069 stats_batch.batched_calls.get(),
2070 0,
2071 "no separate back-solve pass may remain over the same factor"
2072 );
2073 assert_eq!(
2079 stats_batch.backsolves.get(),
2080 1,
2081 "only the main RHS solve may remain over the cached factor"
2082 );
2083 }
2084
2085 #[test]
2086 fn resolve_reuses_the_factor_instead_of_refactorizing() {
2087 let space_x = DenseVectorSpace::new(1);
2091 let space_zero = DenseVectorSpace::new(0);
2092 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
2093 let mut lr = lr_space.make_new_low_rank();
2094 lr.set_diag(dvec_rc(&space_x, &[2.0]));
2095 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
2096 let (j_c, j_d) = empty_jacobians(1);
2097 let delta_x = 0.001;
2098
2099 let (inner, stats) = CountingInner::new();
2100 let mut solver = LowRankAugSystemSolver::new(Box::new(inner));
2101
2102 let coeffs = AugSysCoeffs {
2103 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
2104 w_factor: 1.0,
2105 d_x: None,
2106 delta_x,
2107 d_s: None,
2108 delta_s: 0.0,
2109 j_c: &j_c as &dyn Matrix,
2110 d_c: None,
2111 delta_c: 0.0,
2112 j_d: &j_d as &dyn Matrix,
2113 d_d: None,
2114 delta_d: 0.0,
2115 };
2116 let rhs_x = dvec(&space_x, &[1.0]);
2117 let rhs_zero = dvec(&space_zero, &[]);
2118 let rhs = AugSysRhs {
2119 rhs_x: &rhs_x,
2120 rhs_s: &rhs_zero,
2121 rhs_c: &rhs_zero,
2122 rhs_d: &rhs_zero,
2123 };
2124 let expected = 1.0 / (2.0 + delta_x);
2125
2126 for round in 0..4 {
2127 let (mut sx, mut z1, mut z2, mut z3) = (
2128 dvec(&space_x, &[0.0]),
2129 dvec(&space_zero, &[]),
2130 dvec(&space_zero, &[]),
2131 dvec(&space_zero, &[]),
2132 );
2133 {
2136 let mut sol = AugSysSol {
2137 sol_x: &mut sx,
2138 sol_s: &mut z1,
2139 sol_c: &mut z2,
2140 sol_d: &mut z3,
2141 };
2142 let status = if round == 0 {
2143 solver.solve(&coeffs, &rhs, &mut sol, false, 0)
2144 } else {
2145 solver.resolve(&coeffs, &rhs, &mut sol)
2146 };
2147 assert_eq!(status, ESymSolverStatus::Success);
2148 }
2149 assert!(
2150 (sx.expanded_values()[0] - expected).abs() < 1e-12,
2151 "round {round} gave {:?}",
2152 sx.expanded_values()
2153 );
2154 }
2155 assert_eq!(
2156 stats.factorizations.get(),
2157 1,
2158 "three refinement re-solves must not re-factorize"
2159 );
2160 }
2161
2162 #[test]
2163 fn rebuild_with_empty_history_still_factorizes() {
2164 let space_x = DenseVectorSpace::new(1);
2181 let space_zero = DenseVectorSpace::new(0);
2182 let space_lr = LowRankUpdateSymMatrixSpace::new(1, None, false);
2183
2184 let mut lr1 = space_lr.make_new_low_rank();
2186 lr1.set_diag(dvec_rc(&space_x, &[2.0]));
2187 let v_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
2188 let mut v = v_space.make_new_multi_vector();
2189 v.set_vector(0, dvec_rc(&space_x, &[0.5]));
2190 lr1.set_v(Rc::new(v));
2191 let lr1_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr1);
2192
2193 let mut lr2 = LowRankUpdateSymMatrixSpace::new(1, None, false).make_new_low_rank();
2195 lr2.set_diag(dvec_rc(&space_x, &[7.0]));
2196 let lr2_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr2);
2197
2198 let (j_c, j_d) = empty_jacobians(1);
2199
2200 let (inner, stats) = CountingInner::new();
2201 let mut solver = LowRankAugSystemSolver::new(Box::new(inner));
2202
2203 let rhs_x = dvec(&space_x, &[1.0]);
2204 let rhs_zero = dvec(&space_zero, &[]);
2205 let rhs = AugSysRhs {
2206 rhs_x: &rhs_x,
2207 rhs_s: &rhs_zero,
2208 rhs_c: &rhs_zero,
2209 rhs_d: &rhs_zero,
2210 };
2211
2212 let run = |lr: &Rc<LowRankUpdateSymMatrix>, solver: &mut LowRankAugSystemSolver| {
2213 let coeffs = AugSysCoeffs {
2214 w: Some(lr.as_ref() as &dyn SymMatrix),
2215 w_factor: 1.0,
2216 d_x: None,
2217 delta_x: 0.0,
2218 d_s: None,
2219 delta_s: 0.0,
2220 j_c: &j_c as &dyn Matrix,
2221 d_c: None,
2222 delta_c: 0.0,
2223 j_d: &j_d as &dyn Matrix,
2224 d_d: None,
2225 delta_d: 0.0,
2226 };
2227 let (mut sx, mut z1, mut z2, mut z3) = (
2228 dvec(&space_x, &[0.0]),
2229 dvec(&space_zero, &[]),
2230 dvec(&space_zero, &[]),
2231 dvec(&space_zero, &[]),
2232 );
2233 {
2234 let mut sol = AugSysSol {
2235 sol_x: &mut sx,
2236 sol_s: &mut z1,
2237 sol_c: &mut z2,
2238 sol_d: &mut z3,
2239 };
2240 assert_eq!(
2241 solver.solve(&coeffs, &rhs, &mut sol, false, 0),
2242 ESymSolverStatus::Success
2243 );
2244 }
2245 sx.expanded_values()[0]
2246 };
2247
2248 run(&lr1_rc, &mut solver);
2249 let after_first = stats.factorizations.get();
2250
2251 let repeat = run(&lr1_rc, &mut solver);
2263 assert_eq!(
2264 stats.factorizations.get(),
2265 after_first,
2266 "round 1 must leave a reusable factor, else round 2 has no \
2267 stale factor to wrongly reuse and this test is vacuous"
2268 );
2269 assert!(
2274 (repeat - 4.0 / 9.0).abs() < 1e-12,
2275 "round 1 re-solve should answer 4/9 from the cached factor, \
2276 got {repeat}"
2277 );
2278
2279 assert!(
2282 lr2_rc.get_v().is_none() && lr2_rc.get_u().is_none(),
2283 "round 2 fixture must have empty L-BFGS history"
2284 );
2285
2286 let got = run(&lr2_rc, &mut solver);
2287
2288 assert_eq!(
2289 stats.factorizations.get(),
2290 after_first + 1,
2291 "a rebuild that performs no inner solve of its own must leave \
2292 the diagonal solve to factorize the new Wdiag"
2293 );
2294 assert!(
2298 (got - 1.0 / 7.0).abs() < 1e-12,
2299 "expected the new Wdiag (1/7), got {got} — stale factor reused"
2300 );
2301 }
2302}