Skip to main content

holos_tda/classes/
provenance.rs

1use sha2::{Digest, Sha256};
2
3use crate::{Bar, Cocycle, Error, Result, SparseDistanceMatrix};
4
5use super::canonical::basis_class_id;
6use super::model::{PersistentClass, PersistentClassProvenance};
7
8impl PersistentClassProvenance {
9    pub(crate) fn new(
10        graph: &SparseDistanceMatrix,
11        class_digest: [u8; 32],
12        interval: Bar,
13        cocycle: &Cocycle,
14    ) -> Self {
15        Self {
16            source_graph_digest: source_graph_digest(graph, cocycle.scale),
17            class_digest,
18            interval,
19            modulus: cocycle.modulus,
20            scale: cocycle.scale,
21        }
22    }
23
24    /// Construct a provenance record from previously recorded digests.
25    ///
26    /// `class_digest` must be the canonical [`crate::BasisClassId`] bytes for the
27    /// class that uses this record.
28    pub fn from_parts(
29        source_graph_digest: [u8; 32],
30        class_digest: [u8; 32],
31        interval: Bar,
32        modulus: u32,
33        scale: f64,
34    ) -> Self {
35        Self {
36            source_graph_digest,
37            class_digest,
38            interval,
39            modulus,
40            scale,
41        }
42    }
43
44    /// Check the class identity, interval, and representative against a graph.
45    ///
46    /// The class digest must equal the canonical [`crate::BasisClassId`] bytes for
47    /// the supplied cocycle.
48    pub fn validate(
49        &self,
50        graph: &SparseDistanceMatrix,
51        class_digest: [u8; 32],
52        interval: Bar,
53        cocycle: &Cocycle,
54    ) -> Result<()> {
55        self.validate_metadata(class_digest, interval, cocycle)?;
56        if self.source_graph_digest != source_graph_digest(graph, self.scale) {
57            return Err(Error::InvalidInput(
58                "persistent class belongs to a different active graph".into(),
59            ));
60        }
61        Ok(())
62    }
63
64    pub(crate) fn validate_metadata(
65        &self,
66        class_digest: [u8; 32],
67        interval: Bar,
68        cocycle: &Cocycle,
69    ) -> Result<()> {
70        if self.class_digest != class_digest {
71            return Err(Error::InvalidInput(
72                "persistent class identity differs from its provenance".into(),
73            ));
74        }
75        if !bar_bits_equal(self.interval, interval) {
76            return Err(Error::InvalidInput(
77                "persistent class interval differs from its provenance".into(),
78            ));
79        }
80        if self.modulus != cocycle.modulus {
81            return Err(Error::InvalidInput(
82                "persistent class field differs from its provenance".into(),
83            ));
84        }
85        if self.scale.to_bits() != cocycle.scale.to_bits() {
86            return Err(Error::InvalidInput(
87                "persistent class scale differs from its provenance".into(),
88            ));
89        }
90        if !valid_interval_and_scale(interval, self.scale) {
91            return Err(Error::InvalidInput(
92                "persistent class representative is outside its interval".into(),
93            ));
94        }
95        Ok(())
96    }
97}
98
99impl PersistentClass {
100    /// Source binding for a scalar Rips class, when available.
101    pub fn provenance(&self) -> Option<&PersistentClassProvenance> {
102        self.provenance.as_ref()
103    }
104
105    /// Check that this class belongs to the supplied active graph.
106    pub fn validate_provenance(&self, graph: &SparseDistanceMatrix) -> Result<()> {
107        let provenance = self.provenance.as_ref().ok_or_else(|| {
108            Error::InvalidInput("persistent class has no interval-bound provenance".into())
109        })?;
110        provenance.validate(graph, *self.id.as_bytes(), self.interval, &self.cocycle)?;
111        if basis_class_id(self.group_id, self.basis_index, &self.cocycle) != self.id {
112            return Err(Error::InvalidInput(
113                "persistent class identifier is not canonical".into(),
114            ));
115        }
116        Ok(())
117    }
118}
119
120fn valid_interval_and_scale(interval: Bar, scale: f64) -> bool {
121    interval.dim == 1
122        && valid_nonnegative_finite(interval.birth)
123        && valid_death(interval.death)
124        && (interval.death.is_infinite() || interval.death > interval.birth)
125        && valid_nonnegative_finite(scale)
126        && scale >= interval.birth
127        && (interval.death.is_infinite() || scale < interval.death)
128}
129
130fn valid_nonnegative_finite(value: f64) -> bool {
131    value.is_finite() && value >= 0.0 && !is_negative_zero(value)
132}
133
134fn valid_death(value: f64) -> bool {
135    (value.is_finite() && value >= 0.0 && !is_negative_zero(value))
136        || (value.is_infinite() && value.is_sign_positive())
137}
138
139fn is_negative_zero(value: f64) -> bool {
140    value == 0.0 && value.to_bits() != 0
141}
142
143pub(crate) fn source_graph_digest(graph: &SparseDistanceMatrix, scale: f64) -> [u8; 32] {
144    let active = graph
145        .edges()
146        .filter(|&(_, _, value)| value <= scale)
147        .collect::<Vec<_>>();
148    let mut hash = Sha256::new();
149    hash.update(b"holos-persistent-class-source-v1");
150    hash.update((graph.len() as u64).to_be_bytes());
151    hash.update(scale.to_bits().to_be_bytes());
152    hash.update((active.len() as u64).to_be_bytes());
153    for (u, v, value) in active {
154        hash.update((u as u64).to_be_bytes());
155        hash.update((v as u64).to_be_bytes());
156        hash.update(value.to_bits().to_be_bytes());
157    }
158    hash.finalize().into()
159}
160
161fn bar_bits_equal(a: Bar, b: Bar) -> bool {
162    a.dim == b.dim
163        && a.birth.to_bits() == b.birth.to_bits()
164        && a.death.to_bits() == b.death.to_bits()
165}