use petir::mathf::RealMath;
use crate::fields::boundary::bc::BoundaryCondition;
use crate::fields::surface_field::SurfaceScalarField;
use crate::fields::vol_field::{VolScalarField, VolVectorField};
use crate::fv_operators::fvc::grad_least_squares;
use crate::ldu_matrix::fv_matrix::FvMatrix;
use crate::mesh::fv_mesh::PatchKind;
use crate::primitives::Vector3;
const NON_ORTH_FLOOR: f64 = 0.05;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum NonOrthoScheme {
#[default]
Orthogonal,
Corrected,
Limited(f64),
}
impl NonOrthoScheme {
pub fn correction_fraction(self) -> f64 {
match self {
NonOrthoScheme::Orthogonal => 0.0,
NonOrthoScheme::Corrected => 1.0,
NonOrthoScheme::Limited(w) => w.clamp(0.0, 1.0),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct NonOrthoGeometry {
pub delta_coeff: f64,
pub corr_vec: Vector3,
pub angle_deg: f64,
}
pub fn non_ortho_geometry(sf: Vector3, d: Vector3) -> Option<NonOrthoGeometry> {
let mag_sf = sf.mag();
let mag_d = d.mag();
if mag_sf < 1.0e-300 || mag_d < 1.0e-300 {
return None;
}
let n = sf * (1.0 / mag_sf);
let n_dot_d = n.dot(d);
let cos_theta = (n_dot_d / mag_d).clamp(-1.0, 1.0);
let angle_deg = cos_theta.r_acos().to_degrees();
let delta_coeff = 1.0 / n_dot_d.max(NON_ORTH_FLOOR * mag_d);
let corr_vec = n - d * delta_coeff;
Some(NonOrthoGeometry {
delta_coeff,
corr_vec,
angle_deg,
})
}
pub fn max_non_orthogonality_deg(mesh: &crate::mesh::fv_mesh::FvMesh) -> f64 {
let mut worst = 0.0_f64;
for f in 0..mesh.n_internal_faces {
let d = mesh.cell_centres[mesh.neighbour[f]] - mesh.cell_centres[mesh.owner[f]];
if let Some(g) = non_ortho_geometry(mesh.face_area_vectors[f], d) {
worst = worst.max(g.angle_deg);
}
}
worst
}
pub fn laplacian_corrected(
gamma: &SurfaceScalarField,
phi: &VolScalarField,
grad_phi: &VolVectorField,
scheme: NonOrthoScheme,
) -> FvMatrix {
let mesh = phi.mesh.clone();
let mut mat = super::laplacian(gamma, phi);
let w = scheme.correction_fraction();
for f in 0..mesh.n_internal_faces {
let o = mesh.owner[f];
let nb = mesh.neighbour[f];
let d = mesh.cell_centres[nb] - mesh.cell_centres[o];
let Some(g) = non_ortho_geometry(mesh.face_area_vectors[f], d) else {
continue;
};
let gamma_a = gamma.internal[f] * mesh.face_areas[f];
if w > 0.0 {
let coeff_orth = gamma_a / d.mag();
let coeff_corr = gamma_a * g.delta_coeff;
let delta = coeff_corr - coeff_orth;
mat.ldu.diag[o] += delta;
mat.ldu.diag[nb] += delta;
mat.ldu.upper[f] -= delta;
mat.ldu.lower[f] -= delta;
let grad_f = (grad_phi.internal[o] + grad_phi.internal[nb]) * 0.5;
let corr = w * gamma_a * g.corr_vec.dot(grad_f);
mat.source[o] += corr;
mat.source[nb] -= corr;
}
}
if w > 0.0 {
for (pi, patch) in mesh.patches.iter().enumerate() {
if patch.kind == PatchKind::Cyclic || patch.kind == PatchKind::CyclicAmi {
continue;
}
for fi in 0..patch.size {
let gf = patch.start + fi;
let owner = mesh.owner[gf];
let d_b = mesh.face_centres[gf] - mesh.cell_centres[owner];
let Some(g) = non_ortho_geometry(mesh.face_area_vectors[gf], d_b) else {
continue;
};
let gamma_a = gamma.boundary[pi].values[fi] * mesh.face_areas[gf];
let coeff_orth = gamma_a / d_b.mag();
let coeff_corr = gamma_a * g.delta_coeff;
let delta = coeff_corr - coeff_orth;
let value = match &phi.boundary[pi].bc {
BoundaryCondition::FixedValue(v) => Some(*v),
BoundaryCondition::FixedField(ff) => Some(ff[fi]),
BoundaryCondition::NoSlip => Some(0.0),
_ => None,
};
let Some(v) = value else { continue };
mat.ldu.diag[owner] += delta;
mat.source[owner] += delta * v;
mat.source[owner] += w * gamma_a * g.corr_vec.dot(grad_phi.internal[owner]);
}
}
}
mat
}
pub fn solve_laplacian_non_orthogonal(
gamma: &SurfaceScalarField,
phi: &VolScalarField,
scheme: NonOrthoScheme,
n_correctors: usize,
settings: crate::ldu_matrix::SolverSettings,
) -> (VolScalarField, crate::ldu_matrix::SolverPerformance) {
use crate::ldu_matrix::KrylovOptions;
let mat = super::laplacian(gamma, phi);
let (mut current, mut perf) =
mat.solve_bicgstab(phi.name.as_str(), KrylovOptions::default(), settings);
current.boundary = phi.boundary.clone();
for _ in 0..n_correctors {
let g = grad_least_squares(¤t);
let mat = laplacian_corrected(gamma, ¤t, &g, scheme);
let (next, p) = mat.solve_bicgstab(phi.name.as_str(), KrylovOptions::default(), settings);
current.internal = next.internal;
perf = p;
}
(current, perf)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fields::field::Field;
use crate::mesh::fv_mesh::{BoundaryPatch, FvMesh, FvMeshBuilder, PatchKind};
use std::sync::Arc;
fn ortho_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![0.5, 0.5])
.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 orthogonal_scheme_reproduces_the_uncorrected_matrix() {
let mesh = ortho_mesh();
let phi = VolScalarField::new(
"T",
mesh.clone(),
Field::new(vec![1.0, 2.0]),
mesh.patches
.iter()
.map(|p| crate::fields::boundary::bc::PatchField::zero_gradient(p.size))
.collect(),
);
let gamma = SurfaceScalarField::uniform("Gamma", mesh.clone(), 1.0);
let g = grad_least_squares(&phi);
let a = super::super::laplacian(&gamma, &phi);
let b = laplacian_corrected(&gamma, &phi, &g, NonOrthoScheme::Orthogonal);
assert_eq!(a.ldu.diag, b.ldu.diag);
assert_eq!(a.ldu.upper, b.ldu.upper);
assert_eq!(a.ldu.lower, b.ldu.lower);
for c in 0..mesh.n_cells {
assert_eq!(a.source[c], b.source[c]);
}
}
#[test]
fn correction_is_a_no_op_on_an_orthogonal_mesh() {
let mesh = ortho_mesh();
assert!(max_non_orthogonality_deg(&mesh) < 1e-9);
let phi = VolScalarField::new(
"T",
mesh.clone(),
Field::new(vec![1.0, 2.0]),
mesh.patches
.iter()
.map(|p| crate::fields::boundary::bc::PatchField::zero_gradient(p.size))
.collect(),
);
let gamma = SurfaceScalarField::uniform("Gamma", mesh.clone(), 1.0);
let g = grad_least_squares(&phi);
let a = super::super::laplacian(&gamma, &phi);
let b = laplacian_corrected(&gamma, &phi, &g, NonOrthoScheme::Corrected);
for i in 0..a.ldu.diag.len() {
assert!((a.ldu.diag[i] - b.ldu.diag[i]).abs() < 1e-12);
}
for c in 0..mesh.n_cells {
assert!((a.source[c] - b.source[c]).abs() < 1e-12);
}
}
#[test]
fn geometry_reports_the_expected_angle_and_reduces_when_orthogonal() {
let sf = Vector3::new(1.0, 0.0, 0.0);
let d = Vector3::new(1.0, 1.0, 0.0);
let g = non_ortho_geometry(sf, d).unwrap();
assert!((g.angle_deg - 45.0).abs() < 1e-9, "angle {}", g.angle_deg);
assert!((g.delta_coeff - 1.0).abs() < 1e-12);
assert!((g.corr_vec - Vector3::new(0.0, -1.0, 0.0)).mag() < 1e-12);
assert!(non_ortho_geometry(Vector3::ZERO, d).is_none());
assert!(non_ortho_geometry(sf, Vector3::ZERO).is_none());
}
#[test]
fn limited_scheme_interpolates_between_the_two_extremes() {
assert_eq!(NonOrthoScheme::Orthogonal.correction_fraction(), 0.0);
assert_eq!(NonOrthoScheme::Corrected.correction_fraction(), 1.0);
assert_eq!(NonOrthoScheme::Limited(0.33).correction_fraction(), 0.33);
assert_eq!(NonOrthoScheme::Limited(-1.0).correction_fraction(), 0.0);
assert_eq!(NonOrthoScheme::Limited(7.0).correction_fraction(), 1.0);
}
}