use crate::fields::boundary::bc::BoundaryCondition;
use crate::fields::surface_field::SurfaceScalarField;
use crate::fields::vol_field::VolScalarField;
use crate::ldu_matrix::fv_matrix::FvMatrix;
use crate::mesh::fv_mesh::PatchKind;
pub fn div(phi: &SurfaceScalarField, psi: &VolScalarField) -> FvMatrix {
let mesh = psi.mesh.clone();
let mut mat = FvMatrix::new(mesh.clone());
for f in 0..mesh.n_internal_faces {
let o = mesh.owner[f];
let n = mesh.neighbour[f];
let phi_f = phi.internal[f];
mat.ldu.diag[o] += phi_f.max(0.0);
mat.ldu.upper[f] += phi_f.min(0.0);
mat.ldu.diag[n] -= phi_f.min(0.0);
mat.ldu.lower[f] -= phi_f.max(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 owner = mesh.owner[patch.start + fi];
let phi_f = phi.boundary[pi].values[fi];
match &psi.boundary[pi].bc {
BoundaryCondition::ZeroGradient | BoundaryCondition::Symmetry => {
mat.ldu.diag[owner] += phi_f;
}
BoundaryCondition::FixedValue(v) => {
if phi_f >= 0.0 {
mat.ldu.diag[owner] += phi_f;
} else {
mat.source[owner] -= phi_f * v;
}
}
BoundaryCondition::FixedField(ff) => {
if phi_f >= 0.0 {
mat.ldu.diag[owner] += phi_f;
} else {
mat.source[owner] -= phi_f * ff[fi];
}
}
BoundaryCondition::InletOutlet { inlet_value } => {
if phi_f >= 0.0 {
mat.ldu.diag[owner] += phi_f; } else {
mat.source[owner] -= phi_f * inlet_value; }
}
BoundaryCondition::OutletInlet { outlet_value } => {
if phi_f >= 0.0 {
mat.source[owner] -= phi_f * outlet_value; } else {
mat.ldu.diag[owner] += phi_f; }
}
BoundaryCondition::Freestream { freestream_value } => {
if phi_f >= 0.0 {
mat.ldu.diag[owner] += phi_f; } else {
mat.source[owner] -= phi_f * freestream_value; }
}
BoundaryCondition::TotalPressure { .. } => {
if phi_f >= 0.0 {
mat.ldu.diag[owner] += phi_f;
} else {
mat.source[owner] -= phi_f * psi.boundary[pi].values[fi];
}
}
_ => {
if phi_f >= 0.0 {
mat.ldu.diag[owner] += phi_f;
}
}
}
}
}
for (i, cc) in mesh.cyclic_couplings.iter().enumerate() {
let cf = mesh.cyclic_coupling_face(i);
let o = cc.owner;
let nb = cc.neighbour;
let phi_f = phi.boundary[cc.patch_a].values[cc.local];
mat.ldu.diag[o] += phi_f.max(0.0);
mat.ldu.upper[cf] += phi_f.min(0.0);
mat.ldu.diag[nb] -= phi_f.min(0.0);
mat.ldu.lower[cf] -= phi_f.max(0.0);
}
let mut cf = mesh.ami_ldu_start();
for coupling in &mesh.ami_couplings {
let o = coupling.target_cell;
let phi_target = phi.boundary[coupling.target_patch].values[coupling.local];
for w in &coupling.weights {
let nb = w.source_cell;
let phi_f = phi_target * w.weight;
mat.ldu.diag[o] += phi_f.max(0.0);
mat.ldu.upper[cf] += phi_f.min(0.0);
mat.ldu.diag[nb] -= phi_f.min(0.0);
mat.ldu.lower[cf] -= phi_f.max(0.0);
cf += 1;
}
}
mat
}
#[cfg(test)]
mod tests {
use petir::mathf::RealMath;
use super::*;
use crate::fields::boundary::bc::{BoundaryCondition, PatchField};
use crate::fields::field::Field;
use crate::fields::surface_field::SurfaceScalarField;
use crate::fields::vol_field::VolScalarField;
use crate::mesh::fv_mesh::{BoundaryPatch, FvMeshBuilder, PatchKind};
use crate::primitives::Vector3;
use std::sync::Arc;
fn unit_mesh() -> Arc<crate::mesh::fv_mesh::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(),
)
}
fn make_phi(
m: Arc<crate::mesh::fv_mesh::FvMesh>,
internal: f64,
bnd: f64,
) -> SurfaceScalarField {
let n_int = m.n_internal_faces;
let bnd_vals: Vec<_> = m
.patches
.iter()
.map(|p| PatchField {
bc: BoundaryCondition::ZeroGradient,
values: Field::uniform(p.size, bnd),
})
.collect();
SurfaceScalarField::new("phi", m, Field::uniform(n_int, internal), bnd_vals)
}
#[test]
fn upwind_positive_flux_on_diagonal() {
let m = unit_mesh();
let phi = make_phi(m.clone(), 1.0, 0.0);
let psi = VolScalarField::uniform("psi", m.clone(), 0.0);
let mat = div(&phi, &psi);
assert!(
(mat.ldu.diag[0] - 1.0).abs() < 1e-12,
"diag[0]={}",
mat.ldu.diag[0]
);
assert!((mat.ldu.upper[0] - 0.0).abs() < 1e-12);
assert!((mat.ldu.diag[1] - 0.0).abs() < 1e-12);
assert!(
(mat.ldu.lower[0] - (-1.0)).abs() < 1e-12,
"lower[0]={}",
mat.ldu.lower[0]
);
}
#[test]
fn upwind_negative_flux_on_upper() {
let m = unit_mesh();
let phi = make_phi(m.clone(), -1.0, 0.0);
let psi = VolScalarField::uniform("psi", m.clone(), 0.0);
let mat = div(&phi, &psi);
assert!((mat.ldu.upper[0] - (-1.0)).abs() < 1e-12);
assert!((mat.ldu.diag[0] - 0.0).abs() < 1e-12);
assert!((mat.ldu.diag[1] - 1.0).abs() < 1e-12);
}
fn phi_right_only(m: Arc<crate::mesh::fv_mesh::FvMesh>, right_phi: f64) -> SurfaceScalarField {
let bnd = vec![
PatchField {
bc: BoundaryCondition::ZeroGradient,
values: Field::new(vec![right_phi]), },
PatchField {
bc: BoundaryCondition::ZeroGradient,
values: Field::new(vec![0.0]), },
];
SurfaceScalarField::new(
"phi",
m.clone(),
Field::uniform(m.n_internal_faces, 0.0),
bnd,
)
}
#[test]
fn vv_inlet_outlet_flux_switch() {
let m = unit_mesh();
let make_psi = || {
let bc = vec![
PatchField {
bc: BoundaryCondition::InletOutlet { inlet_value: 5.0 },
values: Field::new(vec![0.0]),
},
PatchField {
bc: BoundaryCondition::ZeroGradient,
values: Field::new(vec![0.0]),
},
];
VolScalarField::new("psi", m.clone(), Field::zeros(2), bc)
};
let out = div(&phi_right_only(m.clone(), 2.0), &make_psi());
assert!(
(out.ldu.diag[1] - 2.0).abs() < 1e-12,
"diag[1]={}",
out.ldu.diag[1]
);
assert!(out.source[1].abs() < 1e-12, "source[1]={}", out.source[1]);
let inn = div(&phi_right_only(m.clone(), -2.0), &make_psi());
assert!(inn.ldu.diag[1].abs() < 1e-12, "diag[1]={}", inn.ldu.diag[1]);
assert!(
(inn.source[1] - 10.0).abs() < 1e-12,
"source[1]={}",
inn.source[1]
);
}
fn ring_mesh(n: usize, h: f64, area: f64) -> Arc<crate::mesh::fv_mesh::FvMesh> {
use std::f64::consts::PI;
let r = h / (2.0 * (PI / n as f64).r_sin());
let owner: Vec<usize> = (0..n).collect();
let neighbour: Vec<usize> = (0..n).map(|f| (f + 1) % n).collect();
let cell_centres: Vec<Vector3> = (0..n)
.map(|i| {
let th = 2.0 * PI * i as f64 / n as f64;
Vector3::new(r * th.r_cos(), r * th.r_sin(), 0.0)
})
.collect();
let face_centres: Vec<Vector3> = (0..n)
.map(|f| (cell_centres[f] + cell_centres[(f + 1) % n]) * 0.5)
.collect();
let face_area_vectors: Vec<Vector3> =
(0..n).map(|_| Vector3::new(area, 0.0, 0.0)).collect();
Arc::new(
FvMeshBuilder::new()
.n_cells(n)
.n_internal_faces(n)
.owner(owner)
.neighbour(neighbour)
.patches(vec![])
.cell_volumes(vec![h * area; n])
.cell_centres(cell_centres)
.face_area_vectors(face_area_vectors)
.face_centres(face_centres)
.build()
.expect("ring mesh valid"),
)
}
#[test]
fn vv_cyclic_advection_conserves_and_matches_ring() {
let n = 4;
let h = 0.25;
let area = 1.0;
let phi_mag = 1.0;
let cyc = Arc::new(crate::mesh::fv_mesh::FvMesh::periodic_1d(
n,
h * n as f64,
area,
));
let ring = ring_mesh(n, h, area);
let phi_cyc = SurfaceScalarField::new(
"phi",
cyc.clone(),
Field::uniform(cyc.n_internal_faces, phi_mag),
vec![
PatchField {
bc: BoundaryCondition::ZeroGradient,
values: Field::new(vec![-phi_mag]), },
PatchField {
bc: BoundaryCondition::ZeroGradient,
values: Field::new(vec![phi_mag]), },
],
);
let phi_ring = SurfaceScalarField::new(
"phi",
ring.clone(),
Field::uniform(ring.n_internal_faces, phi_mag),
vec![],
);
let psi_cyc = VolScalarField::uniform("psi", cyc.clone(), 0.0);
let psi_ring = VolScalarField::uniform("psi", ring.clone(), 0.0);
let m_cyc = div(&phi_cyc, &psi_cyc);
let m_ring = div(&phi_ring, &psi_ring);
let ones = vec![1.0; n];
let ay = m_cyc.ldu.multiply(&ones);
for c in 0..n {
assert!(
ay[c].abs() < 1e-12,
"A·1 not conserved at cell {c}: {}",
ay[c]
);
}
let probes = [
vec![1.0, 0.0, 0.0, 0.0],
vec![0.0, 0.0, 1.0, 0.0],
vec![1.0, 2.0, 3.0, 4.0],
];
for x in &probes {
let yc = m_cyc.ldu.multiply(x);
let yr = m_ring.ldu.multiply(x);
for c in 0..n {
assert!(
(yc[c] - yr[c]).abs() < 1e-12,
"div A·x mismatch at cell {c}: cyc={} ring={}",
yc[c],
yr[c]
);
}
}
}
fn ring_uniform_phi(m: Arc<crate::mesh::fv_mesh::FvMesh>, u: f64) -> SurfaceScalarField {
let boundary = m
.patches
.iter()
.map(|p| {
let vals: Vec<f64> = (0..p.size)
.map(|fi| u * m.face_area_vectors[p.start + fi].x)
.collect();
PatchField {
bc: BoundaryCondition::ZeroGradient,
values: Field::new(vals),
}
})
.collect();
SurfaceScalarField::new(
"phi",
m.clone(),
Field::uniform(m.n_internal_faces, 0.0),
boundary,
)
}
#[test]
fn vv_ami_nonconformal_advection_conserves() {
let ring = Arc::new(crate::mesh::fv_mesh::FvMesh::periodic_ring_ami(
2, 4, 1.0, 1.0, 1.0,
));
let phi = ring_uniform_phi(ring.clone(), 1.0);
let psi = VolScalarField::uniform("psi", ring.clone(), 0.0);
let mat = div(&phi, &psi);
let ones = vec![1.0; ring.n_cells];
for (c, &y) in mat.ldu.multiply(&ones).iter().enumerate() {
assert!(
y.abs() < 1e-12,
"advection A·1 not conserved at cell {c}: {y}"
);
}
}
#[test]
fn vv_ami_matching_advection_conserves() {
let ring = Arc::new(crate::mesh::fv_mesh::FvMesh::periodic_ring_ami(
3, 3, 1.0, 1.0, 1.0,
));
let phi = ring_uniform_phi(ring.clone(), 1.0);
let psi = VolScalarField::uniform("psi", ring.clone(), 0.0);
let mat = div(&phi, &psi);
let ones = vec![1.0; ring.n_cells];
for (c, &y) in mat.ldu.multiply(&ones).iter().enumerate() {
assert!(
y.abs() < 1e-12,
"advection A·1 not conserved at cell {c}: {y}"
);
}
}
}