Skip to main content

holos_tda/collapse/
model.rs

1use crate::SparseDistanceMatrix;
2
3/// The downstream work an adaptive collapse schedule targets.
4///
5/// In each pass, `H1` favors removals that destroy more triangles. `H2`
6/// first favors removals that destroy more tetrahedra, then uses the
7/// triangle count as a tie breaker. Each planned removal is tested again
8/// against the current graph. The score guides the order only. Every
9/// removal passes the same filtration-wide predicate.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum CollapseObjective {
13    /// Target the cofacets used most directly by an H1 computation.
14    H1,
15    /// Target H2 cofacets, then H1 cofacets.
16    H2,
17}
18
19/// Why a collapse run stopped.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum CollapseCompleteness {
23    /// The output has no edge that passes the collapse predicate.
24    CompleteFixedPoint,
25    /// The declared work limit stopped the run before a fixed-point check.
26    BudgetLimited,
27}
28
29/// Parameters for the adaptive version 3 collapse schedule.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[non_exhaustive]
32pub struct AdaptiveCollapseParams {
33    /// Downstream work used to rank removable edges.
34    pub objective: CollapseObjective,
35    /// Maximum removability tests. `None` runs to a fixed point.
36    ///
37    /// One work unit is one complete evaluation of the filtration-wide
38    /// edge predicate, including score construction when the edge is
39    /// removable. A run never starts a test after it consumes this limit.
40    pub work_limit: Option<u64>,
41}
42
43impl Default for AdaptiveCollapseParams {
44    fn default() -> Self {
45        Self {
46            objective: CollapseObjective::H2,
47            work_limit: None,
48        }
49    }
50}
51
52impl AdaptiveCollapseParams {
53    /// Run the adaptive schedule to a fixed point with `objective`.
54    pub fn new(objective: CollapseObjective) -> Self {
55        Self {
56            objective,
57            work_limit: None,
58        }
59    }
60
61    /// Stop before starting a predicate evaluation beyond `work_limit`.
62    pub fn with_work_limit(mut self, work_limit: u64) -> Self {
63        self.work_limit = Some(work_limit);
64        self
65    }
66}
67
68/// Where one removal sits in its schedule.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70#[non_exhaustive]
71pub enum SchedulePosition {
72    /// A serial or ordered version 1 pass.
73    Pass(usize),
74    /// A rounds-schedule version 2 round.
75    Round(usize),
76    /// An unstructured version 3 removal sequence.
77    Sequence(usize),
78}
79
80impl SchedulePosition {
81    /// The 1-based pass, round, or sequence number.
82    pub fn number(self) -> usize {
83        match self {
84            Self::Pass(number) | Self::Round(number) | Self::Sequence(number) => number,
85        }
86    }
87}
88
89/// One removed edge: endpoints, original value, schedule position, and the
90/// piecewise witness function.
91#[derive(Debug, Clone, PartialEq)]
92pub struct RemovalStep {
93    pub(super) u: usize,
94    pub(super) v: usize,
95    pub(super) value: f64,
96    pub(super) position: SchedulePosition,
97    pub(super) witnesses: Vec<(f64, usize)>,
98}
99
100impl RemovalStep {
101    /// Original endpoints, smaller index first.
102    pub fn edge(&self) -> (usize, usize) {
103        (self.u, self.v)
104    }
105
106    /// Original edge value, preserved bit for bit.
107    pub fn value(&self) -> f64 {
108        self.value
109    }
110
111    /// The 1-based schedule position of the removal.
112    pub fn position(&self) -> SchedulePosition {
113        self.position
114    }
115
116    /// Witness segments as `(start value, apex vertex)`. Segment `i` covers
117    /// scales from its start value up to the next segment's start; the last
118    /// segment covers through the terminal level. The first start value is
119    /// the edge value.
120    pub fn witnesses(&self) -> &[(f64, usize)] {
121        &self.witnesses
122    }
123}
124
125/// Replayable record of one collapse run.
126///
127/// The certificate plus the collapsed matrix reconstruct the thresholded
128/// input. The certificate is not a chain map and does not transport
129/// representatives.
130#[derive(Debug, Clone, PartialEq)]
131pub struct CollapseCertificate {
132    pub(super) algorithm_version: u32,
133    pub(super) objective: Option<CollapseObjective>,
134    pub(super) completeness: CollapseCompleteness,
135    pub(super) work_limit: Option<u64>,
136    pub(super) work_used: u64,
137    pub(super) vertex_count: usize,
138    pub(super) requested_threshold: Option<f64>,
139    pub(super) terminal_level: f64,
140    pub(super) input_edge_count: usize,
141    pub(super) output_edge_count: usize,
142    pub(super) steps: Vec<RemovalStep>,
143}
144
145impl CollapseCertificate {
146    /// Version of the collapse scheme that produced this certificate.
147    pub fn algorithm_version(&self) -> u32 {
148        self.algorithm_version
149    }
150
151    /// Downstream objective for an adaptive version 3 run.
152    ///
153    /// Versions 1 and 2 return `None`. Those schedules do not rank
154    /// removals by a downstream-work estimate.
155    pub fn objective(&self) -> Option<CollapseObjective> {
156        self.objective
157    }
158
159    /// Whether the output is a fixed point or a safe partial collapse.
160    pub fn completeness(&self) -> CollapseCompleteness {
161        self.completeness
162    }
163
164    /// Declared adaptive work limit, when the caller set one.
165    pub fn work_limit(&self) -> Option<u64> {
166        self.work_limit
167    }
168
169    /// Adaptive work units consumed by the schedule.
170    ///
171    /// Versions 1 and 2 report zero.
172    pub fn work_used(&self) -> u64 {
173        self.work_used
174    }
175
176    /// Number of vertices in the input.
177    pub fn vertex_count(&self) -> usize {
178        self.vertex_count
179    }
180
181    /// The threshold the caller passed, verbatim.
182    pub fn requested_threshold(&self) -> Option<f64> {
183        self.requested_threshold
184    }
185
186    /// Terminal filtration level: the resolved threshold if finite,
187    /// otherwise the largest finite edge value.
188    pub fn terminal_level(&self) -> f64 {
189        self.terminal_level
190    }
191
192    /// Edges in the thresholded input.
193    pub fn input_edge_count(&self) -> usize {
194        self.input_edge_count
195    }
196
197    /// Edges that survived the collapse.
198    pub fn output_edge_count(&self) -> usize {
199        self.output_edge_count
200    }
201
202    /// The removals, in execution order.
203    pub fn steps(&self) -> &[RemovalStep] {
204        &self.steps
205    }
206}
207
208/// Counters from one collapse run.
209///
210/// The structural fields (`input_edges`, `output_edges`, `removed_edges`,
211/// `epochs`, `witness_segments`, `logical_tests`) are the same at every
212/// worker count and window. The other fields describe one execution:
213/// `edge_tests`, `max_common_neighborhood`, and the fields marked
214/// "ordered schedule only" can differ between ordered runs with different
215/// worker counts or windows.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
217#[non_exhaustive]
218pub struct CollapseStats {
219    /// Edges in the thresholded input.
220    pub input_edges: usize,
221    /// Edges that survived.
222    pub output_edges: usize,
223    /// Edges removed.
224    pub removed_edges: usize,
225    /// Schedule epochs, including the final epoch that removes nothing:
226    /// passes for versions 1 and 3, rounds for version 2. A budget-limited
227    /// version 3 run can stop during its last pass.
228    pub epochs: usize,
229    /// Predicate evaluations, physical calls. On the ordered schedule
230    /// this depends on the worker count and window and can exceed
231    /// `logical_tests`; on the other schedules it equals it.
232    pub edge_tests: usize,
233    /// Witness segments recorded across all removal steps.
234    pub witness_segments: usize,
235    /// Largest common-neighborhood size seen by the predicate. On the
236    /// ordered schedule a discarded speculative test can see a larger
237    /// neighborhood than the serial run tests against, so this field
238    /// depends on the worker count and window.
239    pub max_common_neighborhood: usize,
240    /// Tests of the schedule's own trace. Equals `edge_tests` for the
241    /// serial and rounds schedules; for the ordered schedule it is the
242    /// serial trace's test count, and it does not depend on the worker
243    /// count or window.
244    pub logical_tests: usize,
245    /// Cached speculative results dropped before use, whether a
246    /// conflicting removal or a large-neighborhood bail invalidated them.
247    /// Each dropped result is re-evaluated serially at its turn. Ordered
248    /// schedule only.
249    pub invalidated_results: usize,
250    /// Large-neighborhood marking bails during retirement that dropped at
251    /// least one cached result ahead of them. Ordered schedule only.
252    pub global_invalidations: usize,
253    /// Speculative window stages executed. Ordered schedule only.
254    pub window_batches: usize,
255    /// Window slots offered across all stages: stages times the window
256    /// size. With `window_members_formed` it gives window occupancy.
257    /// Ordered schedule only.
258    pub window_slots_offered: usize,
259    /// Positions collected into windows across all stages. Ordered
260    /// schedule only.
261    pub window_members_formed: usize,
262    /// Cached member verdicts consumed at retirement without a repair.
263    /// Ordered schedule only.
264    pub window_members_reused: usize,
265    /// Score evaluations by the adaptive schedule. This includes pass
266    /// planning and successful retirement tests.
267    pub adaptive_score_evaluations: usize,
268    /// Planned removals considered in score order by the adaptive schedule.
269    pub adaptive_queue_pops: usize,
270    /// Planned removals that failed their retirement test after an earlier
271    /// removal changed the graph.
272    pub adaptive_stale_pops: usize,
273    /// Triangles destroyed by the removals selected by the adaptive
274    /// schedule, counted immediately before each removal.
275    pub adaptive_triangles_removed: u64,
276    /// Tetrahedra destroyed by the removals selected by the adaptive
277    /// schedule, counted immediately before each removal.
278    pub adaptive_tetrahedra_removed: u64,
279}
280
281impl CollapseStats {
282    /// Counters with `input_edges` set and every input edge still an output edge.
283    pub(super) fn new(input_edges: usize) -> Self {
284        CollapseStats {
285            input_edges,
286            output_edges: input_edges,
287            ..Default::default()
288        }
289    }
290}
291
292/// Wall-clock split of one collapse run, in nanoseconds.
293///
294/// Zero on the serial and rounds schedules, which have no speculative
295/// phases to separate. The ordered schedule reports the parallel test
296/// phase, the serial retirement walk, and the repairs inside it. Timings
297/// are diagnostics: they vary between runs and never affect an output
298/// field.
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
300#[non_exhaustive]
301pub struct CollapseTimings {
302    /// Time in the parallel test phase, summed over stages.
303    pub predicate_ns: u64,
304    /// Time in the serial retirement walk, summed over stages. Includes
305    /// the repairs counted in `repair_ns`.
306    pub retirement_ns: u64,
307    /// Time spent re-evaluating invalidated verdicts at their turn.
308    pub repair_ns: u64,
309}
310
311/// Reduced graph, collapse certificate, run counters, and wall-clock timings.
312///
313/// The matrix is the input to [`crate::rips_persistence_sparse`]. The
314/// reduction does not depend on modulus, homology dimension, optimization
315/// toggles, or solver thread count. The schedule does change which edges
316/// survive.
317#[derive(Debug, Clone)]
318#[non_exhaustive]
319pub struct CollapsedRips {
320    /// The reduced graph. Surviving edge values are the input values, bit
321    /// for bit.
322    pub matrix: SparseDistanceMatrix,
323    /// Replayable proof of every removal.
324    pub certificate: CollapseCertificate,
325    /// Run counters.
326    pub stats: CollapseStats,
327    /// Wall-clock split of the run. Diagnostics only.
328    pub timings: CollapseTimings,
329}