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
6#[cfg(feature = "ppl")]
7use fugue::{Address, Trace};
8use rand::Rng;
9use serde::{Deserialize, Serialize};
10
11use crate::error::GenomeError;
12use crate::genome::bounds::MultiBounds;
13use crate::genome::traits::EvolutionaryGenome;
14
15/// A composite genome combining two different genome types
16///
17/// This is useful for problems that require multiple representations,
18/// such as:
19/// - Continuous parameters + discrete choices
20/// - Feature selection (binary) + feature weights (continuous)
21/// - Topology (permutation) + parameters (continuous)
22///
23/// # Type Parameters
24/// - `A`: The first genome type
25/// - `B`: The second genome type
26#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
27#[serde(
28    bound = "A: Serialize + for<'de2> Deserialize<'de2>, B: Serialize + for<'de2> Deserialize<'de2>"
29)]
30pub struct CompositeGenome<A, B>
31where
32    A: EvolutionaryGenome,
33    B: EvolutionaryGenome,
34{
35    /// First component genome
36    pub first: A,
37    /// Second component genome
38    pub second: B,
39}
40
41impl<A, B> CompositeGenome<A, B>
42where
43    A: EvolutionaryGenome,
44    B: EvolutionaryGenome,
45{
46    /// Create a new composite genome from two components
47    pub fn new(first: A, second: B) -> Self {
48        Self { first, second }
49    }
50
51    /// Get a reference to the first component
52    pub fn first(&self) -> &A {
53        &self.first
54    }
55
56    /// Get a mutable reference to the first component
57    pub fn first_mut(&mut self) -> &mut A {
58        &mut self.first
59    }
60
61    /// Get a reference to the second component
62    pub fn second(&self) -> &B {
63        &self.second
64    }
65
66    /// Get a mutable reference to the second component
67    pub fn second_mut(&mut self) -> &mut B {
68        &mut self.second
69    }
70
71    /// Consume and return the components
72    pub fn into_parts(self) -> (A, B) {
73        (self.first, self.second)
74    }
75
76    /// Map a function over the first component
77    pub fn map_first<F, C>(self, f: F) -> CompositeGenome<C, B>
78    where
79        F: FnOnce(A) -> C,
80        C: EvolutionaryGenome,
81    {
82        CompositeGenome {
83            first: f(self.first),
84            second: self.second,
85        }
86    }
87
88    /// Map a function over the second component
89    pub fn map_second<F, C>(self, f: F) -> CompositeGenome<A, C>
90    where
91        F: FnOnce(B) -> C,
92        C: EvolutionaryGenome,
93    {
94        CompositeGenome {
95            first: self.first,
96            second: f(self.second),
97        }
98    }
99}
100
101impl<A, B> EvolutionaryGenome for CompositeGenome<A, B>
102where
103    A: EvolutionaryGenome + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de>,
104    B: EvolutionaryGenome + Clone + Send + Sync + Serialize + for<'de> Deserialize<'de>,
105{
106    type Allele = (A::Allele, B::Allele);
107    type Phenotype = (A::Phenotype, B::Phenotype);
108
109    fn decode(&self) -> Self::Phenotype {
110        (self.first.decode(), self.second.decode())
111    }
112
113    fn dimension(&self) -> usize {
114        self.first.dimension() + self.second.dimension()
115    }
116
117    fn generate<R: Rng>(rng: &mut R, bounds: &MultiBounds) -> Self {
118        // Split bounds between components
119        // This is a simplification - in practice, you'd want separate bounds
120        let first_dim = bounds.dimension() / 2;
121        let second_dim = bounds.dimension() - first_dim;
122
123        let first_bounds =
124            MultiBounds::new(bounds.bounds.iter().take(first_dim).cloned().collect());
125        let second_bounds = MultiBounds::new(
126            bounds
127                .bounds
128                .iter()
129                .skip(first_dim)
130                .take(second_dim)
131                .cloned()
132                .collect(),
133        );
134
135        Self {
136            first: A::generate(rng, &first_bounds),
137            second: B::generate(rng, &second_bounds),
138        }
139    }
140
141    fn distance(&self, other: &Self) -> f64 {
142        // Combined distance (could be weighted)
143        self.first.distance(&other.first) + self.second.distance(&other.second)
144    }
145
146    fn try_distance(&self, other: &Self) -> Result<f64, GenomeError> {
147        Ok(self.first.try_distance(&other.first)? + self.second.try_distance(&other.second)?)
148    }
149}
150
151#[cfg(feature = "ppl")]
152impl<A, B> crate::genome::trace_genome::TraceGenome for CompositeGenome<A, B>
153where
154    A: crate::genome::trace_genome::TraceGenome
155        + Clone
156        + Send
157        + Sync
158        + Serialize
159        + for<'de> Deserialize<'de>,
160    B: crate::genome::trace_genome::TraceGenome
161        + Clone
162        + Send
163        + Sync
164        + Serialize
165        + for<'de> Deserialize<'de>,
166{
167    /// Convert composite genome to Fugue trace.
168    ///
169    /// Each component is delegated to its own
170    /// [`to_trace`](crate::genome::trace_genome::TraceGenome::to_trace),
171    /// and every entry of the resulting nested trace is copied verbatim under a
172    /// namespace prefix (`"first/"` / `"second/"`). This preserves full fidelity
173    /// for *any* component genome type — including `Permutation`, `TreeGenome`
174    /// and future encodings — rather than special-casing a fixed set of address
175    /// name literals.
176    fn to_trace(&self) -> Trace {
177        let mut trace = Trace::default();
178        namespace_into(&self.first.to_trace(), "first", &mut trace);
179        namespace_into(&self.second.to_trace(), "second", &mut trace);
180        trace
181    }
182
183    /// Reconstruct composite genome from Fugue trace.
184    ///
185    /// Strips the `"first/"` / `"second/"` namespace back off each entry to
186    /// rebuild the two component sub-traces, then delegates to each component's
187    /// own [`from_trace`](crate::genome::trace_genome::TraceGenome::from_trace).
188    fn from_trace(trace: &Trace) -> Result<Self, GenomeError> {
189        let (first_trace, saw_first) = extract_namespace(trace, "first");
190        let (second_trace, saw_second) = extract_namespace(trace, "second");
191
192        if !saw_first {
193            return Err(GenomeError::MissingAddress("first/*".to_string()));
194        }
195        if !saw_second {
196            return Err(GenomeError::MissingAddress("second/*".to_string()));
197        }
198
199        let first = A::from_trace(&first_trace)?;
200        let second = B::from_trace(&second_trace)?;
201
202        Ok(Self { first, second })
203    }
204
205    fn trace_prefix() -> &'static str {
206        "composite"
207    }
208}
209
210/// Copy every entry of `src` into `dst`, prefixing each address with
211/// `"<namespace>/"`. Used to merge a component's own trace into the composite
212/// trace without interpreting the component's address scheme.
213#[cfg(feature = "ppl")]
214fn namespace_into(src: &Trace, namespace: &str, dst: &mut Trace) {
215    for (addr, choice) in &src.choices {
216        dst.insert_choice(
217            Address::new(format!("{}/{}", namespace, addr.as_str())),
218            choice.value.clone(),
219            choice.logp,
220        );
221    }
222}
223
224/// Inverse of [`namespace_into`]: collect every entry whose address begins with
225/// `"<namespace>/"` into a fresh trace with the prefix stripped back off.
226/// Returns the reconstructed sub-trace and whether any entry was found.
227#[cfg(feature = "ppl")]
228fn extract_namespace(trace: &Trace, namespace: &str) -> (Trace, bool) {
229    let prefix = format!("{namespace}/");
230    let mut sub = Trace::default();
231    let mut found = false;
232    for (addr, choice) in &trace.choices {
233        if let Some(rest) = addr.as_str().strip_prefix(&prefix) {
234            sub.insert_choice(
235                Address::new(rest.to_string()),
236                choice.value.clone(),
237                choice.logp,
238            );
239            found = true;
240        }
241    }
242    (sub, found)
243}
244
245/// Builder for composite bounds that tracks bounds for each component
246#[derive(Clone, Debug)]
247pub struct CompositeBounds {
248    /// Bounds for the first component
249    pub first_bounds: MultiBounds,
250    /// Bounds for the second component
251    pub second_bounds: MultiBounds,
252}
253
254impl CompositeBounds {
255    /// Create composite bounds from two separate MultiBounds
256    pub fn new(first_bounds: MultiBounds, second_bounds: MultiBounds) -> Self {
257        Self {
258            first_bounds,
259            second_bounds,
260        }
261    }
262
263    /// Get combined bounds (concatenated)
264    pub fn combined(&self) -> MultiBounds {
265        let mut all_bounds: Vec<_> = self.first_bounds.bounds.to_vec();
266        all_bounds.extend(self.second_bounds.bounds.iter().cloned());
267        MultiBounds::new(all_bounds)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use crate::genome::bit_string::BitString;
275    #[cfg(feature = "ppl")]
276    use crate::genome::permutation::Permutation;
277    use crate::genome::real_vector::RealVector;
278    #[cfg(feature = "ppl")]
279    use crate::genome::traits::PermutationGenome;
280    use crate::genome::traits::{BinaryGenome, RealValuedGenome};
281
282    #[test]
283    fn test_composite_creation() {
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.clone(), binary.clone());
288
289        assert_eq!(composite.first().genes(), real.genes());
290        assert_eq!(composite.second().bits(), binary.bits());
291    }
292
293    #[test]
294    fn test_composite_dimension() {
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
300        // 3 real + 4 binary = 7
301        assert_eq!(composite.dimension(), 7);
302    }
303
304    #[test]
305    fn test_composite_decode() {
306        let real = RealVector::new(vec![1.0, 2.0, 3.0]);
307        let binary = BitString::new(vec![true, false, true, false]);
308
309        let composite = CompositeGenome::new(real, binary);
310        let (decoded_real, decoded_binary) = composite.decode();
311
312        assert_eq!(decoded_real, vec![1.0, 2.0, 3.0]);
313        assert_eq!(decoded_binary, vec![true, false, true, false]);
314    }
315
316    #[test]
317    fn test_composite_into_parts() {
318        let real = RealVector::new(vec![1.0, 2.0]);
319        let binary = BitString::new(vec![true, true, false]);
320
321        let composite = CompositeGenome::new(real.clone(), binary.clone());
322        let (r, b) = composite.into_parts();
323
324        assert_eq!(r.genes(), real.genes());
325        assert_eq!(b.bits(), binary.bits());
326    }
327
328    #[test]
329    fn test_composite_map() {
330        let real = RealVector::new(vec![1.0, 2.0]);
331        let binary = BitString::new(vec![true, false]);
332
333        let composite = CompositeGenome::new(real, binary);
334
335        // Map first to double values
336        let mapped = composite.map_first(|r| r.scale(2.0));
337        assert_eq!(mapped.first().genes(), &[2.0, 4.0]);
338    }
339
340    #[test]
341    fn test_composite_distance() {
342        let c1 = CompositeGenome::new(
343            RealVector::new(vec![0.0, 0.0]),
344            BitString::new(vec![true, false]),
345        );
346
347        let c2 = CompositeGenome::new(
348            RealVector::new(vec![3.0, 4.0]),
349            BitString::new(vec![false, true]),
350        );
351
352        let dist = c1.distance(&c2);
353
354        // Real distance = 5.0, binary distance = 2 (Hamming)
355        assert!(dist > 0.0);
356    }
357
358    #[test]
359    fn test_composite_generate() {
360        let bounds = MultiBounds::symmetric(5.0, 6); // Split as 3+3
361        let mut rng = rand::thread_rng();
362
363        // This test requires that both component types can generate from bounds
364        // For simplicity, test with two RealVectors
365        let composite: CompositeGenome<RealVector, RealVector> =
366            CompositeGenome::generate(&mut rng, &bounds);
367
368        assert_eq!(composite.dimension(), 6);
369    }
370
371    #[test]
372    fn test_composite_bounds() {
373        use crate::genome::bounds::Bounds;
374
375        let first_bounds = MultiBounds::symmetric(5.0, 3);
376        let second_bounds = MultiBounds::uniform(Bounds::unit(), 4);
377
378        let composite_bounds = CompositeBounds::new(first_bounds, second_bounds);
379        let combined = composite_bounds.combined();
380
381        assert_eq!(combined.dimension(), 7);
382    }
383
384    #[test]
385    fn test_composite_first_mut() {
386        let real = RealVector::new(vec![1.0, 2.0, 3.0]);
387        let binary = BitString::new(vec![true, false]);
388
389        let mut composite = CompositeGenome::new(real, binary);
390
391        // Modify first component through mutable reference
392        composite.first_mut().genes_mut()[0] = 10.0;
393
394        assert_eq!(composite.first().genes()[0], 10.0);
395    }
396
397    #[test]
398    fn test_composite_second_mut() {
399        let real = RealVector::new(vec![1.0, 2.0]);
400        let binary = BitString::new(vec![true, false, true]);
401
402        let mut composite = CompositeGenome::new(real, binary);
403
404        // Modify second component through mutable reference
405        composite.second_mut().bits_mut()[0] = false;
406
407        assert!(!composite.second().bits()[0]);
408    }
409
410    #[test]
411    fn test_composite_map_second() {
412        let real = RealVector::new(vec![1.0, 2.0]);
413        let second_real = RealVector::new(vec![3.0, 4.0]);
414
415        let composite = CompositeGenome::new(real, second_real);
416
417        // Map second to double values
418        let mapped = composite.map_second(|r| r.scale(2.0));
419        assert_eq!(mapped.second().genes(), &[6.0, 8.0]);
420    }
421
422    #[test]
423    #[cfg(feature = "ppl")]
424    fn test_composite_trace_roundtrip_real_vectors() {
425        use crate::genome::trace_genome::TraceGenome;
426        let first = RealVector::new(vec![1.5, 2.5, 3.5]);
427        let second = RealVector::new(vec![4.5, 5.5]);
428
429        let composite = CompositeGenome::new(first.clone(), second.clone());
430        let trace = composite.to_trace();
431        let recovered: CompositeGenome<RealVector, RealVector> =
432            CompositeGenome::from_trace(&trace).expect("Should deserialize");
433
434        assert_eq!(recovered.first().genes(), first.genes());
435        assert_eq!(recovered.second().genes(), second.genes());
436    }
437
438    #[test]
439    #[cfg(feature = "ppl")]
440    fn test_composite_trace_roundtrip_mixed() {
441        // regression: EV-03 — full round-trip fidelity for a mixed composite.
442        use crate::genome::trace_genome::TraceGenome;
443        let real = RealVector::new(vec![1.0, 2.0]);
444        let binary = BitString::new(vec![true, false, true]);
445
446        let composite = CompositeGenome::new(real.clone(), binary.clone());
447        let trace = composite.to_trace();
448        let recovered: CompositeGenome<RealVector, BitString> =
449            CompositeGenome::from_trace(&trace).expect("mixed composite should round-trip");
450
451        assert_eq!(recovered.first().genes(), real.genes());
452        assert_eq!(recovered.second().bits(), binary.bits());
453    }
454
455    #[test]
456    #[cfg(feature = "ppl")]
457    fn test_composite_trace_roundtrip_permutation_realvector() {
458        // regression: EV-03 — previously to_trace dropped every permutation value
459        // (no "perm" prefix was recognized) and from_trace always failed. The
460        // module doc explicitly showcases "topology (permutation) + parameters".
461        use crate::genome::trace_genome::TraceGenome;
462        let perm = Permutation::new(vec![2, 0, 3, 1]);
463        let real = RealVector::new(vec![1.5, -2.5, 3.5]);
464
465        let composite = CompositeGenome::new(perm.clone(), real.clone());
466        let trace = composite.to_trace();
467        let recovered: CompositeGenome<Permutation, RealVector> =
468            CompositeGenome::from_trace(&trace)
469                .expect("permutation+real composite should round-trip");
470
471        assert_eq!(recovered.first().permutation(), perm.permutation());
472        assert_eq!(recovered.second().genes(), real.genes());
473        assert_eq!(recovered, composite);
474    }
475
476    #[test]
477    #[cfg(feature = "ppl")]
478    fn test_composite_trace_roundtrip_bitstring_permutation() {
479        // regression: EV-03 — round-trip for BitString + Permutation.
480        use crate::genome::trace_genome::TraceGenome;
481        let bits = BitString::new(vec![true, false, true, true]);
482        let perm = Permutation::new(vec![1, 3, 0, 2]);
483
484        let composite = CompositeGenome::new(bits.clone(), perm.clone());
485        let trace = composite.to_trace();
486        let recovered: CompositeGenome<BitString, Permutation> =
487            CompositeGenome::from_trace(&trace)
488                .expect("bitstring+permutation composite should round-trip");
489
490        assert_eq!(recovered.first().bits(), bits.bits());
491        assert_eq!(recovered.second().permutation(), perm.permutation());
492        assert_eq!(recovered, composite);
493    }
494
495    #[test]
496    #[cfg(feature = "ppl")]
497    fn test_composite_trace_prefix() {
498        use crate::genome::trace_genome::TraceGenome;
499        assert_eq!(
500            <CompositeGenome<RealVector, BitString>>::trace_prefix(),
501            "composite"
502        );
503    }
504
505    #[test]
506    #[cfg(feature = "ppl")]
507    fn test_composite_from_trace_missing_dim_error() {
508        use crate::genome::trace_genome::TraceGenome;
509        use fugue::Trace;
510        let empty_trace = Trace::default();
511
512        let result: Result<CompositeGenome<RealVector, RealVector>, _> =
513            CompositeGenome::from_trace(&empty_trace);
514
515        assert!(result.is_err());
516    }
517}