Skip to main content

mesh_sieve/data/
closure.rs

1//! Orientation-aware closure DOF extraction in the style of PETSc DMPlex.
2//!
3//! A [`ClosureIndexCache`] stores reusable closure indices keyed by
4//! `(cell, section_version, topology_version)`.  The indices flatten all DOFs
5//! in a cell closure once, so repeated element assembly can call
6//! [`get_closure`], [`set_closure`], or [`add_closure`] without re-traversing the
7//! topology DAG.
8
9use crate::data::section::Section;
10use crate::data::storage::Storage;
11use crate::mesh_error::MeshSieveError;
12use crate::topology::point::PointId;
13use crate::topology::sieve::{Orientation, OrientedSieve, Sieve};
14use std::collections::{HashMap, HashSet, VecDeque};
15use std::hash::Hash;
16use std::ops::AddAssign;
17
18/// Monotonic version supplied by the caller for topology changes.
19pub type TopologyVersion = u64;
20
21/// Stable cache key for closure indices.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub struct ClosureIndexKey {
24    /// Seed cell for the transitive closure.
25    pub cell: PointId,
26    /// [`Atlas`](crate::data::atlas::Atlas) version backing the section.
27    pub section_version: u64,
28    /// Caller-maintained topology version.
29    pub topology_version: TopologyVersion,
30}
31
32/// Ordering policy for points in a cell closure.
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub enum ClosureOrder {
35    /// Breadth-first DMPlex-style order: cell, cone, cone-of-cone, ... .
36    BreadthFirstDmpLex,
37    /// Deterministic lexicographic tensor order.  `dims` describes the tensor
38    /// grid shape when known; this implementation uses stable point ids as the
39    /// lexicographic coordinate surrogate for topological closures.
40    LexicographicTensor {
41        /// Tensor grid dimensions used by callers to describe the lexicographic shape.
42        dims: Vec<usize>,
43    },
44    /// User-provided point order.  Points not listed are appended in
45    /// breadth-first order.
46    Custom(Vec<PointId>),
47}
48
49impl Default for ClosureOrder {
50    fn default() -> Self {
51        Self::BreadthFirstDmpLex
52    }
53}
54
55/// One point-sized span inside a flattened closure vector.
56#[derive(Clone, Debug, PartialEq, Eq)]
57pub struct ClosurePointIndex<O> {
58    /// Topology point contributing these DOFs.
59    pub point: PointId,
60    /// Accumulated orientation from the closure seed to `point`.
61    pub orientation: O,
62    /// Offset in the flattened closure vector.
63    pub offset: usize,
64    /// Number of DOFs contributed by this point.
65    pub len: usize,
66    /// Maps closure-local DOF slots to section-local indices for this point.
67    pub permutation: Vec<usize>,
68}
69
70/// Reusable flattened index for a closure.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct ClosureIndex<O> {
73    /// Cache key used to build this index.
74    pub key: ClosureIndexKey,
75    /// Point spans in flattened closure order.
76    pub points: Vec<ClosurePointIndex<O>>,
77    /// Total number of flattened DOFs.
78    pub len: usize,
79}
80
81impl<O> ClosureIndex<O> {
82    /// Closure points in flattened order.
83    pub fn point_order(&self) -> impl Iterator<Item = PointId> + '_ {
84        self.points.iter().map(|entry| entry.point)
85    }
86}
87
88/// PetscSectionSym-like per-point orientation symmetries.
89///
90/// The returned permutation maps each closure-local DOF slot to the source
91/// index within that point's section slice. Returning `None` means identity.
92pub trait SectionSym<O> {
93    /// Return the orientation-dependent DOF permutation for `point`.
94    fn permutation(&self, point: PointId, orientation: O, dof_count: usize) -> Option<Vec<usize>>;
95}
96
97/// Identity section symmetry.
98#[derive(Clone, Copy, Debug, Default)]
99pub struct IdentitySectionSym;
100
101impl<O: Copy> SectionSym<O> for IdentitySectionSym {
102    #[inline]
103    fn permutation(
104        &self,
105        _point: PointId,
106        _orientation: O,
107        _dof_count: usize,
108    ) -> Option<Vec<usize>> {
109        None
110    }
111}
112
113/// Table-backed section symmetry keyed by `(point, orientation)`.
114#[derive(Clone, Debug, Default)]
115pub struct TableSectionSym<O> {
116    by_point_orientation: HashMap<(PointId, O), Vec<usize>>,
117    by_orientation: HashMap<O, Vec<usize>>,
118}
119
120impl<O> TableSectionSym<O>
121where
122    O: Copy + Eq + Hash,
123{
124    /// Create an empty table.
125    pub fn new() -> Self {
126        Self {
127            by_point_orientation: HashMap::new(),
128            by_orientation: HashMap::new(),
129        }
130    }
131
132    /// Set a permutation used for a specific point and orientation.
133    pub fn insert_point(&mut self, point: PointId, orientation: O, permutation: Vec<usize>) {
134        self.by_point_orientation
135            .insert((point, orientation), permutation);
136    }
137
138    /// Set a default permutation for an orientation, independent of point.
139    pub fn insert_orientation(&mut self, orientation: O, permutation: Vec<usize>) {
140        self.by_orientation.insert(orientation, permutation);
141    }
142}
143
144impl<O> SectionSym<O> for TableSectionSym<O>
145where
146    O: Copy + Eq + Hash,
147{
148    fn permutation(&self, point: PointId, orientation: O, _dof_count: usize) -> Option<Vec<usize>> {
149        self.by_point_orientation
150            .get(&(point, orientation))
151            .or_else(|| self.by_orientation.get(&orientation))
152            .cloned()
153    }
154}
155
156/// User-provided symmetry closure.
157pub struct FnSectionSym<F>(pub F);
158
159impl<O, F> SectionSym<O> for FnSectionSym<F>
160where
161    F: Fn(PointId, O, usize) -> Option<Vec<usize>>,
162{
163    fn permutation(&self, point: PointId, orientation: O, dof_count: usize) -> Option<Vec<usize>> {
164        (self.0)(point, orientation, dof_count)
165    }
166}
167
168/// Cache for repeated closure-index construction.
169#[derive(Clone, Debug, Default)]
170pub struct ClosureIndexCache<O> {
171    entries: HashMap<ClosureIndexKey, ClosureIndex<O>>,
172}
173
174impl<O> ClosureIndexCache<O>
175where
176    O: Orientation + Eq + Hash,
177{
178    /// Create an empty cache.
179    pub fn new() -> Self {
180        Self {
181            entries: HashMap::new(),
182        }
183    }
184
185    /// Drop all cached indices.
186    pub fn clear(&mut self) {
187        self.entries.clear();
188    }
189
190    /// Return a cached index or build and insert it.
191    pub fn get_or_build<T, V, Sct, Sym>(
192        &mut self,
193        topology: &T,
194        section: &Section<V, Sct>,
195        cell: PointId,
196        topology_version: TopologyVersion,
197        order: &ClosureOrder,
198        sym: &Sym,
199    ) -> Result<&ClosureIndex<O>, MeshSieveError>
200    where
201        T: OrientedSieve<Point = PointId, Orient = O>,
202        Sct: Storage<V>,
203        Sym: SectionSym<O>,
204    {
205        let key = ClosureIndexKey {
206            cell,
207            section_version: section.atlas().version(),
208            topology_version,
209        };
210        if let std::collections::hash_map::Entry::Vacant(e) = self.entries.entry(key) {
211            let index = build_closure_index(topology, section, cell, topology_version, order, sym)?;
212            e.insert(index);
213        }
214        Ok(self.entries.get(&key).expect("inserted or present"))
215    }
216}
217
218impl ClosureIndexCache<()> {
219    /// Return a cached non-oriented index or build and insert it.
220    pub fn get_or_build_unoriented<T, V, Sct, Sym>(
221        &mut self,
222        topology: &T,
223        section: &Section<V, Sct>,
224        cell: PointId,
225        topology_version: TopologyVersion,
226        order: &ClosureOrder,
227        sym: &Sym,
228    ) -> Result<&ClosureIndex<()>, MeshSieveError>
229    where
230        T: Sieve<Point = PointId>,
231        Sct: Storage<V>,
232        Sym: SectionSym<()>,
233    {
234        let key = ClosureIndexKey {
235            cell,
236            section_version: section.atlas().version(),
237            topology_version,
238        };
239        if let std::collections::hash_map::Entry::Vacant(e) = self.entries.entry(key) {
240            let index = build_closure_index_unoriented(
241                topology,
242                section,
243                cell,
244                topology_version,
245                order,
246                sym,
247            )?;
248            e.insert(index);
249        }
250        Ok(self.entries.get(&key).expect("inserted or present"))
251    }
252}
253
254/// Build an orientation-aware closure index without caching.
255pub fn build_closure_index<T, V, Sct, O, Sym>(
256    topology: &T,
257    section: &Section<V, Sct>,
258    cell: PointId,
259    topology_version: TopologyVersion,
260    order: &ClosureOrder,
261    sym: &Sym,
262) -> Result<ClosureIndex<O>, MeshSieveError>
263where
264    T: OrientedSieve<Point = PointId, Orient = O>,
265    Sct: Storage<V>,
266    O: Orientation + Eq + Hash,
267    Sym: SectionSym<O>,
268{
269    let key = ClosureIndexKey {
270        cell,
271        section_version: section.atlas().version(),
272        topology_version,
273    };
274    let oriented_points = ordered_oriented_closure(topology, cell, order);
275    let mut offset = 0usize;
276    let mut points = Vec::with_capacity(oriented_points.len());
277    for (point, orientation) in oriented_points {
278        let len = match section.atlas().get(point) {
279            Some((_, len)) => len,
280            None => continue,
281        };
282        let permutation =
283            normalized_permutation(point, len, sym.permutation(point, orientation, len))?;
284        points.push(ClosurePointIndex {
285            point,
286            orientation,
287            offset,
288            len,
289            permutation,
290        });
291        offset += len;
292    }
293    Ok(ClosureIndex {
294        key,
295        points,
296        len: offset,
297    })
298}
299
300/// Build a non-oriented closure index for a plain [`Sieve`].
301pub fn build_closure_index_unoriented<T, V, Sct, Sym>(
302    topology: &T,
303    section: &Section<V, Sct>,
304    cell: PointId,
305    topology_version: TopologyVersion,
306    order: &ClosureOrder,
307    sym: &Sym,
308) -> Result<ClosureIndex<()>, MeshSieveError>
309where
310    T: Sieve<Point = PointId>,
311    Sct: Storage<V>,
312    Sym: SectionSym<()>,
313{
314    let key = ClosureIndexKey {
315        cell,
316        section_version: section.atlas().version(),
317        topology_version,
318    };
319    let points_only = ordered_closure(topology, cell, order);
320    let mut offset = 0usize;
321    let mut points = Vec::with_capacity(points_only.len());
322    for point in points_only {
323        let len = match section.atlas().get(point) {
324            Some((_, len)) => len,
325            None => continue,
326        };
327        let permutation = normalized_permutation(point, len, sym.permutation(point, (), len))?;
328        points.push(ClosurePointIndex {
329            point,
330            orientation: (),
331            offset,
332            len,
333            permutation,
334        });
335        offset += len;
336    }
337    Ok(ClosureIndex {
338        key,
339        points,
340        len: offset,
341    })
342}
343
344/// Extract closure DOFs using a precomputed index.
345pub fn get_closure<V, Sct, O>(
346    section: &Section<V, Sct>,
347    index: &ClosureIndex<O>,
348) -> Result<Vec<V>, MeshSieveError>
349where
350    V: Clone,
351    Sct: Storage<V>,
352{
353    validate_section_version(section, index)?;
354    let mut out = Vec::with_capacity(index.len);
355    for entry in &index.points {
356        let slice = section.try_restrict(entry.point)?;
357        for &src in &entry.permutation {
358            out.push(slice[src].clone());
359        }
360    }
361    Ok(out)
362}
363
364/// Set closure DOFs using a precomputed index.
365pub fn set_closure<V, Sct, O>(
366    section: &mut Section<V, Sct>,
367    index: &ClosureIndex<O>,
368    values: &[V],
369) -> Result<(), MeshSieveError>
370where
371    V: Clone,
372    Sct: Storage<V>,
373{
374    validate_section_version(section, index)?;
375    validate_closure_len(index, values.len())?;
376    for entry in &index.points {
377        let slice = section.try_restrict_mut(entry.point)?;
378        for (local_slot, &section_slot) in entry.permutation.iter().enumerate() {
379            slice[section_slot] = values[entry.offset + local_slot].clone();
380        }
381    }
382    Ok(())
383}
384
385/// Add closure DOFs into a section using a precomputed index.
386pub fn add_closure<V, Sct, O>(
387    section: &mut Section<V, Sct>,
388    index: &ClosureIndex<O>,
389    values: &[V],
390) -> Result<(), MeshSieveError>
391where
392    V: Clone + AddAssign<V>,
393    Sct: Storage<V>,
394{
395    validate_section_version(section, index)?;
396    validate_closure_len(index, values.len())?;
397    for entry in &index.points {
398        let slice = section.try_restrict_mut(entry.point)?;
399        for (local_slot, &section_slot) in entry.permutation.iter().enumerate() {
400            slice[section_slot] += values[entry.offset + local_slot].clone();
401        }
402    }
403    Ok(())
404}
405
406/// Build an index and extract closure DOFs in one call.
407pub fn get_closure_oriented<T, V, Sct, O, Sym>(
408    topology: &T,
409    section: &Section<V, Sct>,
410    cell: PointId,
411    topology_version: TopologyVersion,
412    order: &ClosureOrder,
413    sym: &Sym,
414) -> Result<Vec<V>, MeshSieveError>
415where
416    T: OrientedSieve<Point = PointId, Orient = O>,
417    V: Clone,
418    Sct: Storage<V>,
419    O: Orientation + Eq + Hash,
420    Sym: SectionSym<O>,
421{
422    let index = build_closure_index(topology, section, cell, topology_version, order, sym)?;
423    get_closure(section, &index)
424}
425
426fn validate_section_version<V, Sct, O>(
427    section: &Section<V, Sct>,
428    index: &ClosureIndex<O>,
429) -> Result<(), MeshSieveError>
430where
431    Sct: Storage<V>,
432{
433    let current = section.atlas().version();
434    if current != index.key.section_version {
435        return Err(MeshSieveError::AtlasPlanStale {
436            expected: index.key.section_version,
437            found: current,
438        });
439    }
440    Ok(())
441}
442
443fn validate_closure_len<O>(index: &ClosureIndex<O>, found: usize) -> Result<(), MeshSieveError> {
444    if index.len != found {
445        return Err(MeshSieveError::ScatterLengthMismatch {
446            expected: index.len,
447            found,
448        });
449    }
450    Ok(())
451}
452
453fn normalized_permutation(
454    point: PointId,
455    len: usize,
456    permutation: Option<Vec<usize>>,
457) -> Result<Vec<usize>, MeshSieveError> {
458    let perm = permutation.unwrap_or_else(|| (0..len).collect());
459    if perm.len() != len {
460        return Err(MeshSieveError::SliceLengthMismatch {
461            point,
462            expected: len,
463            found: perm.len(),
464        });
465    }
466    let mut seen = vec![false; len];
467    for &idx in &perm {
468        if idx >= len || seen[idx] {
469            return Err(MeshSieveError::InvalidPermutation(format!(
470                "invalid closure permutation for {point:?}: {perm:?}"
471            )));
472        }
473        seen[idx] = true;
474    }
475    Ok(perm)
476}
477
478fn ordered_closure<T>(topology: &T, cell: PointId, order: &ClosureOrder) -> Vec<PointId>
479where
480    T: Sieve<Point = PointId>,
481{
482    let bfs = bfs_closure(topology, cell);
483    apply_order(bfs, order)
484}
485
486fn ordered_oriented_closure<T, O>(
487    topology: &T,
488    cell: PointId,
489    order: &ClosureOrder,
490) -> Vec<(PointId, O)>
491where
492    T: OrientedSieve<Point = PointId, Orient = O>,
493    O: Orientation + Eq + Hash,
494{
495    let bfs = bfs_oriented_closure(topology, cell);
496    let ordered_points = apply_order(bfs.iter().map(|(point, _)| *point).collect(), order);
497    let orientations: HashMap<PointId, O> = bfs.into_iter().collect();
498    ordered_points
499        .into_iter()
500        .filter_map(|point| orientations.get(&point).copied().map(|o| (point, o)))
501        .collect()
502}
503
504fn apply_order(mut bfs: Vec<PointId>, order: &ClosureOrder) -> Vec<PointId> {
505    match order {
506        ClosureOrder::BreadthFirstDmpLex => bfs,
507        ClosureOrder::LexicographicTensor { .. } => {
508            if let Some((&cell, rest)) = bfs.split_first() {
509                let mut out = vec![cell];
510                let mut rest = rest.to_vec();
511                rest.sort_unstable();
512                out.extend(rest);
513                out
514            } else {
515                bfs
516            }
517        }
518        ClosureOrder::Custom(custom) => {
519            let bfs_set: HashSet<_> = bfs.iter().copied().collect();
520            let mut used = HashSet::new();
521            let mut out = Vec::with_capacity(bfs.len());
522            for &point in custom {
523                if bfs_set.contains(&point) && used.insert(point) {
524                    out.push(point);
525                }
526            }
527            for point in bfs.drain(..) {
528                if used.insert(point) {
529                    out.push(point);
530                }
531            }
532            out
533        }
534    }
535}
536
537fn bfs_closure<T>(topology: &T, cell: PointId) -> Vec<PointId>
538where
539    T: Sieve<Point = PointId>,
540{
541    let mut seen = HashSet::new();
542    let mut queue = VecDeque::from([cell]);
543    let mut out = Vec::new();
544    while let Some(point) = queue.pop_front() {
545        if !seen.insert(point) {
546            continue;
547        }
548        out.push(point);
549        for child in topology.cone_points(point) {
550            if !seen.contains(&child) {
551                queue.push_back(child);
552            }
553        }
554    }
555    out
556}
557
558fn bfs_oriented_closure<T, O>(topology: &T, cell: PointId) -> Vec<(PointId, O)>
559where
560    T: OrientedSieve<Point = PointId, Orient = O>,
561    O: Orientation + Eq + Hash,
562{
563    let mut seen = HashSet::new();
564    let mut queue = VecDeque::from([(cell, O::default())]);
565    let mut out = Vec::new();
566    while let Some((point, orientation)) = queue.pop_front() {
567        if !seen.insert(point) {
568            continue;
569        }
570        out.push((point, orientation));
571        for (child, edge_orientation) in topology.cone_o(point) {
572            if !seen.contains(&child) {
573                queue.push_back((child, O::compose(orientation, edge_orientation)));
574            }
575        }
576    }
577    out
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583    use crate::data::atlas::Atlas;
584    use crate::data::storage::VecStorage;
585    use crate::topology::orientation::Sign;
586    use crate::topology::sieve::{InMemoryOrientedSieve, InMemorySieve};
587
588    fn p(id: u64) -> PointId {
589        PointId::new(id).unwrap()
590    }
591
592    #[test]
593    fn get_set_add_unoriented_closure() {
594        let mut topo = InMemorySieve::<PointId, ()>::default();
595        topo.add_arrow(p(1), p(2), ());
596        topo.add_arrow(p(1), p(3), ());
597
598        let mut atlas = Atlas::default();
599        atlas.try_insert(p(1), 1).unwrap();
600        atlas.try_insert(p(2), 2).unwrap();
601        atlas.try_insert(p(3), 1).unwrap();
602        let mut section = Section::<i32, VecStorage<i32>>::new(atlas);
603        section.try_scatter_in_order(&[10, 20, 21, 30]).unwrap();
604
605        let index = build_closure_index_unoriented(
606            &topo,
607            &section,
608            p(1),
609            0,
610            &ClosureOrder::BreadthFirstDmpLex,
611            &IdentitySectionSym,
612        )
613        .unwrap();
614        assert_eq!(get_closure(&section, &index).unwrap(), vec![10, 20, 21, 30]);
615
616        set_closure(&mut section, &index, &[1, 2, 3, 4]).unwrap();
617        add_closure(&mut section, &index, &[10, 20, 30, 40]).unwrap();
618        assert_eq!(get_closure(&section, &index).unwrap(), vec![11, 22, 33, 44]);
619
620        let mut cache = ClosureIndexCache::new();
621        let cached = cache
622            .get_or_build_unoriented(
623                &topo,
624                &section,
625                p(1),
626                0,
627                &ClosureOrder::BreadthFirstDmpLex,
628                &IdentitySectionSym,
629            )
630            .unwrap();
631        assert_eq!(cached.key.cell, p(1));
632    }
633
634    #[test]
635    fn oriented_symmetry_permutates_point_dofs() {
636        let mut topo = InMemoryOrientedSieve::<PointId, (), Sign>::default();
637        topo.add_arrow_o(p(1), p(2), (), Sign(true));
638
639        let mut atlas = Atlas::default();
640        atlas.try_insert(p(2), 3).unwrap();
641        let mut section = Section::<i32, VecStorage<i32>>::new(atlas);
642        section.try_scatter_in_order(&[1, 2, 3]).unwrap();
643
644        let mut sym = TableSectionSym::new();
645        sym.insert_orientation(Sign(true), vec![2, 1, 0]);
646        let index = build_closure_index(
647            &topo,
648            &section,
649            p(1),
650            7,
651            &ClosureOrder::BreadthFirstDmpLex,
652            &sym,
653        )
654        .unwrap();
655        assert_eq!(index.key.topology_version, 7);
656        assert_eq!(get_closure(&section, &index).unwrap(), vec![3, 2, 1]);
657    }
658
659    #[test]
660    fn custom_order_appends_unlisted_points() {
661        let mut topo = InMemorySieve::<PointId, ()>::default();
662        topo.add_arrow(p(1), p(2), ());
663        topo.add_arrow(p(1), p(3), ());
664        let ordered = ordered_closure(&topo, p(1), &ClosureOrder::Custom(vec![p(3)]));
665        assert_eq!(ordered, vec![p(3), p(1), p(2)]);
666    }
667}