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 {
55 Self {
56 n,
57 data: vec![0.0; n * n],
58 }
59 }
60
61 pub fn n(&self) -> usize {
63 self.n
64 }
65
66 #[inline]
68 pub fn get(&self, i: usize, j: usize) -> f64 {
69 self.data[i * self.n + j]
70 }
71
72 #[inline]
74 pub fn set(&mut self, i: usize, j: usize, v: f64) {
75 self.data[i * self.n + j] = v;
76 }
77
78 #[inline]
80 pub fn add(&mut self, i: usize, j: usize, v: f64) {
81 self.data[i * self.n + j] += v;
82 }
83
84 pub fn fill_zero(&mut self) {
86 self.data.iter_mut().for_each(|x| *x = 0.0);
87 }
88
89 pub fn lu_decompose(&mut self) -> Vec<usize> {
94 let n = self.n;
95 let mut pivot = vec![0usize; n];
96
97 let mut vv: Vec<f64> = (0..n)
99 .map(|i| {
100 let mx = self.data[i * n..(i + 1) * n]
101 .iter()
102 .map(|x| x.abs())
103 .fold(0.0_f64, f64::max);
104 if mx > 0.0 {
105 1.0 / mx
106 } else {
107 0.0
108 }
109 })
110 .collect();
111
112 for j in 0..n {
113 for i in 0..j {
115 let mut sum = self.data[i * n + j];
116 for k in 0..i {
117 sum -= self.data[i * n + k] * self.data[k * n + j];
118 }
119 self.data[i * n + j] = sum;
120 }
121
122 let mut i_max = j;
124 let mut largest = 0.0_f64;
125 for i in j..n {
126 let mut sum = self.data[i * n + j];
127 for k in 0..j {
128 sum -= self.data[i * n + k] * self.data[k * n + j];
129 }
130 self.data[i * n + j] = sum;
131 let tmp = vv[i] * sum.abs();
132 if tmp >= largest {
133 largest = tmp;
134 i_max = i;
135 }
136 }
137
138 pivot[j] = i_max;
140 if j != i_max {
141 for k in 0..n {
142 self.data.swap(j * n + k, i_max * n + k);
143 }
144 vv[i_max] = vv[j];
145 }
146
147 if self.data[j * n + j] == 0.0 {
149 self.data[j * n + j] = f64::EPSILON;
150 }
151
152 if j < n - 1 {
154 let r = 1.0 / self.data[j * n + j];
155 for i in (j + 1)..n {
156 self.data[i * n + j] *= r;
157 }
158 }
159 }
160
161 pivot
162 }
163
164 pub fn lu_back_substitute(&self, pivot: &[usize], b: &mut Vec<f64>) {
169 let n = self.n;
170
171 let mut first_nz: Option<usize> = None;
173 for i in 0..n {
174 let ip = pivot[i];
175 let mut sum = b[ip];
176 b[ip] = b[i];
177 if let Some(start) = first_nz {
178 for j in start..i {
179 sum -= self.data[i * n + j] * b[j];
180 }
181 } else if sum != 0.0 {
182 first_nz = Some(i);
183 }
184 b[i] = sum;
185 }
186
187 for i in (0..n).rev() {
189 let mut sum = b[i];
190 for j in (i + 1)..n {
191 sum -= self.data[i * n + j] * b[j];
192 }
193 b[i] = sum / self.data[i * n + i];
194 }
195 }
196
197 pub fn solve(&self, rhs: &[f64]) -> Result<Vec<f64>, MatrixError> {
204 let mut a = self.clone();
205 let pivot = a.lu_decompose();
206 for i in 0..self.n {
209 if a.data[i * self.n + i] == f64::EPSILON {
210 return Err(MatrixError::Singular { col: i });
211 }
212 }
213 let mut b = rhs.to_vec();
214 a.lu_back_substitute(&pivot, &mut b);
215 Ok(b)
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn lu_solve_2x2() {
225 let mut m = SquareMatrix::new(2);
227 m.set(0, 0, 2.0);
228 m.set(0, 1, 1.0);
229 m.set(1, 0, 1.0);
230 m.set(1, 1, 3.0);
231 let x = m.solve(&[5.0, 10.0]).unwrap();
232 assert!((x[0] - 1.0).abs() < 1e-12, "x={}", x[0]);
233 assert!((x[1] - 3.0).abs() < 1e-12, "y={}", x[1]);
234 }
235
236 #[test]
237 fn lu_solve_3x3() {
238 let mut m = SquareMatrix::new(3);
241 m.set(0, 0, 2.0);
242 m.set(0, 1, 1.0);
243 m.set(0, 2, 1.0);
244 m.set(1, 0, 1.0);
245 m.set(1, 1, 3.0);
246 m.set(1, 2, 1.0);
247 m.set(2, 0, 1.0);
248 m.set(2, 1, 1.0);
249 m.set(2, 2, 4.0);
250 let x = m.solve(&[7.0, 10.0, 15.0]).unwrap();
251 assert!((x[0] - 1.0).abs() < 1e-12, "x={}", x[0]);
252 assert!((x[1] - 2.0).abs() < 1e-12, "y={}", x[1]);
253 assert!((x[2] - 3.0).abs() < 1e-12, "z={}", x[2]);
254 }
255
256 #[test]
257 fn lu_solve_needs_pivoting() {
258 let mut m = SquareMatrix::new(2);
261 m.set(0, 0, 0.0);
262 m.set(0, 1, 1.0);
263 m.set(1, 0, 2.0);
264 m.set(1, 1, 0.0);
265 let x = m.solve(&[3.0, 4.0]).unwrap();
266 assert!((x[0] - 2.0).abs() < 1e-12, "x={}", x[0]);
267 assert!((x[1] - 3.0).abs() < 1e-12, "y={}", x[1]);
268 }
269
270 #[test]
271 fn lu_identity() {
272 let mut m = SquareMatrix::new(3);
273 m.set(0, 0, 1.0);
274 m.set(1, 1, 1.0);
275 m.set(2, 2, 1.0);
276 let x = m.solve(&[7.0, -2.0, 5.0]).unwrap();
277 assert!((x[0] - 7.0).abs() < 1e-12);
278 assert!((x[1] + 2.0).abs() < 1e-12);
279 assert!((x[2] - 5.0).abs() < 1e-12);
280 }
281
282 #[test]
283 fn singular_matrix_returns_err() {
284 let mut m = SquareMatrix::new(2);
286 m.set(0, 0, 1.0);
287 m.set(0, 1, 1.0);
288 m.set(1, 0, 1.0);
289 m.set(1, 1, 1.0);
290 let result = m.solve(&[2.0, 2.0]);
291 assert!(
292 matches!(result, Err(MatrixError::Singular { .. })),
293 "expected Singular, got {:?}",
294 result
295 );
296 }
297
298 #[test]
299 fn fully_zero_matrix_returns_err() {
300 let m = SquareMatrix::new(3); let result = m.solve(&[1.0, 0.0, 0.0]);
302 assert!(
303 matches!(result, Err(MatrixError::Singular { col: 0 })),
304 "expected Singular at col 0, got {:?}",
305 result
306 );
307 }
308
309 #[test]
310 fn hilbert_5x5_residual_acceptable() {
311 let n = 5;
314 let mut m = SquareMatrix::new(n);
315 for i in 0..n {
316 for j in 0..n {
317 m.set(i, j, 1.0 / (i + j + 1) as f64);
318 }
319 }
320 let b: Vec<f64> = (0..n).map(|_| 1.0).collect();
321 let x = m
322 .solve(&b)
323 .expect("Hilbert 5×5 should not be detected as singular");
324
325 let mut m2 = SquareMatrix::new(n);
327 for i in 0..n {
328 for j in 0..n {
329 m2.set(i, j, 1.0 / (i + j + 1) as f64);
330 }
331 }
332 let mut r_norm_sq = 0.0_f64;
333 let mut b_norm_sq = 0.0_f64;
334 for i in 0..n {
335 let ax_i: f64 = (0..n).map(|j| m2.get(i, j) * x[j]).sum();
336 r_norm_sq += (ax_i - b[i]).powi(2);
337 b_norm_sq += b[i].powi(2);
338 }
339 let rel_residual = r_norm_sq.sqrt() / b_norm_sq.sqrt();
340 assert!(
341 rel_residual < 1e-8,
342 "relative residual {rel_residual:.3e} too large"
343 );
344 }
345
346 #[test]
347 fn scaled_pivoting_large_row_contrast() {
348 let mut m = SquareMatrix::new(2);
356 m.set(0, 0, 1.0e8);
357 m.set(0, 1, 1.0);
358 m.set(1, 0, 1.0);
359 m.set(1, 1, 1.0e8);
360 let rhs = [1.0e8 + 1.0, 1.0e8 + 1.0];
361 let x = m.solve(&rhs).unwrap();
362 assert!((x[0] - 1.0).abs() < 1e-6, "x[0]={}", x[0]);
363 assert!((x[1] - 1.0).abs() < 1e-6, "x[1]={}", x[1]);
364 }
365
366 #[test]
367 fn lu_multiple_rhs() {
368 let mut m = SquareMatrix::new(2);
372 m.set(0, 0, 3.0);
373 m.set(0, 1, 1.0);
374 m.set(1, 0, 1.0);
375 m.set(1, 1, 2.0);
376 let pivot = m.lu_decompose();
377
378 let mut b1 = vec![5.0, 5.0];
379 m.lu_back_substitute(&pivot, &mut b1);
380 assert!((b1[0] - 1.0).abs() < 1e-12, "b1[0]={}", b1[0]);
381 assert!((b1[1] - 2.0).abs() < 1e-12, "b1[1]={}", b1[1]);
382
383 let mut b2 = vec![13.0, 11.0];
384 m.lu_back_substitute(&pivot, &mut b2);
385 assert!((b2[0] - 3.0).abs() < 1e-12, "b2[0]={}", b2[0]);
386 assert!((b2[1] - 4.0).abs() < 1e-12, "b2[1]={}", b2[1]);
387 }
388}