1use 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#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum Basis {
19 LagrangeP1Segment,
21 LagrangeQ1Quadrilateral,
23 Lagrange {
25 cell_type: CellType,
27 degree: usize,
29 family: BasisFamily,
31 },
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum BasisFamily {
37 Simplex,
39 TensorProduct,
41 Prism,
43 Pyramid,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum QuadratureFamily {
50 Point,
52 GaussLegendre,
54 Simplex,
56 PrismProduct,
58 Pyramid,
60 PolygonCentroid,
62 PolyhedronCentroid,
64 Explicit,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct DiscretizationCapability {
71 pub cell_type: CellType,
73 pub dimension: usize,
75 pub basis_families: &'static [BasisFamily],
77 pub degree_range: (usize, usize),
79 pub quadrature_families: &'static [QuadratureFamily],
81 pub geometry_order_range: (usize, usize),
83 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(°ree)
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 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 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 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 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 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 pub fn degree(&self) -> usize {
193 match self {
194 Basis::LagrangeP1Segment | Basis::LagrangeQ1Quadrilateral => 1,
195 Basis::Lagrange { degree, .. } => *degree,
196 }
197 }
198
199 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 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 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#[derive(Clone, Debug)]
239pub struct BasisTabulation {
240 pub values: Vec<Vec<f64>>,
242 pub gradients: Vec<Vec<Vec<f64>>>,
244}
245
246#[derive(Clone, Debug)]
248pub struct QuadratureRule {
249 pub name: String,
251 pub points: Vec<Vec<f64>>,
253 pub weights: Vec<f64>,
255}
256
257impl QuadratureRule {
258 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 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 pub fn dimension(&self) -> usize {
311 self.points.first().map(|p| p.len()).unwrap_or(0)
312 }
313}
314
315#[derive(Clone, Debug)]
317pub struct ElementRuntime {
318 pub basis: Basis,
319 pub quadrature: QuadratureRule,
320}
321
322pub 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#[derive(Clone, Debug)]
350pub struct ElementTabulation {
351 pub reference_points: Vec<Vec<f64>>,
353 pub weights: Vec<f64>,
355 pub physical_points: Vec<Vec<f64>>,
357 pub basis_values: Vec<Vec<f64>>,
359 pub basis_gradients: Vec<Vec<Vec<f64>>>,
361 pub jacobians: Vec<Vec<f64>>,
363 pub jacobian_dets: Vec<f64>,
365}
366
367pub 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
451pub 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
475pub 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#[derive(Clone, Debug, PartialEq)]
498pub struct FiniteVolumeMetadata {
499 pub components: usize,
501 pub reconstruction_order: usize,
503 pub limiter: Option<String>,
505}
506
507impl FiniteVolumeMetadata {
508 pub fn new(components: usize) -> Self {
510 Self {
511 components,
512 reconstruction_order: 1,
513 limiter: None,
514 }
515 }
516
517 pub fn with_reconstruction_order(mut self, order: usize) -> Self {
519 self.reconstruction_order = order;
520 self
521 }
522
523 pub fn with_limiter(mut self, limiter: impl Into<String>) -> Self {
525 self.limiter = Some(limiter.into());
526 self
527 }
528}
529
530#[derive(Clone, Debug, PartialEq)]
532pub struct CellGeometry {
533 pub centroid: Vec<f64>,
535 pub volume: f64,
537}
538
539#[derive(Clone, Debug, PartialEq)]
541pub struct FaceGeometry {
542 pub face: PointId,
544 pub centroid: Vec<f64>,
546 pub normal: Vec<f64>,
548 pub area: f64,
550 pub neighbors: Vec<PointId>,
552}
553
554#[derive(Clone, Debug, PartialEq, Eq)]
556pub struct FluxStencil {
557 pub face: PointId,
559 pub left: PointId,
561 pub right: Option<PointId>,
563}
564
565pub 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
601pub 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
649pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
671pub struct ClosureDof {
672 pub point: PointId,
674 pub local_dof: usize,
676}
677
678#[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 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 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 pub fn dofs(&self) -> &[PointId] {
719 &self.dofs
720 }
721
722 pub fn closure_dofs(&self) -> &[ClosureDof] {
724 &self.closure_dofs
725 }
726
727 pub fn len(&self) -> usize {
729 self.closure_dofs.len()
730 }
731
732 pub fn is_empty(&self) -> bool {
734 self.closure_dofs.is_empty()
735 }
736
737 pub fn index(&self, point: PointId) -> Option<usize> {
739 self.first_point_indices.get(&point).copied()
740 }
741
742 pub fn slot_index(&self, point: PointId, local_dof: usize) -> Option<usize> {
744 self.indices.get(&ClosureDof { point, local_dof }).copied()
745 }
746}
747
748pub 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
771pub 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
785pub 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
809pub 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
833pub 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
865pub 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
897pub 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
925const 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
1028pub fn discretization_capability(cell_type: CellType) -> Option<DiscretizationCapability> {
1030 capability_for(cell_type).copied()
1031}
1032
1033pub 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
1150pub 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
1902pub 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#[derive(Clone, Debug, PartialEq, Eq)]
1934pub struct CsrPattern {
1935 pub xadj: Vec<usize>,
1937 pub adjncy: Vec<ClosureDof>,
1939 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}