oxits 0.1.0

Time series classification and transformation library for Rust
Documentation
/// Compute the Sakoe-Chiba band constraint for DTW.
///
/// Returns a vector of (min_j, max_j) pairs for each row i,
/// defining the allowed column range in the cost matrix.
pub fn sakoe_chiba_band(n: usize, m: usize, window_size: usize) -> Vec<(usize, usize)> {
    assert!(n > 0 && m > 0, "Dimensions must be positive");

    (0..n)
        .map(|i| {
            let center = (i as f64 * (m - 1) as f64 / (n - 1).max(1) as f64).round() as isize;
            let lo = (center - window_size as isize).max(0) as usize;
            let hi = ((center + window_size as isize) as usize).min(m - 1);
            (lo, hi)
        })
        .collect()
}

/// Compute the Itakura parallelogram constraint for DTW.
///
/// Returns a vector of (min_j, max_j) pairs for each row i,
/// defining the allowed column range in the cost matrix.
pub fn itakura_parallelogram(n: usize, m: usize, max_slope: f64) -> Vec<(usize, usize)> {
    assert!(n > 0 && m > 0, "Dimensions must be positive");
    assert!(max_slope >= 1.0, "max_slope must be >= 1.0");

    let min_slope = 1.0 / max_slope;

    (0..n)
        .map(|i| {
            let i_f = i as f64;
            let n_f = (n - 1) as f64;
            let m_f = (m - 1) as f64;

            // Upper bound: line from (0,0) with slope max_slope, and
            // line from (n-1,m-1) with slope min_slope
            let upper1 = (i_f * max_slope * m_f / n_f).floor() as usize;
            let upper2 = (m_f - (n_f - i_f) * min_slope * m_f / n_f).ceil() as usize;
            let hi = upper1.min(upper2).min(m - 1);

            // Lower bound: line from (0,0) with slope min_slope, and
            // line from (n-1,m-1) with slope max_slope
            let lower1 = (i_f * min_slope * m_f / n_f).ceil() as usize;
            let lower2 = (m_f - (n_f - i_f) * max_slope * m_f / n_f).max(0.0).floor() as usize;
            let lo = lower1.max(lower2);

            (lo, hi)
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sakoe_chiba_basic() {
        let band = sakoe_chiba_band(5, 5, 1);
        assert_eq!(band.len(), 5);
        // Diagonal with window 1
        assert_eq!(band[0], (0, 1));
        assert_eq!(band[2], (1, 3));
        assert_eq!(band[4], (3, 4));
    }

    #[test]
    fn test_sakoe_chiba_full() {
        let band = sakoe_chiba_band(4, 4, 10);
        // Large window should cover everything
        for &(lo, hi) in &band {
            assert_eq!(lo, 0);
            assert_eq!(hi, 3);
        }
    }

    #[test]
    fn test_itakura_basic() {
        let band = itakura_parallelogram(5, 5, 2.0);
        assert_eq!(band.len(), 5);
        // Start and end should be constrained
        assert_eq!(band[0].0, 0);
        assert_eq!(band[4].1, 4);
    }

    #[test]
    fn test_itakura_symmetric() {
        let band = itakura_parallelogram(10, 10, 2.0);
        // For equal-length, should be roughly symmetric
        for i in 0..10 {
            let (lo, hi) = band[i];
            assert!(lo <= i);
            assert!(hi >= i);
        }
    }
}