Skip to main content

rlevo_evolution/algorithms/gep/
tree.rs

1//! The decoded GEP phenotype: a level-order expression tree.
2
3use crate::function_set::{FunctionSet, Symbol};
4
5use super::alphabet::{Alphabet, SymbolKind};
6
7/// Magnitude cap applied to a diverged node value in [`ExpressionTree::eval`].
8///
9/// Sized so its square stays finite (`1e15² = 1e30 < f32::MAX ≈ 3.4e38`), even
10/// summed over a large dataset, so a clamped prediction still produces a finite
11/// — but very large — MSE. A diverged (`±Inf`) subtree therefore ranks worst
12/// rather than collapsing to a deceptively small error.
13const EVAL_CLAMP: f32 = 1e15;
14
15/// Node-value sanitizer for the GEP evaluator: `NaN → 0.0`, `±Inf → ±`
16/// [`EVAL_CLAMP`], finite values unchanged.
17///
18/// `f32::clamp` *propagates* `NaN`, so `NaN` must be handled before the clamp
19/// (mirrors the EDA `bayesian_network` finiteness convention). Unlike the
20/// shared [`finite_or_zero`](crate::function_set::finite_or_zero) rule used by
21/// CGP, this preserves the sign and magnitude of an overflowed value so a
22/// bloated subtree is penalized, not hidden (finding tree.rs §1.2).
23#[must_use]
24#[inline]
25fn finite_or_clamp(v: f32) -> f32 {
26    if v.is_nan() {
27        0.0
28    } else {
29        v.clamp(-EVAL_CLAMP, EVAL_CLAMP)
30    }
31}
32
33/// A decoded expression tree stored in level-order (breadth-first) layout.
34///
35/// The coding prefix of a chromosome decodes to this structure (see
36/// [`GepDecoder`](super::GepDecoder)). Nodes are held in BFS order, so every
37/// node's children occupy a contiguous, strictly-higher index range. That lets
38/// evaluation run as a single right-to-left sweep: when a parent is reached,
39/// its children (higher indices) have already been computed.
40#[derive(Clone, Debug)]
41pub struct ExpressionTree {
42    /// Symbols in level order; `nodes[0]` is the root.
43    nodes: Vec<Symbol>,
44    /// `arities[i]` is the number of children of node `i` (cached at decode).
45    arities: Vec<usize>,
46    /// `child_start[i]` is the index of node `i`'s first child; its children
47    /// are `child_start[i] .. child_start[i] + arities[i]`.
48    child_start: Vec<usize>,
49}
50
51impl ExpressionTree {
52    /// Builds a tree from its level-order parts.
53    ///
54    /// Intended for [`GepDecoder`](super::GepDecoder); the three vectors must
55    /// be parallel and internally consistent (BFS child assignment).
56    #[must_use]
57    pub(super) fn from_parts(
58        nodes: Vec<Symbol>,
59        arities: Vec<usize>,
60        child_start: Vec<usize>,
61    ) -> Self {
62        debug_assert_eq!(nodes.len(), arities.len());
63        debug_assert_eq!(nodes.len(), child_start.len());
64        Self {
65            nodes,
66            arities,
67            child_start,
68        }
69    }
70
71    /// Number of coding nodes (the open-reading-frame length).
72    #[must_use]
73    pub fn node_count(&self) -> usize {
74        self.nodes.len()
75    }
76
77    /// Symbols in level order (root first). Primarily for tests/inspection.
78    #[must_use]
79    pub fn nodes(&self) -> &[Symbol] {
80        &self.nodes
81    }
82
83    /// Tree depth: edges on the longest root-to-leaf path (a single-node tree
84    /// has depth 0). A bloat metric.
85    #[must_use]
86    pub fn depth(&self) -> usize {
87        let n = self.nodes.len();
88        if n == 0 {
89            return 0;
90        }
91        // Right-to-left: a node's children have higher indices, so they are
92        // resolved first.
93        let mut node_depth = vec![0usize; n];
94        for i in (0..n).rev() {
95            let arity = self.arities[i];
96            if arity == 0 {
97                node_depth[i] = 0;
98            } else {
99                let start = self.child_start[i];
100                let mut max_child = 0;
101                for k in 0..arity {
102                    max_child = max_child.max(node_depth[start + k]);
103                }
104                node_depth[i] = 1 + max_child;
105            }
106        }
107        node_depth[0]
108    }
109
110    /// Evaluates the tree on one input row.
111    ///
112    /// Variable nodes resolve to `inputs[input_index]` (missing indices read as
113    /// `0.0`); constant nodes resolve to their stored value; function nodes call
114    /// [`FunctionSet::apply`] with their
115    /// children's already-computed results, then sanitize the result via
116    /// `finite_or_clamp`: a `NaN` collapses to `0.0` (it has no meaningful
117    /// sign or magnitude), while `±Inf` clamps to `±EVAL_CLAMP` (sign
118    /// preserved). Clamping — rather than zeroing — a diverged (`±Inf`) subtree
119    /// keeps its magnitude large, so it yields a large MSE and ranks *worst*
120    /// instead of masquerading as a perfect `0.0` predictor near zero-valued
121    /// targets (GEP finding tree.rs §1.2). This differs from the CGP evaluator's
122    /// `finite_or_zero` rule, which zeros both.
123    ///
124    /// `alphabet` must be the same one the tree was decoded with.
125    #[must_use]
126    pub fn eval<F: FunctionSet>(&self, alphabet: &Alphabet<F>, inputs: &[f32]) -> f32 {
127        let n = self.nodes.len();
128        if n == 0 {
129            return 0.0;
130        }
131        let mut results = vec![0.0f32; n];
132        let max_arity = alphabet.max_arity().max(1);
133        let mut arg_buf = vec![0.0f32; max_arity];
134
135        for i in (0..n).rev() {
136            let symbol = self.nodes[i];
137            results[i] = match alphabet.classify(symbol) {
138                SymbolKind::Variable { input_index } => {
139                    inputs.get(input_index).copied().unwrap_or(0.0)
140                }
141                SymbolKind::Constant { value } => value,
142                SymbolKind::Function { .. } => {
143                    let arity = self.arities[i];
144                    let start = self.child_start[i];
145                    // For a well-formed genome the child range always fits
146                    // (`start + arity <= n`; Ferreira 2001 eq. 3.4, enforced by
147                    // the decoder's `debug_assert!`). This clamp is a defensive
148                    // guard for the documented precondition: a contract-violating
149                    // genome (built via `Symbol::from_raw`, bypassing the
150                    // head/tail rule) would otherwise slice out of bounds and
151                    // panic here. Clamping `end` degrades the malformed case to
152                    // a finite value; `apply` reads any missing tail arguments
153                    // as `0.0`. Bit-identical to `&results[start..start + arity]`
154                    // whenever the precondition holds.
155                    let end = (start + arity).min(results.len());
156                    let avail = end.saturating_sub(start);
157                    arg_buf[..avail].copy_from_slice(&results[start..end]);
158                    arg_buf[avail..arity].fill(0.0);
159                    let v = alphabet.functions.apply(symbol, &arg_buf[..arity]);
160                    finite_or_clamp(v)
161                }
162            };
163        }
164        results[0]
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::algorithms::gep::GepDecoder;
172    use crate::algorithms::gep::decode::GenotypePhenotypeMap;
173    use crate::function_set::ArithmeticFunctionSet;
174
175    fn alphabet(n_vars: usize) -> Alphabet<ArithmeticFunctionSet> {
176        Alphabet::new(ArithmeticFunctionSet, n_vars, vec![])
177    }
178
179    /// Genome `[+, x, 1]` (ids: add=0, var x=8, const-1=7) decodes to `x + 1`.
180    #[test]
181    fn evaluates_x_plus_one() {
182        let a = alphabet(1);
183        // head [0, 8, 7], tail [8] (terminals). ORF = first 3 (add needs 2
184        // children: x and const-1).
185        let genome = vec![
186            Symbol::from_raw(0),
187            Symbol::from_raw(8),
188            Symbol::from_raw(7),
189            Symbol::from_raw(8),
190        ];
191        let tree = GepDecoder.decode(&a, &genome);
192        assert_eq!(tree.node_count(), 3);
193        approx::assert_relative_eq!(tree.eval(&a, &[2.0]), 3.0, epsilon = 1e-6);
194        approx::assert_relative_eq!(tree.eval(&a, &[-5.0]), -4.0, epsilon = 1e-6);
195    }
196
197    /// `[*, x, x]` decodes to `x * x` with depth 1.
198    #[test]
199    fn evaluates_x_squared_with_depth_one() {
200        let a = alphabet(1);
201        let genome = vec![
202            Symbol::from_raw(2),
203            Symbol::from_raw(8),
204            Symbol::from_raw(8),
205            Symbol::from_raw(8),
206        ];
207        let tree = GepDecoder.decode(&a, &genome);
208        assert_eq!(tree.node_count(), 3);
209        assert_eq!(tree.depth(), 1);
210        approx::assert_relative_eq!(tree.eval(&a, &[3.0]), 9.0, epsilon = 1e-6);
211    }
212
213    /// A single terminal head decodes to a depth-0, one-node tree.
214    #[test]
215    fn single_terminal_is_leaf() {
216        let a = alphabet(1);
217        let genome = vec![
218            Symbol::from_raw(8),
219            Symbol::from_raw(8),
220            Symbol::from_raw(8),
221        ];
222        let tree = GepDecoder.decode(&a, &genome);
223        assert_eq!(tree.node_count(), 1);
224        assert_eq!(tree.depth(), 0);
225        approx::assert_relative_eq!(tree.eval(&a, &[7.0]), 7.0, epsilon = 1e-6);
226    }
227
228    /// `finite_or_clamp`: `NaN → 0.0`, `±Inf → ±EVAL_CLAMP`, finite passes.
229    #[test]
230    fn test_finite_or_clamp_zeroes_nan_and_clamps_inf() {
231        approx::assert_relative_eq!(finite_or_clamp(f32::NAN), 0.0, epsilon = 1e-6);
232        approx::assert_relative_eq!(finite_or_clamp(f32::INFINITY), EVAL_CLAMP);
233        approx::assert_relative_eq!(finite_or_clamp(f32::NEG_INFINITY), -EVAL_CLAMP);
234        approx::assert_relative_eq!(finite_or_clamp(3.5), 3.5, epsilon = 1e-6);
235    }
236
237    /// An overflowing `x * x` clamps to `EVAL_CLAMP` (not `0.0`), so a diverged
238    /// tree carries a large magnitude and is penalized (finding tree.rs §1.2).
239    #[test]
240    fn test_eval_clamps_overflow_instead_of_zeroing() {
241        let a = alphabet(1);
242        // `[*, x, x]` = x * x; x = 1e30 overflows f32 to +Inf.
243        let genome = vec![
244            Symbol::from_raw(2),
245            Symbol::from_raw(8),
246            Symbol::from_raw(8),
247            Symbol::from_raw(8),
248        ];
249        let tree = GepDecoder.decode(&a, &genome);
250        let pred = tree.eval(&a, &[1e30]);
251        approx::assert_relative_eq!(pred, EVAL_CLAMP);
252        // The squared error against a near-zero target is huge, not near-zero:
253        // the old `finite_or_zero` rule would have made `pred == 0.0` here.
254        assert!(
255            (pred - 0.1).powi(2) > 1e20,
256            "diverged prediction must yield a large error, got pred = {pred}"
257        );
258    }
259
260    // §7.1 -----------------------------------------------------------------
261
262    /// An empty tree evaluates to the inert `0.0` (no nodes to reduce).
263    #[test]
264    fn eval_of_empty_tree_is_zero() {
265        let a = alphabet(1);
266        let tree = ExpressionTree::from_parts(Vec::new(), Vec::new(), Vec::new());
267        assert_eq!(tree.node_count(), 0);
268        approx::assert_relative_eq!(tree.eval(&a, &[42.0]), 0.0, epsilon = 1e-6);
269    }
270
271    /// A variable node whose input index is out of range reads `0.0` (the
272    /// evaluator's missing-input policy), not a panic. A `+Inf` *input* still
273    /// clamps to `EVAL_CLAMP` rather than collapsing to `0.0`, so a diverged
274    /// value is penalized (matches `finite_or_clamp`).
275    #[test]
276    fn eval_variable_index_and_inf_policy() {
277        let a = alphabet(2);
278        // Single variable node reading input_index 1 (id 8 = var 0, 9 = var 1).
279        let tree = GepDecoder.decode(&a, &[Symbol::from_raw(9)]);
280        assert_eq!(tree.node_count(), 1);
281        // Out-of-range input row (len 0): missing index reads 0.0.
282        approx::assert_relative_eq!(tree.eval(&a, &[]), 0.0, epsilon = 1e-6);
283        // Present index passes through.
284        approx::assert_relative_eq!(tree.eval(&a, &[3.0, 7.0]), 7.0, epsilon = 1e-6);
285        // A raw variable leaf reads its input verbatim (no per-node clamp on a
286        // leaf), so an infinite input surfaces as-is; the clamp applies at
287        // function nodes. Feed the leaf through a `+` with a `0` constant is not
288        // available here, so assert the documented leaf policy directly.
289        assert!(tree.eval(&a, &[0.0, f32::INFINITY]).is_infinite());
290    }
291
292    // §7.2 -----------------------------------------------------------------
293
294    /// Regression for issue #147 §1.1. A contract-violating genome — one that
295    /// breaks Ferreira's head/tail rule (eq. 3.4) and so leaves an unfilled
296    /// child slot — is built here directly via `from_parts`, bypassing the
297    /// decoder's `debug_assert!`. Its child range `1..3` overruns the single
298    /// node. Before the fix, `eval` sliced `results[1..3]` out of bounds and
299    /// panicked; the defensive clamp now degrades it to a finite value.
300    #[test]
301    fn eval_does_not_panic_on_out_of_bounds_child_range() {
302        let a = alphabet(1);
303        // A lone binary `+` (id 0, arity 2) claiming children at indices 1..3,
304        // but there is only one node. `child_start = [1]`, `arities = [2]`.
305        let tree = ExpressionTree::from_parts(vec![Symbol::from_raw(0)], vec![2], vec![1]);
306        assert_eq!(tree.node_count(), 1);
307        // Missing children read as 0.0: 0.0 + 0.0 = 0.0. No panic, finite value.
308        let y = tree.eval(&a, &[5.0]);
309        assert!(y.is_finite());
310        approx::assert_relative_eq!(y, 0.0, epsilon = 1e-6);
311    }
312
313    /// A partially out-of-bounds child range (one valid child, one past the
314    /// end) copies the in-bounds child and zero-fills the rest, still finite.
315    #[test]
316    fn eval_partial_out_of_bounds_child_range() {
317        let a = alphabet(1);
318        // node 0 = `+` (arity 2) with children at 1..3; node 1 = var x (id 8).
319        // Index 2 is past the end, so the second argument zero-fills.
320        let tree = ExpressionTree::from_parts(
321            vec![Symbol::from_raw(0), Symbol::from_raw(8)],
322            vec![2, 0],
323            vec![1, 2],
324        );
325        // x + 0.0 = x.
326        let y = tree.eval(&a, &[4.0]);
327        assert!(y.is_finite());
328        approx::assert_relative_eq!(y, 4.0, epsilon = 1e-6);
329    }
330}