outram_foam_basic_lib/steady_conduction/
mesh.rs1use std::sync::Arc;
38
39use uom::si::area::square_meter;
40use uom::si::f64::{Area, Length};
41use uom::si::length::meter;
42
43use super::SteadyConductionError;
44use crate::mesh::fv_mesh::{BoundaryPatch, FvMesh, FvMeshBuilder, PatchKind};
45use crate::primitives::Vector3;
46
47#[derive(Debug, Clone)]
49pub struct ConductionMesh {
50 pub mesh: Arc<FvMesh>,
52 pub cell_zone: Vec<usize>,
54 pub n_zones: usize,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
60pub enum RadialGeometry {
61 Slab {
64 area: Area,
66 },
67 Cylinder {
69 height: Length,
71 },
72 Sphere,
74}
75
76impl RadialGeometry {
77 #[must_use]
79 pub fn exponent(self) -> i32 {
80 match self {
81 Self::Slab { .. } => 0,
82 Self::Cylinder { .. } => 1,
83 Self::Sphere => 2,
84 }
85 }
86
87 fn area_at(self, r: f64) -> f64 {
89 match self {
90 Self::Slab { area } => area.get::<square_meter>(),
91 Self::Cylinder { height } => 2.0 * std::f64::consts::PI * r * height.get::<meter>(),
92 Self::Sphere => 4.0 * std::f64::consts::PI * r * r,
93 }
94 }
95
96 fn volume_between(self, r1: f64, r2: f64) -> f64 {
98 match self {
99 Self::Slab { area } => area.get::<square_meter>() * (r2 - r1),
100 Self::Cylinder { height } => {
101 std::f64::consts::PI * height.get::<meter>() * (r2 * r2 - r1 * r1)
102 }
103 Self::Sphere => 4.0 / 3.0 * std::f64::consts::PI * (r2.powi(3) - r1.powi(3)),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq)]
110pub struct RadialShell {
111 pub outer_radius: Length,
113 pub cells: usize,
115}
116
117pub const RADIAL_SURFACE_PATCH: &str = "surface";
119pub const RADIAL_CENTRE_PATCH: &str = "centre";
122
123pub fn radial_mesh(
136 geometry: RadialGeometry,
137 shells: &[RadialShell],
138) -> Result<ConductionMesh, SteadyConductionError> {
139 if shells.is_empty() {
140 return Err(SteadyConductionError::InvalidInput(
141 "radial mesh needs a shell".into(),
142 ));
143 }
144 let mut edges = vec![0.0_f64];
146 let mut cell_zone = Vec::new();
147 for (j, shell) in shells.iter().enumerate() {
148 let r_in = edges[edges.len() - 1];
149 let r_out = shell.outer_radius.get::<meter>();
150 if shell.cells == 0 || !(r_out > r_in) || !r_out.is_finite() {
151 return Err(SteadyConductionError::InvalidInput(format!(
152 "shell {j}: needs at least one cell and an outer radius above {r_in} m (got {} cells, {r_out} m)",
153 shell.cells
154 )));
155 }
156 let dr = (r_out - r_in) / shell.cells as f64;
157 for i in 1..=shell.cells {
158 edges.push(if i == shell.cells {
159 r_out
160 } else {
161 r_in + dr * i as f64
162 });
163 cell_zone.push(j);
164 }
165 }
166 let n = cell_zone.len();
167 let axis = |x: f64| Vector3::new(x, 0.0, 0.0);
168
169 let cell_volumes: Vec<f64> = (0..n)
170 .map(|c| geometry.volume_between(edges[c], edges[c + 1]))
171 .collect();
172 let cell_centres: Vec<Vector3> = (0..n)
173 .map(|c| axis(0.5 * (edges[c] + edges[c + 1])))
174 .collect();
175
176 let mut owner: Vec<usize> = (0..n - 1).collect();
179 let neighbour: Vec<usize> = (1..n).collect();
180 let mut face_area_vectors: Vec<Vector3> = (0..n - 1)
181 .map(|c| axis(geometry.area_at(edges[c + 1])))
182 .collect();
183 let mut face_centres: Vec<Vector3> = (0..n - 1).map(|c| axis(edges[c + 1])).collect();
184 owner.push(n - 1);
185 face_area_vectors.push(axis(geometry.area_at(edges[n])));
186 face_centres.push(axis(edges[n]));
187 owner.push(0);
188 face_area_vectors.push(axis(-geometry.area_at(0.0)));
189 face_centres.push(axis(0.0));
190
191 let centre_kind = match geometry {
192 RadialGeometry::Slab { .. } => PatchKind::Symmetry,
193 _ => PatchKind::Patch,
194 };
195 let mesh = FvMeshBuilder::new()
196 .n_cells(n)
197 .n_internal_faces(n - 1)
198 .owner(owner)
199 .neighbour(neighbour)
200 .patches(vec![
201 BoundaryPatch::new(RADIAL_SURFACE_PATCH, n - 1, 1, PatchKind::Patch),
202 BoundaryPatch::new(RADIAL_CENTRE_PATCH, n, 1, centre_kind),
203 ])
204 .cell_volumes(cell_volumes)
205 .cell_centres(cell_centres)
206 .face_area_vectors(face_area_vectors)
207 .face_centres(face_centres)
208 .build()
209 .map_err(|e| SteadyConductionError::Mesh(format!("{e:?}")))?;
210 Ok(ConductionMesh {
211 mesh: Arc::new(mesh),
212 cell_zone,
213 n_zones: shells.len(),
214 })
215}
216
217#[derive(Debug, Clone, Copy, PartialEq)]
219pub struct VoxelGrid {
220 pub origin: [Length; 3],
222 pub spacing: Length,
224 pub counts: [usize; 3],
226}
227
228impl VoxelGrid {
229 #[must_use]
231 pub fn flat(&self, ijk: [usize; 3]) -> usize {
232 ijk[0] + self.counts[0] * (ijk[1] + self.counts[1] * ijk[2])
233 }
234
235 #[must_use]
237 pub fn centre(&self, ijk: [usize; 3]) -> [Length; 3] {
238 let h = self.spacing;
239 [0, 1, 2].map(|a| self.origin[a] + h * (ijk[a] as f64 + 0.5))
240 }
241
242 #[must_use]
244 pub fn voxel_of(&self, p: [Length; 3]) -> Option<[usize; 3]> {
245 let h = self.spacing.get::<meter>();
246 let mut ijk = [0usize; 3];
247 for a in 0..3 {
248 let s = (p[a] - self.origin[a]).get::<meter>() / h;
249 if !(s >= 0.0) || s >= self.counts[a] as f64 {
250 return None;
251 }
252 ijk[a] = s as usize;
253 }
254 Some(ijk)
255 }
256}
257
258#[derive(Debug, Clone)]
263pub struct VoxelMesh {
264 pub conduction: ConductionMesh,
266 pub grid: VoxelGrid,
268 pub voxel_cell: Vec<Option<usize>>,
270 pub cell_voxel: Vec<[usize; 3]>,
272 pub surface_normal: Vec<[f64; 3]>,
274}
275
276pub const VOXEL_SURFACE_PATCH: &str = "surface";
278
279impl VoxelMesh {
280 #[must_use]
282 pub fn cell_at(&self, p: [Length; 3]) -> Option<usize> {
283 self.grid
284 .voxel_of(p)
285 .and_then(|ijk| self.voxel_cell[self.grid.flat(ijk)])
286 }
287
288 #[must_use]
290 pub fn n_surface_faces(&self) -> usize {
291 self.surface_normal.len()
292 }
293}
294
295pub fn voxel_mesh<F>(grid: &VoxelGrid, zone_at: F) -> Result<VoxelMesh, SteadyConductionError>
314where
315 F: Fn([Length; 3]) -> Option<usize>,
316{
317 let h = grid.spacing.get::<meter>();
318 if grid.counts.contains(&0) || !(h > 0.0) || !h.is_finite() {
319 return Err(SteadyConductionError::InvalidInput(format!(
320 "voxel grid needs non-zero counts and a positive spacing (got {:?}, {h} m)",
321 grid.counts
322 )));
323 }
324 let [nx, ny, nz] = grid.counts;
325 let n_vox = nx * ny * nz;
326 let mut voxel_cell = vec![None; n_vox];
327 let mut cell_voxel = Vec::new();
328 let mut cell_zone = Vec::new();
329 for k in 0..nz {
330 for j in 0..ny {
331 for i in 0..nx {
332 if let Some(z) = zone_at(grid.centre([i, j, k])) {
333 voxel_cell[grid.flat([i, j, k])] = Some(cell_voxel.len());
334 cell_voxel.push([i, j, k]);
335 cell_zone.push(z);
336 }
337 }
338 }
339 }
340 let n = cell_voxel.len();
341 if n == 0 {
342 return Err(SteadyConductionError::InvalidInput(
343 "no voxel centre lies inside the body; refine the grid or check the box".into(),
344 ));
345 }
346 let n_zones = cell_zone.iter().copied().max().unwrap_or(0) + 1;
347 let o = grid.origin.map(|l| l.get::<meter>());
348 let centre = |ijk: [usize; 3]| {
349 Vector3::new(
350 o[0] + h * (ijk[0] as f64 + 0.5),
351 o[1] + h * (ijk[1] as f64 + 0.5),
352 o[2] + h * (ijk[2] as f64 + 0.5),
353 )
354 };
355 let unit = |a: usize, s: f64| {
356 let mut v = [0.0; 3];
357 v[a] = s;
358 v
359 };
360 let area = h * h;
361
362 let mut owner = Vec::new();
363 let mut neighbour = Vec::new();
364 let mut sf = Vec::new();
365 let mut cf = Vec::new();
366 let mut b_owner = Vec::new();
368 let mut b_sf = Vec::new();
369 let mut b_cf = Vec::new();
370 let mut surface_normal = Vec::new();
371 for (c, ijk) in cell_voxel.iter().enumerate() {
372 let cc = centre(*ijk);
373 for a in 0..3 {
374 for s in [-1.0_f64, 1.0] {
375 let n_hat = unit(a, s);
376 let n_vec = Vector3::new(n_hat[0], n_hat[1], n_hat[2]);
377 let neighbour_voxel = if s < 0.0 {
378 ijk[a].checked_sub(1).map(|v| {
379 let mut t = *ijk;
380 t[a] = v;
381 t
382 })
383 } else if ijk[a] + 1 < grid.counts[a] {
384 let mut t = *ijk;
385 t[a] += 1;
386 Some(t)
387 } else {
388 None
389 };
390 let nb_cell = neighbour_voxel.and_then(|t| voxel_cell[grid.flat(t)]);
391 match nb_cell {
392 Some(nb) if s > 0.0 => {
395 owner.push(c);
396 neighbour.push(nb);
397 sf.push(n_vec * area);
398 cf.push(cc + n_vec * (0.5 * h));
399 }
400 Some(_) => {}
401 None => {
402 b_owner.push(c);
403 b_sf.push(n_vec * area);
404 b_cf.push(cc + n_vec * (0.5 * h));
405 surface_normal.push(n_hat);
406 }
407 }
408 }
409 }
410 }
411 let n_internal = owner.len();
412 let n_boundary = b_owner.len();
413 owner.extend(b_owner);
414 sf.extend(b_sf);
415 cf.extend(b_cf);
416 let mesh = FvMeshBuilder::new()
417 .n_cells(n)
418 .n_internal_faces(n_internal)
419 .owner(owner)
420 .neighbour(neighbour)
421 .patches(vec![BoundaryPatch::new(
422 VOXEL_SURFACE_PATCH,
423 n_internal,
424 n_boundary,
425 PatchKind::Patch,
426 )])
427 .cell_volumes(vec![h * h * h; n])
428 .cell_centres(cell_voxel.iter().map(|v| centre(*v)).collect())
429 .face_area_vectors(sf)
430 .face_centres(cf)
431 .build()
432 .map_err(|e| SteadyConductionError::Mesh(format!("{e:?}")))?;
433 Ok(VoxelMesh {
434 conduction: ConductionMesh {
435 mesh: Arc::new(mesh),
436 cell_zone,
437 n_zones,
438 },
439 grid: *grid,
440 voxel_cell,
441 cell_voxel,
442 surface_normal,
443 })
444}