Skip to main content

mesh_sieve/physics/
fe.rs

1//! Finite-element utilities for evaluation and assembly.
2
3use crate::data::closure::{
4    ClosureOrder, IdentitySectionSym, SectionSym, build_closure_index,
5    build_closure_index_unoriented, get_closure,
6};
7use crate::data::coordinates::Coordinates;
8use crate::data::discretization::DiscretizationMetadata;
9use crate::data::global_map::LocalToGlobalMap;
10use crate::data::section::Section;
11use crate::data::storage::Storage;
12use crate::discretization::runtime::{
13    Basis, BasisTabulation, QuadratureRule, ensure_geometry_order_supported, local_load_vector,
14    local_stiffness_matrix, runtime_from_metadata, tabulate_element,
15};
16use crate::mesh_error::MeshSieveError;
17use crate::topology::cell_type::CellType;
18use crate::topology::point::PointId;
19use crate::topology::sieve::{Orientation, OrientedSieve, Sieve};
20
21/// Basis and quadrature evaluation on the reference element.
22#[derive(Clone, Debug)]
23pub struct ReferenceElementEvaluation {
24    /// Basis implementation used for evaluation.
25    pub basis: Basis,
26    /// Quadrature rule on the reference element.
27    pub quadrature: QuadratureRule,
28    /// Basis tabulation (values and gradients) on the reference element.
29    pub tabulation: BasisTabulation,
30}
31
32/// Evaluate basis functions at quadrature points on the reference element.
33pub fn evaluate_reference_element(
34    cell_type: CellType,
35    metadata: &DiscretizationMetadata,
36) -> Result<ReferenceElementEvaluation, MeshSieveError> {
37    let runtime = runtime_from_metadata(metadata, cell_type)?;
38    let tabulation = runtime.basis.tabulate(&runtime.quadrature.points)?;
39    Ok(ReferenceElementEvaluation {
40        basis: runtime.basis,
41        quadrature: runtime.quadrature,
42        tabulation,
43    })
44}
45
46/// Integrate a scalar function on the reference element using quadrature.
47pub fn integrate_reference_scalar<F>(
48    evaluation: &ReferenceElementEvaluation,
49    f: F,
50) -> Result<f64, MeshSieveError>
51where
52    F: Fn(&[f64]) -> f64,
53{
54    if evaluation.quadrature.points.len() != evaluation.quadrature.weights.len() {
55        return Err(MeshSieveError::InvalidGeometry(
56            "quadrature points/weights length mismatch".to_string(),
57        ));
58    }
59    let mut total = 0.0;
60    for (point, weight) in evaluation
61        .quadrature
62        .points
63        .iter()
64        .zip(evaluation.quadrature.weights.iter())
65    {
66        total += weight * f(point);
67    }
68    Ok(total)
69}
70
71/// Local element matrices and vectors for a scalar Poisson-like operator.
72#[derive(Clone, Debug)]
73pub struct ElementMatrices {
74    /// Element stiffness matrix in row-major order.
75    pub stiffness: Vec<f64>,
76    /// Element load vector.
77    pub load: Vec<f64>,
78}
79
80/// Assemble element matrices from coordinates, metadata, and a source term.
81pub fn assemble_element_matrices<S, F>(
82    coordinates: &Coordinates<f64, S>,
83    cell_type: CellType,
84    cell_nodes: &[PointId],
85    metadata: &DiscretizationMetadata,
86    rhs: F,
87) -> Result<ElementMatrices, MeshSieveError>
88where
89    S: Storage<f64>,
90    F: Fn(&[f64]) -> f64,
91{
92    let node_coords = gather_node_coordinates(coordinates, cell_nodes)?;
93    let runtime = runtime_from_metadata(metadata, cell_type)?;
94    let tabulation = tabulate_element(&runtime, &node_coords)?;
95    Ok(ElementMatrices {
96        stiffness: local_stiffness_matrix(&tabulation),
97        load: local_load_vector(&tabulation, rhs),
98    })
99}
100
101fn gather_node_coordinates<S: Storage<f64>>(
102    coordinates: &Coordinates<f64, S>,
103    cell_nodes: &[PointId],
104) -> Result<Vec<Vec<f64>>, MeshSieveError> {
105    let mut node_coords = Vec::with_capacity(cell_nodes.len());
106    for node in cell_nodes {
107        let slice = coordinates.section().try_restrict(*node)?;
108        node_coords.push(slice.to_vec());
109    }
110    Ok(node_coords)
111}
112
113fn closure_vertex_coordinates<T, S>(
114    topology: &T,
115    coordinates: &Coordinates<f64, S>,
116    cell: PointId,
117    topology_version: u64,
118    order: &ClosureOrder,
119) -> Result<Vec<Vec<f64>>, MeshSieveError>
120where
121    T: Sieve<Point = PointId>,
122    S: Storage<f64>,
123{
124    let index = build_closure_index_unoriented(
125        topology,
126        coordinates.section(),
127        cell,
128        topology_version,
129        order,
130        &IdentitySectionSym,
131    )?;
132    let mut cell_nodes = Vec::new();
133    for point in index.point_order() {
134        if topology.cone_points(point).next().is_none() {
135            cell_nodes.push(point);
136        }
137    }
138    gather_node_coordinates(coordinates, &cell_nodes)
139}
140
141/// Assemble element matrices using vertices selected from a DMPlex-style closure.
142///
143/// This is the closure-aware counterpart to [`assemble_element_matrices`].  The
144/// closure order is explicit, so high-order and tensor-product callers can share
145/// the same predictable ordering used for solution-vector closure access.
146pub fn assemble_element_matrices_from_closure<T, S, F>(
147    topology: &T,
148    coordinates: &Coordinates<f64, S>,
149    cell_type: CellType,
150    cell: PointId,
151    topology_version: u64,
152    order: &ClosureOrder,
153    metadata: &DiscretizationMetadata,
154    rhs: F,
155) -> Result<ElementMatrices, MeshSieveError>
156where
157    T: Sieve<Point = PointId>,
158    S: Storage<f64>,
159    F: Fn(&[f64]) -> f64,
160{
161    let runtime = runtime_from_metadata(metadata, cell_type)?;
162    let node_coords = if let Some(high_order) = coordinates.high_order() {
163        if high_order.section().atlas().contains(cell) {
164            let values = high_order.section().try_restrict(cell)?;
165            let dim = high_order.dimension();
166            let geometry_nodes = values.len() / dim;
167            let geometry_order = metadata
168                .basis_order
169                .unwrap_or(runtime.basis.degree())
170                .max(1);
171            ensure_geometry_order_supported(cell_type, geometry_order)?;
172            if geometry_nodes == runtime.basis.num_nodes() {
173                values.chunks(dim).map(|tuple| tuple.to_vec()).collect()
174            } else {
175                return Err(MeshSieveError::InvalidGeometry(format!(
176                    "high-order coordinates for {cell:?} provide {geometry_nodes} nodes, but {:?} P{} requires {}",
177                    cell_type,
178                    runtime.basis.degree(),
179                    runtime.basis.num_nodes()
180                )));
181            }
182        } else {
183            closure_vertex_coordinates(topology, coordinates, cell, topology_version, order)?
184        }
185    } else {
186        closure_vertex_coordinates(topology, coordinates, cell, topology_version, order)?
187    };
188    let tabulation = tabulate_element(&runtime, &node_coords)?;
189    Ok(ElementMatrices {
190        stiffness: local_stiffness_matrix(&tabulation),
191        load: local_load_vector(&tabulation, rhs),
192    })
193}
194
195/// Orientation-aware FE closure data for element kernels.
196#[derive(Clone, Debug)]
197pub struct ElementClosureData<V> {
198    /// Cell whose closure was extracted.
199    pub cell: PointId,
200    /// Flattened closure values after applying point orientation symmetries.
201    pub values: Vec<V>,
202    /// Local point/slot to global-index map for each closure scalar DOF, when provided.
203    ///
204    /// Constrained/eliminated DOFs that have no owned global slot are recorded
205    /// as `u64::MAX`; constraint-aware insertion helpers distribute those
206    /// entries through their parent equations instead of using the sentinel.
207    pub global_indices: Option<Vec<u64>>,
208    /// Closure point and local-DOF order corresponding to `values`.
209    pub points: Vec<ElementClosurePoint>,
210}
211
212/// One point contribution in an element closure.
213#[derive(Clone, Debug, PartialEq, Eq)]
214pub struct ElementClosurePoint {
215    /// Point in the closure.
216    pub point: PointId,
217    /// Local DOF indices in oriented closure order.
218    pub local_dofs: Vec<usize>,
219}
220
221/// Extract closure values for a FE cell using DMPLEX-like ordering.
222pub fn extract_element_closure<T, V, Sct>(
223    topology: &T,
224    section: &Section<V, Sct>,
225    cell: PointId,
226    topology_version: u64,
227    order: &ClosureOrder,
228) -> Result<ElementClosureData<V>, MeshSieveError>
229where
230    T: Sieve<Point = PointId>,
231    V: Clone,
232    Sct: Storage<V>,
233{
234    let index = build_closure_index_unoriented(
235        topology,
236        section,
237        cell,
238        topology_version,
239        order,
240        &IdentitySectionSym,
241    )?;
242    let values = get_closure(section, &index)?;
243    let points = index
244        .points
245        .iter()
246        .map(|entry| ElementClosurePoint {
247            point: entry.point,
248            local_dofs: entry.permutation.clone(),
249        })
250        .collect();
251    Ok(ElementClosureData {
252        cell,
253        values,
254        global_indices: None,
255        points,
256    })
257}
258
259/// Extract oriented closure values and optional global indices for a FE cell.
260pub fn extract_oriented_element_closure<T, V, Sct, O, Sym>(
261    topology: &T,
262    section: &Section<V, Sct>,
263    global_map: Option<&LocalToGlobalMap>,
264    cell: PointId,
265    topology_version: u64,
266    order: &ClosureOrder,
267    sym: &Sym,
268) -> Result<ElementClosureData<V>, MeshSieveError>
269where
270    T: OrientedSieve<Point = PointId, Orient = O>,
271    V: Clone,
272    Sct: Storage<V>,
273    O: Orientation + Eq + std::hash::Hash,
274    Sym: SectionSym<O>,
275{
276    let index = build_closure_index(topology, section, cell, topology_version, order, sym)?;
277    let values = get_closure(section, &index)?;
278    let global_indices = if let Some(map) = global_map {
279        let mut indices = Vec::with_capacity(index.len);
280        for entry in &index.points {
281            for &local_dof in &entry.permutation {
282                match map.global_index(entry.point, local_dof) {
283                    Ok(global) => indices.push(global),
284                    Err(MeshSieveError::ConstraintIndexOutOfBounds { .. }) => {
285                        indices.push(u64::MAX)
286                    }
287                    Err(err) => return Err(err),
288                }
289            }
290        }
291        Some(indices)
292    } else {
293        None
294    };
295    let points = index
296        .points
297        .iter()
298        .map(|entry| ElementClosurePoint {
299            point: entry.point,
300            local_dofs: entry.permutation.clone(),
301        })
302        .collect();
303    Ok(ElementClosureData {
304        cell,
305        values,
306        global_indices,
307        points,
308    })
309}
310
311/// Scatter an element residual into a global vector while honoring hanging-node constraints.
312///
313/// Free closure DOFs are inserted at their own global index. Contributions for a
314/// hanging DOF are distributed to its parent DOFs using the constraint weights,
315/// matching the transpose action of linear constraint elimination.
316pub fn insert_element_residual_with_hanging_constraints<V>(
317    closure: &ElementClosureData<V>,
318    element_residual: &[V],
319    global_map: &LocalToGlobalMap,
320    constraints: &crate::data::hanging_node_constraints::HangingNodeConstraints<V>,
321    global_residual: &mut [V],
322) -> Result<(), MeshSieveError>
323where
324    V: Clone + Default + core::ops::AddAssign + core::ops::Mul<Output = V>,
325{
326    if closure.values.len() != element_residual.len() {
327        return Err(MeshSieveError::InvalidGeometry(format!(
328            "element residual length {} does not match closure length {} for {:?}",
329            element_residual.len(),
330            closure.values.len(),
331            closure.cell
332        )));
333    }
334
335    let Some(global_indices) = &closure.global_indices else {
336        return Err(MeshSieveError::InvalidGeometry(
337            "global indices are required for constrained residual insertion".to_string(),
338        ));
339    };
340    if global_indices.len() != element_residual.len() {
341        return Err(MeshSieveError::InvalidGeometry(format!(
342            "global-index length {} does not match residual length {} for {:?}",
343            global_indices.len(),
344            element_residual.len(),
345            closure.cell
346        )));
347    }
348
349    let mut cursor = 0usize;
350    for entry in &closure.points {
351        for &local_dof in &entry.local_dofs {
352            let value = element_residual[cursor].clone();
353            if let Some(point_constraints) = constraints.constraints_for(entry.point)
354                && let Some(constraint) = point_constraints.iter().find(|c| c.index == local_dof)
355            {
356                for term in &constraint.terms {
357                    let global = global_map.global_index(term.point, term.index)? as usize;
358                    let len = global_residual.len();
359                    let slot = global_residual.get_mut(global).ok_or_else(|| {
360                        MeshSieveError::InvalidGeometry(format!(
361                            "global residual index {global} out of bounds (len={len})"
362                        ))
363                    })?;
364                    *slot += value.clone() * term.weight.clone();
365                }
366            } else {
367                let global = global_indices[cursor] as usize;
368                let len = global_residual.len();
369                let slot = global_residual.get_mut(global).ok_or_else(|| {
370                    MeshSieveError::InvalidGeometry(format!(
371                        "global residual index {global} out of bounds (len={len})"
372                    ))
373                })?;
374                *slot += value;
375            }
376            cursor += 1;
377        }
378    }
379    Ok(())
380}