Skip to main content

cliffy_protocols/
delta.rs

1//! State delta computation for efficient synchronization
2//!
3//! This module provides geometric delta computation between states,
4//! enabling bandwidth-efficient synchronization in distributed systems.
5//!
6//! # Key Concepts
7//!
8//! - **Delta**: The minimal transformation to go from one state to another
9//! - **Compression**: Represent deltas in log space for smaller payloads
10//! - **Batching**: Combine multiple deltas into single compound transformations
11//!
12//! # Example
13//!
14//! ```rust
15//! use cliffy_protocols::delta::{compute_delta, apply_additive_delta};
16//! use cliffy_core::GA3;
17//!
18//! let from = GA3::scalar(1.0);
19//! let to = GA3::scalar(5.0);
20//!
21//! // Compute the delta between states
22//! let delta = compute_delta(&from, &to);
23//!
24//! // Apply delta to reconstruct target state
25//! let mut state = from.clone();
26//! apply_additive_delta(&mut state, &delta);
27//!
28//! assert!((state.get(0) - to.get(0)).abs() < 1e-10);
29//! ```
30
31use crate::serde_ga3;
32use crate::VectorClock;
33use cliffy_core::GA3;
34use serde::{Deserialize, Serialize};
35use uuid::Uuid;
36
37/// A state delta representing the transformation between two states.
38///
39/// Deltas can be represented in different forms for efficiency:
40/// - `Additive`: Simple difference (to - from)
41/// - `Multiplicative`: Versor/rotor transformation
42/// - `Compressed`: Log-space representation for smaller payloads
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct StateDelta {
45    /// The delta transformation
46    #[serde(with = "serde_ga3")]
47    pub transform: GA3,
48    /// Type of delta encoding
49    pub encoding: DeltaEncoding,
50    /// Source state clock (for causal ordering)
51    pub from_clock: VectorClock,
52    /// Target state clock
53    pub to_clock: VectorClock,
54    /// Node that computed this delta
55    pub source_node: Uuid,
56}
57
58/// How the delta is encoded
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60pub enum DeltaEncoding {
61    /// Simple additive delta: result = state + delta
62    Additive,
63    /// Multiplicative (sandwich) delta: result = delta * state * reverse(delta)
64    Multiplicative,
65    /// Compressed log-space: result = exp(delta) * state
66    Compressed,
67}
68
69impl StateDelta {
70    /// Create a new additive delta.
71    pub fn additive(
72        transform: GA3,
73        from_clock: VectorClock,
74        to_clock: VectorClock,
75        source_node: Uuid,
76    ) -> Self {
77        Self {
78            transform,
79            encoding: DeltaEncoding::Additive,
80            from_clock,
81            to_clock,
82            source_node,
83        }
84    }
85
86    /// Create a new multiplicative (versor) delta.
87    pub fn multiplicative(
88        transform: GA3,
89        from_clock: VectorClock,
90        to_clock: VectorClock,
91        source_node: Uuid,
92    ) -> Self {
93        Self {
94            transform,
95            encoding: DeltaEncoding::Multiplicative,
96            from_clock,
97            to_clock,
98            source_node,
99        }
100    }
101
102    /// Create a compressed (log-space) delta.
103    pub fn compressed(
104        transform: GA3,
105        from_clock: VectorClock,
106        to_clock: VectorClock,
107        source_node: Uuid,
108    ) -> Self {
109        Self {
110            transform,
111            encoding: DeltaEncoding::Compressed,
112            from_clock,
113            to_clock,
114            source_node,
115        }
116    }
117
118    /// Get the approximate size of this delta in bytes (for bandwidth estimation).
119    pub fn estimated_size(&self) -> usize {
120        // 8 coefficients * 8 bytes each + overhead
121        8 * 8 + 32
122    }
123
124    /// Check if this delta is causally applicable to a state with the given clock.
125    pub fn is_applicable_to(&self, state_clock: &VectorClock) -> bool {
126        // Delta is applicable if state_clock >= from_clock
127        self.from_clock.happens_before(state_clock) || self.from_clock == *state_clock
128    }
129}
130
131/// Compute the delta between two states.
132///
133/// Returns an additive delta by default. Use `compute_delta_compressed`
134/// for log-space representation.
135pub fn compute_delta(from: &GA3, to: &GA3) -> GA3 {
136    to - from
137}
138
139/// Compute a compressed (log-space) delta.
140///
141/// This representation is more compact for states that differ by
142/// multiplicative factors rather than additive differences.
143pub fn compute_delta_compressed(from: &GA3, to: &GA3) -> GA3 {
144    // For compressed representation, we compute log(to) - log(from)
145    // which can be applied as exp(delta) * from
146    //
147    // For now, we use a simplified version that works well for
148    // scalar-dominated multivectors
149    let from_mag = from.magnitude();
150    let to_mag = to.magnitude();
151
152    if from_mag < 1e-10 {
153        // Can't compute log of zero - fall back to additive
154        return to - from;
155    }
156
157    // Compute the ratio and take log
158    let ratio = to_mag / from_mag;
159    let log_ratio = ratio.ln();
160
161    // Return as scalar multivector (simplified)
162    GA3::scalar(log_ratio)
163}
164
165/// Apply a delta to a state, modifying it in place.
166pub fn apply_delta(state: &mut GA3, delta: &StateDelta) {
167    match delta.encoding {
168        DeltaEncoding::Additive => {
169            *state = &*state + &delta.transform;
170        }
171        DeltaEncoding::Multiplicative => {
172            // Sandwich product: delta * state * reverse(delta)
173            let rev = delta.transform.reverse();
174            *state = delta
175                .transform
176                .geometric_product(state)
177                .geometric_product(&rev);
178        }
179        DeltaEncoding::Compressed => {
180            // Exponential application: exp(delta) * state
181            let exp_delta = delta.transform.exp();
182            *state = exp_delta.geometric_product(state);
183        }
184    }
185}
186
187/// Apply a raw additive delta to a state.
188pub fn apply_additive_delta(state: &mut GA3, delta: &GA3) {
189    *state = &*state + delta;
190}
191
192/// A batch of deltas that can be applied together.
193///
194/// Batching reduces network overhead when multiple deltas need to
195/// be transmitted.
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct DeltaBatch {
198    /// The deltas in this batch, in causal order
199    pub deltas: Vec<StateDelta>,
200    /// Combined clock covering all deltas
201    pub combined_clock: VectorClock,
202}
203
204impl DeltaBatch {
205    /// Create a new empty batch.
206    pub fn new() -> Self {
207        Self {
208            deltas: Vec::new(),
209            combined_clock: VectorClock::new(),
210        }
211    }
212
213    /// Add a delta to the batch.
214    pub fn push(&mut self, delta: StateDelta) {
215        self.combined_clock.update(&delta.to_clock);
216        self.deltas.push(delta);
217    }
218
219    /// Check if the batch is empty.
220    pub fn is_empty(&self) -> bool {
221        self.deltas.is_empty()
222    }
223
224    /// Get the number of deltas in the batch.
225    pub fn len(&self) -> usize {
226        self.deltas.len()
227    }
228
229    /// Combine all additive deltas into a single delta.
230    ///
231    /// This only works for additive deltas; mixed batches are not combined.
232    pub fn combine_additive(&self) -> Option<GA3> {
233        if self.deltas.is_empty() {
234            return None;
235        }
236
237        // Check all deltas are additive
238        if !self
239            .deltas
240            .iter()
241            .all(|d| d.encoding == DeltaEncoding::Additive)
242        {
243            return None;
244        }
245
246        // Sum all transforms
247        let combined = self
248            .deltas
249            .iter()
250            .fold(GA3::zero(), |acc, d| &acc + &d.transform);
251
252        Some(combined)
253    }
254
255    /// Apply all deltas in the batch to a state.
256    pub fn apply_to(&self, state: &mut GA3) {
257        for delta in &self.deltas {
258            apply_delta(state, delta);
259        }
260    }
261
262    /// Get the estimated total size in bytes.
263    pub fn estimated_size(&self) -> usize {
264        self.deltas.iter().map(|d| d.estimated_size()).sum()
265    }
266}
267
268impl Default for DeltaBatch {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274/// Compute the size savings from using deltas vs full state sync.
275///
276/// Returns (delta_size, full_size, savings_ratio).
277pub fn compute_savings(delta: &StateDelta, _full_state: &GA3) -> (usize, usize, f64) {
278    let delta_size = delta.estimated_size();
279    let full_size = 8 * 8; // 8 coefficients * 8 bytes
280
281    let savings = if full_size > 0 {
282        1.0 - (delta_size as f64 / full_size as f64)
283    } else {
284        0.0
285    };
286
287    (delta_size, full_size, savings)
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_compute_additive_delta() {
296        let from = GA3::scalar(3.0);
297        let to = GA3::scalar(7.0);
298
299        let delta = compute_delta(&from, &to);
300
301        // Delta should be 4.0
302        assert!((delta.get(0) - 4.0).abs() < 1e-10);
303    }
304
305    #[test]
306    fn test_apply_additive_delta() {
307        let from = GA3::scalar(3.0);
308        let to = GA3::scalar(7.0);
309
310        let delta_transform = compute_delta(&from, &to);
311        let delta = StateDelta::additive(
312            delta_transform,
313            VectorClock::new(),
314            VectorClock::new(),
315            Uuid::new_v4(),
316        );
317
318        let mut state = from.clone();
319        apply_delta(&mut state, &delta);
320
321        assert!((state.get(0) - 7.0).abs() < 1e-10);
322    }
323
324    #[test]
325    fn test_apply_multiplicative_delta() {
326        // Create a rotor-like delta (simplified: scalar 2.0 for doubling)
327        let delta = StateDelta::multiplicative(
328            GA3::scalar(2.0),
329            VectorClock::new(),
330            VectorClock::new(),
331            Uuid::new_v4(),
332        );
333
334        let mut state = GA3::scalar(3.0);
335        apply_delta(&mut state, &delta);
336
337        // sandwich(2, 3, 2) = 2 * 3 * 2 = 12
338        assert!((state.get(0) - 12.0).abs() < 1e-10);
339    }
340
341    #[test]
342    fn test_apply_compressed_delta() {
343        let delta = StateDelta::compressed(
344            GA3::scalar(1.0), // e^1 ≈ 2.718
345            VectorClock::new(),
346            VectorClock::new(),
347            Uuid::new_v4(),
348        );
349
350        let mut state = GA3::scalar(1.0);
351        apply_delta(&mut state, &delta);
352
353        // exp(1) * 1 ≈ 2.718
354        let expected = std::f64::consts::E;
355        assert!((state.get(0) - expected).abs() < 1e-10);
356    }
357
358    #[test]
359    fn test_delta_batch_combine() {
360        let mut batch = DeltaBatch::new();
361
362        batch.push(StateDelta::additive(
363            GA3::scalar(1.0),
364            VectorClock::new(),
365            VectorClock::new(),
366            Uuid::new_v4(),
367        ));
368
369        batch.push(StateDelta::additive(
370            GA3::scalar(2.0),
371            VectorClock::new(),
372            VectorClock::new(),
373            Uuid::new_v4(),
374        ));
375
376        batch.push(StateDelta::additive(
377            GA3::scalar(3.0),
378            VectorClock::new(),
379            VectorClock::new(),
380            Uuid::new_v4(),
381        ));
382
383        let combined = batch.combine_additive().unwrap();
384
385        // 1 + 2 + 3 = 6
386        assert!((combined.get(0) - 6.0).abs() < 1e-10);
387    }
388
389    #[test]
390    fn test_delta_batch_apply() {
391        let mut batch = DeltaBatch::new();
392
393        batch.push(StateDelta::additive(
394            GA3::scalar(1.0),
395            VectorClock::new(),
396            VectorClock::new(),
397            Uuid::new_v4(),
398        ));
399
400        batch.push(StateDelta::additive(
401            GA3::scalar(2.0),
402            VectorClock::new(),
403            VectorClock::new(),
404            Uuid::new_v4(),
405        ));
406
407        let mut state = GA3::scalar(10.0);
408        batch.apply_to(&mut state);
409
410        // 10 + 1 + 2 = 13
411        assert!((state.get(0) - 13.0).abs() < 1e-10);
412    }
413
414    #[test]
415    fn test_delta_applicability() {
416        let mut from_clock = VectorClock::new();
417        let node = Uuid::new_v4();
418        from_clock.tick(node);
419
420        let mut to_clock = from_clock.clone();
421        to_clock.tick(node);
422
423        let delta = StateDelta::additive(GA3::scalar(1.0), from_clock.clone(), to_clock, node);
424
425        // Delta should be applicable to state with from_clock
426        assert!(delta.is_applicable_to(&from_clock));
427
428        // Delta should be applicable to state with higher clock
429        let mut higher_clock = from_clock.clone();
430        higher_clock.tick(node);
431        assert!(delta.is_applicable_to(&higher_clock));
432    }
433
434    #[test]
435    fn test_compute_delta_compressed() {
436        let from = GA3::scalar(2.0);
437        let to = GA3::scalar(8.0);
438
439        let delta = compute_delta_compressed(&from, &to);
440
441        // ln(8/2) = ln(4) ≈ 1.386
442        let expected = (8.0_f64 / 2.0_f64).ln();
443        assert!((delta.get(0) - expected).abs() < 1e-10);
444    }
445
446    #[test]
447    fn test_delta_encoding_equality() {
448        assert_eq!(DeltaEncoding::Additive, DeltaEncoding::Additive);
449        assert_ne!(DeltaEncoding::Additive, DeltaEncoding::Multiplicative);
450    }
451}