#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum Correlation {
Independent,
Comonotonic,
}
pub fn aggregate(segments: &[&[f64]], corr: Correlation) -> Vec<f64> {
let n = match segments.iter().map(|s| s.len()).min() {
Some(n) if n > 0 && !segments.is_empty() => n,
_ => return Vec::new(),
};
match corr {
Correlation::Independent => {
let mut total = vec![0.0_f64; n];
for seg in segments {
for (t, v) in total.iter_mut().zip(seg.iter()) {
*t += *v;
}
}
total
}
Correlation::Comonotonic => {
let mut total = vec![0.0_f64; n];
for seg in segments {
let mut sorted: Vec<f64> = seg[..n].to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
for (t, v) in total.iter_mut().zip(sorted.iter()) {
*t += *v;
}
}
total
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_inputs_give_empty() {
assert!(aggregate(&[], Correlation::Independent).is_empty());
let empty: &[f64] = &[];
assert!(aggregate(&[empty], Correlation::Comonotonic).is_empty());
}
#[test]
fn independent_is_index_wise_sum() {
let a = [1.0, 2.0, 3.0];
let b = [10.0, 20.0, 30.0];
let out = aggregate(&[&a, &b], Correlation::Independent);
assert_eq!(out, vec![11.0, 22.0, 33.0]);
}
#[test]
fn comonotonic_sums_by_rank() {
let a = [3.0, 1.0, 2.0];
let b = [10.0, 30.0, 20.0];
let out = aggregate(&[&a, &b], Correlation::Comonotonic);
assert_eq!(out, vec![11.0, 22.0, 33.0]);
}
#[test]
fn comonotonic_widens_the_spread_vs_independent() {
let a = [1.0, 2.0, 3.0, 4.0];
let b = [4.0, 3.0, 2.0, 1.0];
let ind = aggregate(&[&a, &b], Correlation::Independent);
let com = aggregate(&[&a, &b], Correlation::Comonotonic);
let range = |v: &[f64]| {
v.iter().cloned().fold(f64::MIN, f64::max) - v.iter().cloned().fold(f64::MAX, f64::min)
};
assert_eq!(ind, vec![5.0, 5.0, 5.0, 5.0]); assert!(range(&com) > range(&ind));
}
#[test]
fn ragged_segments_truncate_to_shortest() {
let a = [1.0, 2.0, 3.0, 4.0];
let b = [10.0, 20.0];
let out = aggregate(&[&a, &b], Correlation::Independent);
assert_eq!(out, vec![11.0, 22.0]);
}
}