chronix_analytics/multivariate/
derived.rs1use std::collections::HashMap;
4
5use crate::multivariate::context::MultiSeriesContext;
6use crate::multivariate::error::MultivariateError;
7
8pub trait DerivedSeriesExpr: Send + Sync {
10 fn evaluate(&self, ctx: &MultiSeriesContext) -> Result<Vec<f64>, MultivariateError>;
12}
13
14pub struct DerivedSeriesDefinition {
16 pub name: String,
18 pub expression: Box<dyn DerivedSeriesExpr>,
20 pub dependencies: Vec<String>,
22}
23
24pub struct ArithmeticExpr {
28 pub left: String,
30 pub right: String,
32 pub op: ArithOp,
34}
35
36#[derive(Debug, Clone, Copy)]
38pub enum ArithOp {
39 Add,
41 Sub,
43 Mul,
45 Div,
47}
48
49impl DerivedSeriesExpr for ArithmeticExpr {
50 fn evaluate(&self, ctx: &MultiSeriesContext) -> Result<Vec<f64>, MultivariateError> {
51 let l = ctx
52 .matrix
53 .series_by_name(&self.left)
54 .ok_or_else(|| MultivariateError::SeriesNotFound(self.left.clone()))?;
55 let r = ctx
56 .matrix
57 .series_by_name(&self.right)
58 .ok_or_else(|| MultivariateError::SeriesNotFound(self.right.clone()))?;
59 if l.len() != r.len() {
60 return Err(MultivariateError::DimensionMismatch {
61 expected: l.len(),
62 got: r.len(),
63 });
64 }
65 let result = match self.op {
66 ArithOp::Add => l.iter().zip(r.iter()).map(|(a, b)| a + b).collect(),
67 ArithOp::Sub => l.iter().zip(r.iter()).map(|(a, b)| a - b).collect(),
68 ArithOp::Mul => l.iter().zip(r.iter()).map(|(a, b)| a * b).collect(),
69 ArithOp::Div => l
70 .iter()
71 .zip(r.iter())
72 .map(|(a, b)| if b.abs() > 1e-15 { a / b } else { 0.0 })
73 .collect(),
74 };
75 Ok(result)
76 }
77}
78
79pub struct LogReturnExpr {
81 pub series_name: String,
83}
84
85impl DerivedSeriesExpr for LogReturnExpr {
86 fn evaluate(&self, ctx: &MultiSeriesContext) -> Result<Vec<f64>, MultivariateError> {
87 let s = ctx
88 .matrix
89 .series_by_name(&self.series_name)
90 .ok_or_else(|| MultivariateError::SeriesNotFound(self.series_name.clone()))?;
91 let mut result = Vec::with_capacity(s.len());
92 result.push(0.0); for w in s.windows(2) {
94 if w[0].abs() > 1e-15 {
95 let ratio = w[1] / w[0];
96 if ratio > 0.0 {
97 result.push(ratio.ln());
98 } else {
99 result.push(f64::NAN);
100 }
101 } else {
102 result.push(0.0);
103 }
104 }
105 Ok(result)
106 }
107}
108
109pub struct RollingStatExpr {
111 pub series_name: String,
113 pub window: usize,
115 pub stat: RollingStat,
117}
118
119#[derive(Debug, Clone, Copy)]
121pub enum RollingStat {
122 Mean,
124 Std,
126 Min,
128 Max,
130}
131
132impl DerivedSeriesExpr for RollingStatExpr {
133 fn evaluate(&self, ctx: &MultiSeriesContext) -> Result<Vec<f64>, MultivariateError> {
134 let s = ctx
135 .matrix
136 .series_by_name(&self.series_name)
137 .ok_or_else(|| MultivariateError::SeriesNotFound(self.series_name.clone()))?;
138 let n = s.len();
139 let mut result = Vec::with_capacity(n);
140 let mut sum = 0.0;
141 let mut sum_sq = 0.0;
142 let mut min_deque: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
144 let mut max_deque: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
145
146 for i in 0..n {
147 sum += s[i];
148 sum_sq += s[i] * s[i];
149 if i >= self.window {
150 sum -= s[i - self.window];
151 sum_sq -= s[i - self.window] * s[i - self.window];
152 }
153
154 if i > 0 && i % 1024 == 0 {
156 let start = i.saturating_sub(self.window.saturating_sub(1));
157 sum = s[start..=i].iter().sum();
158 sum_sq = s[start..=i].iter().map(|&v| v * v).sum();
159 }
160
161 let count = (i + 1).min(self.window) as f64;
162 match self.stat {
163 RollingStat::Mean => {
164 result.push(sum / count);
165 }
166 RollingStat::Std => {
167 if count < 2.0 {
168 result.push(0.0);
169 } else {
170 let var = ((sum_sq - sum * sum / count) / (count - 1.0)).max(0.0);
172 result.push(var.sqrt());
173 }
174 }
175 RollingStat::Min => {
176 while min_deque.front().is_some_and(|&f| f + self.window <= i) {
178 min_deque.pop_front();
179 }
180 while min_deque.back().is_some_and(|&b| s[b] >= s[i]) {
182 min_deque.pop_back();
183 }
184 min_deque.push_back(i);
185 result.push(s[min_deque[0]]);
186 }
187 RollingStat::Max => {
188 while max_deque.front().is_some_and(|&f| f + self.window <= i) {
190 max_deque.pop_front();
191 }
192 while max_deque.back().is_some_and(|&b| s[b] <= s[i]) {
194 max_deque.pop_back();
195 }
196 max_deque.push_back(i);
197 result.push(s[max_deque[0]]);
198 }
199 }
200 }
201 Ok(result)
202 }
203}
204
205pub struct LazyDerivedSeries {
212 definition: DerivedSeriesDefinition,
213 cache: parking_lot::Mutex<Option<Vec<f64>>>,
214}
215
216impl LazyDerivedSeries {
217 pub fn new(definition: DerivedSeriesDefinition) -> Self {
219 Self {
220 definition,
221 cache: parking_lot::Mutex::new(None),
222 }
223 }
224
225 pub fn get(&self, ctx: &MultiSeriesContext) -> Result<Vec<f64>, MultivariateError> {
227 let mut cache = self.cache.lock();
228 if let Some(ref cached) = *cache {
229 return Ok(cached.clone());
230 }
231 let result = self.definition.expression.evaluate(ctx)?;
232 *cache = Some(result.clone());
233 Ok(result)
234 }
235
236 pub fn invalidate(&self) {
238 let mut cache = self.cache.lock();
239 *cache = None;
240 }
241
242 pub fn is_cached(&self) -> bool {
244 self.cache.lock().is_some()
245 }
246
247 pub fn definition(&self) -> &DerivedSeriesDefinition {
249 &self.definition
250 }
251}
252
253pub struct DerivedSeriesEngine {
265 definitions: Vec<DerivedSeriesDefinition>,
266}
267
268impl DerivedSeriesEngine {
269 pub fn new(definitions: Vec<DerivedSeriesDefinition>) -> Self {
271 Self { definitions }
272 }
273
274 pub fn evaluate(
280 &self,
281 ctx: &mut MultiSeriesContext,
282 ) -> Result<HashMap<String, Vec<f64>>, MultivariateError> {
283 use rayon::prelude::*;
284
285 let _start = std::time::Instant::now();
286 let levels = self.topological_sort_levels()?;
287 let mut results = HashMap::new();
288
289 for level in &levels {
290 if level.len() == 1 {
291 let def = &self.definitions[level[0]];
293 let vals = def.expression.evaluate(ctx)?;
294 Self::insert_into_context(ctx, &def.name, vals.clone());
295 results.insert(def.name.clone(), vals);
296 } else {
297 let wave_results: Vec<Result<(String, Vec<f64>), MultivariateError>> = level
299 .par_iter()
300 .map(|&idx| {
301 let def = &self.definitions[idx];
302 let vals = def.expression.evaluate(ctx)?;
303 Ok((def.name.clone(), vals))
304 })
305 .collect();
306
307 for result in wave_results {
309 let (name, vals) = result?;
310 Self::insert_into_context(ctx, &name, vals.clone());
311 results.insert(name, vals);
312 }
313 }
314 }
315
316 metrics::histogram!("chronix_derived_series_eval_duration_seconds")
317 .record(_start.elapsed().as_secs_f64());
318 Ok(results)
319 }
320
321 fn insert_into_context(ctx: &mut MultiSeriesContext, name: &str, vals: Vec<f64>) {
323 if let Some(pos) = ctx.matrix.series_ids.iter().position(|n| n == name) {
324 ctx.matrix.data[pos] = vals;
325 } else {
326 ctx.matrix.series_ids.push(name.to_string());
327 ctx.matrix.data.push(vals);
328 }
329 }
330
331 fn topological_sort_levels(&self) -> Result<Vec<Vec<usize>>, MultivariateError> {
336 let n = self.definitions.len();
337 let name_to_idx: HashMap<&str, usize> = self
338 .definitions
339 .iter()
340 .enumerate()
341 .map(|(i, d)| (d.name.as_str(), i))
342 .collect();
343
344 let mut in_degree = vec![0usize; n];
346 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
347 for (i, def) in self.definitions.iter().enumerate() {
348 for dep in &def.dependencies {
349 if let Some(&j) = name_to_idx.get(dep.as_str()) {
350 adj[j].push(i);
351 in_degree[i] += 1;
352 }
353 }
354 }
355
356 let mut current_level: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
358 let mut levels = Vec::new();
359 let mut processed = 0usize;
360
361 while !current_level.is_empty() {
362 let mut next_level = Vec::new();
363 for &node in ¤t_level {
364 processed += 1;
365 for &next in &adj[node] {
366 in_degree[next] -= 1;
367 if in_degree[next] == 0 {
368 next_level.push(next);
369 }
370 }
371 }
372 levels.push(current_level);
373 current_level = next_level;
374 }
375
376 if processed != n {
377 return Err(MultivariateError::CycleDetected);
378 }
379 Ok(levels)
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 fn test_ctx() -> MultiSeriesContext {
388 MultiSeriesContext::build(
389 vec![
390 (
391 "a".to_string(),
392 vec![0, 1_000_000_000, 2_000_000_000],
393 vec![10.0, 20.0, 30.0],
394 ),
395 (
396 "b".to_string(),
397 vec![0, 1_000_000_000, 2_000_000_000],
398 vec![5.0, 10.0, 15.0],
399 ),
400 ],
401 None,
402 )
403 .unwrap()
404 }
405
406 #[test]
407 fn arithmetic_add() {
408 let ctx = test_ctx();
409 let expr = ArithmeticExpr {
410 left: "a".to_string(),
411 right: "b".to_string(),
412 op: ArithOp::Add,
413 };
414 let result = expr.evaluate(&ctx).unwrap();
415 assert!((result[0] - 15.0).abs() < 1e-6);
416 assert!((result[1] - 30.0).abs() < 1e-6);
417 assert!((result[2] - 45.0).abs() < 1e-6);
418 }
419
420 #[test]
421 fn log_return() {
422 let ctx = test_ctx();
423 let expr = LogReturnExpr {
424 series_name: "a".to_string(),
425 };
426 let result = expr.evaluate(&ctx).unwrap();
427 assert!((result[0] - 0.0).abs() < 1e-6);
428 assert!((result[1] - (20.0f64 / 10.0).ln()).abs() < 1e-6);
429 }
430
431 #[test]
432 fn dag_evaluation() {
433 let mut ctx = test_ctx();
434 let defs = vec![DerivedSeriesDefinition {
435 name: "spread".to_string(),
436 expression: Box::new(ArithmeticExpr {
437 left: "a".to_string(),
438 right: "b".to_string(),
439 op: ArithOp::Sub,
440 }),
441 dependencies: vec![],
442 }];
443 let engine = DerivedSeriesEngine::new(defs);
444 let results = engine.evaluate(&mut ctx).unwrap();
445 let spread = &results["spread"];
446 assert!((spread[0] - 5.0).abs() < 1e-6);
447 assert!((spread[1] - 10.0).abs() < 1e-6);
448 }
449
450 #[test]
451 fn cycle_detection() {
452 let defs = vec![
453 DerivedSeriesDefinition {
454 name: "x".to_string(),
455 expression: Box::new(ArithmeticExpr {
456 left: "a".to_string(),
457 right: "y".to_string(),
458 op: ArithOp::Add,
459 }),
460 dependencies: vec!["y".to_string()],
461 },
462 DerivedSeriesDefinition {
463 name: "y".to_string(),
464 expression: Box::new(ArithmeticExpr {
465 left: "a".to_string(),
466 right: "x".to_string(),
467 op: ArithOp::Add,
468 }),
469 dependencies: vec!["x".to_string()],
470 },
471 ];
472 let engine = DerivedSeriesEngine::new(defs);
473 let mut ctx = test_ctx();
474 assert!(engine.evaluate(&mut ctx).is_err());
475 }
476
477 #[test]
478 fn lazy_derived_series_caches_result() {
479 let ctx = test_ctx();
480
481 let def = DerivedSeriesDefinition {
482 name: "double_a".to_string(),
483 expression: Box::new(ArithmeticExpr {
484 left: "a".to_string(),
485 right: "a".to_string(),
486 op: ArithOp::Add,
487 }),
488 dependencies: vec!["a".to_string()],
489 };
490
491 let lazy = LazyDerivedSeries::new(def);
492
493 assert!(!lazy.is_cached());
495
496 let result = lazy.get(&ctx).unwrap();
498 assert_eq!(result, vec![20.0, 40.0, 60.0]);
499 assert!(lazy.is_cached());
500
501 let result2 = lazy.get(&ctx).unwrap();
503 assert_eq!(result2, vec![20.0, 40.0, 60.0]);
504
505 lazy.invalidate();
507 assert!(!lazy.is_cached());
508 }
509}