Skip to main content

fugue_evo/operators/
crossover.rs

1//! Crossover operators
2//!
3//! This module provides various crossover operators for genetic algorithms.
4
5use std::collections::{HashMap, HashSet};
6
7use rand::Rng;
8
9use crate::error::{OperatorError, OperatorResult};
10use crate::genome::bit_string::BitString;
11use crate::genome::bounds::MultiBounds;
12use crate::genome::permutation::Permutation;
13use crate::genome::real_vector::RealVector;
14use crate::genome::traits::{
15    BinaryGenome, EvolutionaryGenome, PermutationGenome, RealValuedGenome,
16};
17use crate::operators::traits::{BoundedCrossoverOperator, CrossoverOperator};
18
19/// Simulated Binary Crossover (SBX)
20///
21/// SBX generates offspring from parents using a spread factor that
22/// simulates single-point crossover for binary strings.
23///
24/// # Two distinct probabilities
25///
26/// SBX involves two conceptually separate probabilities that used to be
27/// conflated into a single field (audit EV-72):
28///
29/// - [`crossover_probability`](Self::crossover_probability) — the *per-pair*
30///   probability that the operator is applied at all. This is the quantity
31///   reported by [`CrossoverOperator::crossover_probability`] and consumed by
32///   the driving algorithm (e.g. `SimpleGA`, `NSGA-II`) to decide, once per
33///   parent pair, whether to recombine. Canonical default: `0.9`.
34/// - [`exchange_probability`](Self::exchange_probability) — the *per-gene*
35///   probability, used *inside* SBX, that an individual variable is recombined
36///   via the spread factor (otherwise the children inherit the parents'
37///   values unchanged for that variable). Deb's canonical NSGA-II code uses
38///   `0.5`.
39///
40/// # Bounds handling
41///
42/// When bounds are supplied (via [`BoundedCrossoverOperator`]) the operator
43/// uses Deb & Agrawal's bounds-aware spread factor: for each variable the
44/// spread is drawn from the polynomial SBX distribution *truncated* to the
45/// distance to each bound, so offspring land inside `[min, max]` by
46/// construction with no probability mass piled onto the boundaries (audit
47/// EV-71). The unbounded path uses the classic (untruncated) spread factor.
48///
49/// Reference: Deb, K., & Agrawal, R. B. (1995). Simulated Binary Crossover
50/// for Continuous Search Space. Bounds-aware variant: Deb, K. (2001),
51/// NSGA-II `crossover.c`.
52#[derive(Clone, Debug)]
53pub struct SbxCrossover {
54    /// Distribution index (typically 2-20)
55    /// Higher values = offspring closer to parents
56    pub eta: f64,
57    /// Per-pair probability that the operator is applied (reported by
58    /// [`CrossoverOperator::crossover_probability`]). Default: `0.9`.
59    pub crossover_probability: f64,
60    /// Per-gene exchange probability used inside SBX. Default: `0.5`
61    /// (canonical). This is a distinct quantity from
62    /// [`crossover_probability`](Self::crossover_probability).
63    pub exchange_probability: f64,
64}
65
66impl SbxCrossover {
67    /// Create a new SBX crossover with the given distribution index
68    pub fn new(eta: f64) -> Self {
69        assert!(eta >= 0.0, "Distribution index must be non-negative");
70        Self {
71            eta,
72            crossover_probability: 0.9,
73            exchange_probability: 0.5,
74        }
75    }
76
77    /// Set the per-pair crossover probability reported by
78    /// [`CrossoverOperator::crossover_probability`].
79    ///
80    /// This is the probability the driving algorithm uses to decide, once per
81    /// parent pair, whether to apply the operator. It is *not* the per-gene
82    /// exchange rate — for that see [`with_exchange_probability`](Self::with_exchange_probability).
83    pub fn with_probability(mut self, probability: f64) -> Self {
84        assert!(
85            (0.0..=1.0).contains(&probability),
86            "Probability must be in [0, 1]"
87        );
88        self.crossover_probability = probability;
89        self
90    }
91
92    /// Set the per-gene exchange probability used inside SBX (canonical: `0.5`).
93    pub fn with_exchange_probability(mut self, probability: f64) -> Self {
94        assert!(
95            (0.0..=1.0).contains(&probability),
96            "Probability must be in [0, 1]"
97        );
98        self.exchange_probability = probability;
99        self
100    }
101
102    /// Compute the (unbounded) spread factor β from a uniform random value
103    fn spread_factor(&self, u: f64) -> f64 {
104        if u <= 0.5 {
105            (2.0 * u).powf(1.0 / (self.eta + 1.0))
106        } else {
107            (1.0 / (2.0 * (1.0 - u))).powf(1.0 / (self.eta + 1.0))
108        }
109    }
110
111    /// Deb & Agrawal's bounds-aware SBX draw for a single variable.
112    ///
113    /// Given ordered parents `y1 <= y2` lying inside `[yl, yu]` and a uniform
114    /// deviate `u`, returns the two offspring `(c_low, c_high)`. The spread
115    /// factor is drawn from the polynomial SBX distribution truncated at the
116    /// distance to each bound, so `c_low >= yl` and `c_high <= yu` hold *by
117    /// construction* — there is no clamping and no probability atom at the
118    /// bounds. See NSGA-II `crossover.c` (`realcross`).
119    fn sbx_bounded_pair(&self, y1: f64, y2: f64, yl: f64, yu: f64, u: f64) -> (f64, f64) {
120        let dy = y2 - y1;
121        let exp = self.eta + 1.0;
122        let power = 1.0 / exp;
123
124        // Lower child: β_l limits the spread so the child cannot fall below yl.
125        let beta_l = 1.0 + 2.0 * (y1 - yl) / dy;
126        let alpha_l = 2.0 - beta_l.powf(-exp);
127        let betaq_l = if u <= 1.0 / alpha_l {
128            (u * alpha_l).powf(power)
129        } else {
130            (1.0 / (2.0 - u * alpha_l)).powf(power)
131        };
132        let c_low = 0.5 * ((y1 + y2) - betaq_l * dy);
133
134        // Upper child: β_u limits the spread so the child cannot exceed yu.
135        let beta_u = 1.0 + 2.0 * (yu - y2) / dy;
136        let alpha_u = 2.0 - beta_u.powf(-exp);
137        let betaq_u = if u <= 1.0 / alpha_u {
138            (u * alpha_u).powf(power)
139        } else {
140            (1.0 / (2.0 - u * alpha_u)).powf(power)
141        };
142        let c_high = 0.5 * ((y1 + y2) + betaq_u * dy);
143
144        // The values are inside [yl, yu] mathematically; clamp only to absorb
145        // floating-point drift at the extreme (measure-zero) tails. This does
146        // not create a boundary atom the way the old unconditional clamp did.
147        (c_low.clamp(yl, yu), c_high.clamp(yl, yu))
148    }
149
150    /// Apply SBX crossover to two f64 slices
151    fn apply_sbx<R: Rng>(
152        &self,
153        parent1: &[f64],
154        parent2: &[f64],
155        bounds: Option<&MultiBounds>,
156        rng: &mut R,
157    ) -> (Vec<f64>, Vec<f64>) {
158        let mut child1: Vec<f64> = parent1.to_vec();
159        let mut child2: Vec<f64> = parent2.to_vec();
160
161        for i in 0..parent1.len() {
162            // Per-gene exchange coin (canonical 0.5). Distinct from the
163            // per-pair crossover_probability the algorithm applies.
164            if rng.gen::<f64>() < self.exchange_probability {
165                let x1 = parent1[i];
166                let x2 = parent2[i];
167
168                // Only apply if parents differ sufficiently
169                if (x1 - x2).abs() > 1e-14 {
170                    let u = rng.gen::<f64>();
171
172                    let (c_low, c_high) = match bounds.and_then(|b| b.get(i)) {
173                        Some(bound) => {
174                            // Bounds-aware, truncated spread (EV-71).
175                            let (y1, y2) = if x1 <= x2 { (x1, x2) } else { (x2, x1) };
176                            self.sbx_bounded_pair(y1, y2, bound.min, bound.max, u)
177                        }
178                        None => {
179                            // Classic unbounded SBX.
180                            let beta = self.spread_factor(u);
181                            let y1 = 0.5 * ((1.0 + beta) * x1 + (1.0 - beta) * x2);
182                            let y2 = 0.5 * ((1.0 - beta) * x1 + (1.0 + beta) * x2);
183                            (y1, y2)
184                        }
185                    };
186
187                    // Randomly assign the low/high offspring to child1/child2
188                    // (canonical: removes systematic bias tied to parent order).
189                    if rng.gen::<bool>() {
190                        child1[i] = c_low;
191                        child2[i] = c_high;
192                    } else {
193                        child1[i] = c_high;
194                        child2[i] = c_low;
195                    }
196                }
197            }
198        }
199
200        (child1, child2)
201    }
202}
203
204impl CrossoverOperator<RealVector> for SbxCrossover {
205    fn crossover<R: Rng>(
206        &self,
207        parent1: &RealVector,
208        parent2: &RealVector,
209        rng: &mut R,
210    ) -> OperatorResult<(RealVector, RealVector)> {
211        if parent1.dimension() != parent2.dimension() {
212            return OperatorResult::Failed(OperatorError::CrossoverFailed(
213                "Parent dimensions do not match".to_string(),
214            ));
215        }
216
217        let (child1_genes, child2_genes) =
218            self.apply_sbx(parent1.genes(), parent2.genes(), None, rng);
219
220        let child1 = RealVector::from_genes(child1_genes).unwrap();
221        let child2 = RealVector::from_genes(child2_genes).unwrap();
222
223        OperatorResult::Success((child1, child2))
224    }
225
226    fn crossover_probability(&self) -> f64 {
227        self.crossover_probability
228    }
229}
230
231impl BoundedCrossoverOperator<RealVector> for SbxCrossover {
232    fn crossover_bounded<R: Rng>(
233        &self,
234        parent1: &RealVector,
235        parent2: &RealVector,
236        bounds: &MultiBounds,
237        rng: &mut R,
238    ) -> OperatorResult<(RealVector, RealVector)> {
239        if parent1.dimension() != parent2.dimension() {
240            return OperatorResult::Failed(OperatorError::CrossoverFailed(
241                "Parent dimensions do not match".to_string(),
242            ));
243        }
244
245        let (child1_genes, child2_genes) =
246            self.apply_sbx(parent1.genes(), parent2.genes(), Some(bounds), rng);
247
248        let child1 = RealVector::from_genes(child1_genes).unwrap();
249        let child2 = RealVector::from_genes(child2_genes).unwrap();
250
251        OperatorResult::Success((child1, child2))
252    }
253}
254
255/// Blend Crossover (BLX-α)
256///
257/// Creates offspring within an extended range defined by parents.
258#[derive(Clone, Debug)]
259pub struct BlxAlphaCrossover {
260    /// Extension factor (typically 0.5)
261    pub alpha: f64,
262}
263
264impl BlxAlphaCrossover {
265    /// Create a new BLX-α crossover
266    pub fn new(alpha: f64) -> Self {
267        assert!(alpha >= 0.0, "Alpha must be non-negative");
268        Self { alpha }
269    }
270
271    /// Create with default alpha = 0.5
272    pub fn default_alpha() -> Self {
273        Self::new(0.5)
274    }
275}
276
277impl CrossoverOperator<RealVector> for BlxAlphaCrossover {
278    fn crossover<R: Rng>(
279        &self,
280        parent1: &RealVector,
281        parent2: &RealVector,
282        rng: &mut R,
283    ) -> OperatorResult<(RealVector, RealVector)> {
284        if parent1.dimension() != parent2.dimension() {
285            return OperatorResult::Failed(OperatorError::CrossoverFailed(
286                "Parent dimensions do not match".to_string(),
287            ));
288        }
289
290        let mut child1_genes = Vec::with_capacity(parent1.dimension());
291        let mut child2_genes = Vec::with_capacity(parent2.dimension());
292
293        for i in 0..parent1.dimension() {
294            let x1 = parent1[i];
295            let x2 = parent2[i];
296
297            let min_val = x1.min(x2);
298            let max_val = x1.max(x2);
299            let range = max_val - min_val;
300
301            let low = min_val - self.alpha * range;
302            let high = max_val + self.alpha * range;
303
304            child1_genes.push(rng.gen_range(low..=high));
305            child2_genes.push(rng.gen_range(low..=high));
306        }
307
308        let child1 = RealVector::from_genes(child1_genes).unwrap();
309        let child2 = RealVector::from_genes(child2_genes).unwrap();
310
311        OperatorResult::Success((child1, child2))
312    }
313}
314
315/// Uniform crossover for bit strings
316///
317/// Each bit is independently chosen from either parent with equal probability.
318#[derive(Clone, Debug)]
319pub struct UniformCrossover {
320    /// Probability of choosing from parent1 (default: 0.5)
321    pub bias: f64,
322}
323
324impl UniformCrossover {
325    /// Create a new uniform crossover
326    pub fn new() -> Self {
327        Self { bias: 0.5 }
328    }
329
330    /// Create with a specific bias towards parent1
331    pub fn with_bias(bias: f64) -> Self {
332        assert!((0.0..=1.0).contains(&bias), "Bias must be in [0, 1]");
333        Self { bias }
334    }
335}
336
337impl Default for UniformCrossover {
338    fn default() -> Self {
339        Self::new()
340    }
341}
342
343impl CrossoverOperator<BitString> for UniformCrossover {
344    fn crossover<R: Rng>(
345        &self,
346        parent1: &BitString,
347        parent2: &BitString,
348        rng: &mut R,
349    ) -> OperatorResult<(BitString, BitString)> {
350        if parent1.dimension() != parent2.dimension() {
351            return OperatorResult::Failed(OperatorError::CrossoverFailed(
352                "Parent dimensions do not match".to_string(),
353            ));
354        }
355
356        let mut child1_bits = Vec::with_capacity(parent1.dimension());
357        let mut child2_bits = Vec::with_capacity(parent2.dimension());
358
359        for i in 0..parent1.dimension() {
360            if rng.gen::<f64>() < self.bias {
361                child1_bits.push(parent1[i]);
362                child2_bits.push(parent2[i]);
363            } else {
364                child1_bits.push(parent2[i]);
365                child2_bits.push(parent1[i]);
366            }
367        }
368
369        let child1 = BitString::from_bits(child1_bits).unwrap();
370        let child2 = BitString::from_bits(child2_bits).unwrap();
371
372        OperatorResult::Success((child1, child2))
373    }
374}
375
376/// One-point crossover for bit strings
377#[derive(Clone, Debug, Default)]
378pub struct OnePointCrossover;
379
380impl OnePointCrossover {
381    /// Create a new one-point crossover
382    pub fn new() -> Self {
383        Self
384    }
385}
386
387impl CrossoverOperator<BitString> for OnePointCrossover {
388    fn crossover<R: Rng>(
389        &self,
390        parent1: &BitString,
391        parent2: &BitString,
392        rng: &mut R,
393    ) -> OperatorResult<(BitString, BitString)> {
394        if parent1.dimension() != parent2.dimension() {
395            return OperatorResult::Failed(OperatorError::CrossoverFailed(
396                "Parent dimensions do not match".to_string(),
397            ));
398        }
399
400        let n = parent1.dimension();
401        if n == 0 {
402            return OperatorResult::Success((parent1.clone(), parent2.clone()));
403        }
404
405        let crossover_point = rng.gen_range(0..n);
406
407        let mut child1_bits = Vec::with_capacity(n);
408        let mut child2_bits = Vec::with_capacity(n);
409
410        for i in 0..n {
411            if i < crossover_point {
412                child1_bits.push(parent1[i]);
413                child2_bits.push(parent2[i]);
414            } else {
415                child1_bits.push(parent2[i]);
416                child2_bits.push(parent1[i]);
417            }
418        }
419
420        let child1 = BitString::from_bits(child1_bits).unwrap();
421        let child2 = BitString::from_bits(child2_bits).unwrap();
422
423        OperatorResult::Success((child1, child2))
424    }
425}
426
427/// Two-point crossover for bit strings
428#[derive(Clone, Debug, Default)]
429pub struct TwoPointCrossover;
430
431impl TwoPointCrossover {
432    /// Create a new two-point crossover
433    pub fn new() -> Self {
434        Self
435    }
436}
437
438impl CrossoverOperator<BitString> for TwoPointCrossover {
439    fn crossover<R: Rng>(
440        &self,
441        parent1: &BitString,
442        parent2: &BitString,
443        rng: &mut R,
444    ) -> OperatorResult<(BitString, BitString)> {
445        if parent1.dimension() != parent2.dimension() {
446            return OperatorResult::Failed(OperatorError::CrossoverFailed(
447                "Parent dimensions do not match".to_string(),
448            ));
449        }
450
451        let n = parent1.dimension();
452        if n < 2 {
453            return OperatorResult::Success((parent1.clone(), parent2.clone()));
454        }
455
456        let mut point1 = rng.gen_range(0..n);
457        let mut point2 = rng.gen_range(0..n);
458        if point1 > point2 {
459            std::mem::swap(&mut point1, &mut point2);
460        }
461
462        let mut child1_bits = Vec::with_capacity(n);
463        let mut child2_bits = Vec::with_capacity(n);
464
465        for i in 0..n {
466            if i < point1 || i >= point2 {
467                child1_bits.push(parent1[i]);
468                child2_bits.push(parent2[i]);
469            } else {
470                child1_bits.push(parent2[i]);
471                child2_bits.push(parent1[i]);
472            }
473        }
474
475        let child1 = BitString::from_bits(child1_bits).unwrap();
476        let child2 = BitString::from_bits(child2_bits).unwrap();
477
478        OperatorResult::Success((child1, child2))
479    }
480}
481
482/// Arithmetic crossover for real-valued genomes
483///
484/// Creates offspring as weighted averages of parents.
485#[derive(Clone, Debug)]
486pub struct ArithmeticCrossover {
487    /// Weight for parent1 (parent2 weight = 1 - weight)
488    pub weight: f64,
489}
490
491impl ArithmeticCrossover {
492    /// Create a new arithmetic crossover with the given weight
493    pub fn new(weight: f64) -> Self {
494        assert!((0.0..=1.0).contains(&weight), "Weight must be in [0, 1]");
495        Self { weight }
496    }
497
498    /// Create with uniform weight (0.5)
499    pub fn uniform() -> Self {
500        Self::new(0.5)
501    }
502}
503
504impl CrossoverOperator<RealVector> for ArithmeticCrossover {
505    fn crossover<R: Rng>(
506        &self,
507        parent1: &RealVector,
508        parent2: &RealVector,
509        _rng: &mut R,
510    ) -> OperatorResult<(RealVector, RealVector)> {
511        if parent1.dimension() != parent2.dimension() {
512            return OperatorResult::Failed(OperatorError::CrossoverFailed(
513                "Parent dimensions do not match".to_string(),
514            ));
515        }
516
517        let w = self.weight;
518        let mut child1_genes = Vec::with_capacity(parent1.dimension());
519        let mut child2_genes = Vec::with_capacity(parent2.dimension());
520
521        for i in 0..parent1.dimension() {
522            child1_genes.push(w * parent1[i] + (1.0 - w) * parent2[i]);
523            child2_genes.push((1.0 - w) * parent1[i] + w * parent2[i]);
524        }
525
526        let child1 = RealVector::from_genes(child1_genes).unwrap();
527        let child2 = RealVector::from_genes(child2_genes).unwrap();
528
529        OperatorResult::Success((child1, child2))
530    }
531}
532
533// =============================================================================
534// Permutation Crossover Operators
535// =============================================================================
536
537/// Partially Mapped Crossover (PMX) for permutation genomes
538///
539/// PMX preserves relative order and position information from both parents.
540/// It selects a segment from parent1 and maps the corresponding positions
541/// from parent2, creating valid permutations.
542///
543/// Reference: Goldberg, D. E., & Lingle, R. (1985). Alleles, Loci, and the
544/// Traveling Salesman Problem. ICGA.
545#[derive(Clone, Debug, Default)]
546pub struct PmxCrossover;
547
548impl PmxCrossover {
549    /// Create a new PMX crossover operator
550    pub fn new() -> Self {
551        Self
552    }
553}
554
555impl CrossoverOperator<Permutation> for PmxCrossover {
556    fn crossover<R: Rng>(
557        &self,
558        parent1: &Permutation,
559        parent2: &Permutation,
560        rng: &mut R,
561    ) -> OperatorResult<(Permutation, Permutation)> {
562        let n = parent1.dimension();
563
564        if n != parent2.dimension() {
565            return OperatorResult::Failed(OperatorError::CrossoverFailed(
566                "Parent dimensions do not match".to_string(),
567            ));
568        }
569
570        if n < 2 {
571            return OperatorResult::Success((parent1.clone(), parent2.clone()));
572        }
573
574        // Select two crossover points
575        let mut start = rng.gen_range(0..n);
576        let mut end = rng.gen_range(0..n);
577        if start > end {
578            std::mem::swap(&mut start, &mut end);
579        }
580
581        let p1 = parent1.permutation();
582        let p2 = parent2.permutation();
583
584        // Initialize children with sentinel values
585        let mut child1 = vec![usize::MAX; n];
586        let mut child2 = vec![usize::MAX; n];
587
588        // Copy segments from opposite parent
589        for i in start..=end {
590            child1[i] = p2[i];
591            child2[i] = p1[i];
592        }
593
594        // Build mappings for the segment
595        let mut map1: HashMap<usize, usize> = HashMap::new();
596        let mut map2: HashMap<usize, usize> = HashMap::new();
597        for i in start..=end {
598            map1.insert(p2[i], p1[i]);
599            map2.insert(p1[i], p2[i]);
600        }
601
602        // Fill remaining positions
603        for i in (0..start).chain((end + 1)..n) {
604            // For child1: try to place p1[i], resolve conflicts via mapping
605            let mut val1 = p1[i];
606            while child1[start..=end].contains(&val1) {
607                val1 = *map1.get(&val1).unwrap_or(&val1);
608            }
609            child1[i] = val1;
610
611            // For child2: try to place p2[i], resolve conflicts via mapping
612            let mut val2 = p2[i];
613            while child2[start..=end].contains(&val2) {
614                val2 = *map2.get(&val2).unwrap_or(&val2);
615            }
616            child2[i] = val2;
617        }
618
619        let c1 = match Permutation::try_new(child1) {
620            Ok(p) => p,
621            Err(e) => {
622                return OperatorResult::Failed(OperatorError::CrossoverFailed(format!(
623                    "PMX produced invalid child1: {}",
624                    e
625                )))
626            }
627        };
628        let c2 = match Permutation::try_new(child2) {
629            Ok(p) => p,
630            Err(e) => {
631                return OperatorResult::Failed(OperatorError::CrossoverFailed(format!(
632                    "PMX produced invalid child2: {}",
633                    e
634                )))
635            }
636        };
637
638        OperatorResult::Success((c1, c2))
639    }
640}
641
642/// Order Crossover (OX) for permutation genomes
643///
644/// OX preserves the relative order of elements from one parent while
645/// copying a segment from the other parent.
646///
647/// Reference: Davis, L. (1985). Applying Adaptive Algorithms to Epistatic Domains.
648/// IJCAI.
649#[derive(Clone, Debug, Default)]
650pub struct OxCrossover;
651
652impl OxCrossover {
653    /// Create a new OX crossover operator
654    pub fn new() -> Self {
655        Self
656    }
657}
658
659impl CrossoverOperator<Permutation> for OxCrossover {
660    fn crossover<R: Rng>(
661        &self,
662        parent1: &Permutation,
663        parent2: &Permutation,
664        rng: &mut R,
665    ) -> OperatorResult<(Permutation, Permutation)> {
666        let n = parent1.dimension();
667
668        if n != parent2.dimension() {
669            return OperatorResult::Failed(OperatorError::CrossoverFailed(
670                "Parent dimensions do not match".to_string(),
671            ));
672        }
673
674        if n < 2 {
675            return OperatorResult::Success((parent1.clone(), parent2.clone()));
676        }
677
678        // Select two crossover points
679        let mut start = rng.gen_range(0..n);
680        let mut end = rng.gen_range(0..n);
681        if start > end {
682            std::mem::swap(&mut start, &mut end);
683        }
684
685        let child1 = Self::ox_single(parent1, parent2, start, end);
686        let child2 = Self::ox_single(parent2, parent1, start, end);
687
688        let c1 = match Permutation::try_new(child1) {
689            Ok(p) => p,
690            Err(e) => {
691                return OperatorResult::Failed(OperatorError::CrossoverFailed(format!(
692                    "OX produced invalid child1: {}",
693                    e
694                )))
695            }
696        };
697        let c2 = match Permutation::try_new(child2) {
698            Ok(p) => p,
699            Err(e) => {
700                return OperatorResult::Failed(OperatorError::CrossoverFailed(format!(
701                    "OX produced invalid child2: {}",
702                    e
703                )))
704            }
705        };
706
707        OperatorResult::Success((c1, c2))
708    }
709}
710
711impl OxCrossover {
712    /// Create a single child using OX
713    fn ox_single(
714        parent1: &Permutation,
715        parent2: &Permutation,
716        start: usize,
717        end: usize,
718    ) -> Vec<usize> {
719        let n = parent1.dimension();
720        let p1 = parent1.permutation();
721        let p2 = parent2.permutation();
722
723        let mut child = vec![usize::MAX; n];
724
725        // Copy segment from parent1
726        let segment: HashSet<usize> = p1[start..=end].iter().copied().collect();
727        for i in start..=end {
728            child[i] = p1[i];
729        }
730
731        // Fill remaining positions from parent2 in order, skipping segment elements
732        let mut pos = (end + 1) % n;
733        let mut p2_idx = (end + 1) % n;
734
735        while pos != start {
736            // Find next element from parent2 not in segment
737            while segment.contains(&p2[p2_idx]) {
738                p2_idx = (p2_idx + 1) % n;
739            }
740
741            child[pos] = p2[p2_idx];
742            pos = (pos + 1) % n;
743            p2_idx = (p2_idx + 1) % n;
744        }
745
746        child
747    }
748}
749
750/// Cycle Crossover (CX) for permutation genomes
751///
752/// CX produces offspring where each element's position comes from one parent,
753/// preserving the absolute position of elements. It identifies cycles in the
754/// parent mappings and alternates which parent contributes each cycle.
755///
756/// Reference: Oliver, I. M., Smith, D. J., & Holland, J. R. (1987).
757/// A Study of Permutation Crossover Operators on the Traveling Salesman Problem.
758#[derive(Clone, Debug, Default)]
759pub struct CxCrossover;
760
761impl CxCrossover {
762    /// Create a new CX crossover operator
763    pub fn new() -> Self {
764        Self
765    }
766}
767
768impl CrossoverOperator<Permutation> for CxCrossover {
769    fn crossover<R: Rng>(
770        &self,
771        parent1: &Permutation,
772        parent2: &Permutation,
773        _rng: &mut R,
774    ) -> OperatorResult<(Permutation, Permutation)> {
775        let n = parent1.dimension();
776
777        if n != parent2.dimension() {
778            return OperatorResult::Failed(OperatorError::CrossoverFailed(
779                "Parent dimensions do not match".to_string(),
780            ));
781        }
782
783        if n == 0 {
784            return OperatorResult::Success((parent1.clone(), parent2.clone()));
785        }
786
787        let p1 = parent1.permutation();
788        let p2 = parent2.permutation();
789
790        // Build position map: value -> position in parent1
791        let mut pos_in_p1: HashMap<usize, usize> = HashMap::new();
792        for (i, &val) in p1.iter().enumerate() {
793            pos_in_p1.insert(val, i);
794        }
795
796        // Find cycles and assign to children
797        let mut child1 = vec![usize::MAX; n];
798        let mut child2 = vec![usize::MAX; n];
799        let mut visited = vec![false; n];
800        let mut use_p1 = true; // Alternate which parent cycle goes to child1
801
802        for start in 0..n {
803            if visited[start] {
804                continue;
805            }
806
807            // Find the cycle starting at position `start`
808            let mut cycle_positions = Vec::new();
809            let mut pos = start;
810
811            loop {
812                cycle_positions.push(pos);
813                visited[pos] = true;
814
815                // Follow the cycle: position in p1 -> value in p2 at same position -> position of that value in p1
816                let val_in_p2 = p2[pos];
817                pos = *pos_in_p1.get(&val_in_p2).unwrap();
818
819                if pos == start {
820                    break;
821                }
822            }
823
824            // Assign cycle positions to children
825            for &cycle_pos in &cycle_positions {
826                if use_p1 {
827                    child1[cycle_pos] = p1[cycle_pos];
828                    child2[cycle_pos] = p2[cycle_pos];
829                } else {
830                    child1[cycle_pos] = p2[cycle_pos];
831                    child2[cycle_pos] = p1[cycle_pos];
832                }
833            }
834
835            use_p1 = !use_p1; // Alternate for next cycle
836        }
837
838        let c1 = match Permutation::try_new(child1) {
839            Ok(p) => p,
840            Err(e) => {
841                return OperatorResult::Failed(OperatorError::CrossoverFailed(format!(
842                    "CX produced invalid child1: {}",
843                    e
844                )))
845            }
846        };
847        let c2 = match Permutation::try_new(child2) {
848            Ok(p) => p,
849            Err(e) => {
850                return OperatorResult::Failed(OperatorError::CrossoverFailed(format!(
851                    "CX produced invalid child2: {}",
852                    e
853                )))
854            }
855        };
856
857        OperatorResult::Success((c1, c2))
858    }
859}
860
861/// Edge Recombination Crossover (ERX) for permutation genomes
862///
863/// ERX focuses on preserving edges (adjacencies) from both parents,
864/// which is particularly useful for TSP-like problems.
865#[derive(Clone, Debug, Default)]
866pub struct EdgeRecombinationCrossover;
867
868impl EdgeRecombinationCrossover {
869    /// Create a new edge recombination crossover operator
870    pub fn new() -> Self {
871        Self
872    }
873
874    /// Build edge table from parents
875    fn build_edge_table(
876        parent1: &Permutation,
877        parent2: &Permutation,
878    ) -> HashMap<usize, HashSet<usize>> {
879        let n = parent1.dimension();
880        let p1 = parent1.permutation();
881        let p2 = parent2.permutation();
882
883        let mut edges: HashMap<usize, HashSet<usize>> = HashMap::new();
884
885        // Initialize all nodes
886        for i in 0..n {
887            edges.insert(i, HashSet::new());
888        }
889
890        // Add edges from parent1
891        for i in 0..n {
892            let curr = p1[i];
893            let prev = p1[(i + n - 1) % n];
894            let next = p1[(i + 1) % n];
895            edges.get_mut(&curr).unwrap().insert(prev);
896            edges.get_mut(&curr).unwrap().insert(next);
897        }
898
899        // Add edges from parent2
900        for i in 0..n {
901            let curr = p2[i];
902            let prev = p2[(i + n - 1) % n];
903            let next = p2[(i + 1) % n];
904            edges.get_mut(&curr).unwrap().insert(prev);
905            edges.get_mut(&curr).unwrap().insert(next);
906        }
907
908        edges
909    }
910}
911
912impl CrossoverOperator<Permutation> for EdgeRecombinationCrossover {
913    fn crossover<R: Rng>(
914        &self,
915        parent1: &Permutation,
916        parent2: &Permutation,
917        rng: &mut R,
918    ) -> OperatorResult<(Permutation, Permutation)> {
919        let n = parent1.dimension();
920
921        if n != parent2.dimension() {
922            return OperatorResult::Failed(OperatorError::CrossoverFailed(
923                "Parent dimensions do not match".to_string(),
924            ));
925        }
926
927        if n < 2 {
928            return OperatorResult::Success((parent1.clone(), parent2.clone()));
929        }
930
931        // Build edge table
932        let mut edges = Self::build_edge_table(parent1, parent2);
933
934        // Build child
935        let mut child = Vec::with_capacity(n);
936        let mut remaining: HashSet<usize> = (0..n).collect();
937
938        // Start with first element of parent1
939        let mut current = parent1.permutation()[0];
940        child.push(current);
941        remaining.remove(&current);
942
943        // Remove current from all edge lists
944        for edge_set in edges.values_mut() {
945            edge_set.remove(&current);
946        }
947
948        while child.len() < n {
949            // Get neighbors of current
950            let neighbors = edges.get(&current).cloned().unwrap_or_default();
951
952            // Choose next: prefer neighbor with fewest remaining edges
953            let next = if !neighbors.is_empty() {
954                let filtered: Vec<usize> = neighbors
955                    .iter()
956                    .filter(|x| remaining.contains(x))
957                    .copied()
958                    .collect();
959                if filtered.is_empty() {
960                    // Pick random from remaining
961                    let remaining_vec: Vec<usize> = remaining.iter().copied().collect();
962                    remaining_vec[rng.gen_range(0..remaining_vec.len())]
963                } else {
964                    // Pick one with minimum edge count
965                    *filtered
966                        .iter()
967                        .min_by_key(|&&x| edges.get(&x).map(|s| s.len()).unwrap_or(0))
968                        .unwrap()
969                }
970            } else {
971                // No neighbors, pick random from remaining
972                let remaining_vec: Vec<usize> = remaining.iter().copied().collect();
973                remaining_vec[rng.gen_range(0..remaining_vec.len())]
974            };
975
976            child.push(next);
977            remaining.remove(&next);
978            current = next;
979
980            // Remove current from all edge lists
981            for edge_set in edges.values_mut() {
982                edge_set.remove(&current);
983            }
984        }
985
986        // Create second child by running again with different starting point
987        let mut edges2 = Self::build_edge_table(parent1, parent2);
988        let mut child2 = Vec::with_capacity(n);
989        let mut remaining2: HashSet<usize> = (0..n).collect();
990
991        // Start with first element of parent2
992        let mut current2 = parent2.permutation()[0];
993        child2.push(current2);
994        remaining2.remove(&current2);
995
996        for edge_set in edges2.values_mut() {
997            edge_set.remove(&current2);
998        }
999
1000        while child2.len() < n {
1001            let neighbors = edges2.get(&current2).cloned().unwrap_or_default();
1002
1003            let next2 = if !neighbors.is_empty() {
1004                let filtered: Vec<usize> = neighbors
1005                    .iter()
1006                    .filter(|x| remaining2.contains(x))
1007                    .copied()
1008                    .collect();
1009                if filtered.is_empty() {
1010                    let remaining_vec: Vec<usize> = remaining2.iter().copied().collect();
1011                    remaining_vec[rng.gen_range(0..remaining_vec.len())]
1012                } else {
1013                    *filtered
1014                        .iter()
1015                        .min_by_key(|&&x| edges2.get(&x).map(|s| s.len()).unwrap_or(0))
1016                        .unwrap()
1017                }
1018            } else {
1019                let remaining_vec: Vec<usize> = remaining2.iter().copied().collect();
1020                remaining_vec[rng.gen_range(0..remaining_vec.len())]
1021            };
1022
1023            child2.push(next2);
1024            remaining2.remove(&next2);
1025            current2 = next2;
1026
1027            for edge_set in edges2.values_mut() {
1028                edge_set.remove(&current2);
1029            }
1030        }
1031
1032        let c1 = match Permutation::try_new(child) {
1033            Ok(p) => p,
1034            Err(e) => {
1035                return OperatorResult::Failed(OperatorError::CrossoverFailed(format!(
1036                    "ERX produced invalid child1: {}",
1037                    e
1038                )))
1039            }
1040        };
1041        let c2 = match Permutation::try_new(child2) {
1042            Ok(p) => p,
1043            Err(e) => {
1044                return OperatorResult::Failed(OperatorError::CrossoverFailed(format!(
1045                    "ERX produced invalid child2: {}",
1046                    e
1047                )))
1048            }
1049        };
1050
1051        OperatorResult::Success((c1, c2))
1052    }
1053}
1054
1055// ============================================================================
1056// Genetic Programming Crossover
1057// ============================================================================
1058
1059use crate::genome::tree::{Function, Terminal, TreeGenome, TreeNode};
1060
1061/// Subtree Crossover for genetic programming
1062///
1063/// Selects random subtrees from two parent trees and swaps them
1064/// to create offspring. This is the standard crossover operator for GP.
1065#[derive(Clone, Debug)]
1066pub struct SubtreeCrossover {
1067    /// Maximum depth for offspring trees (to control bloat)
1068    pub max_depth: Option<usize>,
1069    /// Probability of selecting a function node (vs terminal)
1070    pub function_probability: f64,
1071}
1072
1073impl Default for SubtreeCrossover {
1074    fn default() -> Self {
1075        Self {
1076            max_depth: Some(17),       // Standard GP default
1077            function_probability: 0.9, // Favor internal nodes
1078        }
1079    }
1080}
1081
1082impl SubtreeCrossover {
1083    /// Create a new subtree crossover operator
1084    pub fn new() -> Self {
1085        Self::default()
1086    }
1087
1088    /// Create with a specific maximum depth limit
1089    pub fn with_max_depth(mut self, max_depth: usize) -> Self {
1090        self.max_depth = Some(max_depth);
1091        self
1092    }
1093
1094    /// Create with no depth limit
1095    pub fn without_depth_limit(mut self) -> Self {
1096        self.max_depth = None;
1097        self
1098    }
1099
1100    /// Set the probability of selecting a function node
1101    pub fn with_function_probability(mut self, prob: f64) -> Self {
1102        self.function_probability = prob.clamp(0.0, 1.0);
1103        self
1104    }
1105
1106    /// Select a random crossover point in the tree
1107    fn select_crossover_point<T: Terminal, F: Function, R: Rng>(
1108        &self,
1109        tree: &TreeNode<T, F>,
1110        rng: &mut R,
1111    ) -> Vec<usize> {
1112        // Decide whether to select a function or terminal node
1113        let select_function = rng.gen::<f64>() < self.function_probability;
1114
1115        let positions = if select_function {
1116            let func_pos = tree.function_positions();
1117            if func_pos.is_empty() {
1118                tree.positions() // Fall back to all positions
1119            } else {
1120                func_pos
1121            }
1122        } else {
1123            let term_pos = tree.terminal_positions();
1124            if term_pos.is_empty() {
1125                tree.positions()
1126            } else {
1127                term_pos
1128            }
1129        };
1130
1131        if positions.is_empty() {
1132            vec![] // Root position
1133        } else {
1134            positions[rng.gen_range(0..positions.len())].clone()
1135        }
1136    }
1137}
1138
1139impl<T: Terminal, F: Function> CrossoverOperator<TreeGenome<T, F>> for SubtreeCrossover {
1140    fn crossover<R: Rng>(
1141        &self,
1142        parent1: &TreeGenome<T, F>,
1143        parent2: &TreeGenome<T, F>,
1144        rng: &mut R,
1145    ) -> OperatorResult<(TreeGenome<T, F>, TreeGenome<T, F>)> {
1146        // Select crossover points in each parent
1147        let point1 = self.select_crossover_point(&parent1.root, rng);
1148        let point2 = self.select_crossover_point(&parent2.root, rng);
1149
1150        // Get the subtrees at those points
1151        let subtree1 = parent1
1152            .root
1153            .get_subtree(&point1)
1154            .cloned()
1155            .unwrap_or_else(|| parent1.root.clone());
1156        let subtree2 = parent2
1157            .root
1158            .get_subtree(&point2)
1159            .cloned()
1160            .unwrap_or_else(|| parent2.root.clone());
1161
1162        // Create offspring by swapping subtrees
1163        let mut child1_root = parent1.root.clone();
1164        let mut child2_root = parent2.root.clone();
1165
1166        // Replace subtrees
1167        if point1.is_empty() {
1168            child1_root = subtree2.clone();
1169        } else {
1170            child1_root.replace_subtree(&point1, subtree2.clone());
1171        }
1172
1173        if point2.is_empty() {
1174            child2_root = subtree1.clone();
1175        } else {
1176            child2_root.replace_subtree(&point2, subtree1);
1177        }
1178
1179        // Check depth limit and reject if exceeded
1180        if let Some(max_depth) = self.max_depth {
1181            if child1_root.depth() > max_depth {
1182                // Fallback: return clones of parents
1183                return OperatorResult::Success((parent1.clone(), parent2.clone()));
1184            }
1185            if child2_root.depth() > max_depth {
1186                return OperatorResult::Success((parent1.clone(), parent2.clone()));
1187            }
1188        }
1189
1190        let child1 = TreeGenome::new(child1_root, parent1.max_depth);
1191        let child2 = TreeGenome::new(child2_root, parent2.max_depth);
1192
1193        OperatorResult::Success((child1, child2))
1194    }
1195}
1196
1197#[cfg(test)]
1198mod tests {
1199    use super::*;
1200    use approx::assert_relative_eq;
1201
1202    #[test]
1203    fn test_sbx_creates_valid_offspring() {
1204        let mut rng = rand::thread_rng();
1205        let parent1 = RealVector::new(vec![0.0, 0.0, 0.0]);
1206        let parent2 = RealVector::new(vec![1.0, 1.0, 1.0]);
1207
1208        let sbx = SbxCrossover::new(20.0);
1209        let result = sbx.crossover(&parent1, &parent2, &mut rng);
1210
1211        assert!(result.is_ok());
1212        let (child1, child2) = result.genome().unwrap();
1213        assert_eq!(child1.dimension(), 3);
1214        assert_eq!(child2.dimension(), 3);
1215    }
1216
1217    #[test]
1218    fn test_sbx_with_bounds() {
1219        let mut rng = rand::thread_rng();
1220        // Use parents within bounds
1221        let parent1 = RealVector::new(vec![-0.3, -0.2]);
1222        let parent2 = RealVector::new(vec![0.3, 0.4]);
1223        let bounds = MultiBounds::symmetric(0.5, 2);
1224
1225        // Low eta = more spread; force per-gene recombination so bounds are
1226        // actually exercised.
1227        let sbx = SbxCrossover::new(2.0).with_exchange_probability(1.0);
1228
1229        // Run multiple times and check bounds
1230        for _ in 0..100 {
1231            let result = sbx.crossover_bounded(&parent1, &parent2, &bounds, &mut rng);
1232            let (child1, child2) = result.genome().unwrap();
1233
1234            for gene in child1.genes() {
1235                assert!(
1236                    *gene >= -0.5 && *gene <= 0.5,
1237                    "gene {} out of bounds [-0.5, 0.5]",
1238                    gene
1239                );
1240            }
1241            for gene in child2.genes() {
1242                assert!(
1243                    *gene >= -0.5 && *gene <= 0.5,
1244                    "gene {} out of bounds [-0.5, 0.5]",
1245                    gene
1246                );
1247            }
1248        }
1249    }
1250
1251    #[test]
1252    fn test_sbx_default_probabilities_are_distinct() {
1253        // regression: EV-72 — the per-pair crossover probability (0.9) and the
1254        // per-gene SBX exchange probability (0.5) are separate quantities with
1255        // canonical defaults. Previously a single 0.9 field was overloaded as
1256        // both, so the reported per-pair rate equalled the per-gene mixing rate.
1257        let sbx = SbxCrossover::new(20.0);
1258        assert_eq!(sbx.crossover_probability, 0.9);
1259        assert_eq!(sbx.exchange_probability, 0.5);
1260
1261        // The trait-reported probability is the per-pair rate, and setting it
1262        // must not disturb the per-gene exchange rate.
1263        let sbx = SbxCrossover::new(20.0).with_probability(0.7);
1264        assert_eq!(
1265            CrossoverOperator::<RealVector>::crossover_probability(&sbx),
1266            0.7
1267        );
1268        assert_eq!(sbx.exchange_probability, 0.5);
1269
1270        // ...and vice versa.
1271        let sbx = SbxCrossover::new(20.0).with_exchange_probability(0.3);
1272        assert_eq!(sbx.exchange_probability, 0.3);
1273        assert_eq!(
1274            CrossoverOperator::<RealVector>::crossover_probability(&sbx),
1275            0.9
1276        );
1277    }
1278
1279    #[test]
1280    fn test_sbx_bounded_no_atom_at_bounds_mean_preserving() {
1281        // regression: EV-71 — bounds-aware SBX draws children inside [min, max]
1282        // by construction; it must NOT pile probability mass onto the bounds
1283        // the way the old clamp-only path did. Monte Carlo: (a) every child is
1284        // strictly within bounds, (b) essentially no children land exactly on a
1285        // bound (pre-fix ~10% did), and (c) the mean is preserved (symmetric
1286        // parents => midpoint preserved exactly per draw, so the sample mean is
1287        // the parent midpoint).
1288        use rand::SeedableRng;
1289        let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
1290
1291        let bounds = MultiBounds::symmetric(1.0, 1); // [-1, 1]
1292        let parent1 = RealVector::new(vec![-0.6]);
1293        let parent2 = RealVector::new(vec![0.6]);
1294        let parent_mid = 0.0;
1295
1296        // Low eta => wide spread => the old clamp path would frequently push
1297        // offspring onto the bounds; force per-gene recombination every draw.
1298        let sbx = SbxCrossover::new(2.0).with_exchange_probability(1.0);
1299
1300        let n = 20_000;
1301        let mut atom_count = 0usize;
1302        let mut sum_children = 0.0;
1303        let mut child_samples = 0usize;
1304        let eps = 1e-9;
1305
1306        for _ in 0..n {
1307            let result = sbx.crossover_bounded(&parent1, &parent2, &bounds, &mut rng);
1308            let (c1, c2) = result.genome().unwrap();
1309            for &g in c1.genes().iter().chain(c2.genes()) {
1310                assert!(
1311                    (-1.0..=1.0).contains(&g),
1312                    "child gene {g} escaped bounds [-1, 1]"
1313                );
1314                if (g - 1.0).abs() < eps || (g + 1.0).abs() < eps {
1315                    atom_count += 1;
1316                }
1317                sum_children += g;
1318                child_samples += 1;
1319            }
1320        }
1321
1322        let atom_frac = atom_count as f64 / child_samples as f64;
1323        assert!(
1324            atom_frac < 0.01,
1325            "too many children pinned to the bounds: {atom_frac} (expected ~0)"
1326        );
1327
1328        let mean_children = sum_children / child_samples as f64;
1329        assert!(
1330            (mean_children - parent_mid).abs() < 0.02,
1331            "bounded SBX is not mean-preserving: mean {mean_children} vs parent midpoint {parent_mid}"
1332        );
1333    }
1334
1335    #[test]
1336    fn test_sbx_spread_factor() {
1337        let sbx = SbxCrossover::new(20.0);
1338
1339        // At u = 0.5, β should be 1.0
1340        let beta = sbx.spread_factor(0.5);
1341        assert_relative_eq!(beta, 1.0, epsilon = 1e-10);
1342
1343        // β should be symmetric around 0.5
1344        let beta_low = sbx.spread_factor(0.25);
1345        let beta_high = sbx.spread_factor(0.75);
1346        assert_relative_eq!(beta_low, 1.0 / beta_high, epsilon = 1e-10);
1347    }
1348
1349    #[test]
1350    fn test_sbx_identical_parents() {
1351        let mut rng = rand::thread_rng();
1352        let parent = RealVector::new(vec![1.0, 2.0, 3.0]);
1353
1354        let sbx = SbxCrossover::new(20.0);
1355        let result = sbx.crossover(&parent, &parent, &mut rng);
1356
1357        let (child1, child2) = result.genome().unwrap();
1358        // With identical parents, children should equal parents
1359        assert_eq!(child1.genes(), parent.genes());
1360        assert_eq!(child2.genes(), parent.genes());
1361    }
1362
1363    #[test]
1364    fn test_sbx_dimension_mismatch() {
1365        let mut rng = rand::thread_rng();
1366        let parent1 = RealVector::new(vec![1.0, 2.0]);
1367        let parent2 = RealVector::new(vec![1.0, 2.0, 3.0]);
1368
1369        let sbx = SbxCrossover::new(20.0);
1370        let result = sbx.crossover(&parent1, &parent2, &mut rng);
1371
1372        assert!(!result.is_ok());
1373    }
1374
1375    #[test]
1376    fn test_blx_alpha_creates_valid_offspring() {
1377        let mut rng = rand::thread_rng();
1378        let parent1 = RealVector::new(vec![0.0, 0.0]);
1379        let parent2 = RealVector::new(vec![1.0, 1.0]);
1380
1381        let blx = BlxAlphaCrossover::new(0.5);
1382        let result = blx.crossover(&parent1, &parent2, &mut rng);
1383
1384        assert!(result.is_ok());
1385        let (child1, child2) = result.genome().unwrap();
1386        assert_eq!(child1.dimension(), 2);
1387        assert_eq!(child2.dimension(), 2);
1388    }
1389
1390    #[test]
1391    fn test_blx_alpha_range() {
1392        let mut rng = rand::thread_rng();
1393        let parent1 = RealVector::new(vec![0.0]);
1394        let parent2 = RealVector::new(vec![1.0]);
1395
1396        let blx = BlxAlphaCrossover::new(0.0); // No extension
1397
1398        for _ in 0..100 {
1399            let result = blx.crossover(&parent1, &parent2, &mut rng);
1400            let (child1, child2) = result.genome().unwrap();
1401
1402            // With α = 0, offspring should be in [0, 1]
1403            assert!(child1[0] >= 0.0 && child1[0] <= 1.0);
1404            assert!(child2[0] >= 0.0 && child2[0] <= 1.0);
1405        }
1406    }
1407
1408    #[test]
1409    fn test_uniform_crossover_creates_valid_offspring() {
1410        let mut rng = rand::thread_rng();
1411        let parent1 = BitString::new(vec![true, true, true, true]);
1412        let parent2 = BitString::new(vec![false, false, false, false]);
1413
1414        let ux = UniformCrossover::new();
1415        let result = ux.crossover(&parent1, &parent2, &mut rng);
1416
1417        assert!(result.is_ok());
1418        let (child1, child2) = result.genome().unwrap();
1419        assert_eq!(child1.len(), 4);
1420        assert_eq!(child2.len(), 4);
1421    }
1422
1423    #[test]
1424    fn test_uniform_crossover_complementary() {
1425        let mut rng = rand::thread_rng();
1426        let parent1 = BitString::new(vec![true, true, true, true]);
1427        let parent2 = BitString::new(vec![false, false, false, false]);
1428
1429        let ux = UniformCrossover::new();
1430        let result = ux.crossover(&parent1, &parent2, &mut rng);
1431
1432        let (child1, child2) = result.genome().unwrap();
1433
1434        // Children should be complementary
1435        for i in 0..4 {
1436            assert_ne!(child1[i], child2[i]);
1437        }
1438    }
1439
1440    #[test]
1441    fn test_one_point_crossover() {
1442        let mut rng = rand::thread_rng();
1443        let parent1 = BitString::ones(10);
1444        let parent2 = BitString::zeros(10);
1445
1446        let opx = OnePointCrossover::new();
1447        let result = opx.crossover(&parent1, &parent2, &mut rng);
1448
1449        assert!(result.is_ok());
1450        let (child1, child2) = result.genome().unwrap();
1451
1452        // Children should have a contiguous segment from each parent
1453        // and the children should be complementary
1454        for i in 0..10 {
1455            assert_ne!(child1[i], child2[i]);
1456        }
1457    }
1458
1459    #[test]
1460    fn test_two_point_crossover() {
1461        let mut rng = rand::thread_rng();
1462        let parent1 = BitString::ones(10);
1463        let parent2 = BitString::zeros(10);
1464
1465        let tpx = TwoPointCrossover::new();
1466        let result = tpx.crossover(&parent1, &parent2, &mut rng);
1467
1468        assert!(result.is_ok());
1469        let (child1, child2) = result.genome().unwrap();
1470        assert_eq!(child1.len(), 10);
1471        assert_eq!(child2.len(), 10);
1472    }
1473
1474    #[test]
1475    fn test_arithmetic_crossover() {
1476        let mut rng = rand::thread_rng();
1477        let parent1 = RealVector::new(vec![0.0, 0.0]);
1478        let parent2 = RealVector::new(vec![1.0, 1.0]);
1479
1480        let ax = ArithmeticCrossover::new(0.5);
1481        let result = ax.crossover(&parent1, &parent2, &mut rng);
1482
1483        let (child1, child2) = result.genome().unwrap();
1484
1485        // With 0.5 weight, both children should be at midpoint
1486        for gene in child1.genes() {
1487            assert_relative_eq!(*gene, 0.5);
1488        }
1489        for gene in child2.genes() {
1490            assert_relative_eq!(*gene, 0.5);
1491        }
1492    }
1493
1494    #[test]
1495    fn test_arithmetic_crossover_weighted() {
1496        let mut rng = rand::thread_rng();
1497        let parent1 = RealVector::new(vec![0.0]);
1498        let parent2 = RealVector::new(vec![1.0]);
1499
1500        let ax = ArithmeticCrossover::new(0.75);
1501        let result = ax.crossover(&parent1, &parent2, &mut rng);
1502
1503        let (child1, child2) = result.genome().unwrap();
1504
1505        // child1 = 0.75 * 0 + 0.25 * 1 = 0.25
1506        // child2 = 0.25 * 0 + 0.75 * 1 = 0.75
1507        assert_relative_eq!(child1[0], 0.25);
1508        assert_relative_eq!(child2[0], 0.75);
1509    }
1510
1511    // =========================================================================
1512    // Permutation Crossover Tests
1513    // =========================================================================
1514
1515    #[test]
1516    fn test_pmx_creates_valid_permutations() {
1517        let mut rng = rand::thread_rng();
1518        let parent1 = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1519        let parent2 = Permutation::new(vec![7, 6, 5, 4, 3, 2, 1, 0]);
1520
1521        let pmx = PmxCrossover::new();
1522
1523        for _ in 0..100 {
1524            let result = pmx.crossover(&parent1, &parent2, &mut rng);
1525            assert!(result.is_ok());
1526            let (child1, child2) = result.genome().unwrap();
1527
1528            assert!(child1.is_valid_permutation());
1529            assert!(child2.is_valid_permutation());
1530            assert_eq!(child1.dimension(), 8);
1531            assert_eq!(child2.dimension(), 8);
1532        }
1533    }
1534
1535    #[test]
1536    fn test_pmx_identical_parents() {
1537        let mut rng = rand::thread_rng();
1538        let parent = Permutation::new(vec![0, 1, 2, 3, 4]);
1539
1540        let pmx = PmxCrossover::new();
1541        let result = pmx.crossover(&parent, &parent, &mut rng);
1542
1543        let (child1, child2) = result.genome().unwrap();
1544        // With identical parents, children should equal parents
1545        assert_eq!(child1.as_slice(), parent.as_slice());
1546        assert_eq!(child2.as_slice(), parent.as_slice());
1547    }
1548
1549    #[test]
1550    fn test_pmx_dimension_mismatch() {
1551        let mut rng = rand::thread_rng();
1552        let parent1 = Permutation::new(vec![0, 1, 2, 3]);
1553        let parent2 = Permutation::new(vec![0, 1, 2, 3, 4]);
1554
1555        let pmx = PmxCrossover::new();
1556        let result = pmx.crossover(&parent1, &parent2, &mut rng);
1557
1558        assert!(!result.is_ok());
1559    }
1560
1561    #[test]
1562    fn test_ox_creates_valid_permutations() {
1563        let mut rng = rand::thread_rng();
1564        let parent1 = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1565        let parent2 = Permutation::new(vec![7, 6, 5, 4, 3, 2, 1, 0]);
1566
1567        let ox = OxCrossover::new();
1568
1569        for _ in 0..100 {
1570            let result = ox.crossover(&parent1, &parent2, &mut rng);
1571            assert!(result.is_ok());
1572            let (child1, child2) = result.genome().unwrap();
1573
1574            assert!(child1.is_valid_permutation());
1575            assert!(child2.is_valid_permutation());
1576            assert_eq!(child1.dimension(), 8);
1577            assert_eq!(child2.dimension(), 8);
1578        }
1579    }
1580
1581    #[test]
1582    fn test_ox_preserves_segment() {
1583        use rand::SeedableRng;
1584        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1585        let parent1 = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1586        let parent2 = Permutation::new(vec![7, 6, 5, 4, 3, 2, 1, 0]);
1587
1588        let ox = OxCrossover::new();
1589        let result = ox.crossover(&parent1, &parent2, &mut rng);
1590
1591        let (child1, child2) = result.genome().unwrap();
1592        assert!(child1.is_valid_permutation());
1593        assert!(child2.is_valid_permutation());
1594    }
1595
1596    #[test]
1597    fn test_cx_creates_valid_permutations() {
1598        let mut rng = rand::thread_rng();
1599        let parent1 = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1600        let parent2 = Permutation::new(vec![1, 2, 3, 4, 5, 6, 7, 0]);
1601
1602        let cx = CxCrossover::new();
1603
1604        for _ in 0..100 {
1605            let result = cx.crossover(&parent1, &parent2, &mut rng);
1606            assert!(result.is_ok());
1607            let (child1, child2) = result.genome().unwrap();
1608
1609            assert!(child1.is_valid_permutation());
1610            assert!(child2.is_valid_permutation());
1611            assert_eq!(child1.dimension(), 8);
1612            assert_eq!(child2.dimension(), 8);
1613        }
1614    }
1615
1616    #[test]
1617    fn test_cx_preserves_positions() {
1618        let mut rng = rand::thread_rng();
1619        // CX should preserve positions from one parent or the other
1620        let parent1 = Permutation::new(vec![0, 1, 2, 3, 4]);
1621        let parent2 = Permutation::new(vec![4, 3, 2, 1, 0]);
1622
1623        let cx = CxCrossover::new();
1624        let result = cx.crossover(&parent1, &parent2, &mut rng);
1625
1626        let (child1, child2) = result.genome().unwrap();
1627
1628        // Each position in child should have the value from one of the parents at that position
1629        for i in 0..5 {
1630            let c1_val = child1[i];
1631            let c2_val = child2[i];
1632
1633            assert!(c1_val == parent1[i] || c1_val == parent2[i]);
1634            assert!(c2_val == parent1[i] || c2_val == parent2[i]);
1635        }
1636    }
1637
1638    #[test]
1639    fn test_cx_identical_parents() {
1640        let mut rng = rand::thread_rng();
1641        let parent = Permutation::new(vec![0, 1, 2, 3, 4]);
1642
1643        let cx = CxCrossover::new();
1644        let result = cx.crossover(&parent, &parent, &mut rng);
1645
1646        let (child1, child2) = result.genome().unwrap();
1647        // With identical parents, children should equal parents
1648        assert_eq!(child1.as_slice(), parent.as_slice());
1649        assert_eq!(child2.as_slice(), parent.as_slice());
1650    }
1651
1652    #[test]
1653    fn test_erx_creates_valid_permutations() {
1654        let mut rng = rand::thread_rng();
1655        let parent1 = Permutation::new(vec![0, 1, 2, 3, 4, 5, 6, 7]);
1656        let parent2 = Permutation::new(vec![7, 6, 5, 4, 3, 2, 1, 0]);
1657
1658        let erx = EdgeRecombinationCrossover::new();
1659
1660        for _ in 0..50 {
1661            let result = erx.crossover(&parent1, &parent2, &mut rng);
1662            assert!(result.is_ok());
1663            let (child1, child2) = result.genome().unwrap();
1664
1665            assert!(child1.is_valid_permutation());
1666            assert!(child2.is_valid_permutation());
1667            assert_eq!(child1.dimension(), 8);
1668            assert_eq!(child2.dimension(), 8);
1669        }
1670    }
1671
1672    #[test]
1673    fn test_erx_preserves_some_edges() {
1674        use rand::SeedableRng;
1675        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1676
1677        // Create parents with some common edges
1678        let parent1 = Permutation::new(vec![0, 1, 2, 3, 4]);
1679        let parent2 = Permutation::new(vec![0, 1, 4, 3, 2]);
1680
1681        // Edge 0-1 is common to both parents
1682        let erx = EdgeRecombinationCrossover::new();
1683        let result = erx.crossover(&parent1, &parent2, &mut rng);
1684
1685        let (child1, _child2) = result.genome().unwrap();
1686        assert!(child1.is_valid_permutation());
1687    }
1688}