Skip to main content

rlevo_evolution/algorithms/gep/
decode.rs

1//! Genotype → phenotype decoding (the head/tail → expression-tree map).
2
3use crate::function_set::{FunctionSet, Symbol};
4
5use super::alphabet::Alphabet;
6use super::tree::ExpressionTree;
7
8/// Maps a linear GEP chromosome to its expression-tree phenotype.
9///
10/// Implementors decode `genome` (a head/tail symbol string) against `alphabet`
11/// into an [`ExpressionTree`]. The decode is deterministic: the same genome and
12/// alphabet always yield the same tree.
13///
14/// # Invariants
15///
16/// The `genome` is expected to satisfy the GEP head/tail layout: a head region
17/// whose loci may hold functions or terminals, followed by a terminal-only tail
18/// of length `t = h(n - 1) + 1`, where `h` is the head length and `n` the
19/// alphabet's maximum arity (Ferreira 2001, eq. 3.4). Any chromosome produced by
20/// the sampling and operator paths in this crate satisfies this by construction;
21/// a hand-built genome that violates it is a precondition breach (see the
22/// implementation's `# Panics`).
23pub trait GenotypePhenotypeMap<F: FunctionSet> {
24    /// Decodes a chromosome into its phenotype.
25    ///
26    /// # Arguments
27    ///
28    /// * `alphabet` — the symbol alphabet used to classify each locus (its
29    ///   arity and kind); must be the same alphabet the `genome` was sampled
30    ///   against.
31    /// * `genome` — a linear head/tail chromosome (see the trait-level
32    ///   `# Invariants`). An empty genome decodes to an empty tree.
33    ///
34    /// # Returns
35    ///
36    /// The [`ExpressionTree`] phenotype: the open-reading-frame coding region of
37    /// `genome` in level order.
38    ///
39    /// # Panics
40    ///
41    /// Debug builds assert the head/tail precondition
42    /// (`debug_assert!(needed == 0, ...)`); a genome that violates it panics here
43    /// in debug. In release the assertion is absent — `decode` itself does not
44    /// panic, but a malformed tree silently degrades to a finite-but-incorrect
45    /// value on evaluation (see [`ExpressionTree::eval`]'s defensive clamp,
46    /// issue #147).
47    #[must_use]
48    fn decode(&self, alphabet: &Alphabet<F>, genome: &[Symbol]) -> ExpressionTree;
49}
50
51/// The canonical Ferreira (2001) decoder: open-reading-frame scan, then
52/// level-order (breadth-first) tree construction.
53///
54/// # Algorithm
55///
56/// 1. **ORF-length pass.** A single left-to-right scan tracks the number of
57///    still-unfilled child slots, starting at 1 (the root). Each symbol fills
58///    one slot and opens `arity` new ones; the coding region ends when no slots
59///    remain. The tail-length constraint guarantees this terminates within the
60///    chromosome.
61/// 2. **BFS child assignment.** Because the coding prefix is already in level
62///    order, a node's children are simply the next unread symbols. A running
63///    read cursor assigns each node `arity` contiguous children — no explicit
64///    queue is needed, since array order *is* BFS order.
65///
66/// # Examples
67///
68/// ```
69/// use rlevo_evolution::algorithms::gep::{Alphabet, GenotypePhenotypeMap, GepDecoder};
70/// use rlevo_evolution::function_set::ArithmeticFunctionSet;
71/// use rlevo_evolution::rng::{seed_stream, SeedPurpose};
72///
73/// // A one-variable arithmetic alphabet (functions over a single `x`).
74/// let alphabet = Alphabet::new(ArithmeticFunctionSet, 1, vec![]);
75///
76/// // Sample a valid head/tail genome: head loci draw any symbol, tail loci
77/// // draw terminals only, with the tail sized per Ferreira (2001) eq. 3.4.
78/// let mut rng = seed_stream(42, 0, SeedPurpose::Mutation);
79/// let head_len = 3;
80/// let tail_len = head_len * (alphabet.max_arity() - 1) + 1;
81/// let mut genome = Vec::with_capacity(head_len + tail_len);
82/// for _ in 0..head_len {
83///     genome.push(alphabet.sample_head_symbol(&mut rng));
84/// }
85/// for _ in 0..tail_len {
86///     genome.push(alphabet.sample_tail_symbol(&mut rng));
87/// }
88///
89/// // Decode the linear chromosome into its expression-tree phenotype.
90/// let tree = GepDecoder.decode(&alphabet, &genome);
91/// assert!(tree.node_count() >= 1);
92/// ```
93#[derive(Clone, Copy, Debug, Default)]
94pub struct GepDecoder;
95
96impl<F: FunctionSet> GenotypePhenotypeMap<F> for GepDecoder {
97    fn decode(&self, alphabet: &Alphabet<F>, genome: &[Symbol]) -> ExpressionTree {
98        if genome.is_empty() {
99            return ExpressionTree::from_parts(Vec::new(), Vec::new(), Vec::new());
100        }
101
102        // 1. ORF-length pass.
103        let mut needed: usize = 1;
104        let mut orf_len = 0;
105        while needed > 0 && orf_len < genome.len() {
106            let arity = alphabet.arity(genome[orf_len]);
107            needed = needed - 1 + arity;
108            orf_len += 1;
109        }
110
111        // A well-formed GEP gene (head ∈ F∪T, tail ∈ T strictly, tail length
112        // t = h(n−1)+1 with n = max arity) always satisfies every open child
113        // slot within the chromosome, so the scan must exit with `needed == 0`
114        // (Ferreira 2001, Complex Systems 13(2), §3.2 eq. 3.4). A residual
115        // `needed > 0` can only arise from a contract-violating genome (e.g.
116        // one hand-built via `Symbol::from_raw` that bypasses the head/tail
117        // rule): its child ranges would overrun `node_count()` and later panic
118        // in `eval`. Flag that precondition breach in debug builds; `eval`
119        // carries a matching release-time clamp so the failure degrades to a
120        // finite value rather than an out-of-bounds slice.
121        debug_assert!(
122            needed == 0,
123            "genome violates GEP head/tail invariant t = h(n-1)+1 (Ferreira \
124             2001 eq. 3.4): {needed} child slot(s) left unfilled"
125        );
126
127        // 2. Level-order parts. Children are the next unread symbols; a single
128        //    read cursor walks them in BFS order.
129        let nodes: Vec<Symbol> = genome[..orf_len].to_vec();
130        let mut arities = Vec::with_capacity(orf_len);
131        let mut child_start = Vec::with_capacity(orf_len);
132        let mut read = 1usize;
133        for &symbol in &nodes {
134            let arity = alphabet.arity(symbol);
135            arities.push(arity);
136            child_start.push(read);
137            read += arity;
138        }
139
140        ExpressionTree::from_parts(nodes, arities, child_start)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::function_set::ArithmeticFunctionSet;
148    use rand::rngs::StdRng;
149    use rand::{RngExt, SeedableRng};
150
151    fn alphabet(n_vars: usize) -> Alphabet<ArithmeticFunctionSet> {
152        Alphabet::new(ArithmeticFunctionSet, n_vars, vec![])
153    }
154
155    /// ORF length for `[+, *, x, x, x, ...]`: root + needs 2 (the * and the
156    /// last x), the * needs 2 more (x, x). 5 coding symbols.
157    #[test]
158    fn orf_length_for_nested_tree() {
159        let a = alphabet(1);
160        // ids: + = 0, * = 2, x = 8
161        let genome = vec![
162            Symbol::from_raw(0),
163            Symbol::from_raw(2),
164            Symbol::from_raw(8),
165            Symbol::from_raw(8),
166            Symbol::from_raw(8),
167            Symbol::from_raw(8), // tail junk
168            Symbol::from_raw(8),
169        ];
170        let tree = GepDecoder.decode(&a, &genome);
171        // + (root) -> children {*, x}; * -> children {x, x}. 5 nodes.
172        assert_eq!(tree.node_count(), 5);
173    }
174
175    /// Decode is deterministic: same genome twice -> identical node lists.
176    #[test]
177    fn decode_is_deterministic() {
178        let a = alphabet(2);
179        let genome = vec![
180            Symbol::from_raw(0),
181            Symbol::from_raw(1),
182            Symbol::from_raw(8),
183            Symbol::from_raw(9),
184            Symbol::from_raw(8),
185            Symbol::from_raw(9),
186            Symbol::from_raw(8),
187        ];
188        let t1 = GepDecoder.decode(&a, &genome);
189        let t2 = GepDecoder.decode(&a, &genome);
190        assert_eq!(t1.nodes(), t2.nodes());
191        assert_eq!(t1.node_count(), t2.node_count());
192    }
193
194    /// An all-terminal head yields a single-node ORF.
195    #[test]
196    fn all_terminal_head_is_one_node() {
197        let a = alphabet(1);
198        let genome = vec![
199            Symbol::from_raw(8),
200            Symbol::from_raw(8),
201            Symbol::from_raw(8),
202        ];
203        let tree = GepDecoder.decode(&a, &genome);
204        assert_eq!(tree.node_count(), 1);
205    }
206
207    // §7.1 -----------------------------------------------------------------
208
209    /// An empty genome decodes to an empty (zero-node) tree without panic.
210    #[test]
211    fn empty_genome_decodes_to_zero_node_tree() {
212        let a = alphabet(1);
213        let tree = GepDecoder.decode(&a, &[]);
214        assert_eq!(tree.node_count(), 0);
215        // A zero-node tree evaluates to the inert 0.0.
216        approx::assert_relative_eq!(tree.eval(&a, &[1.0]), 0.0, epsilon = 1e-6);
217    }
218
219    // §7.3 -----------------------------------------------------------------
220
221    /// A unary (arity-1) function head decodes to a two-node ORF: `sin(x)`.
222    #[test]
223    fn arity_one_function_decodes_two_nodes() {
224        let a = alphabet(1);
225        // ids: sin = 4 (arity 1), var x = 8. head [4, 8], tail [8].
226        let genome = vec![
227            Symbol::from_raw(4),
228            Symbol::from_raw(8),
229            Symbol::from_raw(8),
230        ];
231        let tree = GepDecoder.decode(&a, &genome);
232        // sin (root) -> child {x}. 2 nodes.
233        assert_eq!(tree.node_count(), 2);
234        approx::assert_relative_eq!(tree.eval(&a, &[0.0]), 0.0f32.sin(), epsilon = 1e-6);
235    }
236
237    /// A maximally nested (full-tail) binary chromosome decodes to a deep tree
238    /// that consumes the whole coding region.
239    #[test]
240    fn full_tail_deep_tree() {
241        let a = alphabet(1);
242        // head all binary `+` (id 0), length h = 4; tail all `x` (id 8),
243        // length t = h(n-1)+1 = 4*1+1 = 5. A left-full binary tree of 4
244        // internal nodes has 5 leaves: 9 coding nodes total.
245        let mut genome = vec![Symbol::from_raw(0); 4];
246        genome.extend(std::iter::repeat_n(Symbol::from_raw(8), 5));
247        let tree = GepDecoder.decode(&a, &genome);
248        assert_eq!(tree.node_count(), 9);
249        // 4 additions of x summed over the tree: value is 5*x for x-leaves? No
250        // — a chain of `+` with x leaves sums the 5 leaves = 5x.
251        approx::assert_relative_eq!(tree.eval(&a, &[2.0]), 10.0, epsilon = 1e-6);
252    }
253
254    /// An out-of-range head symbol is treated as an inert arity-0 terminal, so
255    /// it terminates the ORF at a single node rather than opening child slots.
256    #[test]
257    fn out_of_range_symbol_is_terminal() {
258        let a = alphabet(1);
259        // id 999 is beyond len(); classify() reports arity 0.
260        let genome = vec![
261            Symbol::from_raw(999),
262            Symbol::from_raw(8),
263            Symbol::from_raw(8),
264        ];
265        let tree = GepDecoder.decode(&a, &genome);
266        assert_eq!(tree.node_count(), 1);
267        approx::assert_relative_eq!(tree.eval(&a, &[3.0]), 0.0, epsilon = 1e-6);
268    }
269
270    // §7.4 -----------------------------------------------------------------
271
272    /// The key guard (issue #147 §1.1). Ferreira (2001, eq. 3.4) guarantees any
273    /// well-formed head/tail gene decodes to a complete tree: the ORF scan never
274    /// leaves an unfilled child slot, so every child range stays in bounds and
275    /// `eval` never slices past `node_count()`. Generate many random but
276    /// well-formed genomes (head ∈ F∪T, tail ∈ T strictly, tail length
277    /// t = h(n−1)+1) and assert the guarantee holds structurally and that `eval`
278    /// returns a finite value without panic.
279    #[test]
280    fn wellformed_genomes_always_decode_in_bounds() {
281        let mut rng = StdRng::seed_from_u64(0x9E37_79B9_7F4A_7C15);
282        let max_arity = 2; // max arity of ArithmeticFunctionSet.
283        for n_vars in 1..=3usize {
284            let alpha = alphabet(n_vars);
285            for _ in 0..2_000 {
286                let head_len = 1 + rng_usize(&mut rng, 12); // head length 1..=12
287                let tail_len = head_len * (max_arity - 1) + 1; // Ferreira eq. 3.4.
288                let mut genome = Vec::with_capacity(head_len + tail_len);
289                for _ in 0..head_len {
290                    genome.push(alpha.sample_head_symbol(&mut rng));
291                }
292                for _ in 0..tail_len {
293                    genome.push(alpha.sample_tail_symbol(&mut rng));
294                }
295
296                let tree = GepDecoder.decode(&alpha, &genome);
297                let node_count = tree.node_count();
298                assert!(node_count >= 1, "coding region must be non-empty");
299
300                // Every non-root coding node is exactly one child of exactly one
301                // parent, so the total child count equals node_count - 1. This
302                // is the public-API restatement of `child_start[i] + arity[i]
303                // <= node_count` for all i (BFS layout): the scan filled every
304                // slot (Ferreira eq. 3.4), i.e. the decoder's `needed == 0`.
305                let total_children: usize = tree.nodes().iter().map(|&sym| alpha.arity(sym)).sum();
306                assert_eq!(
307                    total_children + 1,
308                    node_count,
309                    "well-formed genome left an unfilled child slot: {genome:?}"
310                );
311
312                // `eval` must not panic and must return a finite value for any
313                // input row (the finite_or_clamp guard neutralizes overflow).
314                let inputs: Vec<f32> = (0..n_vars).map(|_| rng_input(&mut rng)).collect();
315                let value = tree.eval(&alpha, &inputs);
316                assert!(value.is_finite(), "eval produced non-finite {value}");
317            }
318        }
319    }
320
321    /// Small uniform `usize` in `0..bound` from a seeded RNG (avoids pulling in
322    /// the distribution imports the alphabet already re-exports).
323    fn rng_usize(rng: &mut StdRng, bound: usize) -> usize {
324        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
325        let hi = bound as i32;
326        #[allow(clippy::cast_sign_loss)]
327        {
328            rng.random_range(0..hi) as usize
329        }
330    }
331
332    /// A bounded, occasionally-large input to exercise the overflow clamp.
333    fn rng_input(rng: &mut StdRng) -> f32 {
334        rng.random_range(-1.0e6f32..1.0e6f32)
335    }
336}