use crate::multivariate::error::MultivariateError;
#[derive(Debug, Clone)]
pub struct ColumnarMatrix {
pub data: Vec<Vec<f64>>,
pub timestamps: Vec<i64>,
pub series_ids: Vec<String>,
}
impl ColumnarMatrix {
pub fn n_series(&self) -> usize {
self.data.len()
}
pub fn n_timestamps(&self) -> usize {
self.timestamps.len()
}
pub fn series(&self, idx: usize) -> Option<&[f64]> {
self.data.get(idx).map(std::vec::Vec::as_slice)
}
pub fn series_by_name(&self, name: &str) -> Option<&[f64]> {
self.series_ids
.iter()
.position(|s| s == name)
.and_then(|idx| self.series(idx))
}
}
#[derive(Debug, Clone)]
pub struct MultiSeriesContext {
pub matrix: ColumnarMatrix,
}
impl MultiSeriesContext {
pub fn build(
series: Vec<(String, Vec<i64>, Vec<f64>)>,
max_series: Option<usize>,
) -> Result<Self, MultivariateError> {
Self::build_with_max_points(series, max_series, None)
}
#[allow(clippy::needless_pass_by_value)] pub fn build_with_max_points(
series: Vec<(String, Vec<i64>, Vec<f64>)>,
max_series: Option<usize>,
max_points: Option<usize>,
) -> Result<Self, MultivariateError> {
if series.is_empty() {
return Err(MultivariateError::InsufficientData { min: 1, got: 0 });
}
let max = max_series.unwrap_or(100);
if series.len() > max {
return Err(MultivariateError::InvalidParameter(format!(
"too many series: {} > max {}",
series.len(),
max
)));
}
let mut global_min = i64::MAX;
let mut global_max = i64::MIN;
let mut finest_interval = i64::MAX;
for (_, ts, _) in &series {
if ts.is_empty() {
continue;
}
global_min = global_min.min(ts[0]);
if let Some(&last) = ts.last() {
global_max = global_max.max(last);
}
for w in ts.windows(2) {
let delta = w[1] - w[0];
if delta > 0 {
finest_interval = finest_interval.min(delta);
}
}
}
if global_min > global_max {
let mut only_ts = i64::MIN;
for (_, ts, _) in &series {
if let Some(&t) = ts.first() {
only_ts = only_ts.max(t);
}
}
if only_ts == i64::MIN {
return Err(MultivariateError::InsufficientData { min: 1, got: 0 });
}
global_min = only_ts;
global_max = only_ts;
}
if finest_interval == i64::MAX || finest_interval <= 0 {
finest_interval = 1_000_000_000; }
let n_points = ((global_max - global_min) / finest_interval + 1) as usize;
const DEFAULT_MAX_POINTS: usize = 1_000_000;
let cap = max_points.unwrap_or(DEFAULT_MAX_POINTS);
if n_points > cap {
return Err(MultivariateError::InvalidParameter(format!(
"resampled timeline would produce {n_points} points (max {cap}); \
reduce the time range or increase the sampling interval"
)));
}
let timestamps: Vec<i64> = (0..n_points)
.map(|i| global_min + i as i64 * finest_interval)
.collect();
let mut data = Vec::with_capacity(series.len());
let mut ids = Vec::with_capacity(series.len());
for (id, ts, vals) in &series {
ids.push(id.clone());
let aligned = linear_interpolate(ts, vals, ×tamps);
data.push(aligned);
}
Ok(Self {
matrix: ColumnarMatrix {
data,
timestamps,
series_ids: ids,
},
})
}
}
fn linear_interpolate(ts: &[i64], vals: &[f64], target_ts: &[i64]) -> Vec<f64> {
let mut result = Vec::with_capacity(target_ts.len());
if ts.is_empty() || vals.is_empty() {
result.resize(target_ts.len(), f64::NAN);
return result;
}
let mut j = 0usize;
for &t in target_ts {
while j + 1 < ts.len() && ts[j + 1] <= t {
j += 1;
}
if t < ts[0] {
result.push(f64::NAN);
} else if j + 1 >= ts.len() {
if t == ts[ts.len() - 1] {
result.push(vals[ts.len() - 1]);
} else {
result.push(f64::NAN);
}
} else {
let dt = (ts[j + 1] - ts[j]) as f64;
let frac = if dt > 0.0 {
(t - ts[j]) as f64 / dt
} else {
0.0
};
result.push(vals[j] + frac * (vals[j + 1] - vals[j]));
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_aligned_context() {
let s1 = (
"cpu".to_string(),
vec![0, 1_000_000_000, 2_000_000_000],
vec![10.0, 20.0, 30.0],
);
let s2 = (
"mem".to_string(),
vec![0, 2_000_000_000],
vec![100.0, 200.0],
);
let ctx = MultiSeriesContext::build(vec![s1, s2], None).unwrap();
assert_eq!(ctx.matrix.n_series(), 2);
assert!(ctx.matrix.n_timestamps() >= 2);
}
#[test]
fn series_by_name() {
let s1 = ("cpu".to_string(), vec![0, 1_000_000_000], vec![10.0, 20.0]);
let ctx = MultiSeriesContext::build(vec![s1], None).unwrap();
assert!(ctx.matrix.series_by_name("cpu").is_some());
assert!(ctx.matrix.series_by_name("missing").is_none());
}
#[test]
fn too_many_series() {
let series: Vec<_> = (0..200)
.map(|i| (format!("s{i}"), vec![0i64, 1], vec![0.0, 1.0]))
.collect();
assert!(MultiSeriesContext::build(series, Some(100)).is_err());
}
#[test]
fn interpolation_correctness() {
let ts = vec![0i64, 10];
let vals = vec![0.0, 100.0];
let target = vec![0, 5, 10];
let result = linear_interpolate(&ts, &vals, &target);
assert!((result[0] - 0.0).abs() < 1e-6);
assert!((result[1] - 50.0).abs() < 1e-6);
assert!((result[2] - 100.0).abs() < 1e-6);
}
}