use crate::fields::boundary::bc::PatchField;
use crate::fields::field::Field;
use crate::fields::vol_field::{VolScalarField, VolVectorField};
use crate::matrix::SquareMatrix;
use crate::primitives::Vector3;
const RANK_TOL: f64 = 1.0e-10;
fn boundary_face_value(phi: &VolScalarField, pi: usize, fi: usize, owner: usize) -> f64 {
use crate::fields::boundary::bc::BoundaryCondition as Bc;
match &phi.boundary[pi].bc {
Bc::ZeroGradient | Bc::Symmetry | Bc::Empty => phi.internal[owner],
Bc::FixedValue(v) => *v,
Bc::FixedField(ff) => ff[fi],
Bc::NoSlip => 0.0,
_ => phi.boundary[pi].values[fi],
}
}
pub fn grad_least_squares(phi: &VolScalarField) -> VolVectorField {
let mesh = phi.mesh.clone();
let n = mesh.n_cells;
let mut m = vec![[0.0_f64; 6]; n];
let mut r = vec![Vector3::ZERO; n];
let accumulate =
|cell: usize, d: Vector3, dphi: f64, m: &mut Vec<[f64; 6]>, r: &mut Vec<Vector3>| {
let d2 = d.mag_sqr();
if d2 < 1.0e-300 {
return;
}
let w2 = 1.0 / d2;
let e = &mut m[cell];
e[0] += w2 * d.x * d.x;
e[1] += w2 * d.x * d.y;
e[2] += w2 * d.x * d.z;
e[3] += w2 * d.y * d.y;
e[4] += w2 * d.y * d.z;
e[5] += w2 * d.z * d.z;
r[cell] = r[cell] + d * (w2 * dphi);
};
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 dphi = phi.internal[nb] - phi.internal[o];
accumulate(o, d, dphi, &mut m, &mut r);
accumulate(nb, -d, -dphi, &mut m, &mut r);
}
for (pi, patch) in mesh.patches.iter().enumerate() {
for fi in 0..patch.size {
let gf = patch.start + fi;
let owner = mesh.owner[gf];
let d = mesh.face_centres[gf] - mesh.cell_centres[owner];
let face_value = boundary_face_value(phi, pi, fi, owner);
let dphi = face_value - phi.internal[owner];
accumulate(owner, d, dphi, &mut m, &mut r);
}
}
let mut g = vec![Vector3::ZERO; n];
for c in 0..n {
let e = m[c];
let trace = e[0] + e[3] + e[5];
if trace < 1.0e-300 {
continue; }
let mut a = SquareMatrix::new(3);
a.set(0, 0, e[0]);
a.set(0, 1, e[1]);
a.set(0, 2, e[2]);
a.set(1, 0, e[1]);
a.set(1, 1, e[3]);
a.set(1, 2, e[4]);
a.set(2, 0, e[2]);
a.set(2, 1, e[4]);
a.set(2, 2, e[5]);
let mut rhs = [r[c].x, r[c].y, r[c].z];
for (k, diag) in [e[0], e[3], e[5]].into_iter().enumerate() {
if diag <= RANK_TOL * trace {
for j in 0..3 {
a.set(k, j, 0.0);
a.set(j, k, 0.0);
}
a.set(k, k, 1.0);
rhs[k] = 0.0;
}
}
if let Ok(sol) = a.solve(&rhs) {
g[c] = Vector3::new(sol[0], sol[1], sol[2]);
}
}
let boundary = mesh
.patches
.iter()
.map(|p| PatchField::zero_gradient_vec(p.size))
.collect();
VolVectorField::new(
crate::fv_operators::naming::derived_name("grad", &phi.name),
mesh,
Field::new(g),
boundary,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fields::boundary::bc::PatchField;
use crate::mesh::fv_mesh::{BoundaryPatch, FvMesh, FvMeshBuilder, PatchKind};
use approx::assert_relative_eq;
use std::sync::Arc;
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![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 uniform_field_has_zero_gradient() {
let m = unit_mesh();
let p = VolScalarField::uniform("p", m, 3.0);
let g = grad_least_squares(&p);
assert!(g.internal[0].mag() < 1e-12);
assert!(g.internal[1].mag() < 1e-12);
}
#[test]
fn linear_field_recovered_exactly_in_1d() {
let m = unit_mesh();
let boundary = vec![
PatchField::fixed_value(1, 1.0), PatchField::fixed_value(1, 0.0), ];
let p = VolScalarField::new("p", m, Field::new(vec![0.25, 0.75]), boundary);
let g = grad_least_squares(&p);
for c in 0..2 {
assert_relative_eq!(g.internal[c].x, 1.0, epsilon = 1e-12);
assert_eq!(g.internal[c].y, 0.0);
assert_eq!(g.internal[c].z, 0.0);
}
}
#[test]
fn periodic_ring_uniform_field_has_zero_gradient() {
let mesh = Arc::new(FvMesh::periodic_1d(8, 1.0, 1.0));
let p = VolScalarField::uniform("p", mesh, 2.5);
let g = grad_least_squares(&p);
for c in 0..8 {
assert!(g.internal[c].mag() < 1e-12, "cell {c}: {:?}", g.internal[c]);
}
}
}