use std::ops::{Add, AddAssign, Neg, Sub, SubAssign};
use std::sync::Arc;
use super::ldu_matrix::LduMatrix;
use super::solvers::conjugate_gradient::conjugate_gradient;
use super::solvers::gamg::gamg;
use super::solvers::gauss_seidel::gauss_seidel;
use super::solvers::krylov_solve::{krylov_solve, KrylovMethod, KrylovOptions};
use crate::fields::boundary::bc::PatchField;
use crate::fields::field::Field;
use crate::fields::vol_field::VolScalarField;
use crate::mesh::fv_mesh::FvMesh;
pub struct FvMatrix {
pub mesh: Arc<FvMesh>,
pub ldu: LduMatrix,
pub source: Field<f64>,
}
#[derive(Debug, Clone, Copy)]
pub struct SolverSettings {
pub tolerance: f64,
pub max_iter: usize,
}
impl Default for SolverSettings {
fn default() -> Self {
Self {
tolerance: 1e-7,
max_iter: 1000,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SolverPerformance {
pub n_iterations: usize,
pub final_residual: f64,
pub converged: bool,
}
impl FvMatrix {
pub fn new(mesh: Arc<FvMesh>) -> Self {
let mut owner = mesh.owner[..mesh.n_internal_faces].to_vec();
let mut neighbour = mesh.neighbour.to_vec();
for cc in &mesh.cyclic_couplings {
owner.push(cc.owner);
neighbour.push(cc.neighbour);
}
for cc in &mesh.ami_couplings {
for w in &cc.weights {
owner.push(cc.target_cell);
neighbour.push(w.source_cell);
}
}
let n_cells = mesh.n_cells;
Self {
mesh,
ldu: LduMatrix::new(n_cells, owner, neighbour),
source: Field::zeros(n_cells),
}
}
pub fn solve(
&self,
name: impl Into<String>,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
let mut x = vec![0.0_f64; self.mesh.n_cells];
let b: Vec<f64> = self.source.iter().copied().collect();
let (iters, res) =
gauss_seidel(&self.ldu, &b, &mut x, settings.tolerance, settings.max_iter);
let boundary = self
.mesh
.patches
.iter()
.map(|p| PatchField::zero_gradient(p.size))
.collect();
let field = VolScalarField::new(name, self.mesh.clone(), Field::new(x), boundary);
let perf = SolverPerformance {
n_iterations: iters,
final_residual: res,
converged: res < settings.tolerance,
};
(field, perf)
}
pub fn solve_cg(
&self,
name: impl Into<String>,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
self.solve_cg_inner(name, None, settings)
}
pub fn solve_cg_with_guess(
&self,
name: impl Into<String>,
initial: &VolScalarField,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
self.solve_cg_inner(name, Some(initial.internal.as_slice()), settings)
}
fn solve_cg_inner(
&self,
name: impl Into<String>,
x0: Option<&[f64]>,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
let b: Vec<f64> = self.source.iter().copied().collect();
let (x, perf) = conjugate_gradient(&self.ldu, &b, x0, &settings);
let boundary = self
.mesh
.patches
.iter()
.map(|p| PatchField::zero_gradient(p.size))
.collect();
let field = VolScalarField::new(name, self.mesh.clone(), Field::new(x), boundary);
(field, perf)
}
pub fn solve_gamg(
&self,
name: impl Into<String>,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
self.solve_gamg_inner(name, None, settings)
}
pub fn solve_gamg_with_guess(
&self,
name: impl Into<String>,
initial: &VolScalarField,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
self.solve_gamg_inner(name, Some(initial.internal.as_slice()), settings)
}
fn solve_gamg_inner(
&self,
name: impl Into<String>,
x0: Option<&[f64]>,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
let b: Vec<f64> = self.source.iter().copied().collect();
let (x, perf) = gamg(&self.ldu, &b, x0, &settings);
let boundary = self
.mesh
.patches
.iter()
.map(|p| PatchField::zero_gradient(p.size))
.collect();
let field = VolScalarField::new(name, self.mesh.clone(), Field::new(x), boundary);
(field, perf)
}
pub fn solve_bicgstab(
&self,
name: impl Into<String>,
options: KrylovOptions,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
self.solve_krylov_inner(name, None, KrylovMethod::BiCGStab, options, settings)
}
pub fn solve_bicgstab_with_guess(
&self,
name: impl Into<String>,
initial: &VolScalarField,
options: KrylovOptions,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
self.solve_krylov_inner(
name,
Some(initial.internal.as_slice()),
KrylovMethod::BiCGStab,
options,
settings,
)
}
pub fn solve_gmres(
&self,
name: impl Into<String>,
options: KrylovOptions,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
self.solve_krylov_inner(name, None, KrylovMethod::Gmres, options, settings)
}
pub fn solve_gmres_with_guess(
&self,
name: impl Into<String>,
initial: &VolScalarField,
options: KrylovOptions,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
self.solve_krylov_inner(
name,
Some(initial.internal.as_slice()),
KrylovMethod::Gmres,
options,
settings,
)
}
pub fn solve_krylov(
&self,
name: impl Into<String>,
initial: Option<&VolScalarField>,
method: KrylovMethod,
options: KrylovOptions,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
self.solve_krylov_inner(
name,
initial.map(|f| f.internal.as_slice()),
method,
options,
settings,
)
}
fn solve_krylov_inner(
&self,
name: impl Into<String>,
x0: Option<&[f64]>,
method: KrylovMethod,
options: KrylovOptions,
settings: SolverSettings,
) -> (VolScalarField, SolverPerformance) {
let b: Vec<f64> = self.source.iter().copied().collect();
let (x, perf) = krylov_solve(&self.ldu, &b, x0, method, options, &settings);
let boundary = self
.mesh
.patches
.iter()
.map(|p| PatchField::zero_gradient(p.size))
.collect();
let field = VolScalarField::new(name, self.mesh.clone(), Field::new(x), boundary);
(field, perf)
}
pub fn add_to_diag(&mut self, coeff: &Field<f64>) {
for (d, &c) in self.ldu.diag.iter_mut().zip(coeff.iter()) {
*d += c;
}
}
pub fn add_to_source(&mut self, term: &Field<f64>) {
self.source += term.clone();
}
pub fn add_face_coeff(&mut self, face: usize, coeff: f64) {
let o = self.ldu.owner[face];
let n = self.ldu.neighbour[face];
self.ldu.upper[face] -= coeff;
self.ldu.lower[face] -= coeff;
self.ldu.diag[o] += coeff;
self.ldu.diag[n] += coeff;
}
pub fn set_reference(&mut self, cell: usize, value: f64) {
self.ldu.diag[cell] += 1e30;
self.source[cell] += 1e30 * value;
}
pub fn a_field(&self) -> VolScalarField {
let boundary = self
.mesh
.patches
.iter()
.map(|p| PatchField::zero_gradient(p.size))
.collect();
VolScalarField::new(
"A",
self.mesh.clone(),
Field::new(self.ldu.diag.clone()),
boundary,
)
}
pub fn h_field(&self, x: &VolScalarField) -> VolScalarField {
let n = self.mesh.n_cells;
let mut h = vec![0.0_f64; n];
for c in 0..n {
h[c] = self.source[c];
}
for f in 0..self.ldu.n_internal_faces {
let o = self.ldu.owner[f];
let nb = self.ldu.neighbour[f];
h[o] -= x.internal[nb] * self.ldu.upper[f];
h[nb] -= x.internal[o] * self.ldu.lower[f];
}
let boundary = self
.mesh
.patches
.iter()
.map(|p| PatchField::zero_gradient(p.size))
.collect();
VolScalarField::new("H", self.mesh.clone(), Field::new(h), boundary)
}
}
impl Add for FvMatrix {
type Output = Self;
fn add(mut self, rhs: Self) -> Self {
for (a, b) in self.ldu.diag.iter_mut().zip(&rhs.ldu.diag) {
*a += b;
}
for (a, b) in self.ldu.lower.iter_mut().zip(&rhs.ldu.lower) {
*a += b;
}
for (a, b) in self.ldu.upper.iter_mut().zip(&rhs.ldu.upper) {
*a += b;
}
self.source += rhs.source;
self
}
}
impl Sub for FvMatrix {
type Output = Self;
fn sub(mut self, rhs: Self) -> Self {
for (a, b) in self.ldu.diag.iter_mut().zip(&rhs.ldu.diag) {
*a -= b;
}
for (a, b) in self.ldu.lower.iter_mut().zip(&rhs.ldu.lower) {
*a -= b;
}
for (a, b) in self.ldu.upper.iter_mut().zip(&rhs.ldu.upper) {
*a -= b;
}
self.source -= rhs.source;
self
}
}
impl Neg for FvMatrix {
type Output = Self;
fn neg(mut self) -> Self {
for x in self.ldu.diag.iter_mut() {
*x = -*x;
}
for x in self.ldu.lower.iter_mut() {
*x = -*x;
}
for x in self.ldu.upper.iter_mut() {
*x = -*x;
}
self.source = -self.source;
self
}
}
impl AddAssign for FvMatrix {
fn add_assign(&mut self, rhs: Self) {
for (a, b) in self.ldu.diag.iter_mut().zip(&rhs.ldu.diag) {
*a += b;
}
for (a, b) in self.ldu.lower.iter_mut().zip(&rhs.ldu.lower) {
*a += b;
}
for (a, b) in self.ldu.upper.iter_mut().zip(&rhs.ldu.upper) {
*a += b;
}
self.source += rhs.source;
}
}
impl SubAssign for FvMatrix {
fn sub_assign(&mut self, rhs: Self) {
for (a, b) in self.ldu.diag.iter_mut().zip(&rhs.ldu.diag) {
*a -= b;
}
for (a, b) in self.ldu.lower.iter_mut().zip(&rhs.ldu.lower) {
*a -= b;
}
for (a, b) in self.ldu.upper.iter_mut().zip(&rhs.ldu.upper) {
*a -= b;
}
self.source -= rhs.source;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::fv_mesh::{BoundaryPatch, FvMeshBuilder, PatchKind};
use crate::primitives::Vector3;
use approx::assert_relative_eq;
fn unit_mesh() -> Arc<FvMesh> {
Arc::new(
FvMeshBuilder::new()
.n_cells(2)
.n_internal_faces(1)
.owner(vec![0, 1, 0])
.neighbour(vec![1])
.patches(vec![
BoundaryPatch::new("right", 1, 1, PatchKind::Wall),
BoundaryPatch::new("left", 2, 1, PatchKind::Wall),
])
.cell_volumes(vec![1.0, 1.0])
.cell_centres(vec![
Vector3::new(0.25, 0.0, 0.0),
Vector3::new(0.75, 0.0, 0.0),
])
.face_area_vectors(vec![
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(-1.0, 0.0, 0.0),
])
.face_centres(vec![
Vector3::new(0.5, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 0.0),
])
.build()
.unwrap(),
)
}
#[test]
fn solve_diagonal_system() {
let mesh = unit_mesh();
let mut mat = FvMatrix::new(mesh);
mat.ldu.diag[0] = 3.0;
mat.ldu.diag[1] = 5.0;
mat.source = Field::new(vec![6.0, 10.0]);
let (phi, perf) = mat.solve("phi", SolverSettings::default());
assert!(
perf.converged,
"did not converge: residual = {}",
perf.final_residual
);
assert_relative_eq!(phi.internal[0], 2.0, epsilon = 1e-6);
assert_relative_eq!(phi.internal[1], 2.0, epsilon = 1e-6);
}
#[test]
fn add_face_coeff_symmetrises_diag() {
let mesh = unit_mesh();
let mut mat = FvMatrix::new(mesh);
mat.add_face_coeff(0, 1.0);
assert_relative_eq!(mat.ldu.diag[0], 1.0);
assert_relative_eq!(mat.ldu.diag[1], 1.0);
assert_relative_eq!(mat.ldu.upper[0], -1.0);
assert_relative_eq!(mat.ldu.lower[0], -1.0);
}
}