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}