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 number_of_neg_evals(&self) -> Index {
929 if self.inner.provides_inertia() {
930 self.inner.number_of_neg_evals()
931 } else {
932 self.num_neg_evals
933 }
934 }
935
936 fn increase_quality(&mut self) -> bool {
937 self.inner_has_factor = false;
940 self.inner_factor_neg_evals = None;
941 let inner = self.inner.increase_quality();
945 let bypass = self
946 .bypass
947 .as_deref_mut()
948 .map(|b| b.increase_quality())
949 .unwrap_or(false);
950 inner || bypass
951 }
952
953 fn last_solve_status(&self) -> ESymSolverStatus {
954 match (self.last_solve_took_bypass, self.bypass.as_deref()) {
957 (true, Some(bypass)) => bypass.last_solve_status(),
958 _ => self.inner.last_solve_status(),
959 }
960 }
961
962 fn set_timing_stats(&mut self, timing: Rc<TimingStatistics>) {
963 if let Some(bypass) = self.bypass.as_deref_mut() {
967 bypass.set_timing_stats(Rc::clone(&timing));
968 }
969 self.inner.set_timing_stats(timing);
970 }
971
972 fn set_slack_scaling(&mut self, nx: Index, s_scale: &[Number]) {
973 if let Some(bypass) = self.bypass.as_deref_mut() {
976 bypass.set_slack_scaling(nx, s_scale);
977 }
978 self.inner.set_slack_scaling(nx, s_scale);
979 }
980
981 fn handles_low_rank_w(&self) -> bool {
982 true
983 }
984
985 fn solve(
986 &mut self,
987 coeffs: &AugSysCoeffs<'_>,
988 rhs: &AugSysRhs<'_>,
989 sol: &mut AugSysSol<'_>,
990 check_neg_evals: bool,
991 num_neg_evals: Index,
992 ) -> ESymSolverStatus {
993 let mut check_neg_evals = check_neg_evals;
996 if !self.inner.provides_inertia() {
997 check_neg_evals = false;
998 }
999
1000 let lr_w_opt = coeffs
1009 .w
1010 .and_then(|w| w.as_any().downcast_ref::<LowRankUpdateSymMatrix>());
1011 let Some(lr_w) = lr_w_opt else {
1012 self.last_solve_took_bypass = true;
1021 let target = match self.bypass.as_deref_mut() {
1022 Some(bypass) => bypass,
1023 None => {
1024 self.inner_has_factor = false;
1029 self.inner_factor_neg_evals = None;
1030 self.inner.as_mut()
1031 }
1032 };
1033 let status = target.solve(coeffs, rhs, sol, check_neg_evals, num_neg_evals);
1034 if target.provides_inertia() {
1035 self.num_neg_evals = target.number_of_neg_evals();
1036 }
1037 return status;
1038 };
1039
1040 self.last_solve_took_bypass = false;
1041
1042 let needs_rebuild = self.first_call || self.augmented_system_requires_change(coeffs);
1043 if needs_rebuild {
1044 let status =
1045 self.update_factorization(lr_w, coeffs, rhs, check_neg_evals, num_neg_evals);
1046 if status != ESymSolverStatus::Success {
1047 return status;
1048 }
1049 self.store_cache(coeffs);
1050 self.first_call = false;
1051 }
1052
1053 let reuse = self.inner_has_factor
1066 && (!check_neg_evals || self.inner_factor_neg_evals == Some(num_neg_evals));
1067 let ic = inner_coeffs(&self.factor, coeffs);
1068 let status = if reuse {
1069 self.inner.resolve(&ic, rhs, sol)
1070 } else {
1071 self.inner
1072 .solve(&ic, rhs, sol, check_neg_evals, num_neg_evals)
1073 };
1074 if self.inner.provides_inertia() {
1075 self.num_neg_evals = self.inner.number_of_neg_evals();
1076 }
1077 if status != ESymSolverStatus::Success {
1078 self.inner_has_factor = false;
1079 self.inner_factor_neg_evals = None;
1080 return status;
1081 }
1082 if !reuse {
1083 self.inner_has_factor = true;
1084 self.inner_factor_neg_evals = check_neg_evals.then_some(num_neg_evals);
1085 }
1086
1087 if self.factor.utilde2_x.is_some() {
1090 self.apply_smw(1.0, true, rhs, sol);
1091 }
1092 if self.factor.vtilde1_x.is_some() {
1093 self.apply_smw(-1.0, false, rhs, sol);
1094 }
1095
1096 ESymSolverStatus::Success
1097 }
1098
1099 fn resolve(
1116 &mut self,
1117 coeffs: &AugSysCoeffs<'_>,
1118 rhs: &AugSysRhs<'_>,
1119 sol: &mut AugSysSol<'_>,
1120 ) -> ESymSolverStatus {
1121 let is_low_rank = coeffs
1131 .w
1132 .and_then(|w| w.as_any().downcast_ref::<LowRankUpdateSymMatrix>())
1133 .is_some();
1134 if !is_low_rank
1135 || self.first_call
1136 || !self.inner_has_factor
1137 || self.augmented_system_requires_change(coeffs)
1138 {
1139 return self.solve(coeffs, rhs, sol, false, 0);
1140 }
1141
1142 let ic = inner_coeffs(&self.factor, coeffs);
1143 let status = self.inner.resolve(&ic, rhs, sol);
1144 if status != ESymSolverStatus::Success {
1145 self.inner_has_factor = false;
1146 self.inner_factor_neg_evals = None;
1147 return status;
1148 }
1149
1150 if self.factor.utilde2_x.is_some() {
1152 self.apply_smw(1.0, true, rhs, sol);
1153 }
1154 if self.factor.vtilde1_x.is_some() {
1155 self.apply_smw(-1.0, false, rhs, sol);
1156 }
1157
1158 ESymSolverStatus::Success
1159 }
1160
1161 }
1175
1176impl LowRankAugSystemSolver {
1177 fn apply_smw(&self, sign: Number, use_u: bool, rhs: &AugSysRhs<'_>, sol: &mut AugSysSol<'_>) {
1183 let (mvx, mvs, mvc, mvd, j) = if use_u {
1184 (
1185 self.factor.utilde2_x.as_ref().unwrap(),
1186 self.factor.utilde2_s.as_ref().unwrap(),
1187 self.factor.utilde2_c.as_ref().unwrap(),
1188 self.factor.utilde2_d.as_ref().unwrap(),
1189 self.factor.j2.as_ref().unwrap(),
1190 )
1191 } else {
1192 (
1193 self.factor.vtilde1_x.as_ref().unwrap(),
1194 self.factor.vtilde1_s.as_ref().unwrap(),
1195 self.factor.vtilde1_c.as_ref().unwrap(),
1196 self.factor.vtilde1_d.as_ref().unwrap(),
1197 self.factor.j1.as_ref().unwrap(),
1198 )
1199 };
1200 let n = mvx.n_cols();
1201 let mut b_vec: Vec<Number> = Vec::with_capacity(n as usize);
1205 for k in 0..n {
1206 let dot = mvx.get_vector(k).dot(rhs.rhs_x)
1207 + mvs.get_vector(k).dot(rhs.rhs_s)
1208 + mvc.get_vector(k).dot(rhs.rhs_c)
1209 + mvd.get_vector(k).dot(rhs.rhs_d);
1210 b_vec.push(dot);
1211 }
1212 let space_b = DenseVectorSpace::new(n);
1213 let mut b = space_b.make_new_dense();
1214 b.set_values(&b_vec);
1215 j.cholesky_solve_vector(&mut b);
1217 mvx.mult_vector(sign, &b, 1.0, sol.sol_x);
1219 mvs.mult_vector(sign, &b, 1.0, sol.sol_s);
1220 mvc.mult_vector(sign, &b, 1.0, sol.sol_c);
1221 mvd.mult_vector(sign, &b, 1.0, sol.sol_d);
1222 }
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227 use super::*;
1228 use pounce_linalg::dense_vector::DenseVectorSpace;
1229 use pounce_linalg::low_rank_update_sym_matrix::LowRankUpdateSymMatrixSpace;
1230 use std::cell::{Cell, RefCell};
1231
1232 struct DiagInner {
1237 calls: Cell<usize>,
1238 }
1239 impl AugSystemSolver for DiagInner {
1240 fn provides_inertia(&self) -> bool {
1241 true
1242 }
1243 fn number_of_neg_evals(&self) -> Index {
1244 0
1245 }
1246 fn increase_quality(&mut self) -> bool {
1247 true
1248 }
1249 fn last_solve_status(&self) -> ESymSolverStatus {
1250 ESymSolverStatus::Success
1251 }
1252 fn solve(
1253 &mut self,
1254 coeffs: &AugSysCoeffs<'_>,
1255 rhs: &AugSysRhs<'_>,
1256 sol: &mut AugSysSol<'_>,
1257 _check_neg_evals: bool,
1258 _num_neg_evals: Index,
1259 ) -> ESymSolverStatus {
1260 self.calls.set(self.calls.get() + 1);
1261 let wdiag = coeffs
1262 .w
1263 .expect("DiagInner requires W")
1264 .as_any()
1265 .downcast_ref::<DiagMatrix>()
1266 .expect("DiagInner requires W to be a DiagMatrix");
1267 let diag_rc = wdiag.get_diag().expect("Wdiag has no diag set").clone();
1268 let diag = downcast_dense(diag_rc.as_ref()).expanded_values();
1269 let rhs_x = downcast_dense(rhs.rhs_x).expanded_values();
1270 let dx_vals: Option<Vec<Number>> =
1271 coeffs.d_x.map(|d| downcast_dense(d).expanded_values());
1272 let mut out = vec![0.0; rhs_x.len()];
1273 for i in 0..rhs_x.len() {
1274 let dx_i = match &dx_vals {
1275 Some(v) => v[i],
1276 None => 0.0,
1277 };
1278 let denom = diag[i] + dx_i + coeffs.delta_x;
1279 out[i] = rhs_x[i] / denom;
1280 }
1281 let sol_x_dv = sol
1282 .sol_x
1283 .as_any_mut()
1284 .downcast_mut::<DenseVector>()
1285 .unwrap();
1286 sol_x_dv.set_values(&out);
1287 ESymSolverStatus::Success
1289 }
1290 }
1291
1292 #[derive(Default)]
1303 struct InnerStats {
1304 factorizations: Cell<usize>,
1305 backsolves: Cell<usize>,
1306 batched_calls: Cell<usize>,
1307 batched_cols: Cell<usize>,
1308 factor_batched_calls: Cell<usize>,
1312 factor_batched_cols: Cell<usize>,
1313 factored_diag: RefCell<Vec<Number>>,
1314 factored_delta_x: Cell<Number>,
1315 }
1316
1317 struct CountingInner {
1318 stats: Rc<InnerStats>,
1319 packed: bool,
1325 }
1326
1327 impl CountingInner {
1328 fn new() -> (Self, Rc<InnerStats>) {
1331 let stats = Rc::new(InnerStats::default());
1332 (
1333 Self {
1334 stats: Rc::clone(&stats),
1335 packed: false,
1336 },
1337 stats,
1338 )
1339 }
1340
1341 fn with_packed_path() -> (Self, Rc<InnerStats>) {
1343 let stats = Rc::new(InnerStats::default());
1344 (
1345 Self {
1346 stats: Rc::clone(&stats),
1347 packed: true,
1348 },
1349 stats,
1350 )
1351 }
1352
1353 fn apply_cached(&self, rhs: &AugSysRhs<'_>, sol: &mut AugSysSol<'_>) {
1355 let rhs_x = downcast_dense(rhs.rhs_x).expanded_values();
1356 let diag = self.stats.factored_diag.borrow();
1357 let delta_x = self.stats.factored_delta_x.get();
1358 let out: Vec<Number> = (0..rhs_x.len())
1359 .map(|i| rhs_x[i] / (diag[i] + delta_x))
1360 .collect();
1361 sol.sol_x
1362 .as_any_mut()
1363 .downcast_mut::<DenseVector>()
1364 .unwrap()
1365 .set_values(&out);
1366 }
1367 }
1368
1369 impl AugSystemSolver for CountingInner {
1370 fn provides_inertia(&self) -> bool {
1371 false
1372 }
1373 fn number_of_neg_evals(&self) -> Index {
1374 0
1375 }
1376 fn increase_quality(&mut self) -> bool {
1377 false
1378 }
1379 fn last_solve_status(&self) -> ESymSolverStatus {
1380 ESymSolverStatus::Success
1381 }
1382 fn solve(
1383 &mut self,
1384 coeffs: &AugSysCoeffs<'_>,
1385 rhs: &AugSysRhs<'_>,
1386 sol: &mut AugSysSol<'_>,
1387 _check_neg_evals: bool,
1388 _num_neg_evals: Index,
1389 ) -> ESymSolverStatus {
1390 self.stats
1391 .factorizations
1392 .set(self.stats.factorizations.get() + 1);
1393 let wdiag = coeffs
1394 .w
1395 .expect("CountingInner requires W")
1396 .as_any()
1397 .downcast_ref::<DiagMatrix>()
1398 .expect("CountingInner requires W to be a DiagMatrix");
1399 let diag_rc = wdiag.get_diag().expect("Wdiag has no diag set").clone();
1400 *self.stats.factored_diag.borrow_mut() =
1401 downcast_dense(diag_rc.as_ref()).expanded_values();
1402 self.stats.factored_delta_x.set(coeffs.delta_x);
1403 self.apply_cached(rhs, sol);
1404 ESymSolverStatus::Success
1405 }
1406 fn resolve(
1407 &mut self,
1408 _coeffs: &AugSysCoeffs<'_>,
1409 rhs: &AugSysRhs<'_>,
1410 sol: &mut AugSysSol<'_>,
1411 ) -> ESymSolverStatus {
1412 assert!(
1413 !self.stats.factored_diag.borrow().is_empty(),
1414 "resolve reached with no cached factor"
1415 );
1416 self.stats.backsolves.set(self.stats.backsolves.get() + 1);
1417 self.apply_cached(rhs, sol);
1418 ESymSolverStatus::Success
1419 }
1420 fn system_dim(&self) -> Index {
1421 if self.packed {
1422 self.stats.factored_diag.borrow().len() as Index
1423 } else {
1424 0
1425 }
1426 }
1427 fn multi_solve_matches_single_solve(&self, _nrhs: usize) -> bool {
1433 self.packed
1434 }
1435
1436 fn try_solve_many_flat(
1441 &mut self,
1442 coeffs: &AugSysCoeffs<'_>,
1443 packed_rhs: &mut [Number],
1444 nrhs: usize,
1445 _check_neg_evals: bool,
1446 _num_neg_evals: Index,
1447 ) -> Option<ESymSolverStatus> {
1448 if !self.packed {
1449 return None;
1450 }
1451 let wdiag = coeffs
1452 .w
1453 .expect("CountingInner requires W")
1454 .as_any()
1455 .downcast_ref::<DiagMatrix>()
1456 .expect("CountingInner requires W to be a DiagMatrix");
1457 let diag_rc = wdiag.get_diag().expect("Wdiag has no diag set").clone();
1458 let diag = downcast_dense(diag_rc.as_ref()).expanded_values();
1459 let dim = diag.len();
1460 if packed_rhs.len() != dim * nrhs {
1461 return None;
1462 }
1463 *self.stats.factored_diag.borrow_mut() = diag.clone();
1464 self.stats.factored_delta_x.set(coeffs.delta_x);
1465 self.stats
1466 .factorizations
1467 .set(self.stats.factorizations.get() + 1);
1468 self.stats
1469 .factor_batched_calls
1470 .set(self.stats.factor_batched_calls.get() + 1);
1471 self.stats
1472 .factor_batched_cols
1473 .set(self.stats.factor_batched_cols.get() + nrhs);
1474 let delta_x = coeffs.delta_x;
1475 for col in packed_rhs.chunks_mut(dim) {
1476 for (i, x) in col.iter_mut().enumerate() {
1477 *x /= diag[i] + delta_x;
1478 }
1479 }
1480 Some(ESymSolverStatus::Success)
1481 }
1482
1483 fn try_resolve_many_flat(
1484 &mut self,
1485 _coeffs: &AugSysCoeffs<'_>,
1486 packed_rhs: &mut [Number],
1487 nrhs: usize,
1488 ) -> Option<ESymSolverStatus> {
1489 if !self.packed {
1490 return None;
1491 }
1492 let diag = self.stats.factored_diag.borrow();
1493 if diag.is_empty() {
1494 return None;
1495 }
1496 let dim = diag.len();
1497 if packed_rhs.len() != dim * nrhs {
1498 return Some(ESymSolverStatus::FatalError);
1499 }
1500 self.stats
1501 .batched_calls
1502 .set(self.stats.batched_calls.get() + 1);
1503 self.stats
1504 .batched_cols
1505 .set(self.stats.batched_cols.get() + nrhs);
1506 let delta_x = self.stats.factored_delta_x.get();
1507 for col in packed_rhs.chunks_mut(dim) {
1508 for (i, x) in col.iter_mut().enumerate() {
1509 *x /= diag[i] + delta_x;
1510 }
1511 }
1512 Some(ESymSolverStatus::Success)
1513 }
1514 }
1515
1516 fn dvec(space: &Rc<DenseVectorSpace>, vals: &[Number]) -> DenseVector {
1517 let mut v = space.make_new_dense();
1518 v.set_values(vals);
1519 v
1520 }
1521
1522 fn dvec_rc(space: &Rc<DenseVectorSpace>, vals: &[Number]) -> Rc<DenseVector> {
1523 Rc::new(dvec(space, vals))
1524 }
1525
1526 #[test]
1527 fn smw_recovers_low_rank_inverse() {
1528 let space_x = DenseVectorSpace::new(1);
1533 let space_zero = DenseVectorSpace::new(0);
1534 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
1535 let mut lr = lr_space.make_new_low_rank();
1536 let b0_rc: Rc<dyn Vector> = dvec_rc(&space_x, &[2.0]);
1537 lr.set_diag(b0_rc);
1538 let v_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
1539 let mut v_mvm = v_space.make_new_multi_vector();
1540 v_mvm.set_vector(0, dvec_rc(&space_x, &[3.0]) as Rc<dyn Vector>);
1541 lr.set_v(Rc::new(v_mvm));
1542 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1543
1544 let mut solver = LowRankAugSystemSolver::new(Box::new(DiagInner {
1545 calls: Cell::new(0),
1546 }));
1547
1548 let j_c_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1550 let j_d_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1551 let j_c = j_c_space.make_new_dense_gen();
1552 let j_d = j_d_space.make_new_dense_gen();
1553
1554 let coeffs = AugSysCoeffs {
1555 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1556 w_factor: 1.0,
1557 d_x: None,
1558 delta_x: 0.0,
1559 d_s: None,
1560 delta_s: 0.0,
1561 j_c: &j_c as &dyn Matrix,
1562 d_c: None,
1563 delta_c: 0.0,
1564 j_d: &j_d as &dyn Matrix,
1565 d_d: None,
1566 delta_d: 0.0,
1567 };
1568
1569 let rhs_x = dvec(&space_x, &[5.0]);
1570 let rhs_s = dvec(&space_zero, &[]);
1571 let rhs_c = dvec(&space_zero, &[]);
1572 let rhs_d = dvec(&space_zero, &[]);
1573 let rhs = AugSysRhs {
1574 rhs_x: &rhs_x,
1575 rhs_s: &rhs_s,
1576 rhs_c: &rhs_c,
1577 rhs_d: &rhs_d,
1578 };
1579 let mut sol_x = dvec(&space_x, &[0.0]);
1580 let mut sol_s = dvec(&space_zero, &[]);
1581 let mut sol_c = dvec(&space_zero, &[]);
1582 let mut sol_d = dvec(&space_zero, &[]);
1583 let mut sol = AugSysSol {
1584 sol_x: &mut sol_x,
1585 sol_s: &mut sol_s,
1586 sol_c: &mut sol_c,
1587 sol_d: &mut sol_d,
1588 };
1589 let status = solver.solve(&coeffs, &rhs, &mut sol, false, 0);
1590 assert_eq!(status, ESymSolverStatus::Success);
1591 let got = sol_x.expanded_values()[0];
1593 let want = 5.0 / 11.0;
1594 assert!((got - want).abs() < 1e-12, "got {} want {}", got, want);
1595 }
1596
1597 #[test]
1598 fn smw_with_u_only_applies_positive_correction() {
1599 let space_x = DenseVectorSpace::new(1);
1602 let space_zero = DenseVectorSpace::new(0);
1603 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
1604 let mut lr = lr_space.make_new_low_rank();
1605 lr.set_diag(dvec_rc(&space_x, &[5.0]));
1606 let u_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
1607 let mut u_mvm = u_space.make_new_multi_vector();
1608 u_mvm.set_vector(0, dvec_rc(&space_x, &[1.5]) as Rc<dyn Vector>);
1609 lr.set_u(Rc::new(u_mvm));
1610 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1611
1612 let mut solver = LowRankAugSystemSolver::new(Box::new(DiagInner {
1613 calls: Cell::new(0),
1614 }));
1615
1616 let j_c_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1617 let j_d_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1618 let j_c = j_c_space.make_new_dense_gen();
1619 let j_d = j_d_space.make_new_dense_gen();
1620
1621 let coeffs = AugSysCoeffs {
1622 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1623 w_factor: 1.0,
1624 d_x: None,
1625 delta_x: 0.0,
1626 d_s: None,
1627 delta_s: 0.0,
1628 j_c: &j_c as &dyn Matrix,
1629 d_c: None,
1630 delta_c: 0.0,
1631 j_d: &j_d as &dyn Matrix,
1632 d_d: None,
1633 delta_d: 0.0,
1634 };
1635
1636 let rhs_x = dvec(&space_x, &[7.0]);
1637 let rhs_s = dvec(&space_zero, &[]);
1638 let rhs_c = dvec(&space_zero, &[]);
1639 let rhs_d = dvec(&space_zero, &[]);
1640 let rhs = AugSysRhs {
1641 rhs_x: &rhs_x,
1642 rhs_s: &rhs_s,
1643 rhs_c: &rhs_c,
1644 rhs_d: &rhs_d,
1645 };
1646 let mut sol_x = dvec(&space_x, &[0.0]);
1647 let mut sol_s = dvec(&space_zero, &[]);
1648 let mut sol_c = dvec(&space_zero, &[]);
1649 let mut sol_d = dvec(&space_zero, &[]);
1650 let mut sol = AugSysSol {
1651 sol_x: &mut sol_x,
1652 sol_s: &mut sol_s,
1653 sol_c: &mut sol_c,
1654 sol_d: &mut sol_d,
1655 };
1656 let status = solver.solve(&coeffs, &rhs, &mut sol, false, 0);
1657 assert_eq!(status, ESymSolverStatus::Success);
1658 let got = sol_x.expanded_values()[0];
1660 let want = 7.0 / 2.75;
1661 assert!((got - want).abs() < 1e-12, "got {} want {}", got, want);
1662 }
1663
1664 #[test]
1665 fn smw_reports_wrong_inertia_on_indefinite_negative_update() {
1666 let space_x = DenseVectorSpace::new(1);
1676 let space_zero = DenseVectorSpace::new(0);
1677 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
1678 let mut lr = lr_space.make_new_low_rank();
1679 lr.set_diag(dvec_rc(&space_x, &[2.0]));
1680 let u_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
1681 let mut u_mvm = u_space.make_new_multi_vector();
1682 u_mvm.set_vector(0, dvec_rc(&space_x, &[2.0]) as Rc<dyn Vector>);
1683 lr.set_u(Rc::new(u_mvm));
1684 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1685
1686 let mut solver = LowRankAugSystemSolver::new(Box::new(DiagInner {
1687 calls: Cell::new(0),
1688 }));
1689
1690 let j_c_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1691 let j_d_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1692 let j_c = j_c_space.make_new_dense_gen();
1693 let j_d = j_d_space.make_new_dense_gen();
1694
1695 let coeffs = AugSysCoeffs {
1696 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1697 w_factor: 1.0,
1698 d_x: None,
1699 delta_x: 0.0,
1700 d_s: None,
1701 delta_s: 0.0,
1702 j_c: &j_c as &dyn Matrix,
1703 d_c: None,
1704 delta_c: 0.0,
1705 j_d: &j_d as &dyn Matrix,
1706 d_d: None,
1707 delta_d: 0.0,
1708 };
1709
1710 let rhs_x = dvec(&space_x, &[1.0]);
1711 let rhs_s = dvec(&space_zero, &[]);
1712 let rhs_c = dvec(&space_zero, &[]);
1713 let rhs_d = dvec(&space_zero, &[]);
1714 let rhs = AugSysRhs {
1715 rhs_x: &rhs_x,
1716 rhs_s: &rhs_s,
1717 rhs_c: &rhs_c,
1718 rhs_d: &rhs_d,
1719 };
1720 let mut sol_x = dvec(&space_x, &[0.0]);
1721 let mut sol_s = dvec(&space_zero, &[]);
1722 let mut sol_c = dvec(&space_zero, &[]);
1723 let mut sol_d = dvec(&space_zero, &[]);
1724 let mut sol = AugSysSol {
1725 sol_x: &mut sol_x,
1726 sol_s: &mut sol_s,
1727 sol_c: &mut sol_c,
1728 sol_d: &mut sol_d,
1729 };
1730 let status = solver.solve(&coeffs, &rhs, &mut sol, false, 0);
1731 assert_eq!(status, ESymSolverStatus::WrongInertia);
1732 }
1733
1734 #[test]
1735 fn smw_with_v_and_u_combines_corrections() {
1736 let space_x = DenseVectorSpace::new(1);
1739 let space_zero = DenseVectorSpace::new(0);
1740 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
1741 let mut lr = lr_space.make_new_low_rank();
1742 lr.set_diag(dvec_rc(&space_x, &[10.0]));
1743 let v_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
1744 let mut v_mvm = v_space.make_new_multi_vector();
1745 v_mvm.set_vector(0, dvec_rc(&space_x, &[2.0]) as Rc<dyn Vector>);
1746 lr.set_v(Rc::new(v_mvm));
1747 let u_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
1748 let mut u_mvm = u_space.make_new_multi_vector();
1749 u_mvm.set_vector(0, dvec_rc(&space_x, &[1.0]) as Rc<dyn Vector>);
1750 lr.set_u(Rc::new(u_mvm));
1751 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1752
1753 let mut solver = LowRankAugSystemSolver::new(Box::new(DiagInner {
1754 calls: Cell::new(0),
1755 }));
1756
1757 let j_c_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1758 let j_d_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1759 let j_c = j_c_space.make_new_dense_gen();
1760 let j_d = j_d_space.make_new_dense_gen();
1761
1762 let coeffs = AugSysCoeffs {
1763 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1764 w_factor: 1.0,
1765 d_x: None,
1766 delta_x: 0.0,
1767 d_s: None,
1768 delta_s: 0.0,
1769 j_c: &j_c as &dyn Matrix,
1770 d_c: None,
1771 delta_c: 0.0,
1772 j_d: &j_d as &dyn Matrix,
1773 d_d: None,
1774 delta_d: 0.0,
1775 };
1776
1777 let rhs_x = dvec(&space_x, &[1.0]);
1778 let rhs_s = dvec(&space_zero, &[]);
1779 let rhs_c = dvec(&space_zero, &[]);
1780 let rhs_d = dvec(&space_zero, &[]);
1781 let rhs = AugSysRhs {
1782 rhs_x: &rhs_x,
1783 rhs_s: &rhs_s,
1784 rhs_c: &rhs_c,
1785 rhs_d: &rhs_d,
1786 };
1787 let mut sol_x = dvec(&space_x, &[0.0]);
1788 let mut sol_s = dvec(&space_zero, &[]);
1789 let mut sol_c = dvec(&space_zero, &[]);
1790 let mut sol_d = dvec(&space_zero, &[]);
1791 let mut sol = AugSysSol {
1792 sol_x: &mut sol_x,
1793 sol_s: &mut sol_s,
1794 sol_c: &mut sol_c,
1795 sol_d: &mut sol_d,
1796 };
1797 let status = solver.solve(&coeffs, &rhs, &mut sol, false, 0);
1798 assert_eq!(status, ESymSolverStatus::Success);
1799 let got = sol_x.expanded_values()[0];
1801 let want = 1.0 / 13.0;
1802 assert!((got - want).abs() < 1e-12, "got {} want {}", got, want);
1803 }
1804
1805 #[test]
1806 fn unchanged_coeffs_skip_rebuild_after_first_call() {
1807 let mut lr_solver = LowRankAugSystemSolver::new(Box::new(DiagInner {
1808 calls: Cell::new(0),
1809 }));
1810 let space_x = DenseVectorSpace::new(1);
1811 let space_zero = DenseVectorSpace::new(0);
1812 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
1813 let mut lr = lr_space.make_new_low_rank();
1814 lr.set_diag(dvec_rc(&space_x, &[2.0]));
1815 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1816 let j_c_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1817 let j_d_space = pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, 1);
1818 let j_c = j_c_space.make_new_dense_gen();
1819 let j_d = j_d_space.make_new_dense_gen();
1820 let coeffs = AugSysCoeffs {
1821 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1822 w_factor: 1.0,
1823 d_x: None,
1824 delta_x: 0.001,
1825 d_s: None,
1826 delta_s: 0.0,
1827 j_c: &j_c as &dyn Matrix,
1828 d_c: None,
1829 delta_c: 0.0,
1830 j_d: &j_d as &dyn Matrix,
1831 d_d: None,
1832 delta_d: 0.0,
1833 };
1834 let rhs_x = dvec(&space_x, &[1.0]);
1835 let rhs_zero = dvec(&space_zero, &[]);
1836 let rhs = AugSysRhs {
1837 rhs_x: &rhs_x,
1838 rhs_s: &rhs_zero,
1839 rhs_c: &rhs_zero,
1840 rhs_d: &rhs_zero,
1841 };
1842 let mut sol_x = dvec(&space_x, &[0.0]);
1843 let mut sol_z1 = dvec(&space_zero, &[]);
1844 let mut sol_z2 = dvec(&space_zero, &[]);
1845 let mut sol_z3 = dvec(&space_zero, &[]);
1846 {
1847 let mut sol = AugSysSol {
1848 sol_x: &mut sol_x,
1849 sol_s: &mut sol_z1,
1850 sol_c: &mut sol_z2,
1851 sol_d: &mut sol_z3,
1852 };
1853 lr_solver.solve(&coeffs, &rhs, &mut sol, false, 0);
1854 }
1855 assert!(!lr_solver.augmented_system_requires_change(&coeffs));
1857 }
1858
1859 fn empty_jacobians(
1864 n_x: Index,
1865 ) -> (
1866 pounce_linalg::dense_gen_matrix::DenseGenMatrix,
1867 pounce_linalg::dense_gen_matrix::DenseGenMatrix,
1868 ) {
1869 (
1870 pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, n_x).make_new_dense_gen(),
1871 pounce_linalg::dense_gen_matrix::DenseGenMatrixSpace::new(0, n_x).make_new_dense_gen(),
1872 )
1873 }
1874
1875 #[test]
1876 fn smw_columns_share_one_factorization() {
1877 let space_x = DenseVectorSpace::new(3);
1882 let space_zero = DenseVectorSpace::new(0);
1883 let lr_space = LowRankUpdateSymMatrixSpace::new(3, None, false);
1884 let mut lr = lr_space.make_new_low_rank();
1885 lr.set_diag(dvec_rc(&space_x, &[2.0, 3.0, 4.0]));
1886 let v_space = MultiVectorMatrixSpace::new(3, Rc::clone(&space_x));
1887 let mut v = v_space.make_new_multi_vector();
1888 v.set_vector(0, dvec_rc(&space_x, &[0.5, 0.0, 0.0]));
1889 v.set_vector(1, dvec_rc(&space_x, &[0.0, 0.5, 0.0]));
1890 v.set_vector(2, dvec_rc(&space_x, &[0.0, 0.0, 0.5]));
1891 lr.set_v(Rc::new(v));
1892 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1893 let (j_c, j_d) = empty_jacobians(3);
1894 let delta_x = 0.0;
1895
1896 let (inner, stats) = CountingInner::new();
1897 let mut solver = LowRankAugSystemSolver::new(Box::new(inner));
1898
1899 let coeffs = AugSysCoeffs {
1900 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1901 w_factor: 1.0,
1902 d_x: None,
1903 delta_x,
1904 d_s: None,
1905 delta_s: 0.0,
1906 j_c: &j_c as &dyn Matrix,
1907 d_c: None,
1908 delta_c: 0.0,
1909 j_d: &j_d as &dyn Matrix,
1910 d_d: None,
1911 delta_d: 0.0,
1912 };
1913 let rhs_x = dvec(&space_x, &[1.0, 1.0, 1.0]);
1914 let rhs_zero = dvec(&space_zero, &[]);
1915 let rhs = AugSysRhs {
1916 rhs_x: &rhs_x,
1917 rhs_s: &rhs_zero,
1918 rhs_c: &rhs_zero,
1919 rhs_d: &rhs_zero,
1920 };
1921 let (mut sx, mut z1, mut z2, mut z3) = (
1922 dvec(&space_x, &[0.0, 0.0, 0.0]),
1923 dvec(&space_zero, &[]),
1924 dvec(&space_zero, &[]),
1925 dvec(&space_zero, &[]),
1926 );
1927 {
1928 let mut sol = AugSysSol {
1929 sol_x: &mut sx,
1930 sol_s: &mut z1,
1931 sol_c: &mut z2,
1932 sol_d: &mut z3,
1933 };
1934 assert_eq!(
1935 solver.solve(&coeffs, &rhs, &mut sol, false, 0),
1936 ESymSolverStatus::Success
1937 );
1938 }
1939 assert_eq!(
1940 stats.factorizations.get(),
1941 1,
1942 "three SMW columns plus the diagonal solve must share one factorization"
1943 );
1944 assert_eq!(
1945 stats.backsolves.get(),
1946 3,
1947 "2 remaining V columns + diagonal solve"
1948 );
1949 }
1950
1951 #[test]
1952 fn batched_smw_columns_match_the_per_column_path() {
1953 fn run(packed: bool) -> (Vec<Number>, Rc<InnerStats>) {
1961 let space_x = DenseVectorSpace::new(3);
1962 let space_zero = DenseVectorSpace::new(0);
1963 let lr_space = LowRankUpdateSymMatrixSpace::new(3, None, false);
1964 let mut lr = lr_space.make_new_low_rank();
1965 lr.set_diag(dvec_rc(&space_x, &[2.0, 3.0, 4.0]));
1966 let v_space = MultiVectorMatrixSpace::new(3, Rc::clone(&space_x));
1967 let mut v = v_space.make_new_multi_vector();
1968 v.set_vector(0, dvec_rc(&space_x, &[0.5, 0.25, 0.0]));
1969 v.set_vector(1, dvec_rc(&space_x, &[0.0, 0.5, 0.125]));
1970 v.set_vector(2, dvec_rc(&space_x, &[0.25, 0.0, 0.5]));
1971 lr.set_v(Rc::new(v));
1972 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
1973 let (j_c, j_d) = empty_jacobians(3);
1974
1975 let (inner, stats) = if packed {
1976 CountingInner::with_packed_path()
1977 } else {
1978 CountingInner::new()
1979 };
1980 let mut solver = LowRankAugSystemSolver::new(Box::new(inner));
1981 let coeffs = AugSysCoeffs {
1982 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
1983 w_factor: 1.0,
1984 d_x: None,
1985 delta_x: 0.0,
1986 d_s: None,
1987 delta_s: 0.0,
1988 j_c: &j_c as &dyn Matrix,
1989 d_c: None,
1990 delta_c: 0.0,
1991 j_d: &j_d as &dyn Matrix,
1992 d_d: None,
1993 delta_d: 0.0,
1994 };
1995 let rhs_x = dvec(&space_x, &[1.0, -2.0, 3.5]);
1996 let rhs_zero = dvec(&space_zero, &[]);
1997 let rhs = AugSysRhs {
1998 rhs_x: &rhs_x,
1999 rhs_s: &rhs_zero,
2000 rhs_c: &rhs_zero,
2001 rhs_d: &rhs_zero,
2002 };
2003 let (mut sx, mut z1, mut z2, mut z3) = (
2004 dvec(&space_x, &[0.0, 0.0, 0.0]),
2005 dvec(&space_zero, &[]),
2006 dvec(&space_zero, &[]),
2007 dvec(&space_zero, &[]),
2008 );
2009 {
2010 let mut sol = AugSysSol {
2011 sol_x: &mut sx,
2012 sol_s: &mut z1,
2013 sol_c: &mut z2,
2014 sol_d: &mut z3,
2015 };
2016 assert_eq!(
2017 solver.solve(&coeffs, &rhs, &mut sol, false, 0),
2018 ESymSolverStatus::Success
2019 );
2020 }
2021 (sx.expanded_values(), stats)
2022 }
2023
2024 let (sol_loop, stats_loop) = run(false);
2025 let (sol_batch, stats_batch) = run(true);
2026
2027 assert_eq!(
2028 stats_loop.batched_calls.get(),
2029 0,
2030 "a mock without the packed path must take the per-column arm"
2031 );
2032 assert!(
2033 stats_batch.factor_batched_calls.get() >= 1,
2034 "the packed mock must actually reach the merged arm — a mock \
2035 whose `system_dim()` is 0 when cold silently would not, and \
2036 then this test would be green about code it never ran"
2037 );
2038 assert_eq!(
2039 stats_batch.factorizations.get(),
2040 1,
2041 "batching must not cost an extra factorization"
2042 );
2043 assert_eq!(
2044 sol_loop, sol_batch,
2045 "batched and per-column arms must agree bit-for-bit"
2046 );
2047
2048 assert_eq!(
2055 stats_batch.factor_batched_calls.get(),
2056 1,
2057 "the cold SMW block must be ONE factorizing multi-RHS call"
2058 );
2059 assert_eq!(
2060 stats_batch.factor_batched_cols.get(),
2061 3,
2062 "all 3 V columns must ride along with the factorization"
2063 );
2064 assert_eq!(
2065 stats_batch.batched_calls.get(),
2066 0,
2067 "no separate back-solve pass may remain over the same factor"
2068 );
2069 assert_eq!(
2075 stats_batch.backsolves.get(),
2076 1,
2077 "only the main RHS solve may remain over the cached factor"
2078 );
2079 }
2080
2081 #[test]
2082 fn resolve_reuses_the_factor_instead_of_refactorizing() {
2083 let space_x = DenseVectorSpace::new(1);
2087 let space_zero = DenseVectorSpace::new(0);
2088 let lr_space = LowRankUpdateSymMatrixSpace::new(1, None, false);
2089 let mut lr = lr_space.make_new_low_rank();
2090 lr.set_diag(dvec_rc(&space_x, &[2.0]));
2091 let lr_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr);
2092 let (j_c, j_d) = empty_jacobians(1);
2093 let delta_x = 0.001;
2094
2095 let (inner, stats) = CountingInner::new();
2096 let mut solver = LowRankAugSystemSolver::new(Box::new(inner));
2097
2098 let coeffs = AugSysCoeffs {
2099 w: Some(lr_rc.as_ref() as &dyn SymMatrix),
2100 w_factor: 1.0,
2101 d_x: None,
2102 delta_x,
2103 d_s: None,
2104 delta_s: 0.0,
2105 j_c: &j_c as &dyn Matrix,
2106 d_c: None,
2107 delta_c: 0.0,
2108 j_d: &j_d as &dyn Matrix,
2109 d_d: None,
2110 delta_d: 0.0,
2111 };
2112 let rhs_x = dvec(&space_x, &[1.0]);
2113 let rhs_zero = dvec(&space_zero, &[]);
2114 let rhs = AugSysRhs {
2115 rhs_x: &rhs_x,
2116 rhs_s: &rhs_zero,
2117 rhs_c: &rhs_zero,
2118 rhs_d: &rhs_zero,
2119 };
2120 let expected = 1.0 / (2.0 + delta_x);
2121
2122 for round in 0..4 {
2123 let (mut sx, mut z1, mut z2, mut z3) = (
2124 dvec(&space_x, &[0.0]),
2125 dvec(&space_zero, &[]),
2126 dvec(&space_zero, &[]),
2127 dvec(&space_zero, &[]),
2128 );
2129 {
2132 let mut sol = AugSysSol {
2133 sol_x: &mut sx,
2134 sol_s: &mut z1,
2135 sol_c: &mut z2,
2136 sol_d: &mut z3,
2137 };
2138 let status = if round == 0 {
2139 solver.solve(&coeffs, &rhs, &mut sol, false, 0)
2140 } else {
2141 solver.resolve(&coeffs, &rhs, &mut sol)
2142 };
2143 assert_eq!(status, ESymSolverStatus::Success);
2144 }
2145 assert!(
2146 (sx.expanded_values()[0] - expected).abs() < 1e-12,
2147 "round {round} gave {:?}",
2148 sx.expanded_values()
2149 );
2150 }
2151 assert_eq!(
2152 stats.factorizations.get(),
2153 1,
2154 "three refinement re-solves must not re-factorize"
2155 );
2156 }
2157
2158 #[test]
2159 fn rebuild_with_empty_history_still_factorizes() {
2160 let space_x = DenseVectorSpace::new(1);
2177 let space_zero = DenseVectorSpace::new(0);
2178 let space_lr = LowRankUpdateSymMatrixSpace::new(1, None, false);
2179
2180 let mut lr1 = space_lr.make_new_low_rank();
2182 lr1.set_diag(dvec_rc(&space_x, &[2.0]));
2183 let v_space = MultiVectorMatrixSpace::new(1, Rc::clone(&space_x));
2184 let mut v = v_space.make_new_multi_vector();
2185 v.set_vector(0, dvec_rc(&space_x, &[0.5]));
2186 lr1.set_v(Rc::new(v));
2187 let lr1_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr1);
2188
2189 let mut lr2 = LowRankUpdateSymMatrixSpace::new(1, None, false).make_new_low_rank();
2191 lr2.set_diag(dvec_rc(&space_x, &[7.0]));
2192 let lr2_rc: Rc<LowRankUpdateSymMatrix> = Rc::new(lr2);
2193
2194 let (j_c, j_d) = empty_jacobians(1);
2195
2196 let (inner, stats) = CountingInner::new();
2197 let mut solver = LowRankAugSystemSolver::new(Box::new(inner));
2198
2199 let rhs_x = dvec(&space_x, &[1.0]);
2200 let rhs_zero = dvec(&space_zero, &[]);
2201 let rhs = AugSysRhs {
2202 rhs_x: &rhs_x,
2203 rhs_s: &rhs_zero,
2204 rhs_c: &rhs_zero,
2205 rhs_d: &rhs_zero,
2206 };
2207
2208 let run = |lr: &Rc<LowRankUpdateSymMatrix>, solver: &mut LowRankAugSystemSolver| {
2209 let coeffs = AugSysCoeffs {
2210 w: Some(lr.as_ref() as &dyn SymMatrix),
2211 w_factor: 1.0,
2212 d_x: None,
2213 delta_x: 0.0,
2214 d_s: None,
2215 delta_s: 0.0,
2216 j_c: &j_c as &dyn Matrix,
2217 d_c: None,
2218 delta_c: 0.0,
2219 j_d: &j_d as &dyn Matrix,
2220 d_d: None,
2221 delta_d: 0.0,
2222 };
2223 let (mut sx, mut z1, mut z2, mut z3) = (
2224 dvec(&space_x, &[0.0]),
2225 dvec(&space_zero, &[]),
2226 dvec(&space_zero, &[]),
2227 dvec(&space_zero, &[]),
2228 );
2229 {
2230 let mut sol = AugSysSol {
2231 sol_x: &mut sx,
2232 sol_s: &mut z1,
2233 sol_c: &mut z2,
2234 sol_d: &mut z3,
2235 };
2236 assert_eq!(
2237 solver.solve(&coeffs, &rhs, &mut sol, false, 0),
2238 ESymSolverStatus::Success
2239 );
2240 }
2241 sx.expanded_values()[0]
2242 };
2243
2244 run(&lr1_rc, &mut solver);
2245 let after_first = stats.factorizations.get();
2246
2247 let repeat = run(&lr1_rc, &mut solver);
2259 assert_eq!(
2260 stats.factorizations.get(),
2261 after_first,
2262 "round 1 must leave a reusable factor, else round 2 has no \
2263 stale factor to wrongly reuse and this test is vacuous"
2264 );
2265 assert!(
2270 (repeat - 4.0 / 9.0).abs() < 1e-12,
2271 "round 1 re-solve should answer 4/9 from the cached factor, \
2272 got {repeat}"
2273 );
2274
2275 assert!(
2278 lr2_rc.get_v().is_none() && lr2_rc.get_u().is_none(),
2279 "round 2 fixture must have empty L-BFGS history"
2280 );
2281
2282 let got = run(&lr2_rc, &mut solver);
2283
2284 assert_eq!(
2285 stats.factorizations.get(),
2286 after_first + 1,
2287 "a rebuild that performs no inner solve of its own must leave \
2288 the diagonal solve to factorize the new Wdiag"
2289 );
2290 assert!(
2294 (got - 1.0 / 7.0).abs() < 1e-12,
2295 "expected the new Wdiag (1/7), got {got} — stale factor reused"
2296 );
2297 }
2298}