1use crate::{
4 CorrespondenceMode, Error, IndexDeltaProof, IndexProofError, IndexSnapshotProof,
5 IndexTransition, PersistenceIndex, Result, SparseDistanceMatrix, TopologyPatch,
6};
7
8#[derive(Debug, Clone)]
10pub enum IndexStreamProof {
11 Delta(IndexDeltaProof),
13 Snapshot(IndexSnapshotProof),
15}
16
17impl IndexStreamProof {
18 pub fn encode(&self) -> std::result::Result<Vec<u8>, IndexProofError> {
20 match self {
21 Self::Delta(proof) => proof.encode(),
22 Self::Snapshot(proof) => proof.encode(),
23 }
24 }
25
26 pub fn is_snapshot(&self) -> bool {
28 matches!(self, Self::Snapshot(_))
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct IndexStreamStep {
35 pub sequence: u64,
37 pub transition: IndexTransition,
39 pub proof: IndexStreamProof,
41}
42
43#[derive(Debug, Clone)]
45pub struct IndexStream {
46 index: PersistenceIndex,
47 sequence: u64,
48}
49
50impl IndexStream {
51 pub fn new(index: PersistenceIndex) -> Self {
53 Self { index, sequence: 0 }
54 }
55
56 pub fn current(&self) -> &PersistenceIndex {
58 &self.index
59 }
60
61 pub fn sequence(&self) -> u64 {
63 self.sequence
64 }
65
66 pub fn checkpoint(&self) -> std::result::Result<IndexSnapshotProof, IndexProofError> {
68 IndexSnapshotProof::from_index(&self.index)
69 }
70
71 pub fn apply_patch(
73 &mut self,
74 patch: &TopologyPatch,
75 correspondence_mode: CorrespondenceMode,
76 ) -> Result<IndexStreamStep> {
77 let old = self.index.clone();
78 let transition = old.transition_patch_with(patch, correspondence_mode)?;
79 self.commit(old, transition)
80 }
81
82 pub fn apply_graph(
87 &mut self,
88 graph: &SparseDistanceMatrix,
89 correspondence_mode: CorrespondenceMode,
90 ) -> Result<IndexStreamStep> {
91 let old = self.index.clone();
92 let transition = old.transition_with(graph, correspondence_mode)?;
93 self.commit(old, transition)
94 }
95
96 pub fn apply_patches(
100 &mut self,
101 patches: &[TopologyPatch],
102 correspondence_mode: CorrespondenceMode,
103 ) -> Result<Vec<IndexStreamStep>> {
104 let mut candidate = self.clone();
105 let mut steps = Vec::with_capacity(patches.len());
106 for patch in patches {
107 steps.push(candidate.apply_patch(patch, correspondence_mode)?);
108 }
109 *self = candidate;
110 Ok(steps)
111 }
112
113 fn commit(
114 &mut self,
115 old: PersistenceIndex,
116 transition: IndexTransition,
117 ) -> Result<IndexStreamStep> {
118 let proof = if old.topology() == transition.index.topology()
119 && old.graph().len() == transition.index.graph().len()
120 {
121 IndexStreamProof::Delta(
122 IndexDeltaProof::between(&old, &transition.index).map_err(proof_error)?,
123 )
124 } else {
125 IndexStreamProof::Snapshot(
126 IndexSnapshotProof::from_index(&transition.index).map_err(proof_error)?,
127 )
128 };
129 let sequence = self
130 .sequence
131 .checked_add(1)
132 .ok_or_else(|| Error::InvalidInput("index stream sequence overflow".into()))?;
133 self.index = transition.index.clone();
134 self.sequence = sequence;
135 Ok(IndexStreamStep {
136 sequence,
137 transition,
138 proof,
139 })
140 }
141}
142
143fn proof_error(error: IndexProofError) -> Error {
144 Error::InvalidInput(error.to_string())
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::{CertificateLimits, IndexEdit, IndexParams, RipsParams};
151
152 fn graph(weight: f64) -> SparseDistanceMatrix {
153 SparseDistanceMatrix::from_triplets(
154 5,
155 &[
156 (0, 1, weight),
157 (0, 2, 1.0),
158 (1, 2, 1.5),
159 (0, 3, 1.1),
160 (1, 3, 1.6),
161 (0, 4, 1.2),
162 (1, 4, 1.7),
163 ],
164 )
165 .unwrap()
166 }
167
168 #[test]
169 fn stream_emits_warm_and_cold_records() {
170 let mut params = RipsParams::new(1).with_modulus(3);
171 params.threshold = Some(2.0);
172 let index = PersistenceIndex::compile(
173 &graph(0.0),
174 ¶ms,
175 IndexParams::default(),
176 CertificateLimits::default(),
177 )
178 .unwrap();
179 let mut stream = IndexStream::new(index);
180 let patch = TopologyPatch::new(vec![IndexEdit::deactivate(0, 1)]);
181 let warm = stream
182 .apply_patch(&patch, CorrespondenceMode::Omit)
183 .unwrap();
184 assert_eq!(warm.sequence, 1);
185 assert!(!warm.proof.is_snapshot());
186
187 let changed_envelope = SparseDistanceMatrix::from_triplets(5, &[(0, 1, 0.5)]).unwrap();
188 let cold = stream
189 .apply_graph(&changed_envelope, CorrespondenceMode::Omit)
190 .unwrap();
191 assert_eq!(cold.sequence, 2);
192 assert!(cold.proof.is_snapshot());
193 }
194
195 #[test]
196 fn patch_batch_is_atomic() {
197 let mut params = RipsParams::new(1);
198 params.threshold = Some(2.0);
199 let index = PersistenceIndex::compile(
200 &graph(0.0),
201 ¶ms,
202 IndexParams::default(),
203 CertificateLimits::default(),
204 )
205 .unwrap();
206 let mut stream = IndexStream::new(index);
207 let old_root = stream.current().version();
208 let patches = [
209 TopologyPatch::new(vec![IndexEdit::set_weight(0, 2, 1.1)]),
210 TopologyPatch::new(vec![IndexEdit::activate(0, 1, 3.0)]),
211 ];
212 assert!(
213 stream
214 .apply_patches(&patches, CorrespondenceMode::Omit)
215 .is_err()
216 );
217 assert_eq!(stream.sequence(), 0);
218 assert_eq!(stream.current().version(), old_root);
219 }
220}