Skip to main content

mesh_sieve/accelerator/
fvm.rs

1//! Device-ready finite-volume plans and CPU reference execution.
2
3use 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/// Stable device ABI for an internal face.
23#[repr(C)]
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)]
25pub struct DeviceInternalFace {
26    /// Owner cell dense index.
27    pub owner: u32,
28    /// Neighbor cell dense index.
29    pub neighbor: u32,
30    /// Geometry dense index.
31    pub geometry: u32,
32    /// Reserved flags for model-specific use.
33    pub flags: u32,
34}
35
36// SAFETY: repr(C), Copy, and every field is a CUDA-representable u32.
37#[cfg(feature = "cuda")]
38unsafe impl cudarc::driver::DeviceRepr for DeviceInternalFace {}
39// SAFETY: the all-zero bit pattern is valid for every u32 field.
40#[cfg(feature = "cuda")]
41unsafe impl cudarc::driver::ValidAsZeroBits for DeviceInternalFace {}
42
43/// Stable device ABI for a boundary face.
44#[repr(C)]
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Pod, Zeroable)]
46pub struct DeviceBoundaryFace {
47    /// Owner cell dense index.
48    pub owner: u32,
49    /// Geometry dense index.
50    pub geometry: u32,
51    /// Encoded [`FvBoundaryBranch`], or `u32::MAX` if unclassified.
52    pub kind: u32,
53    /// Reserved flags for model-specific use.
54    pub flags: u32,
55}
56
57// SAFETY: repr(C), Copy, and every field is a CUDA-representable u32.
58#[cfg(feature = "cuda")]
59unsafe impl cudarc::driver::DeviceRepr for DeviceBoundaryFace {}
60// SAFETY: the all-zero bit pattern is valid for every u32 field.
61#[cfg(feature = "cuda")]
62unsafe impl cudarc::driver::ValidAsZeroBits for DeviceBoundaryFace {}
63
64/// Built-in device physics parameters. Solver-specific state stays outside
65/// mesh-sieve and may be supplied through custom PTX entry points.
66#[repr(C)]
67#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
68pub struct DevicePhysicsParams {
69    /// Fluid density.
70    pub density: f64,
71    /// Kinematic/dynamic viscosity selected by the caller's model.
72    pub viscosity: f64,
73    /// Gravitational acceleration.
74    pub gravity: f64,
75    /// Scalar diffusivity.
76    pub diffusivity: f64,
77}
78
79// SAFETY: repr(C), Copy, and every field is a CUDA-representable f64.
80#[cfg(feature = "cuda")]
81unsafe impl cudarc::driver::DeviceRepr for DevicePhysicsParams {}
82// SAFETY: the all-zero bit pattern is valid for every f64 field.
83#[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/// Scalar convective interpolation available in the first device executor.
98#[repr(u32)]
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum ScalarFluxScheme {
101    /// Select the upwind cell value using the sign of mass flux.
102    Upwind = 0,
103    /// Average owner and neighbor/boundary values.
104    Central = 1,
105}
106
107/// Floating-point types supported by the packed FVM executor.
108pub trait FvmScalar: DeviceValue + Copy + Default + std::fmt::Debug + PartialEq {
109    /// Convert from the reference executor's accumulation type.
110    fn from_f64(value: f64) -> Self;
111    /// Convert to the reference executor's accumulation type.
112    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
133/// Structure-of-arrays device plan for conservative face/cell execution.
134pub struct DeviceFvmPlan<B: AcceleratorBackend> {
135    pub(crate) backend_id: u64,
136    plan_id: u64,
137    /// Epochs captured when this plan was compiled.
138    pub(crate) epochs: PlanEpochs,
139    /// Stable cell IDs in dense field order (host-only diagnostics/output map).
140    pub(crate) cell_ids: Vec<PointId>,
141    /// Stable face IDs in flux-array order: internal, then boundary.
142    pub(crate) face_ids: Vec<PointId>,
143    /// Internal face owner cell indices.
144    pub(crate) internal_owner: B::Buffer<u32>,
145    /// Internal face neighbor cell indices.
146    pub(crate) internal_neighbor: B::Buffer<u32>,
147    /// Internal face geometry indices.
148    pub(crate) internal_geometry: B::Buffer<u32>,
149    /// Boundary face owner cell indices.
150    pub(crate) boundary_owner: B::Buffer<u32>,
151    /// Boundary face geometry indices.
152    pub(crate) boundary_geometry: B::Buffer<u32>,
153    /// Encoded boundary branch.
154    pub(crate) boundary_kind: B::Buffer<u32>,
155    /// Face area in input geometry order.
156    pub(crate) face_area: B::Buffer<f64>,
157    /// X component of the owner-oriented face normal.
158    pub(crate) face_normal_x: B::Buffer<f64>,
159    /// Y component of the owner-oriented face normal.
160    pub(crate) face_normal_y: B::Buffer<f64>,
161    /// Z component of the owner-oriented face normal.
162    pub(crate) face_normal_z: B::Buffer<f64>,
163    /// X component of face center.
164    pub(crate) face_center_x: B::Buffer<f64>,
165    /// Y component of face center.
166    pub(crate) face_center_y: B::Buffer<f64>,
167    /// Z component of face center.
168    pub(crate) face_center_z: B::Buffer<f64>,
169    /// Geometry index for each face in combined flux order.
170    pub(crate) face_geometry_indices: B::Buffer<u32>,
171    /// Cell volume in dense field order.
172    pub(crate) cell_volume: B::Buffer<f64>,
173    /// X component of cell center.
174    pub(crate) cell_center_x: B::Buffer<f64>,
175    /// Y component of cell center.
176    pub(crate) cell_center_y: B::Buffer<f64>,
177    /// Z component of cell center.
178    pub(crate) cell_center_z: B::Buffer<f64>,
179    /// Cell-to-face CSR offsets.
180    pub(crate) cell_face_offsets: B::Buffer<u32>,
181    /// Indices into the combined face flux array.
182    pub(crate) cell_face_indices: B::Buffer<u32>,
183    /// `+1` for owner and `-1` for neighbor contribution.
184    pub(crate) cell_face_signs: B::Buffer<i8>,
185    /// One byte per face; zero suppresses its flux.
186    pub(crate) face_active: B::Buffer<u8>,
187    /// One byte per cell; zero marks a dry/inactive cell.
188    pub(crate) cell_active: B::Buffer<u8>,
189    /// Number of spatial dimensions represented by the geometry.
190    pub(crate) dimension: usize,
191    /// Number of internal faces.
192    pub(crate) internal_face_count: usize,
193    /// Number of boundary faces.
194    pub(crate) boundary_face_count: usize,
195}
196
197static NEXT_PLAN_ID: AtomicU64 = AtomicU64::new(1);
198
199impl<B: AcceleratorBackend> DeviceFvmPlan<B> {
200    /// Compile FVM stencils, geometry, and optional wet/dry/boundary labels.
201    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    /// Reject execution with stale topology, atlas, or geometry epochs.
486    pub fn validate(&self, current: PlanEpochs) -> Result<(), AcceleratorError> {
487        self.epochs.validate(current)
488    }
489
490    /// Total number of faces in the combined flux array.
491    pub fn face_count(&self) -> usize {
492        self.internal_face_count + self.boundary_face_count
493    }
494
495    /// Stable cell IDs in dense field order.
496    pub fn cell_ids(&self) -> &[PointId] {
497        &self.cell_ids
498    }
499
500    /// Stable face IDs in combined internal/boundary order.
501    pub fn face_ids(&self) -> &[PointId] {
502        &self.face_ids
503    }
504
505    /// Number of packed cells.
506    pub fn cell_count(&self) -> usize {
507        self.cell_ids.len()
508    }
509
510    /// Number of spatial dimensions represented by the geometry.
511    pub fn dimension(&self) -> usize {
512        self.dimension
513    }
514
515    /// Number of internal faces.
516    pub fn internal_face_count(&self) -> usize {
517        self.internal_face_count
518    }
519
520    /// Number of boundary faces.
521    pub fn boundary_face_count(&self) -> usize {
522        self.boundary_face_count
523    }
524
525    /// Read-only cell activity mask.
526    pub fn cell_active(&self) -> &B::Buffer<u8> {
527        &self.cell_active
528    }
529
530    /// Read-only face activity mask.
531    pub fn face_active(&self) -> &B::Buffer<u8> {
532        &self.face_active
533    }
534}
535
536/// Persistent scalar fields used by the two-kernel conservative FVM path.
537pub struct DeviceFvmState<T: FvmScalar, B: AcceleratorBackend> {
538    pub(crate) backend_id: u64,
539    plan_id: u64,
540    /// Number of component-major fields in each cell/face workspace.
541    components: usize,
542    /// Cell-centered scalar values.
543    pub(crate) cell_values: B::Buffer<T>,
544    /// Mass flux values in the plan's combined face order.
545    pub(crate) face_mass_flux: B::Buffer<T>,
546    /// Boundary exterior values in boundary-face order.
547    pub(crate) boundary_values: B::Buffer<T>,
548    /// Marks component/face entries that explicitly override packed Dirichlet values.
549    pub(crate) boundary_override_active: B::Buffer<u8>,
550    /// Oriented flux values in combined face order.
551    pub(crate) face_flux: B::Buffer<T>,
552    /// Explicit caller-provided face flux additions.
553    pub(crate) face_source: B::Buffer<T>,
554    /// Deferred non-orthogonal source contribution per face/component.
555    pub(crate) face_deferred_source: B::Buffer<T>,
556    /// Interpolated face values used by Green--Gauss gradients.
557    pub(crate) face_values: B::Buffer<T>,
558    /// Gathered cell residuals.
559    pub(crate) residual: B::Buffer<T>,
560    /// Explicit caller-provided cell source additions.
561    pub(crate) cell_source: B::Buffer<T>,
562    /// X component of the cell gradient.
563    pub(crate) gradient_x: B::Buffer<T>,
564    /// Y component of the cell gradient.
565    pub(crate) gradient_y: B::Buffer<T>,
566    /// Z component of the cell gradient.
567    pub(crate) gradient_z: B::Buffer<T>,
568}
569
570impl<T: FvmScalar, B: AcceleratorBackend> DeviceFvmState<T, B> {
571    /// Upload persistent field data and allocate flux/residual workspaces.
572    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    /// Upload component-major state and allocate persistent workspaces.
590    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    /// Refresh explicit component-major face and cell source arrays.
637    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    /// Enable explicit per-component Dirichlet overrides for every boundary face.
655    ///
656    /// Operator evaluations normally use the values packed in the boundary policy.
657    /// This method is the explicit opt-in for component-dependent Dirichlet data.
658    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    /// Disable all explicit per-component Dirichlet overrides.
674    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    /// Number of component-major fields in this state.
683    pub fn components(&self) -> usize {
684        self.components
685    }
686
687    /// Read-only resident cell values.
688    pub fn cell_values(&self) -> &B::Buffer<T> {
689        &self.cell_values
690    }
691
692    /// Read-only resident face mass fluxes.
693    pub fn face_mass_flux(&self) -> &B::Buffer<T> {
694        &self.face_mass_flux
695    }
696
697    /// Read-only resident residual values.
698    pub fn residual(&self) -> &B::Buffer<T> {
699        &self.residual
700    }
701
702    /// Read-only resident x-gradient values.
703    pub fn gradient_x(&self) -> &B::Buffer<T> {
704        &self.gradient_x
705    }
706
707    /// Refresh all component-major cell values.
708    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    /// Refresh the shared face mass-flux field.
721    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    /// Download the gathered residual vector.
734    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    /// Reference Green--Gauss gradient using the plan's deterministic CSR.
746    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    /// Reference orthogonal internal/boundary diffusive face fluxes.
800    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    /// Reference implementation of internal/boundary face flux kernels.
869    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    /// Reference implementation of deterministic cell residual gathering.
923    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
974/// Packed standard boundary conditions for convective and diffusive kernels.
975pub struct DeviceFvmBoundaryConditions<B: AcceleratorBackend> {
976    /// Boundary-condition tag: 0 Dirichlet, 1 Neumann, 2 Robin.
977    pub(crate) convective_kind: B::Buffer<u8>,
978    /// Convective Robin/Dirichlet alpha coefficient.
979    pub(crate) convective_alpha: B::Buffer<f64>,
980    /// Convective beta coefficient.
981    pub(crate) convective_beta: B::Buffer<f64>,
982    /// Convective prescribed value/gradient/gamma coefficient.
983    pub(crate) convective_gamma: B::Buffer<f64>,
984    /// Boundary-condition tag: 0 Dirichlet, 1 Neumann, 2 Robin.
985    pub(crate) diffusive_kind: B::Buffer<u8>,
986    /// Diffusive alpha coefficient.
987    pub(crate) diffusive_alpha: B::Buffer<f64>,
988    /// Diffusive beta coefficient.
989    pub(crate) diffusive_beta: B::Buffer<f64>,
990    /// Diffusive prescribed value/gradient/gamma coefficient.
991    pub(crate) diffusive_gamma: B::Buffer<f64>,
992}
993
994/// Precomputed least-squares coefficients aligned with cell-face CSR entries.
995pub struct DeviceLeastSquaresPlan<B: AcceleratorBackend> {
996    /// Neighbor cell, or `u32::MAX` for a boundary sample.
997    pub(crate) neighbor: B::Buffer<u32>,
998    /// X coefficient multiplying the neighbor/boundary value difference.
999    pub(crate) weight_x: B::Buffer<f64>,
1000    /// Y coefficient multiplying the neighbor/boundary value difference.
1001    pub(crate) weight_y: B::Buffer<f64>,
1002    /// Z coefficient multiplying the neighbor/boundary value difference.
1003    pub(crate) weight_z: B::Buffer<f64>,
1004    /// One for cells whose least-squares normal matrix was singular.
1005    pub(crate) fallback: B::Buffer<u8>,
1006    fallback_host: Vec<u8>,
1007}
1008
1009/// Complete, persistent finite-volume numerical operator.
1010pub struct DeviceFvmOperator<B: AcceleratorBackend> {
1011    /// Connectivity, geometry, masks, and deterministic gather structure.
1012    pub(crate) plan: DeviceFvmPlan<B>,
1013    /// Number of transported components and reconstruction order.
1014    pub(crate) metadata: FiniteVolumeMetadata,
1015    /// Numerical schemes captured by the operator.
1016    pub(crate) schemes: FvmSchemeSettings,
1017    /// Packed standard boundary conditions.
1018    pub(crate) boundary: DeviceFvmBoundaryConditions<B>,
1019    /// Precomputed least-squares reconstruction data.
1020    pub(crate) least_squares: DeviceLeastSquaresPlan<B>,
1021}
1022
1023impl<B: AcceleratorBackend> DeviceFvmOperator<B> {
1024    /// Immutable packed plan owned by this operator.
1025    pub fn plan(&self) -> &DeviceFvmPlan<B> {
1026        &self.plan
1027    }
1028
1029    /// Immutable finite-volume metadata captured at compilation.
1030    pub fn metadata(&self) -> &FiniteVolumeMetadata {
1031        &self.metadata
1032    }
1033
1034    /// Immutable numerical settings captured at compilation.
1035    pub fn schemes(&self) -> &FvmSchemeSettings {
1036        &self.schemes
1037    }
1038
1039    /// Compile a complete numerical operator without uploading solution state.
1040    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    /// Refresh boundary coefficients while preserving topology and geometry.
1133    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    /// Validate epochs and component-major state lengths.
1151    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    /// Evaluate all configured FVM terms into the resident residual buffer.
1480    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}