Skip to main content

radiate_core/
diversity.rs

1use crate::{
2    Chromosome, Gene, Phenotype,
3    chromosomes::{NumericAllele, gene::NumericGene},
4    fitness::Novelty,
5    math::distance,
6};
7use std::sync::Arc;
8
9pub trait Distance<T>: Send + Sync {
10    fn calculate(&self, one: &T, two: &T) -> f32;
11}
12
13/// Trait for measuring diversity between two [Genotype]s.
14/// Within radiate this is mostly used for speciation and determining how genetically
15/// similar two individuals are. Through this, the engine can determine
16/// whether two individuals belong to the same [Species](super::genome::species::Species) or not.
17pub trait Diversity<C: Chromosome>: Send + Sync {
18    fn measure(&self, geno_one: &Phenotype<C>, geno_two: &Phenotype<C>) -> f32;
19}
20
21pub struct DistanceDiversityAdapter<C: Chromosome> {
22    diversity: Arc<dyn Diversity<C>>,
23}
24
25impl<C: Chromosome> DistanceDiversityAdapter<C> {
26    pub fn new(diversity: Arc<dyn Diversity<C>>) -> Self {
27        Self { diversity }
28    }
29}
30
31impl<C: Chromosome> Distance<Phenotype<C>> for DistanceDiversityAdapter<C> {
32    fn calculate(&self, one: &Phenotype<C>, two: &Phenotype<C>) -> f32 {
33        self.diversity.measure(one, two)
34    }
35}
36
37/// A concrete implementation of the [Diversity] trait that calculates the Hamming distance
38/// between two [Genotype]s. The Hamming distance is the number of positions at which the
39/// corresponding genes are different normalized by the total number of genes.
40#[derive(Clone)]
41pub struct HammingDistance;
42
43impl<G, C> Diversity<C> for HammingDistance
44where
45    C: Chromosome<Gene = G>,
46    G: Gene,
47    G::Allele: PartialEq,
48{
49    #[inline]
50    fn measure(&self, geno_one: &Phenotype<C>, geno_two: &Phenotype<C>) -> f32 {
51        let geno_one = geno_one.genotype();
52        let geno_two = geno_two.genotype();
53
54        let mut distance = 0.0;
55        let mut total_genes = 0.0;
56        for (chrom_one, chrom_two) in geno_one.iter().zip(geno_two.iter()) {
57            for (gene_one, gene_two) in chrom_one.iter().zip(chrom_two.iter()) {
58                total_genes += 1.0;
59                if gene_one.allele() != gene_two.allele() {
60                    distance += 1.0;
61                }
62            }
63        }
64
65        distance / total_genes
66    }
67}
68
69impl<P: AsRef<[f32]>> Distance<P> for HammingDistance {
70    fn calculate(&self, one: &P, two: &P) -> f32 {
71        let vec_one = one.as_ref();
72        let vec_two = two.as_ref();
73
74        distance::hamming(vec_one, vec_two)
75    }
76}
77
78impl Novelty<Vec<f32>> for HammingDistance {
79    fn description(&self, phenotype: &Vec<f32>) -> Vec<f32> {
80        phenotype.clone()
81    }
82}
83
84/// Implementation of the [Diversity] trait that calculates the Euclidean distance
85/// between two [Genotype]s. The Euclidean distance is the square root of the sum of the
86/// squared differences between the corresponding genes' alleles, normalized by the number of genes.
87#[derive(Clone)]
88pub struct EuclideanDistance;
89
90impl<G, C> Diversity<C> for EuclideanDistance
91where
92    C: Chromosome<Gene = G>,
93    G: NumericGene,
94    G::Allele: NumericAllele,
95{
96    #[inline]
97    fn measure(&self, geno_one: &Phenotype<C>, geno_two: &Phenotype<C>) -> f32 {
98        let geno_one = geno_one.genotype();
99        let geno_two = geno_two.genotype();
100
101        let mut distance = 0.0;
102        let mut total_genes = 0.0;
103        for (chrom_one, chrom_two) in geno_one.iter().zip(geno_two.iter()) {
104            for (gene_one, gene_two) in chrom_one.iter().zip(chrom_two.iter()) {
105                let one_as_f64 = gene_one.allele().extract::<f64>();
106                let two_as_f64 = gene_two.allele().extract::<f64>();
107
108                if let Some((one, two)) = one_as_f64.zip(two_as_f64) {
109                    if one.is_nan() || two.is_nan() {
110                        continue;
111                    }
112
113                    let diff = one - two;
114                    distance += diff * diff;
115                    total_genes += 1.0;
116                }
117            }
118        }
119
120        if total_genes == 0.0 {
121            return 0.0;
122        }
123
124        (distance / total_genes).sqrt() as f32
125    }
126}
127
128impl<P: AsRef<[f32]>> Distance<P> for EuclideanDistance {
129    fn calculate(&self, one: &P, two: &P) -> f32 {
130        let vec_one = one.as_ref();
131        let vec_two = two.as_ref();
132
133        distance::euclidean(vec_one, vec_two)
134    }
135}
136
137impl Novelty<Vec<f32>> for EuclideanDistance {
138    fn description(&self, phenotype: &Vec<f32>) -> Vec<f32> {
139        phenotype.clone()
140    }
141}
142
143#[derive(Clone)]
144pub struct CosineDistance;
145
146impl<G, C> Diversity<C> for CosineDistance
147where
148    C: Chromosome<Gene = G>,
149    G: NumericGene,
150    G::Allele: NumericAllele,
151{
152    #[inline]
153    fn measure(&self, geno_one: &Phenotype<C>, geno_two: &Phenotype<C>) -> f32 {
154        let geno_one = geno_one.genotype();
155        let geno_two = geno_two.genotype();
156
157        let mut dot_product = 0.0;
158        let mut norm_one = 0.0;
159        let mut norm_two = 0.0;
160
161        for (chrom_one, chrom_two) in geno_one.iter().zip(geno_two.iter()) {
162            for (gene_one, gene_two) in chrom_one.iter().zip(chrom_two.iter()) {
163                let one_as_f64 = gene_one.allele().extract::<f64>();
164                let two_as_f64 = gene_two.allele().extract::<f64>();
165
166                if let Some((one, two)) = one_as_f64.zip(two_as_f64) {
167                    if one.is_nan() || two.is_nan() {
168                        continue;
169                    }
170
171                    dot_product += one * two;
172                    norm_one += one * one;
173                    norm_two += two * two;
174                }
175            }
176        }
177
178        if norm_one == 0.0 || norm_two == 0.0 {
179            return 1.0;
180        }
181
182        1.0 - (dot_product / (norm_one.sqrt() * norm_two.sqrt())) as f32
183    }
184}
185
186impl<P: AsRef<[f32]>> Distance<P> for CosineDistance {
187    fn calculate(&self, one: &P, two: &P) -> f32 {
188        let vec_one = one.as_ref();
189        let vec_two = two.as_ref();
190
191        distance::cosine(vec_one, vec_two)
192    }
193}
194
195impl Novelty<Vec<f32>> for CosineDistance {
196    fn description(&self, phenotype: &Vec<f32>) -> Vec<f32> {
197        phenotype.clone()
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_hamming_distance() {
207        let distance = HammingDistance;
208        let vec_one = vec![1.0, 2.0, 3.0];
209        let vec_two = vec![1.0, 2.0, 4.0];
210
211        assert_eq!(distance.calculate(&vec_one, &vec_two), 1.0 / 3.0);
212    }
213
214    #[test]
215    fn test_euclidean_distance() {
216        let distance = EuclideanDistance;
217        let vec_one = vec![1.0, 2.0, 3.0];
218        let vec_two = vec![1.0, 2.0, 4.0];
219
220        assert_eq!(distance.calculate(&vec_one, &vec_two), 1.0);
221    }
222
223    #[test]
224    fn test_cosine_distance() {
225        let distance = CosineDistance;
226        let vec_one = vec![1.0, 2.0, 3.0];
227        let vec_two = vec![1.0, 2.0, 4.0];
228
229        assert_eq!(distance.calculate(&vec_one, &vec_two), 0.008539915);
230    }
231}