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}