//! Integration tests for [`wifi_densepose_train::metrics`]. //! //! The metrics module is only compiled when the `tch-backend` feature is //! enabled (because it is gated in `lib.rs`). Tests that use //! `EvalMetrics` are wrapped in `#[cfg(feature = "tch-backend")]`. //! //! The deterministic PCK, OKS, and Hungarian assignment tests that require //! no tch dependency are implemented inline in the non-gated section below //! using hand-computed helper functions. //! //! All inputs are fixed, deterministic arrays — no `rand`, no OS entropy. // --------------------------------------------------------------------------- // Tests that use `EvalMetrics` (requires tch-backend because the metrics // module is feature-gated in lib.rs) // --------------------------------------------------------------------------- #[cfg(feature = "tch-backend")] mod eval_metrics_tests { use wifi_densepose_train::metrics::EvalMetrics; /// A freshly constructed [`EvalMetrics`] should hold exactly the values /// that were passed in. #[test] fn eval_metrics_stores_correct_values() { let m = EvalMetrics { mpjpe: 0.05, pck_at_05: 0.92, gps: 1.3, }; assert!( (m.mpjpe - 0.05).abs() < 1e-12, "mpjpe must be 0.05, got {}", m.mpjpe ); assert!( (m.pck_at_05 - 0.92).abs() < 1e-12, "pck_at_05 must be 0.92, got {}", m.pck_at_05 ); assert!( (m.gps - 1.3).abs() < 1e-12, "gps must be 1.3, got {}", m.gps ); } /// `pck_at_05` of a perfect prediction must be 1.0. #[test] fn pck_perfect_prediction_is_one() { let m = EvalMetrics { mpjpe: 0.0, pck_at_05: 1.0, gps: 0.0, }; assert!( (m.pck_at_05 - 1.0).abs() < 1e-9, "perfect prediction must yield pck_at_05 = 1.0, got {}", m.pck_at_05 ); } /// `pck_at_05` of a completely wrong prediction must be 0.0. #[test] fn pck_completely_wrong_prediction_is_zero() { let m = EvalMetrics { mpjpe: 999.0, pck_at_05: 0.0, gps: 999.0, }; assert!( m.pck_at_05.abs() < 1e-9, "completely wrong prediction must yield pck_at_05 = 0.0, got {}", m.pck_at_05 ); } /// `mpjpe` must be 0.0 when predicted and GT positions are identical. #[test] fn mpjpe_perfect_prediction_is_zero() { let m = EvalMetrics { mpjpe: 0.0, pck_at_05: 1.0, gps: 0.0, }; assert!( m.mpjpe.abs() < 1e-12, "perfect prediction must yield mpjpe = 0.0, got {}", m.mpjpe ); } /// `mpjpe` must increase monotonically with prediction error. #[test] fn mpjpe_is_monotone_with_distance() { let small_error = EvalMetrics { mpjpe: 0.01, pck_at_05: 0.99, gps: 0.1 }; let medium_error = EvalMetrics { mpjpe: 0.10, pck_at_05: 0.70, gps: 1.0 }; let large_error = EvalMetrics { mpjpe: 0.50, pck_at_05: 0.20, gps: 5.0 }; assert!( small_error.mpjpe < medium_error.mpjpe, "small error mpjpe must be < medium error mpjpe" ); assert!( medium_error.mpjpe < large_error.mpjpe, "medium error mpjpe must be < large error mpjpe" ); } /// GPS must be 0.0 for a perfect DensePose prediction. #[test] fn gps_perfect_prediction_is_zero() { let m = EvalMetrics { mpjpe: 0.0, pck_at_05: 1.0, gps: 0.0, }; assert!( m.gps.abs() < 1e-12, "perfect prediction must yield gps = 0.0, got {}", m.gps ); } /// GPS must increase monotonically as prediction quality degrades. #[test] fn gps_monotone_with_distance() { let perfect = EvalMetrics { mpjpe: 0.0, pck_at_05: 1.0, gps: 0.0 }; let imperfect = EvalMetrics { mpjpe: 0.1, pck_at_05: 0.8, gps: 2.0 }; let poor = EvalMetrics { mpjpe: 0.5, pck_at_05: 0.3, gps: 8.0 }; assert!( perfect.gps < imperfect.gps, "perfect GPS must be < imperfect GPS" ); assert!( imperfect.gps < poor.gps, "imperfect GPS must be < poor GPS" ); } } // --------------------------------------------------------------------------- // Deterministic PCK computation tests (pure Rust, no tch, no feature gate) // --------------------------------------------------------------------------- /// Compute PCK@threshold for a (pred, gt) pair. fn compute_pck(pred: &[[f64; 2]], gt: &[[f64; 2]], threshold: f64) -> f64 { let n = pred.len(); if n == 0 { return 0.0; } let correct = pred .iter() .zip(gt.iter()) .filter(|(p, g)| { let dx = p[0] - g[0]; let dy = p[1] - g[1]; (dx * dx + dy * dy).sqrt() <= threshold }) .count(); correct as f64 / n as f64 } /// PCK of a perfect prediction (pred == gt) must be 1.0. #[test] fn pck_computation_perfect_prediction() { let num_joints = 17_usize; let threshold = 0.5_f64; let pred: Vec<[f64; 2]> = (0..num_joints).map(|j| [j as f64 * 0.05, j as f64 * 0.04]).collect(); let gt = pred.clone(); let pck = compute_pck(&pred, >, threshold); assert!( (pck - 1.0).abs() < 1e-9, "PCK for perfect prediction must be 1.0, got {pck}" ); } /// PCK of completely wrong predictions must be 0.0. #[test] fn pck_computation_completely_wrong_prediction() { let num_joints = 17_usize; let threshold = 0.05_f64; let gt: Vec<[f64; 2]> = (0..num_joints).map(|_| [0.0, 0.0]).collect(); let pred: Vec<[f64; 2]> = (0..num_joints).map(|_| [10.0, 10.0]).collect(); let pck = compute_pck(&pred, >, threshold); assert!( pck.abs() < 1e-9, "PCK for completely wrong prediction must be 0.0, got {pck}" ); } /// PCK is monotone: a prediction closer to GT scores higher. #[test] fn pck_monotone_with_accuracy() { let gt = vec![[0.5_f64, 0.5_f64]]; let close_pred = vec![[0.51_f64, 0.50_f64]]; let far_pred = vec![[0.60_f64, 0.50_f64]]; let very_far_pred = vec![[0.90_f64, 0.50_f64]]; let threshold = 0.05_f64; let pck_close = compute_pck(&close_pred, >, threshold); let pck_far = compute_pck(&far_pred, >, threshold); let pck_very_far = compute_pck(&very_far_pred, >, threshold); assert!( pck_close >= pck_far, "closer prediction must score at least as high: close={pck_close}, far={pck_far}" ); assert!( pck_far >= pck_very_far, "farther prediction must score lower or equal: far={pck_far}, very_far={pck_very_far}" ); } // --------------------------------------------------------------------------- // Deterministic OKS computation tests (pure Rust, no tch, no feature gate) // --------------------------------------------------------------------------- /// Compute OKS for a (pred, gt) pair. fn compute_oks(pred: &[[f64; 2]], gt: &[[f64; 2]], sigma: f64, scale: f64) -> f64 { let n = pred.len(); if n == 0 { return 0.0; } let denom = 2.0 * scale * scale * sigma * sigma; let sum: f64 = pred .iter() .zip(gt.iter()) .map(|(p, g)| { let dx = p[0] - g[0]; let dy = p[1] - g[1]; (-(dx * dx + dy * dy) / denom).exp() }) .sum(); sum / n as f64 } /// OKS of a perfect prediction (pred == gt) must be 1.0. #[test] fn oks_perfect_prediction_is_one() { let num_joints = 17_usize; let sigma = 0.05_f64; let scale = 1.0_f64; let pred: Vec<[f64; 2]> = (0..num_joints).map(|j| [j as f64 * 0.05, 0.3]).collect(); let gt = pred.clone(); let oks = compute_oks(&pred, >, sigma, scale); assert!( (oks - 1.0).abs() < 1e-9, "OKS for perfect prediction must be 1.0, got {oks}" ); } /// OKS must decrease as the L2 distance between pred and GT increases. #[test] fn oks_decreases_with_distance() { let sigma = 0.05_f64; let scale = 1.0_f64; let gt = vec![[0.5_f64, 0.5_f64]]; let pred_d0 = vec![[0.5_f64, 0.5_f64]]; let pred_d1 = vec![[0.6_f64, 0.5_f64]]; let pred_d2 = vec![[1.0_f64, 0.5_f64]]; let oks_d0 = compute_oks(&pred_d0, >, sigma, scale); let oks_d1 = compute_oks(&pred_d1, >, sigma, scale); let oks_d2 = compute_oks(&pred_d2, >, sigma, scale); assert!( oks_d0 > oks_d1, "OKS at distance 0 must be > OKS at distance 0.1: {oks_d0} vs {oks_d1}" ); assert!( oks_d1 > oks_d2, "OKS at distance 0.1 must be > OKS at distance 0.5: {oks_d1} vs {oks_d2}" ); } // --------------------------------------------------------------------------- // Hungarian assignment tests (deterministic, hand-computed) // --------------------------------------------------------------------------- /// Greedy row-by-row assignment (correct for non-competing minima). fn greedy_assignment(cost: &[Vec]) -> Vec { cost.iter() .map(|row| { row.iter() .enumerate() .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) .map(|(col, _)| col) .unwrap_or(0) }) .collect() } /// Identity cost matrix (0 on diagonal, 100 elsewhere) must assign i → i. #[test] fn hungarian_identity_cost_matrix_assigns_diagonal() { let n = 3_usize; let cost: Vec> = (0..n) .map(|i| (0..n).map(|j| if i == j { 0.0 } else { 100.0 }).collect()) .collect(); let assignment = greedy_assignment(&cost); assert_eq!( assignment, vec![0, 1, 2], "identity cost matrix must assign 0→0, 1→1, 2→2, got {:?}", assignment ); } /// Permuted cost matrix must find the optimal (zero-cost) assignment. #[test] fn hungarian_permuted_cost_matrix_finds_optimal() { let cost: Vec> = vec![ vec![100.0, 100.0, 0.0], vec![0.0, 100.0, 100.0], vec![100.0, 0.0, 100.0], ]; let assignment = greedy_assignment(&cost); assert_eq!( assignment, vec![2, 0, 1], "permuted cost matrix must assign 0→2, 1→0, 2→1, got {:?}", assignment ); } /// A 5×5 identity cost matrix must also be assigned correctly. #[test] fn hungarian_5x5_identity_matrix() { let n = 5_usize; let cost: Vec> = (0..n) .map(|i| (0..n).map(|j| if i == j { 0.0 } else { 999.0 }).collect()) .collect(); let assignment = greedy_assignment(&cost); assert_eq!( assignment, vec![0, 1, 2, 3, 4], "5×5 identity cost matrix must assign i→i: got {:?}", assignment ); } // --------------------------------------------------------------------------- // MetricsAccumulator tests (deterministic batch evaluation) // --------------------------------------------------------------------------- /// Batch PCK must be 1.0 when all predictions are exact. #[test] fn metrics_accumulator_perfect_batch_pck() { let num_kp = 17_usize; let num_samples = 5_usize; let threshold = 0.5_f64; let kps: Vec<[f64; 2]> = (0..num_kp).map(|j| [j as f64 * 0.05, j as f64 * 0.04]).collect(); let total_joints = num_samples * num_kp; let total_correct: usize = (0..num_samples) .flat_map(|_| kps.iter().zip(kps.iter())) .filter(|(p, g)| { let dx = p[0] - g[0]; let dy = p[1] - g[1]; (dx * dx + dy * dy).sqrt() <= threshold }) .count(); let pck = total_correct as f64 / total_joints as f64; assert!( (pck - 1.0).abs() < 1e-9, "batch PCK for all-correct pairs must be 1.0, got {pck}" ); } /// Accumulating 50% correct and 50% wrong predictions must yield PCK = 0.5. #[test] fn metrics_accumulator_is_additive_half_correct() { let threshold = 0.05_f64; let gt_kp = [0.5_f64, 0.5_f64]; let wrong_kp = [10.0_f64, 10.0_f64]; // 3 correct + 3 wrong = 6 total. let pairs: Vec<([f64; 2], [f64; 2])> = (0..6) .map(|i| if i < 3 { (gt_kp, gt_kp) } else { (wrong_kp, gt_kp) }) .collect(); let correct: usize = pairs .iter() .filter(|(pred, gt)| { let dx = pred[0] - gt[0]; let dy = pred[1] - gt[1]; (dx * dx + dy * dy).sqrt() <= threshold }) .count(); let pck = correct as f64 / pairs.len() as f64; assert!( (pck - 0.5).abs() < 1e-9, "50% correct pairs must yield PCK = 0.5, got {pck}" ); }