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}