Skip to main content

rlevo_evolution/ops/
crossover.rs

1//! Recombination / crossover operators for real-valued genomes.
2//!
3//! Operators in this module consume two parent tensors of shape
4//! `(N, D)` and produce an offspring tensor of the same shape. Seeding
5//! the per-generation RNG via `B::seed(device, ...)` is the caller's
6//! responsibility — see `crate::strategy` for the canonical pattern.
7//!
8//! # BLX-α
9//!
10//! For each gene, child ∈ `U(min(a,b) − α·|a−b|, max(a,b) + α·|a−b|)`.
11//! A common default is α = 0.5.
12//!
13//! # Uniform
14//!
15//! For each gene, child takes parent A's value with probability `p` and
16//! parent B's otherwise. Pure swap crossover — no blending — so the
17//! distribution is exactly preserved. A binary-genome variant
18//! ([`binary_uniform_crossover`]) operates on `Tensor<B, 2, Int>` with
19//! values in `{0, 1}`.
20
21use burn::tensor::{backend::Backend, Distribution, Int, Tensor};
22
23/// BLX-α crossover between two parent populations.
24///
25/// # Panics
26///
27/// Panics if the parents do not have matching shapes.
28#[must_use]
29pub fn blx_alpha<B: Backend>(
30    parent_a: Tensor<B, 2>,
31    parent_b: Tensor<B, 2>,
32    alpha: f32,
33    device: &B::Device,
34) -> Tensor<B, 2> {
35    assert_eq!(
36        parent_a.shape().dims,
37        parent_b.shape().dims,
38        "BLX-α: parents must have identical shapes"
39    );
40    let shape = parent_a.shape();
41
42    let min = parent_a.clone().min_pair(parent_b.clone());
43    let max = parent_a.max_pair(parent_b);
44    let diff = max.clone() - min.clone();
45    let lo = min - diff.clone().mul_scalar(alpha);
46    let hi = max + diff.mul_scalar(alpha);
47
48    let u = Tensor::<B, 2>::random(shape, Distribution::Uniform(0.0, 1.0), device);
49    lo.clone() + u * (hi - lo)
50}
51
52/// Uniform crossover: per-gene Bernoulli swap between parents.
53///
54/// `p` is the probability of keeping parent A's gene.
55///
56/// # Panics
57///
58/// Panics if the parents do not have matching shapes.
59#[must_use]
60pub fn uniform_crossover<B: Backend>(
61    parent_a: Tensor<B, 2>,
62    parent_b: Tensor<B, 2>,
63    p: f32,
64    device: &B::Device,
65) -> Tensor<B, 2> {
66    assert_eq!(
67        parent_a.shape().dims,
68        parent_b.shape().dims,
69        "uniform crossover: parents must have identical shapes"
70    );
71    let shape = parent_a.shape();
72    let u = Tensor::<B, 2>::random(shape, Distribution::Uniform(0.0, 1.0), device);
73    let keep_a = u.lower_elem(p);
74    parent_a.mask_where(keep_a.bool_not(), parent_b)
75}
76
77/// Binary uniform crossover on `Tensor<B, 2, Int>` populations.
78///
79/// For each gene, keep parent A with probability `p`, else parent B.
80/// `parent_a` and `parent_b` must hold values in `{0, 1}`.
81///
82/// # Panics
83///
84/// Panics if the parents do not have matching shapes.
85#[must_use]
86pub fn binary_uniform_crossover<B: Backend>(
87    parent_a: Tensor<B, 2, Int>,
88    parent_b: Tensor<B, 2, Int>,
89    p: f32,
90    device: &B::Device,
91) -> Tensor<B, 2, Int> {
92    assert_eq!(
93        parent_a.shape().dims,
94        parent_b.shape().dims,
95        "binary uniform crossover: parents must have identical shapes"
96    );
97    let shape = parent_a.shape();
98    let u = Tensor::<B, 2>::random(shape, Distribution::Uniform(0.0, 1.0), device);
99    let keep_a = u.lower_elem(p);
100    parent_a.mask_where(keep_a.bool_not(), parent_b)
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use burn::backend::{ndarray::NdArrayDevice, NdArray};
107    use burn::tensor::TensorData;
108    #[allow(unused_imports)]
109    use burn::tensor::backend::Backend as _;
110    type TestBackend = NdArray;
111
112    #[test]
113    fn blx_alpha_lies_between_bounds() {
114        let device: NdArrayDevice = Default::default();
115        TestBackend::seed(&device, 13);
116        let a = Tensor::<TestBackend, 2>::from_data(
117            TensorData::new(vec![0.0_f32, 0.0, 0.0, 0.0], [2, 2]),
118            &device,
119        );
120        let b = Tensor::<TestBackend, 2>::from_data(
121            TensorData::new(vec![1.0_f32, 1.0, 1.0, 1.0], [2, 2]),
122            &device,
123        );
124        let c = blx_alpha(a, b, 0.0, &device);
125        let values = c.into_data().into_vec::<f32>().unwrap();
126        // α = 0: children lie strictly in [0, 1].
127        for v in values {
128            assert!((0.0..=1.0).contains(&v), "value out of bounds: {v}");
129        }
130    }
131
132    #[test]
133    fn uniform_all_from_a_when_p_is_one() {
134        let device: NdArrayDevice = Default::default();
135        TestBackend::seed(&device, 5);
136        let a = Tensor::<TestBackend, 2>::from_data(
137            TensorData::new(vec![7.0_f32, 7.0, 7.0, 7.0], [2, 2]),
138            &device,
139        );
140        let b = Tensor::<TestBackend, 2>::from_data(
141            TensorData::new(vec![-7.0_f32, -7.0, -7.0, -7.0], [2, 2]),
142            &device,
143        );
144        let c = uniform_crossover(a, b, 1.0, &device);
145        let values = c.into_data().into_vec::<f32>().unwrap();
146        for v in values {
147            approx::assert_relative_eq!(v, 7.0, epsilon = 1e-6);
148        }
149    }
150
151    #[test]
152    fn uniform_all_from_b_when_p_is_zero() {
153        let device: NdArrayDevice = Default::default();
154        TestBackend::seed(&device, 5);
155        let a = Tensor::<TestBackend, 2>::from_data(
156            TensorData::new(vec![7.0_f32, 7.0, 7.0, 7.0], [2, 2]),
157            &device,
158        );
159        let b = Tensor::<TestBackend, 2>::from_data(
160            TensorData::new(vec![-7.0_f32, -7.0, -7.0, -7.0], [2, 2]),
161            &device,
162        );
163        let c = uniform_crossover(a, b, 0.0, &device);
164        let values = c.into_data().into_vec::<f32>().unwrap();
165        for v in values {
166            approx::assert_relative_eq!(v, -7.0, epsilon = 1e-6);
167        }
168    }
169}