89 lines
2.7 KiB
Rust
89 lines
2.7 KiB
Rust
#![no_main]
|
|
//! Fuzz `ClampToBounds` + `ProjectToSimplex` repair operators for
|
|
//! idempotence and target-set membership.
|
|
|
|
use arbitrary::Arbitrary;
|
|
use libfuzzer_sys::fuzz_target;
|
|
|
|
use heuropt::prelude::*;
|
|
|
|
#[derive(Arbitrary, Debug)]
|
|
struct Input {
|
|
bounds: Vec<(f64, f64)>,
|
|
x: Vec<f64>,
|
|
simplex_total: f64,
|
|
}
|
|
|
|
fuzz_target!(|input: Input| {
|
|
if input.bounds.is_empty() || input.bounds.len() > 16 {
|
|
return;
|
|
}
|
|
if input.x.len() != input.bounds.len() {
|
|
return;
|
|
}
|
|
let bounds: Vec<(f64, f64)> = input
|
|
.bounds
|
|
.iter()
|
|
.filter_map(|&(lo, hi)| {
|
|
if lo.is_finite() && hi.is_finite() && lo < hi {
|
|
Some((lo, hi))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
if bounds.len() != input.bounds.len() {
|
|
return;
|
|
}
|
|
// Restrict to a numerically-reasonable magnitude range for repair
|
|
// operators — they are invoked downstream of evolutionary search where
|
|
// candidate magnitudes are bounded.
|
|
if input.x.iter().any(|v| !v.is_finite() || v.abs() > 1e30) {
|
|
return;
|
|
}
|
|
|
|
let mut x = input.x.clone();
|
|
let mut clamp = ClampToBounds::new(bounds.clone());
|
|
clamp.repair(&mut x);
|
|
for (j, &v) in x.iter().enumerate() {
|
|
let (lo, hi) = bounds[j];
|
|
assert!(v >= lo && v <= hi, "clamp out of bounds");
|
|
}
|
|
let after_one = x.clone();
|
|
clamp.repair(&mut x);
|
|
assert_eq!(x, after_one, "clamp not idempotent");
|
|
|
|
// Simplex projection only meaningful when total > 0 and dim >= 1.
|
|
// The Duchi/Held-Wolfe projection loses precision when |x| ≫ total
|
|
// (τ becomes indistinguishable from max(x) in f64). Restrict to inputs
|
|
// within the algorithm's well-conditioned regime, |x_i| ≤ total · 1e6.
|
|
let max_abs = input.x.iter().fold(0.0_f64, |a, &b| a.max(b.abs()));
|
|
if input.simplex_total.is_finite()
|
|
&& input.simplex_total > 1.0
|
|
&& input.simplex_total < 1e9
|
|
&& max_abs <= input.simplex_total * 1e6
|
|
{
|
|
let mut y = input.x.clone();
|
|
let mut proj = ProjectToSimplex::new(input.simplex_total);
|
|
proj.repair(&mut y);
|
|
for &v in &y {
|
|
assert!(v >= 0.0, "project negative entry");
|
|
}
|
|
let s: f64 = y.iter().sum();
|
|
assert!(
|
|
(s - input.simplex_total).abs() < 1e-6 * input.simplex_total.max(1.0),
|
|
"project sum {s} != target {}",
|
|
input.simplex_total,
|
|
);
|
|
let after = y.clone();
|
|
proj.repair(&mut y);
|
|
for (a, b) in after.iter().zip(y.iter()) {
|
|
let scale = a.abs().max(b.abs()).max(1.0);
|
|
assert!(
|
|
(a - b).abs() < 1e-9 * scale,
|
|
"project not idempotent: {a} vs {b}",
|
|
);
|
|
}
|
|
}
|
|
});
|