Skip to main content

mesh_sieve/algs/
reduction.rs

1use crate::mesh_error::MeshSieveError;
2use crate::topology::sieve::{Sieve, SieveRef};
3
4/// Abstraction over reachability rows used in transitive algorithms.
5///
6/// Default implementation is dense and backed by `Vec<u64>`. Enable the
7/// `sparse-bitset` feature for a chunked sparse representation suitable for
8/// very large, sparse graphs.
9trait ReachRow {
10    /// Create a row able to track `n` bits.
11    fn with_size(n: usize) -> Self;
12    /// Set bit `i`.
13    fn set(&mut self, i: usize);
14    /// Read bit `i`.
15    fn get(&self, i: usize) -> bool;
16    /// Bitwise OR assignment with `other`.
17    fn or_assign_from(&mut self, other: &Self);
18}
19
20/// Dense bitset implementation using `Vec<u64>` words.
21#[derive(Clone)]
22struct DenseRow {
23    words: Vec<u64>,
24}
25
26impl ReachRow for DenseRow {
27    #[inline]
28    fn with_size(n: usize) -> Self {
29        Self {
30            words: vec![0; n.div_ceil(64)],
31        }
32    }
33    #[inline]
34    fn set(&mut self, i: usize) {
35        self.words[i / 64] |= 1u64 << (i % 64);
36    }
37    #[inline]
38    fn get(&self, i: usize) -> bool {
39        (self.words[i / 64] >> (i % 64)) & 1 == 1
40    }
41    #[inline]
42    fn or_assign_from(&mut self, other: &Self) {
43        for (a, b) in self.words.iter_mut().zip(&other.words) {
44            *a |= *b;
45        }
46    }
47}
48
49#[cfg(feature = "sparse-bitset")]
50mod sparse {
51    use super::ReachRow;
52    use std::collections::BTreeMap;
53
54    /// Sparse bitset chunked by 64-bit words.
55    pub struct SparseRow {
56        chunks: BTreeMap<usize, u64>,
57    }
58
59    impl ReachRow for SparseRow {
60        fn with_size(_n: usize) -> Self {
61            Self {
62                chunks: BTreeMap::new(),
63            }
64        }
65        #[inline]
66        fn set(&mut self, i: usize) {
67            let w = i / 64;
68            let b = 1u64 << (i % 64);
69            *self.chunks.entry(w).or_insert(0) |= b;
70        }
71        #[inline]
72        fn get(&self, i: usize) -> bool {
73            let w = i / 64;
74            let b = 1u64 << (i % 64);
75            self.chunks.get(&w).map_or(0, |&x| x) & b != 0
76        }
77        fn or_assign_from(&mut self, other: &Self) {
78            for (&w, &bits) in &other.chunks {
79                *self.chunks.entry(w).or_insert(0) |= bits;
80            }
81        }
82    }
83}
84
85#[cfg(feature = "sparse-bitset")]
86type Row = sparse::SparseRow;
87#[cfg(not(feature = "sparse-bitset"))]
88type Row = DenseRow;
89
90#[inline]
91#[cfg(any(
92    debug_assertions,
93    feature = "strict-invariants",
94    feature = "check-invariants"
95))]
96fn is_acyclic_by_chart<S>(s: &S, chart: &[S::Point]) -> bool
97where
98    S: Sieve + SieveRef,
99{
100    use std::collections::HashMap;
101    let mut idx = HashMap::with_capacity(chart.len());
102    for (i, &p) in chart.iter().enumerate() {
103        idx.insert(p, i);
104    }
105    for &u in chart {
106        let ui = idx[&u];
107        for (v, _) in s.cone_ref(u) {
108            if idx[&v] <= ui {
109                return false;
110            }
111        }
112    }
113    true
114}
115
116/// Remove all transitive edges in a **DAG**. Returns number of removed edges.
117///
118/// # Preconditions
119/// - `s` must be acyclic (DAG). We rely on [`chart_points`](Sieve::chart_points)
120///   for a topological order and return `Err(MeshSieveError::CycleDetected)` on
121///   cycles.
122///
123/// # Complexity (dense bitset)
124/// - Time: ~`O(E + V * (V/64) + Σ_u deg(u)^2 / W)` where `W = 64`.
125/// - Memory: `O(V * ⌈V/64⌉)` words. Enable the `sparse-bitset` feature for a
126///   memory-saving sparse representation.
127pub fn transitive_reduction_dag<S>(s: &mut S) -> Result<usize, MeshSieveError>
128where
129    S: Sieve + SieveRef,
130    S::Point: Copy + Eq + std::hash::Hash + Ord + std::fmt::Debug,
131{
132    use std::collections::HashMap;
133    let chart = s.chart_points()?;
134    #[cfg(any(
135        debug_assertions,
136        feature = "strict-invariants",
137        feature = "check-invariants"
138    ))]
139    debug_assert!(is_acyclic_by_chart(s, &chart), "chart must be acyclic");
140    let n = chart.len();
141    let mut idx = HashMap::with_capacity(n);
142    for (i, &p) in chart.iter().enumerate() {
143        idx.insert(p, i);
144    }
145    let mut reach: Vec<Row> = (0..n).map(|_| Row::with_size(n)).collect();
146    for &u in chart.iter().rev() {
147        let ui = idx[&u];
148        for (v, _) in s.cone_ref(u) {
149            let vi = idx[&v];
150            // vi > ui in a topological order
151            let (row_u, row_v) = {
152                let (pre, suf) = reach.split_at_mut(vi);
153                (&mut pre[ui], &suf[0])
154            };
155            row_u.or_assign_from(row_v);
156            row_u.set(vi);
157        }
158    }
159    let mut to_remove = Vec::new();
160    for &u in &chart {
161        let mut neigh: Vec<_> = SieveRef::cone_points(s, u).collect();
162        neigh.sort_unstable();
163        neigh.dedup();
164        for &v in &neigh {
165            let vi = idx[&v];
166            let implied = neigh
167                .iter()
168                .copied()
169                .any(|w| w != v && reach[idx[&w]].get(vi));
170            if implied {
171                to_remove.push((u, v));
172            }
173        }
174    }
175    to_remove.sort_unstable_by_key(|&(u, v)| (u, v));
176    for (u, v) in &to_remove {
177        let _ = s.remove_arrow(*u, *v);
178    }
179    Ok(to_remove.len())
180}
181
182/// Compute missing transitive-closure edges of a DAG (`u ⇒ v` without a direct edge`).
183/// Does not modify the sieve.
184///
185/// # Preconditions
186/// - `s` must be acyclic (DAG); cycles yield `Err(MeshSieveError::CycleDetected)`.
187///
188/// # Complexity (dense bitset)
189/// - Time: ~`O(E + V * (V/64))` to build reachability plus membership checks.
190/// - Memory: `O(V * ⌈V/64⌉)` words.
191///
192/// Returned edge order is deterministic, following the chart order.
193pub fn transitive_closure_edges<S>(s: &mut S) -> Result<Vec<(S::Point, S::Point)>, MeshSieveError>
194where
195    S: Sieve + SieveRef,
196    S::Point: Copy + Eq + std::hash::Hash + Ord + std::fmt::Debug,
197{
198    use std::collections::{HashMap, HashSet};
199    let chart = s.chart_points()?;
200    #[cfg(any(
201        debug_assertions,
202        feature = "strict-invariants",
203        feature = "check-invariants"
204    ))]
205    debug_assert!(is_acyclic_by_chart(s, &chart), "chart must be acyclic");
206    let n = chart.len();
207    let mut idx = HashMap::with_capacity(n);
208    for (i, &p) in chart.iter().enumerate() {
209        idx.insert(p, i);
210    }
211    let mut reach: Vec<Row> = (0..n).map(|_| Row::with_size(n)).collect();
212    let mut direct = HashSet::new();
213    for &u in chart.iter().rev() {
214        let ui = idx[&u];
215        let mut neigh: Vec<_> = s.cone_ref(u).map(|(v, _)| idx[&v]).collect();
216        neigh.sort_unstable();
217        neigh.dedup();
218        for &vi in &neigh {
219            direct.insert((ui, vi));
220            let (row_u, row_v) = {
221                let (pre, suf) = reach.split_at_mut(vi);
222                (&mut pre[ui], &suf[0])
223            };
224            row_u.or_assign_from(row_v);
225            row_u.set(vi);
226        }
227    }
228    let mut out = Vec::new();
229    for (ui, &u) in chart.iter().enumerate() {
230        for vi in 0..n {
231            if ui == vi {
232                continue;
233            }
234            if reach[ui].get(vi) && !direct.contains(&(ui, vi)) {
235                out.push((u, chart[vi]));
236            }
237        }
238    }
239    Ok(out)
240}
241
242/// Summary statistics from [`transitive_reduction_dag_stats`].
243#[derive(Debug, Clone, Copy, Eq, PartialEq)]
244pub struct ReductionStats {
245    pub removed: usize,
246    pub remaining: usize,
247}
248
249/// Perform [`transitive_reduction_dag`] and report removed/remaining edge counts.
250pub fn transitive_reduction_dag_stats<S>(s: &mut S) -> Result<ReductionStats, MeshSieveError>
251where
252    S: Sieve + SieveRef,
253    S::Point: Copy + Eq + std::hash::Hash + Ord + std::fmt::Debug,
254{
255    fn arrow_count<S2>(s: &S2) -> usize
256    where
257        S2: Sieve + SieveRef,
258        S2::Point: Copy + Eq + std::hash::Hash + Ord + std::fmt::Debug,
259    {
260        s.base_points()
261            .map(|u| SieveRef::cone_points(s, u).count())
262            .sum()
263    }
264
265    let before = arrow_count(s);
266    let removed = transitive_reduction_dag(s)?;
267    let after = arrow_count(s);
268    debug_assert_eq!(before - removed, after);
269    Ok(ReductionStats {
270        removed,
271        remaining: after,
272    })
273}