Functional Weave
Code in Rust

monitor.anomaly@1.0.0

impl/rust.rs

3,990 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

fn isqrt(n: i128) -> i128 {
    if n < 2 {
        return n;
    }
    let mut x = n;
    let mut y = (x + 1) / 2;
    while y < x {
        x = y;
        y = (x + n / x) / 2;
    }
    x
}

// Half-up with halves away from zero, as math.round-div rounds.
fn half_up(n: i128, d: i128) -> i128 {
    let q = (2 * n.abs() + d) / (2 * d);
    if n < 0 { -q } else { q }
}

fn median_twice(sorted: &[i128]) -> i128 {
    let m = sorted.len() / 2;
    if sorted.len() % 2 == 1 { 2 * sorted[m] } else { sorted[m - 1] + sorted[m] }
}

/// Whether `value` is an outlier against `history`, by z-score (`stddev`) or
/// by Iglewicz and Hoaglin's modified z-score (`mad`). Exact integer
/// arithmetic: the threshold comparison never depends on float rounding.
///
/// # Panics
/// Panics on a history under 2 values, a threshold under 1 or an unknown method.
pub fn detect_anomaly(history: &[i64], value: i64, method: &str, threshold_hundredths: i64) -> Anomaly {
    if history.len() < 2 {
        panic!("history must have at least 2 values, received {}", history.len());
    }
    if threshold_hundredths < 1 {
        panic!("thresholdHundredths must be at least 1, received {}", threshold_hundredths);
    }
    let xs: Vec<i128> = history.iter().map(|&x| x as i128).collect();
    let n = xs.len() as i128;
    let v = value as i128;
    let t = threshold_hundredths as i128;
    let (anomaly, signed, center, spread, score) = match method {
        "stddev" => {
            let s: i128 = xs.iter().sum();
            let ss: i128 = xs.iter().map(|x| x * x).sum();
            // n^2 * variance and n * (value - mean), both whole numbers.
            let q = n * ss - s * s;
            let signed = n * v - s;
            let center = half_up(s * 1000, n);
            let spread = isqrt(1_000_000 * q) / n;
            if q == 0 {
                (signed != 0, signed, center, spread, None)
            } else {
                let anomaly = 10000 * signed * signed > t * t * q;
                (anomaly, signed, center, spread, Some(isqrt(10000 * signed * signed / q)))
            }
        }
        "mad" => {
            // Twice the median and four times the MAD are whole numbers.
            let mut sorted = xs.clone();
            sorted.sort();
            let med2 = median_twice(&sorted);
            let mut devs: Vec<i128> = xs.iter().map(|x| (2 * x - med2).abs()).collect();
            devs.sort();
            let mad4 = median_twice(&devs);
            let signed = 2 * v - med2;
            if mad4 == 0 {
                (signed != 0, signed, med2 * 500, 0, None)
            } else {
                // |M| = 0.6745 * |2v - med2| / 2 / (mad4 / 4) = 1349 * |signed| / (1000 * mad4)
                let anomaly = 1349 * signed.abs() * 100 > t * 1000 * mad4;
                let score = 1349 * signed.abs() * 100 / (1000 * mad4);
                (anomaly, signed, med2 * 500, mad4 * 250, Some(score))
            }
        }
        other => panic!("unknown anomaly method: {}", other),
    };
    let direction = if !anomaly { "normal" } else if signed > 0 { "high" } else { "low" };
    Anomaly {
        anomaly,
        direction: direction.to_string(),
        center_milli: center as i64,
        spread_milli: spread as i64,
        score_hundredths: score.map(|s| s as i64),
    }
}

pub fn anomaly_to_value(a: &Anomaly) -> Value {
    Value::obj(vec![
        ("anomaly", Value::Bool(a.anomaly)),
        ("direction", Value::str(&a.direction)),
        ("centerMilli", Value::Int(a.center_milli)),
        ("spreadMilli", Value::Int(a.spread_milli)),
        ("scoreHundredths", match a.score_hundredths {
            Some(s) => Value::Int(s),
            None => Value::Null,
        }),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let history: Vec<i64> = args[0].as_arr().iter().map(|x| x.as_i64()).collect();
    anomaly_to_value(&detect_anomaly(&history, args[1].as_i64(), args[2].as_str(), args[3].as_i64()))
}