Skip to main content

mesh_sieve/discretization/
runtime.rs

1//! Runtime basis/quadrature lookup and element assembly utilities.
2
3use crate::data::closure::{
4    ClosureIndex, ClosureOrder, IdentitySectionSym, build_closure_index_unoriented,
5};
6use crate::data::coordinates::Coordinates;
7use crate::data::discretization::DiscretizationMetadata;
8use crate::data::section::Section;
9use crate::data::storage::Storage;
10use crate::mesh_error::MeshSieveError;
11use crate::topology::cell_type::CellType;
12use crate::topology::point::PointId;
13use crate::topology::sieve::Sieve;
14use std::collections::{BTreeSet, HashMap};
15
16/// Supported finite-element basis implementations.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum Basis {
19    /// Linear Lagrange basis on a segment (legacy spelling).
20    LagrangeP1Segment,
21    /// Bilinear Lagrange basis on a quadrilateral (legacy spelling).
22    LagrangeQ1Quadrilateral,
23    /// Runtime Lagrange basis on a supported reference cell.
24    Lagrange {
25        /// Reference cell shape.
26        cell_type: CellType,
27        /// Polynomial/interpolation degree.
28        degree: usize,
29        /// Shape family used to place nodes and choose interpolation monomials.
30        family: BasisFamily,
31    },
32}
33
34/// Lagrange basis family, mirroring PetscFE simplex/tensor-product setup choices.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum BasisFamily {
37    /// Simplex-complete polynomial basis on segment/triangle/tetrahedron.
38    Simplex,
39    /// Tensor-product basis on segment/quadrilateral/hexahedron.
40    TensorProduct,
41    /// Tensor product of triangle and segment bases for prisms.
42    Prism,
43    /// Layered tensor-product basis for pyramids.
44    Pyramid,
45}
46
47/// Quadrature families accepted by the runtime capability matrix.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum QuadratureFamily {
50    /// Single point/centroid rules.
51    Point,
52    /// Gauss-Legendre rules on tensor-product reference cells.
53    GaussLegendre,
54    /// Simplex rules on triangle/tetrahedron/simplex reference cells.
55    Simplex,
56    /// Tensor product of a simplex rule and a segment rule.
57    PrismProduct,
58    /// Pyramid reference rule.
59    Pyramid,
60    /// Polygon centroid/fan rule.
61    PolygonCentroid,
62    /// Generic polyhedron centroid rule.
63    PolyhedronCentroid,
64    /// Caller-provided points and weights.
65    Explicit,
66}
67
68/// One row in the supported PetscFE/PetscFV-equivalent discretization matrix.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct DiscretizationCapability {
71    /// Canonical cell topology.
72    pub cell_type: CellType,
73    /// Topological dimension.
74    pub dimension: usize,
75    /// Lagrange node/basis families supported for this topology.
76    pub basis_families: &'static [BasisFamily],
77    /// Inclusive polynomial degree range supported by the runtime tabulator.
78    pub degree_range: (usize, usize),
79    /// Quadrature families supported for this topology.
80    pub quadrature_families: &'static [QuadratureFamily],
81    /// Inclusive coordinate geometry order range supported by element assembly.
82    pub geometry_order_range: (usize, usize),
83    /// Whether cell-centered FV stencils/geometry are supported.
84    pub finite_volume: bool,
85}
86
87impl DiscretizationCapability {
88    const fn new(
89        cell_type: CellType,
90        dimension: usize,
91        basis_families: &'static [BasisFamily],
92        degree_range: (usize, usize),
93        quadrature_families: &'static [QuadratureFamily],
94        geometry_order_range: (usize, usize),
95        finite_volume: bool,
96    ) -> Self {
97        Self {
98            cell_type,
99            dimension,
100            basis_families,
101            degree_range,
102            quadrature_families,
103            geometry_order_range,
104            finite_volume,
105        }
106    }
107
108    fn supports_basis(self, family: BasisFamily, degree: usize) -> bool {
109        self.basis_families.contains(&family)
110            && (self.degree_range.0..=self.degree_range.1).contains(&degree)
111    }
112
113    fn supports_quadrature(self, family: QuadratureFamily) -> bool {
114        self.quadrature_families.contains(&family)
115    }
116
117    fn supports_geometry_order(self, order: usize) -> bool {
118        (self.geometry_order_range.0..=self.geometry_order_range.1).contains(&order)
119    }
120}
121
122impl Basis {
123    /// Resolve a basis implementation from a metadata label and cell type.
124    pub fn from_metadata(name: &str, cell_type: CellType) -> Result<Self, MeshSieveError> {
125        let normalized = normalize_name(name);
126        let explicit_degree = parse_trailing_degree(&normalized);
127        let degree = explicit_degree.unwrap_or(1);
128        let is_lagrange = normalized.contains("lagrange")
129            || normalized == "p"
130            || normalized == "q"
131            || normalized.starts_with('p')
132            || normalized.starts_with('q')
133            || normalized == "linear"
134            || normalized == "bilinear"
135            || normalized == "trilinear";
136        if !is_lagrange {
137            return Err(MeshSieveError::InvalidGeometry(format!(
138                "unsupported basis '{name}' for {cell_type:?}"
139            )));
140        }
141        if matches!(cell_type, CellType::Segment) && degree == 1 {
142            return Ok(Self::LagrangeP1Segment);
143        }
144        if matches!(cell_type, CellType::Quadrilateral) && degree == 1 {
145            return Ok(Self::LagrangeQ1Quadrilateral);
146        }
147        Self::lagrange(cell_type, degree)
148    }
149
150    /// Build a Lagrange basis with a degree inferred from metadata.
151    pub fn from_metadata_with_order(
152        metadata: &DiscretizationMetadata,
153        cell_type: CellType,
154    ) -> Result<Self, MeshSieveError> {
155        let mut basis = Self::from_metadata(&metadata.basis, cell_type)?;
156        if let Some(order) = metadata.basis_order {
157            basis = Self::lagrange(cell_type, order)?;
158        }
159        Ok(basis)
160    }
161
162    /// Construct a configurable Lagrange basis for PetscFE-like setup paths.
163    pub fn lagrange(cell_type: CellType, degree: usize) -> Result<Self, MeshSieveError> {
164        let family = default_lagrange_family(cell_type)?;
165        Self::lagrange_with_family(cell_type, degree, family)
166    }
167
168    /// Construct a Lagrange basis with an explicit node-placement family.
169    pub fn lagrange_with_family(
170        cell_type: CellType,
171        degree: usize,
172        family: BasisFamily,
173    ) -> Result<Self, MeshSieveError> {
174        ensure_lagrange_supported(cell_type, degree, family)?;
175        Ok(Self::Lagrange {
176            cell_type,
177            degree,
178            family,
179        })
180    }
181
182    /// Reference dimension of the basis.
183    pub fn dimension(&self) -> usize {
184        match self {
185            Basis::LagrangeP1Segment => 1,
186            Basis::LagrangeQ1Quadrilateral => 2,
187            Basis::Lagrange { cell_type, .. } => cell_type.dimension() as usize,
188        }
189    }
190
191    /// Polynomial/interpolation degree.
192    pub fn degree(&self) -> usize {
193        match self {
194            Basis::LagrangeP1Segment | Basis::LagrangeQ1Quadrilateral => 1,
195            Basis::Lagrange { degree, .. } => *degree,
196        }
197    }
198
199    /// Number of basis functions per element.
200    pub fn num_nodes(&self) -> usize {
201        match self {
202            Basis::LagrangeP1Segment => 2,
203            Basis::LagrangeQ1Quadrilateral => 4,
204            Basis::Lagrange {
205                cell_type,
206                degree,
207                family,
208            } => reference_nodes(*cell_type, *degree, *family)
209                .map(|n| n.len())
210                .unwrap_or(0),
211        }
212    }
213
214    /// The cell type supported by this basis.
215    pub fn cell_type(&self) -> CellType {
216        match self {
217            Basis::LagrangeP1Segment => CellType::Segment,
218            Basis::LagrangeQ1Quadrilateral => CellType::Quadrilateral,
219            Basis::Lagrange { cell_type, .. } => *cell_type,
220        }
221    }
222
223    /// Evaluate basis values and gradients at reference points.
224    pub fn tabulate(&self, points: &[Vec<f64>]) -> Result<BasisTabulation, MeshSieveError> {
225        match self {
226            Basis::LagrangeP1Segment => tabulate_p1_segment(points),
227            Basis::LagrangeQ1Quadrilateral => tabulate_q1_quad(points),
228            Basis::Lagrange {
229                cell_type,
230                degree,
231                family,
232            } => tabulate_lagrange(*cell_type, *degree, *family, points),
233        }
234    }
235}
236
237/// Basis function tabulation on the reference element.
238#[derive(Clone, Debug)]
239pub struct BasisTabulation {
240    /// Basis values per quadrature point: `[qp][basis]`.
241    pub values: Vec<Vec<f64>>,
242    /// Reference gradients per quadrature point: `[qp][basis][dim]`.
243    pub gradients: Vec<Vec<Vec<f64>>>,
244}
245
246/// Quadrature rule on the reference element.
247#[derive(Clone, Debug)]
248pub struct QuadratureRule {
249    /// Name for diagnostics.
250    pub name: String,
251    /// Quadrature points in reference coordinates.
252    pub points: Vec<Vec<f64>>,
253    /// Quadrature weights.
254    pub weights: Vec<f64>,
255}
256
257impl QuadratureRule {
258    /// Construct a quadrature rule from a metadata label and cell type.
259    pub fn from_metadata(name: &str, cell_type: CellType) -> Result<Self, MeshSieveError> {
260        let normalized = normalize_name(name);
261        let family = quadrature_family_from_name(&normalized, cell_type)?;
262        ensure_quadrature_supported(cell_type, family)?;
263        let order = parse_trailing_degree(&normalized).unwrap_or(match normalized.as_str() {
264            "midpoint" | "centroid" => 1,
265            _ => 2,
266        });
267        if order > MAX_RUNTIME_ORDER {
268            return Err(MeshSieveError::InvalidGeometry(format!(
269                "unsupported quadrature order {order} for {cell_type:?}; maximum supported order is {MAX_RUNTIME_ORDER}"
270            )));
271        }
272        match canonical_cell_type(cell_type) {
273            CellType::Vertex => Ok(point_quadrature(name)),
274            CellType::Segment => Ok(gauss_legendre_1d(order.min(3), name)),
275            CellType::Quadrilateral => Ok(tensor_power_quadrature(2, order.min(3), name)),
276            CellType::Hexahedron => Ok(tensor_power_quadrature(3, order.min(3), name)),
277            CellType::Triangle => simplex_quadrature(2, order, name),
278            CellType::Tetrahedron => simplex_quadrature(3, order, name),
279            CellType::Prism => prism_quadrature(order, name),
280            CellType::Pyramid => pyramid_quadrature(order, name),
281            CellType::Polygon(n) => polygon_quadrature(n as usize, name),
282            CellType::Polyhedron => Ok(QuadratureRule {
283                name: name.to_string(),
284                points: vec![vec![0.0, 0.0, 0.0]],
285                weights: vec![1.0],
286            }),
287            CellType::Simplex(dim) => simplex_quadrature(dim as usize, order, name),
288        }
289    }
290
291    /// Construct a quadrature rule from explicit points and weights.
292    pub fn from_explicit(
293        name: impl Into<String>,
294        points: Vec<Vec<f64>>,
295        weights: Vec<f64>,
296    ) -> Result<Self, MeshSieveError> {
297        if points.len() != weights.len() {
298            return Err(MeshSieveError::InvalidGeometry(
299                "quadrature points/weights length mismatch".to_string(),
300            ));
301        }
302        Ok(Self {
303            name: name.into(),
304            points,
305            weights,
306        })
307    }
308
309    /// Dimension of the quadrature points.
310    pub fn dimension(&self) -> usize {
311        self.points.first().map(|p| p.len()).unwrap_or(0)
312    }
313}
314
315/// Combined runtime basis/quadrature for an element.
316#[derive(Clone, Debug)]
317pub struct ElementRuntime {
318    pub basis: Basis,
319    pub quadrature: QuadratureRule,
320}
321
322/// Resolve runtime basis/quadrature from metadata.
323pub fn runtime_from_metadata(
324    metadata: &DiscretizationMetadata,
325    cell_type: CellType,
326) -> Result<ElementRuntime, MeshSieveError> {
327    let basis = Basis::from_metadata_with_order(metadata, cell_type)?;
328    let quadrature = if metadata.has_quadrature_data() {
329        ensure_quadrature_supported(cell_type, QuadratureFamily::Explicit)?;
330        QuadratureRule::from_explicit(
331            metadata.quadrature.clone(),
332            metadata.quadrature_points.clone(),
333            metadata.quadrature_weights.clone(),
334        )?
335    } else {
336        QuadratureRule::from_metadata(&metadata.quadrature, cell_type)?
337    };
338    if quadrature.dimension() != basis.dimension() {
339        return Err(MeshSieveError::InvalidGeometry(format!(
340            "quadrature dimension {} does not match basis dimension {} for {cell_type:?}",
341            quadrature.dimension(),
342            basis.dimension()
343        )));
344    }
345    Ok(ElementRuntime { basis, quadrature })
346}
347
348/// Tabulation data on a physical element.
349#[derive(Clone, Debug)]
350pub struct ElementTabulation {
351    /// Quadrature points in reference space.
352    pub reference_points: Vec<Vec<f64>>,
353    /// Quadrature weights (reference).
354    pub weights: Vec<f64>,
355    /// Quadrature points in physical space.
356    pub physical_points: Vec<Vec<f64>>,
357    /// Basis values per quadrature point.
358    pub basis_values: Vec<Vec<f64>>,
359    /// Basis gradients per quadrature point in physical coordinates.
360    pub basis_gradients: Vec<Vec<Vec<f64>>>,
361    /// Jacobian matrix per quadrature point, row-major.
362    pub jacobians: Vec<Vec<f64>>,
363    /// Absolute value of the Jacobian determinant per quadrature point.
364    pub jacobian_dets: Vec<f64>,
365}
366
367/// Build element tabulation including geometric terms.
368pub fn tabulate_element(
369    runtime: &ElementRuntime,
370    node_coords: &[Vec<f64>],
371) -> Result<ElementTabulation, MeshSieveError> {
372    let basis = &runtime.basis;
373    let num_nodes = basis.num_nodes();
374    if node_coords.len() != num_nodes {
375        return Err(MeshSieveError::InvalidGeometry(format!(
376            "expected {num_nodes} node coordinates, found {}",
377            node_coords.len()
378        )));
379    }
380    let dim = basis.dimension();
381    let embed_dim = node_coords
382        .first()
383        .map(|coords| coords.len())
384        .ok_or_else(|| MeshSieveError::InvalidGeometry("missing node coordinates".to_string()))?;
385    if dim != embed_dim {
386        return Err(MeshSieveError::InvalidGeometry(format!(
387            "basis dimension {dim} does not match coordinate dimension {embed_dim}"
388        )));
389    }
390    for coords in node_coords {
391        if coords.len() != embed_dim {
392            return Err(MeshSieveError::InvalidGeometry(
393                "inconsistent coordinate dimension".to_string(),
394            ));
395        }
396    }
397
398    let quad = &runtime.quadrature;
399    if quad.dimension() != dim {
400        return Err(MeshSieveError::InvalidGeometry(format!(
401            "quadrature dimension {} does not match basis dimension {dim}",
402            quad.dimension()
403        )));
404    }
405    let basis_tab = basis.tabulate(&quad.points)?;
406    let mut jacobians = Vec::with_capacity(quad.points.len());
407    let mut jacobian_dets = Vec::with_capacity(quad.points.len());
408    let mut basis_gradients = Vec::with_capacity(quad.points.len());
409    let mut physical_points = Vec::with_capacity(quad.points.len());
410
411    for (qp, ref_grads) in basis_tab.gradients.iter().enumerate() {
412        let jac = build_jacobian(dim, node_coords, ref_grads)?;
413        let (det, inv) = invert_jacobian(dim, &jac)?;
414        jacobian_dets.push(det.abs());
415        jacobians.push(jac.clone());
416
417        let mut grad_qp = Vec::with_capacity(num_nodes);
418        for ref_grad in ref_grads {
419            let mut phys_grad = vec![0.0; dim];
420            for phys_dim in 0..dim {
421                let mut value = 0.0;
422                for ref_dim in 0..dim {
423                    value += inv[ref_dim * dim + phys_dim] * ref_grad[ref_dim];
424                }
425                phys_grad[phys_dim] = value;
426            }
427            grad_qp.push(phys_grad);
428        }
429        basis_gradients.push(grad_qp);
430
431        let mut x = vec![0.0; dim];
432        for (node, value) in node_coords.iter().zip(basis_tab.values[qp].iter()) {
433            for d in 0..dim {
434                x[d] += value * node[d];
435            }
436        }
437        physical_points.push(x);
438    }
439
440    Ok(ElementTabulation {
441        reference_points: quad.points.clone(),
442        weights: quad.weights.clone(),
443        physical_points,
444        basis_values: basis_tab.values,
445        basis_gradients,
446        jacobians,
447        jacobian_dets,
448    })
449}
450
451/// Compute a local Poisson stiffness matrix from tabulation data.
452pub fn local_stiffness_matrix(tabulation: &ElementTabulation) -> Vec<f64> {
453    let num_nodes = tabulation
454        .basis_values
455        .first()
456        .map(|v| v.len())
457        .unwrap_or(0);
458    let mut matrix = vec![0.0; num_nodes * num_nodes];
459    for (qp, grads) in tabulation.basis_gradients.iter().enumerate() {
460        let weight = tabulation.weights[qp] * tabulation.jacobian_dets[qp];
461        for i in 0..num_nodes {
462            for j in 0..num_nodes {
463                let dot = grads[i]
464                    .iter()
465                    .zip(grads[j].iter())
466                    .map(|(a, b)| a * b)
467                    .sum::<f64>();
468                matrix[i * num_nodes + j] += weight * dot;
469            }
470        }
471    }
472    matrix
473}
474
475/// Compute a local load vector for a source term `rhs`.
476pub fn local_load_vector<F>(tabulation: &ElementTabulation, rhs: F) -> Vec<f64>
477where
478    F: Fn(&[f64]) -> f64,
479{
480    let num_nodes = tabulation
481        .basis_values
482        .first()
483        .map(|v| v.len())
484        .unwrap_or(0);
485    let mut vector = vec![0.0; num_nodes];
486    for (qp, values) in tabulation.basis_values.iter().enumerate() {
487        let weight = tabulation.weights[qp] * tabulation.jacobian_dets[qp];
488        let f_val = rhs(&tabulation.physical_points[qp]);
489        for i in 0..num_nodes {
490            vector[i] += weight * f_val * values[i];
491        }
492    }
493    vector
494}
495
496/// Finite-volume discretization metadata resembling PetscFV setup state.
497#[derive(Clone, Debug, PartialEq)]
498pub struct FiniteVolumeMetadata {
499    /// Number of conserved/scalar components per cell.
500    pub components: usize,
501    /// Reconstruction order for face states.
502    pub reconstruction_order: usize,
503    /// Optional limiter name used by callers.
504    pub limiter: Option<String>,
505}
506
507impl FiniteVolumeMetadata {
508    /// Construct cell-centered FV metadata.
509    pub fn new(components: usize) -> Self {
510        Self {
511            components,
512            reconstruction_order: 1,
513            limiter: None,
514        }
515    }
516
517    /// Set reconstruction order.
518    pub fn with_reconstruction_order(mut self, order: usize) -> Self {
519        self.reconstruction_order = order;
520        self
521    }
522
523    /// Set limiter label.
524    pub fn with_limiter(mut self, limiter: impl Into<String>) -> Self {
525        self.limiter = Some(limiter.into());
526        self
527    }
528}
529
530/// Cell geometry used by finite-volume residuals.
531#[derive(Clone, Debug, PartialEq)]
532pub struct CellGeometry {
533    /// Cell centroid in physical coordinates.
534    pub centroid: Vec<f64>,
535    /// Cell measure (length/area/volume).
536    pub volume: f64,
537}
538
539/// Face geometry used by finite-volume flux residuals.
540#[derive(Clone, Debug, PartialEq)]
541pub struct FaceGeometry {
542    /// Face identifier.
543    pub face: PointId,
544    /// Face centroid.
545    pub centroid: Vec<f64>,
546    /// Outward normal scaled by face measure for the owner/left cell.
547    pub normal: Vec<f64>,
548    /// Face measure.
549    pub area: f64,
550    /// Adjacent cells in deterministic support order.
551    pub neighbors: Vec<PointId>,
552}
553
554/// Cell-centered finite-volume stencil for one face.
555#[derive(Clone, Debug, PartialEq, Eq)]
556pub struct FluxStencil {
557    /// Face connecting the stencil cells.
558    pub face: PointId,
559    /// Left/owner cell.
560    pub left: PointId,
561    /// Optional right neighbor; `None` means boundary face.
562    pub right: Option<PointId>,
563}
564
565/// Compute an affine cell geometry from vertices.
566pub fn cell_geometry_from_vertices(vertices: &[Vec<f64>]) -> Result<CellGeometry, MeshSieveError> {
567    if vertices.is_empty() {
568        return Err(MeshSieveError::InvalidGeometry(
569            "cell geometry requires vertices".to_string(),
570        ));
571    }
572    let dim = vertices[0].len();
573    let mut centroid = vec![0.0; dim];
574    for vertex in vertices {
575        if vertex.len() != dim {
576            return Err(MeshSieveError::InvalidGeometry(
577                "inconsistent vertex dimension".to_string(),
578            ));
579        }
580        for d in 0..dim {
581            centroid[d] += vertex[d];
582        }
583    }
584    for value in &mut centroid {
585        *value /= vertices.len() as f64;
586    }
587    let volume = match dim {
588        1 => (vertices
589            .iter()
590            .map(|v| v[0])
591            .fold(f64::NEG_INFINITY, f64::max)
592            - vertices.iter().map(|v| v[0]).fold(f64::INFINITY, f64::min))
593        .abs(),
594        2 => polygon_area(vertices),
595        3 => bounding_box_volume(vertices),
596        _ => 0.0,
597    };
598    Ok(CellGeometry { centroid, volume })
599}
600
601/// Compute face geometry from face vertices and support cells.
602pub fn face_geometry_from_vertices(
603    face: PointId,
604    vertices: &[Vec<f64>],
605    neighbors: Vec<PointId>,
606) -> Result<FaceGeometry, MeshSieveError> {
607    let cell = cell_geometry_from_vertices(vertices)?;
608    let dim = cell.centroid.len();
609    let (normal, area) = match dim {
610        1 => (vec![1.0], 1.0),
611        2 => {
612            if vertices.len() < 2 {
613                return Err(MeshSieveError::InvalidGeometry(
614                    "2D face requires two vertices".to_string(),
615                ));
616            }
617            let dx = vertices[1][0] - vertices[0][0];
618            let dy = vertices[1][1] - vertices[0][1];
619            let length = (dx * dx + dy * dy).sqrt();
620            (vec![dy, -dx], length)
621        }
622        3 => {
623            if vertices.len() < 3 {
624                return Err(MeshSieveError::InvalidGeometry(
625                    "3D face requires at least three vertices".to_string(),
626                ));
627            }
628            let a = sub(&vertices[1], &vertices[0]);
629            let b = sub(&vertices[2], &vertices[0]);
630            let cross = vec![
631                a[1] * b[2] - a[2] * b[1],
632                a[2] * b[0] - a[0] * b[2],
633                a[0] * b[1] - a[1] * b[0],
634            ];
635            let area = norm(&cross) * 0.5;
636            (cross, area)
637        }
638        _ => (vec![], 0.0),
639    };
640    Ok(FaceGeometry {
641        face,
642        centroid: cell.centroid,
643        normal,
644        area,
645        neighbors,
646    })
647}
648
649/// Build neighbor flux stencils from face supports.
650pub fn flux_stencils<T>(topology: &T, faces: impl IntoIterator<Item = PointId>) -> Vec<FluxStencil>
651where
652    T: Sieve<Point = PointId>,
653{
654    let mut stencils = Vec::new();
655    for face in faces {
656        let mut cells: Vec<_> = topology.support_points(face).collect();
657        cells.sort_unstable();
658        if let Some(&left) = cells.first() {
659            stencils.push(FluxStencil {
660                face,
661                left,
662                right: cells.get(1).copied(),
663            });
664        }
665    }
666    stencils
667}
668
669/// One scalar degree of freedom in an element closure.
670#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
671pub struct ClosureDof {
672    /// Mesh point that owns this scalar DOF.
673    pub point: PointId,
674    /// Section-local DOF slot on `point`.
675    pub local_dof: usize,
676}
677
678/// Mapping from closure-local scalar DOFs to contiguous assembly indices.
679#[derive(Clone, Debug)]
680pub struct DofMap {
681    dofs: Vec<PointId>,
682    closure_dofs: Vec<ClosureDof>,
683    indices: HashMap<ClosureDof, usize>,
684    first_point_indices: HashMap<PointId, usize>,
685}
686
687impl DofMap {
688    /// Build a scalar DOF map from an ordered list of points.
689    pub fn new(dofs: Vec<PointId>) -> Self {
690        let closure_dofs: Vec<_> = dofs
691            .iter()
692            .map(|&point| ClosureDof {
693                point,
694                local_dof: 0,
695            })
696            .collect();
697        Self::from_closure_dofs(closure_dofs)
698    }
699
700    /// Build a DOF map from orientation-correct closure DOF slots.
701    pub fn from_closure_dofs(closure_dofs: Vec<ClosureDof>) -> Self {
702        let dofs = closure_dofs.iter().map(|dof| dof.point).collect();
703        let mut indices = HashMap::with_capacity(closure_dofs.len());
704        let mut first_point_indices = HashMap::new();
705        for (idx, &dof) in closure_dofs.iter().enumerate() {
706            indices.insert(dof, idx);
707            first_point_indices.entry(dof.point).or_insert(idx);
708        }
709        Self {
710            dofs,
711            closure_dofs,
712            indices,
713            first_point_indices,
714        }
715    }
716
717    /// Return the ordered DOF owner points.
718    pub fn dofs(&self) -> &[PointId] {
719        &self.dofs
720    }
721
722    /// Return the ordered point/local-slot DOFs consumed by closure kernels.
723    pub fn closure_dofs(&self) -> &[ClosureDof] {
724        &self.closure_dofs
725    }
726
727    /// Total number of scalar DOFs.
728    pub fn len(&self) -> usize {
729        self.closure_dofs.len()
730    }
731
732    /// Returns true when this map has no DOFs.
733    pub fn is_empty(&self) -> bool {
734        self.closure_dofs.is_empty()
735    }
736
737    /// Lookup the first index for a point.
738    pub fn index(&self, point: PointId) -> Option<usize> {
739        self.first_point_indices.get(&point).copied()
740    }
741
742    /// Lookup the assembly index for a point-local DOF slot.
743    pub fn slot_index(&self, point: PointId, local_dof: usize) -> Option<usize> {
744        self.indices.get(&ClosureDof { point, local_dof }).copied()
745    }
746}
747
748/// Build an assembly DOF map from a cached/un-cached DMPlex-style closure order.
749pub fn closure_dof_map<T, V, Sct>(
750    topology: &T,
751    section: &Section<V, Sct>,
752    cell: PointId,
753    topology_version: u64,
754    order: &ClosureOrder,
755) -> Result<DofMap, MeshSieveError>
756where
757    T: Sieve<Point = PointId>,
758    Sct: Storage<V>,
759{
760    let index = build_closure_index_unoriented(
761        topology,
762        section,
763        cell,
764        topology_version,
765        order,
766        &IdentitySectionSym,
767    )?;
768    Ok(dof_map_from_closure_index(&index))
769}
770
771/// Convert a closure index to the point-level DOF map used by scalar assembly.
772pub fn dof_map_from_closure_index<O>(index: &ClosureIndex<O>) -> DofMap {
773    let mut dofs = Vec::with_capacity(index.len);
774    for entry in &index.points {
775        for &local_dof in &entry.permutation {
776            dofs.push(ClosureDof {
777                point: entry.point,
778                local_dof,
779            });
780        }
781    }
782    DofMap::from_closure_dofs(dofs)
783}
784
785/// Add a local vector contribution into a global section.
786pub fn assemble_local_vector<S: Storage<f64>>(
787    section: &mut Section<f64, S>,
788    dofs: &[PointId],
789    local: &[f64],
790) -> Result<(), MeshSieveError> {
791    if dofs.len() != local.len() {
792        return Err(MeshSieveError::InvalidGeometry(
793            "local vector length mismatch".to_string(),
794        ));
795    }
796    for (point, value) in dofs.iter().zip(local.iter()) {
797        let slice = section.try_restrict_mut(*point)?;
798        if slice.len() != 1 {
799            return Err(MeshSieveError::InvalidGeometry(format!(
800                "expected scalar slice for {point:?}, found {}",
801                slice.len()
802            )));
803        }
804        slice[0] += value;
805    }
806    Ok(())
807}
808
809/// Add a local vector contribution into a section using point/local-slot closure DOFs.
810pub fn assemble_local_vector_closure<S: Storage<f64>>(
811    section: &mut Section<f64, S>,
812    dofs: &[ClosureDof],
813    local: &[f64],
814) -> Result<(), MeshSieveError> {
815    if dofs.len() != local.len() {
816        return Err(MeshSieveError::InvalidGeometry(
817            "local vector length mismatch".to_string(),
818        ));
819    }
820    for (dof, value) in dofs.iter().zip(local.iter()) {
821        let slice = section.try_restrict_mut(dof.point)?;
822        if dof.local_dof >= slice.len() {
823            return Err(MeshSieveError::InvalidGeometry(format!(
824                "missing local DOF {} for {:?}",
825                dof.local_dof, dof.point
826            )));
827        }
828        slice[dof.local_dof] += value;
829    }
830    Ok(())
831}
832
833/// Add a local matrix contribution into a global dense-matrix section.
834pub fn assemble_local_matrix<S: Storage<f64>>(
835    section: &mut Section<f64, S>,
836    dofs: &[PointId],
837    dof_map: &DofMap,
838    local: &[f64],
839) -> Result<(), MeshSieveError> {
840    let num_nodes = dofs.len();
841    if local.len() != num_nodes * num_nodes {
842        return Err(MeshSieveError::InvalidGeometry(
843            "local matrix length mismatch".to_string(),
844        ));
845    }
846    for (row, point) in dofs.iter().enumerate() {
847        let row_slice = section.try_restrict_mut(*point)?;
848        if row_slice.len() != dof_map.len() {
849            return Err(MeshSieveError::InvalidGeometry(format!(
850                "expected row length {}, found {} for {point:?}",
851                dof_map.len(),
852                row_slice.len()
853            )));
854        }
855        for (col, col_point) in dofs.iter().enumerate() {
856            let col_idx = dof_map.index(*col_point).ok_or_else(|| {
857                MeshSieveError::InvalidGeometry(format!("missing DOF index for {col_point:?}"))
858            })?;
859            row_slice[col_idx] += local[row * num_nodes + col];
860        }
861    }
862    Ok(())
863}
864
865/// Retrieve the vertices that define a cell, in oriented cone/closure order.
866///
867/// This helper deliberately does not sort by `PointId`: identifiers are not
868/// geometric ordering keys.  Geometry-sensitive callers should use the
869/// topology-backed canonical ordering API with coordinates.
870pub fn cell_vertices<S: Sieve<Point = PointId>>(
871    sieve: &S,
872    cell: PointId,
873    expected: usize,
874) -> Result<Vec<PointId>, MeshSieveError> {
875    let cone: Vec<PointId> = sieve.cone_points(cell).collect();
876    if cone.len() == expected && cone.iter().all(|p| sieve.cone_points(*p).next().is_none()) {
877        return Ok(cone);
878    }
879
880    let mut vertices = Vec::new();
881    for point in sieve.closure(std::iter::once(cell)) {
882        if sieve.cone_points(point).next().is_none() {
883            vertices.push(point);
884        }
885    }
886    let mut unique = std::collections::HashSet::new();
887    vertices.retain(|point| unique.insert(*point));
888    if vertices.len() != expected {
889        return Err(MeshSieveError::InvalidGeometry(format!(
890            "expected {expected} vertices for cell {cell:?}, found {}",
891            vertices.len()
892        )));
893    }
894    Ok(vertices)
895}
896
897/// Gather coordinate vectors for points.
898pub fn gather_point_coordinates<S: Storage<f64>>(
899    coordinates: &Coordinates<f64, S>,
900    points: &[PointId],
901) -> Result<Vec<Vec<f64>>, MeshSieveError> {
902    let mut coords = Vec::with_capacity(points.len());
903    for point in points {
904        coords.push(coordinates.section().try_restrict(*point)?.to_vec());
905    }
906    Ok(coords)
907}
908
909fn normalize_name(name: &str) -> String {
910    name.to_ascii_lowercase().replace(['-', ' '], "_")
911}
912
913fn parse_trailing_degree(name: &str) -> Option<usize> {
914    let digits: String = name
915        .chars()
916        .rev()
917        .take_while(|c| c.is_ascii_digit())
918        .collect::<String>()
919        .chars()
920        .rev()
921        .collect();
922    (!digits.is_empty()).then(|| digits.parse().ok()).flatten()
923}
924
925/// Runtime finite-element/FV support policy.
926///
927/// This table is the single source of truth for combinations implemented by
928/// this module. Unsupported entries are rejected as policy errors before node,
929/// quadrature, Jacobian, or stencil code is reached. The degree and geometry
930/// order limits are intentionally finite because the current runtime tabulator
931/// builds dense interpolation matrices and the built-in quadrature rules are
932/// only intended for common low-order PetscFE/PetscFV setup paths.
933const MAX_RUNTIME_ORDER: usize = 4;
934
935const SIMPLEX_FAMILIES: &[BasisFamily] = &[BasisFamily::Simplex];
936const SEGMENT_FAMILIES: &[BasisFamily] = &[BasisFamily::Simplex, BasisFamily::TensorProduct];
937const TENSOR_FAMILIES: &[BasisFamily] = &[BasisFamily::TensorProduct];
938const PRISM_FAMILIES: &[BasisFamily] = &[BasisFamily::Prism];
939const PYRAMID_FAMILIES: &[BasisFamily] = &[BasisFamily::Pyramid];
940const POINT_FAMILIES: &[BasisFamily] = &[BasisFamily::Simplex, BasisFamily::TensorProduct];
941
942const POINT_QUADRATURE: &[QuadratureFamily] =
943    &[QuadratureFamily::Point, QuadratureFamily::Explicit];
944const GAUSS_QUADRATURE: &[QuadratureFamily] =
945    &[QuadratureFamily::GaussLegendre, QuadratureFamily::Explicit];
946const SIMPLEX_QUADRATURE: &[QuadratureFamily] =
947    &[QuadratureFamily::Simplex, QuadratureFamily::Explicit];
948const PRISM_QUADRATURE: &[QuadratureFamily] =
949    &[QuadratureFamily::PrismProduct, QuadratureFamily::Explicit];
950const PYRAMID_QUADRATURE: &[QuadratureFamily] =
951    &[QuadratureFamily::Pyramid, QuadratureFamily::Explicit];
952
953const RUNTIME_CAPABILITIES: &[DiscretizationCapability] = &[
954    DiscretizationCapability::new(
955        CellType::Vertex,
956        0,
957        POINT_FAMILIES,
958        (0, MAX_RUNTIME_ORDER),
959        POINT_QUADRATURE,
960        (1, 1),
961        false,
962    ),
963    DiscretizationCapability::new(
964        CellType::Segment,
965        1,
966        SEGMENT_FAMILIES,
967        (1, MAX_RUNTIME_ORDER),
968        GAUSS_QUADRATURE,
969        (1, MAX_RUNTIME_ORDER),
970        true,
971    ),
972    DiscretizationCapability::new(
973        CellType::Triangle,
974        2,
975        SIMPLEX_FAMILIES,
976        (1, MAX_RUNTIME_ORDER),
977        SIMPLEX_QUADRATURE,
978        (1, MAX_RUNTIME_ORDER),
979        true,
980    ),
981    DiscretizationCapability::new(
982        CellType::Quadrilateral,
983        2,
984        TENSOR_FAMILIES,
985        (1, MAX_RUNTIME_ORDER),
986        GAUSS_QUADRATURE,
987        (1, MAX_RUNTIME_ORDER),
988        true,
989    ),
990    DiscretizationCapability::new(
991        CellType::Tetrahedron,
992        3,
993        SIMPLEX_FAMILIES,
994        (1, MAX_RUNTIME_ORDER),
995        SIMPLEX_QUADRATURE,
996        (1, MAX_RUNTIME_ORDER),
997        true,
998    ),
999    DiscretizationCapability::new(
1000        CellType::Hexahedron,
1001        3,
1002        TENSOR_FAMILIES,
1003        (1, MAX_RUNTIME_ORDER),
1004        GAUSS_QUADRATURE,
1005        (1, MAX_RUNTIME_ORDER),
1006        true,
1007    ),
1008    DiscretizationCapability::new(
1009        CellType::Prism,
1010        3,
1011        PRISM_FAMILIES,
1012        (1, MAX_RUNTIME_ORDER),
1013        PRISM_QUADRATURE,
1014        (1, MAX_RUNTIME_ORDER),
1015        true,
1016    ),
1017    DiscretizationCapability::new(
1018        CellType::Pyramid,
1019        3,
1020        PYRAMID_FAMILIES,
1021        (1, MAX_RUNTIME_ORDER),
1022        PYRAMID_QUADRATURE,
1023        (1, MAX_RUNTIME_ORDER),
1024        true,
1025    ),
1026];
1027
1028/// Return the explicit runtime capability row for a cell type, when one exists.
1029pub fn discretization_capability(cell_type: CellType) -> Option<DiscretizationCapability> {
1030    capability_for(cell_type).copied()
1031}
1032
1033/// Iterate over the static supported-discretization matrix.
1034pub fn supported_discretizations() -> &'static [DiscretizationCapability] {
1035    RUNTIME_CAPABILITIES
1036}
1037
1038fn canonical_cell_type(cell_type: CellType) -> CellType {
1039    match cell_type {
1040        CellType::Simplex(0) => CellType::Vertex,
1041        CellType::Simplex(1) => CellType::Segment,
1042        CellType::Simplex(2) => CellType::Triangle,
1043        CellType::Simplex(3) => CellType::Tetrahedron,
1044        other => other,
1045    }
1046}
1047
1048fn is_dynamic_lagrange_supported(cell_type: CellType, family: BasisFamily) -> bool {
1049    matches!(
1050        (cell_type, family),
1051        (CellType::Simplex(_), BasisFamily::Simplex)
1052            | (CellType::Polygon(_), BasisFamily::TensorProduct)
1053            | (CellType::Polyhedron, BasisFamily::TensorProduct)
1054    )
1055}
1056
1057fn capability_for(cell_type: CellType) -> Option<&'static DiscretizationCapability> {
1058    let canonical = canonical_cell_type(cell_type);
1059    RUNTIME_CAPABILITIES
1060        .iter()
1061        .find(|capability| capability.cell_type == canonical)
1062}
1063
1064fn quadrature_family_from_name(
1065    normalized: &str,
1066    cell_type: CellType,
1067) -> Result<QuadratureFamily, MeshSieveError> {
1068    if normalized.contains("explicit") || normalized.contains("custom") {
1069        return Ok(QuadratureFamily::Explicit);
1070    }
1071    let canonical = canonical_cell_type(cell_type);
1072    match canonical {
1073        CellType::Vertex => Ok(QuadratureFamily::Point),
1074        CellType::Segment | CellType::Quadrilateral | CellType::Hexahedron => {
1075            if normalized.contains("gauss")
1076                || normalized.contains("legendre")
1077                || normalized.contains("gll")
1078                || normalized.contains("lobatto")
1079                || normalized.contains("tensor")
1080            {
1081                Ok(QuadratureFamily::GaussLegendre)
1082            } else {
1083                Err(MeshSieveError::InvalidGeometry(format!(
1084                    "unsupported quadrature '{normalized}' for {cell_type:?}; expected a Gauss/tensor-product rule"
1085                )))
1086            }
1087        }
1088        CellType::Triangle | CellType::Tetrahedron | CellType::Simplex(_) => {
1089            if normalized.contains("gauss")
1090                || normalized.contains("simplex")
1091                || normalized.contains("centroid")
1092                || normalized.contains("midpoint")
1093            {
1094                Ok(QuadratureFamily::Simplex)
1095            } else {
1096                Err(MeshSieveError::InvalidGeometry(format!(
1097                    "unsupported quadrature '{normalized}' for {cell_type:?}; expected a simplex rule"
1098                )))
1099            }
1100        }
1101        CellType::Prism => Ok(QuadratureFamily::PrismProduct),
1102        CellType::Pyramid => Ok(QuadratureFamily::Pyramid),
1103        CellType::Polygon(_) => Ok(QuadratureFamily::PolygonCentroid),
1104        CellType::Polyhedron => Ok(QuadratureFamily::PolyhedronCentroid),
1105    }
1106}
1107
1108fn ensure_quadrature_supported(
1109    cell_type: CellType,
1110    family: QuadratureFamily,
1111) -> Result<(), MeshSieveError> {
1112    if matches!(cell_type, CellType::Simplex(_))
1113        && matches!(
1114            family,
1115            QuadratureFamily::Simplex | QuadratureFamily::Explicit
1116        )
1117    {
1118        return Ok(());
1119    }
1120    if matches!(cell_type, CellType::Polygon(_))
1121        && matches!(
1122            family,
1123            QuadratureFamily::PolygonCentroid | QuadratureFamily::Explicit
1124        )
1125    {
1126        return Ok(());
1127    }
1128    if matches!(cell_type, CellType::Polyhedron)
1129        && matches!(
1130            family,
1131            QuadratureFamily::PolyhedronCentroid | QuadratureFamily::Explicit
1132        )
1133    {
1134        return Ok(());
1135    }
1136    let capability = capability_for(cell_type).ok_or_else(|| {
1137        MeshSieveError::InvalidGeometry(format!(
1138            "unsupported cell type {cell_type:?} for quadrature"
1139        ))
1140    })?;
1141    if !capability.supports_quadrature(family) {
1142        return Err(MeshSieveError::InvalidGeometry(format!(
1143            "unsupported quadrature family {family:?} for {cell_type:?}; supported families: {:?}",
1144            capability.quadrature_families
1145        )));
1146    }
1147    Ok(())
1148}
1149
1150/// Validate a coordinate geometry order against the explicit support matrix.
1151pub fn ensure_geometry_order_supported(
1152    cell_type: CellType,
1153    geometry_order: usize,
1154) -> Result<(), MeshSieveError> {
1155    if matches!(
1156        cell_type,
1157        CellType::Simplex(_) | CellType::Polygon(_) | CellType::Polyhedron
1158    ) && (1..=MAX_RUNTIME_ORDER).contains(&geometry_order)
1159    {
1160        return Ok(());
1161    }
1162    let capability = capability_for(cell_type).ok_or_else(|| {
1163        MeshSieveError::InvalidGeometry(format!(
1164            "unsupported cell type {cell_type:?} for geometry order {geometry_order}"
1165        ))
1166    })?;
1167    if !capability.supports_geometry_order(geometry_order) {
1168        return Err(MeshSieveError::InvalidGeometry(format!(
1169            "unsupported geometry order {geometry_order} for {cell_type:?}; supported range: {:?}",
1170            capability.geometry_order_range
1171        )));
1172    }
1173    Ok(())
1174}
1175
1176fn default_lagrange_family(cell_type: CellType) -> Result<BasisFamily, MeshSieveError> {
1177    if matches!(cell_type, CellType::Simplex(_)) {
1178        return Ok(BasisFamily::Simplex);
1179    }
1180    if matches!(cell_type, CellType::Polygon(_) | CellType::Polyhedron) {
1181        return Ok(BasisFamily::TensorProduct);
1182    }
1183    capability_for(cell_type)
1184        .and_then(|capability| capability.basis_families.first().copied())
1185        .ok_or_else(|| {
1186            MeshSieveError::InvalidGeometry(format!(
1187                "unsupported cell type {cell_type:?} for Lagrange basis"
1188            ))
1189        })
1190}
1191
1192fn ensure_lagrange_supported(
1193    cell_type: CellType,
1194    degree: usize,
1195    family: BasisFamily,
1196) -> Result<(), MeshSieveError> {
1197    if degree == 0 && canonical_cell_type(cell_type) != CellType::Vertex {
1198        return Err(MeshSieveError::InvalidGeometry(
1199            "Lagrange degree must be at least one for non-vertex cells".to_string(),
1200        ));
1201    }
1202    if is_dynamic_lagrange_supported(cell_type, family) {
1203        if degree <= MAX_RUNTIME_ORDER {
1204            return Ok(());
1205        }
1206        return Err(MeshSieveError::InvalidGeometry(format!(
1207            "unsupported Lagrange degree {degree} for {cell_type:?}; maximum supported degree is {MAX_RUNTIME_ORDER}"
1208        )));
1209    }
1210    let capability = capability_for(cell_type).ok_or_else(|| {
1211        MeshSieveError::InvalidGeometry(format!(
1212            "unsupported cell type {cell_type:?} for Lagrange basis"
1213        ))
1214    })?;
1215    if !capability.supports_basis(family, degree) {
1216        return Err(MeshSieveError::InvalidGeometry(format!(
1217            "unsupported Lagrange family/degree {family:?}/P{degree} for {cell_type:?}; supported families: {:?}, degree range: {:?}",
1218            capability.basis_families, capability.degree_range
1219        )));
1220    }
1221    Ok(())
1222}
1223
1224fn tabulate_p1_segment(points: &[Vec<f64>]) -> Result<BasisTabulation, MeshSieveError> {
1225    let mut values = Vec::with_capacity(points.len());
1226    let mut gradients = Vec::with_capacity(points.len());
1227    for point in points {
1228        if point.len() != 1 {
1229            return Err(MeshSieveError::InvalidGeometry(
1230                "segment quadrature must be 1D".to_string(),
1231            ));
1232        }
1233        let xi = point[0];
1234        values.push(vec![0.5 * (1.0 - xi), 0.5 * (1.0 + xi)]);
1235        gradients.push(vec![vec![-0.5], vec![0.5]]);
1236    }
1237    Ok(BasisTabulation { values, gradients })
1238}
1239
1240fn tabulate_q1_quad(points: &[Vec<f64>]) -> Result<BasisTabulation, MeshSieveError> {
1241    let mut values = Vec::with_capacity(points.len());
1242    let mut gradients = Vec::with_capacity(points.len());
1243    for point in points {
1244        if point.len() != 2 {
1245            return Err(MeshSieveError::InvalidGeometry(
1246                "quadrilateral quadrature must be 2D".to_string(),
1247            ));
1248        }
1249        let xi = point[0];
1250        let eta = point[1];
1251        let n1 = 0.25 * (1.0 - xi) * (1.0 - eta);
1252        let n2 = 0.25 * (1.0 + xi) * (1.0 - eta);
1253        let n3 = 0.25 * (1.0 + xi) * (1.0 + eta);
1254        let n4 = 0.25 * (1.0 - xi) * (1.0 + eta);
1255        values.push(vec![n1, n2, n3, n4]);
1256        gradients.push(vec![
1257            vec![-0.25 * (1.0 - eta), -0.25 * (1.0 - xi)],
1258            vec![0.25 * (1.0 - eta), -0.25 * (1.0 + xi)],
1259            vec![0.25 * (1.0 + eta), 0.25 * (1.0 + xi)],
1260            vec![-0.25 * (1.0 + eta), 0.25 * (1.0 - xi)],
1261        ]);
1262    }
1263    Ok(BasisTabulation { values, gradients })
1264}
1265
1266fn tabulate_lagrange(
1267    cell_type: CellType,
1268    degree: usize,
1269    family: BasisFamily,
1270    points: &[Vec<f64>],
1271) -> Result<BasisTabulation, MeshSieveError> {
1272    let nodes = reference_nodes(cell_type, degree, family)?;
1273    let dim = cell_type.dimension() as usize;
1274    for point in points {
1275        if point.len() != dim {
1276            return Err(MeshSieveError::InvalidGeometry(format!(
1277                "quadrature point dimension {} does not match {dim}",
1278                point.len()
1279            )));
1280        }
1281    }
1282    let exponents =
1283        interpolation_exponents(canonical_cell_type(cell_type), degree, family, nodes.len());
1284    let vandermonde: Vec<Vec<f64>> = nodes
1285        .iter()
1286        .map(|node| monomials(node, &exponents))
1287        .collect();
1288    let inv = invert_square(vandermonde)?;
1289    let mut values = Vec::with_capacity(points.len());
1290    let mut gradients = Vec::with_capacity(points.len());
1291    for point in points {
1292        let mons = monomials(point, &exponents);
1293        let dmons = monomial_gradients(point, &exponents);
1294        let mut vals = vec![0.0; nodes.len()];
1295        let mut grads = vec![vec![0.0; dim]; nodes.len()];
1296        for basis in 0..nodes.len() {
1297            for m in 0..exponents.len() {
1298                let coeff = inv[m][basis];
1299                vals[basis] += coeff * mons[m];
1300                for d in 0..dim {
1301                    grads[basis][d] += coeff * dmons[m][d];
1302                }
1303            }
1304        }
1305        values.push(vals);
1306        gradients.push(grads);
1307    }
1308    Ok(BasisTabulation { values, gradients })
1309}
1310
1311fn reference_nodes(
1312    cell_type: CellType,
1313    degree: usize,
1314    family: BasisFamily,
1315) -> Result<Vec<Vec<f64>>, MeshSieveError> {
1316    ensure_lagrange_supported(cell_type, degree, family)?;
1317    let p = degree;
1318    let canonical = canonical_cell_type(cell_type);
1319    let denom = p.max(1) as f64;
1320    let nodes = match (canonical, family) {
1321        (CellType::Vertex, BasisFamily::Simplex | BasisFamily::TensorProduct) => vec![Vec::new()],
1322        (CellType::Segment, BasisFamily::Simplex | BasisFamily::TensorProduct) => (0..=p)
1323            .map(|i| vec![-1.0 + 2.0 * i as f64 / denom])
1324            .collect(),
1325        (CellType::Triangle, BasisFamily::Simplex) => {
1326            let mut out = Vec::new();
1327            for i in 0..=p {
1328                for j in 0..=p - i {
1329                    out.push(vec![i as f64 / denom, j as f64 / denom]);
1330                }
1331            }
1332            out
1333        }
1334        (CellType::Tetrahedron, BasisFamily::Simplex) => simplex_nodes(3, p),
1335        (CellType::Simplex(dim), BasisFamily::Simplex) => simplex_nodes(dim as usize, p),
1336        (CellType::Polygon(n), BasisFamily::TensorProduct) => polygon_nodes(n as usize),
1337        (CellType::Polyhedron, BasisFamily::TensorProduct) => vec![vec![0.0, 0.0, 0.0]],
1338        (CellType::Quadrilateral, BasisFamily::TensorProduct) => tensor_nodes(2, p),
1339        (CellType::Hexahedron, BasisFamily::TensorProduct) => tensor_nodes(3, p),
1340        (CellType::Prism, BasisFamily::Prism) => {
1341            let tri = reference_nodes(CellType::Triangle, p, BasisFamily::Simplex)?;
1342            let seg = reference_nodes(CellType::Segment, p, BasisFamily::Simplex)?;
1343            let mut out = Vec::new();
1344            for t in &tri {
1345                for z in &seg {
1346                    out.push(vec![t[0], t[1], z[0]]);
1347                }
1348            }
1349            out
1350        }
1351        (CellType::Pyramid, BasisFamily::Pyramid) => {
1352            let mut out = Vec::new();
1353            for k in 0..=p {
1354                let layer = p - k;
1355                let z = -1.0 + 2.0 * k as f64 / denom;
1356                if layer == 0 {
1357                    out.push(vec![0.0, 0.0, z]);
1358                } else {
1359                    for i in 0..=layer {
1360                        for j in 0..=layer {
1361                            let scale = layer as f64 / denom;
1362                            out.push(vec![
1363                                scale * (-1.0 + 2.0 * i as f64 / layer as f64),
1364                                scale * (-1.0 + 2.0 * j as f64 / layer as f64),
1365                                z,
1366                            ]);
1367                        }
1368                    }
1369                }
1370            }
1371            out
1372        }
1373        _ => {
1374            return Err(MeshSieveError::InvalidGeometry(format!(
1375                "unsupported node layout for {cell_type:?} / {family:?}"
1376            )));
1377        }
1378    };
1379    Ok(nodes)
1380}
1381
1382fn simplex_nodes(dim: usize, degree: usize) -> Vec<Vec<f64>> {
1383    if dim == 0 {
1384        return vec![Vec::new()];
1385    }
1386    let denom = degree.max(1) as f64;
1387    simplex_exponents(dim, degree)
1388        .into_iter()
1389        .filter(|exp| exp.iter().sum::<usize>() <= degree)
1390        .map(|exp| exp.into_iter().map(|v| v as f64 / denom).collect())
1391        .collect()
1392}
1393
1394fn polygon_nodes(vertex_count: usize) -> Vec<Vec<f64>> {
1395    let n = vertex_count.max(3);
1396    (0..n)
1397        .map(|i| {
1398            let theta = std::f64::consts::TAU * i as f64 / n as f64;
1399            vec![theta.cos(), theta.sin()]
1400        })
1401        .collect()
1402}
1403
1404fn tensor_nodes(dim: usize, degree: usize) -> Vec<Vec<f64>> {
1405    let mut out = Vec::new();
1406    let mut cur = vec![0.0; dim];
1407    fn rec(out: &mut Vec<Vec<f64>>, cur: &mut [f64], axis: usize, degree: usize) {
1408        if axis == cur.len() {
1409            out.push(cur.to_vec());
1410            return;
1411        }
1412        for i in 0..=degree {
1413            cur[axis] = -1.0 + 2.0 * i as f64 / degree as f64;
1414            rec(out, cur, axis + 1, degree);
1415        }
1416    }
1417    rec(&mut out, &mut cur, 0, degree);
1418    out
1419}
1420
1421fn interpolation_exponents(
1422    cell_type: CellType,
1423    degree: usize,
1424    family: BasisFamily,
1425    count: usize,
1426) -> Vec<Vec<usize>> {
1427    let exponents = match (cell_type, family) {
1428        (CellType::Vertex, BasisFamily::Simplex | BasisFamily::TensorProduct) => vec![Vec::new()],
1429        (CellType::Segment, BasisFamily::Simplex | BasisFamily::TensorProduct) => {
1430            (0..=degree).map(|i| vec![i]).collect()
1431        }
1432        (CellType::Triangle, BasisFamily::Simplex) => simplex_exponents(2, degree),
1433        (CellType::Tetrahedron, BasisFamily::Simplex) => simplex_exponents(3, degree),
1434        (CellType::Simplex(dim), BasisFamily::Simplex) => simplex_exponents(dim as usize, degree),
1435        (CellType::Polygon(_), BasisFamily::TensorProduct) => monomial_exponents(2, count),
1436        (CellType::Polyhedron, BasisFamily::TensorProduct) => monomial_exponents(3, count),
1437        (CellType::Quadrilateral, BasisFamily::TensorProduct) => tensor_exponents(2, degree),
1438        (CellType::Hexahedron, BasisFamily::TensorProduct) => tensor_exponents(3, degree),
1439        (CellType::Prism, BasisFamily::Prism) => {
1440            let mut out = Vec::new();
1441            for tri in simplex_exponents(2, degree) {
1442                for z in 0..=degree {
1443                    out.push(vec![tri[0], tri[1], z]);
1444                }
1445            }
1446            out
1447        }
1448        (CellType::Pyramid, BasisFamily::Pyramid) => {
1449            let mut out = Vec::new();
1450            for z in 0..=degree {
1451                let layer_degree = degree - z;
1452                for x in 0..=layer_degree {
1453                    for y in 0..=layer_degree {
1454                        out.push(vec![x, y, z]);
1455                    }
1456                }
1457            }
1458            out
1459        }
1460        _ => monomial_exponents(cell_type.dimension() as usize, count),
1461    };
1462    debug_assert_eq!(exponents.len(), count);
1463    exponents
1464}
1465
1466fn simplex_exponents(dim: usize, degree: usize) -> Vec<Vec<usize>> {
1467    let mut out = Vec::new();
1468    for total in 0..=degree {
1469        let mut cur = vec![0; dim];
1470        gen_exponents_of_total(dim, total, 0, &mut cur, &mut out, usize::MAX);
1471    }
1472    out
1473}
1474
1475fn tensor_exponents(dim: usize, degree: usize) -> Vec<Vec<usize>> {
1476    let mut out = Vec::new();
1477    let mut cur = vec![0; dim];
1478    fn rec(out: &mut Vec<Vec<usize>>, cur: &mut [usize], axis: usize, degree: usize) {
1479        if axis == cur.len() {
1480            out.push(cur.to_vec());
1481            return;
1482        }
1483        for i in 0..=degree {
1484            cur[axis] = i;
1485            rec(out, cur, axis + 1, degree);
1486        }
1487    }
1488    rec(&mut out, &mut cur, 0, degree);
1489    out
1490}
1491
1492fn monomial_exponents(dim: usize, count: usize) -> Vec<Vec<usize>> {
1493    if dim == 0 {
1494        return vec![Vec::new(); count];
1495    }
1496    let mut exps = Vec::new();
1497    let mut total = 0;
1498    while exps.len() < count {
1499        let mut cur = vec![0; dim];
1500        gen_exponents_of_total(dim, total, 0, &mut cur, &mut exps, count);
1501        total += 1;
1502    }
1503    exps
1504}
1505
1506fn gen_exponents_of_total(
1507    dim: usize,
1508    total: usize,
1509    axis: usize,
1510    cur: &mut [usize],
1511    out: &mut Vec<Vec<usize>>,
1512    limit: usize,
1513) {
1514    if out.len() >= limit {
1515        return;
1516    }
1517    if axis + 1 == dim {
1518        cur[axis] = total;
1519        out.push(cur.to_vec());
1520        return;
1521    }
1522    for v in 0..=total {
1523        cur[axis] = v;
1524        gen_exponents_of_total(dim, total - v, axis + 1, cur, out, limit);
1525    }
1526}
1527
1528fn monomials(point: &[f64], exponents: &[Vec<usize>]) -> Vec<f64> {
1529    exponents
1530        .iter()
1531        .map(|exp| {
1532            point
1533                .iter()
1534                .zip(exp.iter())
1535                .map(|(x, e)| x.powi(*e as i32))
1536                .product()
1537        })
1538        .collect()
1539}
1540
1541fn monomial_gradients(point: &[f64], exponents: &[Vec<usize>]) -> Vec<Vec<f64>> {
1542    exponents
1543        .iter()
1544        .map(|exp| {
1545            (0..point.len())
1546                .map(|d| {
1547                    if exp[d] == 0 {
1548                        0.0
1549                    } else {
1550                        let mut value = exp[d] as f64;
1551                        for (axis, (x, e)) in point.iter().zip(exp.iter()).enumerate() {
1552                            let pow = if axis == d { e - 1 } else { *e };
1553                            value *= x.powi(pow as i32);
1554                        }
1555                        value
1556                    }
1557                })
1558                .collect()
1559        })
1560        .collect()
1561}
1562
1563fn invert_square(mut a: Vec<Vec<f64>>) -> Result<Vec<Vec<f64>>, MeshSieveError> {
1564    let n = a.len();
1565    if a.iter().any(|row| row.len() != n) {
1566        return Err(MeshSieveError::InvalidGeometry(
1567            "non-square interpolation matrix".to_string(),
1568        ));
1569    }
1570    let mut inv = vec![vec![0.0; n]; n];
1571    for (i, row) in inv.iter_mut().enumerate() {
1572        row[i] = 1.0;
1573    }
1574    for col in 0..n {
1575        let pivot = (col..n)
1576            .max_by(|&a_row, &b_row| a[a_row][col].abs().total_cmp(&a[b_row][col].abs()))
1577            .unwrap();
1578        if a[pivot][col].abs() < 1e-12 {
1579            return Err(MeshSieveError::InvalidGeometry(
1580                "singular interpolation matrix for basis nodes".to_string(),
1581            ));
1582        }
1583        a.swap(col, pivot);
1584        inv.swap(col, pivot);
1585        let scale = a[col][col];
1586        for j in 0..n {
1587            a[col][j] /= scale;
1588            inv[col][j] /= scale;
1589        }
1590        for r in 0..n {
1591            if r == col {
1592                continue;
1593            }
1594            let factor = a[r][col];
1595            if factor == 0.0 {
1596                continue;
1597            }
1598            for j in 0..n {
1599                a[r][j] -= factor * a[col][j];
1600                inv[r][j] -= factor * inv[col][j];
1601            }
1602        }
1603    }
1604    Ok(inv)
1605}
1606
1607fn point_quadrature(name: &str) -> QuadratureRule {
1608    QuadratureRule {
1609        name: name.to_string(),
1610        points: vec![Vec::new()],
1611        weights: vec![1.0],
1612    }
1613}
1614
1615fn gauss_legendre_1d(order: usize, name: &str) -> QuadratureRule {
1616    match order {
1617        1 => QuadratureRule {
1618            name: name.to_string(),
1619            points: vec![vec![0.0]],
1620            weights: vec![2.0],
1621        },
1622        3 => {
1623            let pt = (3.0_f64 / 5.0).sqrt();
1624            QuadratureRule {
1625                name: name.to_string(),
1626                points: vec![vec![-pt], vec![0.0], vec![pt]],
1627                weights: vec![5.0 / 9.0, 8.0 / 9.0, 5.0 / 9.0],
1628            }
1629        }
1630        _ => {
1631            let pt = 1.0_f64 / 3.0_f64.sqrt();
1632            QuadratureRule {
1633                name: name.to_string(),
1634                points: vec![vec![-pt], vec![pt]],
1635                weights: vec![1.0, 1.0],
1636            }
1637        }
1638    }
1639}
1640
1641fn tensor_power_quadrature(dim: usize, order: usize, name: &str) -> QuadratureRule {
1642    let one = gauss_legendre_1d(order, name);
1643    let mut points = vec![Vec::new()];
1644    let mut weights = vec![1.0];
1645    for _ in 0..dim {
1646        let mut next_points = Vec::new();
1647        let mut next_weights = Vec::new();
1648        for (prefix, pw) in points.iter().zip(weights.iter()) {
1649            for (p, w) in one.points.iter().zip(one.weights.iter()) {
1650                let mut q = prefix.clone();
1651                q.push(p[0]);
1652                next_points.push(q);
1653                next_weights.push(pw * w);
1654            }
1655        }
1656        points = next_points;
1657        weights = next_weights;
1658    }
1659    QuadratureRule {
1660        name: name.to_string(),
1661        points,
1662        weights,
1663    }
1664}
1665
1666fn simplex_quadrature(
1667    dim: usize,
1668    order: usize,
1669    name: &str,
1670) -> Result<QuadratureRule, MeshSieveError> {
1671    match dim {
1672        0 => Ok(point_quadrature(name)),
1673        1 => Ok(gauss_legendre_1d(order.min(3), name)),
1674        2 if order > 1 => Ok(QuadratureRule {
1675            name: name.to_string(),
1676            points: vec![
1677                vec![1.0 / 6.0, 1.0 / 6.0],
1678                vec![2.0 / 3.0, 1.0 / 6.0],
1679                vec![1.0 / 6.0, 2.0 / 3.0],
1680            ],
1681            weights: vec![1.0 / 6.0, 1.0 / 6.0, 1.0 / 6.0],
1682        }),
1683        2 => Ok(QuadratureRule {
1684            name: name.to_string(),
1685            points: vec![vec![1.0 / 3.0, 1.0 / 3.0]],
1686            weights: vec![0.5],
1687        }),
1688        3 => Ok(QuadratureRule {
1689            name: name.to_string(),
1690            points: vec![vec![0.25, 0.25, 0.25]],
1691            weights: vec![1.0 / 6.0],
1692        }),
1693        _ => Ok(QuadratureRule {
1694            name: name.to_string(),
1695            points: vec![vec![1.0 / (dim as f64 + 1.0); dim]],
1696            weights: vec![1.0 / factorial(dim) as f64],
1697        }),
1698    }
1699}
1700
1701fn factorial(n: usize) -> usize {
1702    (1..=n).product::<usize>().max(1)
1703}
1704
1705fn polygon_quadrature(vertex_count: usize, name: &str) -> Result<QuadratureRule, MeshSieveError> {
1706    Ok(QuadratureRule {
1707        name: name.to_string(),
1708        points: vec![vec![0.0, 0.0]],
1709        weights: vec![regular_polygon_area(vertex_count.max(3))],
1710    })
1711}
1712
1713fn regular_polygon_area(vertex_count: usize) -> f64 {
1714    let n = vertex_count as f64;
1715    0.5 * n * (std::f64::consts::TAU / n).sin()
1716}
1717
1718fn prism_quadrature(order: usize, name: &str) -> Result<QuadratureRule, MeshSieveError> {
1719    let tri = simplex_quadrature(2, order, name)?;
1720    let seg = gauss_legendre_1d(order.min(3), name);
1721    let mut points = Vec::new();
1722    let mut weights = Vec::new();
1723    for (tp, tw) in tri.points.iter().zip(tri.weights.iter()) {
1724        for (sp, sw) in seg.points.iter().zip(seg.weights.iter()) {
1725            points.push(vec![tp[0], tp[1], sp[0]]);
1726            weights.push(tw * sw);
1727        }
1728    }
1729    Ok(QuadratureRule {
1730        name: name.to_string(),
1731        points,
1732        weights,
1733    })
1734}
1735
1736fn pyramid_quadrature(_order: usize, name: &str) -> Result<QuadratureRule, MeshSieveError> {
1737    Ok(QuadratureRule {
1738        name: name.to_string(),
1739        points: vec![vec![0.0, 0.0, -0.5]],
1740        weights: vec![8.0 / 3.0],
1741    })
1742}
1743
1744fn build_jacobian(
1745    dim: usize,
1746    node_coords: &[Vec<f64>],
1747    ref_grads: &[Vec<f64>],
1748) -> Result<Vec<f64>, MeshSieveError> {
1749    let mut jac = vec![0.0; dim * dim];
1750    for (node, grad) in node_coords.iter().zip(ref_grads.iter()) {
1751        for phys_dim in 0..dim {
1752            for ref_dim in 0..dim {
1753                jac[phys_dim * dim + ref_dim] += node[phys_dim] * grad[ref_dim];
1754            }
1755        }
1756    }
1757    Ok(jac)
1758}
1759
1760fn invert_jacobian(dim: usize, jac: &[f64]) -> Result<(f64, Vec<f64>), MeshSieveError> {
1761    match dim {
1762        0 => Ok((1.0, Vec::new())),
1763        1 => {
1764            let det = jac[0];
1765            if det.abs() < f64::EPSILON {
1766                return Err(MeshSieveError::InvalidGeometry(
1767                    "zero Jacobian determinant".to_string(),
1768                ));
1769            }
1770            Ok((det, vec![1.0 / det]))
1771        }
1772        2 => {
1773            let a = jac[0];
1774            let b = jac[1];
1775            let c = jac[2];
1776            let d = jac[3];
1777            let det = a * d - b * c;
1778            if det.abs() < f64::EPSILON {
1779                return Err(MeshSieveError::InvalidGeometry(
1780                    "zero Jacobian determinant".to_string(),
1781                ));
1782            }
1783            Ok((det, vec![d / det, -b / det, -c / det, a / det]))
1784        }
1785        3 => {
1786            let a = jac[0];
1787            let b = jac[1];
1788            let c = jac[2];
1789            let d = jac[3];
1790            let e = jac[4];
1791            let f = jac[5];
1792            let g = jac[6];
1793            let h = jac[7];
1794            let i = jac[8];
1795            let det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
1796            if det.abs() < f64::EPSILON {
1797                return Err(MeshSieveError::InvalidGeometry(
1798                    "zero Jacobian determinant".to_string(),
1799                ));
1800            }
1801            let inv = vec![
1802                (e * i - f * h) / det,
1803                (c * h - b * i) / det,
1804                (b * f - c * e) / det,
1805                (f * g - d * i) / det,
1806                (a * i - c * g) / det,
1807                (c * d - a * f) / det,
1808                (d * h - e * g) / det,
1809                (b * g - a * h) / det,
1810                (a * e - b * d) / det,
1811            ];
1812            Ok((det, inv))
1813        }
1814        _ => invert_square_jacobian(dim, jac),
1815    }
1816}
1817
1818fn invert_square_jacobian(dim: usize, jac: &[f64]) -> Result<(f64, Vec<f64>), MeshSieveError> {
1819    if jac.len() != dim * dim {
1820        return Err(MeshSieveError::InvalidGeometry(format!(
1821            "Jacobian length {} does not match {dim}D square matrix",
1822            jac.len()
1823        )));
1824    }
1825    let mut a = vec![vec![0.0; dim]; dim];
1826    for r in 0..dim {
1827        for c in 0..dim {
1828            a[r][c] = jac[r * dim + c];
1829        }
1830    }
1831    let mut det = 1.0;
1832    let mut inv = vec![vec![0.0; dim]; dim];
1833    for i in 0..dim {
1834        inv[i][i] = 1.0;
1835    }
1836    for col in 0..dim {
1837        let pivot = (col..dim)
1838            .max_by(|&a_row, &b_row| a[a_row][col].abs().total_cmp(&a[b_row][col].abs()))
1839            .unwrap();
1840        if a[pivot][col].abs() < f64::EPSILON {
1841            return Err(MeshSieveError::InvalidGeometry(
1842                "zero Jacobian determinant".to_string(),
1843            ));
1844        }
1845        if pivot != col {
1846            a.swap(col, pivot);
1847            inv.swap(col, pivot);
1848            det = -det;
1849        }
1850        let scale = a[col][col];
1851        det *= scale;
1852        for j in 0..dim {
1853            a[col][j] /= scale;
1854            inv[col][j] /= scale;
1855        }
1856        for r in 0..dim {
1857            if r == col {
1858                continue;
1859            }
1860            let factor = a[r][col];
1861            for j in 0..dim {
1862                a[r][j] -= factor * a[col][j];
1863                inv[r][j] -= factor * inv[col][j];
1864            }
1865        }
1866    }
1867    Ok((det, inv.into_iter().flatten().collect()))
1868}
1869
1870fn polygon_area(vertices: &[Vec<f64>]) -> f64 {
1871    if vertices.len() < 3 {
1872        return 0.0;
1873    }
1874    let mut area = 0.0;
1875    for i in 0..vertices.len() {
1876        let j = (i + 1) % vertices.len();
1877        area += vertices[i][0] * vertices[j][1] - vertices[j][0] * vertices[i][1];
1878    }
1879    0.5 * area.abs()
1880}
1881
1882fn bounding_box_volume(vertices: &[Vec<f64>]) -> f64 {
1883    let mut mins = [f64::INFINITY; 3];
1884    let mut maxs = [f64::NEG_INFINITY; 3];
1885    for v in vertices {
1886        for d in 0..3 {
1887            mins[d] = mins[d].min(v[d]);
1888            maxs[d] = maxs[d].max(v[d]);
1889        }
1890    }
1891    (0..3).map(|d| maxs[d] - mins[d]).product::<f64>().abs()
1892}
1893
1894fn sub(a: &[f64], b: &[f64]) -> Vec<f64> {
1895    a.iter().zip(b.iter()).map(|(x, y)| x - y).collect()
1896}
1897
1898fn norm(v: &[f64]) -> f64 {
1899    v.iter().map(|x| x * x).sum::<f64>().sqrt()
1900}
1901
1902/// Build a CSR row/column structure from element closure DOFs.
1903pub fn csr_from_element_dof_maps(maps: impl IntoIterator<Item = DofMap>) -> CsrPattern {
1904    let mut rows: HashMap<ClosureDof, BTreeSet<ClosureDof>> = HashMap::new();
1905    for map in maps {
1906        let dofs = map.closure_dofs().to_vec();
1907        for row in &dofs {
1908            let cols = rows.entry(*row).or_default();
1909            for col in &dofs {
1910                cols.insert(*col);
1911            }
1912        }
1913    }
1914    let mut row_dofs: Vec<_> = rows.keys().copied().collect();
1915    row_dofs.sort_unstable();
1916    let mut xadj = Vec::with_capacity(row_dofs.len() + 1);
1917    let mut adjncy = Vec::new();
1918    xadj.push(0);
1919    for row in &row_dofs {
1920        if let Some(cols) = rows.get(row) {
1921            adjncy.extend(cols.iter().copied());
1922        }
1923        xadj.push(adjncy.len());
1924    }
1925    CsrPattern {
1926        xadj,
1927        adjncy,
1928        rows: row_dofs,
1929    }
1930}
1931
1932/// Symbolic CSR pattern over closure DOF rows and columns.
1933#[derive(Clone, Debug, PartialEq, Eq)]
1934pub struct CsrPattern {
1935    /// CSR offsets by row.
1936    pub xadj: Vec<usize>,
1937    /// CSR columns as closure DOFs.
1938    pub adjncy: Vec<ClosureDof>,
1939    /// Row closure DOFs.
1940    pub rows: Vec<ClosureDof>,
1941}
1942
1943#[cfg(test)]
1944mod tests {
1945    use super::*;
1946
1947    const CELL_CASES: &[CellType] = &[
1948        CellType::Vertex,
1949        CellType::Segment,
1950        CellType::Triangle,
1951        CellType::Quadrilateral,
1952        CellType::Tetrahedron,
1953        CellType::Hexahedron,
1954        CellType::Prism,
1955        CellType::Pyramid,
1956        CellType::Simplex(0),
1957        CellType::Simplex(1),
1958        CellType::Simplex(2),
1959        CellType::Simplex(3),
1960        CellType::Simplex(4),
1961        CellType::Polygon(5),
1962        CellType::Polyhedron,
1963    ];
1964
1965    const FAMILY_CASES: &[BasisFamily] = &[
1966        BasisFamily::Simplex,
1967        BasisFamily::TensorProduct,
1968        BasisFamily::Prism,
1969        BasisFamily::Pyramid,
1970    ];
1971
1972    fn capability_supports(cell_type: CellType, family: BasisFamily) -> bool {
1973        is_dynamic_lagrange_supported(cell_type, family)
1974            || capability_for(cell_type)
1975                .map(|capability| capability.basis_families.contains(&family))
1976                .unwrap_or(false)
1977    }
1978
1979    #[test]
1980    fn capability_table_is_the_lagrange_family_policy() {
1981        for &cell_type in CELL_CASES {
1982            for &family in FAMILY_CASES {
1983                let result = Basis::lagrange_with_family(cell_type, 1, family);
1984                assert_eq!(
1985                    result.is_ok(),
1986                    capability_supports(cell_type, family),
1987                    "{cell_type:?} / {family:?} should match capability table"
1988                );
1989            }
1990        }
1991    }
1992
1993    #[test]
1994    fn supported_basis_quadrature_and_node_layouts_tabulate() {
1995        for capability in RUNTIME_CAPABILITIES {
1996            for &family in capability.basis_families {
1997                for degree in 1..=2 {
1998                    let basis =
1999                        Basis::lagrange_with_family(capability.cell_type, degree, family).unwrap();
2000                    let quad =
2001                        QuadratureRule::from_metadata("gauss2", capability.cell_type).unwrap();
2002                    assert_eq!(quad.dimension(), basis.dimension());
2003
2004                    let tabulation = basis.tabulate(&quad.points).unwrap();
2005                    assert_eq!(tabulation.values.len(), quad.points.len());
2006                    assert_eq!(tabulation.gradients.len(), quad.points.len());
2007                    for values in &tabulation.values {
2008                        assert_eq!(values.len(), basis.num_nodes());
2009                        assert!((values.iter().sum::<f64>() - 1.0).abs() < 1e-10);
2010                    }
2011                }
2012            }
2013        }
2014    }
2015
2016    #[test]
2017    fn simplex_aliases_use_canonical_capabilities() {
2018        let aliases = [
2019            (CellType::Simplex(0), CellType::Vertex, 1),
2020            (CellType::Simplex(1), CellType::Segment, 2),
2021            (CellType::Simplex(2), CellType::Triangle, 3),
2022            (CellType::Simplex(3), CellType::Tetrahedron, 4),
2023        ];
2024        for (alias, canonical, expected_nodes) in aliases {
2025            let basis = Basis::lagrange(alias, 1).unwrap();
2026            assert_eq!(basis.num_nodes(), expected_nodes);
2027            let quad = QuadratureRule::from_metadata("gauss2", alias).unwrap();
2028            assert_eq!(quad.dimension(), canonical.dimension() as usize);
2029        }
2030    }
2031
2032    #[test]
2033    fn unsupported_combinations_are_explicit_policy_errors() {
2034        let bad_family =
2035            Basis::lagrange_with_family(CellType::Triangle, 1, BasisFamily::TensorProduct)
2036                .unwrap_err();
2037        assert!(format!("{bad_family}").contains("unsupported Lagrange family"));
2038
2039        let bad_cell =
2040            Basis::lagrange_with_family(CellType::Polygon(5), 1, BasisFamily::Simplex).unwrap_err();
2041        assert!(format!("{bad_cell}").contains("unsupported"));
2042
2043        let simplex4_quad = QuadratureRule::from_metadata("gauss2", CellType::Simplex(4)).unwrap();
2044        assert_eq!(simplex4_quad.dimension(), 4);
2045    }
2046
2047    #[test]
2048    fn jacobian_inversion_policy_covers_zero_through_three_dimensions() {
2049        let cases = [
2050            (0, Vec::new()),
2051            (1, vec![2.0]),
2052            (2, vec![2.0, 0.0, 0.0, 3.0]),
2053            (3, vec![2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0]),
2054            (
2055                4,
2056                vec![
2057                    2.0, 0.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 0.0, 5.0,
2058                ],
2059            ),
2060        ];
2061        for (dim, jacobian) in cases {
2062            let (det, inverse) = invert_jacobian(dim, &jacobian).unwrap();
2063            assert!(det > 0.0);
2064            assert_eq!(inverse.len(), dim * dim);
2065        }
2066        assert!(invert_jacobian(4, &[0.0; 16]).is_err());
2067    }
2068
2069    #[test]
2070    fn vertex_element_tabulates_with_zero_dimensional_jacobian() {
2071        let runtime = ElementRuntime {
2072            basis: Basis::lagrange(CellType::Vertex, 1).unwrap(),
2073            quadrature: QuadratureRule::from_metadata("point", CellType::Vertex).unwrap(),
2074        };
2075        let tabulation = tabulate_element(&runtime, &[Vec::new()]).unwrap();
2076        assert_eq!(tabulation.reference_points, vec![Vec::<f64>::new()]);
2077        assert_eq!(tabulation.physical_points, vec![Vec::<f64>::new()]);
2078        assert_eq!(tabulation.jacobians, vec![Vec::<f64>::new()]);
2079        assert_eq!(tabulation.jacobian_dets, vec![1.0]);
2080    }
2081}