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