1use super::{RunContext, Runner};
7use crate::executor::{Context, RunMode};
8
9use somatize_compiler::ExecutionPlan;
10use somatize_core::error::{Result, SomaError};
11use somatize_core::keys::{GRAPH_INPUT, input_key};
12use somatize_core::value::Value;
13use std::collections::HashMap;
14
15pub struct LocalRunner;
17
18impl LocalRunner {
19 fn walk(
27 &self,
28 plan: &ExecutionPlan,
29 ctx: &RunContext<'_>,
30 input: &Value,
31 mode: RunMode,
32 ) -> Result<Context> {
33 let mut exec = Context::new(ctx.events.clone(), ctx.run_id)
34 .with_graph_info(ctx.graph_info.clone())
35 .with_seed(ctx.seed);
36 exec.mode = mode;
37 exec.driver = ctx.driver();
38
39 if let Some(first) = plan.node_ids().first() {
40 exec.set(input_key(first), input.clone());
41 }
42 exec.set(GRAPH_INPUT, input.clone());
43
44 crate::executor::execute(plan, &mut exec, ctx.catalog, ctx.cache)?;
45 Ok(exec)
46 }
47
48 fn last_output(exec: &Context) -> Option<Value> {
52 exec.execution_order()
53 .iter()
54 .rev()
55 .find(|id| !somatize_core::keys::is_reserved(id))
56 .and_then(|id| exec.get(id).cloned())
57 }
58}
59
60impl Runner for LocalRunner {
61 fn fit(
62 &self,
63 plan: &ExecutionPlan,
64 ctx: &RunContext<'_>,
65 input: &Value,
66 y: Option<&Value>,
67 ) -> Result<(Value, HashMap<String, Value>)> {
68 let exec = self.walk(plan, ctx, input, RunMode::Fit { y: y.cloned() })?;
69
70 let last = Self::last_output(&exec).unwrap_or(Value::Empty);
74 let mut produced = exec.into_outputs();
75 produced.retain(|id, _| !somatize_core::keys::is_input_key(id));
77
78 Ok((last, produced))
79 }
80
81 fn forward(&self, plan: &ExecutionPlan, ctx: &RunContext<'_>, input: &Value) -> Result<Value> {
82 let exec = self.walk(plan, ctx, input, RunMode::Forward)?;
83 Self::last_output(&exec).ok_or_else(|| SomaError::Other("no output produced".into()))
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::EventBus;
91 use crate::cache::MemoryCache;
92 use crate::executor::GraphInfo;
93 use crate::node_catalog::NodeCatalog;
94 use somatize_core::cache::{CacheKey, CacheStore};
95 use somatize_core::filter::{Filter, FilterKind, FilterMeta, StreamMode};
96 use std::sync::Arc;
97 use std::sync::atomic::{AtomicUsize, Ordering};
98
99 struct CountingFitFilter {
101 fits: Arc<AtomicUsize>,
102 }
103
104 impl Filter for CountingFitFilter {
105 fn config_hash(&self) -> CacheKey {
106 CacheKey::from_parts(&[b"CountingFit"])
107 }
108 fn fit(&self, _x: &Value, y: Option<&Value>) -> Result<Value> {
109 self.fits.fetch_add(1, Ordering::SeqCst);
110 Ok(y.cloned().unwrap_or(Value::Empty))
111 }
112 fn forward(&self, x: &Value, _state: &Value) -> Result<Value> {
113 Ok(x.clone())
114 }
115 fn meta(&self) -> FilterMeta {
116 FilterMeta {
117 name: "CountingFit".into(),
118 kind: FilterKind::Trainable,
119 cacheable: true,
120 differentiable: false,
121 deterministic: true,
122 stream_mode: StreamMode::FixedState,
123 distribution: somatize_core::filter::Distribution::Local,
124 input_schema: None,
125 output_schema: None,
126 }
127 }
128 }
129
130 #[test]
131 fn state_cache_key_is_sensitive_to_labels() {
132 let fits = Arc::new(AtomicUsize::new(0));
133 let mut filters = NodeCatalog::new();
134 filters.register("clf", Box::new(CountingFitFilter { fits: fits.clone() }));
135
136 let cache = MemoryCache::default();
137 let bus = Arc::new(EventBus::new(64));
138 let plan = ExecutionPlan::Execute {
139 node_id: "clf".into(),
140 };
141 let runner = LocalRunner;
142 fn ctx<'a>(
144 filters: &'a NodeCatalog,
145 cache: &'a dyn CacheStore,
146 bus: &'a Arc<EventBus>,
147 ) -> RunContext<'a> {
148 RunContext::new(filters, cache, bus, "test_run", GraphInfo::new())
149 }
150 let x = Value::tensor(vec![1.0, 2.0], vec![2]);
151 let y_a = Value::tensor(vec![0.0, 1.0], vec![2]);
152 let y_b = Value::tensor(vec![1.0, 0.0], vec![2]);
153
154 runner
155 .fit(&plan, &ctx(&filters, &cache, &bus), &x, Some(&y_a))
156 .unwrap();
157 assert_eq!(fits.load(Ordering::SeqCst), 1);
158
159 runner
161 .fit(&plan, &ctx(&filters, &cache, &bus), &x, Some(&y_a))
162 .unwrap();
163 assert_eq!(fits.load(Ordering::SeqCst), 1);
164
165 runner
167 .fit(&plan, &ctx(&filters, &cache, &bus), &x, Some(&y_b))
168 .unwrap();
169 assert_eq!(
170 fits.load(Ordering::SeqCst),
171 2,
172 "different labels must not reuse the cached state"
173 );
174
175 runner
177 .fit(&plan, &ctx(&filters, &cache, &bus), &x, None)
178 .unwrap();
179 assert_eq!(fits.load(Ordering::SeqCst), 3);
180 }
181}