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}