test(repair): pin ProjectToSimplex argmax tie-breaking and position

The degenerate-magnitude shortcut in ProjectToSimplex::repair scans
`decision` for the argmax and concentrates all mass there. cargo
mutants found that the strict-greater scan was unpinned: replacing
`>` with `>=` (which would shift the argmax to the last tied
index) and `>` with `==` (which would silently skip larger
values further along) both survived.

Two tests:
- A 3-element vector with two tied maxima at the front pins that the
  scan keeps the first index on a tie.
- A 3-element vector whose argmax is at index 1 pins that the scan
  actually walks past the start when later values are larger.

Other mutants in this file (the `>` ↔ `>=` threshold check at line
106, the `*` ↔ `+` in the threshold constant, the `-` ↔ `+` /
`/` in the tau-fallback initializer, and the `>` ↔ `>=` in the
projection loop's rho update) are equivalent mutants for non-pathological
inputs: the normal-path and shortcut-path math converge to the same
projection result for any input the operator is documented to handle.
Leaving them in the residue.
This commit is contained in:
2026-05-13 20:22:40 -06:00
parent c94357abe3
commit 47cb1d5f79
+28
View File
@@ -249,4 +249,32 @@ mod tests {
assert!(approx_eq(v, 0.25, 1e-12));
}
}
// ---- Mutation-test coverage for ProjectToSimplex ----------------------
//
// The degenerate-magnitude shortcut concentrates mass on argmax(x). The
// next two tests pin the *position* of that argmax precisely.
/// The shortcut picks the **first** index on a tie. Strict `>` keeps
/// the earlier index; `>=` would overwrite with the later equal index.
/// Kills `> → >=` in the argmax scan.
#[test]
fn project_extreme_magnitudes_keeps_first_index_on_tie() {
let mut r = ProjectToSimplex::new(1.0);
let mut x = vec![1e20, 1e20, -1e20];
r.repair(&mut x);
assert_eq!(x, vec![1.0, 0.0, 0.0]);
}
/// The shortcut finds the argmax at a non-zero index. With `> → ==`
/// the scan stops updating because `1e20 == -1e20` is false at i=1
/// and the argmax stays at 0 — but the true argmax is at index 1.
/// Kills `> → ==` in the argmax scan.
#[test]
fn project_extreme_magnitudes_finds_argmax_at_non_zero_index() {
let mut r = ProjectToSimplex::new(1.0);
let mut x = vec![-1e20, 1e20, 5e19];
r.repair(&mut x);
assert_eq!(x, vec![0.0, 1.0, 0.0]);
}
}