use crate::mesh::ami::AmiCoupling;
use crate::mesh::error::MeshError;
use crate::primitives::Vector3;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BoundaryPatch {
pub name: String,
pub start: usize,
pub size: usize,
pub kind: PatchKind,
pub cyclic_partner: Option<usize>,
}
impl BoundaryPatch {
pub fn new(name: impl Into<String>, start: usize, size: usize, kind: PatchKind) -> Self {
Self {
name: name.into(),
start,
size,
kind,
cyclic_partner: None,
}
}
pub fn new_cyclic(
name: impl Into<String>,
start: usize,
size: usize,
partner_patch: usize,
) -> Self {
Self {
name: name.into(),
start,
size,
kind: PatchKind::Cyclic,
cyclic_partner: Some(partner_patch),
}
}
pub fn end(&self) -> usize {
self.start + self.size
}
pub fn contains_face(&self, f: usize) -> bool {
f >= self.start && f < self.end()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PatchKind {
Patch,
Wall,
Symmetry,
Empty,
Wedge,
Cyclic,
CyclicAmi,
Processor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CyclicCoupling {
pub owner: usize,
pub neighbour: usize,
pub face_a: usize,
pub face_b: usize,
pub patch_a: usize,
pub patch_b: usize,
pub local: usize,
}
#[derive(Debug, Clone)]
pub struct FvMesh {
pub n_cells: usize,
pub n_internal_faces: usize,
pub n_faces: usize,
pub owner: Vec<usize>,
pub neighbour: Vec<usize>,
pub patches: Vec<BoundaryPatch>,
pub cyclic_couplings: Vec<CyclicCoupling>,
pub ami_couplings: Vec<AmiCoupling>,
pub cell_volumes: Vec<f64>,
pub cell_centres: Vec<Vector3>,
pub face_area_vectors: Vec<Vector3>,
pub face_areas: Vec<f64>,
pub face_centres: Vec<Vector3>,
}
impl FvMesh {
pub fn n_boundary_faces(&self) -> usize {
self.n_faces - self.n_internal_faces
}
pub fn n_patches(&self) -> usize {
self.patches.len()
}
pub fn is_internal_face(&self, f: usize) -> bool {
f < self.n_internal_faces
}
pub fn patch_for_face(&self, f: usize) -> Option<(usize, usize)> {
if self.is_internal_face(f) {
return None;
}
for (pi, patch) in self.patches.iter().enumerate() {
if patch.contains_face(f) {
return Some((pi, f - patch.start));
}
}
None
}
pub fn n_cyclic_couplings(&self) -> usize {
self.cyclic_couplings.len()
}
pub fn cyclic_coupling_face(&self, i: usize) -> usize {
self.n_internal_faces + i
}
pub fn cyclic_partner_face(&self, global_face: usize) -> Option<usize> {
let (pi, local) = self.patch_for_face(global_face)?;
let patch = &self.patches[pi];
if patch.kind != PatchKind::Cyclic {
return None;
}
let pj = patch.cyclic_partner?;
let partner = self.patches.get(pj)?;
if local >= partner.size {
return None;
}
Some(partner.start + local)
}
pub fn periodic_1d(n: usize, length: f64, area: f64) -> FvMesh {
assert!(n >= 2, "periodic_1d needs at least 2 cells, got {n}");
assert!(length > 0.0, "length must be positive, got {length}");
assert!(area > 0.0, "area must be positive, got {area}");
let h = length / n as f64;
let n_int = n - 1;
let mut owner: Vec<usize> = (0..n_int).collect();
owner.push(0); owner.push(n - 1); let neighbour: Vec<usize> = (1..n).collect();
let cell_volumes = vec![h * area; n];
let cell_centres = (0..n)
.map(|i| Vector3::new((i as f64 + 0.5) * h, 0.0, 0.0))
.collect();
let face_centres: Vec<Vector3> = (0..n_int)
.map(|f| Vector3::new((f as f64 + 1.0) * h, 0.0, 0.0))
.chain([Vector3::new(0.0, 0.0, 0.0), Vector3::new(length, 0.0, 0.0)])
.collect();
let face_area_vectors: Vec<Vector3> = (0..n_int)
.map(|_| Vector3::new(area, 0.0, 0.0))
.chain([Vector3::new(-area, 0.0, 0.0), Vector3::new(area, 0.0, 0.0)])
.collect();
FvMeshBuilder::new()
.n_cells(n)
.n_internal_faces(n_int)
.owner(owner)
.neighbour(neighbour)
.patches(vec![
BoundaryPatch::new_cyclic("left", n_int, 1, 1),
BoundaryPatch::new_cyclic("right", n_int + 1, 1, 0),
])
.cell_volumes(cell_volumes)
.cell_centres(cell_centres)
.face_area_vectors(face_area_vectors)
.face_centres(face_centres)
.build()
.expect("periodic_1d builds a consistent mesh")
}
pub fn validate(&self) -> Result<(), MeshError> {
let check_len = |array, got, expected| {
if got != expected {
Err(MeshError::ArrayLengthMismatch {
array,
expected,
got,
})
} else {
Ok(())
}
};
check_len("owner", self.owner.len(), self.n_faces)?;
check_len("neighbour", self.neighbour.len(), self.n_internal_faces)?;
check_len("cell_volumes", self.cell_volumes.len(), self.n_cells)?;
check_len("cell_centres", self.cell_centres.len(), self.n_cells)?;
check_len(
"face_area_vectors",
self.face_area_vectors.len(),
self.n_faces,
)?;
check_len("face_areas", self.face_areas.len(), self.n_faces)?;
check_len("face_centres", self.face_centres.len(), self.n_faces)?;
let mut covered = self.n_internal_faces;
for p in &self.patches {
if p.start != covered {
return Err(MeshError::PatchStartMismatch {
name: p.name.clone(),
expected: covered,
got: p.start,
});
}
covered += p.size;
}
if covered != self.n_faces {
return Err(MeshError::PatchCoverageMismatch {
covered,
n_faces: self.n_faces,
});
}
for (pi, patch) in self.patches.iter().enumerate() {
let Some(pj) = patch.cyclic_partner else {
continue;
};
let partner = self.patches.get(pj).ok_or(MeshError::CyclicPairMismatch {
name: patch.name.clone(),
reason: "partner patch index out of range",
})?;
if partner.cyclic_partner != Some(pi) {
return Err(MeshError::CyclicPairMismatch {
name: patch.name.clone(),
reason: "partner patch does not name this patch back",
});
}
if partner.size != patch.size {
return Err(MeshError::CyclicPairMismatch {
name: patch.name.clone(),
reason: "cyclic pair halves have different face counts",
});
}
}
for cc in &self.ami_couplings {
if cc.target_cell >= self.n_cells || cc.target_face >= self.n_faces {
return Err(MeshError::AmiCouplingInvalid {
target_face: cc.target_face,
reason: "target cell/face index out of range",
});
}
if cc.weights.is_empty() {
return Err(MeshError::AmiCouplingInvalid {
target_face: cc.target_face,
reason: "AMI target face has no overlapping source faces",
});
}
for w in &cc.weights {
if w.source_cell >= self.n_cells || w.source_face >= self.n_faces {
return Err(MeshError::AmiCouplingInvalid {
target_face: cc.target_face,
reason: "source cell/face index out of range",
});
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct FvMeshBuilder {
n_cells: usize,
n_internal_faces: usize,
owner: Vec<usize>,
neighbour: Vec<usize>,
patches: Vec<BoundaryPatch>,
ami_couplings: Vec<AmiCoupling>,
cell_volumes: Vec<f64>,
cell_centres: Vec<Vector3>,
face_area_vectors: Vec<Vector3>,
face_areas: Vec<f64>,
face_centres: Vec<Vector3>,
}
impl FvMeshBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn n_cells(mut self, n: usize) -> Self {
self.n_cells = n;
self
}
pub fn n_internal_faces(mut self, n: usize) -> Self {
self.n_internal_faces = n;
self
}
pub fn owner(mut self, v: Vec<usize>) -> Self {
self.owner = v;
self
}
pub fn neighbour(mut self, v: Vec<usize>) -> Self {
self.neighbour = v;
self
}
pub fn patches(mut self, v: Vec<BoundaryPatch>) -> Self {
self.patches = v;
self
}
pub fn ami_couplings(mut self, v: Vec<AmiCoupling>) -> Self {
self.ami_couplings = v;
self
}
pub fn cell_volumes(mut self, v: Vec<f64>) -> Self {
self.cell_volumes = v;
self
}
pub fn cell_centres(mut self, v: Vec<Vector3>) -> Self {
self.cell_centres = v;
self
}
pub fn face_area_vectors(mut self, v: Vec<Vector3>) -> Self {
self.face_area_vectors = v;
self
}
pub fn face_areas(mut self, v: Vec<f64>) -> Self {
self.face_areas = v;
self
}
pub fn face_centres(mut self, v: Vec<Vector3>) -> Self {
self.face_centres = v;
self
}
fn ensure_face_areas(&mut self) {
if self.face_areas.is_empty() && !self.face_area_vectors.is_empty() {
self.face_areas = self.face_area_vectors.iter().map(|sf| sf.mag()).collect();
}
}
pub fn build(mut self) -> Result<FvMesh, MeshError> {
self.ensure_face_areas();
let n_faces = self.owner.len();
let cyclic_couplings = Self::derive_cyclic_couplings(&self.patches, &self.owner);
let mesh = FvMesh {
n_cells: self.n_cells,
n_internal_faces: self.n_internal_faces,
n_faces,
owner: self.owner,
neighbour: self.neighbour,
patches: self.patches,
cyclic_couplings,
ami_couplings: self.ami_couplings,
cell_volumes: self.cell_volumes,
cell_centres: self.cell_centres,
face_area_vectors: self.face_area_vectors,
face_areas: self.face_areas,
face_centres: self.face_centres,
};
mesh.validate()?;
Ok(mesh)
}
fn derive_cyclic_couplings(patches: &[BoundaryPatch], owner: &[usize]) -> Vec<CyclicCoupling> {
let mut couplings = Vec::new();
for (pi, patch) in patches.iter().enumerate() {
if patch.kind != PatchKind::Cyclic {
continue;
}
let Some(pj) = patch.cyclic_partner else {
continue;
};
if pj <= pi {
continue;
}
let Some(partner) = patches.get(pj) else {
continue;
};
if partner.cyclic_partner != Some(pi) || partner.size != patch.size {
continue;
}
for local in 0..patch.size {
let face_a = patch.start + local;
let face_b = partner.start + local;
couplings.push(CyclicCoupling {
owner: owner[face_a],
neighbour: owner[face_b],
face_a,
face_b,
patch_a: pi,
patch_b: pj,
local,
});
}
}
couplings
}
}
#[cfg(test)]
mod tests {
use super::*;
fn two_cell_mesh() -> FvMesh {
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()
.expect("two_cell_mesh should be valid")
}
#[test]
fn validate_two_cell_mesh() {
let m = two_cell_mesh();
assert_eq!(m.n_cells, 2);
assert_eq!(m.n_internal_faces, 1);
assert_eq!(m.n_faces, 3);
assert_eq!(m.n_boundary_faces(), 2);
}
#[test]
fn is_internal_face() {
let m = two_cell_mesh();
assert!(m.is_internal_face(0));
assert!(!m.is_internal_face(1));
assert!(!m.is_internal_face(2));
}
#[test]
fn patch_for_face() {
let m = two_cell_mesh();
assert_eq!(m.patch_for_face(0), None); assert_eq!(m.patch_for_face(1), Some((0, 0))); assert_eq!(m.patch_for_face(2), Some((1, 0))); }
#[test]
fn face_areas_derived() {
let m = two_cell_mesh();
assert!((m.face_areas[0] - 1.0).abs() < 1e-15);
}
#[test]
fn periodic_1d_topology() {
let m = FvMesh::periodic_1d(4, 1.0, 1.0);
assert_eq!(m.n_cells, 4);
assert_eq!(m.n_internal_faces, 3);
assert_eq!(m.n_faces, 5); assert_eq!(m.patches.len(), 2);
assert_eq!(m.patches[0].kind, PatchKind::Cyclic);
assert_eq!(m.patches[0].cyclic_partner, Some(1));
assert_eq!(m.patches[1].cyclic_partner, Some(0));
assert_eq!(m.n_cyclic_couplings(), 1);
let cc = m.cyclic_couplings[0];
assert_eq!(cc.owner, 0);
assert_eq!(cc.neighbour, 3);
assert_eq!(cc.patch_a, 0);
assert_eq!(cc.patch_b, 1);
assert_eq!(m.cyclic_coupling_face(0), 3);
}
#[test]
fn cyclic_partner_face_maps_across_seam() {
let m = FvMesh::periodic_1d(4, 1.0, 1.0);
assert_eq!(m.cyclic_partner_face(3), Some(4));
assert_eq!(m.cyclic_partner_face(4), Some(3));
assert_eq!(m.cyclic_partner_face(0), None);
}
#[test]
fn cyclic_pair_validation_rejects_asymmetric_partner() {
let err = FvMeshBuilder::new()
.n_cells(2)
.n_internal_faces(1)
.owner(vec![0, 0, 1])
.neighbour(vec![1])
.patches(vec![
BoundaryPatch::new_cyclic("left", 1, 1, 1),
BoundaryPatch::new("right", 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(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
])
.build();
assert!(matches!(err, Err(MeshError::CyclicPairMismatch { .. })));
}
}