use petir::mathf::RealMath;
use std::sync::Arc;
use uom::si::area::square_meter;
use uom::si::f64::{Area, Length};
use uom::si::length::meter;
use super::SteadyConductionError;
use crate::mesh::fv_mesh::{BoundaryPatch, FvMesh, FvMeshBuilder, PatchKind};
use crate::primitives::Vector3;
#[derive(Debug, Clone)]
pub struct ConductionMesh {
pub mesh: Arc<FvMesh>,
pub cell_zone: Vec<usize>,
pub n_zones: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RadialGeometry {
Slab {
area: Area,
},
Cylinder {
height: Length,
},
Sphere,
}
impl RadialGeometry {
#[must_use]
pub fn exponent(self) -> i32 {
match self {
Self::Slab { .. } => 0,
Self::Cylinder { .. } => 1,
Self::Sphere => 2,
}
}
fn area_at(self, r: f64) -> f64 {
match self {
Self::Slab { area } => area.get::<square_meter>(),
Self::Cylinder { height } => 2.0 * std::f64::consts::PI * r * height.get::<meter>(),
Self::Sphere => 4.0 * std::f64::consts::PI * r * r,
}
}
fn volume_between(self, r1: f64, r2: f64) -> f64 {
match self {
Self::Slab { area } => area.get::<square_meter>() * (r2 - r1),
Self::Cylinder { height } => {
std::f64::consts::PI * height.get::<meter>() * (r2 * r2 - r1 * r1)
}
Self::Sphere => 4.0 / 3.0 * std::f64::consts::PI * (r2.r_powi(3) - r1.r_powi(3)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RadialShell {
pub outer_radius: Length,
pub cells: usize,
}
pub const RADIAL_SURFACE_PATCH: &str = "surface";
pub const RADIAL_CENTRE_PATCH: &str = "centre";
pub fn radial_mesh(
geometry: RadialGeometry,
shells: &[RadialShell],
) -> Result<ConductionMesh, SteadyConductionError> {
if shells.is_empty() {
return Err(SteadyConductionError::InvalidInput(
"radial mesh needs a shell".into(),
));
}
let mut edges = vec![0.0_f64];
let mut cell_zone = Vec::new();
for (j, shell) in shells.iter().enumerate() {
let r_in = edges[edges.len() - 1];
let r_out = shell.outer_radius.get::<meter>();
if shell.cells == 0 || !(r_out > r_in) || !r_out.is_finite() {
return Err(SteadyConductionError::InvalidInput(format!(
"shell {j}: needs at least one cell and an outer radius above {r_in} m (got {} cells, {r_out} m)",
shell.cells
)));
}
let dr = (r_out - r_in) / shell.cells as f64;
for i in 1..=shell.cells {
edges.push(if i == shell.cells {
r_out
} else {
r_in + dr * i as f64
});
cell_zone.push(j);
}
}
let n = cell_zone.len();
let axis = |x: f64| Vector3::new(x, 0.0, 0.0);
let cell_volumes: Vec<f64> = (0..n)
.map(|c| geometry.volume_between(edges[c], edges[c + 1]))
.collect();
let cell_centres: Vec<Vector3> = (0..n)
.map(|c| axis(0.5 * (edges[c] + edges[c + 1])))
.collect();
let mut owner: Vec<usize> = (0..n - 1).collect();
let neighbour: Vec<usize> = (1..n).collect();
let mut face_area_vectors: Vec<Vector3> = (0..n - 1)
.map(|c| axis(geometry.area_at(edges[c + 1])))
.collect();
let mut face_centres: Vec<Vector3> = (0..n - 1).map(|c| axis(edges[c + 1])).collect();
owner.push(n - 1);
face_area_vectors.push(axis(geometry.area_at(edges[n])));
face_centres.push(axis(edges[n]));
owner.push(0);
face_area_vectors.push(axis(-geometry.area_at(0.0)));
face_centres.push(axis(0.0));
let centre_kind = match geometry {
RadialGeometry::Slab { .. } => PatchKind::Symmetry,
_ => PatchKind::Patch,
};
let mesh = FvMeshBuilder::new()
.n_cells(n)
.n_internal_faces(n - 1)
.owner(owner)
.neighbour(neighbour)
.patches(vec![
BoundaryPatch::new(RADIAL_SURFACE_PATCH, n - 1, 1, PatchKind::Patch),
BoundaryPatch::new(RADIAL_CENTRE_PATCH, n, 1, centre_kind),
])
.cell_volumes(cell_volumes)
.cell_centres(cell_centres)
.face_area_vectors(face_area_vectors)
.face_centres(face_centres)
.build()
.map_err(|e| SteadyConductionError::Mesh(format!("{e:?}")))?;
Ok(ConductionMesh {
mesh: Arc::new(mesh),
cell_zone,
n_zones: shells.len(),
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VoxelGrid {
pub origin: [Length; 3],
pub spacing: Length,
pub counts: [usize; 3],
}
impl VoxelGrid {
#[must_use]
pub fn flat(&self, ijk: [usize; 3]) -> usize {
ijk[0] + self.counts[0] * (ijk[1] + self.counts[1] * ijk[2])
}
#[must_use]
pub fn centre(&self, ijk: [usize; 3]) -> [Length; 3] {
let h = self.spacing;
[0, 1, 2].map(|a| self.origin[a] + h * (ijk[a] as f64 + 0.5))
}
#[must_use]
pub fn voxel_of(&self, p: [Length; 3]) -> Option<[usize; 3]> {
let h = self.spacing.get::<meter>();
let mut ijk = [0usize; 3];
for a in 0..3 {
let s = (p[a] - self.origin[a]).get::<meter>() / h;
if !(s >= 0.0) || s >= self.counts[a] as f64 {
return None;
}
ijk[a] = s as usize;
}
Some(ijk)
}
}
#[derive(Debug, Clone)]
pub struct VoxelMesh {
pub conduction: ConductionMesh,
pub grid: VoxelGrid,
pub voxel_cell: Vec<Option<usize>>,
pub cell_voxel: Vec<[usize; 3]>,
pub surface_normal: Vec<[f64; 3]>,
}
pub const VOXEL_SURFACE_PATCH: &str = "surface";
impl VoxelMesh {
#[must_use]
pub fn cell_at(&self, p: [Length; 3]) -> Option<usize> {
self.grid
.voxel_of(p)
.and_then(|ijk| self.voxel_cell[self.grid.flat(ijk)])
}
#[must_use]
pub fn n_surface_faces(&self) -> usize {
self.surface_normal.len()
}
}
pub fn voxel_mesh<F>(grid: &VoxelGrid, zone_at: F) -> Result<VoxelMesh, SteadyConductionError>
where
F: Fn([Length; 3]) -> Option<usize>,
{
let h = grid.spacing.get::<meter>();
if grid.counts.contains(&0) || !(h > 0.0) || !h.is_finite() {
return Err(SteadyConductionError::InvalidInput(format!(
"voxel grid needs non-zero counts and a positive spacing (got {:?}, {h} m)",
grid.counts
)));
}
let [nx, ny, nz] = grid.counts;
let n_vox = nx * ny * nz;
let mut voxel_cell = vec![None; n_vox];
let mut cell_voxel = Vec::new();
let mut cell_zone = Vec::new();
for k in 0..nz {
for j in 0..ny {
for i in 0..nx {
if let Some(z) = zone_at(grid.centre([i, j, k])) {
voxel_cell[grid.flat([i, j, k])] = Some(cell_voxel.len());
cell_voxel.push([i, j, k]);
cell_zone.push(z);
}
}
}
}
let n = cell_voxel.len();
if n == 0 {
return Err(SteadyConductionError::InvalidInput(
"no voxel centre lies inside the body; refine the grid or check the box".into(),
));
}
let n_zones = cell_zone.iter().copied().max().unwrap_or(0) + 1;
let o = grid.origin.map(|l| l.get::<meter>());
let centre = |ijk: [usize; 3]| {
Vector3::new(
o[0] + h * (ijk[0] as f64 + 0.5),
o[1] + h * (ijk[1] as f64 + 0.5),
o[2] + h * (ijk[2] as f64 + 0.5),
)
};
let unit = |a: usize, s: f64| {
let mut v = [0.0; 3];
v[a] = s;
v
};
let area = h * h;
let mut owner = Vec::new();
let mut neighbour = Vec::new();
let mut sf = Vec::new();
let mut cf = Vec::new();
let mut b_owner = Vec::new();
let mut b_sf = Vec::new();
let mut b_cf = Vec::new();
let mut surface_normal = Vec::new();
for (c, ijk) in cell_voxel.iter().enumerate() {
let cc = centre(*ijk);
for a in 0..3 {
for s in [-1.0_f64, 1.0] {
let n_hat = unit(a, s);
let n_vec = Vector3::new(n_hat[0], n_hat[1], n_hat[2]);
let neighbour_voxel = if s < 0.0 {
ijk[a].checked_sub(1).map(|v| {
let mut t = *ijk;
t[a] = v;
t
})
} else if ijk[a] + 1 < grid.counts[a] {
let mut t = *ijk;
t[a] += 1;
Some(t)
} else {
None
};
let nb_cell = neighbour_voxel.and_then(|t| voxel_cell[grid.flat(t)]);
match nb_cell {
Some(nb) if s > 0.0 => {
owner.push(c);
neighbour.push(nb);
sf.push(n_vec * area);
cf.push(cc + n_vec * (0.5 * h));
}
Some(_) => {}
None => {
b_owner.push(c);
b_sf.push(n_vec * area);
b_cf.push(cc + n_vec * (0.5 * h));
surface_normal.push(n_hat);
}
}
}
}
}
let n_internal = owner.len();
let n_boundary = b_owner.len();
owner.extend(b_owner);
sf.extend(b_sf);
cf.extend(b_cf);
let mesh = FvMeshBuilder::new()
.n_cells(n)
.n_internal_faces(n_internal)
.owner(owner)
.neighbour(neighbour)
.patches(vec![BoundaryPatch::new(
VOXEL_SURFACE_PATCH,
n_internal,
n_boundary,
PatchKind::Patch,
)])
.cell_volumes(vec![h * h * h; n])
.cell_centres(cell_voxel.iter().map(|v| centre(*v)).collect())
.face_area_vectors(sf)
.face_centres(cf)
.build()
.map_err(|e| SteadyConductionError::Mesh(format!("{e:?}")))?;
Ok(VoxelMesh {
conduction: ConductionMesh {
mesh: Arc::new(mesh),
cell_zone,
n_zones,
},
grid: *grid,
voxel_cell,
cell_voxel,
surface_normal,
})
}