Skip to main content

fugue_evo/genome/
dynamic_real_vector.rs

1//! Dynamic (variable-length) real-valued vector genome
2//!
3//! This module provides a variable-length real-valued vector genome type
4//! for problems where the solution dimension is not fixed.
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/// Variable-length real-valued vector genome
16///
17/// This genome type represents optimization problems where the solution
18/// dimension can vary. Useful for problems like:
19/// - Neural network architecture search (variable hidden layer sizes)
20/// - Variable-length feature selection
21/// - Adaptive-dimensional optimization
22#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
23pub struct DynamicRealVector {
24    /// The genes (values) of this genome
25    genes: Vec<f64>,
26    /// Minimum allowed length
27    min_length: usize,
28    /// Maximum allowed length
29    max_length: usize,
30}
31
32impl DynamicRealVector {
33    /// Create a new dynamic real vector with the given genes and length constraints
34    pub fn new(genes: Vec<f64>, min_length: usize, max_length: usize) -> Result<Self, GenomeError> {
35        if genes.len() < min_length || genes.len() > max_length {
36            return Err(GenomeError::InvalidStructure(format!(
37                "Gene length {} outside bounds [{}, {}]",
38                genes.len(),
39                min_length,
40                max_length
41            )));
42        }
43        if min_length > max_length {
44            return Err(GenomeError::InvalidStructure(format!(
45                "min_length ({}) > max_length ({})",
46                min_length, max_length
47            )));
48        }
49        Ok(Self {
50            genes,
51            min_length,
52            max_length,
53        })
54    }
55
56    /// Create with default length constraints (1 to usize::MAX)
57    pub fn with_defaults(genes: Vec<f64>) -> Self {
58        Self {
59            genes,
60            min_length: 1,
61            max_length: usize::MAX,
62        }
63    }
64
65    /// Create a zero-filled vector of the given dimension
66    pub fn zeros(
67        dimension: usize,
68        min_length: usize,
69        max_length: usize,
70    ) -> Result<Self, GenomeError> {
71        Self::new(vec![0.0; dimension], min_length, max_length)
72    }
73
74    /// Get the minimum allowed length
75    pub fn min_length(&self) -> usize {
76        self.min_length
77    }
78
79    /// Get the maximum allowed length
80    pub fn max_length(&self) -> usize {
81        self.max_length
82    }
83
84    /// Get the underlying vector
85    pub fn into_inner(self) -> Vec<f64> {
86        self.genes
87    }
88
89    /// Get a reference to the genes
90    pub fn as_vec(&self) -> &Vec<f64> {
91        &self.genes
92    }
93
94    /// Calculate Euclidean norm
95    pub fn norm(&self) -> f64 {
96        self.genes.iter().map(|x| x * x).sum::<f64>().sqrt()
97    }
98
99    /// Calculate squared Euclidean norm
100    pub fn norm_squared(&self) -> f64 {
101        self.genes.iter().map(|x| x * x).sum::<f64>()
102    }
103
104    /// Add a gene to the end (if within bounds)
105    pub fn push(&mut self, gene: f64) -> Result<(), GenomeError> {
106        if self.genes.len() >= self.max_length {
107            return Err(GenomeError::ConstraintViolation(format!(
108                "Cannot add gene: would exceed max_length {}",
109                self.max_length
110            )));
111        }
112        self.genes.push(gene);
113        Ok(())
114    }
115
116    /// Remove the last gene (if within bounds)
117    pub fn pop(&mut self) -> Result<f64, GenomeError> {
118        if self.genes.len() <= self.min_length {
119            return Err(GenomeError::ConstraintViolation(format!(
120                "Cannot remove gene: would go below min_length {}",
121                self.min_length
122            )));
123        }
124        Ok(self.genes.pop().unwrap())
125    }
126
127    /// Insert a gene at a specific position
128    pub fn insert(&mut self, index: usize, gene: f64) -> Result<(), GenomeError> {
129        if self.genes.len() >= self.max_length {
130            return Err(GenomeError::ConstraintViolation(format!(
131                "Cannot insert gene: would exceed max_length {}",
132                self.max_length
133            )));
134        }
135        if index > self.genes.len() {
136            return Err(GenomeError::InvalidStructure(format!(
137                "Insert index {} out of bounds for length {}",
138                index,
139                self.genes.len()
140            )));
141        }
142        self.genes.insert(index, gene);
143        Ok(())
144    }
145
146    /// Remove a gene at a specific position
147    pub fn remove(&mut self, index: usize) -> Result<f64, GenomeError> {
148        if self.genes.len() <= self.min_length {
149            return Err(GenomeError::ConstraintViolation(format!(
150                "Cannot remove gene: would go below min_length {}",
151                self.min_length
152            )));
153        }
154        if index >= self.genes.len() {
155            return Err(GenomeError::InvalidStructure(format!(
156                "Remove index {} out of bounds for length {}",
157                index,
158                self.genes.len()
159            )));
160        }
161        Ok(self.genes.remove(index))
162    }
163
164    /// Check if a gene can be added
165    pub fn can_grow(&self) -> bool {
166        self.genes.len() < self.max_length
167    }
168
169    /// Check if a gene can be removed
170    pub fn can_shrink(&self) -> bool {
171        self.genes.len() > self.min_length
172    }
173
174    /// Element-wise addition (requires same length)
175    pub fn add(&self, other: &Self) -> Result<Self, GenomeError> {
176        if self.genes.len() != other.genes.len() {
177            return Err(GenomeError::DimensionMismatch {
178                expected: self.genes.len(),
179                actual: other.genes.len(),
180            });
181        }
182        Self::new(
183            self.genes
184                .iter()
185                .zip(other.genes.iter())
186                .map(|(a, b)| a + b)
187                .collect(),
188            self.min_length,
189            self.max_length,
190        )
191    }
192
193    /// Element-wise subtraction (requires same length)
194    pub fn sub(&self, other: &Self) -> Result<Self, GenomeError> {
195        if self.genes.len() != other.genes.len() {
196            return Err(GenomeError::DimensionMismatch {
197                expected: self.genes.len(),
198                actual: other.genes.len(),
199            });
200        }
201        Self::new(
202            self.genes
203                .iter()
204                .zip(other.genes.iter())
205                .map(|(a, b)| a - b)
206                .collect(),
207            self.min_length,
208            self.max_length,
209        )
210    }
211
212    /// Scalar multiplication
213    pub fn scale(&self, scalar: f64) -> Self {
214        Self {
215            genes: self.genes.iter().map(|x| x * scalar).collect(),
216            min_length: self.min_length,
217            max_length: self.max_length,
218        }
219    }
220
221    /// Fallibly generate a random variable-length vector from `bounds`.
222    ///
223    /// `bounds.dimension()` is interpreted as the maximum length (minimum length 1)
224    /// and the per-dimension `[min, max]` intervals bound the sampled gene
225    /// values. Returns `Err(GenomeError::InvalidStructure)` for empty
226    /// (0-dimension) `bounds`, which cannot yield a valid non-empty genome —
227    /// this replaces the previous panic on the inverted range `1..=0`.
228    pub fn try_generate<R: Rng>(rng: &mut R, bounds: &MultiBounds) -> Result<Self, GenomeError> {
229        let max_len = bounds.dimension();
230        if max_len == 0 {
231            return Err(GenomeError::InvalidStructure(
232                "Cannot generate a DynamicRealVector from empty (0-dimension) bounds".to_string(),
233            ));
234        }
235        let min_len = 1;
236        let length = if min_len == max_len {
237            min_len
238        } else {
239            rng.gen_range(min_len..=max_len)
240        };
241
242        let genes: Vec<f64> = (0..length)
243            .map(|i| {
244                // `length <= max_len == dimension()`, so `get(i)` is always Some;
245                // fall back to the first bound defensively.
246                let b = bounds
247                    .get(i)
248                    .or_else(|| bounds.get(0))
249                    .expect("bounds is non-empty");
250                rng.gen_range(b.min..=b.max)
251            })
252            .collect();
253
254        Ok(Self {
255            genes,
256            min_length: min_len,
257            max_length: max_len,
258        })
259    }
260
261    /// Generate a random variable-length vector with explicit length bounds.
262    ///
263    /// This is the honest constructor for random generation: the length is
264    /// sampled uniformly from `[min_length, max_length]` and each gene is
265    /// sampled from the corresponding entry of `value_bounds` (falling back to
266    /// the first entry when the sampled length exceeds the number of provided
267    /// bounds). Unlike
268    /// [`EvolutionaryGenome::generate`],
269    /// it does not overload a `MultiBounds`' dimension as a length.
270    pub fn generate_with_len<R: Rng>(
271        rng: &mut R,
272        min_length: usize,
273        max_length: usize,
274        value_bounds: &MultiBounds,
275    ) -> Result<Self, GenomeError> {
276        if min_length > max_length {
277            return Err(GenomeError::InvalidStructure(format!(
278                "min_length ({min_length}) > max_length ({max_length})"
279            )));
280        }
281        if value_bounds.dimension() == 0 {
282            return Err(GenomeError::InvalidStructure(
283                "value_bounds must have at least one dimension".to_string(),
284            ));
285        }
286        let length = if min_length == max_length {
287            min_length
288        } else {
289            rng.gen_range(min_length..=max_length)
290        };
291        let genes: Vec<f64> = (0..length)
292            .map(|i| {
293                let b = value_bounds
294                    .get(i)
295                    .or_else(|| value_bounds.get(0))
296                    .expect("value_bounds is non-empty");
297                rng.gen_range(b.min..=b.max)
298            })
299            .collect();
300        Self::new(genes, min_length, max_length)
301    }
302}
303
304impl EvolutionaryGenome for DynamicRealVector {
305    type Allele = f64;
306    type Phenotype = Vec<f64>;
307
308    fn decode(&self) -> Self::Phenotype {
309        self.genes.clone()
310    }
311
312    fn dimension(&self) -> usize {
313        self.genes.len()
314    }
315
316    /// Generate a random variable-length vector.
317    ///
318    /// `bounds.dimension()` is interpreted as the maximum length (minimum length
319    /// 1); the per-dimension `[min, max]` intervals bound the sampled gene
320    /// values. For empty (0-dimension) `bounds` there is nothing to sample, so
321    /// this degrades gracefully to an empty genome instead of panicking on an
322    /// inverted `gen_range(1..=0)`. Use [`try_generate`](Self::try_generate) if
323    /// you want that degenerate case reported as an error, or
324    /// [`generate_with_len`](Self::generate_with_len) for explicit length bounds.
325    fn generate<R: Rng>(rng: &mut R, bounds: &MultiBounds) -> Self {
326        Self::try_generate(rng, bounds).unwrap_or_else(|_| Self {
327            genes: Vec::new(),
328            min_length: 0,
329            max_length: 0,
330        })
331    }
332
333    fn distance(&self, other: &Self) -> f64 {
334        // Euclidean distance for common genes, plus penalty for different
335        // lengths. Comparing genomes of different lengths is *meaningful* for a
336        // variable-length representation, so this never panics.
337        let common_len = self.genes.len().min(other.genes.len());
338        let mut dist_sq = 0.0;
339
340        for i in 0..common_len {
341            let diff = self.genes[i] - other.genes[i];
342            dist_sq += diff * diff;
343        }
344
345        // Add penalty for length difference
346        let length_penalty = (self.genes.len() as f64 - other.genes.len() as f64).abs();
347        dist_sq.sqrt() + length_penalty
348    }
349
350    fn try_distance(&self, other: &Self) -> Result<f64, GenomeError> {
351        // Length differences are expected and handled by a penalty, so distance
352        // is always well-defined; this never returns Err.
353        Ok(self.distance(other))
354    }
355}
356
357#[cfg(feature = "ppl")]
358impl crate::genome::trace_genome::TraceGenome for DynamicRealVector {
359    /// Convert DynamicRealVector to Fugue trace.
360    ///
361    /// Stores genes at `"<trace_prefix()>#i"` and metadata at "meta#min_length",
362    /// "meta#max_length" and "meta#length". The gene address prefix is taken
363    /// from [`trace_prefix`](crate::genome::trace_genome::TraceGenome::trace_prefix) so that the prefix advertised by the trait and
364    /// the prefix actually written can never diverge.
365    fn to_trace(&self) -> Trace {
366        let mut trace = Trace::default();
367        for (i, &gene) in self.genes.iter().enumerate() {
368            trace.insert_choice(addr!(Self::trace_prefix(), i), ChoiceValue::F64(gene), 0.0);
369        }
370        // Store length constraints in trace
371        trace.insert_choice(
372            addr!("meta", "min_length"),
373            ChoiceValue::I64(self.min_length as i64),
374            0.0,
375        );
376        trace.insert_choice(
377            addr!("meta", "max_length"),
378            ChoiceValue::I64(self.max_length as i64),
379            0.0,
380        );
381        trace.insert_choice(
382            addr!("meta", "length"),
383            ChoiceValue::I64(self.genes.len() as i64),
384            0.0,
385        );
386        trace
387    }
388
389    /// Reconstruct DynamicRealVector from Fugue trace.
390    fn from_trace(trace: &Trace) -> Result<Self, GenomeError> {
391        let min_length = trace
392            .get_i64(&addr!("meta", "min_length"))
393            .map(|v| v as usize)
394            .unwrap_or(1);
395        let max_length = trace
396            .get_i64(&addr!("meta", "max_length"))
397            .map(|v| v as usize)
398            .unwrap_or(usize::MAX);
399        let expected_length = trace.get_i64(&addr!("meta", "length")).map(|v| v as usize);
400
401        let mut genes = Vec::new();
402        let mut i = 0;
403        while let Some(val) = trace.get_f64(&addr!(Self::trace_prefix(), i)) {
404            genes.push(val);
405            i += 1;
406            // Stop if we've reached expected length (handles sparse traces)
407            if let Some(len) = expected_length {
408                if i >= len {
409                    break;
410                }
411            }
412        }
413
414        if genes.is_empty() {
415            return Err(GenomeError::InvalidStructure(
416                "No genes found in trace".to_string(),
417            ));
418        }
419
420        Self::new(genes, min_length, max_length)
421    }
422
423    fn trace_prefix() -> &'static str {
424        "dyn_gene"
425    }
426}
427
428impl RealValuedGenome for DynamicRealVector {
429    fn genes(&self) -> &[f64] {
430        &self.genes
431    }
432
433    fn genes_mut(&mut self) -> &mut [f64] {
434        &mut self.genes
435    }
436
437    fn from_genes(genes: Vec<f64>) -> Result<Self, GenomeError> {
438        if genes.is_empty() {
439            return Err(GenomeError::InvalidStructure(
440                "Cannot create DynamicRealVector with empty genes".to_string(),
441            ));
442        }
443        Ok(Self::with_defaults(genes))
444    }
445
446    fn apply_bounds(&mut self, bounds: &MultiBounds) {
447        for (i, gene) in self.genes.iter_mut().enumerate() {
448            if let Some(b) = bounds.get(i) {
449                *gene = gene.clamp(b.min, b.max);
450            }
451        }
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    #[test]
460    fn test_dynamic_real_vector_creation() {
461        let genes = vec![1.0, 2.0, 3.0];
462        let genome = DynamicRealVector::new(genes.clone(), 1, 10).unwrap();
463        assert_eq!(genome.genes(), &genes[..]);
464        assert_eq!(genome.dimension(), 3);
465    }
466
467    #[test]
468    fn test_dynamic_real_vector_length_constraints() {
469        // Too short
470        let result = DynamicRealVector::new(vec![1.0], 2, 10);
471        assert!(result.is_err());
472
473        // Too long
474        let result = DynamicRealVector::new(vec![1.0, 2.0, 3.0], 1, 2);
475        assert!(result.is_err());
476
477        // Invalid bounds
478        let result = DynamicRealVector::new(vec![1.0, 2.0], 5, 3);
479        assert!(result.is_err());
480    }
481
482    #[test]
483    fn test_push_pop() {
484        let mut genome = DynamicRealVector::new(vec![1.0, 2.0], 1, 5).unwrap();
485
486        // Push
487        genome.push(3.0).unwrap();
488        assert_eq!(genome.dimension(), 3);
489        assert_eq!(genome.genes()[2], 3.0);
490
491        // Pop
492        let val = genome.pop().unwrap();
493        assert_eq!(val, 3.0);
494        assert_eq!(genome.dimension(), 2);
495    }
496
497    #[test]
498    fn test_push_pop_bounds() {
499        let mut genome = DynamicRealVector::new(vec![1.0, 2.0, 3.0], 3, 3).unwrap();
500
501        // Cannot push (at max)
502        assert!(genome.push(4.0).is_err());
503
504        // Cannot pop (at min)
505        assert!(genome.pop().is_err());
506    }
507
508    #[test]
509    fn test_insert_remove() {
510        let mut genome = DynamicRealVector::new(vec![1.0, 3.0], 1, 5).unwrap();
511
512        // Insert in middle
513        genome.insert(1, 2.0).unwrap();
514        assert_eq!(genome.genes(), &[1.0, 2.0, 3.0]);
515
516        // Remove from middle
517        let val = genome.remove(1).unwrap();
518        assert_eq!(val, 2.0);
519        assert_eq!(genome.genes(), &[1.0, 3.0]);
520    }
521
522    #[test]
523    fn test_can_grow_shrink() {
524        let genome = DynamicRealVector::new(vec![1.0, 2.0], 1, 3).unwrap();
525        assert!(genome.can_grow());
526        assert!(genome.can_shrink());
527
528        let genome_at_max = DynamicRealVector::new(vec![1.0, 2.0, 3.0], 1, 3).unwrap();
529        assert!(!genome_at_max.can_grow());
530        assert!(genome_at_max.can_shrink());
531
532        let genome_at_min = DynamicRealVector::new(vec![1.0], 1, 3).unwrap();
533        assert!(genome_at_min.can_grow());
534        assert!(!genome_at_min.can_shrink());
535    }
536
537    #[test]
538    #[cfg(feature = "ppl")]
539    fn test_trace_roundtrip() {
540        use crate::genome::trace_genome::TraceGenome;
541        let genome = DynamicRealVector::new(vec![1.0, 2.0, 3.0], 2, 5).unwrap();
542        let trace = genome.to_trace();
543        let restored = DynamicRealVector::from_trace(&trace).unwrap();
544
545        assert_eq!(genome.genes(), restored.genes());
546        assert_eq!(genome.min_length(), restored.min_length());
547        assert_eq!(genome.max_length(), restored.max_length());
548    }
549
550    #[test]
551    fn test_distance() {
552        let g1 = DynamicRealVector::new(vec![0.0, 0.0], 1, 10).unwrap();
553        let g2 = DynamicRealVector::new(vec![3.0, 4.0], 1, 10).unwrap();
554
555        // Euclidean distance should be 5
556        let dist = g1.distance(&g2);
557        assert!((dist - 5.0).abs() < 0.001);
558
559        // Different lengths add penalty
560        let g3 = DynamicRealVector::new(vec![0.0, 0.0, 0.0], 1, 10).unwrap();
561        let dist_with_penalty = g1.distance(&g3);
562        assert!(dist_with_penalty > 0.0);
563    }
564
565    #[test]
566    fn test_generate_random() {
567        let bounds = MultiBounds::symmetric(5.0, 5);
568        let mut rng = rand::thread_rng();
569
570        let genome = DynamicRealVector::generate(&mut rng, &bounds);
571
572        assert!(genome.dimension() >= 1);
573        assert!(genome.dimension() <= 5);
574        for gene in genome.genes() {
575            assert!(*gene >= -5.0 && *gene <= 5.0);
576        }
577    }
578
579    #[test]
580    fn test_arithmetic_operations() {
581        let g1 = DynamicRealVector::new(vec![1.0, 2.0, 3.0], 1, 10).unwrap();
582        let g2 = DynamicRealVector::new(vec![4.0, 5.0, 6.0], 1, 10).unwrap();
583
584        let sum = g1.add(&g2).unwrap();
585        assert_eq!(sum.genes(), &[5.0, 7.0, 9.0]);
586
587        let diff = g2.sub(&g1).unwrap();
588        assert_eq!(diff.genes(), &[3.0, 3.0, 3.0]);
589
590        let scaled = g1.scale(2.0);
591        assert_eq!(scaled.genes(), &[2.0, 4.0, 6.0]);
592    }
593
594    #[test]
595    fn test_norm() {
596        let genome = DynamicRealVector::new(vec![3.0, 4.0], 1, 10).unwrap();
597        assert!((genome.norm() - 5.0).abs() < 0.001);
598        assert!((genome.norm_squared() - 25.0).abs() < 0.001);
599    }
600
601    #[test]
602    fn test_generate_empty_bounds_does_not_panic() {
603        // regression: EV-58 — generate previously called gen_range(1..=0) for
604        // 0-dimensional bounds, which panics ("cannot sample empty range").
605        let bounds = MultiBounds::new(vec![]);
606        let mut rng = rand::thread_rng();
607        let genome = DynamicRealVector::generate(&mut rng, &bounds);
608        assert_eq!(genome.dimension(), 0);
609    }
610
611    #[test]
612    fn test_try_generate_empty_bounds_errors() {
613        // regression: EV-58 — the fallible form reports the degenerate case.
614        let bounds = MultiBounds::new(vec![]);
615        let mut rng = rand::thread_rng();
616        assert!(DynamicRealVector::try_generate(&mut rng, &bounds).is_err());
617
618        // Non-empty bounds still succeed.
619        let ok_bounds = MultiBounds::symmetric(5.0, 4);
620        let g = DynamicRealVector::try_generate(&mut rng, &ok_bounds).unwrap();
621        assert!(g.dimension() >= 1 && g.dimension() <= 4);
622    }
623
624    #[test]
625    #[cfg(feature = "ppl")]
626    fn test_trace_prefix_matches_addresses() {
627        // regression: EV-91 — the address prefix used by to_trace/from_trace is
628        // now derived from trace_prefix(), so they cannot diverge.
629        use crate::genome::trace_genome::TraceGenome;
630        use fugue::addr;
631        let genome = DynamicRealVector::new(vec![1.0, 2.0, 3.0], 1, 5).unwrap();
632        let trace = genome.to_trace();
633        let prefix = DynamicRealVector::trace_prefix();
634
635        // Values are stored under the advertised prefix...
636        assert!(trace.get_f64(&addr!(prefix, 0)).is_some());
637        assert_eq!(trace.get_f64(&addr!(prefix, 0)), Some(1.0));
638        // ...and round-tripping via that prefix recovers the genome.
639        let restored = DynamicRealVector::from_trace(&trace).unwrap();
640        assert_eq!(restored.genes(), genome.genes());
641    }
642
643    #[test]
644    fn test_generate_with_len_explicit() {
645        // EV-94: honest constructor takes explicit length bounds and value bounds.
646        let mut rng = rand::thread_rng();
647        let value_bounds = MultiBounds::symmetric(2.0, 6);
648        let g = DynamicRealVector::generate_with_len(&mut rng, 3, 5, &value_bounds).unwrap();
649        assert!(g.dimension() >= 3 && g.dimension() <= 5);
650        for gene in g.genes() {
651            assert!(*gene >= -2.0 && *gene <= 2.0);
652        }
653        // Rejects inverted length bounds and empty value bounds.
654        assert!(DynamicRealVector::generate_with_len(&mut rng, 5, 3, &value_bounds).is_err());
655        assert!(
656            DynamicRealVector::generate_with_len(&mut rng, 1, 3, &MultiBounds::new(vec![]))
657                .is_err()
658        );
659    }
660}