outram_foam_basic_lib/matrix/
square_matrix.rs1#[derive(Debug, Clone, PartialEq)]
24pub enum MatrixError {
25 Singular { col: usize },
28}
29
30impl std::fmt::Display for MatrixError {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 MatrixError::Singular { col } => {
34 write!(f, "matrix is singular: zero pivot at column {col}")
35 }
36 }
37 }
38}
39
40impl std::error::Error for MatrixError {}
41
42#[derive(Debug, Clone)]
47pub struct SquareMatrix {
48 n: usize,
49 data: Vec<f64>,
50}
51
52impl SquareMatrix {
53 pub fn new(n: usize) -> Self {
54 Self {
55 n,
56 data: vec![0.0; n * n],
57 }
58 }
59
60 pub fn n(&self) -> usize {
61 self.n
62 }
63
64 #[inline]
65 pub fn get(&self, i: usize, j: usize) -> f64 {
66 self.data[i * self.n + j]
67 }
68
69 #[inline]
70 pub fn set(&mut self, i: usize, j: usize, v: f64) {
71 self.data[i * self.n + j] = v;
72 }
73
74 #[inline]
75 pub fn add(&mut self, i: usize, j: usize, v: f64) {
76 self.data[i * self.n + j] += v;
77 }
78
79 pub fn fill_zero(&mut self) {
80 self.data.iter_mut().for_each(|x| *x = 0.0);
81 }
82
83 pub fn lu_decompose(&mut self) -> Vec<usize> {
88 let n = self.n;
89 let mut pivot = vec![0usize; n];
90
91 let mut vv: Vec<f64> = (0..n)
93 .map(|i| {
94 let mx = self.data[i * n..(i + 1) * n]
95 .iter()
96 .map(|x| x.abs())
97 .fold(0.0_f64, f64::max);
98 if mx > 0.0 {
99 1.0 / mx
100 } else {
101 0.0
102 }
103 })
104 .collect();
105
106 for j in 0..n {
107 for i in 0..j {
109 let mut sum = self.data[i * n + j];
110 for k in 0..i {
111 sum -= self.data[i * n + k] * self.data[k * n + j];
112 }
113 self.data[i * n + j] = sum;
114 }
115
116 let mut i_max = j;
118 let mut largest = 0.0_f64;
119 for i in j..n {
120 let mut sum = self.data[i * n + j];
121 for k in 0..j {
122 sum -= self.data[i * n + k] * self.data[k * n + j];
123 }
124 self.data[i * n + j] = sum;
125 let tmp = vv[i] * sum.abs();
126 if tmp >= largest {
127 largest = tmp;
128 i_max = i;
129 }
130 }
131
132 pivot[j] = i_max;
134 if j != i_max {
135 for k in 0..n {
136 self.data.swap(j * n + k, i_max * n + k);
137 }
138 vv[i_max] = vv[j];
139 }
140
141 if self.data[j * n + j] == 0.0 {
143 self.data[j * n + j] = f64::EPSILON;
144 }
145
146 if j < n - 1 {
148 let r = 1.0 / self.data[j * n + j];
149 for i in (j + 1)..n {
150 self.data[i * n + j] *= r;
151 }
152 }
153 }
154
155 pivot
156 }
157
158 pub fn lu_back_substitute(&self, pivot: &[usize], b: &mut Vec<f64>) {
163 let n = self.n;
164
165 let mut first_nz: Option<usize> = None;
167 for i in 0..n {
168 let ip = pivot[i];
169 let mut sum = b[ip];
170 b[ip] = b[i];
171 if let Some(start) = first_nz {
172 for j in start..i {
173 sum -= self.data[i * n + j] * b[j];
174 }
175 } else if sum != 0.0 {
176 first_nz = Some(i);
177 }
178 b[i] = sum;
179 }
180
181 for i in (0..n).rev() {
183 let mut sum = b[i];
184 for j in (i + 1)..n {
185 sum -= self.data[i * n + j] * b[j];
186 }
187 b[i] = sum / self.data[i * n + i];
188 }
189 }
190
191 pub fn solve(&self, rhs: &[f64]) -> Result<Vec<f64>, MatrixError> {
198 let mut a = self.clone();
199 let pivot = a.lu_decompose();
200 for i in 0..self.n {
203 if a.data[i * self.n + i] == f64::EPSILON {
204 return Err(MatrixError::Singular { col: i });
205 }
206 }
207 let mut b = rhs.to_vec();
208 a.lu_back_substitute(&pivot, &mut b);
209 Ok(b)
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn lu_solve_2x2() {
219 let mut m = SquareMatrix::new(2);
221 m.set(0, 0, 2.0);
222 m.set(0, 1, 1.0);
223 m.set(1, 0, 1.0);
224 m.set(1, 1, 3.0);
225 let x = m.solve(&[5.0, 10.0]).unwrap();
226 assert!((x[0] - 1.0).abs() < 1e-12, "x={}", x[0]);
227 assert!((x[1] - 3.0).abs() < 1e-12, "y={}", x[1]);
228 }
229
230 #[test]
231 fn lu_solve_3x3() {
232 let mut m = SquareMatrix::new(3);
235 m.set(0, 0, 2.0);
236 m.set(0, 1, 1.0);
237 m.set(0, 2, 1.0);
238 m.set(1, 0, 1.0);
239 m.set(1, 1, 3.0);
240 m.set(1, 2, 1.0);
241 m.set(2, 0, 1.0);
242 m.set(2, 1, 1.0);
243 m.set(2, 2, 4.0);
244 let x = m.solve(&[7.0, 10.0, 15.0]).unwrap();
245 assert!((x[0] - 1.0).abs() < 1e-12, "x={}", x[0]);
246 assert!((x[1] - 2.0).abs() < 1e-12, "y={}", x[1]);
247 assert!((x[2] - 3.0).abs() < 1e-12, "z={}", x[2]);
248 }
249
250 #[test]
251 fn lu_solve_needs_pivoting() {
252 let mut m = SquareMatrix::new(2);
255 m.set(0, 0, 0.0);
256 m.set(0, 1, 1.0);
257 m.set(1, 0, 2.0);
258 m.set(1, 1, 0.0);
259 let x = m.solve(&[3.0, 4.0]).unwrap();
260 assert!((x[0] - 2.0).abs() < 1e-12, "x={}", x[0]);
261 assert!((x[1] - 3.0).abs() < 1e-12, "y={}", x[1]);
262 }
263
264 #[test]
265 fn lu_identity() {
266 let mut m = SquareMatrix::new(3);
267 m.set(0, 0, 1.0);
268 m.set(1, 1, 1.0);
269 m.set(2, 2, 1.0);
270 let x = m.solve(&[7.0, -2.0, 5.0]).unwrap();
271 assert!((x[0] - 7.0).abs() < 1e-12);
272 assert!((x[1] + 2.0).abs() < 1e-12);
273 assert!((x[2] - 5.0).abs() < 1e-12);
274 }
275
276 #[test]
277 fn singular_matrix_returns_err() {
278 let mut m = SquareMatrix::new(2);
280 m.set(0, 0, 1.0);
281 m.set(0, 1, 1.0);
282 m.set(1, 0, 1.0);
283 m.set(1, 1, 1.0);
284 let result = m.solve(&[2.0, 2.0]);
285 assert!(
286 matches!(result, Err(MatrixError::Singular { .. })),
287 "expected Singular, got {:?}",
288 result
289 );
290 }
291
292 #[test]
293 fn fully_zero_matrix_returns_err() {
294 let m = SquareMatrix::new(3); let result = m.solve(&[1.0, 0.0, 0.0]);
296 assert!(
297 matches!(result, Err(MatrixError::Singular { col: 0 })),
298 "expected Singular at col 0, got {:?}",
299 result
300 );
301 }
302
303 #[test]
304 fn hilbert_5x5_residual_acceptable() {
305 let n = 5;
308 let mut m = SquareMatrix::new(n);
309 for i in 0..n {
310 for j in 0..n {
311 m.set(i, j, 1.0 / (i + j + 1) as f64);
312 }
313 }
314 let b: Vec<f64> = (0..n).map(|_| 1.0).collect();
315 let x = m
316 .solve(&b)
317 .expect("Hilbert 5×5 should not be detected as singular");
318
319 let mut m2 = SquareMatrix::new(n);
321 for i in 0..n {
322 for j in 0..n {
323 m2.set(i, j, 1.0 / (i + j + 1) as f64);
324 }
325 }
326 let mut r_norm_sq = 0.0_f64;
327 let mut b_norm_sq = 0.0_f64;
328 for i in 0..n {
329 let ax_i: f64 = (0..n).map(|j| m2.get(i, j) * x[j]).sum();
330 r_norm_sq += (ax_i - b[i]).powi(2);
331 b_norm_sq += b[i].powi(2);
332 }
333 let rel_residual = r_norm_sq.sqrt() / b_norm_sq.sqrt();
334 assert!(
335 rel_residual < 1e-8,
336 "relative residual {rel_residual:.3e} too large"
337 );
338 }
339
340 #[test]
341 fn scaled_pivoting_large_row_contrast() {
342 let mut m = SquareMatrix::new(2);
350 m.set(0, 0, 1.0e8);
351 m.set(0, 1, 1.0);
352 m.set(1, 0, 1.0);
353 m.set(1, 1, 1.0e8);
354 let rhs = [1.0e8 + 1.0, 1.0e8 + 1.0];
355 let x = m.solve(&rhs).unwrap();
356 assert!((x[0] - 1.0).abs() < 1e-6, "x[0]={}", x[0]);
357 assert!((x[1] - 1.0).abs() < 1e-6, "x[1]={}", x[1]);
358 }
359
360 #[test]
361 fn lu_multiple_rhs() {
362 let mut m = SquareMatrix::new(2);
366 m.set(0, 0, 3.0);
367 m.set(0, 1, 1.0);
368 m.set(1, 0, 1.0);
369 m.set(1, 1, 2.0);
370 let pivot = m.lu_decompose();
371
372 let mut b1 = vec![5.0, 5.0];
373 m.lu_back_substitute(&pivot, &mut b1);
374 assert!((b1[0] - 1.0).abs() < 1e-12, "b1[0]={}", b1[0]);
375 assert!((b1[1] - 2.0).abs() < 1e-12, "b1[1]={}", b1[1]);
376
377 let mut b2 = vec![13.0, 11.0];
378 m.lu_back_substitute(&pivot, &mut b2);
379 assert!((b2[0] - 3.0).abs() < 1e-12, "b2[0]={}", b2[0]);
380 assert!((b2[1] - 4.0).abs() < 1e-12, "b2[1]={}", b2[1]);
381 }
382}