1use crate::error::{IrisError, Result};
2use crate::features::{BFMatcher, FeatureDetector, FeatureType};
3use crate::image::Image;
4use burn::tensor::backend::Backend;
5
6pub struct Stitcher;
11
12impl Stitcher {
13 pub fn stitch<B: Backend>(&self, images: &[Image<B>]) -> Result<Image<B>> {
19 if images.is_empty() {
20 return Err(IrisError::InvalidParameter(
21 "Images list cannot be empty".into(),
22 ));
23 }
24 if images.len() == 1 {
25 return Ok(images[0].clone());
26 }
27
28 let mut homographies: Vec<[[f64; 3]; 3]> = Vec::new();
30 for i in 0..images.len() - 1 {
31 let h = compute_homography(&images[i], &images[i + 1])?;
32 homographies.push(h);
33 }
34
35 let mut accumulated: Vec<[[f64; 3]; 3]> =
38 vec![[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]];
39 for h in &homographies {
40 let prev = accumulated.last().unwrap();
41 accumulated.push(multiply_3x3(prev, h));
42 }
43
44 let mut min_x = 0.0_f64;
46 let mut min_y = 0.0_f64;
47 let mut max_x = 0.0_f64;
48 let mut max_y = 0.0_f64;
49
50 for (idx, img) in images.iter().enumerate() {
51 let h = img.height() as f64;
52 let w = img.width() as f64;
53 let corners = [(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)];
54
55 let h_mat = &accumulated[idx];
56 for &(cx, cy) in &corners {
57 let denom = h_mat[2][0] * cx + h_mat[2][1] * cy + h_mat[2][2];
58 if denom.abs() > 1e-10 {
59 let wx = (h_mat[0][0] * cx + h_mat[0][1] * cy + h_mat[0][2]) / denom;
60 let wy = (h_mat[1][0] * cx + h_mat[1][1] * cy + h_mat[1][2]) / denom;
61 min_x = min_x.min(wx);
62 min_y = min_y.min(wy);
63 max_x = max_x.max(wx);
64 max_y = max_y.max(wy);
65 }
66 }
67 }
68
69 let canvas_w = (max_x - min_x).ceil() as usize;
71 let canvas_h = (max_y - min_y).ceil() as usize;
72 let tx = -min_x;
73 let ty = -min_y;
74
75 let t = [[1.0, 0.0, tx], [0.0, 1.0, ty], [0.0, 0.0, 1.0]];
77
78 let mut weight_canvas = vec![0.0f32; canvas_h * canvas_w];
80 let mut out_vals = vec![0.0f32; 3 * canvas_h * canvas_w];
81
82 for (idx, img) in images.iter().enumerate() {
83 let d = img.tensor.dims();
84 let img_h = d[1];
85 let img_w = d[2];
86 let data = img.tensor.clone().into_data();
87 let flat: Vec<f32> = data.iter::<f32>().collect();
88
89 let h_final = multiply_3x3(&t, &accumulated[idx]);
91
92 let h_inv = invert_3x3(&h_final).ok_or_else(|| {
94 IrisError::InvalidParameter(format!("Singular homography for image pair {}", idx))
95 })?;
96
97 for dy in 0..canvas_h {
98 for dx in 0..canvas_w {
99 let denom = h_inv[2][0] * dx as f64 + h_inv[2][1] * dy as f64 + h_inv[2][2];
100 if denom.abs() < 1e-10 {
101 continue;
102 }
103 let sx =
104 (h_inv[0][0] * dx as f64 + h_inv[0][1] * dy as f64 + h_inv[0][2]) / denom;
105 let sy =
106 (h_inv[1][0] * dx as f64 + h_inv[1][1] * dy as f64 + h_inv[1][2]) / denom;
107
108 let sx_r = sx.round() as isize;
109 let sy_r = sy.round() as isize;
110
111 if sx_r >= 0 && sx_r < img_w as isize && sy_r >= 0 && sy_r < img_h as isize {
112 let src_x = sx_r as usize;
113 let src_y = sy_r as usize;
114
115 let cx = src_x as f64 - img_w as f64 / 2.0;
117 let cy = src_y as f64 - img_h as f64 / 2.0;
118 let max_r = (img_w as f64 + img_h as f64) / 4.0;
119 let dist = (cx * cx + cy * cy).sqrt() / max_r;
120 let w = (1.0 - dist).max(0.0) as f32;
121
122 let ci = dy * canvas_w + dx;
123 for ch in 0..3 {
124 let src_idx = ch * img_h * img_w + src_y * img_w + src_x;
125 out_vals[ch * canvas_h * canvas_w + ci] += flat[src_idx] * w;
126 }
127 weight_canvas[ci] += w;
128 }
129 }
130 }
131 }
132
133 for ci in 0..canvas_h * canvas_w {
135 if weight_canvas[ci] > 1e-10 {
136 for ch in 0..3 {
137 let idx = ch * canvas_h * canvas_w + ci;
138 out_vals[idx] = (out_vals[idx] / weight_canvas[ci]).clamp(0.0, 1.0);
139 }
140 }
141 }
142
143 let device = images[0].tensor.device();
144 let data = burn::tensor::TensorData::new(out_vals, [3, canvas_h, canvas_w]);
145 let tensor = burn::tensor::Tensor::<B, 3>::from_data(data, &device);
146 Ok(Image::new(tensor))
147 }
148}
149
150fn compute_homography<B: Backend>(img1: &Image<B>, img2: &Image<B>) -> Result<[[f64; 3]; 3]> {
152 let detector = FeatureDetector::new(FeatureType::ORB).with_max_features(500);
153
154 let kps1 = detector.detect(img1)?;
155 let kps2 = detector.detect(img2)?;
156
157 if kps1.len() < 4 || kps2.len() < 4 {
158 return Ok([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
159 }
160
161 let desc1 = detector.compute(img1, &kps1)?;
162 let desc2 = detector.compute(img2, &kps2)?;
163
164 let matcher = BFMatcher;
165 let matches = matcher.match_descriptors(&desc1, &desc2)?;
166
167 let median_dist = {
169 let mut dists: Vec<f32> = matches.iter().map(|m| m.distance).collect();
170 dists.sort_by(|a, b| a.partial_cmp(b).unwrap());
171 dists[dists.len() / 2]
172 };
173 let threshold = median_dist * 1.5;
174
175 let good_matches: Vec<_> = matches.iter().filter(|m| m.distance <= threshold).collect();
176
177 if good_matches.len() < 4 {
178 return Err(IrisError::InvalidParameter(
179 "Not enough good matches for homography estimation".into(),
180 ));
181 }
182
183 let pts1: Vec<(f64, f64)> = good_matches
185 .iter()
186 .map(|m| {
187 let kp = &kps1[m.query_idx];
188 (kp.pt.x, kp.pt.y)
189 })
190 .collect();
191 let pts2: Vec<(f64, f64)> = good_matches
192 .iter()
193 .map(|m| {
194 let kp = &kps2[m.train_idx];
195 (kp.pt.x, kp.pt.y)
196 })
197 .collect();
198
199 let h = ransac_homography(&pts1, &pts2, 1000, 5.0)?;
201 Ok(h)
202}
203
204fn ransac_homography(
206 src: &[(f64, f64)],
207 dst: &[(f64, f64)],
208 max_iterations: usize,
209 inlier_threshold: f64,
210) -> Result<[[f64; 3]; 3]> {
211 let n = src.len();
212 if n < 4 {
213 return Err(IrisError::InvalidParameter(
214 "Need at least 4 point pairs for homography".into(),
215 ));
216 }
217
218 let mut best_h = [[0.0f64; 3]; 3];
219 let mut best_inliers = 0;
220
221 let mut seed: u64 = 0xDEAD_BEEF_CAFE_BABE;
223
224 for _ in 0..max_iterations {
225 let mut indices = [0usize; 4];
227 let mut used = vec![false; n];
228 let mut valid = true;
229 for i in 0..4 {
230 loop {
231 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
232 let idx = ((seed >> 33) as usize) % n;
233 if !used[idx] {
234 used[idx] = true;
235 indices[i] = idx;
236 break;
237 }
238 }
239 if indices[i] >= n {
240 valid = false;
241 break;
242 }
243 }
244 if !valid {
245 continue;
246 }
247
248 let sample_src: Vec<(f64, f64)> = indices.iter().map(|&i| src[i]).collect();
249 let sample_dst: Vec<(f64, f64)> = indices.iter().map(|&i| dst[i]).collect();
250
251 if let Ok(h_candidate) = compute_homography_dlt(&sample_src, &sample_dst) {
252 let mut inlier_count = 0;
254 for (s, d) in src.iter().zip(dst.iter()) {
255 let projected = apply_homography(&h_candidate, s.0, s.1);
256 let dx = projected.0 - d.0;
257 let dy = projected.1 - d.1;
258 if (dx * dx + dy * dy).sqrt() < inlier_threshold {
259 inlier_count += 1;
260 }
261 }
262
263 if inlier_count > best_inliers {
264 best_inliers = inlier_count;
265 best_h = h_candidate;
266 }
267 }
268 }
269
270 if best_inliers == 0 {
271 return Err(IrisError::Generic(
272 "RANSAC homography failed to find any inliers".into(),
273 ));
274 }
275
276 let inlier_src: Vec<(f64, f64)> = src
278 .iter()
279 .zip(dst.iter())
280 .filter_map(|(s, d)| {
281 let projected = apply_homography(&best_h, s.0, s.1);
282 let dx = projected.0 - d.0;
283 let dy = projected.1 - d.1;
284 if (dx * dx + dy * dy).sqrt() < inlier_threshold {
285 Some(*s)
286 } else {
287 None
288 }
289 })
290 .collect();
291 let inlier_dst: Vec<(f64, f64)> = src
292 .iter()
293 .zip(dst.iter())
294 .filter_map(|(s, d)| {
295 let projected = apply_homography(&best_h, s.0, s.1);
296 let dx = projected.0 - d.0;
297 let dy = projected.1 - d.1;
298 if (dx * dx + dy * dy).sqrt() < inlier_threshold {
299 Some(*d)
300 } else {
301 None
302 }
303 })
304 .collect();
305
306 if inlier_src.len() >= 4 {
307 compute_homography_dlt(&inlier_src, &inlier_dst)
308 } else {
309 Ok(best_h)
310 }
311}
312
313fn compute_homography_dlt(src: &[(f64, f64)], dst: &[(f64, f64)]) -> Result<[[f64; 3]; 3]> {
316 let n = src.len();
317 if n < 4 {
318 return Err(IrisError::InvalidParameter(
319 "DLT requires at least 4 point pairs".into(),
320 ));
321 }
322
323 let (src_norm, t_src) = normalize_points(src);
325 let (dst_norm, t_dst) = normalize_points(dst);
326
327 let mut a = vec![0.0f64; 2 * n * 9];
329 for i in 0..n {
330 let (x, y) = src_norm[i];
331 let (xp, yp) = dst_norm[i];
332 a[2 * i * 9] = -x;
334 a[(2 * i) * 9 + 1] = -y;
335 a[(2 * i) * 9 + 2] = -1.0;
336 a[(2 * i) * 9 + 6] = xp * x;
337 a[(2 * i) * 9 + 7] = xp * y;
338 a[(2 * i) * 9 + 8] = xp;
339 a[(2 * i + 1) * 9 + 3] = -x;
341 a[(2 * i + 1) * 9 + 4] = -y;
342 a[(2 * i + 1) * 9 + 5] = -1.0;
343 a[(2 * i + 1) * 9 + 6] = yp * x;
344 a[(2 * i + 1) * 9 + 7] = yp * y;
345 a[(2 * i + 1) * 9 + 8] = yp;
346 }
347
348 let ata = multiply_at_a(&a, 2 * n, 9);
350
351 let h_vec = null_space_via_elimination(&ata, 9)?;
354
355 let mut h_norm = [
356 [h_vec[0], h_vec[1], h_vec[2]],
357 [h_vec[3], h_vec[4], h_vec[5]],
358 [h_vec[6], h_vec[7], h_vec[8]],
359 ];
360
361 let t_dst_inv = invert_3x3(&t_dst).ok_or_else(|| {
363 IrisError::InvalidParameter("Singular destination normalization transform".into())
364 })?;
365 let h_denorm = multiply_3x3(&t_dst_inv, &h_norm);
366 h_norm = multiply_3x3(&h_denorm, &t_src);
367
368 Ok(h_norm)
369}
370
371fn null_space_via_elimination(m: &[f64], n: usize) -> Result<Vec<f64>> {
374 let mut mat = vec![0.0f64; n * n];
376 mat.copy_from_slice(m);
377
378 for col in 0..n {
380 let mut max_val = mat[col * n + col].abs();
382 let mut max_row = col;
383 for row in (col + 1)..n {
384 if mat[row * n + col].abs() > max_val {
385 max_val = mat[row * n + col].abs();
386 max_row = row;
387 }
388 }
389 if max_val < 1e-12 {
390 continue;
391 }
392 for k in 0..n {
394 mat.swap(col * n + k, max_row * n + k);
395 }
396
397 let pivot = mat[col * n + col];
399 for row in (col + 1)..n {
400 let factor = mat[row * n + col] / pivot;
401 for k in col..n {
402 mat[row * n + k] -= factor * mat[col * n + k];
403 }
404 }
405 }
406
407 let mut free_col = 0;
410 let mut min_pivot = f64::MAX;
411 for col in 0..n {
412 let piv = mat[col * n + col].abs();
413 if piv < min_pivot {
414 min_pivot = piv;
415 free_col = col;
416 }
417 }
418
419 let mut v = vec![0.0f64; n];
422 v[free_col] = 1.0;
423
424 for row in (0..n).rev() {
426 if row == free_col {
427 continue;
428 }
429 let mut sum = 0.0;
431 for j in (row + 1)..n {
432 sum += mat[row * n + j] * v[j];
433 }
434 if mat[row * n + row].abs() > 1e-15 {
435 v[row] = -sum / mat[row * n + row];
436 }
437 }
438
439 let norm: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
441 if norm > 1e-15 {
442 for x in &mut v {
443 *x /= norm;
444 }
445 }
446
447 Ok(v)
448}
449
450fn normalize_points(points: &[(f64, f64)]) -> (Vec<(f64, f64)>, [[f64; 3]; 3]) {
452 let n = points.len() as f64;
453 let cx = points.iter().map(|p| p.0).sum::<f64>() / n;
454 let cy = points.iter().map(|p| p.1).sum::<f64>() / n;
455
456 let mean_dist = points
457 .iter()
458 .map(|p| ((p.0 - cx).powi(2) + (p.1 - cy).powi(2)).sqrt())
459 .sum::<f64>()
460 / n;
461
462 let scale = if mean_dist > 1e-10 {
463 std::f64::consts::SQRT_2 / mean_dist
464 } else {
465 1.0
466 };
467
468 let normalized: Vec<(f64, f64)> = points
469 .iter()
470 .map(|p| ((p.0 - cx) * scale, (p.1 - cy) * scale))
471 .collect();
472
473 let t = [
474 [scale, 0.0, -cx * scale],
475 [0.0, scale, -cy * scale],
476 [0.0, 0.0, 1.0],
477 ];
478
479 (normalized, t)
480}
481
482fn multiply_at_a(a: &[f64], rows: usize, cols: usize) -> Vec<f64> {
484 let mut ata = vec![0.0f64; cols * cols];
485 for i in 0..cols {
486 for j in 0..cols {
487 let mut sum = 0.0f64;
488 for k in 0..rows {
489 sum += a[k * cols + i] * a[k * cols + j];
490 }
491 ata[i * cols + j] = sum;
492 }
493 }
494 ata
495}
496
497#[allow(dead_code)]
501fn null_space_jacobi(m: &[f64]) -> Vec<f64> {
502 let n = 9;
503 let mut a = m.to_vec();
504 let mut v = vec![0.0f64; n * n];
505 for i in 0..n {
506 v[i * n + i] = 1.0;
507 }
508
509 for _ in 0..200 {
510 let mut max_val = 0.0;
512 let mut p = 0;
513 let mut q = 1;
514 for i in 0..n {
515 for j in (i + 1)..n {
516 if a[i * n + j].abs() > max_val {
517 max_val = a[i * n + j].abs();
518 p = i;
519 q = j;
520 }
521 }
522 }
523
524 if max_val < 1e-12 {
525 break;
526 }
527
528 let diff = a[p * n + p] - a[q * n + q];
529 let theta = if diff.abs() < 1e-15 {
530 std::f64::consts::FRAC_PI_4
531 } else {
532 0.5 * ((2.0 * a[p * n + q]) / diff).atan()
533 };
534
535 let c = theta.cos();
536 let s = theta.sin();
537
538 let mut row_p = [0.0f64; 9];
540 let mut row_q = [0.0f64; 9];
541 for j in 0..n {
542 row_p[j] = a[p * n + j];
543 row_q[j] = a[q * n + j];
544 }
545
546 for i in 0..n {
548 if i == p || i == q {
549 continue;
550 }
551 let aip = a[i * n + p];
552 let aiq = a[i * n + q];
553 a[i * n + p] = c * aip + s * aiq;
554 a[i * n + q] = -s * aip + c * aiq;
555 }
556
557 for j in 0..n {
559 a[p * n + j] = c * row_p[j] + s * row_q[j];
560 a[q * n + j] = -s * row_p[j] + c * row_q[j];
561 }
562 a[p * n + q] = 0.0;
563 a[q * n + p] = 0.0;
564
565 for i in 0..n {
567 let vip = v[i * n + p];
568 let viq = v[i * n + q];
569 v[i * n + p] = c * vip + s * viq;
570 v[i * n + q] = -s * vip + c * viq;
571 }
572 }
573
574 let mut min_idx = 0;
576 let mut min_val = a[0];
577 for i in 1..n {
578 if a[i * n + i] < min_val {
579 min_val = a[i * n + i];
580 min_idx = i;
581 }
582 }
583
584 (0..n).map(|i| v[i * n + min_idx]).collect()
585}
586
587#[allow(dead_code)]
589fn solve_9x9(a: &[f64], b: &[f64]) -> Option<Vec<f64>> {
590 let n = 9;
591 let mut aug = vec![vec![0.0f64; n + 1]; n];
592 for i in 0..n {
593 for j in 0..n {
594 aug[i][j] = a[i * n + j];
595 }
596 aug[i][n] = b[i];
597 }
598
599 for col in 0..n {
601 let mut max_val = aug[col][col].abs();
603 let mut max_row = col;
604 for row in (col + 1)..n {
605 if aug[row][col].abs() > max_val {
606 max_val = aug[row][col].abs();
607 max_row = row;
608 }
609 }
610 if max_val < 1e-15 {
611 return None;
612 }
613 aug.swap(col, max_row);
614
615 for row in (col + 1)..n {
617 let factor = aug[row][col] / aug[col][col];
618 for k in col..=n {
619 aug[row][k] -= factor * aug[col][k];
620 }
621 }
622 }
623
624 let mut x = vec![0.0f64; n];
626 for i in (0..n).rev() {
627 if aug[i][i].abs() < 1e-15 {
628 return None;
629 }
630 let mut sum = aug[i][n];
631 for j in (i + 1)..n {
632 sum -= aug[i][j] * x[j];
633 }
634 x[i] = sum / aug[i][i];
635 }
636
637 Some(x)
638}
639
640fn multiply_3x3(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
642 let mut result = [[0.0f64; 3]; 3];
643 for i in 0..3 {
644 for j in 0..3 {
645 for k in 0..3 {
646 result[i][j] += a[i][k] * b[k][j];
647 }
648 }
649 }
650 result
651}
652
653fn invert_3x3(m: &[[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
655 let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
656 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
657 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
658
659 if det.abs() < 1e-12 {
660 return None;
661 }
662
663 let inv_det = 1.0 / det;
664 let inv = [
665 [
666 (m[1][1] * m[2][2] - m[1][2] * m[2][1]) * inv_det,
667 (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * inv_det,
668 (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * inv_det,
669 ],
670 [
671 (m[1][2] * m[2][0] - m[1][0] * m[2][2]) * inv_det,
672 (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * inv_det,
673 (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * inv_det,
674 ],
675 [
676 (m[1][0] * m[2][1] - m[1][1] * m[2][0]) * inv_det,
677 (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * inv_det,
678 (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * inv_det,
679 ],
680 ];
681 Some(inv)
682}
683
684fn apply_homography(h: &[[f64; 3]; 3], x: f64, y: f64) -> (f64, f64) {
686 let denom = h[2][0] * x + h[2][1] * y + h[2][2];
687 if denom.abs() < 1e-10 {
688 return (x, y);
689 }
690 let wx = (h[0][0] * x + h[0][1] * y + h[0][2]) / denom;
691 let wy = (h[1][0] * x + h[1][1] * y + h[1][2]) / denom;
692 (wx, wy)
693}
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698 use crate::test_helpers::{TestBackend, test_device};
699 use burn::tensor::{Tensor, TensorData};
700
701 #[test]
702 fn test_stitching_identical_images() {
703 let device = test_device();
704
705 let flat_data = vec![0.5f32; 3 * 16 * 16];
707 let img = Image::new(Tensor::<TestBackend, 3>::from_data(
708 TensorData::new(flat_data, [3, 16, 16]),
709 &device,
710 ));
711
712 let stitcher = Stitcher;
713 let stitched = stitcher.stitch(&[img.clone(), img]).unwrap();
714 assert_eq!(stitched.shape()[0], 3);
715 assert!(stitched.shape()[1] > 0);
716 assert!(stitched.shape()[2] > 0);
717 }
718
719 #[test]
720 fn test_stitching_single_image() {
721 let device = test_device();
722 let flat_data = vec![0.3f32; 3 * 8 * 8];
723 let img = Image::new(Tensor::<TestBackend, 3>::from_data(
724 TensorData::new(flat_data, [3, 8, 8]),
725 &device,
726 ));
727
728 let stitcher = Stitcher;
729 let result = stitcher.stitch(std::slice::from_ref(&img)).unwrap();
730 assert_eq!(result.shape(), [3, 8, 8]);
731 }
732
733 #[test]
734 fn test_stitching_empty_input() {
735 let stitcher = Stitcher;
736 let empty: Vec<Image<TestBackend>> = vec![];
737 assert!(stitcher.stitch(&empty).is_err());
738 }
739
740 #[test]
741 fn test_homography_identity() {
742 let pts = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
743 let mut h = compute_homography_dlt(&pts, &pts).unwrap();
744
745 let s = h[2][2];
747 if s.abs() > 1e-10 {
748 for row in &mut h {
749 for val in row.iter_mut() {
750 *val /= s;
751 }
752 }
753 }
754
755 assert!((h[0][0] - 1.0).abs() < 0.01, "h[0][0] = {}", h[0][0]);
757 assert!((h[1][1] - 1.0).abs() < 0.01, "h[1][1] = {}", h[1][1]);
758 assert!((h[2][2] - 1.0).abs() < 0.01, "h[2][2] = {}", h[2][2]);
759 assert!(h[0][1].abs() < 0.01);
760 assert!(h[0][2].abs() < 0.01);
761 }
762
763 #[test]
764 fn test_invert_3x3() {
765 let m = [[2.0, 1.0, 0.0], [1.0, 3.0, 1.0], [0.0, 1.0, 2.0]];
766 let inv = invert_3x3(&m).unwrap();
767 let product = multiply_3x3(&m, &inv);
768
769 for i in 0..3 {
771 for j in 0..3 {
772 let expected = if i == j { 1.0 } else { 0.0 };
773 assert!(
774 (product[i][j] - expected).abs() < 1e-10,
775 "({}, {}) = {} expected {}",
776 i,
777 j,
778 product[i][j],
779 expected
780 );
781 }
782 }
783 }
784
785 #[test]
786 fn test_solve_9x9() {
787 let mut a = vec![0.0f64; 81];
789 for i in 0..9 {
790 a[i * 9 + i] = 1.0;
791 }
792 let b = vec![1.0f64; 9];
793 let x = solve_9x9(&a, &b).unwrap();
794 for v in &x {
795 assert!((v - 1.0).abs() < 1e-10);
796 }
797 }
798}