const TOLERANCE: f64 = 1e-6;
struct Case {
input: Vec<f64>,
strength: f64,
soft_rank: Vec<f64>,
soft_sort: Vec<f64>,
}
fn load_cases() -> Vec<Case> {
let raw = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/blondel_reference.json"
))
.expect("fixture readable");
let v: serde_json::Value = serde_json::from_str(&raw).expect("fixture parses");
let floats = |x: &serde_json::Value| -> Vec<f64> {
x.as_array()
.expect("array")
.iter()
.map(|f| f.as_f64().expect("f64"))
.collect()
};
v["cases"]
.as_array()
.expect("cases array")
.iter()
.map(|c| Case {
input: floats(&c["input"]),
strength: c["regularization_strength"].as_f64().expect("strength"),
soft_rank: floats(&c["soft_rank"]),
soft_sort: floats(&c["soft_sort"]),
})
.collect()
}
fn assert_close(got: &[f64], want: &[f64], what: &str, case: &Case) {
assert_eq!(got.len(), want.len(), "{what}: length mismatch");
for (i, (g, w)) in got.iter().zip(want).enumerate() {
assert!(
(g - w).abs() < TOLERANCE,
"{what} diverges from the reference at index {i}: got {g}, want {w} \
(input {:?}, strength {})",
case.input,
case.strength,
);
}
}
#[test]
fn fast_soft_sort_matches_reference_implementation() {
let cases = load_cases();
assert!(cases.len() >= 12, "fixture unexpectedly small");
for case in &cases {
let got = fynch::fast_soft_sort(&case.input, case.strength);
assert_close(&got, &case.soft_sort, "fast_soft_sort", case);
}
}
#[test]
fn fast_soft_rank_matches_reference_implementation() {
let cases = load_cases();
for case in &cases {
let got = fynch::fast_soft_rank(&case.input, case.strength).expect("valid input");
assert_close(&got, &case.soft_rank, "fast_soft_rank", case);
}
}
#[test]
fn fast_soft_rank_hard_limit_recovers_integer_ranks() {
let x = [0.5, 0.2, 0.8, 0.1];
let ranks = fynch::fast_soft_rank(&x, 1e-6).expect("valid input");
let expected = [3.0, 2.0, 4.0, 1.0];
for (g, w) in ranks.iter().zip(&expected) {
assert!((g - w).abs() < 1e-3, "hard limit: got {g}, want {w}");
}
}