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