holos_tda/classes/
provenance.rs1use 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 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 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 pub fn provenance(&self) -> Option<&PersistentClassProvenance> {
102 self.provenance.as_ref()
103 }
104
105 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}