dexterior_core/
mesh.rs

1//! The core discretization structure of DEC, the (simplicial) mesh.
2
3/// Low-level mesh construction and corresponding tests.
4mod mesh_construction;
5/// re-export the testing meshes for use in other modules' tests
6/// (pub because they're used outside of the core crate)
7#[doc(hidden)]
8pub use mesh_construction::{tiny_mesh_2d, tiny_mesh_3d};
9
10/// Views and iterators over simplices and dual cells.
11mod views;
12use views::IndexIter;
13pub use views::{DualCellIter, DualCellView, SimplexIter, SimplexView};
14
15mod subset;
16pub use subset::{Subset, SubsetImpl};
17
18//
19
20use fixedbitset as fb;
21use nalgebra as na;
22use nalgebra_sparse as nas;
23
24use itertools::izip;
25use std::{cell::OnceCell, collections::HashMap, rc::Rc};
26
27use crate::{
28    cochain::{Cochain, CochainImpl},
29    operator::Scaling,
30    quadrature::Quadrature,
31};
32
33/// A DEC mesh where the primal cells are all simplices
34/// (points, line segments, triangles, tetrahedra etc).
35#[derive(Clone, Debug)]
36pub struct SimplicialMesh<const DIM: usize> {
37    /// Vertices stored in a Rc so that they can be accessed from multiple locations.
38    /// Mutation after creation is not supported.
39    pub vertices: Rc<[na::SVector<f64, DIM>]>,
40    /// Storage for each dimension of simplex in the mesh.
41    pub(crate) simplices: Vec<SimplexCollection<DIM>>,
42    bounds: BoundingBox<DIM>,
43}
44
45/// An axis-aligned bounding box.
46#[derive(Clone, Copy, Debug)]
47pub struct BoundingBox<const DIM: usize> {
48    /// The minimum (bottom left in 2D) corner of the box.
49    pub min: na::SVector<f64, DIM>,
50    /// The maximum (top right in 2D) corner of the box.
51    pub max: na::SVector<f64, DIM>,
52}
53
54#[derive(Clone, Debug)]
55pub(crate) struct SimplexCollection<const MESH_DIM: usize> {
56    /// points per simplex in the storage Vec
57    simplex_size: usize,
58    /// indices stored in a flat Vec to avoid generics for dimension
59    pub indices: Vec<usize>,
60    /// map from the vertex indices of a simplex to its index in this collection.
61    /// constructed lazily in `SimplicialMesh::find_simplex_index`.
62    index_map: OnceCell<HashMap<Vec<usize>, usize>>,
63    /// matrix where the rows correspond to DIM-simplices,
64    /// the columns to (DIM-1) simplices,
65    /// and the values of -1 or 1 to the relative orientation of the boundary.
66    /// this matrix is the coboundary operator for (DIM-1) simplices,
67    /// but it's stored with DIM-simplices
68    /// because its rows can be efficiently used to navigate to boundary simplices.
69    boundary_map: nas::CsrMatrix<i8>,
70    /// transpose of the DIM+1-dimensional collection's `boundary_map`,
71    /// stored separately for efficient access.
72    /// the rows in this correspond to DIM-simplices again,
73    /// and the columns to DIM+1-simplices.
74    coboundary_map: nas::CsrMatrix<i8>,
75    /// simplices on the boundary of the mesh.
76    mesh_boundary: fb::FixedBitSet,
77    /// user-defined subsets, e.g. physical groups from gmsh meshes.
78    custom_subsets: HashMap<String, fb::FixedBitSet>,
79    /// circumcenters and barycenters Rc'd so that 0-simplices
80    /// can have the mesh vertices here without duplicating data
81    circumcenters: Rc<[na::SVector<f64, MESH_DIM>]>,
82    barycenters: Rc<[na::SVector<f64, MESH_DIM>]>,
83    /// barycentric differentials are not always needed
84    /// and take a fair bit of memory (`simplex_size` N-vectors per simplex),
85    /// so they're computed lazily on first access
86    barycentric_differentials: OnceCell<Vec<na::SVector<f64, MESH_DIM>>>,
87    /// unsigned volumes of the primal simplices
88    volumes: Vec<f64>,
89    /// unsigned volumes of the corresponding dual simplices
90    dual_volumes: Vec<f64>,
91    /// only for MESH_DIM-simplices:
92    /// orientation of the simplex with sorted indices
93    /// relative to the canonical orientation
94    /// (lower-dimensional simplices' orientation is only meaningful in the context of boundaries,
95    /// given by the boundary maps)
96    orientations: Vec<i8>,
97}
98
99impl<const DIM: usize> Default for SimplexCollection<DIM> {
100    fn default() -> Self {
101        // initialize empty collections
102        // which will be filled during mesh construction
103        Self {
104            simplex_size: 0,
105            indices: Vec::new(),
106            index_map: OnceCell::new(),
107            boundary_map: nas::CsrMatrix::zeros(0, 0),
108            coboundary_map: nas::CsrMatrix::zeros(0, 0),
109            mesh_boundary: fb::FixedBitSet::default(),
110            custom_subsets: HashMap::new(),
111            circumcenters: Rc::from([]),
112            barycenters: Rc::from([]),
113            barycentric_differentials: OnceCell::new(),
114            volumes: Vec::new(),
115            dual_volumes: Vec::new(),
116            orientations: Vec::new(),
117        }
118    }
119}
120
121impl<const DIM: usize> SimplexCollection<DIM> {
122    /// Get the number of simplices in the collection.
123    #[inline]
124    fn len(&self) -> usize {
125        self.indices.len() / self.simplex_size
126    }
127
128    /// Get the slice of vertex indices corresponding to a single simplex.
129    fn simplex_indices(&self, simplex_idx: usize) -> &[usize] {
130        let start_idx = simplex_idx * self.simplex_size;
131        &self.indices[start_idx..start_idx + self.simplex_size]
132    }
133}
134
135impl<const MESH_DIM: usize> SimplicialMesh<MESH_DIM> {
136    /// Construct a mesh from raw vertices and indices.
137    ///
138    /// The indices are given as a flat array,
139    /// where every `DIM + 1` indices correspond to one `DIM`-simplex.
140    #[inline]
141    pub fn new(vertices: Vec<na::SVector<f64, MESH_DIM>>, indices: Vec<usize>) -> Self {
142        mesh_construction::build_mesh(vertices, indices)
143    }
144
145    /// Get the number of `DIM`-simplices in the mesh.
146    #[inline]
147    pub fn simplex_count<const DIM: usize>(&self) -> usize
148    where
149        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
150    {
151        self.simplex_count_dyn(DIM)
152    }
153
154    /// Simplex count taking the dimension as a runtime parameter
155    /// to allow usage in dynamic contexts (internal APIs)
156    #[inline]
157    fn simplex_count_dyn(&self, dim: usize) -> usize {
158        self.simplices[dim].len()
159    }
160
161    /// Get a view into a simplex by its index in the data.
162    #[inline]
163    pub fn get_simplex_by_index<const DIM: usize>(
164        &self,
165        idx: usize,
166    ) -> SimplexView<'_, na::Const<DIM>, MESH_DIM>
167    where
168        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
169    {
170        self.get_simplex_by_index_impl::<na::Const<DIM>>(idx)
171    }
172
173    fn get_simplex_by_index_impl<Dim: na::DimName>(
174        &self,
175        idx: usize,
176    ) -> SimplexView<'_, Dim, MESH_DIM> {
177        let start_idx = idx * self.simplices[Dim::USIZE].simplex_size;
178        let idx_range = start_idx..start_idx + self.simplices[Dim::USIZE].simplex_size;
179        SimplexView {
180            mesh: self,
181            index: idx,
182            indices: &self.simplices[Dim::USIZE].indices[idx_range],
183            _marker: std::marker::PhantomData,
184        }
185    }
186
187    /// Get a slice of all vertices in the mesh.
188    #[inline]
189    pub fn vertices(&self) -> &[na::SVector<f64, MESH_DIM>] {
190        &self.vertices
191    }
192
193    /// Iterate over all `DIM`-dimensional simplices in the mesh.
194    pub fn simplices<const DIM: usize>(&self) -> SimplexIter<'_, DIM, MESH_DIM>
195    where
196        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
197    {
198        SimplexIter {
199            mesh: self,
200            idx_iter: IndexIter::All(0..self.simplices[DIM].len()),
201        }
202    }
203
204    /// Iterate over the `DIM`-simplices in the given subset.
205    pub fn simplices_in<'me, 'sub: 'me, const DIM: usize>(
206        &'me self,
207        subset: &'sub Subset<DIM, Primal>,
208    ) -> SimplexIter<'me, DIM, MESH_DIM>
209    where
210        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
211    {
212        SimplexIter {
213            mesh: self,
214            idx_iter: IndexIter::Subset(subset.indices.ones()),
215        }
216    }
217
218    /// Iterate over all `DIM`-dimensional dual cells in the mesh.
219    pub fn dual_cells<const DIM: usize>(&self) -> DualCellIter<'_, DIM, MESH_DIM>
220    where
221        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
222    {
223        DualCellIter {
224            mesh: self,
225            idx_iter: IndexIter::All(0..self.simplices[MESH_DIM - DIM].len()),
226        }
227    }
228
229    /// Iterate over the `DIM`-dimensional dual cells in the given subset.
230    pub fn dual_cells_in<'me, 'sub: 'me, const DIM: usize>(
231        &'me self,
232        subset: &'sub Subset<DIM, Dual>,
233    ) -> DualCellIter<'me, DIM, MESH_DIM>
234    where
235        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
236    {
237        DualCellIter {
238            mesh: self,
239            idx_iter: IndexIter::Subset(subset.indices.ones()),
240        }
241    }
242
243    /// Create an empty subset.
244    ///
245    /// This isn't useful on its own,
246    /// but can be handy when taking unions of many subsets.
247    pub fn empty_subset<const DIM: usize, Primality>(&self) -> Subset<DIM, Primality>
248    where
249        Primality: MeshPrimality,
250        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
251    {
252        Subset::<DIM, Primality>::new(fb::FixedBitSet::new())
253    }
254
255    /// Create a subset containing every `DIM`-cell in the mesh.
256    ///
257    /// This isn't useful on its own,
258    /// but can be handy when taking intersections of many subsets.
259    pub fn full_subset<const DIM: usize, Primality>(&self) -> Subset<DIM, Primality>
260    where
261        Primality: MeshPrimality,
262        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
263    {
264        let simplex_count = if Primality::IS_PRIMAL {
265            self.simplex_count_dyn(DIM)
266        } else {
267            self.simplex_count_dyn(MESH_DIM - DIM)
268        };
269        let mut indices = fb::FixedBitSet::with_capacity(simplex_count);
270        indices.set_range(.., true);
271        Subset::<DIM, Primality>::new(indices)
272    }
273
274    /// Get a subset representing the boundary of a subset of `MESH_DIM`-simplices.
275    ///
276    /// These are the `MESH_DIM - 1`-simplices
277    /// whose coboundary only includes one of this subset's simplices.
278    pub fn subset_boundary(
279        &self,
280        subset: &Subset<MESH_DIM, Primal>,
281    ) -> SubsetImpl<na::DimNameDiff<na::Const<MESH_DIM>, na::U1>, Primal>
282    where
283        // funky trait bounds to get the iterators in the impl working,
284        // these shouldn't limit anything in practice
285        na::Const<MESH_DIM>: na::DimNameSub<na::U1>
286            + na::DimNameSub<na::Const<MESH_DIM>>
287            + na::DimNameSub<na::DimNameSum<na::DimNameDiff<na::Const<MESH_DIM>, na::U1>, na::U1>>
288            + na::DimNameSub<na::DimNameDiff<na::Const<MESH_DIM>, na::U1>>,
289        na::DimNameDiff<na::Const<MESH_DIM>, na::U1>: na::DimNameAdd<na::U1>,
290    {
291        let simplices = self
292            .simplices_in::<MESH_DIM>(subset)
293            .flat_map(|s| s.boundary().map(|(_, b)| b))
294            .filter(|b| {
295                b.coboundary()
296                    .filter(|(_, cob)| subset.indices.contains(cob.index()))
297                    .count()
298                    == 1
299            });
300        SubsetImpl::<na::DimNameDiff<na::Const<MESH_DIM>, na::U1>, Primal>::from_simplex_iter(
301            simplices,
302        )
303    }
304
305    /// Take the complement of a subset, i.e. the cells not in that subset.
306    ///
307    /// Implementation note: this is a method of `mesh` instead of the subset itself
308    /// because subsets don't know the size of the whole mesh,
309    /// only how many indices are in themselves,
310    /// and therefore inverting one requires information from the mesh.
311    pub fn subset_complement<const DIM: usize, Primality>(
312        &self,
313        subset: &Subset<DIM, Primality>,
314    ) -> Subset<DIM, Primality>
315    where
316        Primality: MeshPrimality,
317        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
318    {
319        let mut indices = self.full_subset::<DIM, Primality>().indices;
320        indices.set_range(.., true);
321        indices.difference_with(&subset.indices);
322        Subset::<DIM, Primality>::new(indices)
323    }
324
325    /// Access the vertex indices for the given dimension of simplex
326    /// as a chunked iterator where each element is a `DIM + 1`-length slice
327    /// containing the indices of one simplex.
328    #[inline]
329    pub fn indices<const DIM: usize>(&self) -> std::slice::ChunksExact<'_, usize>
330    where
331        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
332    {
333        self.simplices[DIM].indices.chunks_exact(DIM + 1)
334    }
335
336    /// Get a slice of `DIM`-simplex barycenters.
337    #[inline]
338    pub fn barycenters<const DIM: usize>(&self) -> &[na::SVector<f64, MESH_DIM>]
339    where
340        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
341    {
342        &self.simplices[DIM].barycenters
343    }
344
345    /// Get a bounding box enclosing the entire mesh.
346    #[inline]
347    pub fn bounds(&self) -> BoundingBox<MESH_DIM> {
348        self.bounds
349    }
350
351    /// Create a new cochain with a value of zero
352    /// for each `DIM`-simplex in the mesh.
353    ///
354    /// See the [crate-level docs][crate#operators] for usage information.
355    pub fn new_zero_cochain<const DIM: usize, Primality>(&self) -> crate::Cochain<DIM, Primality>
356    where
357        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
358        Primality: MeshPrimality,
359    {
360        let primal_dim = if Primality::IS_PRIMAL {
361            DIM
362        } else {
363            MESH_DIM - DIM
364        };
365        crate::Cochain::zeros(self.simplex_count_dyn(primal_dim))
366    }
367
368    /// Create a cochain by integrating a function over cells of the mesh.
369    ///
370    /// See the [`quadrature`][crate::quadrature] module for available options.
371    ///
372    /// NOTE: The only cases of this that are fully implemented
373    /// are primal 0- and 1-simplices and dual 0-cells.
374    /// There is an initial sketch of an implementation for the general case,
375    /// but it currently gives vertices in an arbitrary order
376    /// that may not match the orientation of the cell.
377    /// It also hasn't been tested so correctness isn't guaranteed.
378    /// Use with extreme caution.
379    pub fn integrate_cochain<const DIM: usize, Primality>(
380        &self,
381        quadrature: impl Quadrature<DIM, MESH_DIM>,
382    ) -> crate::Cochain<DIM, Primality>
383    where
384        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
385        Primality: MeshPrimality,
386    {
387        let cell_count = if Primality::IS_PRIMAL {
388            self.simplex_count_dyn(DIM)
389        } else {
390            self.simplex_count_dyn(MESH_DIM - DIM)
391        };
392        self.integrate_cochain_impl(IndexIter::All(0..cell_count), quadrature)
393    }
394
395    /// Create a cochain by integrating over cells,
396    /// only performing the integration for the given subset.
397    ///
398    /// This currently has some severe limitations in its usefulness.
399    /// See [`integrate_cochain`][Self::integrate_cochain] for details.
400    pub fn integrate_cochain_partial<const DIM: usize, Primality>(
401        &self,
402        subset: &Subset<DIM, Primality>,
403        quadrature: impl Quadrature<DIM, MESH_DIM>,
404    ) -> crate::Cochain<DIM, Primality>
405    where
406        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
407        Primality: MeshPrimality,
408    {
409        self.integrate_cochain_impl(IndexIter::Subset(subset.indices.ones()), quadrature)
410    }
411
412    /// Overwrite a subset of an existing cochain with newly integrated values.
413    ///
414    /// Convenience method combining [`integrate_cochain_partial`][Self::integrate_cochain_partial]
415    /// and [`Cochain::overwrite`].
416    /// Has the same limitations as the other `integrate_cochain` methods.
417    pub fn integrate_overwrite<const DIM: usize, Primality>(
418        &self,
419        cochain: &mut Cochain<DIM, Primality>,
420        subset: &Subset<DIM, Primality>,
421        quadrature: impl Quadrature<DIM, MESH_DIM>,
422    ) where
423        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>> + Copy,
424        Primality: MeshPrimality + Copy,
425    {
426        cochain.overwrite(subset, &self.integrate_cochain_partial(subset, quadrature));
427    }
428
429    fn integrate_cochain_impl<const DIM: usize, Primality>(
430        &self,
431        idx_iter: IndexIter,
432        quadrature: impl Quadrature<DIM, MESH_DIM>,
433    ) -> crate::Cochain<DIM, Primality>
434    where
435        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
436        Primality: MeshPrimality,
437    {
438        let mut c = self.new_zero_cochain::<DIM, Primality>();
439
440        match (DIM, Primality::IS_PRIMAL) {
441            // special cases for 0-simplices since they don't need to be collected into a slice
442            (0, true) => {
443                for idx in idx_iter {
444                    c.values[idx] = quadrature.compute(&[self.vertices[idx]]);
445                }
446            }
447            (0, false) => {
448                // dual 0-simplices are the circumcenters of highest-dimensional simplices
449                for idx in idx_iter {
450                    c.values[idx] =
451                        quadrature.compute(&[self.simplices[MESH_DIM].circumcenters[idx]]);
452                }
453            }
454            // higher-dimensional simplices
455            // (incomplete cases!! see doc comment)
456            (_, true) => {
457                // buffer to hold vertices of the current cell
458                // so that we can pass them into the function as a slice
459                // (their placement in the source data is generally not contiguous)
460                let mut vertices = Vec::new();
461
462                for idx in idx_iter {
463                    let simplex = self.get_simplex_by_index::<DIM>(idx);
464                    vertices.extend(simplex.vertices());
465                    c.values[idx] = quadrature.compute(&vertices);
466                    vertices.clear();
467                }
468            }
469            (_, false) => {
470                let mut vertices = Vec::new();
471                let primal_dim = MESH_DIM - DIM;
472                // the dual cell is composed of circumcenters of all coboundary simplices
473                // TODO: this is not true -
474                // dual cell of a k-simplex is bounded by the circumcenters of (N-k)-simplices
475                for idx in idx_iter {
476                    let cob_row = self.simplices[primal_dim].coboundary_map.row(idx);
477                    for &cob_idx in cob_row.col_indices() {
478                        let cob_circumcenter =
479                            self.simplices[primal_dim + 1].circumcenters[cob_idx];
480                        vertices.push(cob_circumcenter);
481                    }
482                    // boundary dual 1-simplices are a special case
483                    // as they end on the boundary `MESH_DIM - 1`-simplex
484                    // instead of connecting two `MESH_DIM`-simplices like the rest
485                    if DIM == 1 && vertices.len() <= 1 {
486                        vertices.push(self.simplices[primal_dim].circumcenters[idx])
487                    }
488
489                    c.values[idx] = quadrature.compute(&vertices);
490                    vertices.clear();
491                }
492            }
493        }
494
495        c
496    }
497
498    /// Construct an exterior derivative operator.
499    ///
500    /// See the [crate-level docs][crate#operators] for usage information.
501    pub fn d<const DIM: usize, Primality>(&self) -> crate::ExteriorDerivative<DIM, Primality>
502    where
503        na::Const<DIM>: na::DimNameAdd<na::U1>,
504        na::Const<MESH_DIM>:
505            na::DimNameSub<na::DimNameSum<na::Const<DIM>, na::U1>> + na::DimNameSub<na::Const<DIM>>,
506        Primality: MeshPrimality,
507    {
508        let orientation_mat = if Primality::IS_PRIMAL {
509            &self.simplices[DIM + 1].boundary_map
510        } else {
511            &self.simplices[MESH_DIM - DIM - 1].coboundary_map
512        };
513        // build the same matrix
514        // but with Orientations converted to floats for easy multiplication.
515        // technically we could reference `orientation_mat` directly in the operator,
516        // but that would require introducing lifetimes to operators which gets annoying quickly
517        let float_mat = nas::CsrMatrix::try_from_pattern_and_values(
518            orientation_mat.pattern().clone(),
519            orientation_mat
520                .values()
521                .iter()
522                .map(|o| *o as isize as f64)
523                .collect(),
524        )
525        .unwrap();
526        crate::ExteriorDerivative::new(float_mat)
527    }
528
529    /// Construct a Hodge star operator.
530    ///
531    /// See the [crate-level docs][crate#operators] for usage information.
532    pub fn star<const DIM: usize, Primality>(&self) -> crate::HodgeStar<DIM, MESH_DIM, Primality>
533    where
534        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
535        Primality: MeshPrimality,
536    {
537        let primal_dim = if Primality::IS_PRIMAL {
538            DIM
539        } else {
540            MESH_DIM - DIM
541        };
542        let simplex_count = self.simplex_count_dyn(primal_dim);
543
544        let compute_diag_val = if Primality::IS_PRIMAL {
545            |(primal_vol, dual_vol): (&f64, &f64)| *dual_vol / *primal_vol
546        } else {
547            |(primal_vol, dual_vol): (&f64, &f64)| *primal_vol / *dual_vol
548        };
549
550        let diag = na::DVector::from_iterator(
551            simplex_count,
552            izip!(
553                &self.simplices[primal_dim].volumes,
554                &self.simplices[primal_dim].dual_volumes
555            )
556            .map(compute_diag_val),
557        );
558
559        // for dual star, compute sign to match the definition
560        // star^2 = (-1)^(k*(n-k))
561        // where n is the mesh dimension and k is the primal star dimension
562        let diag = if !Primality::IS_PRIMAL && primal_dim * (MESH_DIM - primal_dim) % 2 != 0 {
563            -diag
564        } else {
565            diag
566        };
567
568        crate::HodgeStar::new(diag)
569    }
570
571    /// Create a nonuniform scaling matrix that assigns coefficients to elements
572    /// of a primal cochain according to the given function.
573    ///
574    /// Due to type system constraints, this method only applies to primal cochains.
575    /// Use [`scaling_dual`][Self::scaling_dual] instead if scaling a dual cochain.
576    pub fn scaling<const DIM: usize>(
577        &self,
578        elem_scaling: impl Fn(SimplexView<na::Const<DIM>, MESH_DIM>) -> f64,
579    ) -> Scaling<DIM, Primal>
580    where
581        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
582    {
583        let diag = na::DVector::from_iterator(
584            self.simplex_count::<DIM>(),
585            self.simplices::<DIM>().map(elem_scaling),
586        );
587        Scaling::new(diag)
588    }
589
590    /// Create a nonuniform scaling matrix that assigns coefficients to elements
591    /// of a dual cochain according to the given function.
592    ///
593    /// Due to type system constraints, this method only applies to dual cochains.
594    /// Use [`scaling`][Self::scaling] instead if scaling a primal cochain.
595    pub fn scaling_dual<const DIM: usize>(
596        &self,
597        elem_scaling: impl Fn(DualCellView<na::Const<DIM>, MESH_DIM>) -> f64,
598    ) -> Scaling<DIM, Dual>
599    where
600        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
601    {
602        let diag = na::DVector::from_iterator(
603            self.simplex_count_dyn(MESH_DIM - DIM),
604            self.dual_cells::<DIM>().map(elem_scaling),
605        );
606        Scaling::new(diag)
607    }
608
609    /// Compute a discrete wedge product between two primal cochains.
610    ///
611    /// The output is a primal cochain whose dimension
612    /// is the sum of the input cochains' dimensions.
613    /// This method is only defined if the output dimension
614    /// is less than or equal to the mesh dimension.
615    ///
616    /// There are several possible ways to define a discrete wedge product.
617    /// We use the metric-independent definition given on page 74, eq. 7.2.1
618    /// of Hirani (2003). Discrete Exterior Calculus.
619    pub fn wedge<const D1: usize, const D2: usize>(
620        &self,
621        c1: &Cochain<D1, Primal>,
622        c2: &Cochain<D2, Primal>,
623    ) -> CochainImpl<na::DimNameSum<na::Const<D1>, na::Const<D2>>, Primal>
624    where
625        na::Const<D1>: na::DimNameAdd<na::Const<D2>>,
626        na::Const<MESH_DIM>: na::DimNameSub<na::DimNameSum<na::Const<D1>, na::Const<D2>>>,
627        na::Const<MESH_DIM>: na::DimNameSub<na::Const<D1>>,
628        na::Const<MESH_DIM>: na::DimNameSub<na::Const<D2>>,
629    {
630        let ret_dim: usize = D1 + D2;
631        let ret_simplex_count = self.simplex_count_dyn(ret_dim);
632        let mut ret = CochainImpl::zeros(ret_simplex_count);
633
634        let permutations = crate::permutation::Permutations::new(ret_dim + 1);
635        // scaling factor 1/(k+l+1)! in the formula
636        let scaling = 1. / (1..=(D1 + D2 + 1)).product::<usize>() as f64;
637
638        // buffers to hold boundary simplices' vertex indices,
639        // needed because we must sort them to easily find the corresponding simplex
640        let mut left_indices: Vec<usize> = Vec::with_capacity(D1 + 1);
641        let mut right_indices: Vec<usize> = Vec::with_capacity(D2 + 1);
642
643        // neat bit of type inference for the dimension of simplex
644        // from the fact that we use the simplex to assign to `ret`
645        for simplex in (0..ret_simplex_count).map(|i| self.get_simplex_by_index_impl(i)) {
646            // if the output is a top-dimensional simplex,
647            // we also need to consider the orientation of the simplex
648            // because vertices are in lexicographic order, not consistently oriented
649            // (lower-dimensional simplices on the other hand are consistently oriented)
650            let simplex_orientation = if ret_dim < MESH_DIM {
651                1.
652            } else {
653                self.simplices[MESH_DIM].orientations[simplex.index()] as f64
654            };
655            let mut sum = 0.;
656            for perm in permutations.iter() {
657                // we need to find the k- and l-dimensional boundary simplices
658                // corresponding to each permutation
659                // as well as their orientation relative to the permutation
660                // to get the right values out of the cochains
661
662                left_indices.extend(perm.indices[..=D1].iter().map(|i| simplex.indices[*i]));
663                right_indices.extend(perm.indices[D1..].iter().map(|i| simplex.indices[*i]));
664                let left_parity = crate::permutation::get_parity(&left_indices) as f64;
665                let right_parity = crate::permutation::get_parity(&right_indices) as f64;
666
667                // sort the indices to find the corresponding simplices using find_simplex_index,
668                // and thus the cochain values we need.
669                // this works because all simplices have their indices in ascending order.
670                // we could also sidestep the hashmap lookup in find_simplex_index
671                // by recursively iterating through boundaries
672                // but this seems easier and likely similar in terms of performance
673                left_indices.sort();
674                right_indices.sort();
675                let left_simplex = self
676                    .find_simplex_index_impl::<na::Const<D1>>(&left_indices)
677                    .unwrap();
678                let right_simplex = self
679                    .find_simplex_index_impl::<na::Const<D2>>(&right_indices)
680                    .unwrap();
681
682                sum += perm.sign as f64
683                    * simplex_orientation
684                    * (left_parity
685                        * c1.values[left_simplex]
686                        * right_parity
687                        * c2.values[right_simplex]);
688
689                left_indices.clear();
690                right_indices.clear();
691            }
692            ret[simplex] = scaling * sum;
693        }
694
695        ret
696    }
697
698    /// Get the set of `DIM`-simplices on the mesh boundary.
699    ///
700    /// This method does not exist for the highest-dimensional simplices in the mesh,
701    /// as they are all in the mesh interior.
702    #[inline]
703    pub fn boundary<const DIM: usize>(&self) -> Subset<DIM, Primal>
704    where
705        na::Const<DIM>: na::DimNameAdd<na::U1>,
706        na::Const<MESH_DIM>: na::DimNameSub<na::DimNameSum<na::Const<DIM>, na::U1>>,
707    {
708        Subset::new(self.simplices[DIM].mesh_boundary.clone())
709    }
710
711    /// Look up a subset of `DIM`-simplices, defined as a `gmsh` physical group, by name.
712    ///
713    /// Returns None if a subset with the name does not exist for this dimension.
714    ///
715    /// Note that the mesh boundary is a special subset
716    /// accessed through [`boundary`][Self::boundary] instead of this method.
717    #[inline]
718    pub fn get_subset<const DIM: usize>(&self, name: &str) -> Option<Subset<DIM, Primal>>
719    where
720        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
721    {
722        let indices = self.simplices[DIM].custom_subsets.get(name)?.clone();
723        Some(Subset::<DIM, Primal>::new(indices))
724    }
725
726    /// Store a named subset in the mesh data.
727    /// Not exposed to the user; used internally by the `gmsh` module.
728    #[inline]
729    pub(crate) fn store_subset<const DIM: usize>(
730        &mut self,
731        name: &str,
732        subset: Subset<DIM, Primal>,
733    ) {
734        self.simplices[DIM]
735            .custom_subsets
736            .insert(name.to_string(), subset.indices);
737    }
738
739    /// Find the index of a simplex in its collection given its vertex indices.
740    ///
741    /// This is not useful very often, but can be used
742    /// to associate a cochain value with a simplex
743    /// in cases when the simplex index is not known but its vertices are.
744    ///
745    /// Returns None if no `DIM`-simplex with the given indices exists.
746    /// This is always the case if the number of indices isn't `DIM + 1`.
747    #[inline]
748    pub fn find_simplex_index<const DIM: usize>(&self, indices: &[usize]) -> Option<usize>
749    where
750        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
751    {
752        self.find_simplex_index_impl(indices)
753    }
754
755    fn find_simplex_index_impl<Dim>(&self, indices: &[usize]) -> Option<usize>
756    where
757        Dim: na::DimName,
758        na::Const<MESH_DIM>: na::DimNameSub<Dim>,
759    {
760        // special case for 0-simplices since this is just the vertex index directly
761        if Dim::USIZE == 0 {
762            return Some(indices[0]);
763        }
764
765        let simplices = &self.simplices[Dim::USIZE];
766        let index_map = simplices.index_map.get_or_init(|| {
767            simplices
768                .indices
769                .chunks_exact(simplices.simplex_size)
770                .enumerate()
771                .map(|(i, vert_is)| (Vec::from(vert_is), i))
772                .collect()
773        });
774        index_map.get(indices).copied()
775    }
776
777    /// Get an iterator over barycentric differentials of each `DIM`-simplex.
778    /// This is primarily useful for constructing Whitney forms by hand.
779    ///
780    /// The returned iterator yields slices of `DIM + 1` vectors.
781    /// This method isn't implemented for 0-simplices.
782    pub fn barycentric_differentials<const DIM: usize>(
783        &self,
784    ) -> std::slice::ChunksExact<na::SVector<f64, MESH_DIM>>
785    where
786        na::Const<MESH_DIM>: na::DimNameSub<na::Const<DIM>>,
787        na::Const<DIM>: na::DimNameSub<na::U1>,
788    {
789        let simplices = &self.simplices[DIM];
790        let bary_diffs = simplices.barycentric_differentials.get_or_init(|| {
791            mesh_construction::compute_barycentric_differentials::<DIM, MESH_DIM>(self)
792        });
793        bary_diffs.chunks_exact(simplices.simplex_size)
794    }
795
796    /// Interpolate a 1-cochain defined on the edges of this mesh
797    /// into vectors at simplex barycenters using the Whitney map.
798    ///
799    /// This method is only defined for 2- and higher-dimensional meshes.
800    pub fn barycentric_interpolate_1(
801        &self,
802        c: &crate::Cochain<1, Primal>,
803    ) -> Vec<na::SVector<f64, MESH_DIM>>
804    where
805        na::Const<MESH_DIM>:
806            na::DimNameSub<na::U1> + na::DimNameSub<na::U2> + na::DimNameSub<na::Const<MESH_DIM>>,
807    {
808        let mut whitney_vals = Vec::new();
809
810        let top_simplices = &self.simplices[MESH_DIM];
811        for (indices, bary_diffs) in izip!(
812            top_simplices
813                .indices
814                .chunks_exact(top_simplices.simplex_size),
815            self.barycentric_differentials::<MESH_DIM>(),
816        ) {
817            let mut whitney_val = na::SVector::zeros();
818            // each combination of two vertices in the simplex
819            // is one of the 1-simplex edges contributing to the Whitney map
820            for (start, end) in
821                (0..indices.len() - 1).flat_map(|s| (s + 1..indices.len()).map(move |e| (s, e)))
822            {
823                let edge_indices = [indices[start], indices[end]];
824                let cochain_idx = self.find_simplex_index::<1>(&edge_indices).unwrap();
825                // the barycentric coordinates at the barycenter are all 1 / MESH_DIM.
826                // we'll multiply this into the value at the end
827                whitney_val += c.values[cochain_idx] * (bary_diffs[start] - bary_diffs[end]);
828            }
829            whitney_val /= MESH_DIM as f64;
830            whitney_vals.push(whitney_val);
831        }
832
833        whitney_vals
834    }
835}
836
837//
838// mesh primality generics
839//
840
841/// Marker type indicating a [`Cochain`][crate::Cochain]
842/// or [`operator`][crate::operator] corresponds to a primal mesh.
843#[derive(Clone, Copy, Debug)]
844pub struct Primal;
845
846/// Marker type indicating a [`Cochain`][crate::Cochain]
847/// or [`operator`][crate::operator] corresponds to a dual mesh.
848#[derive(Clone, Copy, Debug)]
849pub struct Dual;
850
851/// Trait allowing types and mesh methods to be generic
852/// on whether they operate on the primal ([`Primal`][self::Primal])
853/// or dual ([`Dual`][self::Dual]) mesh.
854///
855/// Not intended to be implemented by users,
856/// so contents are hidden from docs.
857pub trait MeshPrimality {
858    /// Constant for runtime branching.
859    ///
860    /// There used to be some GAT magic and associated methods here,
861    /// but branching on this is much easier to read
862    /// and should optimize to roughly the same machine code.
863    #[doc(hidden)]
864    const IS_PRIMAL: bool;
865    /// Maps Primal to Dual and Dual to Primal.
866    #[doc(hidden)]
867    type Opposite: MeshPrimality;
868}
869
870impl MeshPrimality for Primal {
871    const IS_PRIMAL: bool = true;
872    type Opposite = Dual;
873}
874
875impl MeshPrimality for Dual {
876    const IS_PRIMAL: bool = false;
877    type Opposite = Primal;
878}
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883
884    /// Test that the discrete wedge product gives expected results.
885    #[test]
886    fn wedge() {
887        let mesh_2d = tiny_mesh_2d();
888
889        // check with cochains where each value
890        // is an integer equal to the simplex index
891        let mut c0 = mesh_2d.new_zero_cochain::<0, Primal>();
892        let mut c1 = mesh_2d.new_zero_cochain::<1, Primal>();
893        // need a second 1-cochain different from the first
894        // because a cochain wedged with itself is always zero
895        let mut c1_2 = mesh_2d.new_zero_cochain::<1, Primal>();
896        let mut c2 = mesh_2d.new_zero_cochain::<2, Primal>();
897
898        for vals in [
899            c0.values.iter_mut(),
900            c1.values.iter_mut(),
901            c2.values.iter_mut(),
902        ] {
903            for (i, v) in vals.enumerate() {
904                *v = i as f64;
905            }
906        }
907        for (i, v) in c1_2.values.iter_mut().rev().enumerate() {
908            *v = i as f64;
909        }
910
911        let wedge_00 = mesh_2d.wedge(&c0, &c0);
912        let expected_00: Cochain<0, Primal> =
913            Cochain::from_values(vec![0., 1., 4., 9., 16., 25., 36.].into());
914        assert_eq!(
915            wedge_00, expected_00,
916            "0-cochains should be pointwise multiplied by wedge"
917        );
918
919        let wedge_01 = mesh_2d.wedge(&c0, &c1);
920        let wedge_10 = mesh_2d.wedge(&c1, &c0);
921        assert_eq!(wedge_01, wedge_10, "wedge with 0-cochain should commute");
922        let expected_01: Cochain<1, Primal> = Cochain::from_values(
923            vec![0., 1., 3., 6., 10., 12.5, 21., 24.5, 32., 40.5, 50., 60.5].into(),
924        );
925        assert_eq!(wedge_01, expected_01, "wrong result from 0-1 wedge");
926
927        let wedge_11_self = mesh_2d.wedge(&c1, &c1);
928        let expected_11: Cochain<2, Primal> = mesh_2d.new_zero_cochain();
929        assert_eq!(
930            wedge_11_self, expected_11,
931            "wedge of 1-cochain with itself should be zero"
932        );
933
934        let wedge_11_flipped = mesh_2d.wedge(&c1_2, &c1);
935        let wedge_11 = mesh_2d.wedge(&c1, &c1_2);
936        assert_eq!(
937            wedge_11, -wedge_11_flipped,
938            "wedge of 1-cochains should anticommute"
939        );
940        let expected_11: Cochain<2, Primal> =
941            Cochain::from_values(vec![-14. - 2. / 3., 11., -14. - 2. / 3., 11., -11., 11.].into());
942        assert_eq!(wedge_11, expected_11, "wrong result from 1-1 wedge");
943
944        // just going to trust that the correctness of these tests
945        // translates to further cases in 3d and beyond
946        // because computing these by hand is insanely cumbersome
947    }
948}