1use std::ops::{Add, AddAssign, Neg, Sub, SubAssign};
23use std::sync::Arc;
24
25use super::ldu_matrix::LduMatrix;
26use super::solvers::conjugate_gradient::conjugate_gradient;
27use super::solvers::gamg::gamg;
28use super::solvers::gauss_seidel::gauss_seidel;
29use crate::fields::boundary::bc::PatchField;
30use crate::fields::field::Field;
31use crate::fields::vol_field::VolScalarField;
32use crate::mesh::fv_mesh::FvMesh;
33
34pub struct FvMatrix {
42 pub mesh: Arc<FvMesh>,
44 pub ldu: LduMatrix,
46 pub source: Field<f64>,
48}
49
50#[derive(Debug, Clone, Copy)]
52pub struct SolverSettings {
53 pub tolerance: f64,
55 pub max_iter: usize,
57}
58
59impl Default for SolverSettings {
60 fn default() -> Self {
61 Self {
62 tolerance: 1e-7,
63 max_iter: 1000,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy)]
70pub struct SolverPerformance {
71 pub n_iterations: usize,
73 pub final_residual: f64,
75 pub converged: bool,
77}
78
79impl FvMatrix {
80 pub fn new(mesh: Arc<FvMesh>) -> Self {
82 let owner = mesh.owner[..mesh.n_internal_faces].to_vec();
83 let neighbour = mesh.neighbour.to_vec();
84 let n_cells = mesh.n_cells;
85 Self {
86 mesh,
87 ldu: LduMatrix::new(n_cells, owner, neighbour),
88 source: Field::zeros(n_cells),
89 }
90 }
91
92 pub fn solve(
97 &self,
98 name: impl Into<String>,
99 settings: SolverSettings,
100 ) -> (VolScalarField, SolverPerformance) {
101 let mut x = vec![0.0_f64; self.mesh.n_cells];
102 let b: Vec<f64> = self.source.iter().copied().collect();
103 let (iters, res) =
104 gauss_seidel(&self.ldu, &b, &mut x, settings.tolerance, settings.max_iter);
105
106 let boundary = self
107 .mesh
108 .patches
109 .iter()
110 .map(|p| PatchField::zero_gradient(p.size))
111 .collect();
112 let field = VolScalarField::new(name, self.mesh.clone(), Field::new(x), boundary);
113 let perf = SolverPerformance {
114 n_iterations: iters,
115 final_residual: res,
116 converged: res < settings.tolerance,
117 };
118 (field, perf)
119 }
120
121 pub fn solve_cg(
134 &self,
135 name: impl Into<String>,
136 settings: SolverSettings,
137 ) -> (VolScalarField, SolverPerformance) {
138 self.solve_cg_inner(name, None, settings)
139 }
140
141 pub fn solve_cg_with_guess(
151 &self,
152 name: impl Into<String>,
153 initial: &VolScalarField,
154 settings: SolverSettings,
155 ) -> (VolScalarField, SolverPerformance) {
156 self.solve_cg_inner(name, Some(initial.internal.as_slice()), settings)
157 }
158
159 fn solve_cg_inner(
160 &self,
161 name: impl Into<String>,
162 x0: Option<&[f64]>,
163 settings: SolverSettings,
164 ) -> (VolScalarField, SolverPerformance) {
165 let b: Vec<f64> = self.source.iter().copied().collect();
166 let (x, perf) = conjugate_gradient(&self.ldu, &b, x0, &settings);
167
168 let boundary = self
169 .mesh
170 .patches
171 .iter()
172 .map(|p| PatchField::zero_gradient(p.size))
173 .collect();
174 let field = VolScalarField::new(name, self.mesh.clone(), Field::new(x), boundary);
175 (field, perf)
176 }
177
178 pub fn solve_gamg(
188 &self,
189 name: impl Into<String>,
190 settings: SolverSettings,
191 ) -> (VolScalarField, SolverPerformance) {
192 self.solve_gamg_inner(name, None, settings)
193 }
194
195 pub fn solve_gamg_with_guess(
200 &self,
201 name: impl Into<String>,
202 initial: &VolScalarField,
203 settings: SolverSettings,
204 ) -> (VolScalarField, SolverPerformance) {
205 self.solve_gamg_inner(name, Some(initial.internal.as_slice()), settings)
206 }
207
208 fn solve_gamg_inner(
209 &self,
210 name: impl Into<String>,
211 x0: Option<&[f64]>,
212 settings: SolverSettings,
213 ) -> (VolScalarField, SolverPerformance) {
214 let b: Vec<f64> = self.source.iter().copied().collect();
215 let (x, perf) = gamg(&self.ldu, &b, x0, &settings);
216
217 let boundary = self
218 .mesh
219 .patches
220 .iter()
221 .map(|p| PatchField::zero_gradient(p.size))
222 .collect();
223 let field = VolScalarField::new(name, self.mesh.clone(), Field::new(x), boundary);
224 (field, perf)
225 }
226
227 pub fn add_to_diag(&mut self, coeff: &Field<f64>) {
231 for (d, &c) in self.ldu.diag.iter_mut().zip(coeff.iter()) {
232 *d += c;
233 }
234 }
235
236 pub fn add_to_source(&mut self, term: &Field<f64>) {
238 self.source += term.clone();
239 }
240
241 pub fn add_face_coeff(&mut self, face: usize, coeff: f64) {
243 let o = self.ldu.owner[face];
244 let n = self.ldu.neighbour[face];
245 self.ldu.upper[face] -= coeff;
247 self.ldu.lower[face] -= coeff;
248 self.ldu.diag[o] += coeff;
249 self.ldu.diag[n] += coeff;
250 }
251
252 pub fn set_reference(&mut self, cell: usize, value: f64) {
258 self.ldu.diag[cell] += 1e30;
259 self.source[cell] += 1e30 * value;
260 }
261
262 pub fn a_field(&self) -> VolScalarField {
266 let boundary = self
267 .mesh
268 .patches
269 .iter()
270 .map(|p| PatchField::zero_gradient(p.size))
271 .collect();
272 VolScalarField::new(
273 "A",
274 self.mesh.clone(),
275 Field::new(self.ldu.diag.clone()),
276 boundary,
277 )
278 }
279
280 pub fn h_field(&self, x: &VolScalarField) -> VolScalarField {
285 let n = self.mesh.n_cells;
286 let mut h = vec![0.0_f64; n];
287 for c in 0..n {
288 h[c] = self.source[c];
289 }
290 for f in 0..self.mesh.n_internal_faces {
291 let o = self.ldu.owner[f];
292 let nb = self.ldu.neighbour[f];
293 h[o] -= x.internal[nb] * self.ldu.upper[f];
294 h[nb] -= x.internal[o] * self.ldu.lower[f];
295 }
296 let boundary = self
297 .mesh
298 .patches
299 .iter()
300 .map(|p| PatchField::zero_gradient(p.size))
301 .collect();
302 VolScalarField::new("H", self.mesh.clone(), Field::new(h), boundary)
303 }
304}
305
306impl Add for FvMatrix {
309 type Output = Self;
310 fn add(mut self, rhs: Self) -> Self {
311 for (a, b) in self.ldu.diag.iter_mut().zip(&rhs.ldu.diag) {
312 *a += b;
313 }
314 for (a, b) in self.ldu.lower.iter_mut().zip(&rhs.ldu.lower) {
315 *a += b;
316 }
317 for (a, b) in self.ldu.upper.iter_mut().zip(&rhs.ldu.upper) {
318 *a += b;
319 }
320 self.source += rhs.source;
321 self
322 }
323}
324
325impl Sub for FvMatrix {
326 type Output = Self;
327 fn sub(mut self, rhs: Self) -> Self {
328 for (a, b) in self.ldu.diag.iter_mut().zip(&rhs.ldu.diag) {
329 *a -= b;
330 }
331 for (a, b) in self.ldu.lower.iter_mut().zip(&rhs.ldu.lower) {
332 *a -= b;
333 }
334 for (a, b) in self.ldu.upper.iter_mut().zip(&rhs.ldu.upper) {
335 *a -= b;
336 }
337 self.source -= rhs.source;
338 self
339 }
340}
341
342impl Neg for FvMatrix {
343 type Output = Self;
344 fn neg(mut self) -> Self {
345 for x in self.ldu.diag.iter_mut() {
346 *x = -*x;
347 }
348 for x in self.ldu.lower.iter_mut() {
349 *x = -*x;
350 }
351 for x in self.ldu.upper.iter_mut() {
352 *x = -*x;
353 }
354 self.source = -self.source;
355 self
356 }
357}
358
359impl AddAssign for FvMatrix {
360 fn add_assign(&mut self, rhs: Self) {
361 for (a, b) in self.ldu.diag.iter_mut().zip(&rhs.ldu.diag) {
362 *a += b;
363 }
364 for (a, b) in self.ldu.lower.iter_mut().zip(&rhs.ldu.lower) {
365 *a += b;
366 }
367 for (a, b) in self.ldu.upper.iter_mut().zip(&rhs.ldu.upper) {
368 *a += b;
369 }
370 self.source += rhs.source;
371 }
372}
373
374impl SubAssign for FvMatrix {
375 fn sub_assign(&mut self, rhs: Self) {
376 for (a, b) in self.ldu.diag.iter_mut().zip(&rhs.ldu.diag) {
377 *a -= b;
378 }
379 for (a, b) in self.ldu.lower.iter_mut().zip(&rhs.ldu.lower) {
380 *a -= b;
381 }
382 for (a, b) in self.ldu.upper.iter_mut().zip(&rhs.ldu.upper) {
383 *a -= b;
384 }
385 self.source -= rhs.source;
386 }
387}
388
389#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::mesh::fv_mesh::{BoundaryPatch, FvMeshBuilder, PatchKind};
395 use crate::primitives::Vector3;
396 use approx::assert_relative_eq;
397
398 fn unit_mesh() -> Arc<FvMesh> {
399 Arc::new(
400 FvMeshBuilder::new()
401 .n_cells(2)
402 .n_internal_faces(1)
403 .owner(vec![0, 1, 0])
404 .neighbour(vec![1])
405 .patches(vec![
406 BoundaryPatch::new("right", 1, 1, PatchKind::Wall),
407 BoundaryPatch::new("left", 2, 1, PatchKind::Wall),
408 ])
409 .cell_volumes(vec![1.0, 1.0])
410 .cell_centres(vec![
411 Vector3::new(0.25, 0.0, 0.0),
412 Vector3::new(0.75, 0.0, 0.0),
413 ])
414 .face_area_vectors(vec![
415 Vector3::new(1.0, 0.0, 0.0),
416 Vector3::new(1.0, 0.0, 0.0),
417 Vector3::new(-1.0, 0.0, 0.0),
418 ])
419 .face_centres(vec![
420 Vector3::new(0.5, 0.0, 0.0),
421 Vector3::new(1.0, 0.0, 0.0),
422 Vector3::new(0.0, 0.0, 0.0),
423 ])
424 .build()
425 .unwrap(),
426 )
427 }
428
429 #[test]
430 fn solve_diagonal_system() {
431 let mesh = unit_mesh();
432 let mut mat = FvMatrix::new(mesh);
433
434 mat.ldu.diag[0] = 3.0;
436 mat.ldu.diag[1] = 5.0;
437 mat.source = Field::new(vec![6.0, 10.0]);
438
439 let (phi, perf) = mat.solve("phi", SolverSettings::default());
440 assert!(
441 perf.converged,
442 "did not converge: residual = {}",
443 perf.final_residual
444 );
445 assert_relative_eq!(phi.internal[0], 2.0, epsilon = 1e-6);
446 assert_relative_eq!(phi.internal[1], 2.0, epsilon = 1e-6);
447 }
448
449 #[test]
450 fn add_face_coeff_symmetrises_diag() {
451 let mesh = unit_mesh();
452 let mut mat = FvMatrix::new(mesh);
453 mat.add_face_coeff(0, 1.0);
454 assert_relative_eq!(mat.ldu.diag[0], 1.0);
456 assert_relative_eq!(mat.ldu.diag[1], 1.0);
457 assert_relative_eq!(mat.ldu.upper[0], -1.0);
458 assert_relative_eq!(mat.ldu.lower[0], -1.0);
459 }
460}