1use crate::kkt::count_inertia;
81use crate::real::{Real, is_exact_zero, le, lt};
82use crate::{DenseError, Inertia, Perm};
83
84#[derive(Clone, Debug, PartialEq)]
86pub struct DenseBlock<T: Real = f64> {
87 pub rows: usize,
89 pub cols: usize,
91 pub data: Vec<T>,
93}
94
95impl<T: Real> DenseBlock<T> {
96 pub fn zeros(rows: usize, cols: usize) -> Self {
98 Self {
99 rows,
100 cols,
101 data: vec![T::ZERO; rows * cols],
102 }
103 }
104
105 #[inline]
107 pub fn get(&self, i: usize, j: usize) -> T {
108 self.data[i + j * self.rows]
109 }
110
111 #[inline]
113 pub fn set(&mut self, i: usize, j: usize, v: T) {
114 self.data[i + j * self.rows] = v;
115 }
116
117 fn is_consistent(&self) -> bool {
118 self.data.len() == self.rows * self.cols
119 }
120
121 fn square(&self) -> Result<usize, DenseError> {
122 if self.is_consistent() && self.rows == self.cols {
123 Ok(self.rows)
124 } else {
125 Err(DenseError::Dimension)
126 }
127 }
128}
129
130pub trait DenseKernel<T: Real> {
134 fn potrf(&self, a: &mut DenseBlock<T>) -> Result<(), DenseError>;
136
137 fn sytrf(
141 &self,
142 a: &mut DenseBlock<T>,
143 perm: &mut Perm,
144 pivot_2x2: &mut Vec<bool>,
145 ) -> Result<Inertia, DenseError>;
146
147 fn trsm_lower(&self, l: &DenseBlock<T>, b: &mut DenseBlock<T>, transpose: bool);
153
154 fn gemm_sub(&self, a: &DenseBlock<T>, b: &DenseBlock<T>, c: &mut DenseBlock<T>);
159
160 fn syrk_sub(&self, a: &DenseBlock<T>, c: &mut DenseBlock<T>);
165
166 fn name(&self) -> &'static str;
168}
169
170#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
180pub struct NaiveDense;
181
182fn bk_alpha<T: Real>() -> T {
185 (T::ONE + T::from_f64(17.0).sqrt()) / T::from_f64(8.0)
186}
187
188impl<T: Real> DenseKernel<T> for NaiveDense {
189 fn potrf(&self, a: &mut DenseBlock<T>) -> Result<(), DenseError> {
190 let n = a.square()?;
191 for j in 0..n {
192 let d = a.get(j, j) - (0..j).map(|k| a.get(j, k) * a.get(j, k)).sum::<T>();
193 if !d.is_finite() || le(d, T::ZERO, T::ZERO) {
194 return Err(DenseError::NotPositiveDefinite { position: j });
195 }
196 let ljj = d.sqrt();
197 a.set(j, j, ljj);
198 for i in j + 1..n {
199 let s = a.get(i, j) - (0..j).map(|k| a.get(i, k) * a.get(j, k)).sum::<T>();
200 a.set(i, j, s / ljj);
201 }
202 }
203 Ok(())
204 }
205
206 #[allow(clippy::too_many_lines)] fn sytrf(
208 &self,
209 a: &mut DenseBlock<T>,
210 perm: &mut Perm,
211 pivot_2x2: &mut Vec<bool>,
212 ) -> Result<Inertia, DenseError> {
213 let n = a.square()?;
214 let alpha = bk_alpha::<T>();
215 let mut w = vec![T::ZERO; n * n];
217 for j in 0..n {
218 for i in j..n {
219 w[i + j * n] = a.get(i, j);
220 w[j + i * n] = a.get(i, j);
221 }
222 }
223 let mut l = vec![T::ZERO; n * n];
224 let mut fwd: Vec<usize> = (0..n).collect();
225 let mut two = vec![false; n];
226 let mut first = vec![false; n];
228 let mut diag = vec![T::ZERO; n];
229 let mut off = vec![T::ZERO; n];
230 let swap = |w: &mut [T], l: &mut [T], fwd: &mut [usize], k: usize, r: usize| {
231 if k == r {
232 return;
233 }
234 for c in 0..n {
235 w.swap(k + c * n, r + c * n);
236 }
237 for c in 0..n {
238 w.swap(c + k * n, c + r * n);
239 }
240 for c in 0..k {
241 l.swap(k + c * n, r + c * n);
242 }
243 fwd.swap(k, r);
244 };
245 let mut k = 0;
246 while k < n {
247 let mut lambda = T::ZERO;
249 let mut r = k;
250 for i in k + 1..n {
251 if lt(lambda, w[i + k * n].abs()) {
252 lambda = w[i + k * n].abs();
253 r = i;
254 }
255 }
256 let akk = w[k + k * n].abs();
257 let size = if is_exact_zero(lambda) || !lt(akk, alpha * lambda) {
258 1 } else {
260 let mut sigma = T::ZERO;
262 for i in k..n {
263 if i != r {
264 sigma = sigma.max(w[i + r * n].abs());
265 }
266 }
267 if !lt(akk * sigma, alpha * lambda * lambda) {
268 1
269 } else if !lt(w[r + r * n].abs(), alpha * sigma) {
270 swap(&mut w, &mut l, &mut fwd, k, r);
271 1
272 } else {
273 swap(&mut w, &mut l, &mut fwd, k + 1, r);
274 2
275 }
276 };
277 if size == 1 {
279 let d = w[k + k * n];
280 diag[k] = d;
281 l[k + k * n] = T::ONE;
282 if !is_exact_zero(d) {
283 for i in k + 1..n {
284 l[i + k * n] = w[i + k * n] / d;
285 }
286 for j in k + 1..n {
287 let ljd = l[j + k * n] * d;
288 for i in j..n {
289 let v = w[i + j * n] - l[i + k * n] * ljd;
290 w[i + j * n] = v;
291 w[j + i * n] = v;
292 }
293 }
294 }
295 k += 1;
296 } else {
297 let (e11, e21, e22) = (w[k + k * n], w[k + 1 + k * n], w[k + 1 + (k + 1) * n]);
298 let det = e11 * e22 - e21 * e21;
299 diag[k] = e11;
300 diag[k + 1] = e22;
301 off[k] = e21;
302 two[k] = true;
303 two[k + 1] = true;
304 first[k] = true;
305 l[k + k * n] = T::ONE;
306 l[k + 1 + (k + 1) * n] = T::ONE;
307 for i in k + 2..n {
308 let (x, y) = (w[i + k * n], w[i + (k + 1) * n]);
309 l[i + k * n] = (x * e22 - y * e21) / det;
311 l[i + (k + 1) * n] = (y * e11 - x * e21) / det;
312 }
313 for j in k + 2..n {
314 let (xj, yj) = (w[j + k * n], w[j + (k + 1) * n]);
315 for i in j..n {
316 let v = w[i + j * n] - (l[i + k * n] * xj + l[i + (k + 1) * n] * yj);
317 w[i + j * n] = v;
318 w[j + i * n] = v;
319 }
320 }
321 k += 2;
322 }
323 }
324 for j in 0..n {
326 a.set(j, j, diag[j]);
327 for i in j + 1..n {
328 let v = if first[j] && i == j + 1 {
329 off[j]
330 } else {
331 l[i + j * n]
332 };
333 a.set(i, j, v);
334 }
335 }
336 *perm = Perm::from_fwd(fwd.into_iter().map(crate::perm::idx32).collect())
337 .map_err(|_| DenseError::Dimension)?;
338 pivot_2x2.clear();
339 pivot_2x2.extend_from_slice(&two);
340 Ok(block_inertia(a, pivot_2x2))
341 }
342
343 fn trsm_lower(&self, l: &DenseBlock<T>, b: &mut DenseBlock<T>, transpose: bool) {
344 let n = l.square().expect("trsm_lower: L must be square");
345 assert!(b.is_consistent(), "trsm_lower: B is inconsistent");
346 if transpose {
347 assert_eq!(b.cols, n, "trsm_lower: B must have n columns");
349 for i in 0..b.rows {
350 for j in 0..n {
351 let s = b.get(i, j) - (0..j).map(|k| b.get(i, k) * l.get(j, k)).sum::<T>();
352 b.set(i, j, s / l.get(j, j));
353 }
354 }
355 } else {
356 assert_eq!(b.rows, n, "trsm_lower: B must have n rows");
357 for c in 0..b.cols {
358 for i in 0..n {
359 let s = b.get(i, c) - (0..i).map(|k| l.get(i, k) * b.get(k, c)).sum::<T>();
360 b.set(i, c, s / l.get(i, i));
361 }
362 }
363 }
364 }
365
366 fn gemm_sub(&self, a: &DenseBlock<T>, b: &DenseBlock<T>, c: &mut DenseBlock<T>) {
367 assert!(
368 a.is_consistent()
369 && b.is_consistent()
370 && c.is_consistent()
371 && a.cols == b.cols
372 && c.rows == a.rows
373 && c.cols == b.rows,
374 "gemm_sub: dimensions do not agree"
375 );
376 for j in 0..c.cols {
377 for i in 0..c.rows {
378 let s = (0..a.cols).map(|k| a.get(i, k) * b.get(j, k)).sum::<T>();
379 c.set(i, j, c.get(i, j) - s);
380 }
381 }
382 }
383
384 fn syrk_sub(&self, a: &DenseBlock<T>, c: &mut DenseBlock<T>) {
385 let n = c.square().expect("syrk_sub: C must be square");
386 assert!(
387 a.is_consistent() && a.rows == n,
388 "syrk_sub: dimensions do not agree"
389 );
390 for j in 0..n {
391 for i in j..n {
392 let s = (0..a.cols).map(|k| a.get(i, k) * a.get(j, k)).sum::<T>();
393 c.set(i, j, c.get(i, j) - s);
394 }
395 }
396 }
397
398 fn name(&self) -> &'static str {
399 "naive"
400 }
401}
402
403fn block_inertia<T: Real>(a: &DenseBlock<T>, pivot_2x2: &[bool]) -> Inertia {
405 let n = a.rows;
406 let mut ones = Vec::new();
407 let mut twos = Vec::new();
408 let mut k = 0;
409 while k < n {
410 if pivot_2x2[k] && k + 1 < n {
411 twos.push((a.get(k, k), a.get(k + 1, k), a.get(k + 1, k + 1)));
412 k += 2;
413 } else {
414 ones.push(a.get(k, k));
415 k += 1;
416 }
417 }
418 count_inertia(ones.into_iter(), twos.into_iter())
419}
420
421#[cfg(feature = "faer")]
426#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
427pub struct FaerDense;
428
429#[cfg(feature = "faer")]
430mod faer_impl {
431 use faer::dyn_stack::{MemBuffer, MemStack};
432 use faer::linalg::cholesky::lblt::factor::{LbltParams, PivotingStrategy};
433 use faer::linalg::cholesky::llt::factor::LltRegularization;
434 use faer::linalg::matmul::triangular::BlockStructure;
435 use faer::{Accum, ColMut, MatMut, MatRef, Par, Spec};
436
437 use super::{DenseBlock, DenseError, DenseKernel, FaerDense, Inertia, Perm, block_inertia};
438 use crate::real::is_exact_zero;
439
440 fn view(a: &DenseBlock<f64>) -> MatRef<'_, f64> {
441 MatRef::from_column_major_slice(&a.data, a.rows, a.cols)
442 }
443
444 fn view_mut(a: &mut DenseBlock<f64>) -> MatMut<'_, f64> {
445 MatMut::from_column_major_slice_mut(&mut a.data, a.rows, a.cols)
446 }
447
448 impl DenseKernel<f64> for FaerDense {
449 fn potrf(&self, a: &mut DenseBlock<f64>) -> Result<(), DenseError> {
450 let n = a.square()?;
451 let params = Spec::default();
452 let mut buf = MemBuffer::new(
453 faer::linalg::cholesky::llt::factor::cholesky_in_place_scratch::<f64>(
454 n,
455 Par::Seq,
456 params,
457 ),
458 );
459 faer::linalg::cholesky::llt::factor::cholesky_in_place(
460 view_mut(a),
461 LltRegularization::default(),
462 Par::Seq,
463 MemStack::new(&mut buf),
464 params,
465 )
466 .map(|_| ())
467 .map_err(|e| match e {
468 faer::linalg::cholesky::llt::factor::LltError::NonPositivePivot { index } => {
469 DenseError::NotPositiveDefinite { position: index }
470 }
471 })
472 }
473
474 fn sytrf(
475 &self,
476 a: &mut DenseBlock<f64>,
477 perm: &mut Perm,
478 pivot_2x2: &mut Vec<bool>,
479 ) -> Result<Inertia, DenseError> {
480 let n = a.square()?;
481 let mut params: Spec<LbltParams, f64> = Spec::default();
482 params.config.pivoting = PivotingStrategy::Partial;
483 let mut subdiag = vec![0.0_f64; n];
484 let mut fwd = vec![0_usize; n];
485 let mut inv = vec![0_usize; n];
486 let mut buf = MemBuffer::new(
487 faer::linalg::cholesky::lblt::factor::cholesky_in_place_scratch::<usize, f64>(
488 n,
489 Par::Seq,
490 params,
491 ),
492 );
493 faer::linalg::cholesky::lblt::factor::cholesky_in_place(
494 view_mut(a),
495 ColMut::from_slice_mut(&mut subdiag).as_diagonal_mut(),
496 &mut fwd,
497 &mut inv,
498 Par::Seq,
499 MemStack::new(&mut buf),
500 params,
501 );
502 pivot_2x2.clear();
503 pivot_2x2.resize(n, false);
504 let mut k = 0;
505 while k < n {
506 if k + 1 < n && !is_exact_zero(subdiag[k]) {
507 a.set(k + 1, k, subdiag[k]);
508 pivot_2x2[k] = true;
509 pivot_2x2[k + 1] = true;
510 k += 2;
511 } else {
512 k += 1;
513 }
514 }
515 *perm = Perm::from_fwd(fwd.into_iter().map(crate::perm::idx32).collect())
516 .map_err(|_| DenseError::Dimension)?;
517 Ok(block_inertia(a, pivot_2x2))
518 }
519
520 fn trsm_lower(&self, l: &DenseBlock<f64>, b: &mut DenseBlock<f64>, transpose: bool) {
521 let n = l.square().expect("trsm_lower: L must be square");
522 if transpose {
523 assert_eq!(b.cols, n, "trsm_lower: B must have n columns");
524 faer::linalg::triangular_solve::solve_lower_triangular_in_place(
526 view(l),
527 view_mut(b).transpose_mut(),
528 Par::Seq,
529 );
530 } else {
531 assert_eq!(b.rows, n, "trsm_lower: B must have n rows");
532 faer::linalg::triangular_solve::solve_lower_triangular_in_place(
533 view(l),
534 view_mut(b),
535 Par::Seq,
536 );
537 }
538 }
539
540 fn gemm_sub(&self, a: &DenseBlock<f64>, b: &DenseBlock<f64>, c: &mut DenseBlock<f64>) {
541 assert!(
542 a.cols == b.cols && c.rows == a.rows && c.cols == b.rows,
543 "gemm_sub: dimensions do not agree"
544 );
545 faer::linalg::matmul::matmul(
546 view_mut(c),
547 Accum::Add,
548 view(a),
549 view(b).transpose(),
550 -1.0,
551 Par::Seq,
552 );
553 }
554
555 fn syrk_sub(&self, a: &DenseBlock<f64>, c: &mut DenseBlock<f64>) {
556 let n = c.square().expect("syrk_sub: C must be square");
557 assert_eq!(a.rows, n, "syrk_sub: dimensions do not agree");
558 faer::linalg::matmul::triangular::matmul(
559 view_mut(c),
560 BlockStructure::TriangularLower,
561 Accum::Add,
562 view(a),
563 BlockStructure::Rectangular,
564 view(a).transpose(),
565 BlockStructure::Rectangular,
566 -1.0,
567 Par::Seq,
568 );
569 }
570
571 fn name(&self) -> &'static str {
572 "faer"
573 }
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use crate::real::approx_eq;
581 use proptest::prelude::*;
582
583 fn block(rows: usize, cols: usize, seed: u64) -> DenseBlock {
585 let mut s = seed | 1;
586 let data = (0..rows * cols)
587 .map(|_| {
588 s ^= s << 13;
589 s ^= s >> 7;
590 s ^= s << 17;
591 #[allow(clippy::cast_precision_loss)]
592 let u = (s >> 11) as f64 / (1_u64 << 53) as f64;
593 2.0 * u - 1.0
594 })
595 .collect();
596 DenseBlock { rows, cols, data }
597 }
598
599 fn symmetric(n: usize, seed: u64, spd: bool) -> DenseBlock {
602 let m = block(n, n, seed);
603 let mut a = DenseBlock::zeros(n, n);
604 for j in 0..n {
605 for i in 0..n {
606 #[allow(clippy::cast_precision_loss)]
607 let v = if spd {
608 (0..n).map(|k| m.get(i, k) * m.get(j, k)).sum::<f64>()
609 + if i == j { n as f64 } else { 0.0 }
610 } else {
611 m.get(i, j) + m.get(j, i)
612 };
613 a.set(i, j, v);
614 }
615 }
616 a
617 }
618
619 fn close(x: &DenseBlock, y: &DenseBlock, lower_only: bool) -> bool {
620 (0..x.cols).all(|j| {
621 (if lower_only { j } else { 0 }..x.rows)
622 .all(|i| approx_eq(x.get(i, j), y.get(i, j), 1e-10))
623 })
624 }
625
626 fn sytrf_error(a0: &DenseBlock, f: &DenseBlock, perm: &Perm, two: &[bool]) -> f64 {
628 let n = a0.rows;
629 let mut l = DenseBlock::zeros(n, n);
630 let mut b = DenseBlock::zeros(n, n);
631 let mut k = 0;
632 while k < n {
633 if two[k] {
634 b.set(k, k, f.get(k, k));
635 b.set(k + 1, k + 1, f.get(k + 1, k + 1));
636 b.set(k + 1, k, f.get(k + 1, k));
637 b.set(k, k + 1, f.get(k + 1, k));
638 l.set(k, k, 1.0);
639 l.set(k + 1, k + 1, 1.0);
640 for i in k + 2..n {
641 l.set(i, k, f.get(i, k));
642 l.set(i, k + 1, f.get(i, k + 1));
643 }
644 k += 2;
645 } else {
646 b.set(k, k, f.get(k, k));
647 l.set(k, k, 1.0);
648 for i in k + 1..n {
649 l.set(i, k, f.get(i, k));
650 }
651 k += 1;
652 }
653 }
654 let mut err = 0.0_f64;
655 for i in 0..n {
656 for j in 0..=i {
657 let mut s = 0.0;
658 for p in 0..n {
659 for q in 0..n {
660 s += l.get(i, p) * b.get(p, q) * l.get(j, q);
661 }
662 }
663 let (pi, pj) = (perm.fwd[i] as usize, perm.fwd[j] as usize);
664 let aij = if pi >= pj {
665 a0.get(pi, pj)
666 } else {
667 a0.get(pj, pi)
668 };
669 err = err.max((s - aij).abs());
670 }
671 }
672 err
673 }
674
675 #[test]
676 fn potrf_rejects_indefinite_blocks() {
677 let mut a = DenseBlock {
678 rows: 2,
679 cols: 2,
680 data: vec![1.0, 2.0, 0.0, 1.0],
681 };
682 assert_eq!(
683 NaiveDense.potrf(&mut a.clone()),
684 Err(DenseError::NotPositiveDefinite { position: 1 })
685 );
686 #[cfg(feature = "faer")]
687 assert_eq!(
688 FaerDense.potrf(&mut a),
689 Err(DenseError::NotPositiveDefinite { position: 1 })
690 );
691 let mut r = DenseBlock::<f64>::zeros(2, 3);
692 assert_eq!(NaiveDense.potrf(&mut r), Err(DenseError::Dimension));
693 let mut p = Perm::identity(0);
694 let mut t = Vec::new();
695 assert_eq!(
696 NaiveDense.sytrf(&mut r, &mut p, &mut t),
697 Err(DenseError::Dimension)
698 );
699 a.data.pop();
700 assert_eq!(NaiveDense.potrf(&mut a), Err(DenseError::Dimension));
701 }
702
703 #[test]
704 fn sytrf_finds_2x2_pivots_and_inertia() {
705 let mut a = DenseBlock {
707 rows: 2,
708 cols: 2,
709 data: vec![0.0, 1.0, 0.0, 0.0],
710 };
711 let mut p = Perm::identity(0);
712 let mut two = Vec::new();
713 let s = NaiveDense.sytrf(&mut a, &mut p, &mut two).unwrap();
714 assert_eq!(
715 s,
716 Inertia {
717 positive: 1,
718 negative: 1,
719 zero: 0
720 }
721 );
722 assert_eq!(two, vec![true, true]);
723 let mut z = DenseBlock {
725 rows: 2,
726 cols: 2,
727 data: vec![1.0, 0.0, 0.0, 0.0],
728 };
729 let s = NaiveDense.sytrf(&mut z, &mut p, &mut two).unwrap();
730 assert_eq!(s.zero, 1);
731 }
732
733 proptest! {
734 #![proptest_config(ProptestConfig::with_cases(48))]
735 #[test]
739 fn kernels_agree(n in 1_usize..24, k in 1_usize..9, seed in any::<u64>()) {
740 let spd = symmetric(n, seed, true);
742 let mut x = spd.clone();
743 NaiveDense.potrf(&mut x).unwrap();
744 let mut llt = DenseBlock::zeros(n, n);
745 for j in 0..n {
746 for i in j..n {
747 llt.set(i, j, (0..=j).map(|p| x.get(i, p) * x.get(j, p)).sum());
748 }
749 }
750 prop_assert!(close(&llt, &spd, true));
751 let ind = symmetric(n, seed ^ 0x9e37, false);
753 let mut f = ind.clone();
754 let mut p = Perm::identity(0);
755 let mut two = Vec::new();
756 let s = NaiveDense.sytrf(&mut f, &mut p, &mut two).unwrap();
757 prop_assert_eq!(s.positive + s.negative + s.zero, n);
758 prop_assert!(sytrf_error(&ind, &f, &p, &two) <= 1e-9);
759 let b = block(k, n, seed ^ 0x7);
761 let mut t1 = b.clone();
762 NaiveDense.trsm_lower(&x, &mut t1, true);
763 let b2 = block(n, k, seed ^ 0xb);
764 let mut t2 = b2.clone();
765 NaiveDense.trsm_lower(&x, &mut t2, false);
766 let a3 = block(k, n, seed ^ 0xd);
767 let mut c3 = block(k, k, seed ^ 0x11);
768 let c3_0 = c3.clone();
769 NaiveDense.gemm_sub(&a3, &b, &mut c3);
770 for i in 0..k {
771 for j in 0..k {
772 let v: f64 = c3_0.get(i, j) - (0..n).map(|p| a3.get(i, p) * b.get(j, p)).sum::<f64>();
773 prop_assert!(approx_eq(v, c3.get(i, j), 1e-12));
774 }
775 }
776 let mut c4 = symmetric(k, seed ^ 0x13, false);
777 let c4_0 = c4.clone();
778 NaiveDense.syrk_sub(&a3, &mut c4);
779 #[cfg(feature = "faer")]
780 {
781 let mut y = spd.clone();
782 FaerDense.potrf(&mut y).unwrap();
783 prop_assert!(close(&x, &y, true));
784 let mut g = ind.clone();
785 let mut q = Perm::identity(0);
786 let mut two_f = Vec::new();
787 let sf = FaerDense.sytrf(&mut g, &mut q, &mut two_f).unwrap();
788 prop_assert_eq!(s, sf);
789 prop_assert!(sytrf_error(&ind, &g, &q, &two_f) <= 1e-9);
790 let mut u1 = b.clone();
791 FaerDense.trsm_lower(&x, &mut u1, true);
792 prop_assert!(close(&t1, &u1, false));
793 let mut u2 = b2.clone();
794 FaerDense.trsm_lower(&x, &mut u2, false);
795 prop_assert!(close(&t2, &u2, false));
796 let mut d3 = c3_0.clone();
797 FaerDense.gemm_sub(&a3, &b, &mut d3);
798 prop_assert!(close(&c3, &d3, false));
799 let mut d4 = c4_0.clone();
800 FaerDense.syrk_sub(&a3, &mut d4);
801 prop_assert!(close(&c4, &d4, true));
802 prop_assert!((0..k).all(|j| (0..j).all(|i| approx_eq(d4.get(i, j), c4_0.get(i, j), 0.0))));
804 }
805 prop_assert!((0..k).all(|j| (0..j).all(|i| approx_eq(c4.get(i, j), c4_0.get(i, j), 0.0))));
806 for i in 0..k {
808 for j in 0..n {
809 let v: f64 = (0..=j).map(|p| t1.get(i, p) * x.get(j, p)).sum();
810 prop_assert!(approx_eq(v, b.get(i, j), 1e-9));
811 }
812 }
813 }
814 }
815}