test(fuzz): add cargo-fuzz harness for Pareto and operator hot paths
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
Generated
+254
@@ -0,0 +1,254 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heuropt"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"rand_distr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heuropt-fuzz"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"heuropt",
|
||||
"libfuzzer-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_distr"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
@@ -0,0 +1,69 @@
|
||||
[package]
|
||||
name = "heuropt-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2024"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4"
|
||||
arbitrary = { version = "1", features = ["derive"] }
|
||||
heuropt = { path = ".." }
|
||||
|
||||
[[bin]]
|
||||
name = "pareto_compare"
|
||||
path = "fuzz_targets/pareto_compare.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "non_dominated_sort"
|
||||
path = "fuzz_targets/non_dominated_sort.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "hypervolume_2d"
|
||||
path = "fuzz_targets/hypervolume_2d.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "pareto_archive"
|
||||
path = "fuzz_targets/pareto_archive.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "crowding_distance"
|
||||
path = "fuzz_targets/crowding_distance.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "spacing"
|
||||
path = "fuzz_targets/spacing.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "sbx_polymut"
|
||||
path = "fuzz_targets/sbx_polymut.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "clamp_to_bounds"
|
||||
path = "fuzz_targets/clamp_to_bounds.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
@@ -0,0 +1,88 @@
|
||||
#![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}",
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
#![no_main]
|
||||
//! Fuzz `crowding_distance` for shape and non-negativity.
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use heuropt::core::candidate::Candidate;
|
||||
use heuropt::core::evaluation::Evaluation;
|
||||
use heuropt::core::objective::{Objective, ObjectiveSpace};
|
||||
use heuropt::pareto::crowding::crowding_distance;
|
||||
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct Input {
|
||||
points: Vec<(f64, f64)>,
|
||||
}
|
||||
|
||||
fuzz_target!(|input: Input| {
|
||||
if input.points.len() > 64 {
|
||||
return;
|
||||
}
|
||||
// Bound magnitudes — crowding's `(max - min)` and per-axis gaps can
|
||||
// both overflow to +∞ when points span ±f64::MAX, yielding inf/inf=NaN.
|
||||
if input
|
||||
.points
|
||||
.iter()
|
||||
.any(|&(a, b)| !a.is_finite() || !b.is_finite() || a.abs() > 1e150 || b.abs() > 1e150)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let space = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
|
||||
let pop: Vec<Candidate<()>> = input
|
||||
.points
|
||||
.iter()
|
||||
.map(|&(a, b)| Candidate::new((), Evaluation::new(vec![a, b])))
|
||||
.collect();
|
||||
|
||||
let front: Vec<usize> = (0..pop.len()).collect();
|
||||
let d = crowding_distance(&pop, &front, &space);
|
||||
assert_eq!(d.len(), front.len(), "crowding distance length mismatch");
|
||||
for (i, &v) in d.iter().enumerate() {
|
||||
assert!(v >= 0.0 || v.is_infinite(), "negative crowding[{i}] = {v}");
|
||||
assert!(!v.is_nan(), "NaN crowding[{i}]");
|
||||
}
|
||||
// If size <= 2, every entry is +∞.
|
||||
if pop.len() <= 2 {
|
||||
for (i, &v) in d.iter().enumerate() {
|
||||
assert!(v.is_infinite(), "size<=2 crowding[{i}] not inf: {v}");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
#![no_main]
|
||||
//! Fuzz `hypervolume_2d` for non-negativity and reference-point handling.
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use heuropt::core::candidate::Candidate;
|
||||
use heuropt::core::evaluation::Evaluation;
|
||||
use heuropt::core::objective::{Objective, ObjectiveSpace};
|
||||
use heuropt::metrics::hypervolume::hypervolume_2d;
|
||||
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct Input {
|
||||
points: Vec<(f64, f64)>,
|
||||
ref_point: (f64, f64),
|
||||
}
|
||||
|
||||
fuzz_target!(|input: Input| {
|
||||
if input.points.len() > 64 {
|
||||
return;
|
||||
}
|
||||
// Non-finite floats are permitted by Evaluation, but HV is undefined
|
||||
// there — restrict to finite for this property.
|
||||
if !input.ref_point.0.is_finite() || !input.ref_point.1.is_finite() {
|
||||
return;
|
||||
}
|
||||
if input
|
||||
.points
|
||||
.iter()
|
||||
.any(|&(a, b)| !a.is_finite() || !b.is_finite())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let space = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
|
||||
let pop: Vec<Candidate<()>> = input
|
||||
.points
|
||||
.iter()
|
||||
.map(|&(a, b)| Candidate::new((), Evaluation::new(vec![a, b])))
|
||||
.collect();
|
||||
let hv = hypervolume_2d(&pop, &space, [input.ref_point.0, input.ref_point.1]);
|
||||
// HV can be +∞ when the dominated rectangle area overflows f64 (e.g. a
|
||||
// ref point at f64::MAX with deeply negative front coords). The
|
||||
// contracted invariants are non-negativity and non-NaN.
|
||||
assert!(hv >= 0.0, "HV negative: {hv}");
|
||||
assert!(!hv.is_nan(), "HV is NaN");
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
#![no_main]
|
||||
//! Fuzz `non_dominated_sort` for partition correctness.
|
||||
//!
|
||||
//! Invariants checked:
|
||||
//! * Every population index appears in exactly one front.
|
||||
//! * Earlier fronts dominate later fronts (no backwards domination).
|
||||
//! * No panics on any vector of finite or non-finite objective values.
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use heuropt::core::candidate::Candidate;
|
||||
use heuropt::core::evaluation::Evaluation;
|
||||
use heuropt::core::objective::{Objective, ObjectiveSpace};
|
||||
use heuropt::pareto::dominance::{Dominance, pareto_compare};
|
||||
use heuropt::pareto::sort::non_dominated_sort;
|
||||
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct Input {
|
||||
objectives: Vec<(f64, f64)>,
|
||||
}
|
||||
|
||||
fuzz_target!(|input: Input| {
|
||||
if input.objectives.is_empty() || input.objectives.len() > 32 {
|
||||
return;
|
||||
}
|
||||
let space = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
|
||||
let pop: Vec<Candidate<()>> = input
|
||||
.objectives
|
||||
.iter()
|
||||
.map(|&(a, b)| Candidate::new((), Evaluation::new(vec![a, b])))
|
||||
.collect();
|
||||
|
||||
let fronts = non_dominated_sort(&pop, &space);
|
||||
|
||||
// Partition: every index appears exactly once.
|
||||
let mut seen = vec![false; pop.len()];
|
||||
for front in &fronts {
|
||||
for &idx in front {
|
||||
assert!(!seen[idx], "index {idx} in multiple fronts");
|
||||
seen[idx] = true;
|
||||
}
|
||||
}
|
||||
for (i, &was) in seen.iter().enumerate() {
|
||||
assert!(was, "index {i} missing from all fronts");
|
||||
}
|
||||
|
||||
// Earlier fronts cannot be dominated by later fronts.
|
||||
for (k, fk) in fronts.iter().enumerate() {
|
||||
for fl in fronts.iter().skip(k + 1) {
|
||||
for &i in fk {
|
||||
for &j in fl {
|
||||
let r = pareto_compare(&pop[i].evaluation, &pop[j].evaluation, &space);
|
||||
assert!(
|
||||
!matches!(r, Dominance::DominatedBy),
|
||||
"front-{k}/{i} dominated by later front",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
#![no_main]
|
||||
//! Fuzz `ParetoArchive` for the non-domination invariant under arbitrary
|
||||
//! insertion/truncation sequences.
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use heuropt::core::candidate::Candidate;
|
||||
use heuropt::core::evaluation::Evaluation;
|
||||
use heuropt::core::objective::{Objective, ObjectiveSpace};
|
||||
use heuropt::pareto::archive::ParetoArchive;
|
||||
use heuropt::pareto::dominance::{Dominance, pareto_compare};
|
||||
|
||||
#[derive(Arbitrary, Debug)]
|
||||
enum Op {
|
||||
Insert(f64, f64),
|
||||
Truncate(u8),
|
||||
}
|
||||
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct Input {
|
||||
ops: Vec<Op>,
|
||||
}
|
||||
|
||||
fuzz_target!(|input: Input| {
|
||||
if input.ops.len() > 64 {
|
||||
return;
|
||||
}
|
||||
let space = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
|
||||
let mut archive: ParetoArchive<()> = ParetoArchive::new(space.clone());
|
||||
|
||||
for op in input.ops {
|
||||
match op {
|
||||
Op::Insert(a, b) => {
|
||||
let cand = Candidate::new((), Evaluation::new(vec![a, b]));
|
||||
archive.insert(cand);
|
||||
}
|
||||
Op::Truncate(n) => archive.truncate(n as usize),
|
||||
}
|
||||
}
|
||||
|
||||
// Members must be pairwise non-dominated.
|
||||
let m = archive.members();
|
||||
for i in 0..m.len() {
|
||||
for j in 0..m.len() {
|
||||
if i == j {
|
||||
continue;
|
||||
}
|
||||
let r = pareto_compare(&m[i].evaluation, &m[j].evaluation, &space);
|
||||
assert!(
|
||||
!matches!(r, Dominance::DominatedBy),
|
||||
"archive member {i} dominated by {j}: {:?} vs {:?}",
|
||||
m[i].evaluation.objectives,
|
||||
m[j].evaluation.objectives,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
#![no_main]
|
||||
//! Fuzz `pareto_compare` for anti-symmetry and reflexivity.
|
||||
//!
|
||||
//! Invariants checked:
|
||||
//! * `compare(a, b)` and `compare(b, a)` form an anti-symmetric pair
|
||||
//! (`Dominates ↔ DominatedBy`, `Equal ↔ Equal`, `NonDominated ↔ NonDominated`).
|
||||
//! * `compare(a, a) == Equal`.
|
||||
//! * No panics on any combination of finite/non-finite floats.
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use heuropt::core::evaluation::Evaluation;
|
||||
use heuropt::core::objective::{Objective, ObjectiveSpace};
|
||||
use heuropt::pareto::dominance::{Dominance, pareto_compare};
|
||||
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct Input {
|
||||
a_objs: Vec<f64>,
|
||||
b_objs: Vec<f64>,
|
||||
a_violation: f64,
|
||||
b_violation: f64,
|
||||
minimize_mask: u8,
|
||||
}
|
||||
|
||||
fuzz_target!(|input: Input| {
|
||||
if input.a_objs.is_empty() || input.a_objs.len() != input.b_objs.len() {
|
||||
return;
|
||||
}
|
||||
if input.a_objs.len() > 8 {
|
||||
return;
|
||||
}
|
||||
let m = input.a_objs.len();
|
||||
let space = ObjectiveSpace::new(
|
||||
(0..m)
|
||||
.map(|i| {
|
||||
if (input.minimize_mask >> i) & 1 == 0 {
|
||||
Objective::minimize(format!("f{i}"))
|
||||
} else {
|
||||
Objective::maximize(format!("f{i}"))
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
|
||||
let a = Evaluation::constrained(input.a_objs.clone(), input.a_violation);
|
||||
let b = Evaluation::constrained(input.b_objs.clone(), input.b_violation);
|
||||
|
||||
let ab = pareto_compare(&a, &b, &space);
|
||||
let ba = pareto_compare(&b, &a, &space);
|
||||
let aa = pareto_compare(&a, &a, &space);
|
||||
|
||||
// Anti-symmetry pairs.
|
||||
let antisymmetric = matches!(
|
||||
(ab, ba),
|
||||
(Dominance::Dominates, Dominance::DominatedBy)
|
||||
| (Dominance::DominatedBy, Dominance::Dominates)
|
||||
| (Dominance::Equal, Dominance::Equal)
|
||||
| (Dominance::NonDominated, Dominance::NonDominated),
|
||||
);
|
||||
assert!(antisymmetric, "asymmetric: ab={ab:?}, ba={ba:?}");
|
||||
|
||||
// Reflexivity (when objectives are finite — NaNs make equality
|
||||
// ill-defined, so skip the check there).
|
||||
if input.a_objs.iter().all(|v| v.is_finite()) && input.a_violation.is_finite() {
|
||||
assert_eq!(aa, Dominance::Equal);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
#![no_main]
|
||||
//! Fuzz SBX + PolynomialMutation: in-bounds parents must produce in-bounds
|
||||
//! children for any seed and any (η, per-variable-probability) pair.
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use heuropt::core::rng::rng_from_seed;
|
||||
use heuropt::prelude::*;
|
||||
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct Input {
|
||||
bounds: Vec<(f64, f64)>,
|
||||
eta_sbx: f64,
|
||||
eta_pm: f64,
|
||||
pvp_sbx: f64,
|
||||
pvp_pm: f64,
|
||||
a_frac: Vec<f64>,
|
||||
b_frac: Vec<f64>,
|
||||
seed: u64,
|
||||
}
|
||||
|
||||
fuzz_target!(|input: Input| {
|
||||
let n = input.bounds.len();
|
||||
if n == 0 || n > 8 {
|
||||
return;
|
||||
}
|
||||
if !(input.eta_sbx.is_finite() && input.eta_pm.is_finite()) {
|
||||
return;
|
||||
}
|
||||
if !(input.eta_sbx >= 1.0 && input.eta_sbx <= 100.0) {
|
||||
return;
|
||||
}
|
||||
if !(input.eta_pm >= 1.0 && input.eta_pm <= 100.0) {
|
||||
return;
|
||||
}
|
||||
let pvp_sbx = match input.pvp_sbx {
|
||||
v if v.is_finite() && (0.0..=1.0).contains(&v) => v,
|
||||
_ => return,
|
||||
};
|
||||
let pvp_pm = match input.pvp_pm {
|
||||
v if v.is_finite() && (0.0..=1.0).contains(&v) => v,
|
||||
_ => return,
|
||||
};
|
||||
// Sanitize bounds: lo < hi, finite.
|
||||
let bounds: Vec<(f64, f64)> = input
|
||||
.bounds
|
||||
.iter()
|
||||
.filter_map(|&(lo, hi)| {
|
||||
if lo.is_finite() && hi.is_finite() && hi - lo > 1e-9 {
|
||||
Some((lo, hi))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if bounds.len() != n {
|
||||
return;
|
||||
}
|
||||
if input.a_frac.len() < n || input.b_frac.len() < n {
|
||||
return;
|
||||
}
|
||||
|
||||
let p1: Vec<f64> = bounds
|
||||
.iter()
|
||||
.zip(&input.a_frac)
|
||||
.map(|(&(lo, hi), &f)| {
|
||||
let frac = if f.is_finite() { f.fract().abs() } else { 0.5 };
|
||||
lo + frac * (hi - lo)
|
||||
})
|
||||
.collect();
|
||||
let p2: Vec<f64> = bounds
|
||||
.iter()
|
||||
.zip(&input.b_frac)
|
||||
.map(|(&(lo, hi), &f)| {
|
||||
let frac = if f.is_finite() { f.fract().abs() } else { 0.5 };
|
||||
lo + frac * (hi - lo)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut rng = rng_from_seed(input.seed);
|
||||
let mut sbx = SimulatedBinaryCrossover::new(bounds.clone(), input.eta_sbx, pvp_sbx);
|
||||
let kids = sbx.vary(&[p1, p2], &mut rng);
|
||||
assert_eq!(kids.len(), 2);
|
||||
for c in &kids {
|
||||
for (j, &v) in c.iter().enumerate() {
|
||||
let (lo, hi) = bounds[j];
|
||||
assert!(v >= lo && v <= hi, "SBX child[{j}] = {v} out of [{lo}, {hi}]");
|
||||
}
|
||||
}
|
||||
|
||||
let mut pm = PolynomialMutation::new(bounds.clone(), input.eta_pm, pvp_pm);
|
||||
let mutated = pm.vary(std::slice::from_ref(&kids[0]), &mut rng);
|
||||
assert_eq!(mutated.len(), 1);
|
||||
for (j, &v) in mutated[0].iter().enumerate() {
|
||||
let (lo, hi) = bounds[j];
|
||||
assert!(v >= lo && v <= hi, "PM child[{j}] = {v} out of [{lo}, {hi}]");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
#![no_main]
|
||||
//! Fuzz the `spacing` metric for non-negativity.
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use heuropt::core::candidate::Candidate;
|
||||
use heuropt::core::evaluation::Evaluation;
|
||||
use heuropt::core::objective::{Objective, ObjectiveSpace};
|
||||
use heuropt::metrics::spacing::spacing;
|
||||
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct Input {
|
||||
points: Vec<(f64, f64)>,
|
||||
}
|
||||
|
||||
fuzz_target!(|input: Input| {
|
||||
if input.points.len() > 64 {
|
||||
return;
|
||||
}
|
||||
// Bound magnitudes so distance computations don't overflow to
|
||||
// inf-inf=NaN — `spacing` is documented to operate on values produced
|
||||
// by `as_minimization` of problem evaluations, not arbitrary f64s.
|
||||
if input
|
||||
.points
|
||||
.iter()
|
||||
.any(|&(a, b)| !a.is_finite() || !b.is_finite() || a.abs() > 1e150 || b.abs() > 1e150)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let space = ObjectiveSpace::new(vec![Objective::minimize("f1"), Objective::minimize("f2")]);
|
||||
let pop: Vec<Candidate<()>> = input
|
||||
.points
|
||||
.iter()
|
||||
.map(|&(a, b)| Candidate::new((), Evaluation::new(vec![a, b])))
|
||||
.collect();
|
||||
let s = spacing(&pop, &space);
|
||||
// Spacing can overflow to +∞ when point coordinates straddle ±f64::MAX.
|
||||
// Contract is non-negative + non-NaN.
|
||||
assert!(s >= 0.0, "spacing negative: {s}");
|
||||
assert!(!s.is_nan(), "spacing is NaN");
|
||||
});
|
||||
Reference in New Issue
Block a user