Skip to main content

fugue_evo/operators/
mutation.rs

1//! Mutation operators
2//!
3//! This module provides various mutation operators for genetic algorithms.
4
5use rand::Rng;
6use rand_distr::{Distribution, Normal};
7
8use crate::genome::bit_string::BitString;
9use crate::genome::bounds::MultiBounds;
10use crate::genome::permutation::Permutation;
11use crate::genome::real_vector::RealVector;
12use crate::genome::traits::{
13    BinaryGenome, EvolutionaryGenome, PermutationGenome, RealValuedGenome,
14};
15use crate::genome::tree::{Function, Terminal, TreeGenome, TreeNode};
16use crate::operators::traits::{BoundedMutationOperator, MutationOperator};
17
18/// Polynomial mutation (bounded)
19///
20/// Uses the polynomial probability distribution to perturb genes.
21/// Respects bounds and is commonly used with NSGA-II.
22///
23/// Reference: Deb, K. (2001). Multi-Objective Optimization using Evolutionary Algorithms.
24#[derive(Clone, Debug)]
25pub struct PolynomialMutation {
26    /// Distribution index (typically 20-100)
27    /// Higher values = smaller mutations
28    pub eta_m: f64,
29    /// Per-gene mutation probability (default: 1/n)
30    pub mutation_probability: Option<f64>,
31    /// Standard deviation used by the *unbounded* fallback (see
32    /// [`MutationOperator::mutate`]). `None` selects the adaptive default
33    /// `0.1 * (1 + |x|)` per gene; `Some(s)` uses a fixed `s`.
34    pub unbounded_sigma: Option<f64>,
35}
36
37impl PolynomialMutation {
38    /// Create a new polynomial mutation with the given distribution index
39    pub fn new(eta_m: f64) -> Self {
40        assert!(eta_m >= 0.0, "Distribution index must be non-negative");
41        Self {
42            eta_m,
43            mutation_probability: None,
44            unbounded_sigma: None,
45        }
46    }
47
48    /// Set a fixed mutation probability per gene
49    pub fn with_probability(mut self, probability: f64) -> Self {
50        assert!(
51            (0.0..=1.0).contains(&probability),
52            "Probability must be in [0, 1]"
53        );
54        self.mutation_probability = Some(probability);
55        self
56    }
57
58    /// Set the standard deviation of the unbounded Gaussian fallback.
59    ///
60    /// Polynomial mutation is only defined relative to finite bounds; when the
61    /// operator is invoked without bounds it perturbs each gene with Gaussian
62    /// noise instead (audit EV-102). By default the per-gene sigma is
63    /// `0.1 * (1 + |x|)`; this method pins it to a fixed value.
64    pub fn with_unbounded_sigma(mut self, sigma: f64) -> Self {
65        assert!(sigma >= 0.0, "Sigma must be non-negative");
66        self.unbounded_sigma = Some(sigma);
67        self
68    }
69
70    /// Apply polynomial mutation to a gene
71    fn mutate_gene<R: Rng>(&self, gene: f64, min: f64, max: f64, rng: &mut R) -> f64 {
72        let range = max - min;
73        if range <= 0.0 {
74            return gene;
75        }
76
77        let delta1 = (gene - min) / range;
78        let delta2 = (max - gene) / range;
79
80        let u = rng.gen::<f64>();
81        let delta_q = if u <= 0.5 {
82            let val = 2.0 * u + (1.0 - 2.0 * u) * (1.0 - delta1).powf(self.eta_m + 1.0);
83            val.powf(1.0 / (self.eta_m + 1.0)) - 1.0
84        } else {
85            let val = 2.0 * (1.0 - u) + 2.0 * (u - 0.5) * (1.0 - delta2).powf(self.eta_m + 1.0);
86            1.0 - val.powf(1.0 / (self.eta_m + 1.0))
87        };
88
89        (gene + delta_q * range).clamp(min, max)
90    }
91}
92
93impl MutationOperator<RealVector> for PolynomialMutation {
94    fn mutate<R: Rng>(&self, genome: &mut RealVector, rng: &mut R) {
95        // Polynomial mutation is intrinsically bounds-relative and is undefined
96        // without a finite range. Rather than fabricating +/-1e10 bounds (which
97        // turned an "unbounded" mutation into a destructive, near-random reset
98        // of each gene), fall back to a local Gaussian perturbation (EV-102).
99        let n = genome.dimension();
100        let prob = self.mutation_probability.unwrap_or(1.0 / n as f64);
101
102        for gene in genome.genes_mut() {
103            if rng.gen::<f64>() < prob {
104                let sigma = self
105                    .unbounded_sigma
106                    .unwrap_or_else(|| 0.1 * (1.0 + gene.abs()));
107                if sigma > 0.0 {
108                    let normal = Normal::new(0.0, sigma).unwrap();
109                    *gene += normal.sample(rng);
110                }
111            }
112        }
113    }
114
115    fn mutation_probability(&self) -> Option<f64> {
116        self.mutation_probability
117    }
118}
119
120impl BoundedMutationOperator<RealVector> for PolynomialMutation {
121    fn mutate_bounded<R: Rng>(&self, genome: &mut RealVector, bounds: &MultiBounds, rng: &mut R) {
122        let n = genome.dimension();
123        let prob = self.mutation_probability.unwrap_or(1.0 / n as f64);
124
125        for i in 0..n {
126            if rng.gen::<f64>() < prob {
127                if let Some(bound) = bounds.get(i) {
128                    genome.genes_mut()[i] =
129                        self.mutate_gene(genome.genes()[i], bound.min, bound.max, rng);
130                }
131            }
132        }
133    }
134}
135
136/// Gaussian mutation
137///
138/// Adds Gaussian noise to each gene.
139#[derive(Clone, Debug)]
140pub struct GaussianMutation {
141    /// Standard deviation of the Gaussian noise
142    pub sigma: f64,
143    /// Per-gene mutation probability
144    pub mutation_probability: Option<f64>,
145}
146
147impl GaussianMutation {
148    /// Create a new Gaussian mutation with the given standard deviation
149    pub fn new(sigma: f64) -> Self {
150        assert!(sigma >= 0.0, "Sigma must be non-negative");
151        Self {
152            sigma,
153            mutation_probability: None,
154        }
155    }
156
157    /// Set a fixed mutation probability per gene
158    pub fn with_probability(mut self, probability: f64) -> Self {
159        assert!(
160            (0.0..=1.0).contains(&probability),
161            "Probability must be in [0, 1]"
162        );
163        self.mutation_probability = Some(probability);
164        self
165    }
166}
167
168impl MutationOperator<RealVector> for GaussianMutation {
169    fn mutate<R: Rng>(&self, genome: &mut RealVector, rng: &mut R) {
170        let n = genome.dimension();
171        let prob = self.mutation_probability.unwrap_or(1.0 / n as f64);
172        let normal = Normal::new(0.0, self.sigma).unwrap();
173
174        for gene in genome.genes_mut() {
175            if rng.gen::<f64>() < prob {
176                *gene += normal.sample(rng);
177            }
178        }
179    }
180
181    fn mutation_probability(&self) -> Option<f64> {
182        self.mutation_probability
183    }
184}
185
186impl BoundedMutationOperator<RealVector> for GaussianMutation {
187    fn mutate_bounded<R: Rng>(&self, genome: &mut RealVector, bounds: &MultiBounds, rng: &mut R) {
188        let n = genome.dimension();
189        let prob = self.mutation_probability.unwrap_or(1.0 / n as f64);
190        let normal = Normal::new(0.0, self.sigma).unwrap();
191
192        for i in 0..n {
193            if rng.gen::<f64>() < prob {
194                genome.genes_mut()[i] += normal.sample(rng);
195                if let Some(bound) = bounds.get(i) {
196                    genome.genes_mut()[i] = bound.clamp(genome.genes()[i]);
197                }
198            }
199        }
200    }
201}
202
203/// Uniform mutation
204///
205/// Replaces genes with random values within bounds.
206#[derive(Clone, Debug)]
207pub struct UniformMutation {
208    /// Per-gene mutation probability
209    pub mutation_probability: Option<f64>,
210}
211
212impl UniformMutation {
213    /// Create a new uniform mutation
214    pub fn new() -> Self {
215        Self {
216            mutation_probability: None,
217        }
218    }
219
220    /// Set a fixed mutation probability per gene
221    pub fn with_probability(mut self, probability: f64) -> Self {
222        assert!(
223            (0.0..=1.0).contains(&probability),
224            "Probability must be in [0, 1]"
225        );
226        self.mutation_probability = Some(probability);
227        self
228    }
229}
230
231impl Default for UniformMutation {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237impl MutationOperator<RealVector> for UniformMutation {
238    fn mutate<R: Rng>(&self, genome: &mut RealVector, rng: &mut R) {
239        // Uniform mutation requires a finite range; without explicit bounds we
240        // fall back to the modest, non-destructive default range [-1, 1].
241        // Prefer [`mutate_bounded`] with real bounds whenever they are known.
242        let default_bounds = MultiBounds::symmetric(1.0, genome.dimension());
243        self.mutate_bounded(genome, &default_bounds, rng);
244    }
245
246    fn mutation_probability(&self) -> Option<f64> {
247        self.mutation_probability
248    }
249}
250
251impl BoundedMutationOperator<RealVector> for UniformMutation {
252    fn mutate_bounded<R: Rng>(&self, genome: &mut RealVector, bounds: &MultiBounds, rng: &mut R) {
253        let n = genome.dimension();
254        let prob = self.mutation_probability.unwrap_or(1.0 / n as f64);
255
256        for i in 0..n {
257            if rng.gen::<f64>() < prob {
258                if let Some(bound) = bounds.get(i) {
259                    genome.genes_mut()[i] = rng.gen_range(bound.min..=bound.max);
260                }
261            }
262        }
263    }
264}
265
266/// Bit-flip mutation for bit strings
267///
268/// Flips each bit with a given probability.
269#[derive(Clone, Debug)]
270pub struct BitFlipMutation {
271    /// Per-bit mutation probability (default: 1/n)
272    pub mutation_probability: Option<f64>,
273}
274
275impl BitFlipMutation {
276    /// Create a new bit-flip mutation
277    pub fn new() -> Self {
278        Self {
279            mutation_probability: None,
280        }
281    }
282
283    /// Set a fixed mutation probability per bit
284    pub fn with_probability(mut self, probability: f64) -> Self {
285        assert!(
286            (0.0..=1.0).contains(&probability),
287            "Probability must be in [0, 1]"
288        );
289        self.mutation_probability = Some(probability);
290        self
291    }
292}
293
294impl Default for BitFlipMutation {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300impl MutationOperator<BitString> for BitFlipMutation {
301    fn mutate<R: Rng>(&self, genome: &mut BitString, rng: &mut R) {
302        let n = genome.len();
303        let prob = self.mutation_probability.unwrap_or(1.0 / n as f64);
304
305        for i in 0..n {
306            if rng.gen::<f64>() < prob {
307                genome.flip(i);
308            }
309        }
310    }
311
312    fn mutation_probability(&self) -> Option<f64> {
313        self.mutation_probability
314    }
315}
316
317/// Swap mutation for permutation genomes (also works on any genome)
318///
319/// Swaps two random positions in the genome.
320#[derive(Clone, Debug)]
321pub struct SwapMutation {
322    /// Number of swaps to perform
323    pub num_swaps: usize,
324}
325
326impl SwapMutation {
327    /// Create a new swap mutation with a single swap
328    pub fn new() -> Self {
329        Self { num_swaps: 1 }
330    }
331
332    /// Create with multiple swaps
333    pub fn with_swaps(num_swaps: usize) -> Self {
334        Self { num_swaps }
335    }
336}
337
338impl Default for SwapMutation {
339    /// Delegates to [`SwapMutation::new`] (a single swap). A derived `Default`
340    /// would set `num_swaps = 0`, producing a silent no-op operator (EV-101).
341    fn default() -> Self {
342        Self::new()
343    }
344}
345
346impl MutationOperator<BitString> for SwapMutation {
347    fn mutate<R: Rng>(&self, genome: &mut BitString, rng: &mut R) {
348        let n = genome.len();
349        if n < 2 {
350            return;
351        }
352
353        for _ in 0..self.num_swaps {
354            let i = rng.gen_range(0..n);
355            let j = rng.gen_range(0..n);
356            if i != j {
357                let temp = genome.bits()[i];
358                genome.bits_mut()[i] = genome.bits()[j];
359                genome.bits_mut()[j] = temp;
360            }
361        }
362    }
363}
364
365impl MutationOperator<RealVector> for SwapMutation {
366    fn mutate<R: Rng>(&self, genome: &mut RealVector, rng: &mut R) {
367        let n = genome.dimension();
368        if n < 2 {
369            return;
370        }
371
372        for _ in 0..self.num_swaps {
373            let i = rng.gen_range(0..n);
374            let j = rng.gen_range(0..n);
375            if i != j {
376                genome.genes_mut().swap(i, j);
377            }
378        }
379    }
380}
381
382/// Scramble mutation
383///
384/// Scrambles a random segment of the genome.
385#[derive(Clone, Debug, Default)]
386pub struct ScrambleMutation;
387
388impl ScrambleMutation {
389    /// Create a new scramble mutation
390    pub fn new() -> Self {
391        Self
392    }
393}
394
395impl MutationOperator<BitString> for ScrambleMutation {
396    fn mutate<R: Rng>(&self, genome: &mut BitString, rng: &mut R) {
397        use rand::seq::SliceRandom;
398
399        let n = genome.len();
400        if n < 2 {
401            return;
402        }
403
404        let mut start = rng.gen_range(0..n);
405        let mut end = rng.gen_range(0..n);
406        if start > end {
407            std::mem::swap(&mut start, &mut end);
408        }
409
410        // Extract segment, shuffle, and put back
411        let segment: Vec<bool> = (start..=end).map(|i| genome.bits()[i]).collect();
412        let mut shuffled = segment;
413        shuffled.shuffle(rng);
414
415        for (i, val) in shuffled.into_iter().enumerate() {
416            genome.bits_mut()[start + i] = val;
417        }
418    }
419}
420
421impl MutationOperator<RealVector> for ScrambleMutation {
422    fn mutate<R: Rng>(&self, genome: &mut RealVector, rng: &mut R) {
423        use rand::seq::SliceRandom;
424
425        let n = genome.dimension();
426        if n < 2 {
427            return;
428        }
429
430        let mut start = rng.gen_range(0..n);
431        let mut end = rng.gen_range(0..n);
432        if start > end {
433            std::mem::swap(&mut start, &mut end);
434        }
435
436        // Shuffle the segment in place
437        let slice = &mut genome.genes_mut()[start..=end];
438        slice.shuffle(rng);
439    }
440}
441
442// =============================================================================
443// Permutation Mutation Operators
444// =============================================================================
445
446/// Swap mutation for permutation genomes
447///
448/// Swaps two random positions in the permutation.
449/// This is one of the simplest and most commonly used permutation mutations.
450#[derive(Clone, Debug)]
451pub struct PermutationSwapMutation {
452    /// Number of swaps to perform
453    pub num_swaps: usize,
454}
455
456impl PermutationSwapMutation {
457    /// Create a new swap mutation with a single swap
458    pub fn new() -> Self {
459        Self { num_swaps: 1 }
460    }
461
462    /// Create with multiple swaps
463    pub fn with_swaps(num_swaps: usize) -> Self {
464        Self { num_swaps }
465    }
466}
467
468impl Default for PermutationSwapMutation {
469    /// Delegates to [`PermutationSwapMutation::new`] (a single swap). A derived
470    /// `Default` would set `num_swaps = 0`, a silent no-op operator (EV-101).
471    fn default() -> Self {
472        Self::new()
473    }
474}
475
476impl MutationOperator<Permutation> for PermutationSwapMutation {
477    fn mutate<R: Rng>(&self, genome: &mut Permutation, rng: &mut R) {
478        let n = genome.dimension();
479        if n < 2 {
480            return;
481        }
482
483        for _ in 0..self.num_swaps {
484            let i = rng.gen_range(0..n);
485            let j = rng.gen_range(0..n);
486            if i != j {
487                genome.swap(i, j);
488            }
489        }
490    }
491}
492
493/// Insert mutation for permutation genomes
494///
495/// Removes an element from one position and inserts it at another.
496/// This preserves adjacencies better than swap mutation.
497#[derive(Clone, Debug)]
498pub struct InsertMutation {
499    /// Number of insert operations to perform
500    pub num_inserts: usize,
501}
502
503impl InsertMutation {
504    /// Create a new insert mutation with a single insert
505    pub fn new() -> Self {
506        Self { num_inserts: 1 }
507    }
508
509    /// Create with multiple inserts
510    pub fn with_inserts(num_inserts: usize) -> Self {
511        Self { num_inserts }
512    }
513}
514
515impl Default for InsertMutation {
516    /// Delegates to [`InsertMutation::new`] (a single insert). A derived
517    /// `Default` would set `num_inserts = 0`, a silent no-op operator (EV-101).
518    fn default() -> Self {
519        Self::new()
520    }
521}
522
523impl MutationOperator<Permutation> for InsertMutation {
524    fn mutate<R: Rng>(&self, genome: &mut Permutation, rng: &mut R) {
525        let n = genome.dimension();
526        if n < 2 {
527            return;
528        }
529
530        for _ in 0..self.num_inserts {
531            let from = rng.gen_range(0..n);
532            let to = rng.gen_range(0..n);
533            if from != to {
534                genome.insert(from, to);
535            }
536        }
537    }
538}
539
540/// Inversion mutation (2-opt) for permutation genomes
541///
542/// Reverses a random segment of the permutation.
543/// This is particularly effective for TSP-like problems as it can
544/// remove crossing edges.
545#[derive(Clone, Debug, Default)]
546pub struct InversionMutation;
547
548impl InversionMutation {
549    /// Create a new inversion mutation
550    pub fn new() -> Self {
551        Self
552    }
553}
554
555impl MutationOperator<Permutation> for InversionMutation {
556    fn mutate<R: Rng>(&self, genome: &mut Permutation, rng: &mut R) {
557        let n = genome.dimension();
558        if n < 2 {
559            return;
560        }
561
562        let mut start = rng.gen_range(0..n);
563        let mut end = rng.gen_range(0..n);
564        if start > end {
565            std::mem::swap(&mut start, &mut end);
566        }
567
568        genome.reverse_segment(start, end);
569    }
570}
571
572/// Scramble mutation for permutation genomes
573///
574/// Shuffles a random segment of the permutation.
575/// More disruptive than inversion, but still preserves some structure.
576#[derive(Clone, Debug, Default)]
577pub struct PermutationScrambleMutation;
578
579impl PermutationScrambleMutation {
580    /// Create a new scramble mutation
581    pub fn new() -> Self {
582        Self
583    }
584}
585
586impl MutationOperator<Permutation> for PermutationScrambleMutation {
587    fn mutate<R: Rng>(&self, genome: &mut Permutation, rng: &mut R) {
588        use rand::seq::SliceRandom;
589
590        let n = genome.dimension();
591        if n < 2 {
592            return;
593        }
594
595        let mut start = rng.gen_range(0..n);
596        let mut end = rng.gen_range(0..n);
597        if start > end {
598            std::mem::swap(&mut start, &mut end);
599        }
600
601        // Shuffle the segment
602        let perm = genome.permutation_mut();
603        perm[start..=end].shuffle(rng);
604    }
605}
606
607/// Displacement mutation for permutation genomes
608///
609/// Selects a segment, removes it, and inserts it at a random position.
610/// This is similar to insert mutation but operates on segments.
611#[derive(Clone, Debug, Default)]
612pub struct DisplacementMutation;
613
614impl DisplacementMutation {
615    /// Create a new displacement mutation
616    pub fn new() -> Self {
617        Self
618    }
619}
620
621impl MutationOperator<Permutation> for DisplacementMutation {
622    fn mutate<R: Rng>(&self, genome: &mut Permutation, rng: &mut R) {
623        let n = genome.dimension();
624        if n < 3 {
625            return;
626        }
627
628        // Select segment
629        let mut start = rng.gen_range(0..n);
630        let mut end = rng.gen_range(0..n);
631        if start > end {
632            std::mem::swap(&mut start, &mut end);
633        }
634
635        let segment_len = end - start + 1;
636        if segment_len >= n {
637            return; // Can't displace the entire permutation
638        }
639
640        // Extract segment
641        let perm = genome.permutation_mut();
642        let segment: Vec<usize> = perm[start..=end].to_vec();
643
644        // Remove segment
645        let remaining: Vec<usize> = perm[..start]
646            .iter()
647            .chain(perm[end + 1..].iter())
648            .copied()
649            .collect();
650
651        // Choose insertion point in remaining
652        let insert_pos = rng.gen_range(0..=remaining.len());
653
654        // Rebuild permutation
655        let new_perm: Vec<usize> = remaining[..insert_pos]
656            .iter()
657            .chain(segment.iter())
658            .chain(remaining[insert_pos..].iter())
659            .copied()
660            .collect();
661
662        perm.copy_from_slice(&new_perm);
663    }
664}
665
666/// Adaptive mutation rate for permutation genomes
667///
668/// Combines multiple mutation operators with configurable probabilities.
669#[derive(Clone, Debug)]
670pub struct AdaptivePermutationMutation {
671    /// Probability of swap mutation
672    pub swap_prob: f64,
673    /// Probability of insert mutation
674    pub insert_prob: f64,
675    /// Probability of inversion mutation
676    pub inversion_prob: f64,
677    /// Probability of scramble mutation
678    pub scramble_prob: f64,
679}
680
681impl AdaptivePermutationMutation {
682    /// Create with default probabilities (each equally likely)
683    pub fn new() -> Self {
684        Self {
685            swap_prob: 0.25,
686            insert_prob: 0.25,
687            inversion_prob: 0.25,
688            scramble_prob: 0.25,
689        }
690    }
691
692    /// Create with custom probabilities
693    pub fn with_probs(swap: f64, insert: f64, inversion: f64, scramble: f64) -> Self {
694        Self {
695            swap_prob: swap,
696            insert_prob: insert,
697            inversion_prob: inversion,
698            scramble_prob: scramble,
699        }
700    }
701}
702
703impl Default for AdaptivePermutationMutation {
704    fn default() -> Self {
705        Self::new()
706    }
707}
708
709impl MutationOperator<Permutation> for AdaptivePermutationMutation {
710    fn mutate<R: Rng>(&self, genome: &mut Permutation, rng: &mut R) {
711        let total = self.swap_prob + self.insert_prob + self.inversion_prob + self.scramble_prob;
712        if total <= 0.0 {
713            return;
714        }
715
716        let r = rng.gen::<f64>() * total;
717        let mut cumulative = 0.0;
718
719        cumulative += self.swap_prob;
720        if r < cumulative {
721            PermutationSwapMutation::new().mutate(genome, rng);
722            return;
723        }
724
725        cumulative += self.insert_prob;
726        if r < cumulative {
727            InsertMutation::new().mutate(genome, rng);
728            return;
729        }
730
731        cumulative += self.inversion_prob;
732        if r < cumulative {
733            InversionMutation::new().mutate(genome, rng);
734            return;
735        }
736
737        PermutationScrambleMutation::new().mutate(genome, rng);
738    }
739}
740
741// =============================================================================
742// Tree (GP) Mutation Operators
743// =============================================================================
744
745/// Point mutation for tree genomes (genetic programming)
746///
747/// Selects a random node and replaces it with a new random node of the same
748/// type. For function nodes, the replacement has the same arity. For terminal
749/// nodes, another random terminal is selected.
750///
751/// This is a non-destructive mutation that preserves tree structure while
752/// changing individual nodes.
753#[derive(Clone, Debug)]
754pub struct PointMutation {
755    /// Per-node mutation probability
756    pub mutation_probability: f64,
757    /// Probability of selecting a function node (vs terminal)
758    pub function_probability: f64,
759}
760
761impl PointMutation {
762    /// Create a new point mutation with default settings
763    ///
764    /// Defaults: 0.1 per-node probability, 0.9 function probability
765    pub fn new() -> Self {
766        Self {
767            mutation_probability: 0.1,
768            function_probability: 0.9,
769        }
770    }
771
772    /// Set the per-node mutation probability
773    pub fn with_probability(mut self, probability: f64) -> Self {
774        assert!(
775            (0.0..=1.0).contains(&probability),
776            "Probability must be in [0, 1]"
777        );
778        self.mutation_probability = probability;
779        self
780    }
781
782    /// Set the function selection probability
783    pub fn with_function_probability(mut self, probability: f64) -> Self {
784        assert!(
785            (0.0..=1.0).contains(&probability),
786            "Probability must be in [0, 1]"
787        );
788        self.function_probability = probability;
789        self
790    }
791
792    /// Mutate a single node *in place*, preserving its arity.
793    ///
794    /// A terminal is replaced with a fresh random terminal; a function is swapped
795    /// for a randomly chosen function of the same arity (its children are left
796    /// untouched, so tree structure is preserved). In-place mutation via `&mut`
797    /// (rather than returning a new owned node) is what lets the whole traversal
798    /// avoid moving values out of an owned `TreeNode`, which would conflict with
799    /// `TreeNode`'s stack-safe `Drop` impl (EV-60).
800    fn mutate_node_in_place<T: Terminal, F: Function, R: Rng>(
801        &self,
802        node: &mut TreeNode<T, F>,
803        rng: &mut R,
804    ) {
805        match node {
806            TreeNode::Terminal(t) => *t = T::random(rng),
807            TreeNode::Function(func, _children) => {
808                let target_arity = func.arity();
809                let matching_funcs: Vec<&F> = F::functions()
810                    .iter()
811                    .filter(|f| f.arity() == target_arity)
812                    .collect();
813                if !matching_funcs.is_empty() {
814                    *func = matching_funcs[rng.gen_range(0..matching_funcs.len())].clone();
815                }
816                // Children are preserved: point mutation keeps arity/structure.
817            }
818        }
819    }
820}
821
822impl Default for PointMutation {
823    fn default() -> Self {
824        Self::new()
825    }
826}
827
828impl<T: Terminal, F: Function> MutationOperator<TreeGenome<T, F>> for PointMutation {
829    fn mutate<R: Rng>(&self, genome: &mut TreeGenome<T, F>, rng: &mut R) {
830        // Iterative (explicit-stack) in-place point mutation (EV-60): visit every
831        // node without recursion so a pathologically deep tree cannot overflow the
832        // call stack. Each node is independently mutated with probability
833        // `mutation_probability`; point mutation preserves arity, so children are
834        // left in place and only the node's own label may change.
835        let mut stack: Vec<&mut TreeNode<T, F>> = vec![&mut genome.root];
836        while let Some(node) = stack.pop() {
837            if rng.gen::<f64>() < self.mutation_probability {
838                self.mutate_node_in_place(node, rng);
839            }
840            if let TreeNode::Function(_, children) = node {
841                for child in children.iter_mut() {
842                    stack.push(child);
843                }
844            }
845        }
846    }
847
848    fn mutation_probability(&self) -> Option<f64> {
849        Some(self.mutation_probability)
850    }
851}
852
853/// Subtree mutation for tree genomes (genetic programming)
854///
855/// Replaces a randomly selected subtree with a new randomly generated subtree.
856/// This is a more disruptive mutation than point mutation.
857#[derive(Clone, Debug)]
858pub struct SubtreeMutation {
859    /// Maximum depth of the generated subtree
860    pub max_subtree_depth: usize,
861    /// Probability of selecting a function node for replacement
862    pub function_probability: f64,
863    /// Terminal probability for grow method
864    pub terminal_probability: f64,
865}
866
867impl SubtreeMutation {
868    /// Create a new subtree mutation with default settings
869    pub fn new() -> Self {
870        Self {
871            max_subtree_depth: 4,
872            function_probability: 0.9,
873            terminal_probability: 0.3,
874        }
875    }
876
877    /// Set the maximum depth for generated subtrees
878    pub fn with_max_depth(mut self, depth: usize) -> Self {
879        self.max_subtree_depth = depth;
880        self
881    }
882
883    /// Set the function selection probability
884    pub fn with_function_probability(mut self, probability: f64) -> Self {
885        assert!(
886            (0.0..=1.0).contains(&probability),
887            "Probability must be in [0, 1]"
888        );
889        self.function_probability = probability;
890        self
891    }
892
893    /// Set the terminal probability for tree generation
894    pub fn with_terminal_probability(mut self, probability: f64) -> Self {
895        assert!(
896            (0.0..=1.0).contains(&probability),
897            "Probability must be in [0, 1]"
898        );
899        self.terminal_probability = probability;
900        self
901    }
902}
903
904impl Default for SubtreeMutation {
905    fn default() -> Self {
906        Self::new()
907    }
908}
909
910impl<T: Terminal, F: Function> MutationOperator<TreeGenome<T, F>> for SubtreeMutation {
911    fn mutate<R: Rng>(&self, genome: &mut TreeGenome<T, F>, rng: &mut R) {
912        // Decide whether to select a function or terminal position
913        let position = if rng.gen::<f64>() < self.function_probability {
914            genome
915                .random_function_position(rng)
916                .unwrap_or_else(|| genome.random_terminal_position(rng).unwrap_or_default())
917        } else {
918            genome
919                .random_terminal_position(rng)
920                .unwrap_or_else(|| genome.random_function_position(rng).unwrap_or_default())
921        };
922
923        // Depth budget for the replacement subtree (EV-27).
924        //
925        // `position` has one entry per edge from the root, so the selected node
926        // sits at tree level `position.len() + 1` (the root is level 1).
927        // Replacing it with a subtree `S` yields a tree whose depth along that
928        // branch is `position.len() + S.depth()`. To keep the whole tree within
929        // `genome.max_depth` we require `S.depth() <= max_depth - position.len()`.
930        let point_depth = position.len();
931        let budget = genome
932            .max_depth
933            .saturating_sub(point_depth)
934            .min(self.max_subtree_depth)
935            .max(1);
936
937        // `TreeGenome::generate_grow(_, m, _)` can produce a subtree whose
938        // `depth()` is up to `m + 1` (a function node may be created at the last
939        // permitted level and still receives terminal children one level
940        // deeper). We therefore ask for `budget - 1` so the result's depth is at
941        // most `budget`.
942        let new_root =
943            TreeGenome::<T, F>::generate_grow(rng, budget - 1, self.terminal_probability).root;
944
945        // Defensive guard mirroring SubtreeCrossover: if the generated subtree
946        // would still violate the depth limit, fall back to a single terminal,
947        // which always fits since `budget >= 1`.
948        let new_root = if point_depth + new_root.depth() > genome.max_depth {
949            TreeNode::Terminal(T::random(rng))
950        } else {
951            new_root
952        };
953
954        // Replace the subtree
955        genome.root.replace_subtree(&position, new_root);
956    }
957}
958
959/// Hoist mutation for tree genomes (genetic programming)
960///
961/// Selects a random subtree and replaces the entire tree with it.
962/// This is useful for bloat control.
963#[derive(Clone, Debug, Default)]
964pub struct HoistMutation {
965    /// Probability of selecting a function node
966    pub function_probability: f64,
967}
968
969impl HoistMutation {
970    /// Create a new hoist mutation
971    pub fn new() -> Self {
972        Self {
973            function_probability: 0.5,
974        }
975    }
976
977    /// Set the function selection probability
978    pub fn with_function_probability(mut self, probability: f64) -> Self {
979        assert!(
980            (0.0..=1.0).contains(&probability),
981            "Probability must be in [0, 1]"
982        );
983        self.function_probability = probability;
984        self
985    }
986}
987
988impl<T: Terminal, F: Function> MutationOperator<TreeGenome<T, F>> for HoistMutation {
989    fn mutate<R: Rng>(&self, genome: &mut TreeGenome<T, F>, rng: &mut R) {
990        // Select a random position (prefer function nodes to make it interesting)
991        let position = if rng.gen::<f64>() < self.function_probability {
992            genome
993                .random_function_position(rng)
994                .unwrap_or_else(|| genome.random_terminal_position(rng).unwrap_or_default())
995        } else {
996            genome.random_position(rng)
997        };
998
999        // Skip if selecting the root (no change)
1000        if position.is_empty() {
1001            return;
1002        }
1003
1004        // Get the subtree and make it the new root
1005        if let Some(subtree) = genome.root.get_subtree(&position) {
1006            genome.root = subtree.clone();
1007        }
1008    }
1009}
1010
1011/// Shrink mutation for tree genomes (genetic programming)
1012///
1013/// Replaces a randomly selected subtree with one of its terminals.
1014/// This reduces tree size and helps with bloat control.
1015#[derive(Clone, Debug, Default)]
1016pub struct ShrinkMutation;
1017
1018impl ShrinkMutation {
1019    /// Create a new shrink mutation
1020    pub fn new() -> Self {
1021        Self
1022    }
1023}
1024
1025impl<T: Terminal, F: Function> MutationOperator<TreeGenome<T, F>> for ShrinkMutation {
1026    fn mutate<R: Rng>(&self, genome: &mut TreeGenome<T, F>, rng: &mut R) {
1027        // Find a function node to shrink
1028        if let Some(func_position) = genome.random_function_position(rng) {
1029            // Skip root to maintain some structure
1030            if func_position.is_empty() {
1031                return;
1032            }
1033
1034            // Get a terminal from within the selected subtree
1035            if let Some(subtree) = genome.root.get_subtree(&func_position) {
1036                let terminal_positions = subtree.terminal_positions();
1037                if !terminal_positions.is_empty() {
1038                    // Pick a random terminal from the subtree
1039                    let term_pos = &terminal_positions[rng.gen_range(0..terminal_positions.len())];
1040
1041                    // Get the terminal value
1042                    if let Some(terminal_node) = subtree.get_subtree(term_pos) {
1043                        let replacement = terminal_node.clone();
1044                        // Replace the function node with the terminal
1045                        genome.root.replace_subtree(&func_position, replacement);
1046                    }
1047                }
1048            }
1049        }
1050    }
1051}
1052
1053/// Adaptive mutation for tree genomes (genetic programming)
1054///
1055/// Combines multiple tree mutations with configurable probabilities.
1056#[derive(Clone, Debug)]
1057pub struct AdaptiveTreeMutation {
1058    /// Probability of point mutation
1059    pub point_prob: f64,
1060    /// Probability of subtree mutation
1061    pub subtree_prob: f64,
1062    /// Probability of hoist mutation
1063    pub hoist_prob: f64,
1064    /// Probability of shrink mutation
1065    pub shrink_prob: f64,
1066}
1067
1068impl AdaptiveTreeMutation {
1069    /// Create with default probabilities
1070    pub fn new() -> Self {
1071        Self {
1072            point_prob: 0.4,
1073            subtree_prob: 0.3,
1074            hoist_prob: 0.15,
1075            shrink_prob: 0.15,
1076        }
1077    }
1078
1079    /// Create with custom probabilities
1080    pub fn with_probs(point: f64, subtree: f64, hoist: f64, shrink: f64) -> Self {
1081        Self {
1082            point_prob: point,
1083            subtree_prob: subtree,
1084            hoist_prob: hoist,
1085            shrink_prob: shrink,
1086        }
1087    }
1088}
1089
1090impl Default for AdaptiveTreeMutation {
1091    fn default() -> Self {
1092        Self::new()
1093    }
1094}
1095
1096impl<T: Terminal, F: Function> MutationOperator<TreeGenome<T, F>> for AdaptiveTreeMutation {
1097    fn mutate<R: Rng>(&self, genome: &mut TreeGenome<T, F>, rng: &mut R) {
1098        let total = self.point_prob + self.subtree_prob + self.hoist_prob + self.shrink_prob;
1099        if total <= 0.0 {
1100            return;
1101        }
1102
1103        let r = rng.gen::<f64>() * total;
1104        let mut cumulative = 0.0;
1105
1106        cumulative += self.point_prob;
1107        if r < cumulative {
1108            PointMutation::new().mutate(genome, rng);
1109            return;
1110        }
1111
1112        cumulative += self.subtree_prob;
1113        if r < cumulative {
1114            SubtreeMutation::new().mutate(genome, rng);
1115            return;
1116        }
1117
1118        cumulative += self.hoist_prob;
1119        if r < cumulative {
1120            HoistMutation::new().mutate(genome, rng);
1121            return;
1122        }
1123
1124        ShrinkMutation::new().mutate(genome, rng);
1125    }
1126}
1127
1128#[cfg(test)]
1129mod tests {
1130    use super::*;
1131    use approx::assert_relative_eq;
1132
1133    #[test]
1134    fn test_polynomial_mutation_respects_bounds() {
1135        let mut rng = rand::thread_rng();
1136        let bounds = MultiBounds::symmetric(5.0, 10);
1137
1138        for _ in 0..100 {
1139            let mut genome = RealVector::generate(&mut rng, &bounds);
1140            let mutation = PolynomialMutation::new(20.0).with_probability(1.0);
1141            mutation.mutate_bounded(&mut genome, &bounds, &mut rng);
1142
1143            for (i, &gene) in genome.genes().iter().enumerate() {
1144                let bound = bounds.get(i).unwrap();
1145                assert!(
1146                    gene >= bound.min && gene <= bound.max,
1147                    "Gene {} out of bounds: {} not in [{}, {}]",
1148                    i,
1149                    gene,
1150                    bound.min,
1151                    bound.max
1152                );
1153            }
1154        }
1155    }
1156
1157    #[test]
1158    fn test_polynomial_mutation_changes_genome() {
1159        let mut rng = rand::thread_rng();
1160        let bounds = MultiBounds::symmetric(5.0, 10);
1161        let original = RealVector::zeros(10);
1162        let mut genome = original.clone();
1163
1164        let mutation = PolynomialMutation::new(20.0).with_probability(1.0);
1165        mutation.mutate_bounded(&mut genome, &bounds, &mut rng);
1166
1167        // At least some genes should have changed
1168        let changed = genome
1169            .genes()
1170            .iter()
1171            .zip(original.genes())
1172            .filter(|(&a, &b)| a != b)
1173            .count();
1174        assert!(changed > 0, "No genes were mutated");
1175    }
1176
1177    #[test]
1178    fn test_polynomial_mutation_eta_effect() {
1179        let mut rng = rand::thread_rng();
1180        let bounds = MultiBounds::symmetric(1.0, 1);
1181
1182        // Low eta = larger mutations
1183        let low_eta = PolynomialMutation::new(1.0).with_probability(1.0);
1184        // High eta = smaller mutations
1185        let high_eta = PolynomialMutation::new(100.0).with_probability(1.0);
1186
1187        let mut low_total_change = 0.0;
1188        let mut high_total_change = 0.0;
1189        let trials = 1000;
1190
1191        for _ in 0..trials {
1192            let mut genome_low = RealVector::new(vec![0.0]);
1193            let mut genome_high = RealVector::new(vec![0.0]);
1194
1195            low_eta.mutate_bounded(&mut genome_low, &bounds, &mut rng);
1196            high_eta.mutate_bounded(&mut genome_high, &bounds, &mut rng);
1197
1198            low_total_change += genome_low[0].abs();
1199            high_total_change += genome_high[0].abs();
1200        }
1201
1202        assert!(
1203            low_total_change > high_total_change,
1204            "Low eta should produce larger average changes"
1205        );
1206    }
1207
1208    #[test]
1209    fn test_gaussian_mutation_changes_genome() {
1210        let mut rng = rand::thread_rng();
1211        let original = RealVector::zeros(10);
1212        let mut genome = original.clone();
1213
1214        let mutation = GaussianMutation::new(0.1).with_probability(1.0);
1215        mutation.mutate(&mut genome, &mut rng);
1216
1217        assert_ne!(genome, original);
1218    }
1219
1220    #[test]
1221    fn test_gaussian_mutation_bounded() {
1222        let mut rng = rand::thread_rng();
1223        let bounds = MultiBounds::symmetric(1.0, 10);
1224
1225        for _ in 0..100 {
1226            let mut genome = RealVector::zeros(10);
1227            let mutation = GaussianMutation::new(10.0).with_probability(1.0);
1228            mutation.mutate_bounded(&mut genome, &bounds, &mut rng);
1229
1230            for (i, &gene) in genome.genes().iter().enumerate() {
1231                let bound = bounds.get(i).unwrap();
1232                assert!(gene >= bound.min && gene <= bound.max);
1233            }
1234        }
1235    }
1236
1237    #[test]
1238    fn test_uniform_mutation() {
1239        let mut rng = rand::thread_rng();
1240        let bounds = MultiBounds::symmetric(1.0, 10);
1241
1242        for _ in 0..100 {
1243            let mut genome = RealVector::zeros(10);
1244            let mutation = UniformMutation::new().with_probability(1.0);
1245            mutation.mutate_bounded(&mut genome, &bounds, &mut rng);
1246
1247            for (i, &gene) in genome.genes().iter().enumerate() {
1248                let bound = bounds.get(i).unwrap();
1249                assert!(gene >= bound.min && gene <= bound.max);
1250            }
1251        }
1252    }
1253
1254    #[test]
1255    fn test_bit_flip_mutation() {
1256        let mut rng = rand::thread_rng();
1257        let original = BitString::zeros(100);
1258        let mut genome = original.clone();
1259
1260        let mutation = BitFlipMutation::new().with_probability(0.5);
1261        mutation.mutate(&mut genome, &mut rng);
1262
1263        // About half should be flipped
1264        let flipped = genome.count_ones();
1265        assert!(
1266            flipped > 20 && flipped < 80,
1267            "Expected ~50 flips, got {}",
1268            flipped
1269        );
1270    }
1271
1272    #[test]
1273    fn test_bit_flip_mutation_default_probability() {
1274        let mut rng = rand::thread_rng();
1275        let original = BitString::zeros(100);
1276        let mut genome = original.clone();
1277
1278        let mutation = BitFlipMutation::new(); // 1/n probability
1279        mutation.mutate(&mut genome, &mut rng);
1280
1281        // With 1/100 probability, expect ~1 flip on average
1282        // But due to randomness, we just check some change occurred
1283        // over multiple trials
1284        let mut total_flips = 0;
1285        for _ in 0..100 {
1286            let mut g = BitString::zeros(100);
1287            mutation.mutate(&mut g, &mut rng);
1288            total_flips += g.count_ones();
1289        }
1290
1291        // Average should be close to 1
1292        let avg = total_flips as f64 / 100.0;
1293        assert!(avg > 0.5 && avg < 2.0, "Expected avg ~1, got {}", avg);
1294    }
1295
1296    #[test]
1297    fn test_swap_mutation() {
1298        let mut rng = rand::thread_rng();
1299        let mut genome = RealVector::new(vec![0.0, 1.0, 2.0, 3.0, 4.0]);
1300
1301        let mutation = SwapMutation::new();
1302        mutation.mutate(&mut genome, &mut rng);
1303
1304        // The sum should be preserved
1305        let sum: f64 = genome.genes().iter().sum();
1306        assert_relative_eq!(sum, 10.0);
1307    }
1308
1309    #[test]
1310    fn test_swap_mutation_multiple() {
1311        let mut rng = rand::thread_rng();
1312        let original: Vec<f64> = (0..10).map(|i| i as f64).collect();
1313        let mut genome = RealVector::new(original.clone());
1314
1315        let mutation = SwapMutation::with_swaps(5);
1316        mutation.mutate(&mut genome, &mut rng);
1317
1318        // Sum should be preserved
1319        let sum: f64 = genome.genes().iter().sum();
1320        assert_relative_eq!(sum, 45.0);
1321    }
1322
1323    #[test]
1324    fn test_scramble_mutation() {
1325        let mut rng = rand::thread_rng();
1326        let original: Vec<f64> = (0..10).map(|i| i as f64).collect();
1327        let mut genome = RealVector::new(original.clone());
1328
1329        let mutation = ScrambleMutation::new();
1330        mutation.mutate(&mut genome, &mut rng);
1331
1332        // Sum should be preserved
1333        let sum: f64 = genome.genes().iter().sum();
1334        assert_relative_eq!(sum, 45.0);
1335
1336        // All values should still be present
1337        let mut sorted: Vec<f64> = genome.genes().to_vec();
1338        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
1339        assert_eq!(sorted, original);
1340    }
1341
1342    #[test]
1343    fn test_scramble_mutation_bitstring() {
1344        let mut rng = rand::thread_rng();
1345        let original = BitString::new(vec![true, true, true, false, false, false, true, false]);
1346        let mut genome = original.clone();
1347
1348        let mutation = ScrambleMutation::new();
1349        mutation.mutate(&mut genome, &mut rng);
1350
1351        // Count should be preserved
1352        assert_eq!(genome.count_ones(), original.count_ones());
1353    }
1354
1355    // =========================================================================
1356    // Permutation Mutation Tests
1357    // =========================================================================
1358
1359    #[test]
1360    fn test_permutation_swap_mutation() {
1361        let mut rng = rand::thread_rng();
1362        let original = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1363        let mut genome = original.clone();
1364
1365        let mutation = PermutationSwapMutation::new();
1366        mutation.mutate(&mut genome, &mut rng);
1367
1368        // Should still be valid permutation
1369        assert!(genome.is_valid_permutation());
1370        assert_eq!(genome.dimension(), 8);
1371    }
1372
1373    #[test]
1374    fn test_permutation_swap_mutation_multiple() {
1375        let mut rng = rand::thread_rng();
1376        let original = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
1377        let mut genome = original.clone();
1378
1379        let mutation = PermutationSwapMutation::with_swaps(5);
1380        mutation.mutate(&mut genome, &mut rng);
1381
1382        // Should still be valid permutation
1383        assert!(genome.is_valid_permutation());
1384        assert_eq!(genome.dimension(), 10);
1385    }
1386
1387    #[test]
1388    fn test_insert_mutation() {
1389        let mut rng = rand::thread_rng();
1390
1391        for _ in 0..50 {
1392            let mut genome = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1393
1394            let mutation = InsertMutation::new();
1395            mutation.mutate(&mut genome, &mut rng);
1396
1397            assert!(genome.is_valid_permutation());
1398            assert_eq!(genome.dimension(), 8);
1399        }
1400    }
1401
1402    #[test]
1403    fn test_inversion_mutation() {
1404        let mut rng = rand::thread_rng();
1405
1406        for _ in 0..50 {
1407            let mut genome = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1408
1409            let mutation = InversionMutation::new();
1410            mutation.mutate(&mut genome, &mut rng);
1411
1412            assert!(genome.is_valid_permutation());
1413            assert_eq!(genome.dimension(), 8);
1414        }
1415    }
1416
1417    #[test]
1418    fn test_inversion_mutation_reverses_segment() {
1419        use rand::SeedableRng;
1420        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1421
1422        let mut genome = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1423
1424        let mutation = InversionMutation::new();
1425        mutation.mutate(&mut genome, &mut rng);
1426
1427        // Should still be valid permutation
1428        assert!(genome.is_valid_permutation());
1429    }
1430
1431    #[test]
1432    fn test_permutation_scramble_mutation() {
1433        let mut rng = rand::thread_rng();
1434
1435        for _ in 0..50 {
1436            let mut genome = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1437
1438            let mutation = PermutationScrambleMutation::new();
1439            mutation.mutate(&mut genome, &mut rng);
1440
1441            assert!(genome.is_valid_permutation());
1442            assert_eq!(genome.dimension(), 8);
1443        }
1444    }
1445
1446    #[test]
1447    fn test_displacement_mutation() {
1448        let mut rng = rand::thread_rng();
1449
1450        for _ in 0..50 {
1451            let mut genome = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
1452
1453            let mutation = DisplacementMutation::new();
1454            mutation.mutate(&mut genome, &mut rng);
1455
1456            assert!(genome.is_valid_permutation());
1457            assert_eq!(genome.dimension(), 10);
1458        }
1459    }
1460
1461    #[test]
1462    fn test_adaptive_permutation_mutation() {
1463        let mut rng = rand::thread_rng();
1464
1465        for _ in 0..100 {
1466            let mut genome = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1467
1468            let mutation = AdaptivePermutationMutation::new();
1469            mutation.mutate(&mut genome, &mut rng);
1470
1471            assert!(genome.is_valid_permutation());
1472            assert_eq!(genome.dimension(), 8);
1473        }
1474    }
1475
1476    #[test]
1477    fn test_adaptive_permutation_mutation_custom_probs() {
1478        let mut rng = rand::thread_rng();
1479
1480        // Test with only inversion mutation
1481        let mutation = AdaptivePermutationMutation::with_probs(0.0, 0.0, 1.0, 0.0);
1482
1483        for _ in 0..50 {
1484            let mut genome = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1485            mutation.mutate(&mut genome, &mut rng);
1486
1487            assert!(genome.is_valid_permutation());
1488        }
1489    }
1490
1491    // =========================================================================
1492    // Tree (GP) Mutation Tests
1493    // =========================================================================
1494
1495    use crate::genome::tree::{ArithmeticFunction, ArithmeticTerminal};
1496
1497    fn create_test_tree() -> TreeGenome<ArithmeticTerminal, ArithmeticFunction> {
1498        // Create: (+ x0 (* 1.0 x1))
1499        let x0 = TreeNode::terminal(ArithmeticTerminal::Variable(0));
1500        let c1 = TreeNode::terminal(ArithmeticTerminal::Constant(1.0));
1501        let x1 = TreeNode::terminal(ArithmeticTerminal::Variable(1));
1502        let mul = TreeNode::function(ArithmeticFunction::Mul, vec![c1, x1]);
1503        let add = TreeNode::function(ArithmeticFunction::Add, vec![x0, mul]);
1504        TreeGenome::new(add, 5)
1505    }
1506
1507    #[test]
1508    fn test_point_mutation_preserves_structure() {
1509        let mut rng = rand::thread_rng();
1510        let original = create_test_tree();
1511        let original_size = original.size();
1512
1513        for _ in 0..50 {
1514            let mut genome = original.clone();
1515            let mutation = PointMutation::new().with_probability(1.0);
1516            mutation.mutate(&mut genome, &mut rng);
1517
1518            // Point mutation preserves tree structure (size)
1519            assert_eq!(genome.size(), original_size);
1520            assert!(genome.evaluate(&[1.0, 2.0]).is_finite());
1521        }
1522    }
1523
1524    #[test]
1525    fn test_point_mutation_changes_tree() {
1526        let mut rng = rand::thread_rng();
1527        let original = create_test_tree();
1528        let mut any_changed = false;
1529
1530        for _ in 0..100 {
1531            let mut genome = original.clone();
1532            let mutation = PointMutation::new().with_probability(1.0);
1533            mutation.mutate(&mut genome, &mut rng);
1534
1535            // Check if the evaluation changed (indicates mutation occurred)
1536            let orig_val = original.evaluate(&[1.0, 2.0]);
1537            let new_val = genome.evaluate(&[1.0, 2.0]);
1538            if (orig_val - new_val).abs() > 1e-10 {
1539                any_changed = true;
1540                break;
1541            }
1542        }
1543
1544        assert!(
1545            any_changed,
1546            "Point mutation should sometimes change the tree"
1547        );
1548    }
1549
1550    #[test]
1551    fn test_point_mutation_deep_tree_no_stack_overflow() {
1552        // regression: EV-60 — PointMutation must traverse iteratively so a
1553        // ~100k-deep tree can be mutated without overflowing the call stack. The
1554        // previous recursive `mutate_recursive` overflowed at this depth.
1555        use rand::SeedableRng;
1556        let mut rng = rand::rngs::StdRng::seed_from_u64(7);
1557        let depth = 100_000usize;
1558        let mut root: TreeNode<ArithmeticTerminal, ArithmeticFunction> =
1559            TreeNode::terminal(ArithmeticTerminal::Constant(1.0));
1560        for _ in 0..depth {
1561            root = TreeNode::function(ArithmeticFunction::Neg, vec![root]);
1562        }
1563        let mut genome = TreeGenome::new(root, depth + 1);
1564        let size_before = genome.size();
1565
1566        // mutate every node (probability 1.0) — must not overflow.
1567        PointMutation::new()
1568            .with_probability(1.0)
1569            .mutate(&mut genome, &mut rng);
1570
1571        // Point mutation preserves structure (arity/size) even at extreme depth.
1572        assert_eq!(genome.size(), size_before);
1573        // Free iteratively so the test itself doesn't overflow on teardown.
1574        genome.dismantle();
1575    }
1576
1577    #[test]
1578    fn test_subtree_mutation() {
1579        use rand::SeedableRng;
1580        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1581        let original = create_test_tree();
1582
1583        for _ in 0..50 {
1584            let mut genome = original.clone();
1585            let mutation = SubtreeMutation::new().with_max_depth(3);
1586            mutation.mutate(&mut genome, &mut rng);
1587
1588            // Tree should still be valid and have at least one node
1589            assert!(genome.size() >= 1);
1590            // Subtree mutation may generate expressions that are NaN/Inf for some inputs
1591            // (e.g., division by zero), so we only check structural validity
1592            let result = genome.evaluate(&[1.0, 2.0]);
1593            assert!(result.is_nan() || result.is_finite());
1594        }
1595    }
1596
1597    #[test]
1598    fn test_hoist_mutation_reduces_tree() {
1599        let mut rng = rand::thread_rng();
1600
1601        // Create a deeper tree
1602        let tree: TreeGenome<ArithmeticTerminal, ArithmeticFunction> =
1603            TreeGenome::generate_full(&mut rng, 4, 5);
1604
1605        let original_size = tree.size();
1606
1607        for _ in 0..50 {
1608            let mut genome = tree.clone();
1609            let mutation = HoistMutation::new();
1610            mutation.mutate(&mut genome, &mut rng);
1611
1612            // Hoist mutation should reduce or maintain tree size
1613            assert!(genome.size() <= original_size);
1614            assert!(genome.evaluate(&[1.0, 2.0]).is_finite());
1615        }
1616    }
1617
1618    #[test]
1619    fn test_shrink_mutation_reduces_tree() {
1620        let mut rng = rand::thread_rng();
1621
1622        // Create a deeper tree
1623        let tree: TreeGenome<ArithmeticTerminal, ArithmeticFunction> =
1624            TreeGenome::generate_full(&mut rng, 4, 5);
1625
1626        for _ in 0..50 {
1627            let mut genome = tree.clone();
1628            let original_size = genome.size();
1629            let mutation = ShrinkMutation::new();
1630            mutation.mutate(&mut genome, &mut rng);
1631
1632            // Shrink mutation should reduce or maintain tree size
1633            assert!(genome.size() <= original_size);
1634            assert!(genome.evaluate(&[1.0, 2.0]).is_finite());
1635        }
1636    }
1637
1638    #[test]
1639    fn test_adaptive_tree_mutation() {
1640        let mut rng = rand::thread_rng();
1641
1642        for _ in 0..100 {
1643            let mut genome: TreeGenome<ArithmeticTerminal, ArithmeticFunction> =
1644                TreeGenome::generate_ramped_half_and_half(&mut rng, 2, 5);
1645
1646            let mutation = AdaptiveTreeMutation::new();
1647            mutation.mutate(&mut genome, &mut rng);
1648
1649            // Tree should still be valid
1650            assert!(genome.size() >= 1);
1651            assert!(genome.evaluate(&[1.0, 2.0]).is_finite());
1652        }
1653    }
1654
1655    #[test]
1656    fn test_adaptive_tree_mutation_custom_probs() {
1657        let mut rng = rand::thread_rng();
1658
1659        // Test with only point mutation
1660        let mutation = AdaptiveTreeMutation::with_probs(1.0, 0.0, 0.0, 0.0);
1661
1662        for _ in 0..50 {
1663            let mut genome: TreeGenome<ArithmeticTerminal, ArithmeticFunction> =
1664                TreeGenome::generate_ramped_half_and_half(&mut rng, 2, 5);
1665            let original_size = genome.size();
1666            mutation.mutate(&mut genome, &mut rng);
1667
1668            // Point mutation preserves structure
1669            assert_eq!(genome.size(), original_size);
1670        }
1671    }
1672
1673    #[test]
1674    fn test_subtree_mutation_respects_max_depth() {
1675        // regression: EV-27 — SubtreeMutation must never grow a tree beyond its
1676        // `max_depth`. Pre-fix, the depth budget was off by one (generate_grow
1677        // can produce a subtree one level deeper than requested) and the
1678        // violation compounded across repeated mutations. Fuzz 500 mutations
1679        // with a large `max_subtree_depth` (to stress the budget) and assert the
1680        // depth invariant after every single mutation.
1681        use rand::SeedableRng;
1682        let mut rng = rand::rngs::StdRng::seed_from_u64(2024);
1683
1684        let max_depth = 5;
1685        // Start from a valid full tree at exactly `max_depth`
1686        // (generate_full(_, d, _) yields depth d + 1).
1687        let mut genome: TreeGenome<ArithmeticTerminal, ArithmeticFunction> =
1688            TreeGenome::generate_full(&mut rng, max_depth - 1, max_depth);
1689        assert!(genome.depth() <= max_depth);
1690
1691        // Large subtree budget + low terminal probability => generate_grow tends
1692        // to emit functions, maximally exercising the off-by-one path.
1693        let mutation = SubtreeMutation::new()
1694            .with_max_depth(12)
1695            .with_terminal_probability(0.1);
1696
1697        for i in 0..500 {
1698            mutation.mutate(&mut genome, &mut rng);
1699            assert!(
1700                genome.depth() <= max_depth,
1701                "iteration {i}: tree depth {} exceeded max_depth {max_depth}",
1702                genome.depth()
1703            );
1704        }
1705    }
1706
1707    #[test]
1708    fn test_default_swap_mutations_actually_mutate() {
1709        // regression: EV-101 — SwapMutation / PermutationSwapMutation /
1710        // InsertMutation previously derived Default, giving num=0 (a silent
1711        // no-op). Default::default() must now behave like new() (num=1) and
1712        // actually change the genome.
1713        assert_eq!(SwapMutation::default().num_swaps, 1);
1714        assert_eq!(PermutationSwapMutation::default().num_swaps, 1);
1715        assert_eq!(InsertMutation::default().num_inserts, 1);
1716
1717        use rand::SeedableRng;
1718        let mut rng = rand::rngs::StdRng::seed_from_u64(99);
1719
1720        // A default SwapMutation must eventually move genes (num_swaps=0 never would).
1721        let mut any_changed = false;
1722        for _ in 0..50 {
1723            let original = RealVector::new(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
1724            let mut genome = original.clone();
1725            SwapMutation::default().mutate(&mut genome, &mut rng);
1726            if genome.genes() != original.genes() {
1727                any_changed = true;
1728                break;
1729            }
1730        }
1731        assert!(
1732            any_changed,
1733            "Default SwapMutation never mutated (num_swaps == 0?)"
1734        );
1735
1736        // Default permutation swap: a valid perm with a moved element.
1737        let mut perm_changed = false;
1738        for _ in 0..50 {
1739            let original = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1740            let mut genome = original.clone();
1741            PermutationSwapMutation::default().mutate(&mut genome, &mut rng);
1742            if genome.as_slice() != original.as_slice() {
1743                perm_changed = true;
1744                break;
1745            }
1746        }
1747        assert!(
1748            perm_changed,
1749            "Default PermutationSwapMutation never mutated (num_swaps == 0?)"
1750        );
1751
1752        // Default insert mutation.
1753        let mut insert_changed = false;
1754        for _ in 0..50 {
1755            let original = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1756            let mut genome = original.clone();
1757            InsertMutation::default().mutate(&mut genome, &mut rng);
1758            if genome.as_slice() != original.as_slice() {
1759                insert_changed = true;
1760                break;
1761            }
1762        }
1763        assert!(
1764            insert_changed,
1765            "Default InsertMutation never mutated (num_inserts == 0?)"
1766        );
1767    }
1768
1769    #[test]
1770    fn test_unbounded_polynomial_mutation_stays_local() {
1771        // regression: EV-102 — the unbounded PolynomialMutation path used to
1772        // fabricate +/-1e10 bounds, turning a mutation into a near-random reset
1773        // spanning ~1e10 in magnitude. It must instead apply a local Gaussian
1774        // perturbation and keep genes near their original values.
1775        use rand::SeedableRng;
1776        let mut rng = rand::rngs::StdRng::seed_from_u64(1);
1777
1778        for _ in 0..200 {
1779            let mut genome = RealVector::new(vec![0.0, 1.0, -1.0, 2.5, -3.0]);
1780            let mutation = PolynomialMutation::new(20.0).with_probability(1.0);
1781            mutation.mutate(&mut genome, &mut rng); // unbounded path
1782
1783            for &g in genome.genes() {
1784                assert!(
1785                    g.abs() < 100.0,
1786                    "unbounded polynomial mutation produced a destructive value: {g}"
1787                );
1788            }
1789        }
1790
1791        // A fixed sigma is honored.
1792        let mut genome = RealVector::new(vec![0.0; 1000]);
1793        let mutation = PolynomialMutation::new(20.0)
1794            .with_probability(1.0)
1795            .with_unbounded_sigma(0.05);
1796        mutation.mutate(&mut genome, &mut rng);
1797        let variance: f64 =
1798            genome.genes().iter().map(|g| g * g).sum::<f64>() / genome.dimension() as f64;
1799        // Sample std should be near 0.05 (well under any 1e10 fabrication).
1800        assert!(
1801            variance.sqrt() < 0.2,
1802            "fixed sigma not honored: std {}",
1803            variance.sqrt()
1804        );
1805    }
1806
1807    #[test]
1808    fn test_mutation_probability_reports_effective_rate() {
1809        // regression: EV-103 — mutation_probability() used to report 1.0 while
1810        // the operators actually applied the per-gene default 1/n. It must now
1811        // report `None` for the length-dependent default and `Some(p)` for a
1812        // configured rate.
1813        assert_eq!(
1814            MutationOperator::<RealVector>::mutation_probability(&PolynomialMutation::new(20.0)),
1815            None
1816        );
1817        assert_eq!(
1818            MutationOperator::<RealVector>::mutation_probability(
1819                &PolynomialMutation::new(20.0).with_probability(0.25)
1820            ),
1821            Some(0.25)
1822        );
1823        assert_eq!(
1824            MutationOperator::<RealVector>::mutation_probability(&GaussianMutation::new(0.1)),
1825            None
1826        );
1827        assert_eq!(
1828            MutationOperator::<RealVector>::mutation_probability(&UniformMutation::new()),
1829            None
1830        );
1831        assert_eq!(
1832            MutationOperator::<BitString>::mutation_probability(&BitFlipMutation::new()),
1833            None
1834        );
1835        assert_eq!(
1836            MutationOperator::<BitString>::mutation_probability(
1837                &BitFlipMutation::new().with_probability(0.5)
1838            ),
1839            Some(0.5)
1840        );
1841    }
1842}