Skip to main content

radiate_alters/mutators/
polynomial.rs

1use radiate_core::{BoundedGene, Chromosome, FloatGene, Gene, Mutate, Rate, random_provider};
2use radiate_utils::{Float, Primitive};
3
4// Use it when:
5// 	- You’re evolving floating-point representations (like real-valued neural nets, control parameters, orbital mechanics).
6// 	- You want bounded and unbiased mutation behavior.
7// 	- You care about the distribution of the mutations. Unlike Gaussian, polynomial gives more control over tail behavior.
8
9// It’s especially powerful in:
10// 	- Multi-objective algorithms (like NSGA-II)
11// 	- High-precision tuning problems
12// 	- Bounded domains where classic Gaussian mutation may overshoot
13
14// The eta parameter in polynomial mutation controls the shape of the mutation distribution.
15// In other words, how local or global your mutations are:
16// 	- eta is the distribution index (usually denoted as η_m in literature like Deb’s NSGA-II paper).
17// 	- It determines the exploration vs. exploitation trade-off:
18// 	- Low eta (e.g. 1–5): leads to bigger mutations, promoting exploration.
19// 	- High eta (e.g. 20–100): leads to smaller, fine-grained mutations, good for local search.
20#[derive(Debug, Clone)]
21pub struct PolynomialMutator {
22    rate: Rate,
23    eta: f32,
24}
25
26impl PolynomialMutator {
27    pub fn new(rate: impl Into<Rate>, eta: f32) -> Self {
28        let rate = rate.into();
29        PolynomialMutator { rate, eta }
30    }
31
32    fn polynomial_mutation(&self, value: f64, min: f64, max: f64, eta: f64) -> f64 {
33        let u = random_provider::random::<f64>();
34
35        if (max - min).abs() < f64::EPSILON {
36            return value;
37        }
38
39        let delta1 = (value - min) / (max - min);
40        let delta2 = (max - value) / (max - min);
41
42        let mutq = if u <= 0.5 {
43            // Left side of the polynomial
44            let term1 = 2.0 * u;
45            let term2 = (1.0 - 2.0 * u) * (1.0 - delta1).powf(eta + 1.0);
46            (term1 + term2).powf(1.0 / (eta + 1.0))
47        } else {
48            // Right side of the polynomial
49            let term1 = 2.0 * (1.0 - u);
50            let term2 = 2.0 * (u - 0.5) * (1.0 - delta2).powf(eta + 1.0);
51            1.0 - (term1 + term2).powf(1.0 / (eta + 1.0))
52        };
53
54        min + mutq * (max - min)
55    }
56}
57
58impl<F, C> Mutate<C> for PolynomialMutator
59where
60    F: Float + Primitive,
61    C: Chromosome<Gene = FloatGene<F>>,
62{
63    fn rate(&self) -> Rate {
64        self.rate.clone()
65    }
66
67    #[inline]
68    fn mutate_gene(&self, gene: &mut C::Gene) -> usize {
69        let (lower, upper) = gene.bounds();
70        let min = lower.extract::<f64>().unwrap();
71        let max = upper.extract::<f64>().unwrap();
72        let value = gene.allele().extract::<f64>().unwrap();
73        let eta = self.eta as f64;
74
75        let new_value = self.polynomial_mutation(value, min, max, eta);
76
77        let clamped_value = new_value.clamp(min, max);
78        *gene.allele_mut() = clamped_value.extract::<F>().unwrap();
79        1
80    }
81}