Skip to main content

fugue_evo/genome/
real_vector.rs

1//! Real-valued vector genome
2//!
3//! This module provides a fixed-length real-valued vector genome type
4//! with Fugue trace integration for probabilistic operations.
5
6#[cfg(feature = "ppl")]
7use fugue::{addr, ChoiceValue, Trace};
8use rand::Rng;
9use serde::{Deserialize, Serialize};
10
11use crate::error::GenomeError;
12use crate::genome::bounds::MultiBounds;
13use crate::genome::traits::{EvolutionaryGenome, RealValuedGenome};
14
15/// Fixed-length real-valued vector genome
16///
17/// This genome type represents continuous optimization problems where
18/// solutions are vectors of real numbers.
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
20pub struct RealVector {
21    /// The genes (values) of this genome
22    genes: Vec<f64>,
23}
24
25impl RealVector {
26    /// Create a new real vector with the given genes
27    pub fn new(genes: Vec<f64>) -> Self {
28        Self { genes }
29    }
30
31    /// Create a zero-filled vector of the given dimension
32    pub fn zeros(dimension: usize) -> Self {
33        Self {
34            genes: vec![0.0; dimension],
35        }
36    }
37
38    /// Create a vector filled with a constant value
39    pub fn filled(dimension: usize, value: f64) -> Self {
40        Self {
41            genes: vec![value; dimension],
42        }
43    }
44
45    /// Create from an iterator
46    pub fn collect_from<I: IntoIterator<Item = f64>>(iter: I) -> Self {
47        Self {
48            genes: iter.into_iter().collect(),
49        }
50    }
51
52    /// Get the underlying vector
53    pub fn into_inner(self) -> Vec<f64> {
54        self.genes
55    }
56
57    /// Get a reference to the genes
58    pub fn as_vec(&self) -> &Vec<f64> {
59        &self.genes
60    }
61
62    /// Calculate Euclidean norm
63    pub fn norm(&self) -> f64 {
64        self.genes.iter().map(|x| x * x).sum::<f64>().sqrt()
65    }
66
67    /// Calculate squared Euclidean norm
68    pub fn norm_squared(&self) -> f64 {
69        self.genes.iter().map(|x| x * x).sum::<f64>()
70    }
71
72    /// Element-wise addition
73    pub fn add(&self, other: &Self) -> Result<Self, GenomeError> {
74        if self.genes.len() != other.genes.len() {
75            return Err(GenomeError::DimensionMismatch {
76                expected: self.genes.len(),
77                actual: other.genes.len(),
78            });
79        }
80        Ok(Self {
81            genes: self
82                .genes
83                .iter()
84                .zip(other.genes.iter())
85                .map(|(a, b)| a + b)
86                .collect(),
87        })
88    }
89
90    /// Element-wise subtraction
91    pub fn sub(&self, other: &Self) -> Result<Self, GenomeError> {
92        if self.genes.len() != other.genes.len() {
93            return Err(GenomeError::DimensionMismatch {
94                expected: self.genes.len(),
95                actual: other.genes.len(),
96            });
97        }
98        Ok(Self {
99            genes: self
100                .genes
101                .iter()
102                .zip(other.genes.iter())
103                .map(|(a, b)| a - b)
104                .collect(),
105        })
106    }
107
108    /// Scalar multiplication
109    pub fn scale(&self, scalar: f64) -> Self {
110        Self {
111            genes: self.genes.iter().map(|x| x * scalar).collect(),
112        }
113    }
114}
115
116impl EvolutionaryGenome for RealVector {
117    type Allele = f64;
118    type Phenotype = Vec<f64>;
119
120    fn decode(&self) -> Self::Phenotype {
121        self.genes.clone()
122    }
123
124    fn dimension(&self) -> usize {
125        self.genes.len()
126    }
127
128    fn generate<R: Rng>(rng: &mut R, bounds: &MultiBounds) -> Self {
129        let genes = bounds
130            .bounds
131            .iter()
132            .map(|b| rng.gen_range(b.min..=b.max))
133            .collect();
134        Self { genes }
135    }
136
137    fn as_slice(&self) -> Option<&[f64]> {
138        Some(&self.genes)
139    }
140
141    fn as_mut_slice(&mut self) -> Option<&mut [f64]> {
142        Some(&mut self.genes)
143    }
144
145    fn distance(&self, other: &Self) -> f64 {
146        self.try_distance(other).unwrap_or_else(|e| {
147            panic!("RealVector::distance: {e}; use try_distance for a fallible comparison")
148        })
149    }
150
151    fn try_distance(&self, other: &Self) -> Result<f64, GenomeError> {
152        if self.genes.len() != other.genes.len() {
153            return Err(GenomeError::DimensionMismatch {
154                expected: self.genes.len(),
155                actual: other.genes.len(),
156            });
157        }
158        Ok(self
159            .genes
160            .iter()
161            .zip(other.genes.iter())
162            .map(|(a, b)| (a - b).powi(2))
163            .sum::<f64>()
164            .sqrt())
165    }
166}
167
168#[cfg(feature = "ppl")]
169impl crate::genome::trace_genome::TraceGenome for RealVector {
170    /// Convert RealVector to Fugue trace.
171    ///
172    /// Each gene is stored at address "gene#i" where i is the index.
173    fn to_trace(&self) -> Trace {
174        let mut trace = Trace::default();
175        for (i, &gene) in self.genes.iter().enumerate() {
176            trace.insert_choice(addr!("gene", i), ChoiceValue::F64(gene), 0.0);
177        }
178        trace
179    }
180
181    /// Reconstruct RealVector from Fugue trace.
182    ///
183    /// Reads genes from addresses "gene#0", "gene#1", ... until no more are
184    /// found. A *missing* address terminates the scan (normal end of the
185    /// sequence), but an address that is *present with the wrong value type*
186    /// is a corrupt trace and yields [`GenomeError::TypeMismatch`] rather than
187    /// silently truncating the genome.
188    fn from_trace(trace: &Trace) -> Result<Self, GenomeError> {
189        let mut genes = Vec::new();
190        let mut i = 0;
191        loop {
192            match trace.choices.get(&addr!("gene", i)) {
193                // Address absent: end of the gene sequence.
194                None => break,
195                Some(choice) => match choice.value.as_f64() {
196                    Some(val) => {
197                        genes.push(val);
198                        i += 1;
199                    }
200                    // Address present but not an f64: corrupt trace.
201                    None => {
202                        return Err(GenomeError::TypeMismatch {
203                            address: format!("gene#{i}"),
204                            expected: "f64".to_string(),
205                            actual: choice.value.type_name().to_string(),
206                        });
207                    }
208                },
209            }
210        }
211        if genes.is_empty() {
212            return Err(GenomeError::InvalidStructure(
213                "No genes found in trace".to_string(),
214            ));
215        }
216        Ok(Self { genes })
217    }
218}
219
220impl RealValuedGenome for RealVector {
221    fn genes(&self) -> &[f64] {
222        &self.genes
223    }
224
225    fn genes_mut(&mut self) -> &mut [f64] {
226        &mut self.genes
227    }
228
229    fn from_genes(genes: Vec<f64>) -> Result<Self, GenomeError> {
230        Ok(Self { genes })
231    }
232}
233
234impl std::ops::Index<usize> for RealVector {
235    type Output = f64;
236
237    fn index(&self, index: usize) -> &Self::Output {
238        &self.genes[index]
239    }
240}
241
242impl std::ops::IndexMut<usize> for RealVector {
243    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
244        &mut self.genes[index]
245    }
246}
247
248impl From<Vec<f64>> for RealVector {
249    fn from(genes: Vec<f64>) -> Self {
250        Self { genes }
251    }
252}
253
254impl From<RealVector> for Vec<f64> {
255    fn from(genome: RealVector) -> Self {
256        genome.genes
257    }
258}
259
260impl<const N: usize> From<[f64; N]> for RealVector {
261    fn from(arr: [f64; N]) -> Self {
262        Self {
263            genes: arr.to_vec(),
264        }
265    }
266}
267
268impl IntoIterator for RealVector {
269    type Item = f64;
270    type IntoIter = std::vec::IntoIter<f64>;
271
272    fn into_iter(self) -> Self::IntoIter {
273        self.genes.into_iter()
274    }
275}
276
277impl<'a> IntoIterator for &'a RealVector {
278    type Item = &'a f64;
279    type IntoIter = std::slice::Iter<'a, f64>;
280
281    fn into_iter(self) -> Self::IntoIter {
282        self.genes.iter()
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use approx::assert_relative_eq;
290    #[cfg(feature = "ppl")]
291    use fugue::addr;
292
293    #[test]
294    fn test_real_vector_new() {
295        let v = RealVector::new(vec![1.0, 2.0, 3.0]);
296        assert_eq!(v.dimension(), 3);
297        assert_eq!(v.genes(), &[1.0, 2.0, 3.0]);
298    }
299
300    #[test]
301    fn test_real_vector_zeros() {
302        let v = RealVector::zeros(5);
303        assert_eq!(v.dimension(), 5);
304        assert!(v.genes().iter().all(|&x| x == 0.0));
305    }
306
307    #[test]
308    fn test_real_vector_filled() {
309        let v = RealVector::filled(3, 42.0);
310        assert_eq!(v.genes(), &[42.0, 42.0, 42.0]);
311    }
312
313    #[test]
314    fn test_real_vector_from_array() {
315        let v: RealVector = [1.0, 2.0, 3.0].into();
316        assert_eq!(v.genes(), &[1.0, 2.0, 3.0]);
317    }
318
319    #[test]
320    fn test_real_vector_decode() {
321        let v = RealVector::new(vec![1.0, 2.0, 3.0]);
322        let phenotype = v.decode();
323        assert_eq!(phenotype, vec![1.0, 2.0, 3.0]);
324    }
325
326    #[test]
327    fn test_real_vector_generate() {
328        let mut rng = rand::thread_rng();
329        let bounds = MultiBounds::symmetric(5.0, 10);
330        let v = RealVector::generate(&mut rng, &bounds);
331
332        assert_eq!(v.dimension(), 10);
333        for gene in v.genes() {
334            assert!(*gene >= -5.0 && *gene <= 5.0);
335        }
336    }
337
338    #[test]
339    fn test_real_vector_norm() {
340        let v = RealVector::new(vec![3.0, 4.0]);
341        assert_relative_eq!(v.norm(), 5.0);
342        assert_relative_eq!(v.norm_squared(), 25.0);
343    }
344
345    #[test]
346    fn test_real_vector_distance() {
347        let v1 = RealVector::new(vec![0.0, 0.0]);
348        let v2 = RealVector::new(vec![3.0, 4.0]);
349        assert_relative_eq!(v1.distance(&v2), 5.0);
350    }
351
352    #[test]
353    fn test_real_vector_add() {
354        let v1 = RealVector::new(vec![1.0, 2.0, 3.0]);
355        let v2 = RealVector::new(vec![4.0, 5.0, 6.0]);
356        let sum = v1.add(&v2).unwrap();
357        assert_eq!(sum.genes(), &[5.0, 7.0, 9.0]);
358    }
359
360    #[test]
361    fn test_real_vector_add_dimension_mismatch() {
362        let v1 = RealVector::new(vec![1.0, 2.0]);
363        let v2 = RealVector::new(vec![1.0, 2.0, 3.0]);
364        let result = v1.add(&v2);
365        assert!(result.is_err());
366        assert!(matches!(
367            result.unwrap_err(),
368            GenomeError::DimensionMismatch {
369                expected: 2,
370                actual: 3
371            }
372        ));
373    }
374
375    #[test]
376    fn test_real_vector_sub() {
377        let v1 = RealVector::new(vec![5.0, 7.0, 9.0]);
378        let v2 = RealVector::new(vec![1.0, 2.0, 3.0]);
379        let diff = v1.sub(&v2).unwrap();
380        assert_eq!(diff.genes(), &[4.0, 5.0, 6.0]);
381    }
382
383    #[test]
384    fn test_real_vector_scale() {
385        let v = RealVector::new(vec![1.0, 2.0, 3.0]);
386        let scaled = v.scale(2.0);
387        assert_eq!(scaled.genes(), &[2.0, 4.0, 6.0]);
388    }
389
390    #[test]
391    fn test_real_vector_indexing() {
392        let mut v = RealVector::new(vec![1.0, 2.0, 3.0]);
393        assert_eq!(v[0], 1.0);
394        assert_eq!(v[1], 2.0);
395        assert_eq!(v[2], 3.0);
396
397        v[1] = 42.0;
398        assert_eq!(v[1], 42.0);
399    }
400
401    #[test]
402    fn test_real_vector_apply_bounds() {
403        let mut v = RealVector::new(vec![-10.0, 0.0, 10.0]);
404        let bounds = MultiBounds::symmetric(5.0, 3);
405        v.apply_bounds(&bounds);
406        assert_eq!(v.genes(), &[-5.0, 0.0, 5.0]);
407    }
408
409    #[test]
410    fn test_real_vector_iteration() {
411        let v = RealVector::new(vec![1.0, 2.0, 3.0]);
412        let sum: f64 = v.into_iter().sum();
413        assert_relative_eq!(sum, 6.0);
414    }
415
416    #[test]
417    fn test_real_vector_into_inner() {
418        let v = RealVector::new(vec![1.0, 2.0, 3.0]);
419        let inner: Vec<f64> = v.into_inner();
420        assert_eq!(inner, vec![1.0, 2.0, 3.0]);
421    }
422
423    #[test]
424    fn test_real_vector_serialization() {
425        let v = RealVector::new(vec![1.0, 2.0, 3.0]);
426        let serialized = serde_json::to_string(&v).unwrap();
427        let deserialized: RealVector = serde_json::from_str(&serialized).unwrap();
428        assert_eq!(v, deserialized);
429    }
430
431    #[test]
432    #[cfg(feature = "ppl")]
433    fn test_real_vector_to_trace() {
434        use crate::genome::trace_genome::TraceGenome;
435        let v = RealVector::new(vec![1.5, 2.5, 3.5]);
436        let trace = v.to_trace();
437
438        assert_eq!(trace.get_f64(&addr!("gene", 0)), Some(1.5));
439        assert_eq!(trace.get_f64(&addr!("gene", 1)), Some(2.5));
440        assert_eq!(trace.get_f64(&addr!("gene", 2)), Some(3.5));
441        assert_eq!(trace.get_f64(&addr!("gene", 3)), None);
442    }
443
444    #[test]
445    #[cfg(feature = "ppl")]
446    fn test_real_vector_from_trace() {
447        use crate::genome::trace_genome::TraceGenome;
448        let mut trace = Trace::default();
449        trace.insert_choice(addr!("gene", 0), ChoiceValue::F64(1.0), 0.0);
450        trace.insert_choice(addr!("gene", 1), ChoiceValue::F64(2.0), 0.0);
451        trace.insert_choice(addr!("gene", 2), ChoiceValue::F64(3.0), 0.0);
452
453        let v = RealVector::from_trace(&trace).unwrap();
454        assert_eq!(v.genes(), &[1.0, 2.0, 3.0]);
455    }
456
457    #[test]
458    #[cfg(feature = "ppl")]
459    fn test_real_vector_trace_roundtrip() {
460        use crate::genome::trace_genome::TraceGenome;
461        let original = RealVector::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
462        let trace = original.to_trace();
463        let recovered = RealVector::from_trace(&trace).unwrap();
464        assert_eq!(original, recovered);
465    }
466
467    #[test]
468    #[cfg(feature = "ppl")]
469    fn test_real_vector_from_trace_empty() {
470        use crate::genome::trace_genome::TraceGenome;
471        let trace = Trace::default();
472        let result = RealVector::from_trace(&trace);
473        assert!(result.is_err());
474    }
475
476    #[test]
477    fn test_real_vector_try_distance_dimension_mismatch() {
478        // regression: EV-20 — distance previously truncated via zip and reported
479        // 0.0 for vectors of different length, hiding an enormous extra component.
480        let v1 = RealVector::new(vec![0.0, 0.0]);
481        let v2 = RealVector::new(vec![0.0, 0.0, 1_000_000.0]);
482        let result = v1.try_distance(&v2);
483        assert!(matches!(
484            result,
485            Err(GenomeError::DimensionMismatch {
486                expected: 2,
487                actual: 3
488            })
489        ));
490    }
491
492    #[test]
493    #[should_panic(expected = "Dimension mismatch")]
494    fn test_real_vector_distance_dimension_mismatch_panics() {
495        // regression: EV-20 — the infallible distance() must loudly reject a
496        // length mismatch instead of silently returning 0.0.
497        let v1 = RealVector::new(vec![0.0, 0.0]);
498        let v2 = RealVector::new(vec![0.0, 0.0, 1_000_000.0]);
499        let _ = v1.distance(&v2);
500    }
501
502    #[test]
503    #[cfg(feature = "ppl")]
504    fn test_real_vector_from_trace_type_mismatch() {
505        // regression: EV-59 — a present-but-wrong-typed choice mid-sequence must
506        // raise TypeMismatch, not be treated as "end of genes" and truncate.
507        use crate::genome::trace_genome::TraceGenome;
508        let mut trace = Trace::default();
509        trace.insert_choice(addr!("gene", 0), ChoiceValue::F64(1.0), 0.0);
510        trace.insert_choice(addr!("gene", 1), ChoiceValue::Bool(true), 0.0); // wrong type
511        trace.insert_choice(addr!("gene", 2), ChoiceValue::F64(3.0), 0.0);
512
513        let result = RealVector::from_trace(&trace);
514        match result {
515            Err(GenomeError::TypeMismatch {
516                address,
517                expected,
518                actual,
519            }) => {
520                assert_eq!(address, "gene#1");
521                assert_eq!(expected, "f64");
522                assert_eq!(actual, "bool");
523            }
524            other => panic!("expected TypeMismatch, got {other:?}"),
525        }
526    }
527
528    #[test]
529    #[cfg(feature = "ppl")]
530    fn test_real_vector_from_trace_missing_stops_cleanly() {
531        // A genuinely absent address terminates the scan (not an error).
532        use crate::genome::trace_genome::TraceGenome;
533        let mut trace = Trace::default();
534        trace.insert_choice(addr!("gene", 0), ChoiceValue::F64(1.0), 0.0);
535        trace.insert_choice(addr!("gene", 1), ChoiceValue::F64(2.0), 0.0);
536        let v = RealVector::from_trace(&trace).unwrap();
537        assert_eq!(v.genes(), &[1.0, 2.0]);
538    }
539}