1use std::collections::{HashMap, HashSet};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use bytemuck::{Pod, Zeroable};
7
8use crate::discretization::runtime::FiniteVolumeMetadata;
9use crate::physics::fvm::{
10 BoundaryCondition, ConvectiveScheme, FvBoundaryBranch, FvBoundaryPolicy, FvmInputs,
11 FvmSchemeSettings, LimiterOption, NonOrthogonalCorrectionMode, ReconstructionGradient,
12 ReconstructionMode, SlopeLimiterFamily, UnsupportedBoundaryBehavior,
13};
14use crate::topology::coastal::{BOUNDARY_CLASS_LABEL, WET_DRY_MASK_LABEL, WetDryMask};
15use crate::topology::labels::LabelSet;
16use crate::topology::point::PointId;
17
18use super::backend::CpuBackend;
19use super::plan::{checked_u32, upload};
20use super::{AcceleratorBackend, AcceleratorError, DeviceBuffer, DeviceValue, PlanEpochs};
21
22#[repr(C)]
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)]
25pub struct DeviceInternalFace {
26 pub owner: u32,
28 pub neighbor: u32,
30 pub geometry: u32,
32 pub flags: u32,
34}
35
36#[cfg(feature = "cuda")]
38unsafe impl cudarc::driver::DeviceRepr for DeviceInternalFace {}
39#[cfg(feature = "cuda")]
41unsafe impl cudarc::driver::ValidAsZeroBits for DeviceInternalFace {}
42
43#[repr(C)]
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)]
46pub struct DeviceBoundaryFace {
47 pub owner: u32,
49 pub geometry: u32,
51 pub kind: u32,
53 pub flags: u32,
55}
56
57#[cfg(feature = "cuda")]
59unsafe impl cudarc::driver::DeviceRepr for DeviceBoundaryFace {}
60#[cfg(feature = "cuda")]
62unsafe impl cudarc::driver::ValidAsZeroBits for DeviceBoundaryFace {}
63
64#[repr(C)]
67#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
68pub struct DevicePhysicsParams {
69 pub density: f64,
71 pub viscosity: f64,
73 pub gravity: f64,
75 pub diffusivity: f64,
77}
78
79#[cfg(feature = "cuda")]
81unsafe impl cudarc::driver::DeviceRepr for DevicePhysicsParams {}
82#[cfg(feature = "cuda")]
84unsafe impl cudarc::driver::ValidAsZeroBits for DevicePhysicsParams {}
85
86impl Default for DevicePhysicsParams {
87 fn default() -> Self {
88 Self {
89 density: 1.0,
90 viscosity: 0.0,
91 gravity: 9.80665,
92 diffusivity: 0.0,
93 }
94 }
95}
96
97#[repr(u32)]
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum ScalarFluxScheme {
101 Upwind = 0,
103 Central = 1,
105}
106
107pub trait FvmScalar: DeviceValue + Copy + Default + std::fmt::Debug + PartialEq {
109 fn from_f64(value: f64) -> Self;
111 fn to_f64(self) -> f64;
113}
114
115impl FvmScalar for f32 {
116 fn from_f64(value: f64) -> Self {
117 value as f32
118 }
119 fn to_f64(self) -> f64 {
120 self as f64
121 }
122}
123
124impl FvmScalar for f64 {
125 fn from_f64(value: f64) -> Self {
126 value
127 }
128 fn to_f64(self) -> f64 {
129 self
130 }
131}
132
133pub struct DeviceFvmPlan<B: AcceleratorBackend> {
135 pub(crate) backend_id: u64,
136 plan_id: u64,
137 pub(crate) epochs: PlanEpochs,
139 pub(crate) cell_ids: Vec<PointId>,
141 pub(crate) face_ids: Vec<PointId>,
143 pub(crate) internal_owner: B::Buffer<u32>,
145 pub(crate) internal_neighbor: B::Buffer<u32>,
147 pub(crate) internal_geometry: B::Buffer<u32>,
149 pub(crate) boundary_owner: B::Buffer<u32>,
151 pub(crate) boundary_geometry: B::Buffer<u32>,
153 pub(crate) boundary_kind: B::Buffer<u32>,
155 pub(crate) face_area: B::Buffer<f64>,
157 pub(crate) face_normal_x: B::Buffer<f64>,
159 pub(crate) face_normal_y: B::Buffer<f64>,
161 pub(crate) face_normal_z: B::Buffer<f64>,
163 pub(crate) face_center_x: B::Buffer<f64>,
165 pub(crate) face_center_y: B::Buffer<f64>,
167 pub(crate) face_center_z: B::Buffer<f64>,
169 pub(crate) face_geometry_indices: B::Buffer<u32>,
171 pub(crate) cell_volume: B::Buffer<f64>,
173 pub(crate) cell_center_x: B::Buffer<f64>,
175 pub(crate) cell_center_y: B::Buffer<f64>,
177 pub(crate) cell_center_z: B::Buffer<f64>,
179 pub(crate) cell_face_offsets: B::Buffer<u32>,
181 pub(crate) cell_face_indices: B::Buffer<u32>,
183 pub(crate) cell_face_signs: B::Buffer<i8>,
185 pub(crate) face_active: B::Buffer<u8>,
187 pub(crate) cell_active: B::Buffer<u8>,
189 pub(crate) dimension: usize,
191 pub(crate) internal_face_count: usize,
193 pub(crate) boundary_face_count: usize,
195}
196
197static NEXT_PLAN_ID: AtomicU64 = AtomicU64::new(1);
198
199impl<B: AcceleratorBackend> DeviceFvmPlan<B> {
200 pub fn compile(
202 backend: &B,
203 inputs: &FvmInputs,
204 labels: Option<&LabelSet>,
205 epochs: PlanEpochs,
206 ) -> Result<Self, AcceleratorError> {
207 checked_u32(inputs.cell_geometry.len(), "cell count")?;
208 checked_u32(inputs.face_geometry.len(), "face geometry count")?;
209 let total_faces = inputs
210 .loops
211 .internal
212 .len()
213 .checked_add(inputs.loops.boundary.len())
214 .ok_or(AcceleratorError::IndexOverflow {
215 what: "face count",
216 value: usize::MAX,
217 })?;
218 checked_u32(total_faces, "face count")?;
219
220 let dimension = inputs.cell_geometry.first().map_or_else(
221 || {
222 inputs
223 .face_geometry
224 .first()
225 .map_or(0, |(_, g)| g.normal.len())
226 },
227 |(_, g)| g.centroid.len(),
228 );
229 if dimension > 3 {
230 return Err(AcceleratorError::InvalidPlan(format!(
231 "CUDA FVM supports at most 3 dimensions, found {dimension}"
232 )));
233 }
234 if dimension == 0 && (!inputs.cell_geometry.is_empty() || !inputs.face_geometry.is_empty())
235 {
236 return Err(AcceleratorError::InvalidPlan(
237 "finite-volume geometry must have at least one dimension".into(),
238 ));
239 }
240
241 let mut cell_index = HashMap::with_capacity(inputs.cell_geometry.len());
242 let mut cell_ids = Vec::with_capacity(inputs.cell_geometry.len());
243 let mut cell_volume = Vec::with_capacity(inputs.cell_geometry.len());
244 let mut cell_center_x = Vec::with_capacity(inputs.cell_geometry.len());
245 let mut cell_center_y = Vec::with_capacity(inputs.cell_geometry.len());
246 let mut cell_center_z = Vec::with_capacity(inputs.cell_geometry.len());
247 for (idx, (id, geometry)) in inputs.cell_geometry.iter().enumerate() {
248 if geometry.centroid.len() != dimension {
249 return Err(AcceleratorError::InvalidPlan(format!(
250 "cell {id} has dimension {}, expected {dimension}",
251 geometry.centroid.len()
252 )));
253 }
254 if !geometry.volume.is_finite() || geometry.volume <= 0.0 {
255 return Err(AcceleratorError::InvalidPlan(format!(
256 "cell {id} has non-positive or non-finite volume {}",
257 geometry.volume
258 )));
259 }
260 if geometry.centroid.iter().any(|value| !value.is_finite()) {
261 return Err(AcceleratorError::InvalidPlan(format!(
262 "cell {id} has a non-finite centroid"
263 )));
264 }
265 if cell_index
266 .insert(*id, checked_u32(idx, "cell index")?)
267 .is_some()
268 {
269 return Err(AcceleratorError::InvalidPlan(format!(
270 "duplicate cell geometry for {id}"
271 )));
272 }
273 cell_ids.push(*id);
274 cell_volume.push(geometry.volume);
275 cell_center_x.push(component(&geometry.centroid, 0));
276 cell_center_y.push(component(&geometry.centroid, 1));
277 cell_center_z.push(component(&geometry.centroid, 2));
278 }
279
280 let mut face_index = HashMap::with_capacity(inputs.face_geometry.len());
281 let mut face_area = Vec::with_capacity(inputs.face_geometry.len());
282 let mut face_normal_x = Vec::with_capacity(inputs.face_geometry.len());
283 let mut face_normal_y = Vec::with_capacity(inputs.face_geometry.len());
284 let mut face_normal_z = Vec::with_capacity(inputs.face_geometry.len());
285 let mut face_center_x = Vec::with_capacity(inputs.face_geometry.len());
286 let mut face_center_y = Vec::with_capacity(inputs.face_geometry.len());
287 let mut face_center_z = Vec::with_capacity(inputs.face_geometry.len());
288 for (idx, (id, geometry)) in inputs.face_geometry.iter().enumerate() {
289 if geometry.normal.len() != dimension || geometry.centroid.len() != dimension {
290 return Err(AcceleratorError::InvalidPlan(format!(
291 "face {id} has normal/centroid dimensions {}/{}, expected {dimension}",
292 geometry.normal.len(),
293 geometry.centroid.len()
294 )));
295 }
296 if geometry.face != *id {
297 return Err(AcceleratorError::InvalidPlan(format!(
298 "face geometry key {id} disagrees with embedded face {}",
299 geometry.face
300 )));
301 }
302 if !geometry.area.is_finite() || geometry.area <= 0.0 {
303 return Err(AcceleratorError::InvalidPlan(format!(
304 "face {id} has non-positive or non-finite area {}",
305 geometry.area
306 )));
307 }
308 if geometry.centroid.iter().any(|value| !value.is_finite())
309 || geometry.normal.iter().any(|value| !value.is_finite())
310 {
311 return Err(AcceleratorError::InvalidPlan(format!(
312 "face {id} has a non-finite centroid or normal"
313 )));
314 }
315 if geometry
316 .normal
317 .iter()
318 .map(|value| value * value)
319 .sum::<f64>()
320 <= 0.0
321 {
322 return Err(AcceleratorError::InvalidPlan(format!(
323 "face {id} has a zero normal"
324 )));
325 }
326 if face_index
327 .insert(*id, checked_u32(idx, "face geometry index")?)
328 .is_some()
329 {
330 return Err(AcceleratorError::InvalidPlan(format!(
331 "duplicate face geometry for {id}"
332 )));
333 }
334 face_area.push(geometry.area);
335 face_normal_x.push(component(&geometry.normal, 0));
336 face_normal_y.push(component(&geometry.normal, 1));
337 face_normal_z.push(component(&geometry.normal, 2));
338 face_center_x.push(component(&geometry.centroid, 0));
339 face_center_y.push(component(&geometry.centroid, 1));
340 face_center_z.push(component(&geometry.centroid, 2));
341 }
342
343 let mut internal_owner = Vec::with_capacity(inputs.loops.internal.len());
344 let mut internal_neighbor = Vec::with_capacity(inputs.loops.internal.len());
345 let mut internal_geometry = Vec::with_capacity(inputs.loops.internal.len());
346 let mut boundary_owner = Vec::with_capacity(inputs.loops.boundary.len());
347 let mut boundary_geometry = Vec::with_capacity(inputs.loops.boundary.len());
348 let mut boundary_kind = Vec::with_capacity(inputs.loops.boundary.len());
349 let mut face_ids = Vec::with_capacity(total_faces);
350 let mut face_geometry_indices = Vec::with_capacity(total_faces);
351 let mut incidence: Vec<Vec<(u32, i8)>> = vec![Vec::new(); cell_ids.len()];
352 let mut face_active = Vec::with_capacity(total_faces);
353 let cell_active: Vec<u8> = cell_ids
354 .iter()
355 .map(|id| {
356 u8::from(
357 labels.and_then(|set| set.get_label(*id, WET_DRY_MASK_LABEL))
358 != Some(WetDryMask::Dry.code()),
359 )
360 })
361 .collect();
362
363 let mut stencil_faces = HashSet::with_capacity(total_faces);
364 for (flux_idx, stencil) in inputs.loops.internal.iter().enumerate() {
365 if !stencil_faces.insert(stencil.face) {
366 return Err(AcceleratorError::InvalidPlan(format!(
367 "face {} appears in more than one FVM stencil",
368 stencil.face
369 )));
370 }
371 let owner = lookup(&cell_index, stencil.left, "owner cell")?;
372 let neighbor_id = stencil.right.ok_or_else(|| {
373 AcceleratorError::InvalidPlan(format!(
374 "internal face {} has no neighbor",
375 stencil.face
376 ))
377 })?;
378 let neighbor = lookup(&cell_index, neighbor_id, "neighbor cell")?;
379 if owner == neighbor {
380 return Err(AcceleratorError::InvalidPlan(format!(
381 "internal face {} uses the same owner and neighbor cell {}",
382 stencil.face, stencil.left
383 )));
384 }
385 let geometry = lookup(&face_index, stencil.face, "face geometry")?;
386 let geometry_neighbors = &inputs.face_geometry[geometry as usize].1.neighbors;
387 if geometry_neighbors.as_slice() != [stencil.left, neighbor_id] {
388 return Err(AcceleratorError::InvalidPlan(format!(
389 "internal face {} geometry neighbors {:?} do not match owner/neighbor [{}, {}]",
390 stencil.face, geometry_neighbors, stencil.left, neighbor_id
391 )));
392 }
393 internal_owner.push(owner);
394 internal_neighbor.push(neighbor);
395 internal_geometry.push(geometry);
396 face_ids.push(stencil.face);
397 face_geometry_indices.push(geometry);
398 let fi = checked_u32(flux_idx, "flux index")?;
399 incidence[owner as usize].push((fi, 1));
400 incidence[neighbor as usize].push((fi, -1));
401 face_active.push(u8::from(
402 cell_active[owner as usize] != 0 && cell_active[neighbor as usize] != 0,
403 ));
404 }
405 let internal_face_count = internal_owner.len();
406 for (boundary_idx, stencil) in inputs.loops.boundary.iter().enumerate() {
407 if !stencil_faces.insert(stencil.face) {
408 return Err(AcceleratorError::InvalidPlan(format!(
409 "face {} appears in more than one FVM stencil",
410 stencil.face
411 )));
412 }
413 let owner = lookup(&cell_index, stencil.left, "boundary owner cell")?;
414 let geometry = lookup(&face_index, stencil.face, "boundary face geometry")?;
415 let geometry_neighbors = &inputs.face_geometry[geometry as usize].1.neighbors;
416 if geometry_neighbors.as_slice() != [stencil.left] {
417 return Err(AcceleratorError::InvalidPlan(format!(
418 "boundary face {} geometry neighbors {:?} do not match owner {}",
419 stencil.face, geometry_neighbors, stencil.left
420 )));
421 }
422 boundary_owner.push(owner);
423 boundary_geometry.push(geometry);
424 boundary_kind.push(
425 labels
426 .and_then(|set| {
427 crate::physics::fvm::boundary_branch_for_face(set, stencil.face)
428 })
429 .map_or(u32::MAX, encode_boundary_kind),
430 );
431 face_ids.push(stencil.face);
432 face_geometry_indices.push(geometry);
433 let flux_idx = checked_u32(internal_face_count + boundary_idx, "flux index")?;
434 incidence[owner as usize].push((flux_idx, 1));
435 face_active.push(cell_active[owner as usize]);
436 }
437
438 let mut cell_face_offsets = Vec::with_capacity(cell_ids.len() + 1);
439 let mut cell_face_indices = Vec::new();
440 let mut cell_face_signs = Vec::new();
441 cell_face_offsets.push(0);
442 for entries in &incidence {
443 for &(face, sign) in entries {
444 cell_face_indices.push(face);
445 cell_face_signs.push(sign);
446 }
447 cell_face_offsets.push(checked_u32(cell_face_indices.len(), "cell-face offset")?);
448 }
449
450 Ok(Self {
451 backend_id: backend.identity(),
452 plan_id: NEXT_PLAN_ID.fetch_add(1, Ordering::Relaxed),
453 epochs,
454 cell_ids,
455 face_ids,
456 internal_owner: upload(backend, &internal_owner)?,
457 internal_neighbor: upload(backend, &internal_neighbor)?,
458 internal_geometry: upload(backend, &internal_geometry)?,
459 boundary_owner: upload(backend, &boundary_owner)?,
460 boundary_geometry: upload(backend, &boundary_geometry)?,
461 boundary_kind: upload(backend, &boundary_kind)?,
462 face_area: upload(backend, &face_area)?,
463 face_normal_x: upload(backend, &face_normal_x)?,
464 face_normal_y: upload(backend, &face_normal_y)?,
465 face_normal_z: upload(backend, &face_normal_z)?,
466 face_center_x: upload(backend, &face_center_x)?,
467 face_center_y: upload(backend, &face_center_y)?,
468 face_center_z: upload(backend, &face_center_z)?,
469 face_geometry_indices: upload(backend, &face_geometry_indices)?,
470 cell_volume: upload(backend, &cell_volume)?,
471 cell_center_x: upload(backend, &cell_center_x)?,
472 cell_center_y: upload(backend, &cell_center_y)?,
473 cell_center_z: upload(backend, &cell_center_z)?,
474 cell_face_offsets: upload(backend, &cell_face_offsets)?,
475 cell_face_indices: upload(backend, &cell_face_indices)?,
476 cell_face_signs: upload(backend, &cell_face_signs)?,
477 face_active: upload(backend, &face_active)?,
478 cell_active: upload(backend, &cell_active)?,
479 dimension,
480 internal_face_count,
481 boundary_face_count: boundary_owner.len(),
482 })
483 }
484
485 pub fn validate(&self, current: PlanEpochs) -> Result<(), AcceleratorError> {
487 self.epochs.validate(current)
488 }
489
490 pub fn face_count(&self) -> usize {
492 self.internal_face_count + self.boundary_face_count
493 }
494
495 pub fn cell_ids(&self) -> &[PointId] {
497 &self.cell_ids
498 }
499
500 pub fn face_ids(&self) -> &[PointId] {
502 &self.face_ids
503 }
504
505 pub fn cell_count(&self) -> usize {
507 self.cell_ids.len()
508 }
509
510 pub fn dimension(&self) -> usize {
512 self.dimension
513 }
514
515 pub fn internal_face_count(&self) -> usize {
517 self.internal_face_count
518 }
519
520 pub fn boundary_face_count(&self) -> usize {
522 self.boundary_face_count
523 }
524
525 pub fn cell_active(&self) -> &B::Buffer<u8> {
527 &self.cell_active
528 }
529
530 pub fn face_active(&self) -> &B::Buffer<u8> {
532 &self.face_active
533 }
534}
535
536pub struct DeviceFvmState<T: FvmScalar, B: AcceleratorBackend> {
538 pub(crate) backend_id: u64,
539 plan_id: u64,
540 components: usize,
542 pub(crate) cell_values: B::Buffer<T>,
544 pub(crate) face_mass_flux: B::Buffer<T>,
546 pub(crate) boundary_values: B::Buffer<T>,
548 pub(crate) boundary_override_active: B::Buffer<u8>,
550 pub(crate) face_flux: B::Buffer<T>,
552 pub(crate) face_source: B::Buffer<T>,
554 pub(crate) face_deferred_source: B::Buffer<T>,
556 pub(crate) face_values: B::Buffer<T>,
558 pub(crate) residual: B::Buffer<T>,
560 pub(crate) cell_source: B::Buffer<T>,
562 pub(crate) gradient_x: B::Buffer<T>,
564 pub(crate) gradient_y: B::Buffer<T>,
566 pub(crate) gradient_z: B::Buffer<T>,
568}
569
570impl<T: FvmScalar, B: AcceleratorBackend> DeviceFvmState<T, B> {
571 pub fn upload(
573 backend: &B,
574 plan: &DeviceFvmPlan<B>,
575 cell_values: &[T],
576 face_mass_flux: &[T],
577 boundary_values: &[T],
578 ) -> Result<Self, AcceleratorError> {
579 Self::upload_components(
580 backend,
581 plan,
582 1,
583 cell_values,
584 face_mass_flux,
585 boundary_values,
586 )
587 }
588
589 pub fn upload_components(
591 backend: &B,
592 plan: &DeviceFvmPlan<B>,
593 components: usize,
594 cell_values: &[T],
595 face_mass_flux: &[T],
596 boundary_values: &[T],
597 ) -> Result<Self, AcceleratorError> {
598 ensure_backend(plan.backend_id, backend.identity())?;
599 if components == 0 {
600 return Err(AcceleratorError::InvalidPlan(
601 "FVM state requires at least one component".into(),
602 ));
603 }
604 let cell_values_len = checked_product(plan.cell_ids.len(), components, "cell state")?;
605 let face_values_len = checked_product(plan.face_count(), components, "face state")?;
606 let boundary_values_len =
607 checked_product(plan.boundary_face_count, components, "boundary state")?;
608 check_len(cell_values_len, cell_values.len())?;
609 check_len(plan.face_count(), face_mass_flux.len())?;
610 check_len(boundary_values_len, boundary_values.len())?;
611 Ok(Self {
612 backend_id: backend.identity(),
613 plan_id: plan.plan_id,
614 components,
615 cell_values: upload(backend, cell_values)?,
616 face_mass_flux: upload(backend, face_mass_flux)?,
617 boundary_values: upload(backend, boundary_values)?,
618 boundary_override_active: backend.allocate(boundary_values_len).map_err(|error| {
619 AcceleratorError::AllocationFailed {
620 bytes: boundary_values_len,
621 reason: error.to_string(),
622 }
623 })?,
624 face_flux: allocate(backend, face_values_len)?,
625 face_source: allocate(backend, face_values_len)?,
626 face_deferred_source: allocate(backend, face_values_len)?,
627 face_values: allocate(backend, face_values_len)?,
628 residual: allocate(backend, cell_values_len)?,
629 cell_source: allocate(backend, cell_values_len)?,
630 gradient_x: allocate(backend, cell_values_len)?,
631 gradient_y: allocate(backend, cell_values_len)?,
632 gradient_z: allocate(backend, cell_values_len)?,
633 })
634 }
635
636 pub fn upload_sources(
638 &mut self,
639 backend: &B,
640 face_source: &[T],
641 cell_source: &[T],
642 ) -> Result<(), AcceleratorError> {
643 ensure_backend(self.backend_id, backend.identity())?;
644 check_len(self.face_source.len(), face_source.len())?;
645 check_len(self.cell_source.len(), cell_source.len())?;
646 backend
647 .upload_into(face_source, &mut self.face_source)
648 .map_err(|e| AcceleratorError::DeviceTransferFailed(e.to_string()))?;
649 backend
650 .upload_into(cell_source, &mut self.cell_source)
651 .map_err(|e| AcceleratorError::DeviceTransferFailed(e.to_string()))
652 }
653
654 pub fn upload_boundary_overrides(
659 &mut self,
660 backend: &B,
661 values: &[T],
662 ) -> Result<(), AcceleratorError> {
663 ensure_backend(self.backend_id, backend.identity())?;
664 check_len(self.boundary_values.len(), values.len())?;
665 let active = vec![1_u8; values.len()];
666 let replacement_values = upload(backend, values)?;
667 let replacement_active = upload(backend, &active)?;
668 self.boundary_values = replacement_values;
669 self.boundary_override_active = replacement_active;
670 Ok(())
671 }
672
673 pub fn clear_boundary_overrides(&mut self, backend: &B) -> Result<(), AcceleratorError> {
675 ensure_backend(self.backend_id, backend.identity())?;
676 let inactive = vec![0_u8; self.boundary_override_active.len()];
677 let replacement = upload(backend, &inactive)?;
678 self.boundary_override_active = replacement;
679 Ok(())
680 }
681
682 pub fn components(&self) -> usize {
684 self.components
685 }
686
687 pub fn cell_values(&self) -> &B::Buffer<T> {
689 &self.cell_values
690 }
691
692 pub fn face_mass_flux(&self) -> &B::Buffer<T> {
694 &self.face_mass_flux
695 }
696
697 pub fn residual(&self) -> &B::Buffer<T> {
699 &self.residual
700 }
701
702 pub fn gradient_x(&self) -> &B::Buffer<T> {
704 &self.gradient_x
705 }
706
707 pub fn upload_cell_values(
709 &mut self,
710 backend: &B,
711 values: &[T],
712 ) -> Result<(), AcceleratorError> {
713 ensure_backend(self.backend_id, backend.identity())?;
714 check_len(self.cell_values.len(), values.len())?;
715 backend
716 .upload_into(values, &mut self.cell_values)
717 .map_err(|error| AcceleratorError::DeviceTransferFailed(error.to_string()))
718 }
719
720 pub fn upload_face_mass_flux(
722 &mut self,
723 backend: &B,
724 values: &[T],
725 ) -> Result<(), AcceleratorError> {
726 ensure_backend(self.backend_id, backend.identity())?;
727 check_len(self.face_mass_flux.len(), values.len())?;
728 backend
729 .upload_into(values, &mut self.face_mass_flux)
730 .map_err(|error| AcceleratorError::DeviceTransferFailed(error.to_string()))
731 }
732
733 pub fn download_residual(&self, backend: &B) -> Result<Vec<T>, AcceleratorError> {
735 ensure_backend(self.backend_id, backend.identity())?;
736 let mut host = vec![T::zeroed(); self.residual.len()];
737 backend
738 .download(&self.residual, &mut host)
739 .map_err(|e| AcceleratorError::DeviceTransferFailed(e.to_string()))?;
740 Ok(host)
741 }
742}
743
744impl DeviceFvmPlan<CpuBackend> {
745 pub fn compute_gradients<T: FvmScalar>(
747 &self,
748 state: &mut DeviceFvmState<T, CpuBackend>,
749 ) -> Result<(), AcceleratorError> {
750 validate_scalar_state(self, state)?;
751 for face in 0..self.internal_face_count {
752 let owner = self.internal_owner.0[face] as usize;
753 let neighbor = self.internal_neighbor.0[face] as usize;
754 state.face_values.0[face] = T::from_f64(
755 0.5 * (state.cell_values.0[owner].to_f64()
756 + state.cell_values.0[neighbor].to_f64()),
757 );
758 }
759 for boundary in 0..self.boundary_face_count {
760 let face = self.internal_face_count + boundary;
761 state.face_values.0[face] = state.boundary_values.0[boundary];
762 }
763 for cell in 0..self.cell_ids.len() {
764 if self.cell_active.0[cell] == 0 {
765 state.gradient_x.0[cell] = T::zeroed();
766 state.gradient_y.0[cell] = T::zeroed();
767 state.gradient_z.0[cell] = T::zeroed();
768 continue;
769 }
770 let begin = self.cell_face_offsets.0[cell] as usize;
771 let end = self.cell_face_offsets.0[cell + 1] as usize;
772 let mut gradient = [0.0; 3];
773 for incidence in begin..end {
774 let face = self.cell_face_indices.0[incidence] as usize;
775 if self.face_active.0[face] == 0 {
776 continue;
777 }
778 let geometry = self.face_geometry_indices.0[face] as usize;
779 let scale =
780 self.cell_face_signs.0[incidence] as f64 * state.face_values.0[face].to_f64();
781 gradient[0] += scale * self.face_normal_x.0[geometry];
782 gradient[1] += scale * self.face_normal_y.0[geometry];
783 gradient[2] += scale * self.face_normal_z.0[geometry];
784 }
785 let volume = self.cell_volume.0[cell];
786 if volume == 0.0 {
787 return Err(AcceleratorError::InvalidPlan(format!(
788 "cell {} has zero volume",
789 self.cell_ids[cell]
790 )));
791 }
792 state.gradient_x.0[cell] = T::from_f64(gradient[0] / volume);
793 state.gradient_y.0[cell] = T::from_f64(gradient[1] / volume);
794 state.gradient_z.0[cell] = T::from_f64(gradient[2] / volume);
795 }
796 Ok(())
797 }
798
799 pub fn compute_diffusive_face_fluxes<T: FvmScalar>(
801 &self,
802 state: &mut DeviceFvmState<T, CpuBackend>,
803 diffusivity: f64,
804 ) -> Result<(), AcceleratorError> {
805 validate_scalar_state(self, state)?;
806 for face in 0..self.internal_face_count {
807 if self.face_active.0[face] == 0 {
808 state.face_flux.0[face] = T::zeroed();
809 continue;
810 }
811 let owner = self.internal_owner.0[face] as usize;
812 let neighbor = self.internal_neighbor.0[face] as usize;
813 let geometry = self.internal_geometry.0[face] as usize;
814 let d = [
815 self.cell_center_x.0[neighbor] - self.cell_center_x.0[owner],
816 self.cell_center_y.0[neighbor] - self.cell_center_y.0[owner],
817 self.cell_center_z.0[neighbor] - self.cell_center_z.0[owner],
818 ];
819 let normal = [
820 self.face_normal_x.0[geometry],
821 self.face_normal_y.0[geometry],
822 self.face_normal_z.0[geometry],
823 ];
824 let d2 = dot3(d, d);
825 let flux = if d2 == 0.0 {
826 0.0
827 } else {
828 -diffusivity
829 * (state.cell_values.0[neighbor].to_f64() - state.cell_values.0[owner].to_f64())
830 * dot3(normal, d)
831 / d2
832 };
833 state.face_flux.0[face] = T::from_f64(flux);
834 }
835 for boundary in 0..self.boundary_face_count {
836 let face = self.internal_face_count + boundary;
837 if self.face_active.0[face] == 0 {
838 state.face_flux.0[face] = T::zeroed();
839 continue;
840 }
841 let owner = self.boundary_owner.0[boundary] as usize;
842 let geometry = self.boundary_geometry.0[boundary] as usize;
843 let d = [
844 self.face_center_x.0[geometry] - self.cell_center_x.0[owner],
845 self.face_center_y.0[geometry] - self.cell_center_y.0[owner],
846 self.face_center_z.0[geometry] - self.cell_center_z.0[owner],
847 ];
848 let normal = [
849 self.face_normal_x.0[geometry],
850 self.face_normal_y.0[geometry],
851 self.face_normal_z.0[geometry],
852 ];
853 let d2 = dot3(d, d);
854 let flux = if d2 == 0.0 {
855 0.0
856 } else {
857 -diffusivity
858 * (state.boundary_values.0[boundary].to_f64()
859 - state.cell_values.0[owner].to_f64())
860 * dot3(normal, d)
861 / d2
862 };
863 state.face_flux.0[face] = T::from_f64(flux);
864 }
865 Ok(())
866 }
867
868 pub fn compute_face_fluxes<T: FvmScalar>(
870 &self,
871 state: &mut DeviceFvmState<T, CpuBackend>,
872 scheme: ScalarFluxScheme,
873 ) -> Result<(), AcceleratorError> {
874 validate_scalar_state(self, state)?;
875 for face in 0..self.internal_face_count {
876 if self.face_active.0[face] == 0 {
877 state.face_flux.0[face] = T::zeroed();
878 continue;
879 }
880 let owner = self.internal_owner.0[face] as usize;
881 let neighbor = self.internal_neighbor.0[face] as usize;
882 let mass = state.face_mass_flux.0[face].to_f64();
883 let left = state.cell_values.0[owner].to_f64();
884 let right = state.cell_values.0[neighbor].to_f64();
885 let face_value = match scheme {
886 ScalarFluxScheme::Upwind => {
887 if mass >= 0.0 {
888 left
889 } else {
890 right
891 }
892 }
893 ScalarFluxScheme::Central => 0.5 * (left + right),
894 };
895 state.face_flux.0[face] = T::from_f64(mass * face_value);
896 }
897 for boundary in 0..self.boundary_face_count {
898 let face = self.internal_face_count + boundary;
899 if self.face_active.0[face] == 0 {
900 state.face_flux.0[face] = T::zeroed();
901 continue;
902 }
903 let owner = self.boundary_owner.0[boundary] as usize;
904 let mass = state.face_mass_flux.0[face].to_f64();
905 let inside = state.cell_values.0[owner].to_f64();
906 let outside = state.boundary_values.0[boundary].to_f64();
907 let face_value = match scheme {
908 ScalarFluxScheme::Upwind => {
909 if mass >= 0.0 {
910 inside
911 } else {
912 outside
913 }
914 }
915 ScalarFluxScheme::Central => 0.5 * (inside + outside),
916 };
917 state.face_flux.0[face] = T::from_f64(mass * face_value);
918 }
919 Ok(())
920 }
921
922 pub fn assemble_cell_residuals<T: FvmScalar>(
924 &self,
925 state: &mut DeviceFvmState<T, CpuBackend>,
926 ) -> Result<(), AcceleratorError> {
927 validate_scalar_state(self, state)?;
928 for cell in 0..self.cell_ids.len() {
929 if self.cell_active.0[cell] == 0 {
930 state.residual.0[cell] = T::zeroed();
931 continue;
932 }
933 let begin = self.cell_face_offsets.0[cell] as usize;
934 let end = self.cell_face_offsets.0[cell + 1] as usize;
935 let mut sum = 0.0;
936 for incidence in begin..end {
937 let face = self.cell_face_indices.0[incidence] as usize;
938 let sign = self.cell_face_signs.0[incidence] as f64;
939 sum += sign * state.face_flux.0[face].to_f64();
940 }
941 state.residual.0[cell] = T::from_f64(sum);
942 }
943 Ok(())
944 }
945}
946
947pub(crate) fn validate_scalar_state<T: FvmScalar, B: AcceleratorBackend>(
948 plan: &DeviceFvmPlan<B>,
949 state: &DeviceFvmState<T, B>,
950) -> Result<(), AcceleratorError> {
951 ensure_backend(plan.backend_id, state.backend_id)?;
952 ensure_plan(plan.plan_id, state.plan_id)?;
953 if state.components != 1 {
954 return Err(AcceleratorError::InvalidPlan(
955 "scalar DeviceFvmPlan methods require a one-component state; use DeviceFvmOperator"
956 .into(),
957 ));
958 }
959 check_len(plan.cell_ids.len(), state.cell_values.len())?;
960 check_len(plan.cell_ids.len(), state.residual.len())?;
961 check_len(plan.face_count(), state.face_mass_flux.len())?;
962 check_len(plan.face_count(), state.face_flux.len())?;
963 check_len(plan.face_count(), state.face_values.len())?;
964 check_len(plan.cell_ids.len(), state.gradient_x.len())?;
965 check_len(plan.cell_ids.len(), state.gradient_y.len())?;
966 check_len(plan.cell_ids.len(), state.gradient_z.len())?;
967 check_len(plan.boundary_face_count, state.boundary_values.len())?;
968 check_len(
969 plan.boundary_face_count,
970 state.boundary_override_active.len(),
971 )
972}
973
974pub struct DeviceFvmBoundaryConditions<B: AcceleratorBackend> {
976 pub(crate) convective_kind: B::Buffer<u8>,
978 pub(crate) convective_alpha: B::Buffer<f64>,
980 pub(crate) convective_beta: B::Buffer<f64>,
982 pub(crate) convective_gamma: B::Buffer<f64>,
984 pub(crate) diffusive_kind: B::Buffer<u8>,
986 pub(crate) diffusive_alpha: B::Buffer<f64>,
988 pub(crate) diffusive_beta: B::Buffer<f64>,
990 pub(crate) diffusive_gamma: B::Buffer<f64>,
992}
993
994pub struct DeviceLeastSquaresPlan<B: AcceleratorBackend> {
996 pub(crate) neighbor: B::Buffer<u32>,
998 pub(crate) weight_x: B::Buffer<f64>,
1000 pub(crate) weight_y: B::Buffer<f64>,
1002 pub(crate) weight_z: B::Buffer<f64>,
1004 pub(crate) fallback: B::Buffer<u8>,
1006 fallback_host: Vec<u8>,
1007}
1008
1009pub struct DeviceFvmOperator<B: AcceleratorBackend> {
1011 pub(crate) plan: DeviceFvmPlan<B>,
1013 pub(crate) metadata: FiniteVolumeMetadata,
1015 pub(crate) schemes: FvmSchemeSettings,
1017 pub(crate) boundary: DeviceFvmBoundaryConditions<B>,
1019 pub(crate) least_squares: DeviceLeastSquaresPlan<B>,
1021}
1022
1023impl<B: AcceleratorBackend> DeviceFvmOperator<B> {
1024 pub fn plan(&self) -> &DeviceFvmPlan<B> {
1026 &self.plan
1027 }
1028
1029 pub fn metadata(&self) -> &FiniteVolumeMetadata {
1031 &self.metadata
1032 }
1033
1034 pub fn schemes(&self) -> &FvmSchemeSettings {
1036 &self.schemes
1037 }
1038
1039 pub fn compile(
1041 backend: &B,
1042 inputs: &FvmInputs,
1043 metadata: FiniteVolumeMetadata,
1044 labels: Option<&LabelSet>,
1045 boundary_policy: &FvBoundaryPolicy,
1046 schemes: FvmSchemeSettings,
1047 epochs: PlanEpochs,
1048 ) -> Result<Self, AcceleratorError> {
1049 if metadata.components == 0 {
1050 return Err(AcceleratorError::InvalidPlan(
1051 "finite-volume metadata requires at least one component".into(),
1052 ));
1053 }
1054 if metadata.reconstruction_order == 0 {
1055 return Err(AcceleratorError::InvalidPlan(
1056 "finite-volume reconstruction order must be at least one".into(),
1057 ));
1058 }
1059 if !schemes.diffusion.diffusivity.is_finite() {
1060 return Err(AcceleratorError::InvalidPlan(
1061 "finite-volume diffusivity must be finite".into(),
1062 ));
1063 }
1064 let blend = match schemes.convective {
1065 ConvectiveScheme::BoundedLinear { blend }
1066 | ConvectiveScheme::BlendUpwindCentral { blend }
1067 | ConvectiveScheme::HighResolution { blend, .. } => Some(blend),
1068 ConvectiveScheme::Upwind | ConvectiveScheme::Central => None,
1069 };
1070 if blend.is_some_and(|value| !value.is_finite()) {
1071 return Err(AcceleratorError::InvalidPlan(
1072 "finite-volume convection blend must be finite".into(),
1073 ));
1074 }
1075 if let Some(labels) = labels {
1076 for stencil in &inputs.loops.boundary {
1077 if labels
1078 .get_label(stencil.face, BOUNDARY_CLASS_LABEL)
1079 .is_none()
1080 {
1081 continue;
1082 }
1083 let labeled =
1084 crate::physics::fvm::boundary_branch_for_face_checked(labels, stencil.face)
1085 .map_err(|error| {
1086 AcceleratorError::InvalidPlan(format!(
1087 "invalid boundary labels for face {}: {error:?}",
1088 stencil.face
1089 ))
1090 })?;
1091 if boundary_policy.boundary_face_branches.get(&stencil.face) != Some(&labeled) {
1092 return Err(AcceleratorError::InvalidPlan(format!(
1093 "boundary face {} is labeled {labeled:?} but its policy branch is {:?}",
1094 stencil.face,
1095 boundary_policy.boundary_face_branches.get(&stencil.face)
1096 )));
1097 }
1098 }
1099 }
1100 let plan = DeviceFvmPlan::compile(backend, inputs, labels, epochs)?;
1101 let boundary_host = pack_boundary_conditions(
1102 inputs.loops.boundary.iter().map(|face| face.face),
1103 boundary_policy,
1104 )?;
1105 let least_squares_host = build_least_squares(inputs, &plan.cell_ids)?;
1106 if matches!(
1107 schemes.reconstruction.mode,
1108 ReconstructionMode::GradientOnly(ReconstructionGradient::LeastSquares)
1109 ) && least_squares_host.fallback.iter().any(|&value| value != 0)
1110 {
1111 return Err(AcceleratorError::InvalidPlan(
1112 "least-squares reconstruction is singular for at least one cell; select the Green--Gauss fallback mode"
1113 .into(),
1114 ));
1115 }
1116 Ok(Self {
1117 boundary: upload_boundary(backend, boundary_host)?,
1118 least_squares: DeviceLeastSquaresPlan {
1119 neighbor: upload(backend, &least_squares_host.neighbor)?,
1120 weight_x: upload(backend, &least_squares_host.weight_x)?,
1121 weight_y: upload(backend, &least_squares_host.weight_y)?,
1122 weight_z: upload(backend, &least_squares_host.weight_z)?,
1123 fallback: upload(backend, &least_squares_host.fallback)?,
1124 fallback_host: least_squares_host.fallback,
1125 },
1126 plan,
1127 metadata,
1128 schemes,
1129 })
1130 }
1131
1132 pub fn refresh_boundary_conditions(
1134 &mut self,
1135 backend: &B,
1136 boundary_policy: &FvBoundaryPolicy,
1137 ) -> Result<(), AcceleratorError> {
1138 ensure_backend(self.plan.backend_id, backend.identity())?;
1139 let host = pack_boundary_conditions(
1140 self.plan.face_ids[self.plan.internal_face_count..]
1141 .iter()
1142 .copied(),
1143 boundary_policy,
1144 )?;
1145 let replacement = upload_boundary(backend, host)?;
1146 self.boundary = replacement;
1147 Ok(())
1148 }
1149
1150 pub fn validate_state<T: FvmScalar>(
1152 &self,
1153 state: &DeviceFvmState<T, B>,
1154 current_epochs: PlanEpochs,
1155 ) -> Result<(), AcceleratorError> {
1156 self.plan.validate(current_epochs)?;
1157 ensure_backend(self.plan.backend_id, state.backend_id)?;
1158 ensure_plan(self.plan.plan_id, state.plan_id)?;
1159 if state.components != self.metadata.components {
1160 return Err(AcceleratorError::InvalidPlan(format!(
1161 "operator has {} components but state has {}",
1162 self.metadata.components, state.components
1163 )));
1164 }
1165 let cells = checked_product(self.plan.cell_ids.len(), state.components, "cell state")?;
1166 let faces = checked_product(self.plan.face_count(), state.components, "face state")?;
1167 let boundaries = checked_product(
1168 self.plan.boundary_face_count,
1169 state.components,
1170 "boundary state",
1171 )?;
1172 check_len(cells, state.cell_values.len())?;
1173 check_len(cells, state.residual.len())?;
1174 check_len(cells, state.cell_source.len())?;
1175 check_len(cells, state.gradient_x.len())?;
1176 check_len(cells, state.gradient_y.len())?;
1177 check_len(cells, state.gradient_z.len())?;
1178 check_len(faces, state.face_flux.len())?;
1179 check_len(faces, state.face_source.len())?;
1180 check_len(faces, state.face_deferred_source.len())?;
1181 check_len(faces, state.face_values.len())?;
1182 check_len(self.plan.face_count(), state.face_mass_flux.len())?;
1183 check_len(boundaries, state.boundary_values.len())?;
1184 check_len(boundaries, state.boundary_override_active.len())
1185 }
1186}
1187
1188#[derive(Default)]
1189struct BoundaryHost {
1190 convective_kind: Vec<u8>,
1191 convective_alpha: Vec<f64>,
1192 convective_beta: Vec<f64>,
1193 convective_gamma: Vec<f64>,
1194 diffusive_kind: Vec<u8>,
1195 diffusive_alpha: Vec<f64>,
1196 diffusive_beta: Vec<f64>,
1197 diffusive_gamma: Vec<f64>,
1198}
1199
1200fn pack_boundary_conditions(
1201 faces: impl IntoIterator<Item = PointId>,
1202 policy: &FvBoundaryPolicy,
1203) -> Result<BoundaryHost, AcceleratorError> {
1204 let mut host = BoundaryHost::default();
1205 for face in faces {
1206 let branch = policy.boundary_face_branches.get(&face).copied();
1207 let branch = match branch {
1208 Some(branch) if policy.allowed_branches.contains(&branch) => Some(branch),
1209 Some(branch) if policy.unsupported_behavior == UnsupportedBoundaryBehavior::Error => {
1210 return Err(AcceleratorError::InvalidPlan(format!(
1211 "boundary face {face} uses unsupported branch {branch:?}"
1212 )));
1213 }
1214 None if policy.unsupported_behavior == UnsupportedBoundaryBehavior::Error => {
1215 return Err(AcceleratorError::InvalidPlan(format!(
1216 "boundary face {face} has no boundary branch"
1217 )));
1218 }
1219 _ => None,
1220 };
1221 let resolve = |hooks: &HashMap<FvBoundaryBranch, BoundaryCondition>, kind: &str| {
1222 if let Some(branch) = branch {
1223 if let Some(condition) = hooks.get(&branch) {
1224 return Ok(*condition);
1225 }
1226 if policy.unsupported_behavior == UnsupportedBoundaryBehavior::Error {
1227 return Err(AcceleratorError::InvalidPlan(format!(
1228 "boundary face {face} has no {kind} closure for branch {branch:?}"
1229 )));
1230 }
1231 }
1232 Ok(BoundaryCondition::Neumann { gradient: 0.0 })
1233 };
1234 let convective = resolve(&policy.convective_branch_hooks, "convective")?;
1235 let diffusive = resolve(&policy.diffusive_branch_hooks, "diffusive")?;
1236 if !boundary_condition_is_finite(convective) || !boundary_condition_is_finite(diffusive) {
1237 return Err(AcceleratorError::InvalidPlan(format!(
1238 "boundary face {face} has non-finite coefficients"
1239 )));
1240 }
1241 if matches!(diffusive, BoundaryCondition::Robin { beta, .. } if beta.abs() < 1.0e-14) {
1242 return Err(AcceleratorError::InvalidPlan(format!(
1243 "boundary face {face} has a Robin diffusion condition with zero beta"
1244 )));
1245 }
1246 push_boundary(
1247 convective,
1248 &mut host.convective_kind,
1249 &mut host.convective_alpha,
1250 &mut host.convective_beta,
1251 &mut host.convective_gamma,
1252 );
1253 push_boundary(
1254 diffusive,
1255 &mut host.diffusive_kind,
1256 &mut host.diffusive_alpha,
1257 &mut host.diffusive_beta,
1258 &mut host.diffusive_gamma,
1259 );
1260 }
1261 Ok(host)
1262}
1263
1264fn boundary_condition_is_finite(condition: BoundaryCondition) -> bool {
1265 match condition {
1266 BoundaryCondition::Dirichlet { value } => value.is_finite(),
1267 BoundaryCondition::Neumann { gradient } => gradient.is_finite(),
1268 BoundaryCondition::Robin { alpha, beta, gamma } => {
1269 alpha.is_finite() && beta.is_finite() && gamma.is_finite()
1270 }
1271 }
1272}
1273
1274fn push_boundary(
1275 condition: BoundaryCondition,
1276 kind: &mut Vec<u8>,
1277 alpha: &mut Vec<f64>,
1278 beta: &mut Vec<f64>,
1279 gamma: &mut Vec<f64>,
1280) {
1281 let (tag, a, b, g) = match condition {
1282 BoundaryCondition::Dirichlet { value } => (0, 1.0, 0.0, value),
1283 BoundaryCondition::Neumann { gradient } => (1, 0.0, 1.0, gradient),
1284 BoundaryCondition::Robin { alpha, beta, gamma } => (2, alpha, beta, gamma),
1285 };
1286 kind.push(tag);
1287 alpha.push(a);
1288 beta.push(b);
1289 gamma.push(g);
1290}
1291
1292fn upload_boundary<B: AcceleratorBackend>(
1293 backend: &B,
1294 host: BoundaryHost,
1295) -> Result<DeviceFvmBoundaryConditions<B>, AcceleratorError> {
1296 Ok(DeviceFvmBoundaryConditions {
1297 convective_kind: upload(backend, &host.convective_kind)?,
1298 convective_alpha: upload(backend, &host.convective_alpha)?,
1299 convective_beta: upload(backend, &host.convective_beta)?,
1300 convective_gamma: upload(backend, &host.convective_gamma)?,
1301 diffusive_kind: upload(backend, &host.diffusive_kind)?,
1302 diffusive_alpha: upload(backend, &host.diffusive_alpha)?,
1303 diffusive_beta: upload(backend, &host.diffusive_beta)?,
1304 diffusive_gamma: upload(backend, &host.diffusive_gamma)?,
1305 })
1306}
1307
1308#[derive(Default)]
1309struct LeastSquaresHost {
1310 neighbor: Vec<u32>,
1311 weight_x: Vec<f64>,
1312 weight_y: Vec<f64>,
1313 weight_z: Vec<f64>,
1314 fallback: Vec<u8>,
1315}
1316
1317fn build_least_squares(
1318 inputs: &FvmInputs,
1319 cell_ids: &[PointId],
1320) -> Result<LeastSquaresHost, AcceleratorError> {
1321 let dimension = inputs
1322 .cell_geometry
1323 .first()
1324 .map_or(0, |(_, geometry)| geometry.centroid.len());
1325 let cell_index: HashMap<_, _> = cell_ids
1326 .iter()
1327 .copied()
1328 .enumerate()
1329 .map(|(index, point)| (point, index))
1330 .collect();
1331 let cell_centers: HashMap<_, _> = inputs
1332 .cell_geometry
1333 .iter()
1334 .map(|(point, geometry)| (*point, geometry.centroid.as_slice()))
1335 .collect();
1336 let face_centers: HashMap<_, _> = inputs
1337 .face_geometry
1338 .iter()
1339 .map(|(point, geometry)| (*point, geometry.centroid.as_slice()))
1340 .collect();
1341 let mut entries: Vec<Vec<(u32, [f64; 3])>> = vec![Vec::new(); cell_ids.len()];
1342 for stencil in &inputs.loops.internal {
1343 let owner = *cell_index.get(&stencil.left).ok_or_else(|| {
1344 AcceleratorError::InvalidPlan(format!("missing owner cell {}", stencil.left))
1345 })?;
1346 let neighbor_id = stencil.right.ok_or_else(|| {
1347 AcceleratorError::InvalidPlan(format!("internal face {} has no neighbor", stencil.face))
1348 })?;
1349 let neighbor = *cell_index.get(&neighbor_id).ok_or_else(|| {
1350 AcceleratorError::InvalidPlan(format!("missing neighbor cell {neighbor_id}"))
1351 })?;
1352 let owner_center = cell_centers[&stencil.left];
1353 let neighbor_center = cell_centers[&neighbor_id];
1354 entries[owner].push((
1355 checked_u32(neighbor, "least-squares neighbor")?,
1356 delta3(neighbor_center, owner_center),
1357 ));
1358 entries[neighbor].push((
1359 checked_u32(owner, "least-squares neighbor")?,
1360 delta3(owner_center, neighbor_center),
1361 ));
1362 }
1363 for stencil in &inputs.loops.boundary {
1364 let owner = *cell_index.get(&stencil.left).ok_or_else(|| {
1365 AcceleratorError::InvalidPlan(format!("missing boundary owner {}", stencil.left))
1366 })?;
1367 let center = face_centers.get(&stencil.face).ok_or_else(|| {
1368 AcceleratorError::InvalidPlan(format!(
1369 "missing boundary face geometry {}",
1370 stencil.face
1371 ))
1372 })?;
1373 entries[owner].push((u32::MAX, delta3(center, cell_centers[&stencil.left])));
1374 }
1375
1376 let mut host = LeastSquaresHost::default();
1377 host.fallback.reserve(cell_ids.len());
1378 for cell_entries in entries {
1379 let mut normal = [[0.0; 3]; 3];
1380 for (_, delta) in &cell_entries {
1381 let d2 = dot3(*delta, *delta);
1382 let scale = if d2 > 1.0e-28 { 1.0 / d2 } else { 0.0 };
1383 for row in 0..dimension {
1384 for col in 0..dimension {
1385 normal[row][col] += scale * delta[row] * delta[col];
1386 }
1387 }
1388 }
1389 let inverse = invert_normal(normal, dimension);
1390 host.fallback.push(u8::from(inverse.is_none()));
1391 for (neighbor, delta) in cell_entries {
1392 host.neighbor.push(neighbor);
1393 if let Some(inverse) = inverse {
1394 let d2 = dot3(delta, delta);
1395 let scale = if d2 > 1.0e-28 { 1.0 / d2 } else { 0.0 };
1396 let mut weight = [0.0; 3];
1397 for row in 0..dimension {
1398 for col in 0..dimension {
1399 weight[row] += inverse[row][col] * scale * delta[col];
1400 }
1401 }
1402 host.weight_x.push(weight[0]);
1403 host.weight_y.push(weight[1]);
1404 host.weight_z.push(weight[2]);
1405 } else {
1406 host.weight_x.push(0.0);
1407 host.weight_y.push(0.0);
1408 host.weight_z.push(0.0);
1409 }
1410 }
1411 }
1412 Ok(host)
1413}
1414
1415fn delta3(to: &[f64], from: &[f64]) -> [f64; 3] {
1416 [
1417 component(to, 0) - component(from, 0),
1418 component(to, 1) - component(from, 1),
1419 component(to, 2) - component(from, 2),
1420 ]
1421}
1422
1423fn invert_normal(matrix: [[f64; 3]; 3], dimension: usize) -> Option<[[f64; 3]; 3]> {
1424 let mut inverse = [[0.0; 3]; 3];
1425 match dimension {
1426 0 => Some(inverse),
1427 1 => {
1428 if matrix[0][0].abs() <= 1.0e-12 {
1429 None
1430 } else {
1431 inverse[0][0] = 1.0 / matrix[0][0];
1432 Some(inverse)
1433 }
1434 }
1435 2 => {
1436 let determinant = matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
1437 if determinant.abs() <= 1.0e-12 {
1438 return None;
1439 }
1440 inverse[0][0] = matrix[1][1] / determinant;
1441 inverse[0][1] = -matrix[0][1] / determinant;
1442 inverse[1][0] = -matrix[1][0] / determinant;
1443 inverse[1][1] = matrix[0][0] / determinant;
1444 Some(inverse)
1445 }
1446 3 => {
1447 let determinant = matrix[0][0]
1448 * (matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1])
1449 - matrix[0][1] * (matrix[1][0] * matrix[2][2] - matrix[1][2] * matrix[2][0])
1450 + matrix[0][2] * (matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0]);
1451 if determinant.abs() <= 1.0e-12 {
1452 return None;
1453 }
1454 inverse[0][0] =
1455 (matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1]) / determinant;
1456 inverse[0][1] =
1457 (matrix[0][2] * matrix[2][1] - matrix[0][1] * matrix[2][2]) / determinant;
1458 inverse[0][2] =
1459 (matrix[0][1] * matrix[1][2] - matrix[0][2] * matrix[1][1]) / determinant;
1460 inverse[1][0] =
1461 (matrix[1][2] * matrix[2][0] - matrix[1][0] * matrix[2][2]) / determinant;
1462 inverse[1][1] =
1463 (matrix[0][0] * matrix[2][2] - matrix[0][2] * matrix[2][0]) / determinant;
1464 inverse[1][2] =
1465 (matrix[0][2] * matrix[1][0] - matrix[0][0] * matrix[1][2]) / determinant;
1466 inverse[2][0] =
1467 (matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0]) / determinant;
1468 inverse[2][1] =
1469 (matrix[0][1] * matrix[2][0] - matrix[0][0] * matrix[2][1]) / determinant;
1470 inverse[2][2] =
1471 (matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]) / determinant;
1472 Some(inverse)
1473 }
1474 _ => None,
1475 }
1476}
1477
1478impl DeviceFvmOperator<CpuBackend> {
1479 pub fn evaluate_residual<T: FvmScalar>(
1481 &self,
1482 state: &mut DeviceFvmState<T, CpuBackend>,
1483 current_epochs: PlanEpochs,
1484 ) -> Result<(), AcceleratorError> {
1485 self.validate_state(state, current_epochs)?;
1486 self.compute_operator_gradients(state)?;
1487 self.compute_operator_face_fluxes(state)?;
1488 self.gather_operator_residual(state);
1489 Ok(())
1490 }
1491
1492 fn compute_operator_gradients<T: FvmScalar>(
1493 &self,
1494 state: &mut DeviceFvmState<T, CpuBackend>,
1495 ) -> Result<(), AcceleratorError> {
1496 match self.schemes.reconstruction.mode {
1497 ReconstructionMode::GradientOnly(ReconstructionGradient::GreenGauss) => {
1498 self.green_gauss(state)
1499 }
1500 ReconstructionMode::GradientOnly(ReconstructionGradient::LeastSquares) => {
1501 self.least_squares(state, false)
1502 }
1503 ReconstructionMode::LeastSquaresWithGreenGaussFallback => {
1504 self.green_gauss(state)?;
1505 self.least_squares(state, true)
1506 }
1507 }
1508 }
1509
1510 fn green_gauss<T: FvmScalar>(
1511 &self,
1512 state: &mut DeviceFvmState<T, CpuBackend>,
1513 ) -> Result<(), AcceleratorError> {
1514 let cells = self.plan.cell_ids.len();
1515 let faces = self.plan.face_count();
1516 let boundaries = self.plan.boundary_face_count;
1517 for component_index in 0..state.components {
1518 for face in 0..self.plan.internal_face_count {
1519 let owner = self.plan.internal_owner.0[face] as usize;
1520 let neighbor = self.plan.internal_neighbor.0[face] as usize;
1521 state.face_values.0[component_index * faces + face] = T::from_f64(
1522 0.5 * (state.cell_values.0[component_index * cells + owner].to_f64()
1523 + state.cell_values.0[component_index * cells + neighbor].to_f64()),
1524 );
1525 }
1526 for boundary in 0..boundaries {
1527 let face = self.plan.internal_face_count + boundary;
1528 let owner = self.plan.boundary_owner.0[boundary] as usize;
1529 let inside = state.cell_values.0[component_index * cells + owner].to_f64();
1530 let exterior = boundary_exterior(
1531 self.boundary.convective_kind.0[boundary],
1532 self.boundary.convective_alpha.0[boundary],
1533 self.boundary.convective_beta.0[boundary],
1534 self.boundary.convective_gamma.0[boundary],
1535 inside,
1536 state.boundary_override_active.0[component_index * boundaries + boundary] != 0,
1537 state.boundary_values.0[component_index * boundaries + boundary].to_f64(),
1538 );
1539 state.face_values.0[component_index * faces + face] = T::from_f64(exterior);
1540 }
1541 for cell in 0..cells {
1542 let output = component_index * cells + cell;
1543 if self.plan.cell_active.0[cell] == 0 {
1544 state.gradient_x.0[output] = T::zeroed();
1545 state.gradient_y.0[output] = T::zeroed();
1546 state.gradient_z.0[output] = T::zeroed();
1547 continue;
1548 }
1549 let begin = self.plan.cell_face_offsets.0[cell] as usize;
1550 let end = self.plan.cell_face_offsets.0[cell + 1] as usize;
1551 let mut gradient = [0.0; 3];
1552 for incidence in begin..end {
1553 let face = self.plan.cell_face_indices.0[incidence] as usize;
1554 if self.plan.face_active.0[face] == 0 {
1555 continue;
1556 }
1557 let geometry = self.plan.face_geometry_indices.0[face] as usize;
1558 let value = state.face_values.0[component_index * faces + face].to_f64();
1559 let scale = self.plan.cell_face_signs.0[incidence] as f64 * value;
1560 gradient[0] += scale * self.plan.face_normal_x.0[geometry];
1561 gradient[1] += scale * self.plan.face_normal_y.0[geometry];
1562 gradient[2] += scale * self.plan.face_normal_z.0[geometry];
1563 }
1564 let volume = self.plan.cell_volume.0[cell];
1565 state.gradient_x.0[output] = T::from_f64(gradient[0] / volume);
1566 state.gradient_y.0[output] = T::from_f64(gradient[1] / volume);
1567 state.gradient_z.0[output] = T::from_f64(gradient[2] / volume);
1568 }
1569 }
1570 Ok(())
1571 }
1572
1573 fn least_squares<T: FvmScalar>(
1574 &self,
1575 state: &mut DeviceFvmState<T, CpuBackend>,
1576 preserve_fallback: bool,
1577 ) -> Result<(), AcceleratorError> {
1578 let cells = self.plan.cell_ids.len();
1579 for component_index in 0..state.components {
1580 for cell in 0..cells {
1581 if preserve_fallback && self.least_squares.fallback_host[cell] != 0 {
1582 continue;
1583 }
1584 let output = component_index * cells + cell;
1585 if self.plan.cell_active.0[cell] == 0 {
1586 state.gradient_x.0[output] = T::zeroed();
1587 state.gradient_y.0[output] = T::zeroed();
1588 state.gradient_z.0[output] = T::zeroed();
1589 continue;
1590 }
1591 let center_value = state.cell_values.0[output].to_f64();
1592 let begin = self.plan.cell_face_offsets.0[cell] as usize;
1593 let end = self.plan.cell_face_offsets.0[cell + 1] as usize;
1594 let mut gradient = [0.0; 3];
1595 for incidence in begin..end {
1596 let face = self.plan.cell_face_indices.0[incidence] as usize;
1597 if self.plan.face_active.0[face] == 0 {
1598 continue;
1599 }
1600 let neighbor = self.least_squares.neighbor.0[incidence];
1601 let sample = if neighbor == u32::MAX {
1602 let boundary =
1603 face.checked_sub(self.plan.internal_face_count)
1604 .ok_or_else(|| {
1605 AcceleratorError::InvalidPlan(
1606 "boundary LS sample is internal".into(),
1607 )
1608 })?;
1609 boundary_exterior(
1610 self.boundary.convective_kind.0[boundary],
1611 self.boundary.convective_alpha.0[boundary],
1612 self.boundary.convective_beta.0[boundary],
1613 self.boundary.convective_gamma.0[boundary],
1614 center_value,
1615 state.boundary_override_active.0
1616 [component_index * self.plan.boundary_face_count + boundary]
1617 != 0,
1618 state.boundary_values.0
1619 [component_index * self.plan.boundary_face_count + boundary]
1620 .to_f64(),
1621 )
1622 } else {
1623 state.cell_values.0[component_index * cells + neighbor as usize].to_f64()
1624 };
1625 let difference = sample - center_value;
1626 gradient[0] += self.least_squares.weight_x.0[incidence] * difference;
1627 gradient[1] += self.least_squares.weight_y.0[incidence] * difference;
1628 gradient[2] += self.least_squares.weight_z.0[incidence] * difference;
1629 }
1630 state.gradient_x.0[output] = T::from_f64(gradient[0]);
1631 state.gradient_y.0[output] = T::from_f64(gradient[1]);
1632 state.gradient_z.0[output] = T::from_f64(gradient[2]);
1633 }
1634 }
1635 Ok(())
1636 }
1637
1638 fn compute_operator_face_fluxes<T: FvmScalar>(
1639 &self,
1640 state: &mut DeviceFvmState<T, CpuBackend>,
1641 ) -> Result<(), AcceleratorError> {
1642 let cells = self.plan.cell_ids.len();
1643 let faces = self.plan.face_count();
1644 let reconstruct = self.metadata.reconstruction_order > 1;
1645 for component_index in 0..state.components {
1646 for face in 0..self.plan.internal_face_count {
1647 let output = component_index * faces + face;
1648 if self.plan.face_active.0[face] == 0 {
1649 state.face_flux.0[output] = T::zeroed();
1650 state.face_deferred_source.0[output] = T::zeroed();
1651 continue;
1652 }
1653 let owner = self.plan.internal_owner.0[face] as usize;
1654 let neighbor = self.plan.internal_neighbor.0[face] as usize;
1655 let geometry = self.plan.internal_geometry.0[face] as usize;
1656 let left = reconstructed_value(
1657 state,
1658 component_index,
1659 cells,
1660 owner,
1661 geometry,
1662 &self.plan,
1663 reconstruct,
1664 );
1665 let right = reconstructed_value(
1666 state,
1667 component_index,
1668 cells,
1669 neighbor,
1670 geometry,
1671 &self.plan,
1672 reconstruct,
1673 );
1674 let inside_left = state.cell_values.0[component_index * cells + owner].to_f64();
1675 let inside_right = state.cell_values.0[component_index * cells + neighbor].to_f64();
1676 let mass = state.face_mass_flux.0[face].to_f64();
1677 let convective = mass
1678 * operator_face_value(
1679 left,
1680 right,
1681 inside_left,
1682 inside_right,
1683 mass,
1684 self.schemes.convective,
1685 self.schemes.reconstruction.limiter,
1686 );
1687 let delta = [
1688 self.plan.cell_center_x.0[neighbor] - self.plan.cell_center_x.0[owner],
1689 self.plan.cell_center_y.0[neighbor] - self.plan.cell_center_y.0[owner],
1690 self.plan.cell_center_z.0[neighbor] - self.plan.cell_center_z.0[owner],
1691 ];
1692 let normal = [
1693 self.plan.face_normal_x.0[geometry],
1694 self.plan.face_normal_y.0[geometry],
1695 self.plan.face_normal_z.0[geometry],
1696 ];
1697 let d2 = dot3(delta, delta);
1698 let orthogonal = if d2 > 0.0 {
1699 -self.schemes.diffusion.diffusivity
1700 * (inside_right - inside_left)
1701 * dot3(normal, delta)
1702 / d2
1703 } else {
1704 0.0
1705 };
1706 let average_gradient = [
1707 0.5 * (state.gradient_x.0[component_index * cells + owner].to_f64()
1708 + state.gradient_x.0[component_index * cells + neighbor].to_f64()),
1709 0.5 * (state.gradient_y.0[component_index * cells + owner].to_f64()
1710 + state.gradient_y.0[component_index * cells + neighbor].to_f64()),
1711 0.5 * (state.gradient_z.0[component_index * cells + owner].to_f64()
1712 + state.gradient_z.0[component_index * cells + neighbor].to_f64()),
1713 ];
1714 let orthogonal_normal = if d2 > 0.0 {
1715 let scale = dot3(normal, delta) / d2;
1716 [delta[0] * scale, delta[1] * scale, delta[2] * scale]
1717 } else {
1718 [0.0; 3]
1719 };
1720 let nonorthogonal = -self.schemes.diffusion.diffusivity
1721 * dot3(
1722 average_gradient,
1723 [
1724 normal[0] - orthogonal_normal[0],
1725 normal[1] - orthogonal_normal[1],
1726 normal[2] - orthogonal_normal[2],
1727 ],
1728 );
1729 let (diffusive, deferred) = match self.schemes.diffusion.non_orthogonal_mode {
1730 NonOrthogonalCorrectionMode::OrthogonalOnly => (orthogonal, 0.0),
1731 NonOrthogonalCorrectionMode::Deferred => (orthogonal, nonorthogonal),
1732 NonOrthogonalCorrectionMode::FullyCorrected => {
1733 (orthogonal + nonorthogonal, 0.0)
1734 }
1735 };
1736 state.face_deferred_source.0[output] = T::from_f64(deferred);
1737 state.face_flux.0[output] =
1738 T::from_f64(convective + diffusive + state.face_source.0[output].to_f64());
1739 }
1740 for boundary in 0..self.plan.boundary_face_count {
1741 let face = self.plan.internal_face_count + boundary;
1742 let output = component_index * faces + face;
1743 if self.plan.face_active.0[face] == 0 {
1744 state.face_flux.0[output] = T::zeroed();
1745 state.face_deferred_source.0[output] = T::zeroed();
1746 continue;
1747 }
1748 let owner = self.plan.boundary_owner.0[boundary] as usize;
1749 let geometry = self.plan.boundary_geometry.0[boundary] as usize;
1750 let inside = reconstructed_value(
1751 state,
1752 component_index,
1753 cells,
1754 owner,
1755 geometry,
1756 &self.plan,
1757 reconstruct,
1758 );
1759 let exterior = boundary_exterior(
1760 self.boundary.convective_kind.0[boundary],
1761 self.boundary.convective_alpha.0[boundary],
1762 self.boundary.convective_beta.0[boundary],
1763 self.boundary.convective_gamma.0[boundary],
1764 inside,
1765 state.boundary_override_active.0
1766 [component_index * self.plan.boundary_face_count + boundary]
1767 != 0,
1768 state.boundary_values.0
1769 [component_index * self.plan.boundary_face_count + boundary]
1770 .to_f64(),
1771 );
1772 let cell_value = state.cell_values.0[component_index * cells + owner].to_f64();
1773 let base_exterior = boundary_exterior(
1774 self.boundary.convective_kind.0[boundary],
1775 self.boundary.convective_alpha.0[boundary],
1776 self.boundary.convective_beta.0[boundary],
1777 self.boundary.convective_gamma.0[boundary],
1778 cell_value,
1779 state.boundary_override_active.0
1780 [component_index * self.plan.boundary_face_count + boundary]
1781 != 0,
1782 state.boundary_values.0
1783 [component_index * self.plan.boundary_face_count + boundary]
1784 .to_f64(),
1785 );
1786 let mass = state.face_mass_flux.0[face].to_f64();
1787 let convective = mass
1788 * operator_face_value(
1789 inside,
1790 exterior,
1791 cell_value,
1792 base_exterior,
1793 mass,
1794 self.schemes.convective,
1795 self.schemes.reconstruction.limiter,
1796 );
1797 let area = self.plan.face_area.0[geometry];
1798 let dirichlet_value = if state.boundary_override_active.0
1799 [component_index * self.plan.boundary_face_count + boundary]
1800 != 0
1801 {
1802 state.boundary_values.0
1803 [component_index * self.plan.boundary_face_count + boundary]
1804 .to_f64()
1805 } else {
1806 self.boundary.diffusive_gamma.0[boundary]
1807 };
1808 let diffusive = match self.boundary.diffusive_kind.0[boundary] {
1809 0 => {
1810 let delta = [
1811 self.plan.face_center_x.0[geometry] - self.plan.cell_center_x.0[owner],
1812 self.plan.face_center_y.0[geometry] - self.plan.cell_center_y.0[owner],
1813 self.plan.face_center_z.0[geometry] - self.plan.cell_center_z.0[owner],
1814 ];
1815 let distance = dot3(delta, delta).sqrt().max(1.0e-14);
1816 -self.schemes.diffusion.diffusivity * (dirichlet_value - cell_value)
1817 / distance
1818 * area
1819 }
1820 1 => {
1821 -self.schemes.diffusion.diffusivity
1822 * self.boundary.diffusive_gamma.0[boundary]
1823 * area
1824 }
1825 _ => {
1826 let beta = self.boundary.diffusive_beta.0[boundary];
1827 let gradient = (self.boundary.diffusive_gamma.0[boundary]
1828 - self.boundary.diffusive_alpha.0[boundary] * cell_value)
1829 / beta;
1830 -self.schemes.diffusion.diffusivity * gradient * area
1831 }
1832 };
1833 state.face_deferred_source.0[output] = T::zeroed();
1834 state.face_flux.0[output] =
1835 T::from_f64(convective + diffusive + state.face_source.0[output].to_f64());
1836 }
1837 }
1838 Ok(())
1839 }
1840
1841 fn gather_operator_residual<T: FvmScalar>(&self, state: &mut DeviceFvmState<T, CpuBackend>) {
1842 let cells = self.plan.cell_ids.len();
1843 let faces = self.plan.face_count();
1844 for component_index in 0..state.components {
1845 for cell in 0..cells {
1846 let output = component_index * cells + cell;
1847 if self.plan.cell_active.0[cell] == 0 {
1848 state.residual.0[output] = T::zeroed();
1849 continue;
1850 }
1851 let begin = self.plan.cell_face_offsets.0[cell] as usize;
1852 let end = self.plan.cell_face_offsets.0[cell + 1] as usize;
1853 let mut residual = state.cell_source.0[output].to_f64();
1854 for incidence in begin..end {
1855 let face = self.plan.cell_face_indices.0[incidence] as usize;
1856 let sign = self.plan.cell_face_signs.0[incidence] as f64;
1857 residual += sign
1858 * (state.face_flux.0[component_index * faces + face].to_f64()
1859 + state.face_deferred_source.0[component_index * faces + face]
1860 .to_f64());
1861 }
1862 state.residual.0[output] = T::from_f64(residual);
1863 }
1864 }
1865 }
1866}
1867
1868fn reconstructed_value<T: FvmScalar>(
1869 state: &DeviceFvmState<T, CpuBackend>,
1870 component_index: usize,
1871 cell_count: usize,
1872 cell: usize,
1873 geometry: usize,
1874 plan: &DeviceFvmPlan<CpuBackend>,
1875 reconstruct: bool,
1876) -> f64 {
1877 let index = component_index * cell_count + cell;
1878 let value = state.cell_values.0[index].to_f64();
1879 if !reconstruct {
1880 return value;
1881 }
1882 value
1883 + state.gradient_x.0[index].to_f64()
1884 * (plan.face_center_x.0[geometry] - plan.cell_center_x.0[cell])
1885 + state.gradient_y.0[index].to_f64()
1886 * (plan.face_center_y.0[geometry] - plan.cell_center_y.0[cell])
1887 + state.gradient_z.0[index].to_f64()
1888 * (plan.face_center_z.0[geometry] - plan.cell_center_z.0[cell])
1889}
1890
1891fn boundary_exterior(
1892 kind: u8,
1893 alpha: f64,
1894 beta: f64,
1895 gamma: f64,
1896 inside: f64,
1897 override_active: bool,
1898 override_value: f64,
1899) -> f64 {
1900 match kind {
1901 0 if override_active => override_value,
1902 0 => gamma,
1903 1 => inside,
1904 _ if alpha.abs() < 1.0e-14 => inside,
1905 _ => (gamma - beta * inside) / alpha,
1906 }
1907}
1908
1909fn operator_face_value(
1910 left: f64,
1911 right: f64,
1912 base_left: f64,
1913 base_right: f64,
1914 mass: f64,
1915 scheme: ConvectiveScheme,
1916 limiter: LimiterOption,
1917) -> f64 {
1918 let upwind = if mass >= 0.0 { left } else { right };
1919 let central = 0.5 * (left + right);
1920 let minimum = left.min(right);
1921 let maximum = left.max(right);
1922 match scheme {
1923 ConvectiveScheme::Upwind => upwind,
1924 ConvectiveScheme::Central => central,
1925 ConvectiveScheme::BoundedLinear { blend } => {
1926 (upwind + blend.clamp(0.0, 1.0) * (central - upwind)).clamp(minimum, maximum)
1927 }
1928 ConvectiveScheme::BlendUpwindCentral { blend } => {
1929 let blend = blend.clamp(0.0, 1.0);
1930 (1.0 - blend) * upwind + blend * central
1931 }
1932 ConvectiveScheme::HighResolution {
1933 blend,
1934 limiter: scheme_limiter,
1935 } => {
1936 let (base_upwind, base_downwind, reconstructed_upwind) = if mass >= 0.0 {
1937 (base_left, base_right, left)
1938 } else {
1939 (base_right, base_left, right)
1940 };
1941 let correction = reconstructed_upwind - base_upwind;
1942 let ratio = if correction.abs() < 1.0e-14 {
1943 1.0
1944 } else {
1945 (base_downwind - base_upwind) / (2.0 * correction)
1946 };
1947 let psi = limiter_factor(limiter, ratio) * slope_limiter_factor(scheme_limiter, ratio);
1948 let high_resolution = base_upwind + psi * correction;
1949 let blend = blend.clamp(0.0, 1.0);
1950 ((1.0 - blend) * base_upwind + blend * high_resolution)
1951 .clamp(base_left.min(base_right), base_left.max(base_right))
1952 }
1953 }
1954}
1955
1956fn limiter_factor(limiter: LimiterOption, ratio: f64) -> f64 {
1957 match limiter {
1958 LimiterOption::None => 1.0,
1959 LimiterOption::Family(family) => slope_limiter_factor(family, ratio),
1960 }
1961}
1962
1963fn slope_limiter_factor(limiter: SlopeLimiterFamily, ratio: f64) -> f64 {
1964 match limiter {
1965 SlopeLimiterFamily::None => 1.0,
1966 SlopeLimiterFamily::Minmod => ratio.clamp(0.0, 1.0),
1967 SlopeLimiterFamily::VanLeer => (ratio + ratio.abs()) / (1.0 + ratio.abs()),
1968 SlopeLimiterFamily::Superbee => (2.0 * ratio).min(1.0).max(ratio.min(2.0)).max(0.0),
1969 }
1970}
1971
1972fn checked_product(
1973 count: usize,
1974 components: usize,
1975 what: &'static str,
1976) -> Result<usize, AcceleratorError> {
1977 count
1978 .checked_mul(components)
1979 .ok_or(AcceleratorError::IndexOverflow {
1980 what,
1981 value: usize::MAX,
1982 })
1983}
1984
1985fn allocate<T: FvmScalar, B: AcceleratorBackend>(
1986 backend: &B,
1987 len: usize,
1988) -> Result<B::Buffer<T>, AcceleratorError> {
1989 backend
1990 .allocate(len)
1991 .map_err(|e| AcceleratorError::AllocationFailed {
1992 bytes: len.saturating_mul(std::mem::size_of::<T>()),
1993 reason: e.to_string(),
1994 })
1995}
1996
1997fn component(values: &[f64], index: usize) -> f64 {
1998 values.get(index).copied().unwrap_or(0.0)
1999}
2000
2001fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
2002 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
2003}
2004
2005fn lookup(
2006 map: &HashMap<PointId, u32>,
2007 point: PointId,
2008 kind: &'static str,
2009) -> Result<u32, AcceleratorError> {
2010 map.get(&point).copied().ok_or_else(|| {
2011 AcceleratorError::InvalidPlan(format!("{kind} {point} is absent from packed geometry"))
2012 })
2013}
2014
2015fn check_len(expected: usize, found: usize) -> Result<(), AcceleratorError> {
2016 if expected == found {
2017 Ok(())
2018 } else {
2019 Err(AcceleratorError::LengthMismatch { expected, found })
2020 }
2021}
2022
2023pub(crate) fn ensure_backend(expected: u64, found: u64) -> Result<(), AcceleratorError> {
2024 if expected == found {
2025 Ok(())
2026 } else {
2027 Err(AcceleratorError::BackendMismatch { expected, found })
2028 }
2029}
2030
2031fn ensure_plan(expected: u64, found: u64) -> Result<(), AcceleratorError> {
2032 if expected == found {
2033 Ok(())
2034 } else {
2035 Err(AcceleratorError::PlanMismatch { expected, found })
2036 }
2037}
2038
2039fn encode_boundary_kind(branch: FvBoundaryBranch) -> u32 {
2040 match branch {
2041 FvBoundaryBranch::Open => 0,
2042 FvBoundaryBranch::Inflow => 1,
2043 FvBoundaryBranch::Outflow => 2,
2044 FvBoundaryBranch::Tidal => 3,
2045 FvBoundaryBranch::Bed => 4,
2046 FvBoundaryBranch::FreeSurface => 5,
2047 }
2048}
2049
2050#[cfg(test)]
2051mod numerical_contract_tests {
2052 use super::*;
2053
2054 #[test]
2055 fn limiter_families_produce_distinct_wider_stencil_corrections() {
2056 let value = |limiter| {
2057 operator_face_value(
2058 1.5,
2059 4.0,
2060 1.0,
2061 4.0,
2062 1.0,
2063 ConvectiveScheme::HighResolution {
2064 blend: 1.0,
2065 limiter,
2066 },
2067 LimiterOption::None,
2068 )
2069 };
2070 assert_eq!(value(SlopeLimiterFamily::Minmod), 1.5);
2071 assert_eq!(value(SlopeLimiterFamily::VanLeer), 1.75);
2072 assert_eq!(value(SlopeLimiterFamily::Superbee), 2.0);
2073 }
2074}