1use crate::data::section::Section;
17use crate::data::storage::Storage;
18use crate::discretization::runtime::{
19 CellGeometry, FaceGeometry, FluxStencil, discretization_capability,
20};
21use crate::mesh_error::MeshSieveError;
22use crate::topology::cell_type::CellType;
23use crate::topology::coastal::{
24 BOUNDARY_CLASS_LABEL, BOUNDARY_ROLE_LABEL, BoundaryClass, OpenBoundaryRole, WET_DRY_MASK_LABEL,
25 WetDryMask,
26};
27use crate::topology::labels::LabelSet;
28use crate::topology::point::PointId;
29use crate::topology::sieve::Sieve;
30use std::collections::{HashMap, HashSet};
31
32pub fn validate_fv_cell_type(cell_type: CellType) -> Result<(), MeshSieveError> {
34 let supported = match cell_type {
35 CellType::Simplex(dim) => (1..=3).contains(&dim),
36 CellType::Polygon(_) | CellType::Polyhedron => true,
37 other => discretization_capability(other)
38 .map(|capability| capability.finite_volume)
39 .unwrap_or(false),
40 };
41 if supported {
42 Ok(())
43 } else {
44 Err(MeshSieveError::InvalidGeometry(format!(
45 "unsupported finite-volume cell topology {cell_type:?}"
46 )))
47 }
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum FaceKind {
52 Internal,
53 Boundary,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq)]
57pub enum ConvectiveScheme {
58 Upwind,
59 Central,
60 BoundedLinear {
61 blend: f64,
62 },
63 BlendUpwindCentral {
64 blend: f64,
65 },
66 HighResolution {
67 blend: f64,
68 limiter: SlopeLimiterFamily,
69 },
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum ReconstructionGradient {
74 LeastSquares,
75 GreenGauss,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum ReconstructionMode {
80 GradientOnly(ReconstructionGradient),
81 LeastSquaresWithGreenGaussFallback,
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum SlopeLimiterFamily {
86 None,
87 Minmod,
88 VanLeer,
89 Superbee,
90}
91
92#[derive(Clone, Copy, Debug, PartialEq)]
93pub struct ReconstructionSettings {
94 pub mode: ReconstructionMode,
95 pub limiter: LimiterOption,
96}
97
98impl Default for ReconstructionSettings {
99 fn default() -> Self {
100 Self {
101 mode: ReconstructionMode::LeastSquaresWithGreenGaussFallback,
102 limiter: LimiterOption::Family(SlopeLimiterFamily::None),
103 }
104 }
105}
106
107pub trait BoundedReconstructionLimiter {
108 fn limiter_factor(&self, upwind: f64, downwind: f64) -> f64;
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum LimiterOption {
113 None,
114 Family(SlopeLimiterFamily),
115}
116
117impl BoundedReconstructionLimiter for LimiterOption {
118 fn limiter_factor(&self, upwind: f64, downwind: f64) -> f64 {
119 match self {
120 LimiterOption::None => 1.0,
121 LimiterOption::Family(f) => slope_limiter(*f, upwind, downwind),
122 }
123 }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq)]
127pub enum BoundaryCondition {
128 Dirichlet { value: f64 },
129 Neumann { gradient: f64 },
130 Robin { alpha: f64, beta: f64, gamma: f64 },
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
135pub enum FvBoundaryBranch {
136 Open,
137 Inflow,
138 Outflow,
139 Tidal,
140 Bed,
141 FreeSurface,
142}
143
144#[derive(Clone, Debug, PartialEq, Eq)]
146pub enum BoundaryBranchError {
147 MissingBoundaryClass {
148 face: PointId,
149 },
150 OpenBoundaryMissingRole {
151 face: PointId,
152 },
153 OpenBoundaryConflictingRoles {
154 face: PointId,
155 roles: Vec<OpenBoundaryRole>,
156 },
157}
158
159#[derive(Clone, Copy, Debug, PartialEq)]
160pub struct DiffusionSettings {
161 pub diffusivity: f64,
162 pub non_orthogonal_mode: NonOrthogonalCorrectionMode,
163}
164
165#[derive(Clone, Copy, Debug, PartialEq, Eq)]
166pub enum NonOrthogonalCorrectionMode {
167 OrthogonalOnly,
168 Deferred,
169 FullyCorrected,
170}
171
172impl Default for DiffusionSettings {
173 fn default() -> Self {
174 Self {
175 diffusivity: 1.0,
176 non_orthogonal_mode: NonOrthogonalCorrectionMode::FullyCorrected,
177 }
178 }
179}
180
181pub trait FvmPhysicsHooks {
182 fn turbulence_flux(&self, _stencil: &FluxStencil, _inputs: &FvmInputs) -> f64 {
183 0.0
184 }
185 fn free_surface_flux(&self, _stencil: &FluxStencil, _inputs: &FvmInputs) -> f64 {
186 0.0
187 }
188 fn turbulence_source(&self, _cell: PointId, _inputs: &FvmInputs) -> f64 {
189 0.0
190 }
191 fn free_surface_source(&self, _cell: PointId, _inputs: &FvmInputs) -> f64 {
192 0.0
193 }
194}
195
196pub trait FvmSourceFluxHook {
197 fn additional_face_flux(&self, _stencil: &FluxStencil, _inputs: &FvmInputs) -> f64 {
198 0.0
199 }
200 fn additional_cell_source(&self, _cell: PointId, _inputs: &FvmInputs) -> f64 {
201 0.0
202 }
203}
204
205#[derive(Clone, Debug, Default, PartialEq)]
206pub struct FluxAssembly {
207 pub residual: HashMap<PointId, f64>,
208 pub source: HashMap<PointId, f64>,
209}
210
211#[derive(Clone, Debug, PartialEq)]
212pub struct FvmFieldSections {
213 pub cell_scalar: HashMap<PointId, f64>,
214 pub cell_gradient: HashMap<PointId, Vec<f64>>,
215 pub face_mass_flux: HashMap<PointId, f64>,
216}
217
218#[derive(Clone, Debug, PartialEq)]
219pub struct FvmSchemeSettings {
220 pub convective: ConvectiveScheme,
221 pub reconstruction: ReconstructionSettings,
222 pub diffusion: DiffusionSettings,
223}
224
225#[derive(Clone, Debug, PartialEq)]
226pub struct FvmAssemblyOutput {
227 pub convective: FluxAssembly,
228 pub diffusive: FluxAssembly,
229 pub total: FluxAssembly,
230}
231
232#[derive(Debug, PartialEq)]
233pub enum FvmAssemblyError {
234 MissingFaceMetric {
235 face: PointId,
236 },
237 MissingCellMetric {
238 cell: PointId,
239 },
240 MissingBoundaryFaceLabel {
241 face: PointId,
242 },
243 LabelMappedToUnknownFace {
244 face: PointId,
245 },
246 LabelMappedToInternalFace {
247 face: PointId,
248 },
249 UnsupportedBoundaryBranch {
250 face: PointId,
251 branch: FvBoundaryBranch,
252 },
253 MissingBoundaryClosure {
254 face: PointId,
255 branch: FvBoundaryBranch,
256 kind: FaceKind,
257 },
258 Kernel(MeshSieveError),
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub enum UnsupportedBoundaryBehavior {
263 Error,
264 Ignore,
265}
266
267#[derive(Clone, Debug, PartialEq)]
268pub struct FvBoundaryPolicy {
269 pub boundary_face_branches: HashMap<PointId, FvBoundaryBranch>,
270 pub allowed_branches: HashSet<FvBoundaryBranch>,
271 pub convective_branch_hooks: HashMap<FvBoundaryBranch, BoundaryCondition>,
272 pub diffusive_branch_hooks: HashMap<FvBoundaryBranch, BoundaryCondition>,
273 pub unsupported_behavior: UnsupportedBoundaryBehavior,
274}
275
276impl FvBoundaryPolicy {
277 fn build_face_bcs(
278 &self,
279 kind: FaceKind,
280 ) -> Result<HashMap<PointId, BoundaryCondition>, FvmAssemblyError> {
281 let branch_hooks = match kind {
282 FaceKind::Internal => return Ok(HashMap::new()),
283 FaceKind::Boundary => &self.convective_branch_hooks,
284 };
285 let mut result = HashMap::with_capacity(self.boundary_face_branches.len());
286 for (face, branch) in &self.boundary_face_branches {
287 if let Some(bc) = branch_hooks.get(branch) {
288 result.insert(*face, *bc);
289 } else if self.unsupported_behavior == UnsupportedBoundaryBehavior::Error {
290 return Err(FvmAssemblyError::MissingBoundaryClosure {
291 face: *face,
292 branch: *branch,
293 kind,
294 });
295 }
296 }
297 Ok(result)
298 }
299
300 fn build_diffusive_face_bcs(
301 &self,
302 ) -> Result<HashMap<PointId, BoundaryCondition>, FvmAssemblyError> {
303 let mut result = HashMap::with_capacity(self.boundary_face_branches.len());
304 for (face, branch) in &self.boundary_face_branches {
305 if let Some(bc) = self.diffusive_branch_hooks.get(branch) {
306 result.insert(*face, *bc);
307 } else if self.unsupported_behavior == UnsupportedBoundaryBehavior::Error {
308 return Err(FvmAssemblyError::MissingBoundaryClosure {
309 face: *face,
310 branch: *branch,
311 kind: FaceKind::Boundary,
312 });
313 }
314 }
315 Ok(result)
316 }
317}
318
319impl From<MeshSieveError> for FvmAssemblyError {
320 fn from(value: MeshSieveError) -> Self {
321 Self::Kernel(value)
322 }
323}
324
325pub fn assemble_fv_system(
326 inputs: &FvmInputs,
327 fields: &FvmFieldSections,
328 boundary_policy: &FvBoundaryPolicy,
329 schemes: FvmSchemeSettings,
330) -> Result<FvmAssemblyOutput, FvmAssemblyError> {
331 preflight_fv_assembly(inputs, boundary_policy)?;
332 let convective_bcs = boundary_policy.build_face_bcs(FaceKind::Boundary)?;
333 let diffusive_bcs = boundary_policy.build_diffusive_face_bcs()?;
334 let convective = assemble_convective_fluxes_with_reconstruction(
335 inputs,
336 &fields.cell_scalar,
337 &fields.face_mass_flux,
338 &convective_bcs,
339 schemes.convective,
340 schemes.reconstruction,
341 )?;
342 let diffusive = assemble_diffusive_fluxes(
343 inputs,
344 &fields.cell_scalar,
345 &fields.cell_gradient,
346 &diffusive_bcs,
347 schemes.diffusion,
348 )?;
349 let mut total = FluxAssembly::default();
350 for (cell, value) in convective.residual.iter().chain(diffusive.residual.iter()) {
351 total.add_residual(*cell, *value);
352 }
353 for (cell, value) in convective.source.iter().chain(diffusive.source.iter()) {
354 total.add_source(*cell, *value);
355 }
356 Ok(FvmAssemblyOutput {
357 convective,
358 diffusive,
359 total,
360 })
361}
362
363fn preflight_fv_assembly(
364 inputs: &FvmInputs,
365 boundary_policy: &FvBoundaryPolicy,
366) -> Result<(), FvmAssemblyError> {
367 for stencil in inputs.internal_faces() {
368 if inputs.face_metrics(stencil.face).is_none() {
369 return Err(FvmAssemblyError::MissingFaceMetric { face: stencil.face });
370 }
371 if inputs.cell_metrics(stencil.left).is_none() {
372 return Err(FvmAssemblyError::MissingCellMetric { cell: stencil.left });
373 }
374 let right = stencil.right.expect("internal");
375 if inputs.cell_metrics(right).is_none() {
376 return Err(FvmAssemblyError::MissingCellMetric { cell: right });
377 }
378 }
379 let boundary_faces: HashSet<_> = inputs.boundary_faces().map(|s| s.face).collect();
380 for face in boundary_policy.boundary_face_branches.keys() {
381 if inputs.face_metrics(*face).is_none() {
382 return Err(FvmAssemblyError::LabelMappedToUnknownFace { face: *face });
383 }
384 if !boundary_faces.contains(face) {
385 return Err(FvmAssemblyError::LabelMappedToInternalFace { face: *face });
386 }
387 }
388 for stencil in inputs.boundary_faces() {
389 if inputs.face_metrics(stencil.face).is_none() {
390 return Err(FvmAssemblyError::MissingFaceMetric { face: stencil.face });
391 }
392 if inputs.cell_metrics(stencil.left).is_none() {
393 return Err(FvmAssemblyError::MissingCellMetric { cell: stencil.left });
394 }
395 let branch = boundary_policy
396 .boundary_face_branches
397 .get(&stencil.face)
398 .copied()
399 .ok_or(FvmAssemblyError::MissingBoundaryFaceLabel { face: stencil.face })?;
400 if !boundary_policy.allowed_branches.contains(&branch) {
401 if boundary_policy.unsupported_behavior == UnsupportedBoundaryBehavior::Error {
402 return Err(FvmAssemblyError::UnsupportedBoundaryBranch {
403 face: stencil.face,
404 branch,
405 });
406 }
407 continue;
408 }
409 }
410 Ok(())
411}
412
413impl FluxAssembly {
414 fn add_residual(&mut self, cell: PointId, value: f64) {
415 *self.residual.entry(cell).or_insert(0.0) += value;
416 }
417 fn add_source(&mut self, cell: PointId, value: f64) {
418 *self.source.entry(cell).or_insert(0.0) += value;
419 }
420}
421
422#[derive(Clone, Debug, Default)]
423pub struct FvmFaceLoops {
424 pub internal: Vec<FluxStencil>,
425 pub boundary: Vec<FluxStencil>,
426}
427
428#[derive(Clone, Debug, Default)]
429pub struct FvmInputs {
430 pub loops: FvmFaceLoops,
431 pub cell_geometry: Vec<(PointId, CellGeometry)>,
432 pub face_geometry: Vec<(PointId, FaceGeometry)>,
433 cell_index: HashMap<PointId, usize>,
434 face_index: HashMap<PointId, usize>,
435 internal_owner_neighbor_idx: Vec<(usize, usize)>,
436 boundary_owner_idx: Vec<usize>,
437 internal_face_geom_idx: Vec<usize>,
438 boundary_face_geom_idx: Vec<usize>,
439 packed_cache: Option<PackedFvmInputs>,
440}
441
442#[derive(Clone, Debug, Default)]
443pub struct PackedFvmInputs {
444 pub internal_faces: Vec<PackedInternalFace>,
445 pub boundary_faces: Vec<PackedBoundaryFace>,
446}
447
448#[derive(Clone, Debug, Default)]
449pub struct FvmPackedCache {
450 pub packed: PackedFvmInputs,
451}
452
453#[derive(Clone, Debug)]
454pub struct PackedInternalFace {
455 pub face: PointId,
456 pub owner: PointId,
457 pub neighbor: PointId,
458 pub face_geom_idx: usize,
459 pub owner_cell_geom_idx: usize,
460 pub neighbor_cell_geom_idx: usize,
461}
462
463#[derive(Clone, Debug)]
464pub struct PackedBoundaryFace {
465 pub face: PointId,
466 pub owner: PointId,
467 pub face_geom_idx: usize,
468 pub owner_cell_geom_idx: usize,
469}
470
471impl FvmInputs {
472 pub fn new(
473 stencils: impl IntoIterator<Item = FluxStencil>,
474 cell_geometry: Vec<(PointId, CellGeometry)>,
475 face_geometry: Vec<(PointId, FaceGeometry)>,
476 ) -> Self {
477 let mut loops = FvmFaceLoops::default();
478 for stencil in stencils {
479 if stencil.right.is_some() {
480 loops.internal.push(stencil);
481 } else {
482 loops.boundary.push(stencil);
483 }
484 }
485 let mut inputs = Self {
486 loops,
487 cell_geometry,
488 face_geometry,
489 cell_index: HashMap::new(),
490 face_index: HashMap::new(),
491 internal_owner_neighbor_idx: Vec::new(),
492 boundary_owner_idx: Vec::new(),
493 internal_face_geom_idx: Vec::new(),
494 boundary_face_geom_idx: Vec::new(),
495 packed_cache: None,
496 };
497 inputs.rebuild_indices();
498 inputs
499 }
500
501 pub fn internal_faces(&self) -> impl Iterator<Item = &FluxStencil> {
502 self.loops.internal.iter()
503 }
504 pub fn boundary_faces(&self) -> impl Iterator<Item = &FluxStencil> {
505 self.loops.boundary.iter()
506 }
507 pub fn cell_metrics(&self, cell: PointId) -> Option<&CellGeometry> {
508 self.cell_index
509 .get(&cell)
510 .and_then(|&idx| self.cell_geometry.get(idx).map(|(_, m)| m))
511 }
512 pub fn face_metrics(&self, face: PointId) -> Option<&FaceGeometry> {
513 self.face_index
514 .get(&face)
515 .and_then(|&idx| self.face_geometry.get(idx).map(|(_, m)| m))
516 }
517
518 pub fn internal_owner_neighbor_indices(&self) -> &[(usize, usize)] {
519 &self.internal_owner_neighbor_idx
520 }
521
522 pub fn boundary_owner_indices(&self) -> &[usize] {
523 &self.boundary_owner_idx
524 }
525
526 pub fn internal_face_geometry_indices(&self) -> &[usize] {
527 &self.internal_face_geom_idx
528 }
529
530 pub fn boundary_face_geometry_indices(&self) -> &[usize] {
531 &self.boundary_face_geom_idx
532 }
533
534 pub fn packed(&self) -> Option<&PackedFvmInputs> {
535 self.packed_cache.as_ref()
536 }
537
538 pub fn build_packed_cache(&mut self) {
539 let mut internal_faces = Vec::with_capacity(self.loops.internal.len());
540 let mut boundary_faces = Vec::with_capacity(self.loops.boundary.len());
541 for stencil in &self.loops.internal {
542 if let (Some(&fi), Some(&oi), Some(neighbor), Some(&ni)) = (
543 self.face_index.get(&stencil.face),
544 self.cell_index.get(&stencil.left),
545 stencil.right,
546 stencil.right.and_then(|r| self.cell_index.get(&r)),
547 ) {
548 internal_faces.push(PackedInternalFace {
549 face: stencil.face,
550 owner: stencil.left,
551 neighbor,
552 face_geom_idx: fi,
553 owner_cell_geom_idx: oi,
554 neighbor_cell_geom_idx: ni,
555 });
556 }
557 }
558 for stencil in &self.loops.boundary {
559 if let (Some(&fi), Some(&oi)) = (
560 self.face_index.get(&stencil.face),
561 self.cell_index.get(&stencil.left),
562 ) {
563 boundary_faces.push(PackedBoundaryFace {
564 face: stencil.face,
565 owner: stencil.left,
566 face_geom_idx: fi,
567 owner_cell_geom_idx: oi,
568 });
569 }
570 }
571 self.packed_cache = Some(PackedFvmInputs {
572 internal_faces,
573 boundary_faces,
574 });
575 }
576
577 pub fn build_packed_cache_into(&self, cache: &mut FvmPackedCache) {
578 cache.packed.internal_faces.clear();
579 cache.packed.boundary_faces.clear();
580 cache
581 .packed
582 .internal_faces
583 .reserve(self.loops.internal.len());
584 cache
585 .packed
586 .boundary_faces
587 .reserve(self.loops.boundary.len());
588 for stencil in &self.loops.internal {
589 if let (Some(&fi), Some(&oi), Some(neighbor), Some(&ni)) = (
590 self.face_index.get(&stencil.face),
591 self.cell_index.get(&stencil.left),
592 stencil.right,
593 stencil.right.and_then(|r| self.cell_index.get(&r)),
594 ) {
595 cache.packed.internal_faces.push(PackedInternalFace {
596 face: stencil.face,
597 owner: stencil.left,
598 neighbor,
599 face_geom_idx: fi,
600 owner_cell_geom_idx: oi,
601 neighbor_cell_geom_idx: ni,
602 });
603 }
604 }
605 for stencil in &self.loops.boundary {
606 if let (Some(&fi), Some(&oi)) = (
607 self.face_index.get(&stencil.face),
608 self.cell_index.get(&stencil.left),
609 ) {
610 cache.packed.boundary_faces.push(PackedBoundaryFace {
611 face: stencil.face,
612 owner: stencil.left,
613 face_geom_idx: fi,
614 owner_cell_geom_idx: oi,
615 });
616 }
617 }
618 }
619
620 fn rebuild_indices(&mut self) {
621 self.cell_index.clear();
622 self.face_index.clear();
623 for (idx, (id, _)) in self.cell_geometry.iter().enumerate() {
624 self.cell_index.insert(*id, idx);
625 }
626 for (idx, (id, _)) in self.face_geometry.iter().enumerate() {
627 self.face_index.insert(*id, idx);
628 }
629 self.internal_owner_neighbor_idx.clear();
630 self.internal_face_geom_idx.clear();
631 self.internal_owner_neighbor_idx
632 .reserve(self.loops.internal.len());
633 self.internal_face_geom_idx
634 .reserve(self.loops.internal.len());
635 for stencil in &self.loops.internal {
636 if let (Some(&owner), Some(neighbor), Some(&neigh)) = (
637 self.cell_index.get(&stencil.left),
638 stencil.right,
639 stencil.right.and_then(|r| self.cell_index.get(&r)),
640 ) {
641 let _ = neighbor;
642 self.internal_owner_neighbor_idx.push((owner, neigh));
643 }
644 if let Some(&fidx) = self.face_index.get(&stencil.face) {
645 self.internal_face_geom_idx.push(fidx);
646 }
647 }
648 self.boundary_owner_idx.clear();
649 self.boundary_face_geom_idx.clear();
650 self.boundary_owner_idx.reserve(self.loops.boundary.len());
651 self.boundary_face_geom_idx
652 .reserve(self.loops.boundary.len());
653 for stencil in &self.loops.boundary {
654 if let Some(&owner) = self.cell_index.get(&stencil.left) {
655 self.boundary_owner_idx.push(owner);
656 }
657 if let Some(&fidx) = self.face_index.get(&stencil.face) {
658 self.boundary_face_geom_idx.push(fidx);
659 }
660 }
661 }
662}
663
664pub fn classify_face_loops<T>(
665 topology: &T,
666 faces: impl IntoIterator<Item = PointId>,
667) -> Result<FvmFaceLoops, MeshSieveError>
668where
669 T: Sieve<Point = PointId>,
670{
671 let mut loops = FvmFaceLoops::default();
672 for face in faces {
673 let mut cells: Vec<_> = topology.support_points(face).collect();
674 cells.sort_unstable();
675 match cells.as_slice() {
676 [left] => loops.boundary.push(FluxStencil {
677 face,
678 left: *left,
679 right: None,
680 }),
681 [left, right] => loops.internal.push(FluxStencil {
682 face,
683 left: *left,
684 right: Some(*right),
685 }),
686 [] => {
687 return Err(MeshSieveError::InvalidGeometry(format!(
688 "face {face:?} has no supporting cells"
689 )));
690 }
691 _ => {
692 return Err(MeshSieveError::InvalidGeometry(format!(
693 "non-manifold face {face:?} has {} support cells",
694 cells.len()
695 )));
696 }
697 }
698 }
699 Ok(loops)
700}
701
702pub fn interpolate_face_centered_scalar<S: Storage<f64>>(
703 section: &Section<f64, S>,
704 stencil: &FluxStencil,
705) -> Result<f64, MeshSieveError> {
706 let left = section.try_restrict(stencil.left)?;
707 let lval = *left.first().ok_or_else(|| {
708 MeshSieveError::InvalidGeometry(format!("missing scalar value at cell {}", stencil.left))
709 })?;
710 if let Some(right_cell) = stencil.right {
711 let right = section.try_restrict(right_cell)?;
712 let rval = *right.first().ok_or_else(|| {
713 MeshSieveError::InvalidGeometry(format!("missing scalar value at cell {right_cell}"))
714 })?;
715 Ok(0.5 * (lval + rval))
716 } else {
717 Ok(lval)
718 }
719}
720
721pub fn interpolate_cell_centered_scalar<S: Storage<f64>>(
722 section: &Section<f64, S>,
723 incident_faces: &[PointId],
724) -> Result<f64, MeshSieveError> {
725 if incident_faces.is_empty() {
726 return Err(MeshSieveError::InvalidGeometry(
727 "cannot interpolate to cell center from zero incident faces".to_string(),
728 ));
729 }
730 let mut accum = 0.0;
731 for face in incident_faces {
732 let value = section.try_restrict(*face)?;
733 let scalar = *value.first().ok_or_else(|| {
734 MeshSieveError::InvalidGeometry(format!("missing scalar value at face {face}"))
735 })?;
736 accum += scalar;
737 }
738 Ok(accum / incident_faces.len() as f64)
739}
740
741pub fn assemble_convective_fluxes(
742 inputs: &FvmInputs,
743 cell_scalar: &HashMap<PointId, f64>,
744 face_mass_flux: &HashMap<PointId, f64>,
745 boundary_conditions: &HashMap<PointId, BoundaryCondition>,
746 scheme: ConvectiveScheme,
747) -> Result<FluxAssembly, MeshSieveError> {
748 assemble_convective_fluxes_with_reconstruction(
749 inputs,
750 cell_scalar,
751 face_mass_flux,
752 boundary_conditions,
753 scheme,
754 ReconstructionSettings::default(),
755 )
756}
757
758pub fn assemble_convective_fluxes_with_reconstruction(
759 inputs: &FvmInputs,
760 cell_scalar: &HashMap<PointId, f64>,
761 face_mass_flux: &HashMap<PointId, f64>,
762 boundary_conditions: &HashMap<PointId, BoundaryCondition>,
763 scheme: ConvectiveScheme,
764 reconstruction: ReconstructionSettings,
765) -> Result<FluxAssembly, MeshSieveError> {
766 assemble_convective_fluxes_masked(
767 inputs,
768 cell_scalar,
769 face_mass_flux,
770 boundary_conditions,
771 scheme,
772 reconstruction,
773 None,
774 )
775}
776
777#[derive(Clone, Debug, Default)]
779pub struct FluxActivityMask {
780 pub boundary_faces_active: HashMap<PointId, bool>,
781 pub near_boundary_faces_active: HashMap<PointId, bool>,
782}
783
784fn face_is_active(mask: Option<&FluxActivityMask>, stencil: &FluxStencil, boundary: bool) -> bool {
785 let Some(mask) = mask else { return true };
786 if boundary {
787 return mask
788 .boundary_faces_active
789 .get(&stencil.face)
790 .copied()
791 .unwrap_or(true);
792 }
793 if mask
794 .near_boundary_faces_active
795 .get(&stencil.face)
796 .copied()
797 .unwrap_or(true)
798 {
799 return true;
800 }
801 false
802}
803
804pub fn flux_activity_mask_from_wet_dry(inputs: &FvmInputs, labels: &LabelSet) -> FluxActivityMask {
806 let mut mask = FluxActivityMask::default();
807 let dry = WetDryMask::Dry.code();
808 let cell_is_dry = |cell: PointId| labels.get_label(cell, WET_DRY_MASK_LABEL) == Some(dry);
809 for stencil in inputs.boundary_faces() {
810 mask.boundary_faces_active
811 .insert(stencil.face, !cell_is_dry(stencil.left));
812 }
813 for stencil in inputs.internal_faces() {
814 let near_boundary = labels
815 .get_label(stencil.face, BOUNDARY_CLASS_LABEL)
816 .is_some();
817 if near_boundary {
818 let left_dry = cell_is_dry(stencil.left);
819 let right_dry = stencil.right.map(cell_is_dry).unwrap_or(true);
820 mask.near_boundary_faces_active
821 .insert(stencil.face, !(left_dry || right_dry));
822 }
823 }
824 mask
825}
826
827pub fn assemble_convective_fluxes_masked(
829 inputs: &FvmInputs,
830 cell_scalar: &HashMap<PointId, f64>,
831 face_mass_flux: &HashMap<PointId, f64>,
832 boundary_conditions: &HashMap<PointId, BoundaryCondition>,
833 scheme: ConvectiveScheme,
834 reconstruction: ReconstructionSettings,
835 activity_mask: Option<&FluxActivityMask>,
836) -> Result<FluxAssembly, MeshSieveError> {
837 let mut assembly = FluxAssembly::default();
838 for stencil in inputs.internal_faces() {
839 if !face_is_active(activity_mask, stencil, false) {
840 continue;
841 }
842 let mdot = *face_mass_flux.get(&stencil.face).ok_or_else(|| {
843 MeshSieveError::InvalidGeometry(format!("missing mass flux for face {}", stencil.face))
844 })?;
845 let l = *cell_scalar.get(&stencil.left).ok_or_else(|| {
846 MeshSieveError::InvalidGeometry(format!("missing scalar for cell {}", stencil.left))
847 })?;
848 let rcell = stencil.right.expect("internal face must have neighbor");
849 let r = *cell_scalar.get(&rcell).ok_or_else(|| {
850 MeshSieveError::InvalidGeometry(format!("missing scalar for cell {rcell}"))
851 })?;
852 let phi_f = convective_face_value(l, r, mdot, scheme, reconstruction.limiter);
853 let flux = mdot * phi_f;
854 assembly.add_residual(stencil.left, flux);
855 assembly.add_residual(rcell, -flux);
856 }
857 for stencil in inputs.boundary_faces() {
858 if !face_is_active(activity_mask, stencil, true) {
859 continue;
860 }
861 let mdot = *face_mass_flux.get(&stencil.face).ok_or_else(|| {
862 MeshSieveError::InvalidGeometry(format!(
863 "missing mass flux for boundary face {}",
864 stencil.face
865 ))
866 })?;
867 let phi_i = *cell_scalar.get(&stencil.left).ok_or_else(|| {
868 MeshSieveError::InvalidGeometry(format!("missing scalar for cell {}", stencil.left))
869 })?;
870 let bc = boundary_conditions
871 .get(&stencil.face)
872 .copied()
873 .ok_or_else(|| {
874 MeshSieveError::InvalidGeometry(format!(
875 "missing boundary condition for face {}",
876 stencil.face
877 ))
878 })?;
879 let phi_b = match bc {
880 BoundaryCondition::Dirichlet { value } => value,
881 BoundaryCondition::Neumann { .. } => phi_i,
882 BoundaryCondition::Robin { alpha, beta, gamma } => {
883 if alpha.abs() < 1e-14 {
884 phi_i
885 } else {
886 (gamma - beta * phi_i) / alpha
887 }
888 }
889 };
890 let phi_f = convective_face_value(phi_i, phi_b, mdot, scheme, reconstruction.limiter);
891 assembly.add_residual(stencil.left, mdot * phi_f);
892 }
893 Ok(assembly)
894}
895
896pub fn boundary_branch_for_face(labels: &LabelSet, face: PointId) -> Option<FvBoundaryBranch> {
897 match labels.get_label(face, BOUNDARY_CLASS_LABEL) {
898 Some(v) if v == BoundaryClass::FreeSurface.code() => Some(FvBoundaryBranch::FreeSurface),
899 Some(v) if v == BoundaryClass::Bed.code() => Some(FvBoundaryBranch::Bed),
900 Some(v) if v == BoundaryClass::Open.code() => {
901 match labels.get_label(face, BOUNDARY_ROLE_LABEL) {
902 Some(v) if v == OpenBoundaryRole::Inflow.code() => Some(FvBoundaryBranch::Inflow),
903 Some(v) if v == OpenBoundaryRole::Outflow.code() => Some(FvBoundaryBranch::Outflow),
904 Some(v) if v == OpenBoundaryRole::Tidal.code() => Some(FvBoundaryBranch::Tidal),
905 _ => Some(FvBoundaryBranch::Open),
906 }
907 }
908 _ => None,
909 }
910}
911
912pub fn boundary_branch_for_face_checked(
914 labels: &LabelSet,
915 face: PointId,
916) -> Result<FvBoundaryBranch, BoundaryBranchError> {
917 let Some(class) = labels.get_label(face, BOUNDARY_CLASS_LABEL) else {
918 return Err(BoundaryBranchError::MissingBoundaryClass { face });
919 };
920 if class == BoundaryClass::FreeSurface.code() {
921 return Ok(FvBoundaryBranch::FreeSurface);
922 }
923 if class == BoundaryClass::Bed.code() {
924 return Ok(FvBoundaryBranch::Bed);
925 }
926 if class != BoundaryClass::Open.code() {
927 return Err(BoundaryBranchError::MissingBoundaryClass { face });
928 }
929 let inflow =
930 labels.get_label(face, BOUNDARY_ROLE_LABEL) == Some(OpenBoundaryRole::Inflow.code());
931 let outflow =
932 labels.get_label(face, BOUNDARY_ROLE_LABEL) == Some(OpenBoundaryRole::Outflow.code());
933 let tidal = labels.get_label(face, BOUNDARY_ROLE_LABEL) == Some(OpenBoundaryRole::Tidal.code());
934 let mut roles = Vec::new();
935 if inflow {
936 roles.push(OpenBoundaryRole::Inflow);
937 }
938 if outflow {
939 roles.push(OpenBoundaryRole::Outflow);
940 }
941 if tidal {
942 roles.push(OpenBoundaryRole::Tidal);
943 }
944 match roles.as_slice() {
945 [OpenBoundaryRole::Inflow] => Ok(FvBoundaryBranch::Inflow),
946 [OpenBoundaryRole::Outflow] => Ok(FvBoundaryBranch::Outflow),
947 [OpenBoundaryRole::Tidal] => Ok(FvBoundaryBranch::Tidal),
948 [] => Err(BoundaryBranchError::OpenBoundaryMissingRole { face }),
949 _ => Err(BoundaryBranchError::OpenBoundaryConflictingRoles { face, roles }),
950 }
951}
952
953pub fn assemble_diffusive_fluxes(
954 inputs: &FvmInputs,
955 cell_scalar: &HashMap<PointId, f64>,
956 cell_gradient: &HashMap<PointId, Vec<f64>>,
957 boundary_conditions: &HashMap<PointId, BoundaryCondition>,
958 settings: DiffusionSettings,
959) -> Result<FluxAssembly, MeshSieveError> {
960 assemble_diffusive_fluxes_with_hooks(
961 inputs,
962 cell_scalar,
963 cell_gradient,
964 boundary_conditions,
965 settings,
966 None,
967 )
968}
969
970pub fn assemble_diffusive_fluxes_with_hooks(
971 inputs: &FvmInputs,
972 cell_scalar: &HashMap<PointId, f64>,
973 cell_gradient: &HashMap<PointId, Vec<f64>>,
974 boundary_conditions: &HashMap<PointId, BoundaryCondition>,
975 settings: DiffusionSettings,
976 hooks: Option<&dyn FvmPhysicsHooks>,
977) -> Result<FluxAssembly, MeshSieveError> {
978 let mut assembly = FluxAssembly::default();
979 for stencil in inputs.internal_faces() {
980 let fg = inputs.face_metrics(stencil.face).ok_or_else(|| {
981 MeshSieveError::InvalidGeometry(format!(
982 "missing face geometry for face {}",
983 stencil.face
984 ))
985 })?;
986 let lc = inputs.cell_metrics(stencil.left).ok_or_else(|| {
987 MeshSieveError::InvalidGeometry(format!(
988 "missing cell geometry for cell {}",
989 stencil.left
990 ))
991 })?;
992 let rcell = stencil.right.expect("internal face");
993 let rc = inputs.cell_metrics(rcell).ok_or_else(|| {
994 MeshSieveError::InvalidGeometry(format!("missing cell geometry for cell {rcell}"))
995 })?;
996 let phi_l = *cell_scalar.get(&stencil.left).ok_or_else(|| {
997 MeshSieveError::InvalidGeometry(format!("missing scalar for cell {}", stencil.left))
998 })?;
999 let phi_r = *cell_scalar.get(&rcell).ok_or_else(|| {
1000 MeshSieveError::InvalidGeometry(format!("missing scalar for cell {rcell}"))
1001 })?;
1002 let d = sub(&rc.centroid, &lc.centroid);
1003 let d2 = dot(&d, &d);
1004 let a = &fg.normal;
1005 let orth = if d2 > 0.0 {
1006 (phi_r - phi_l) * dot(a, &d) / d2
1007 } else {
1008 0.0
1009 };
1010 let mut flux = -settings.diffusivity * orth;
1011 let mut non_orth_flux = 0.0;
1012 if settings.non_orthogonal_mode != NonOrthogonalCorrectionMode::OrthogonalOnly {
1013 let gf = avg(
1014 cell_gradient.get(&stencil.left),
1015 cell_gradient.get(&rcell),
1016 a.len(),
1017 )?;
1018 let a_orth = if d2 > 0.0 {
1019 scale(&d, dot(a, &d) / d2)
1020 } else {
1021 vec![0.0; a.len()]
1022 };
1023 let a_non = sub(a, &a_orth);
1024 non_orth_flux = -settings.diffusivity * dot(&gf, &a_non);
1025 }
1026 match settings.non_orthogonal_mode {
1027 NonOrthogonalCorrectionMode::OrthogonalOnly => {}
1028 NonOrthogonalCorrectionMode::Deferred => {
1029 assembly.add_source(stencil.left, -non_orth_flux)
1030 }
1031 NonOrthogonalCorrectionMode::FullyCorrected => flux += non_orth_flux,
1032 }
1033 if let Some(h) = hooks {
1034 flux += h.turbulence_flux(stencil, inputs) + h.free_surface_flux(stencil, inputs);
1035 }
1036 assembly.add_residual(stencil.left, flux);
1037 assembly.add_residual(rcell, -flux);
1038 }
1039 for stencil in inputs.boundary_faces() {
1040 let fg = inputs.face_metrics(stencil.face).ok_or_else(|| {
1041 MeshSieveError::InvalidGeometry(format!(
1042 "missing face geometry for boundary face {}",
1043 stencil.face
1044 ))
1045 })?;
1046 let lc = inputs.cell_metrics(stencil.left).ok_or_else(|| {
1047 MeshSieveError::InvalidGeometry(format!(
1048 "missing cell geometry for cell {}",
1049 stencil.left
1050 ))
1051 })?;
1052 let phi_i = *cell_scalar.get(&stencil.left).ok_or_else(|| {
1053 MeshSieveError::InvalidGeometry(format!("missing scalar for cell {}", stencil.left))
1054 })?;
1055 let bc = boundary_conditions
1056 .get(&stencil.face)
1057 .copied()
1058 .ok_or_else(|| {
1059 MeshSieveError::InvalidGeometry(format!(
1060 "missing boundary condition for face {}",
1061 stencil.face
1062 ))
1063 })?;
1064 let n = normalize(&fg.normal);
1065 let d = norm(&sub(&fg.centroid, &lc.centroid)).max(1e-14);
1066 match bc {
1067 BoundaryCondition::Dirichlet { value } => {
1068 let flux = -settings.diffusivity * (value - phi_i) / d * fg.area;
1069 assembly.add_residual(stencil.left, flux);
1070 }
1071 BoundaryCondition::Neumann { gradient } => {
1072 let flux = -settings.diffusivity * gradient * fg.area;
1073 assembly.add_residual(stencil.left, flux);
1074 }
1075 BoundaryCondition::Robin { alpha, beta, gamma } => {
1076 let grad_n = (gamma - alpha * phi_i) / beta.max(1e-14);
1077 let flux = -settings.diffusivity * grad_n * fg.area;
1078 let _ = n;
1079 assembly.add_residual(stencil.left, flux);
1080 assembly.add_source(
1081 stencil.left,
1082 settings.diffusivity * gamma * fg.area / beta.max(1e-14),
1083 );
1084 }
1085 }
1086 }
1087 if let Some(h) = hooks {
1088 for (cell, _) in &inputs.cell_geometry {
1089 assembly.add_source(
1090 *cell,
1091 h.turbulence_source(*cell, inputs) + h.free_surface_source(*cell, inputs),
1092 );
1093 }
1094 }
1095 Ok(assembly)
1096}
1097
1098fn convective_face_value(
1099 l: f64,
1100 r: f64,
1101 mdot: f64,
1102 scheme: ConvectiveScheme,
1103 limiter: LimiterOption,
1104) -> f64 {
1105 let up = if mdot >= 0.0 { l } else { r };
1106 let dn = if mdot >= 0.0 { r } else { l };
1107 let central = 0.5 * (l + r);
1108 let (mn, mx) = (l.min(r), l.max(r));
1109 match scheme {
1110 ConvectiveScheme::Upwind => up,
1111 ConvectiveScheme::Central => central,
1112 ConvectiveScheme::BoundedLinear { blend } => {
1113 clamp(up + clamp01(blend) * (central - up), mn, mx)
1114 }
1115 ConvectiveScheme::BlendUpwindCentral { blend } => {
1116 (1.0 - clamp01(blend)) * up + clamp01(blend) * central
1117 }
1118 ConvectiveScheme::HighResolution {
1119 blend,
1120 limiter: lim,
1121 } => {
1122 let psi = limiter.limiter_factor(up, dn) * slope_limiter(lim, up, dn);
1123 let hr = up + 0.5 * psi * (dn - up);
1124 let b = clamp01(blend);
1125 clamp((1.0 - b) * up + b * hr, mn, mx)
1126 }
1127 }
1128}
1129fn slope_limiter(limiter: SlopeLimiterFamily, up: f64, dn: f64) -> f64 {
1130 let r = if (dn - up).abs() < 1e-14 {
1131 1.0
1132 } else {
1133 ((up - dn) / (dn - up)).abs()
1134 };
1135 match limiter {
1136 SlopeLimiterFamily::None => 1.0,
1137 SlopeLimiterFamily::Minmod => r.clamp(0.0, 1.0),
1138 SlopeLimiterFamily::VanLeer => (r + r.abs()) / (1.0 + r.abs()),
1139 SlopeLimiterFamily::Superbee => (2.0 * r).min(1.0).max(r.min(2.0)).max(0.0),
1140 }
1141}
1142fn clamp(x: f64, lo: f64, hi: f64) -> f64 {
1143 x.max(lo).min(hi)
1144}
1145fn clamp01(x: f64) -> f64 {
1146 clamp(x, 0.0, 1.0)
1147}
1148
1149fn dot(a: &[f64], b: &[f64]) -> f64 {
1150 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
1151}
1152fn sub(a: &[f64], b: &[f64]) -> Vec<f64> {
1153 a.iter().zip(b.iter()).map(|(x, y)| x - y).collect()
1154}
1155fn scale(a: &[f64], s: f64) -> Vec<f64> {
1156 a.iter().map(|x| x * s).collect()
1157}
1158fn norm(a: &[f64]) -> f64 {
1159 dot(a, a).sqrt()
1160}
1161fn normalize(a: &[f64]) -> Vec<f64> {
1162 let n = norm(a).max(1e-14);
1163 scale(a, 1.0 / n)
1164}
1165fn avg(a: Option<&Vec<f64>>, b: Option<&Vec<f64>>, n: usize) -> Result<Vec<f64>, MeshSieveError> {
1166 let la = a.ok_or_else(|| MeshSieveError::InvalidGeometry("missing left gradient".into()))?;
1167 let lb = b.ok_or_else(|| MeshSieveError::InvalidGeometry("missing right gradient".into()))?;
1168 if la.len() != n || lb.len() != n {
1169 return Err(MeshSieveError::InvalidGeometry(
1170 "gradient dimension mismatch".into(),
1171 ));
1172 }
1173 Ok(la
1174 .iter()
1175 .zip(lb.iter())
1176 .map(|(x, y)| 0.5 * (x + y))
1177 .collect())
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183
1184 fn pid(id: u64) -> PointId {
1185 PointId::new(id).unwrap()
1186 }
1187
1188 fn policy(
1189 branches: HashMap<PointId, FvBoundaryBranch>,
1190 allowed: HashSet<FvBoundaryBranch>,
1191 ) -> FvBoundaryPolicy {
1192 FvBoundaryPolicy {
1193 boundary_face_branches: branches,
1194 allowed_branches: allowed,
1195 convective_branch_hooks: HashMap::new(),
1196 diffusive_branch_hooks: HashMap::new(),
1197 unsupported_behavior: UnsupportedBoundaryBehavior::Error,
1198 }
1199 }
1200
1201 #[test]
1202 fn convective_two_cell_conservation() {
1203 let c0 = pid(1);
1204 let c1 = pid(2);
1205 let f = pid(10);
1206 let inputs = FvmInputs::new(
1207 [FluxStencil {
1208 face: f,
1209 left: c0,
1210 right: Some(c1),
1211 }],
1212 vec![],
1213 vec![],
1214 );
1215 let phi = HashMap::from([(c0, 2.0), (c1, 6.0)]);
1216 let mdot = HashMap::from([(f, 3.0)]);
1217 let r = assemble_convective_fluxes(
1218 &inputs,
1219 &phi,
1220 &mdot,
1221 &HashMap::new(),
1222 ConvectiveScheme::Central,
1223 )
1224 .unwrap();
1225 assert!((r.residual[&c0] + r.residual[&c1]).abs() < 1e-12);
1226 assert_eq!(r.residual[&c0], 12.0);
1227 }
1228
1229 #[test]
1230 fn diffusive_two_cell_symmetry() {
1231 let c0 = pid(1);
1232 let c1 = pid(2);
1233 let f = pid(9);
1234 let inputs = FvmInputs::new(
1235 [FluxStencil {
1236 face: f,
1237 left: c0,
1238 right: Some(c1),
1239 }],
1240 vec![
1241 (
1242 c0,
1243 CellGeometry {
1244 centroid: vec![0.0, 0.0],
1245 volume: 1.0,
1246 },
1247 ),
1248 (
1249 c1,
1250 CellGeometry {
1251 centroid: vec![1.0, 0.0],
1252 volume: 1.0,
1253 },
1254 ),
1255 ],
1256 vec![(
1257 f,
1258 FaceGeometry {
1259 face: f,
1260 centroid: vec![0.5, 0.0],
1261 normal: vec![1.0, 0.0],
1262 area: 1.0,
1263 neighbors: vec![c0, c1],
1264 },
1265 )],
1266 );
1267 let phi = HashMap::from([(c0, 1.0), (c1, 3.0)]);
1268 let grad = HashMap::from([(c0, vec![2.0, 0.0]), (c1, vec![2.0, 0.0])]);
1269 let r = assemble_diffusive_fluxes(
1270 &inputs,
1271 &phi,
1272 &grad,
1273 &HashMap::new(),
1274 DiffusionSettings::default(),
1275 )
1276 .unwrap();
1277 assert!((r.residual[&c0] + r.residual[&c1]).abs() < 1e-12);
1278 }
1279
1280 #[test]
1281 fn convective_bounded_linear_is_bounded_on_skewed_pair() {
1282 let c0 = pid(1);
1283 let c1 = pid(2);
1284 let f = pid(10);
1285 let inputs = FvmInputs::new(
1286 [FluxStencil {
1287 face: f,
1288 left: c0,
1289 right: Some(c1),
1290 }],
1291 vec![
1292 (
1293 c0,
1294 CellGeometry {
1295 centroid: vec![0.0, 0.0],
1296 volume: 1.0,
1297 },
1298 ),
1299 (
1300 c1,
1301 CellGeometry {
1302 centroid: vec![1.2, 0.3],
1303 volume: 1.0,
1304 },
1305 ),
1306 ],
1307 vec![(
1308 f,
1309 FaceGeometry {
1310 face: f,
1311 centroid: vec![0.55, 0.2],
1312 normal: vec![0.9, 0.4],
1313 area: 0.98,
1314 neighbors: vec![c0, c1],
1315 },
1316 )],
1317 );
1318 let phi = HashMap::from([(c0, 0.0), (c1, 1.0)]);
1319 let mdot = HashMap::from([(f, 4.0)]);
1320 let r = assemble_convective_fluxes_with_reconstruction(
1321 &inputs,
1322 &phi,
1323 &mdot,
1324 &HashMap::new(),
1325 ConvectiveScheme::BoundedLinear { blend: 1.0 },
1326 ReconstructionSettings {
1327 mode: ReconstructionMode::GradientOnly(ReconstructionGradient::GreenGauss),
1328 limiter: LimiterOption::Family(SlopeLimiterFamily::VanLeer),
1329 },
1330 )
1331 .unwrap();
1332 let face_value = r.residual[&c0] / mdot[&f];
1333 assert!((0.0..=1.0).contains(&face_value));
1334 assert!((r.residual[&c0] + r.residual[&c1]).abs() < 1e-12);
1335 }
1336
1337 #[test]
1338 fn diffusive_non_orthogonal_modes_preserve_pair_conservation() {
1339 let c0 = pid(1);
1340 let c1 = pid(2);
1341 let f = pid(9);
1342 let inputs = FvmInputs::new(
1343 [FluxStencil {
1344 face: f,
1345 left: c0,
1346 right: Some(c1),
1347 }],
1348 vec![
1349 (
1350 c0,
1351 CellGeometry {
1352 centroid: vec![0.0, 0.0],
1353 volume: 1.0,
1354 },
1355 ),
1356 (
1357 c1,
1358 CellGeometry {
1359 centroid: vec![1.0, 0.25],
1360 volume: 1.0,
1361 },
1362 ),
1363 ],
1364 vec![(
1365 f,
1366 FaceGeometry {
1367 face: f,
1368 centroid: vec![0.45, 0.15],
1369 normal: vec![0.8, 0.6],
1370 area: 1.0,
1371 neighbors: vec![c0, c1],
1372 },
1373 )],
1374 );
1375 let phi = HashMap::from([(c0, 1.0), (c1, 3.0)]);
1376 let grad = HashMap::from([(c0, vec![2.0, 0.2]), (c1, vec![2.0, 0.2])]);
1377 for mode in [
1378 NonOrthogonalCorrectionMode::OrthogonalOnly,
1379 NonOrthogonalCorrectionMode::Deferred,
1380 NonOrthogonalCorrectionMode::FullyCorrected,
1381 ] {
1382 let r = assemble_diffusive_fluxes(
1383 &inputs,
1384 &phi,
1385 &grad,
1386 &HashMap::new(),
1387 DiffusionSettings {
1388 diffusivity: 1.0,
1389 non_orthogonal_mode: mode,
1390 },
1391 )
1392 .unwrap();
1393 assert!((r.residual[&c0] + r.residual[&c1]).abs() < 1e-12);
1394 }
1395 }
1396
1397 #[test]
1398 fn limiter_options_bias_toward_bounded_face_values() {
1399 let c0 = pid(1);
1400 let c1 = pid(2);
1401 let f = pid(44);
1402 let inputs = FvmInputs::new(
1403 [FluxStencil {
1404 face: f,
1405 left: c0,
1406 right: Some(c1),
1407 }],
1408 vec![],
1409 vec![],
1410 );
1411 let phi = HashMap::from([(c0, -1.0), (c1, 3.0)]);
1412 let mdot = HashMap::from([(f, 2.0)]);
1413 let unlimited = convective_face_value(
1414 phi[&c0],
1415 phi[&c1],
1416 mdot[&f],
1417 ConvectiveScheme::HighResolution {
1418 blend: 1.0,
1419 limiter: SlopeLimiterFamily::Superbee,
1420 },
1421 LimiterOption::None,
1422 );
1423 let bounded = convective_face_value(
1424 phi[&c0],
1425 phi[&c1],
1426 mdot[&f],
1427 ConvectiveScheme::HighResolution {
1428 blend: 1.0,
1429 limiter: SlopeLimiterFamily::Superbee,
1430 },
1431 LimiterOption::Family(SlopeLimiterFamily::Minmod),
1432 );
1433 assert!(bounded <= unlimited + 1e-12);
1434 let _ = inputs;
1435 }
1436
1437 #[test]
1438 fn diffusive_non_orthogonality_mode_sensitivity_on_skew_patch() {
1439 let c0 = pid(1);
1440 let c1 = pid(2);
1441 let c2 = pid(3);
1442 let f01 = pid(101);
1443 let f12 = pid(102);
1444 let inputs = FvmInputs::new(
1445 [
1446 FluxStencil {
1447 face: f01,
1448 left: c0,
1449 right: Some(c1),
1450 },
1451 FluxStencil {
1452 face: f12,
1453 left: c1,
1454 right: Some(c2),
1455 },
1456 ],
1457 vec![
1458 (
1459 c0,
1460 CellGeometry {
1461 centroid: vec![0.0, 0.0],
1462 volume: 1.0,
1463 },
1464 ),
1465 (
1466 c1,
1467 CellGeometry {
1468 centroid: vec![1.0, 0.35],
1469 volume: 1.0,
1470 },
1471 ),
1472 (
1473 c2,
1474 CellGeometry {
1475 centroid: vec![2.1, 0.05],
1476 volume: 1.0,
1477 },
1478 ),
1479 ],
1480 vec![
1481 (
1482 f01,
1483 FaceGeometry {
1484 face: f01,
1485 centroid: vec![0.45, 0.21],
1486 normal: vec![0.75, 0.62],
1487 area: 1.0,
1488 neighbors: vec![c0, c1],
1489 },
1490 ),
1491 (
1492 f12,
1493 FaceGeometry {
1494 face: f12,
1495 centroid: vec![1.55, 0.16],
1496 normal: vec![0.68, 0.71],
1497 area: 1.0,
1498 neighbors: vec![c1, c2],
1499 },
1500 ),
1501 ],
1502 );
1503 let phi = HashMap::from([(c0, 0.0), (c1, 1.0), (c2, 2.0)]);
1504 let grad = HashMap::from([
1505 (c0, vec![1.0, 0.2]),
1506 (c1, vec![1.0, -0.15]),
1507 (c2, vec![1.0, 0.1]),
1508 ]);
1509 let ortho = assemble_diffusive_fluxes(
1510 &inputs,
1511 &phi,
1512 &grad,
1513 &HashMap::new(),
1514 DiffusionSettings {
1515 diffusivity: 1.0,
1516 non_orthogonal_mode: NonOrthogonalCorrectionMode::OrthogonalOnly,
1517 },
1518 )
1519 .unwrap();
1520 let full = assemble_diffusive_fluxes(
1521 &inputs,
1522 &phi,
1523 &grad,
1524 &HashMap::new(),
1525 DiffusionSettings {
1526 diffusivity: 1.0,
1527 non_orthogonal_mode: NonOrthogonalCorrectionMode::FullyCorrected,
1528 },
1529 )
1530 .unwrap();
1531 let sum_ortho: f64 = ortho.residual.values().sum();
1532 let sum_full: f64 = full.residual.values().sum();
1533 assert!(sum_ortho.abs() < 1e-12);
1534 assert!(sum_full.abs() < 1e-12);
1535 assert_ne!(ortho.residual, full.residual);
1536 }
1537
1538 #[test]
1539 fn assemble_driver_internal_only_mesh() {
1540 let c0 = pid(1);
1541 let c1 = pid(2);
1542 let f = pid(11);
1543 let inputs = FvmInputs::new(
1544 [FluxStencil {
1545 face: f,
1546 left: c0,
1547 right: Some(c1),
1548 }],
1549 vec![
1550 (
1551 c0,
1552 CellGeometry {
1553 centroid: vec![0.0, 0.0],
1554 volume: 1.0,
1555 },
1556 ),
1557 (
1558 c1,
1559 CellGeometry {
1560 centroid: vec![1.0, 0.0],
1561 volume: 1.0,
1562 },
1563 ),
1564 ],
1565 vec![(
1566 f,
1567 FaceGeometry {
1568 face: f,
1569 centroid: vec![0.5, 0.0],
1570 normal: vec![1.0, 0.0],
1571 area: 1.0,
1572 neighbors: vec![c0, c1],
1573 },
1574 )],
1575 );
1576 let out = assemble_fv_system(
1577 &inputs,
1578 &FvmFieldSections {
1579 cell_scalar: HashMap::from([(c0, 1.0), (c1, 3.0)]),
1580 cell_gradient: HashMap::from([(c0, vec![2.0, 0.0]), (c1, vec![2.0, 0.0])]),
1581 face_mass_flux: HashMap::from([(f, 1.0)]),
1582 },
1583 &policy(HashMap::new(), HashSet::new()),
1584 FvmSchemeSettings {
1585 convective: ConvectiveScheme::Central,
1586 reconstruction: ReconstructionSettings::default(),
1587 diffusion: DiffusionSettings::default(),
1588 },
1589 )
1590 .unwrap();
1591 assert!((out.total.residual[&c0] + out.total.residual[&c1]).abs() < 1e-12);
1592 }
1593
1594 #[test]
1595 fn assemble_driver_mixed_internal_boundary_mesh() {
1596 let c0 = pid(1);
1597 let c1 = pid(2);
1598 let fi = pid(20);
1599 let fb = pid(21);
1600 let inputs = FvmInputs::new(
1601 [
1602 FluxStencil {
1603 face: fi,
1604 left: c0,
1605 right: Some(c1),
1606 },
1607 FluxStencil {
1608 face: fb,
1609 left: c0,
1610 right: None,
1611 },
1612 ],
1613 vec![
1614 (
1615 c0,
1616 CellGeometry {
1617 centroid: vec![0.0, 0.0],
1618 volume: 1.0,
1619 },
1620 ),
1621 (
1622 c1,
1623 CellGeometry {
1624 centroid: vec![1.0, 0.0],
1625 volume: 1.0,
1626 },
1627 ),
1628 ],
1629 vec![
1630 (
1631 fi,
1632 FaceGeometry {
1633 face: fi,
1634 centroid: vec![0.5, 0.0],
1635 normal: vec![1.0, 0.0],
1636 area: 1.0,
1637 neighbors: vec![c0, c1],
1638 },
1639 ),
1640 (
1641 fb,
1642 FaceGeometry {
1643 face: fb,
1644 centroid: vec![-0.5, 0.0],
1645 normal: vec![-1.0, 0.0],
1646 area: 1.0,
1647 neighbors: vec![c0],
1648 },
1649 ),
1650 ],
1651 );
1652 let branches = HashMap::from([(fb, FvBoundaryBranch::Inflow)]);
1653 let mut boundary_policy = policy(branches, HashSet::from([FvBoundaryBranch::Inflow]));
1654 boundary_policy.convective_branch_hooks.insert(
1655 FvBoundaryBranch::Inflow,
1656 BoundaryCondition::Dirichlet { value: 5.0 },
1657 );
1658 boundary_policy.diffusive_branch_hooks.insert(
1659 FvBoundaryBranch::Inflow,
1660 BoundaryCondition::Neumann { gradient: 0.0 },
1661 );
1662 let err = assemble_fv_system(
1663 &inputs,
1664 &FvmFieldSections {
1665 cell_scalar: HashMap::from([(c0, 1.0), (c1, 2.0)]),
1666 cell_gradient: HashMap::from([(c0, vec![1.0, 0.0]), (c1, vec![1.0, 0.0])]),
1667 face_mass_flux: HashMap::from([(fi, 1.0), (fb, 1.0)]),
1668 },
1669 &boundary_policy,
1670 FvmSchemeSettings {
1671 convective: ConvectiveScheme::Upwind,
1672 reconstruction: ReconstructionSettings::default(),
1673 diffusion: DiffusionSettings::default(),
1674 },
1675 )
1676 .unwrap();
1677 assert!(err.total.residual.contains_key(&c0));
1678 }
1679
1680 #[test]
1681 fn assemble_driver_boundary_only_rejects_invalid_label_mapping() {
1682 let c0 = pid(1);
1683 let fb = pid(31);
1684 let inputs = FvmInputs::new(
1685 [FluxStencil {
1686 face: fb,
1687 left: c0,
1688 right: None,
1689 }],
1690 vec![(
1691 c0,
1692 CellGeometry {
1693 centroid: vec![0.0, 0.0],
1694 volume: 1.0,
1695 },
1696 )],
1697 vec![(
1698 fb,
1699 FaceGeometry {
1700 face: fb,
1701 centroid: vec![0.0, 1.0],
1702 normal: vec![0.0, 1.0],
1703 area: 1.0,
1704 neighbors: vec![c0],
1705 },
1706 )],
1707 );
1708 let bad_map = HashMap::from([(pid(99), FvBoundaryBranch::Open)]);
1709 let err = assemble_fv_system(
1710 &inputs,
1711 &FvmFieldSections {
1712 cell_scalar: HashMap::from([(c0, 1.0)]),
1713 cell_gradient: HashMap::from([(c0, vec![0.0, 1.0])]),
1714 face_mass_flux: HashMap::from([(fb, 1.0)]),
1715 },
1716 &policy(bad_map, HashSet::from([FvBoundaryBranch::Open])),
1717 FvmSchemeSettings {
1718 convective: ConvectiveScheme::Central,
1719 reconstruction: ReconstructionSettings::default(),
1720 diffusion: DiffusionSettings::default(),
1721 },
1722 )
1723 .unwrap_err();
1724 assert!(matches!(
1725 err,
1726 FvmAssemblyError::LabelMappedToUnknownFace { .. }
1727 ));
1728 }
1729
1730 #[test]
1731 fn preflight_reports_missing_metrics_and_internal_label_mapping() {
1732 let c0 = pid(1);
1733 let c1 = pid(2);
1734 let fi = pid(41);
1735 let fb = pid(42);
1736
1737 let inputs_missing_face_metric = FvmInputs::new(
1738 [FluxStencil {
1739 face: fb,
1740 left: c0,
1741 right: None,
1742 }],
1743 vec![(
1744 c0,
1745 CellGeometry {
1746 centroid: vec![0.0, 0.0],
1747 volume: 1.0,
1748 },
1749 )],
1750 vec![],
1751 );
1752 let err = preflight_fv_assembly(
1753 &inputs_missing_face_metric,
1754 &policy(HashMap::new(), HashSet::new()),
1755 )
1756 .unwrap_err();
1757 assert_eq!(err, FvmAssemblyError::MissingFaceMetric { face: fb });
1758
1759 let inputs_missing_cell_metric = FvmInputs::new(
1760 [FluxStencil {
1761 face: fb,
1762 left: c0,
1763 right: None,
1764 }],
1765 vec![],
1766 vec![(
1767 fb,
1768 FaceGeometry {
1769 face: fb,
1770 centroid: vec![0.0, 0.0],
1771 normal: vec![1.0, 0.0],
1772 area: 1.0,
1773 neighbors: vec![c0],
1774 },
1775 )],
1776 );
1777 let err = preflight_fv_assembly(
1778 &inputs_missing_cell_metric,
1779 &policy(HashMap::new(), HashSet::new()),
1780 )
1781 .unwrap_err();
1782 assert_eq!(err, FvmAssemblyError::MissingCellMetric { cell: c0 });
1783
1784 let inputs_internal_face_label = FvmInputs::new(
1785 [FluxStencil {
1786 face: fi,
1787 left: c0,
1788 right: Some(c1),
1789 }],
1790 vec![
1791 (
1792 c0,
1793 CellGeometry {
1794 centroid: vec![0.0, 0.0],
1795 volume: 1.0,
1796 },
1797 ),
1798 (
1799 c1,
1800 CellGeometry {
1801 centroid: vec![1.0, 0.0],
1802 volume: 1.0,
1803 },
1804 ),
1805 ],
1806 vec![(
1807 fi,
1808 FaceGeometry {
1809 face: fi,
1810 centroid: vec![0.5, 0.0],
1811 normal: vec![1.0, 0.0],
1812 area: 1.0,
1813 neighbors: vec![c0, c1],
1814 },
1815 )],
1816 );
1817 let err = preflight_fv_assembly(
1818 &inputs_internal_face_label,
1819 &policy(
1820 HashMap::from([(fi, FvBoundaryBranch::Open)]),
1821 HashSet::new(),
1822 ),
1823 )
1824 .unwrap_err();
1825 assert_eq!(
1826 err,
1827 FvmAssemblyError::LabelMappedToInternalFace { face: fi }
1828 );
1829 }
1830}