//! Per-operator property tests covering every Variation / Initializer / //! Repair impl heuropt ships. use proptest::prelude::*; use heuropt::core::rng::rng_from_seed; use heuropt::prelude::*; /// Generate per-axis bounds whose width is at least 0.001 (avoid the /// degenerate `lo == hi` case for properties that need a proper interval). fn bounds(dim: usize) -> impl Strategy> { prop::collection::vec((-50.0_f64..50.0, 0.001_f64..50.0), dim..=dim) .prop_map(|pairs| pairs.into_iter().map(|(lo, span)| (lo, lo + span)).collect()) } /// Generate a parent vector inside the given bounds. fn parent_in_bounds(bounds: &[(f64, f64)]) -> Vec { bounds.iter().map(|&(lo, hi)| 0.5 * (lo + hi)).collect() } // ----------------------------------------------------------------------------- // Initializers // ----------------------------------------------------------------------------- proptest! { #[test] fn real_bounds_returns_correct_shape( bounds in bounds(4), size in 1usize..30, seed in any::(), ) { let mut rng = rng_from_seed(seed); let mut init = RealBounds::new(bounds.clone()); let decisions = init.initialize(size, &mut rng); prop_assert_eq!(decisions.len(), size); for d in &decisions { prop_assert_eq!(d.len(), 4); for (j, &v) in d.iter().enumerate() { let (lo, hi) = bounds[j]; prop_assert!(v >= lo && v <= hi, "{v} out of [{lo}, {hi}]"); } } } #[test] fn real_bounds_size_zero_returns_empty( bounds in bounds(3), seed in any::(), ) { let mut rng = rng_from_seed(seed); let mut init = RealBounds::new(bounds); let decisions = init.initialize(0, &mut rng); prop_assert!(decisions.is_empty()); } } // ----------------------------------------------------------------------------- // Real-valued Variation operators // ----------------------------------------------------------------------------- proptest! { #[test] fn gaussian_mutation_preserves_length( sigma in 1e-6_f64..5.0, len in 1usize..10, seed in any::(), ) { let mut rng = rng_from_seed(seed); let parent: Vec = vec![0.0; len]; let mut m = GaussianMutation { sigma }; let children = m.vary(std::slice::from_ref(&parent), &mut rng); prop_assert_eq!(children.len(), 1); prop_assert_eq!(children[0].len(), len); } #[test] fn bounded_gaussian_mutation_in_bounds( sigma in 1e-6_f64..5.0, bounds in bounds(4), seed in any::(), ) { let mut rng = rng_from_seed(seed); let parent = parent_in_bounds(&bounds); let mut m = BoundedGaussianMutation::new(sigma, bounds.clone()); let children = m.vary(std::slice::from_ref(&parent), &mut rng); prop_assert_eq!(children.len(), 1); for (j, &v) in children[0].iter().enumerate() { let (lo, hi) = bounds[j]; prop_assert!(v >= lo && v <= hi); } } #[test] fn bit_flip_mutation_preserves_length( probability in 0.0_f64..=1.0, len in 1usize..32, seed in any::(), ) { let mut rng = rng_from_seed(seed); let parent: Vec = (0..len).map(|i| i % 2 == 0).collect(); let mut m = BitFlipMutation { probability }; let children = m.vary(std::slice::from_ref(&parent), &mut rng); prop_assert_eq!(children.len(), 1); prop_assert_eq!(children[0].len(), len); } #[test] fn swap_mutation_is_a_permutation( len in 2usize..16, seed in any::(), ) { let mut rng = rng_from_seed(seed); let parent: Vec = (0..len).collect(); let mut m = SwapMutation; let children = m.vary(std::slice::from_ref(&parent), &mut rng); prop_assert_eq!(children.len(), 1); let mut sorted = children[0].clone(); sorted.sort(); let identity: Vec = (0..len).collect(); prop_assert_eq!(sorted, identity); } #[test] fn sbx_in_bounds( bounds in bounds(3), eta in 1.0_f64..30.0, per_var_p in 0.0_f64..=1.0, a_frac in 0.0_f64..1.0, b_frac in 0.0_f64..1.0, seed in any::(), ) { let mut rng = rng_from_seed(seed); let p1: Vec = bounds.iter().map(|&(lo, hi)| lo + a_frac * (hi - lo)).collect(); let p2: Vec = bounds.iter().map(|&(lo, hi)| lo + b_frac * (hi - lo)).collect(); let mut sbx = SimulatedBinaryCrossover::new(bounds.clone(), eta, per_var_p); let children = sbx.vary(&[p1, p2], &mut rng); prop_assert_eq!(children.len(), 2); for c in &children { for (j, &v) in c.iter().enumerate() { let (lo, hi) = bounds[j]; prop_assert!(v >= lo && v <= hi); } } } #[test] fn polymut_in_bounds( bounds in bounds(3), eta in 1.0_f64..40.0, per_var_p in 0.0_f64..=1.0, seed in any::(), ) { let mut rng = rng_from_seed(seed); let parent = parent_in_bounds(&bounds); let mut pm = PolynomialMutation::new(bounds.clone(), eta, per_var_p); let children = pm.vary(std::slice::from_ref(&parent), &mut rng); prop_assert_eq!(children.len(), 1); for (j, &v) in children[0].iter().enumerate() { let (lo, hi) = bounds[j]; prop_assert!(v >= lo && v <= hi); } } #[test] fn levy_mutation_in_bounds( bounds in bounds(3), alpha in 0.5_f64..2.0, scale in 0.01_f64..1.0, seed in any::(), ) { let mut rng = rng_from_seed(seed); let parent = parent_in_bounds(&bounds); let mut m = LevyMutation::new(alpha, scale, bounds.clone()); let children = m.vary(std::slice::from_ref(&parent), &mut rng); prop_assert_eq!(children.len(), 1); for (j, &v) in children[0].iter().enumerate() { let (lo, hi) = bounds[j]; prop_assert!(v >= lo && v <= hi); } } #[test] fn composite_variation_preserves_count( bounds in bounds(3), a_frac in 0.0_f64..1.0, b_frac in 0.0_f64..1.0, seed in any::(), ) { let mut rng = rng_from_seed(seed); let p1: Vec = bounds.iter().map(|&(lo, hi)| lo + a_frac * (hi - lo)).collect(); let p2: Vec = bounds.iter().map(|&(lo, hi)| lo + b_frac * (hi - lo)).collect(); // SBX produces 2 children, PolyMut produces 1 each → expect 2. let mut v = CompositeVariation { crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5), mutation: PolynomialMutation::new(bounds, 20.0, 0.5), }; let children = v.vary(&[p1, p2], &mut rng); prop_assert_eq!(children.len(), 2); } } // ----------------------------------------------------------------------------- // Repair operators // ----------------------------------------------------------------------------- proptest! { #[test] fn clamp_to_bounds_lands_in_bounds( bounds in bounds(5), seed in any::(), ) { use rand::Rng as _; let mut rng = rng_from_seed(seed); let mut x: Vec = (0..5).map(|_| rng.random_range(-1000.0..=1000.0)).collect(); let mut r = ClampToBounds::new(bounds.clone()); r.repair(&mut x); for (j, &v) in x.iter().enumerate() { let (lo, hi) = bounds[j]; prop_assert!(v >= lo && v <= hi); } } #[test] fn clamp_to_bounds_idempotent( bounds in bounds(5), seed in any::(), ) { use rand::Rng as _; let mut rng = rng_from_seed(seed); let mut x: Vec = (0..5).map(|_| rng.random_range(-1000.0..=1000.0)).collect(); let mut r = ClampToBounds::new(bounds); r.repair(&mut x); let after_one = x.clone(); r.repair(&mut x); prop_assert_eq!(x, after_one); } #[test] fn project_to_simplex_lands_in_simplex( n in 2usize..8, total in 0.5_f64..10.0, seed in any::(), ) { use rand::Rng as _; let mut rng = rng_from_seed(seed); let mut x: Vec = (0..n).map(|_| rng.random_range(-5.0..5.0)).collect(); let mut r = ProjectToSimplex::new(total); r.repair(&mut x); for &v in &x { prop_assert!(v >= 0.0); } let s: f64 = x.iter().sum(); prop_assert!((s - total).abs() < 1e-9); } #[test] fn project_to_simplex_idempotent( n in 2usize..8, total in 0.5_f64..5.0, seed in any::(), ) { use rand::Rng as _; let mut rng = rng_from_seed(seed); let mut x: Vec = (0..n).map(|_| rng.random_range(-5.0..5.0)).collect(); let mut r = ProjectToSimplex::new(total); r.repair(&mut x); let after_one = x.clone(); r.repair(&mut x); for (a, b) in after_one.iter().zip(x.iter()) { prop_assert!((a - b).abs() < 1e-9); } } }