Skip to main content

fugue_evo/genome/
composite.rs

1//! Composite genome for mixed-representation problems
2//!
3//! This module provides a genome type that combines two different genome types,
4//! enabling optimization over heterogeneous solution spaces.
5
6use fugue::{addr, ChoiceValue, Trace};
7use rand::Rng;
8use serde::{Deserialize, Serialize};
9
10use crate::error::GenomeError;
11use crate::genome::bounds::MultiBounds;
12use crate::genome::traits::EvolutionaryGenome;
13
14/// A composite genome combining two different genome types
15///
16/// This is useful for problems that require multiple representations,
17/// such as:
18/// - Continuous parameters + discrete choices
19/// - Feature selection (binary) + feature weights (continuous)
20/// - Topology (permutation) + parameters (continuous)
21///
22/// # Type Parameters
23/// - `A`: The first genome type
24/// - `B`: The second genome type
25#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
26#[serde(
27    bound = "A: Serialize + for<'de2> Deserialize<'de2>, B: Serialize + for<'de2> Deserialize<'de2>"
28)]
29pub struct CompositeGenome<A, B>
30where
31    A: EvolutionaryGenome,
32    B: EvolutionaryGenome,
33{
34    /// First component genome
35    pub first: A,
36    /// Second component genome
37    pub second: B,
38}
39
40impl<A, B> CompositeGenome<A, B>
41where
42    A: EvolutionaryGenome,
43    B: EvolutionaryGenome,
44{
45    /// Create a new composite genome from two components
46    pub fn new(first: A, second: B) -> Self {
47        Self { first, second }
48    }
49
50    /// Get a reference to the first component
51    pub fn first(&self) -> &A {
52        &self.first
53    }
54
55    /// Get a mutable reference to the first component
56    pub fn first_mut(&mut self) -> &mut A {
57        &mut self.first
58    }
59
60    /// Get a reference to the second component
61    pub fn second(&self) -> &B {
62        &self.second
63    }
64
65    /// Get a mutable reference to the second component
66    pub fn second_mut(&mut self) -> &mut B {
67        &mut self.second
68    }
69
70    /// Consume and return the components
71    pub fn into_parts(self) -> (A, B) {
72        (self.first, self.second)
73    }
74
75    /// Map a function over the first component
76    pub fn map_first<F, C>(self, f: F) -> CompositeGenome<C, B>
77    where
78        F: FnOnce(A) -> C,
79        C: EvolutionaryGenome,
80    {
81        CompositeGenome {
82            first: f(self.first),
83            second: self.second,
84        }
85    }
86
87    /// Map a function over the second component
88    pub fn map_second<F, C>(self, f: F) -> CompositeGenome<A, C>
89    where
90        F: FnOnce(B) -> C,
91        C: EvolutionaryGenome,
92    {
93        CompositeGenome {
94            first: self.first,
95            second: f(self.second),
96        }
97    }
98}
99
100impl<A, B> EvolutionaryGenome for CompositeGenome<A, B>
101where
102    A: EvolutionaryGenome + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de>,
103    B: EvolutionaryGenome + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de>,
104{
105    type Allele = (A::Allele, B::Allele);
106    type Phenotype = (A::Phenotype, B::Phenotype);
107
108    /// Convert composite genome to Fugue trace.
109    ///
110    /// Stores dimensions and serialized data for each component.
111    fn to_trace(&self) -> Trace {
112        let mut trace = Trace::default();
113
114        // Store dimensions
115        trace.insert_choice(
116            addr!("composite", "first_dim"),
117            ChoiceValue::Usize(self.first.dimension()),
118            0.0,
119        );
120        trace.insert_choice(
121            addr!("composite", "second_dim"),
122            ChoiceValue::Usize(self.second.dimension()),
123            0.0,
124        );
125
126        // Store first component's trace entries with prefix
127        let first_trace = self.first.to_trace();
128        for i in 0..self.first.dimension() {
129            if let Some(val) = first_trace.get_f64(&addr!("gene", i)) {
130                trace.insert_choice(addr!("first_gene", i), ChoiceValue::F64(val), 0.0);
131            } else if let Some(val) = first_trace.get_bool(&addr!("bit", i)) {
132                trace.insert_choice(addr!("first_bit", i), ChoiceValue::Bool(val), 0.0);
133            } else if let Some(val) = first_trace.get_usize(&addr!("element", i)) {
134                trace.insert_choice(addr!("first_element", i), ChoiceValue::Usize(val), 0.0);
135            }
136        }
137
138        // Store second component's trace entries with prefix
139        let second_trace = self.second.to_trace();
140        for i in 0..self.second.dimension() {
141            if let Some(val) = second_trace.get_f64(&addr!("gene", i)) {
142                trace.insert_choice(addr!("second_gene", i), ChoiceValue::F64(val), 0.0);
143            } else if let Some(val) = second_trace.get_bool(&addr!("bit", i)) {
144                trace.insert_choice(addr!("second_bit", i), ChoiceValue::Bool(val), 0.0);
145            } else if let Some(val) = second_trace.get_usize(&addr!("element", i)) {
146                trace.insert_choice(addr!("second_element", i), ChoiceValue::Usize(val), 0.0);
147            }
148        }
149
150        trace
151    }
152
153    /// Reconstruct composite genome from Fugue trace.
154    ///
155    /// Note: This is a simplified implementation that may lose some type information.
156    /// For full fidelity, use serde serialization directly.
157    fn from_trace(trace: &Trace) -> Result<Self, GenomeError> {
158        // Get dimensions
159        let first_dim = trace
160            .get_usize(&addr!("composite", "first_dim"))
161            .ok_or_else(|| GenomeError::MissingAddress("composite#first_dim".to_string()))?;
162        let second_dim = trace
163            .get_usize(&addr!("composite", "second_dim"))
164            .ok_or_else(|| GenomeError::MissingAddress("composite#second_dim".to_string()))?;
165
166        // Reconstruct first component's trace
167        let mut first_trace = Trace::default();
168        for i in 0..first_dim {
169            if let Some(val) = trace.get_f64(&addr!("first_gene", i)) {
170                first_trace.insert_choice(addr!("gene", i), ChoiceValue::F64(val), 0.0);
171            } else if let Some(val) = trace.get_bool(&addr!("first_bit", i)) {
172                first_trace.insert_choice(addr!("bit", i), ChoiceValue::Bool(val), 0.0);
173            } else if let Some(val) = trace.get_usize(&addr!("first_element", i)) {
174                first_trace.insert_choice(addr!("element", i), ChoiceValue::Usize(val), 0.0);
175            }
176        }
177
178        // Reconstruct second component's trace
179        let mut second_trace = Trace::default();
180        for i in 0..second_dim {
181            if let Some(val) = trace.get_f64(&addr!("second_gene", i)) {
182                second_trace.insert_choice(addr!("gene", i), ChoiceValue::F64(val), 0.0);
183            } else if let Some(val) = trace.get_bool(&addr!("second_bit", i)) {
184                second_trace.insert_choice(addr!("bit", i), ChoiceValue::Bool(val), 0.0);
185            } else if let Some(val) = trace.get_usize(&addr!("second_element", i)) {
186                second_trace.insert_choice(addr!("element", i), ChoiceValue::Usize(val), 0.0);
187            }
188        }
189
190        let first = A::from_trace(&first_trace)?;
191        let second = B::from_trace(&second_trace)?;
192
193        Ok(Self { first, second })
194    }
195
196    fn decode(&self) -> Self::Phenotype {
197        (self.first.decode(), self.second.decode())
198    }
199
200    fn dimension(&self) -> usize {
201        self.first.dimension() + self.second.dimension()
202    }
203
204    fn generate<R: Rng>(rng: &mut R, bounds: &MultiBounds) -> Self {
205        // Split bounds between components
206        // This is a simplification - in practice, you'd want separate bounds
207        let first_dim = bounds.dimension() / 2;
208        let second_dim = bounds.dimension() - first_dim;
209
210        let first_bounds =
211            MultiBounds::new(bounds.bounds.iter().take(first_dim).cloned().collect());
212        let second_bounds = MultiBounds::new(
213            bounds
214                .bounds
215                .iter()
216                .skip(first_dim)
217                .take(second_dim)
218                .cloned()
219                .collect(),
220        );
221
222        Self {
223            first: A::generate(rng, &first_bounds),
224            second: B::generate(rng, &second_bounds),
225        }
226    }
227
228    fn distance(&self, other: &Self) -> f64 {
229        // Combined distance (could be weighted)
230        self.first.distance(&other.first) + self.second.distance(&other.second)
231    }
232
233    fn trace_prefix() -> &'static str {
234        "composite"
235    }
236}
237
238/// Builder for composite bounds that tracks bounds for each component
239#[derive(Clone, Debug)]
240pub struct CompositeBounds {
241    /// Bounds for the first component
242    pub first_bounds: MultiBounds,
243    /// Bounds for the second component
244    pub second_bounds: MultiBounds,
245}
246
247impl CompositeBounds {
248    /// Create composite bounds from two separate MultiBounds
249    pub fn new(first_bounds: MultiBounds, second_bounds: MultiBounds) -> Self {
250        Self {
251            first_bounds,
252            second_bounds,
253        }
254    }
255
256    /// Get combined bounds (concatenated)
257    pub fn combined(&self) -> MultiBounds {
258        let mut all_bounds: Vec<_> = self.first_bounds.bounds.to_vec();
259        all_bounds.extend(self.second_bounds.bounds.iter().cloned());
260        MultiBounds::new(all_bounds)
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use crate::genome::bit_string::BitString;
268    use crate::genome::real_vector::RealVector;
269    use crate::genome::traits::{BinaryGenome, RealValuedGenome};
270
271    #[test]
272    fn test_composite_creation() {
273        let real = RealVector::new(vec![1.0, 2.0, 3.0]);
274        let binary = BitString::new(vec![true, false, true, false]);
275
276        let composite = CompositeGenome::new(real.clone(), binary.clone());
277
278        assert_eq!(composite.first().genes(), real.genes());
279        assert_eq!(composite.second().bits(), binary.bits());
280    }
281
282    #[test]
283    fn test_composite_dimension() {
284        let real = RealVector::new(vec![1.0, 2.0, 3.0]);
285        let binary = BitString::new(vec![true, false, true, false]);
286
287        let composite = CompositeGenome::new(real, binary);
288
289        // 3 real + 4 binary = 7
290        assert_eq!(composite.dimension(), 7);
291    }
292
293    #[test]
294    fn test_composite_decode() {
295        let real = RealVector::new(vec![1.0, 2.0, 3.0]);
296        let binary = BitString::new(vec![true, false, true, false]);
297
298        let composite = CompositeGenome::new(real, binary);
299        let (decoded_real, decoded_binary) = composite.decode();
300
301        assert_eq!(decoded_real, vec![1.0, 2.0, 3.0]);
302        assert_eq!(decoded_binary, vec![true, false, true, false]);
303    }
304
305    #[test]
306    fn test_composite_into_parts() {
307        let real = RealVector::new(vec![1.0, 2.0]);
308        let binary = BitString::new(vec![true, true, false]);
309
310        let composite = CompositeGenome::new(real.clone(), binary.clone());
311        let (r, b) = composite.into_parts();
312
313        assert_eq!(r.genes(), real.genes());
314        assert_eq!(b.bits(), binary.bits());
315    }
316
317    #[test]
318    fn test_composite_map() {
319        let real = RealVector::new(vec![1.0, 2.0]);
320        let binary = BitString::new(vec![true, false]);
321
322        let composite = CompositeGenome::new(real, binary);
323
324        // Map first to double values
325        let mapped = composite.map_first(|r| r.scale(2.0));
326        assert_eq!(mapped.first().genes(), &[2.0, 4.0]);
327    }
328
329    #[test]
330    fn test_composite_distance() {
331        let c1 = CompositeGenome::new(
332            RealVector::new(vec![0.0, 0.0]),
333            BitString::new(vec![true, false]),
334        );
335
336        let c2 = CompositeGenome::new(
337            RealVector::new(vec![3.0, 4.0]),
338            BitString::new(vec![false, true]),
339        );
340
341        let dist = c1.distance(&c2);
342
343        // Real distance = 5.0, binary distance = 2 (Hamming)
344        assert!(dist > 0.0);
345    }
346
347    #[test]
348    fn test_composite_generate() {
349        let bounds = MultiBounds::symmetric(5.0, 6); // Split as 3+3
350        let mut rng = rand::thread_rng();
351
352        // This test requires that both component types can generate from bounds
353        // For simplicity, test with two RealVectors
354        let composite: CompositeGenome<RealVector, RealVector> =
355            CompositeGenome::generate(&mut rng, &bounds);
356
357        assert_eq!(composite.dimension(), 6);
358    }
359
360    #[test]
361    fn test_composite_bounds() {
362        use crate::genome::bounds::Bounds;
363
364        let first_bounds = MultiBounds::symmetric(5.0, 3);
365        let second_bounds = MultiBounds::uniform(Bounds::unit(), 4);
366
367        let composite_bounds = CompositeBounds::new(first_bounds, second_bounds);
368        let combined = composite_bounds.combined();
369
370        assert_eq!(combined.dimension(), 7);
371    }
372
373    #[test]
374    fn test_composite_first_mut() {
375        let real = RealVector::new(vec![1.0, 2.0, 3.0]);
376        let binary = BitString::new(vec![true, false]);
377
378        let mut composite = CompositeGenome::new(real, binary);
379
380        // Modify first component through mutable reference
381        composite.first_mut().genes_mut()[0] = 10.0;
382
383        assert_eq!(composite.first().genes()[0], 10.0);
384    }
385
386    #[test]
387    fn test_composite_second_mut() {
388        let real = RealVector::new(vec![1.0, 2.0]);
389        let binary = BitString::new(vec![true, false, true]);
390
391        let mut composite = CompositeGenome::new(real, binary);
392
393        // Modify second component through mutable reference
394        composite.second_mut().bits_mut()[0] = false;
395
396        assert!(!composite.second().bits()[0]);
397    }
398
399    #[test]
400    fn test_composite_map_second() {
401        let real = RealVector::new(vec![1.0, 2.0]);
402        let second_real = RealVector::new(vec![3.0, 4.0]);
403
404        let composite = CompositeGenome::new(real, second_real);
405
406        // Map second to double values
407        let mapped = composite.map_second(|r| r.scale(2.0));
408        assert_eq!(mapped.second().genes(), &[6.0, 8.0]);
409    }
410
411    #[test]
412    fn test_composite_trace_roundtrip_real_vectors() {
413        let first = RealVector::new(vec![1.5, 2.5, 3.5]);
414        let second = RealVector::new(vec![4.5, 5.5]);
415
416        let composite = CompositeGenome::new(first.clone(), second.clone());
417        let trace = composite.to_trace();
418        let recovered: CompositeGenome<RealVector, RealVector> =
419            CompositeGenome::from_trace(&trace).expect("Should deserialize");
420
421        assert_eq!(recovered.first().genes(), first.genes());
422        assert_eq!(recovered.second().genes(), second.genes());
423    }
424
425    #[test]
426    fn test_composite_trace_roundtrip_mixed() {
427        let real = RealVector::new(vec![1.0, 2.0]);
428        let binary = BitString::new(vec![true, false, true]);
429
430        let composite = CompositeGenome::new(real.clone(), binary.clone());
431        let trace = composite.to_trace();
432
433        // Note: The trace stores data using type-specific prefixes
434        // This test verifies the trace contains the expected structure
435        assert!(trace.get_usize(&addr!("composite", "first_dim")).is_some());
436        assert!(trace.get_usize(&addr!("composite", "second_dim")).is_some());
437    }
438
439    #[test]
440    fn test_composite_trace_prefix() {
441        assert_eq!(
442            <CompositeGenome<RealVector, BitString>>::trace_prefix(),
443            "composite"
444        );
445    }
446
447    #[test]
448    fn test_composite_from_trace_missing_dim_error() {
449        use fugue::Trace;
450        let empty_trace = Trace::default();
451
452        let result: Result<CompositeGenome<RealVector, RealVector>, _> =
453            CompositeGenome::from_trace(&empty_trace);
454
455        assert!(result.is_err());
456    }
457}