Skip to main content

pe_graph/
matrix_hook.rs

1//! Matrix integration hook — trait for the optional matrix layer.
2//!
3//! pe-graph defines [`MatrixHook`] as the extension point for convergence
4//! tracking and learned routing. pe-matrix provides the implementation;
5//! pe-runtime wires them together. pe-graph never depends on pe-matrix.
6//!
7//! When no hook is provided, the Pregel engine handles `NodeResult::Converge`
8//! by applying the partial_update (degrading to Update) and uses the user's
9//! conditional edge function for routing — unchanged from pre-matrix behavior.
10
11use std::sync::{Arc, Mutex};
12
13/// Hook for the optional matrix layer.
14///
15/// Injected into the Pregel engine by pe-runtime when the matrix layer is
16/// active. The engine calls these methods at the right moments; the hook
17/// implementation does the actual convergence tracking and routing.
18///
19/// `Send + Sync` because the engine may run nodes in parallel.
20///
21/// # Thread safety
22///
23/// The hook is shared across the BSP loop (single-threaded per superstep)
24/// but must be `Send + Sync` to satisfy trait object requirements. Interior
25/// mutability (via Mutex) is expected since the BSP loop is sequential.
26pub trait MatrixHook: Send + Sync {
27    /// Called when a node returns `NodeResult::Converge`.
28    ///
29    /// Records the convergence signal metadata. The engine has already
30    /// extracted and applied the partial_update — this receives only
31    /// the signal values (contribution, surprise, quality) and the
32    /// originating node name.
33    fn on_converge(&self, node_name: &str, actual_contribution: f64, surprise: f64, quality: f64);
34
35    /// Resolve routing for a conditional edge using learned probabilities.
36    ///
37    /// Called instead of the user's router function when the matrix layer
38    /// is active. `from` is the source node, `candidates` are the possible
39    /// target nodes (from the conditional edge's router output).
40    ///
41    /// Returns the selected candidate(s). If this returns `None`, the engine
42    /// falls back to the user's router function (graceful degradation).
43    fn route(&self, from: &str, candidates: &[String]) -> Option<Vec<String>>;
44
45    /// Current C value (aggregate confidence).
46    fn c_value(&self) -> f64;
47
48    /// Current completion estimate.
49    fn completion(&self) -> f64;
50
51    /// Whether convergence threshold has been reached.
52    fn is_converged(&self) -> bool;
53
54    /// Record that a transition occurred (for learning).
55    fn record_transition(&self, from: &str, to: &str, quality: f64);
56}
57
58/// Wrapper around `Arc<dyn MatrixHook>` for ergonomic use in the engine.
59///
60/// Cloneable, shareable, optional.
61#[derive(Clone)]
62pub struct MatrixHookHandle(pub(crate) Arc<dyn MatrixHook>);
63
64impl MatrixHookHandle {
65    /// Create a new handle wrapping a hook implementation.
66    pub fn new(hook: impl MatrixHook + 'static) -> Self {
67        Self(Arc::new(hook))
68    }
69
70    /// Delegate to the inner hook.
71    pub fn on_converge(
72        &self,
73        node_name: &str,
74        actual_contribution: f64,
75        surprise: f64,
76        quality: f64,
77    ) {
78        self.0
79            .on_converge(node_name, actual_contribution, surprise, quality);
80    }
81
82    /// Delegate routing to the inner hook.
83    pub fn route(&self, from: &str, candidates: &[String]) -> Option<Vec<String>> {
84        self.0.route(from, candidates)
85    }
86
87    /// Current C value.
88    pub fn c_value(&self) -> f64 {
89        self.0.c_value()
90    }
91
92    /// Current completion estimate.
93    pub fn completion(&self) -> f64 {
94        self.0.completion()
95    }
96
97    /// Whether convergence has been reached.
98    pub fn is_converged(&self) -> bool {
99        self.0.is_converged()
100    }
101
102    /// Record a transition observation.
103    pub fn record_transition(&self, from: &str, to: &str, quality: f64) {
104        self.0.record_transition(from, to, quality);
105    }
106}
107
108impl std::fmt::Debug for MatrixHookHandle {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_struct("MatrixHookHandle")
111            .field("c_value", &self.c_value())
112            .field("completion", &self.completion())
113            .field("is_converged", &self.is_converged())
114            .finish()
115    }
116}
117
118/// Concrete implementation that bridges pe-matrix types into the hook trait.
119///
120/// Created by pe-runtime when wiring up the matrix layer. Uses `Mutex`
121/// for interior mutability since the BSP loop is sequential.
122///
123/// # Thread safety assumption
124///
125/// This type assumes **single-threaded sequential access** within each
126/// BSP superstep. The `Mutex` satisfies the `Sync` bound but is never
127/// contended in practice. If a `Mutex::lock()` fails (poisoned), the
128/// hook silently returns defaults — this can only happen if the engine
129/// panics while holding the lock, which the BSP loop prevents.
130pub struct DefaultMatrixHook<T: ConvergenceRecorder, R: RoutingResolver> {
131    tracker: Mutex<T>,
132    router: Mutex<R>,
133}
134
135impl<T: ConvergenceRecorder, R: RoutingResolver> DefaultMatrixHook<T, R> {
136    /// Create a new hook from a tracker and router.
137    pub fn new(tracker: T, router: R) -> Self {
138        Self {
139            tracker: Mutex::new(tracker),
140            router: Mutex::new(router),
141        }
142    }
143}
144
145// Mutex<T>: Sync when T: Send — no extra +Sync bounds needed on T/R.
146impl<T: ConvergenceRecorder, R: RoutingResolver> MatrixHook for DefaultMatrixHook<T, R> {
147    fn on_converge(&self, _node_name: &str, actual_contribution: f64, surprise: f64, quality: f64) {
148        if let Ok(mut t) = self.tracker.lock() {
149            t.record(actual_contribution, surprise, quality);
150        }
151    }
152
153    fn route(&self, from: &str, candidates: &[String]) -> Option<Vec<String>> {
154        self.router.lock().ok()?.resolve(from, candidates)
155    }
156
157    fn c_value(&self) -> f64 {
158        self.tracker.lock().map(|t| t.c_value()).unwrap_or(0.0)
159    }
160
161    fn completion(&self) -> f64 {
162        self.tracker.lock().map(|t| t.completion()).unwrap_or(0.0)
163    }
164
165    fn is_converged(&self) -> bool {
166        self.tracker
167            .lock()
168            .map(|t| t.is_converged())
169            .unwrap_or(false)
170    }
171
172    fn record_transition(&self, from: &str, to: &str, quality: f64) {
173        if let Ok(mut r) = self.router.lock() {
174            r.learn(from, to, quality);
175        }
176    }
177}
178
179/// Trait for convergence tracking — implemented by pe-matrix ConvergenceTracker.
180pub trait ConvergenceRecorder: Send {
181    /// Record a convergence observation.
182    fn record(&mut self, actual_contribution: f64, surprise: f64, quality: f64);
183    /// Current C value.
184    fn c_value(&self) -> f64;
185    /// Current completion estimate.
186    fn completion(&self) -> f64;
187    /// Whether convergence threshold is met.
188    fn is_converged(&self) -> bool;
189}
190
191/// Trait for routing resolution — implemented by pe-matrix MatrixRouter.
192pub trait RoutingResolver: Send {
193    /// Select candidates using learned routing. Returns None to fall back.
194    fn resolve(&self, from: &str, candidates: &[String]) -> Option<Vec<String>>;
195    /// Record a transition for learning.
196    fn learn(&mut self, from: &str, to: &str, quality: f64);
197}