use arrow::array::{Array, ArrayRef};
use arrow::datatypes::DataType;
use datafusion::common::{DataFusionError, Result as DFResult};
use super::window::{PartitionKernel, column_f64, constant_f64, exec_err, f64_array, on_dense};
fn positional_timestamps(n: usize) -> Vec<i64> {
(0..n as i64)
.map(|i| i.saturating_mul(1_000_000_000))
.collect()
}
macro_rules! kernel {
(
$(#[$meta:meta])*
$name:ident, $sql:literal, [$($ty:expr),* $(,)?]
, |$args:ident, $rows:ident| $body:block
) => {
$(#[$meta])*
#[derive(Debug)]
pub(super) struct $name;
impl PartitionKernel for $name {
const NAME: &'static str = $sql;
fn arg_types() -> Vec<DataType> {
vec![$($ty),*]
}
fn evaluate($args: &[ArrayRef], $rows: usize) -> DFResult<ArrayRef> $body
}
};
}
kernel! {
AnomalyScoreKernel, "anomaly_score", [DataType::Float64, DataType::Float64],
|args, _rows| {
use chronix_analytics::anomaly::{AnomalyDetector, ZScoreDetector};
let values = column_f64(args, 0)?;
let threshold = constant_f64(args, 1, "anomaly_score threshold")?;
let out = on_dense(&values, |dense| {
if dense.len() < 3 {
return Err(DataFusionError::Plan(
"anomaly_score: need at least 3 non-null rows in the partition".into(),
));
}
let ts = positional_timestamps(dense.len());
let mut detector = ZScoreDetector::new(Some(threshold));
detector.fit(&ts, dense).map_err(exec_err)?;
Ok(detector
.detect(&ts, dense)
.map_err(exec_err)?
.iter()
.map(|s| s.score)
.collect())
})?;
Ok(f64_array(out))
}
}
kernel! {
CorrelationKernel, "correlation", [DataType::Float64, DataType::Float64],
|args, rows| {
use chronix_analytics::multivariate::PearsonCorrelation;
let a = column_f64(args, 0)?;
let b = column_f64(args, 1)?;
let r = PearsonCorrelation::compute(&a, &b).map_err(exec_err)?;
Ok(f64_array(vec![r; rows]))
}
}
kernel! {
CrossCorrelationKernel, "cross_correlation",
[DataType::Float64, DataType::Float64, DataType::Int64],
|args, rows| {
use chronix_analytics::multivariate::LagCorrelation;
let a = column_f64(args, 0)?;
let b = column_f64(args, 1)?;
let arr = args.get(2).ok_or_else(|| {
DataFusionError::Internal("cross_correlation: missing lag".into())
})?;
let lag_col = arr
.as_any()
.downcast_ref::<arrow::array::Int64Array>()
.ok_or_else(|| DataFusionError::Plan("cross_correlation: lag must be an integer".into()))?;
if lag_col.is_empty() || lag_col.is_null(0) {
return Err(DataFusionError::Plan(
"cross_correlation: lag must not be NULL".into(),
));
}
let lag = i32::try_from(lag_col.value(0)).map_err(|_| {
DataFusionError::Plan(format!(
"cross_correlation: lag {} is out of range",
lag_col.value(0)
))
})?;
let results = LagCorrelation::compute(&a, &b, &[lag]).map_err(exec_err)?;
let r = results.first().map_or(f64::NAN, |(_, v)| *v);
Ok(f64_array(vec![r; rows]))
}
}
kernel! {
MultivariateAnomalyKernel, "multivariate_anomaly",
[DataType::Float64, DataType::Float64, DataType::Float64],
|args, rows| {
use chronix_analytics::multivariate::{
ColumnarMatrix, MahalanobisDetector, MultiSeriesContext, MultivariateAnomalyDetector,
};
let a = column_f64(args, 0)?;
let b = column_f64(args, 1)?;
let threshold = constant_f64(args, 2, "multivariate_anomaly threshold")?;
if rows < 3 {
return Err(DataFusionError::Plan(
"multivariate_anomaly: need at least 3 rows in the partition".into(),
));
}
let complete: Vec<usize> = (0..rows)
.filter(|&i| a[i].is_finite() && b[i].is_finite())
.collect();
if complete.len() < 3 {
return Err(DataFusionError::Plan(format!(
"multivariate_anomaly: need at least 3 rows where both columns are \
non-null, got {}",
complete.len()
)));
}
let ctx = MultiSeriesContext {
matrix: ColumnarMatrix {
data: vec![
complete.iter().map(|&i| a[i]).collect(),
complete.iter().map(|&i| b[i]).collect(),
],
timestamps: positional_timestamps(complete.len()),
series_ids: vec!["a".into(), "b".into()],
},
};
let mut detector = MahalanobisDetector::new(Some(threshold), None);
detector.fit(&ctx).map_err(exec_err)?;
let scores = detector.detect(&ctx).map_err(exec_err)?;
let mut out = vec![f64::NAN; rows];
for (slot, score) in complete.iter().zip(scores.iter()) {
out[*slot] = score.score;
}
Ok(f64_array(out))
}
}