Skip to main content

molrs_compute/
result.rs

1//! Traits carried by every [`Compute`](crate::Compute) output.
2//!
3//! - [`ComputeResult`] — "finalize into a fully-usable value". Multi-frame
4//!   accumulations can return a not-yet-normalized intermediate value from
5//!   `Compute::compute`; `ComputeResult::finalize` turns it into the
6//!   user-facing final form. [`Graph::run`](crate::Graph) calls it once for
7//!   every node before inserting into the [`Store`](crate::Store).
8//! - [`DescriptorRow`] — "flatten into an `&[F]` row". Used by downstream
9//!   matrix consumers such as PCA and k-means to treat any prior Compute
10//!   output as a descriptor row without an extra conversion step.
11
12use molrs::types::F;
13
14/// Marker + finalization hook for Compute outputs.
15///
16/// `finalize` is called **once** by [`Graph::run`](crate::Graph) on the output
17/// of every node before the value is moved into the [`Store`](crate::Store).
18/// The default is a no-op, which suits outputs already in their final form
19/// (cluster assignments, per-frame observables, etc.). Accumulating outputs
20/// (notably RDF's raw pair histogram) override it to perform their
21/// normalization step.
22///
23/// # Invariant
24///
25/// Implementations **must** be idempotent: calling `finalize` twice must
26/// yield the same state as calling it once. Graph calls it exactly once, but
27/// users may call it again on persisted results.
28pub trait ComputeResult {
29    /// Convert any accumulated intermediate state into the final user-facing form.
30    fn finalize(&mut self) {}
31}
32
33/// Flatten a Compute output into a row of floats.
34///
35/// Consumed by downstream multi-frame analyses (PCA, k-means) that treat a
36/// `Vec<T: DescriptorRow>` as a row-major matrix. Implementations must return
37/// a **consistent** row length across calls on a given type — changing
38/// dimension between rows is a programmer error that the caller checks at
39/// matrix assembly time.
40pub trait DescriptorRow {
41    /// The flat row representation.
42    fn as_row(&self) -> &[F];
43}
44
45/// Per-frame sequences automatically finalize by propagating to each element.
46impl<T: ComputeResult + Clone + Send + Sync + 'static> ComputeResult for Vec<T> {
47    fn finalize(&mut self) {
48        for item in self.iter_mut() {
49            item.finalize();
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[derive(Clone)]
59    struct NoopResult;
60    impl ComputeResult for NoopResult {}
61
62    #[derive(Clone)]
63    struct Counter {
64        raw: f64,
65        norm: f64,
66        finalized: bool,
67    }
68
69    impl ComputeResult for Counter {
70        fn finalize(&mut self) {
71            if !self.finalized {
72                self.norm = self.raw / 10.0;
73                self.finalized = true;
74            }
75        }
76    }
77
78    #[derive(Clone)]
79    struct Row(Vec<F>);
80    impl DescriptorRow for Row {
81        fn as_row(&self) -> &[F] {
82            &self.0
83        }
84    }
85
86    #[test]
87    fn default_finalize_is_noop() {
88        let mut x = NoopResult;
89        x.finalize();
90        x.finalize();
91    }
92
93    #[test]
94    fn finalize_is_idempotent() {
95        let mut c = Counter {
96            raw: 50.0,
97            norm: 0.0,
98            finalized: false,
99        };
100        c.finalize();
101        assert!((c.norm - 5.0).abs() < 1e-12);
102        c.finalize();
103        assert!((c.norm - 5.0).abs() < 1e-12);
104    }
105
106    #[test]
107    fn descriptor_row_as_row() {
108        let r = Row(vec![1.0, 2.0, 3.0]);
109        assert_eq!(r.as_row(), &[1.0, 2.0, 3.0]);
110    }
111}