1use crate::error::{LinalgError, LinalgResult};
18use crate::gpu_gemm::gemm::{gemm, GemmConfig};
19use scirs2_core::ndarray::{Array2, Axis};
20
21#[derive(Debug, Clone, Copy, PartialEq)]
25#[non_exhaustive]
26pub enum PrecisionMode {
27 Auto,
29 AlwaysF32,
31 AlwaysF64,
33 Mixed,
35}
36
37#[derive(Clone, Debug)]
39pub struct PrecisionDispatchConfig {
40 pub mode: PrecisionMode,
42 pub condition_threshold: f64,
45 pub estimate_condition: bool,
48 pub max_refinement_iters: usize,
51 pub refinement_tol: f64,
54}
55
56impl Default for PrecisionDispatchConfig {
57 fn default() -> Self {
58 Self {
59 mode: PrecisionMode::Auto,
60 condition_threshold: 1e6,
61 estimate_condition: true,
62 max_refinement_iters: 3,
63 refinement_tol: 1e-10,
64 }
65 }
66}
67
68pub struct DispatchResult {
70 pub result: Array2<f64>,
72 pub precision_used: String,
74 pub condition_estimate: Option<f64>,
76 pub numerical_error_estimate: Option<f64>,
78}
79
80pub fn adaptive_gemm(
105 a: &Array2<f64>,
106 b: &Array2<f64>,
107 config: &PrecisionDispatchConfig,
108) -> LinalgResult<DispatchResult> {
109 let cond_est = if config.estimate_condition && a.nrows() == a.ncols() {
112 condition_number_estimate_1norm(a).ok()
113 } else {
114 None
115 };
116
117 let use_f64 = match config.mode {
118 PrecisionMode::AlwaysF64 => true,
119 PrecisionMode::AlwaysF32 => false,
120 PrecisionMode::Mixed => false,
121 PrecisionMode::Auto => cond_est.is_none_or(|c| c > config.condition_threshold),
122 };
123
124 let (result, precision_used) = if use_f64 {
125 let r = gemm(a, b, None, &GemmConfig::default())?;
126 (r, "f64".to_string())
127 } else {
128 let r = gemm_f32_accum_f64(a, b);
129 let label = match config.mode {
130 PrecisionMode::Mixed => "f32-compute/f64-accum",
131 _ => "f32-approx",
132 };
133 (r, label.to_string())
134 };
135
136 let numerical_error_estimate = cond_est.map(|cond| {
138 let res_norm: f64 = result
139 .map(|v| v.abs())
140 .sum_axis(Axis(0))
141 .fold(0.0_f64, |acc, &v| acc.max(v));
142 let eps = if use_f64 {
143 f64::EPSILON
144 } else {
145 f32::EPSILON as f64
146 };
147 eps * cond * res_norm
148 });
149
150 Ok(DispatchResult {
151 result,
152 precision_used,
153 condition_estimate: cond_est,
154 numerical_error_estimate,
155 })
156}
157
158pub fn gemm_f32_accum_f64(a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
180 let (m, k) = (a.nrows(), a.ncols());
181 let n = b.ncols();
182
183 let mut c = Array2::<f64>::zeros((m, n));
184
185 for i in 0..m {
186 for j in 0..n {
187 let mut acc = 0.0_f64;
188 for p in 0..k {
189 let a_f32 = a[[i, p]] as f32;
191 let b_f32 = b[[p, j]] as f32;
192 acc += (a_f32 * b_f32) as f64;
193 }
194 c[[i, j]] = acc;
195 }
196 }
197 c
198}
199
200pub fn condition_number_estimate_1norm(a: &Array2<f64>) -> LinalgResult<f64> {
221 let n = a.nrows();
222 if a.ncols() != n {
223 return Err(LinalgError::DimensionError(
224 "condition_number_estimate_1norm requires a square matrix".to_string(),
225 ));
226 }
227 if n == 0 {
228 return Err(LinalgError::DimensionError(
229 "condition_number_estimate_1norm: empty matrix".to_string(),
230 ));
231 }
232
233 let norm_a = matrix_1norm(a);
235
236 if norm_a == 0.0 {
237 return Ok(f64::INFINITY);
238 }
239
240 let (lu, perm) = lu_factor(a)?;
242
243 let norm_inv = if n <= 64 {
248 let mut max_col_norm = 0.0_f64;
250 for j in 0..n {
251 let mut ej = vec![0.0_f64; n];
252 ej[j] = 1.0;
253 let x = lu_solve(&lu, &perm, &ej)?;
254 let col_norm: f64 = x.iter().map(|xi| xi.abs()).sum();
255 if col_norm > max_col_norm {
256 max_col_norm = col_norm;
257 }
258 }
259 max_col_norm
260 } else {
261 let mut best = 0.0_f64;
263
264 let v_ones: Vec<f64> = vec![1.0 / n as f64; n];
266 let est = estimate_ainv_norm_1norm(&lu, &perm, &v_ones, n)?;
267 best = best.max(est);
268
269 let v_alt: Vec<f64> = (0..n)
271 .map(|i| {
272 if i % 2 == 0 {
273 1.0 / n as f64
274 } else {
275 -1.0 / n as f64
276 }
277 })
278 .collect();
279 let est2 = estimate_ainv_norm_1norm(&lu, &perm, &v_alt, n)?;
280 best = best.max(est2);
281 best
282 };
283
284 Ok(norm_a * norm_inv)
285}
286
287pub fn mixed_precision_solve(
313 a: &Array2<f64>,
314 b: &Array2<f64>,
315 config: &PrecisionDispatchConfig,
316) -> LinalgResult<DispatchResult> {
317 let n = a.nrows();
318 if a.ncols() != n {
319 return Err(LinalgError::DimensionError(
320 "mixed_precision_solve requires a square coefficient matrix".to_string(),
321 ));
322 }
323 if b.nrows() != n {
324 return Err(LinalgError::DimensionError(format!(
325 "mixed_precision_solve: b has {} rows, expected {n}",
326 b.nrows()
327 )));
328 }
329
330 let p = b.ncols();
331
332 let cond_est = if config.estimate_condition {
334 condition_number_estimate_1norm(a).ok()
335 } else {
336 None
337 };
338
339 let (lu, perm) = lu_factor(a)?;
341
342 let mut x = Array2::<f64>::zeros((n, p));
344 for j in 0..p {
345 let rhs: Vec<f64> = (0..n).map(|i| b[[i, j]]).collect();
346 let sol = lu_solve(&lu, &perm, &rhs)?;
347 for i in 0..n {
348 x[[i, j]] = sol[i];
349 }
350 }
351
352 let max_iters = config.max_refinement_iters;
354 let tol = config.refinement_tol;
355
356 for _iter in 0..max_iters {
357 let ax = gemm(a, &x, None, &GemmConfig::default())?;
359 let mut max_res = 0.0_f64;
360 let mut r = Array2::<f64>::zeros((n, p));
361 for i in 0..n {
362 for j in 0..p {
363 let res_ij = b[[i, j]] - ax[[i, j]];
364 r[[i, j]] = res_ij;
365 max_res = max_res.max(res_ij.abs());
366 }
367 }
368
369 if max_res < tol {
370 break;
371 }
372
373 for j in 0..p {
375 let rhs: Vec<f64> = (0..n).map(|i| r[[i, j]]).collect();
376 let dx = lu_solve(&lu, &perm, &rhs)?;
377 for i in 0..n {
378 x[[i, j]] += dx[i];
379 }
380 }
381 }
382
383 let ax_final = gemm(a, &x, None, &GemmConfig::default())?;
385 let mut final_res: f64 = 0.0_f64;
386 for i in 0..n {
387 for j in 0..p {
388 let r = (b[[i, j]] - ax_final[[i, j]]).abs();
389 if r > final_res {
390 final_res = r;
391 }
392 }
393 }
394
395 Ok(DispatchResult {
396 result: x,
397 precision_used: "f64-lu-iterative-refinement".to_string(),
398 condition_estimate: cond_est,
399 numerical_error_estimate: Some(final_res),
400 })
401}
402
403fn matrix_1norm(a: &Array2<f64>) -> f64 {
407 let n = a.ncols();
408 (0..n)
409 .map(|j| (0..a.nrows()).map(|i| a[[i, j]].abs()).sum::<f64>())
410 .fold(0.0_f64, f64::max)
411}
412
413fn lu_factor(a: &Array2<f64>) -> LinalgResult<(Vec<f64>, Vec<usize>)> {
418 let n = a.nrows();
419 let mut lu: Vec<f64> = a.iter().copied().collect();
420 let mut perm: Vec<usize> = (0..n).collect();
421
422 for k in 0..n {
423 let pivot_row = (k..n)
425 .max_by(|&i, &j| {
426 lu[i * n + k]
427 .abs()
428 .partial_cmp(&lu[j * n + k].abs())
429 .unwrap_or(std::cmp::Ordering::Equal)
430 })
431 .ok_or_else(|| LinalgError::ComputationError("LU pivot search failed".to_string()))?;
432
433 if lu[pivot_row * n + k].abs() < f64::EPSILON * 1e3 {
434 return Err(LinalgError::SingularMatrixError(
435 "Matrix is numerically singular during LU factorisation".to_string(),
436 ));
437 }
438
439 if pivot_row != k {
441 perm.swap(k, pivot_row);
442 for col in 0..n {
443 lu.swap(k * n + col, pivot_row * n + col);
444 }
445 }
446
447 let pivot = lu[k * n + k];
448 for i in (k + 1)..n {
449 let factor = lu[i * n + k] / pivot;
450 lu[i * n + k] = factor;
451 for j in (k + 1)..n {
452 let update = factor * lu[k * n + j];
453 lu[i * n + j] -= update;
454 }
455 }
456 }
457
458 Ok((lu, perm))
459}
460
461fn lu_solve(lu: &[f64], perm: &[usize], b: &[f64]) -> LinalgResult<Vec<f64>> {
463 let n = perm.len();
464 let mut pb: Vec<f64> = perm.iter().map(|&i| b[i]).collect();
466
467 for k in 0..n {
469 for i in (k + 1)..n {
470 pb[i] -= lu[i * n + k] * pb[k];
471 }
472 }
473
474 for k in (0..n).rev() {
476 let diag = lu[k * n + k];
477 if diag.abs() < f64::EPSILON * 1e3 {
478 return Err(LinalgError::SingularMatrixError(
479 "Singular diagonal entry during back substitution".to_string(),
480 ));
481 }
482 pb[k] /= diag;
483 for i in 0..k {
484 pb[i] -= lu[i * n + k] * pb[k];
485 }
486 }
487
488 Ok(pb)
489}
490
491fn estimate_ainv_norm_1norm(lu: &[f64], perm: &[usize], v: &[f64], n: usize) -> LinalgResult<f64> {
493 let x = lu_solve(lu, perm, v)?;
495 let sign_x: Vec<f64> = x
497 .iter()
498 .map(|&xi| if xi >= 0.0 { 1.0 } else { -1.0 })
499 .collect();
500 let z = lu_solve_transpose(lu, perm, &sign_x, n)?;
502
503 let norm_x: f64 = x.iter().map(|xi| xi.abs()).sum();
505 let norm_v: f64 = v.iter().map(|vi| vi.abs()).sum();
506 let norm_z_inf: f64 = z.iter().map(|zi| zi.abs()).fold(0.0_f64, f64::max);
507
508 if norm_z_inf <= 1.0 {
510 return Ok(norm_x / norm_v.max(f64::EPSILON));
511 }
512
513 let j = z
515 .iter()
516 .enumerate()
517 .max_by(|(_, a), (_, b)| {
518 a.abs()
519 .partial_cmp(&b.abs())
520 .unwrap_or(std::cmp::Ordering::Equal)
521 })
522 .map(|(idx, _)| idx)
523 .unwrap_or(0);
524
525 let mut ej = vec![0.0_f64; n];
526 ej[j] = 1.0;
527 let x2 = lu_solve(lu, perm, &ej)?;
528 let norm_x2: f64 = x2.iter().map(|xi| xi.abs()).sum();
529 Ok(norm_x2)
530}
531
532fn lu_solve_transpose(lu: &[f64], perm: &[usize], b: &[f64], n: usize) -> LinalgResult<Vec<f64>> {
537 let mut y = b.to_vec();
539 for k in 0..n {
540 let diag = lu[k * n + k];
541 if diag.abs() < f64::EPSILON * 1e3 {
542 return Err(LinalgError::SingularMatrixError(
543 "Singular diagonal in transposed back-substitution".to_string(),
544 ));
545 }
546 y[k] /= diag;
547 for i in (k + 1)..n {
548 y[i] -= lu[k * n + i] * y[k];
549 }
550 }
551
552 for k in (0..n).rev() {
554 for i in 0..k {
555 y[i] -= lu[k * n + i] * y[k];
556 }
557 }
558
559 let mut z = vec![0.0_f64; n];
561 for (i, &pi) in perm.iter().enumerate() {
562 z[pi] = y[i];
563 }
564
565 Ok(z)
566}
567
568#[cfg(test)]
571mod tests {
572 use super::*;
573 use approx::assert_abs_diff_eq;
574 use scirs2_core::ndarray::{array, Array2};
575
576 #[test]
577 fn test_adaptive_gemm_well_conditioned() {
578 let a = Array2::<f64>::eye(3);
579 let b = Array2::<f64>::from_shape_fn((3, 3), |(i, j)| (i + j + 1) as f64);
580 let config = PrecisionDispatchConfig {
581 condition_threshold: 1e6,
582 ..Default::default()
583 };
584 let res = adaptive_gemm(&a, &b, &config).unwrap();
585 assert_abs_diff_eq!(res.result[[0, 0]], b[[0, 0]], epsilon = 1e-4);
587 assert_abs_diff_eq!(res.result[[2, 2]], b[[2, 2]], epsilon = 1e-4);
588 assert!(res.condition_estimate.unwrap() < 10.0);
590 }
591
592 #[test]
593 fn test_adaptive_gemm_ill_conditioned() {
594 let a = array![[1.0_f64, 1.0], [1.0, 1.0 + 1e-8]];
596 let b = array![[1.0_f64, 0.0], [0.0, 1.0]];
597 let config = PrecisionDispatchConfig {
598 mode: PrecisionMode::Auto,
599 condition_threshold: 1e4, estimate_condition: true,
601 ..Default::default()
602 };
603 let res = adaptive_gemm(&a, &b, &config).unwrap();
604 let cond = res.condition_estimate.unwrap();
606 assert!(cond > 1e4, "Expected cond > 1e4, got {cond}");
607 assert_eq!(res.precision_used, "f64");
608 }
609
610 #[test]
611 fn test_adaptive_gemm_always_f32() {
612 let a = array![[2.0_f64, 0.0], [0.0, 2.0]];
613 let b = array![[1.0_f64, 0.0], [0.0, 1.0]];
614 let config = PrecisionDispatchConfig {
615 mode: PrecisionMode::AlwaysF32,
616 estimate_condition: false,
617 ..Default::default()
618 };
619 let res = adaptive_gemm(&a, &b, &config).unwrap();
620 assert_abs_diff_eq!(res.result[[0, 0]], 2.0, epsilon = 1e-4);
622 assert!(res.precision_used.contains("f32"));
623 }
624
625 #[test]
626 fn test_adaptive_gemm_always_f64() {
627 let a = array![[3.0_f64, 1.0], [1.0, 3.0]];
628 let b = array![[1.0_f64, 0.0], [0.0, 1.0]];
629 let config = PrecisionDispatchConfig {
630 mode: PrecisionMode::AlwaysF64,
631 ..Default::default()
632 };
633 let res = adaptive_gemm(&a, &b, &config).unwrap();
634 assert_abs_diff_eq!(res.result[[0, 0]], 3.0, epsilon = 1e-12);
635 assert_eq!(res.precision_used, "f64");
636 }
637
638 #[test]
639 fn test_gemm_f32_accum_f64_identity() {
640 let a = Array2::<f64>::eye(4);
641 let b = Array2::<f64>::from_shape_fn((4, 4), |(i, j)| (i * 4 + j) as f64);
642 let c = gemm_f32_accum_f64(&a, &b);
643 for i in 0..4 {
644 for j in 0..4 {
645 assert_abs_diff_eq!(c[[i, j]], b[[i, j]], epsilon = 1e-4);
646 }
647 }
648 }
649
650 #[test]
651 fn test_gemm_f32_accum_f64_close_to_f64() {
652 let a = Array2::<f64>::from_shape_fn((10, 10), |(i, j)| ((i + j) as f64) * 0.1);
653 let b = Array2::<f64>::from_shape_fn((10, 10), |(i, j)| ((i * j + 1) as f64) * 0.1);
654 let c_f64 = gemm(&a, &b, None, &GemmConfig::default()).unwrap();
655 let c_f32 = gemm_f32_accum_f64(&a, &b);
656 for i in 0..10 {
657 for j in 0..10 {
658 assert_abs_diff_eq!(c_f32[[i, j]], c_f64[[i, j]], epsilon = 1e-3);
660 }
661 }
662 }
663
664 #[test]
665 fn test_condition_estimate_identity() {
666 let eye = Array2::<f64>::eye(5);
667 let cond = condition_number_estimate_1norm(&eye).unwrap();
668 assert_abs_diff_eq!(cond, 1.0, epsilon = 1e-10);
669 }
670
671 #[test]
672 fn test_condition_estimate_diagonal() {
673 let a = array![[1.0_f64, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 10.0]];
675 let cond = condition_number_estimate_1norm(&a).unwrap();
676 assert!((9.0..=11.0).contains(&cond), "Expected ≈10, got {cond}");
678 }
679
680 #[test]
681 fn test_condition_estimate_non_square_error() {
682 let a = Array2::<f64>::zeros((3, 4));
683 assert!(condition_number_estimate_1norm(&a).is_err());
684 }
685
686 #[test]
687 fn test_mixed_precision_solve_2x2() {
688 let a = array![[2.0_f64, 1.0], [1.0, 3.0]];
689 let b = array![[5.0_f64], [10.0]];
690 let config = PrecisionDispatchConfig::default();
691 let res = mixed_precision_solve(&a, &b, &config).unwrap();
692 assert_abs_diff_eq!(res.result[[0, 0]], 1.0, epsilon = 1e-8);
694 assert_abs_diff_eq!(res.result[[1, 0]], 3.0, epsilon = 1e-8);
695 }
696
697 #[test]
698 fn test_mixed_precision_solve_identity() {
699 let a = Array2::<f64>::eye(4);
700 let b = Array2::<f64>::from_shape_fn((4, 2), |(i, j)| (i + j) as f64);
701 let config = PrecisionDispatchConfig::default();
702 let res = mixed_precision_solve(&a, &b, &config).unwrap();
703 for i in 0..4 {
705 for j in 0..2 {
706 assert_abs_diff_eq!(res.result[[i, j]], b[[i, j]], epsilon = 1e-10);
707 }
708 }
709 }
710
711 #[test]
712 fn test_mixed_precision_solve_vs_direct() {
713 let a = array![[4.0_f64, 1.0, 0.0], [1.0, 3.0, 1.0], [0.0, 1.0, 2.0]];
715 let x_true = array![[1.0_f64], [2.0], [3.0]];
716 let b = gemm(&a, &x_true, None, &GemmConfig::default()).unwrap();
718 let config = PrecisionDispatchConfig::default();
719 let res = mixed_precision_solve(&a, &b, &config).unwrap();
720 for i in 0..3 {
721 assert_abs_diff_eq!(res.result[[i, 0]], x_true[[i, 0]], epsilon = 1e-8);
722 }
723 }
724
725 #[test]
726 fn test_mixed_precision_solve_non_square_error() {
727 let a = Array2::<f64>::zeros((3, 4));
728 let b = Array2::<f64>::zeros((3, 1));
729 let config = PrecisionDispatchConfig::default();
730 assert!(mixed_precision_solve(&a, &b, &config).is_err());
731 }
732
733 #[test]
734 fn test_adaptive_gemm_shape() {
735 let a = Array2::<f64>::from_shape_fn((5, 7), |(i, j)| (i + j) as f64 * 0.1);
736 let b = Array2::<f64>::from_shape_fn((7, 3), |(i, j)| (i * j + 1) as f64 * 0.1);
737 let config = PrecisionDispatchConfig::default();
738 let res = adaptive_gemm(&a, &b, &config).unwrap();
739 assert_eq!(res.result.shape(), &[5, 3]);
740 }
741
742 #[test]
743 fn test_dispatch_result_fields_populated() {
744 let a = Array2::<f64>::eye(3);
745 let b = Array2::<f64>::eye(3);
746 let config = PrecisionDispatchConfig {
747 estimate_condition: true,
748 ..Default::default()
749 };
750 let res = adaptive_gemm(&a, &b, &config).unwrap();
751 assert!(res.condition_estimate.is_some());
752 assert!(!res.precision_used.is_empty());
753 }
754}