pub struct ExprGraph { /* private fields */ }Expand description
Arena-based expression graph with structural interning.
Identical subexpressions always return the same ExprId — this gives
automatic common subexpression elimination (CSE) for free.
Implementations§
Source§impl ExprGraph
impl ExprGraph
Sourcepub fn compile(&self, expr: ExprId) -> CompiledExpr
pub fn compile(&self, expr: ExprId) -> CompiledExpr
Compile a single expression to a closure &[f64] -> f64.
Shared subexpressions (from interning) are computed once. Dead nodes (not reachable from output) are skipped.
Examples found in repository?
261fn fitness(expr: &Expr, data: &[(f64, f64)]) -> f64 {
262 let mut g = ExprGraph::new();
263 let root = expr.to_expr(&mut g);
264 let compiled = g.compile(root);
265
266 let mut mse = 0.0;
267 for &(x, y) in data {
268 let pred = compiled(&[x]);
269 if pred.is_nan() || pred.is_infinite() {
270 return f64::INFINITY;
271 }
272 let err = pred - y;
273 mse += err * err;
274 }
275 mse /= data.len() as f64;
276
277 let penalty = 0.001 * expr.size() as f64;
278 mse + penalty
279}
280
281// --- Selection ---------------------------------------------------------------
282
283fn tournament<'a>(pop: &'a [(Expr, f64)], k: usize, rng: &mut Lcg) -> &'a Expr {
284 let mut best_idx = rng.range(pop.len());
285 for _ in 1..k {
286 let idx = rng.range(pop.len());
287 if pop[idx].1 < pop[best_idx].1 {
288 best_idx = idx;
289 }
290 }
291 &pop[best_idx].0
292}
293
294// --- Main --------------------------------------------------------------------
295
296fn main() {
297 println!("=== Symbolic Regression ===\n");
298 println!("target: f(x) = x^2 * sin(x)\n");
299
300 let mut rng = Lcg::new(42);
301 let data = generate_data(50, &mut rng);
302
303 const POP_SIZE: usize = 200;
304 const GENERATIONS: usize = 100;
305 const TOURNAMENT_K: usize = 5;
306
307 // Initialize population
308 let mut population: Vec<(Expr, f64)> = (0..POP_SIZE)
309 .map(|_| {
310 let expr = random_expr(4, &mut rng);
311 let fit = fitness(&expr, &data);
312 (expr, fit)
313 })
314 .collect();
315
316 let mut best_expr: Option<Expr> = None;
317 let mut best_fitness = f64::INFINITY;
318
319 for gen in 0..GENERATIONS {
320 // Track best
321 for (expr, fit) in &population {
322 if *fit < best_fitness {
323 best_fitness = *fit;
324 best_expr = Some(expr.clone());
325 }
326 }
327
328 if (gen + 1) % 10 == 0 || gen == 0 {
329 let b = best_expr.as_ref().unwrap();
330 println!(
331 "gen {:>3}: best fitness = {:.6} nodes = {:>3} expr = {}",
332 gen + 1,
333 best_fitness,
334 b.size(),
335 b.format(),
336 );
337 }
338
339 // Build next generation
340 let mut next_pop = Vec::with_capacity(POP_SIZE);
341
342 // Elitism: keep top 5
343 let mut indices: Vec<usize> = (0..population.len()).collect();
344 indices.sort_by(|&a, &b| population[a].1.partial_cmp(&population[b].1).unwrap());
345 for &i in indices.iter().take(5) {
346 next_pop.push(population[i].clone());
347 }
348
349 // Fill rest via mutation
350 while next_pop.len() < POP_SIZE {
351 let parent = tournament(&population, TOURNAMENT_K, &mut rng);
352 let child = match rng.range(10) {
353 0..=3 => mutate_grow(parent, &mut rng),
354 4..=6 => mutate_point(parent, &mut rng),
355 7..=8 => mutate_simplify(parent),
356 _ => random_expr(4, &mut rng), // fresh blood
357 };
358
359 // Skip overly large expressions
360 if child.size() > 50 {
361 continue;
362 }
363
364 let fit = fitness(&child, &data);
365 next_pop.push((child, fit));
366 }
367
368 population = next_pop;
369 }
370
371 // Final results
372 println!();
373 let best = best_expr.unwrap();
374 let mut g = ExprGraph::new();
375 let root = best.to_expr(&mut g);
376 let simplified = g.simplify(root);
377 let expr_str = g.fmt_expr(simplified);
378 println!("best expression: {}", expr_str);
379 println!("best fitness: {:.6}", best_fitness);
380
381 // Verify on test points
382 println!("\nverification:");
383 let compiled = g.compile(simplified);
384 for &x in &[-2.0, -1.0, 0.0, 1.0, 2.0] {
385 let pred = compiled(&[x]);
386 let exact = target(x);
387 println!(
388 " f({:>5.1}) = {:>8.4} (predicted: {:>8.4}, error: {:>8.4})",
389 x,
390 exact,
391 pred,
392 (pred - exact).abs()
393 );
394 }
395
396 // Symbolic derivative via ExprGraph
397 let dx = g.diff(simplified, 0);
398 let dx = g.simplify(dx);
399 println!(
400 "\nsymbolic derivative: d/dx [{}] = {}",
401 expr_str,
402 g.fmt_expr(dx)
403 );
404}Sourcepub fn compile_many(&self, exprs: &[ExprId]) -> CompiledMany
pub fn compile_many(&self, exprs: &[ExprId]) -> CompiledMany
Compile multiple output expressions to a single closure.
Writes results into the output slice.
Source§impl ExprGraph
impl ExprGraph
Sourcepub fn diff(&mut self, expr: ExprId, var: u16) -> ExprId
pub fn diff(&mut self, expr: ExprId, var: u16) -> ExprId
Differentiate expr with respect to Var(var).
Returns a new ExprId in the same graph. Uses memoization to avoid recomputing derivatives of shared subexpressions.
Examples found in repository?
296fn main() {
297 println!("=== Symbolic Regression ===\n");
298 println!("target: f(x) = x^2 * sin(x)\n");
299
300 let mut rng = Lcg::new(42);
301 let data = generate_data(50, &mut rng);
302
303 const POP_SIZE: usize = 200;
304 const GENERATIONS: usize = 100;
305 const TOURNAMENT_K: usize = 5;
306
307 // Initialize population
308 let mut population: Vec<(Expr, f64)> = (0..POP_SIZE)
309 .map(|_| {
310 let expr = random_expr(4, &mut rng);
311 let fit = fitness(&expr, &data);
312 (expr, fit)
313 })
314 .collect();
315
316 let mut best_expr: Option<Expr> = None;
317 let mut best_fitness = f64::INFINITY;
318
319 for gen in 0..GENERATIONS {
320 // Track best
321 for (expr, fit) in &population {
322 if *fit < best_fitness {
323 best_fitness = *fit;
324 best_expr = Some(expr.clone());
325 }
326 }
327
328 if (gen + 1) % 10 == 0 || gen == 0 {
329 let b = best_expr.as_ref().unwrap();
330 println!(
331 "gen {:>3}: best fitness = {:.6} nodes = {:>3} expr = {}",
332 gen + 1,
333 best_fitness,
334 b.size(),
335 b.format(),
336 );
337 }
338
339 // Build next generation
340 let mut next_pop = Vec::with_capacity(POP_SIZE);
341
342 // Elitism: keep top 5
343 let mut indices: Vec<usize> = (0..population.len()).collect();
344 indices.sort_by(|&a, &b| population[a].1.partial_cmp(&population[b].1).unwrap());
345 for &i in indices.iter().take(5) {
346 next_pop.push(population[i].clone());
347 }
348
349 // Fill rest via mutation
350 while next_pop.len() < POP_SIZE {
351 let parent = tournament(&population, TOURNAMENT_K, &mut rng);
352 let child = match rng.range(10) {
353 0..=3 => mutate_grow(parent, &mut rng),
354 4..=6 => mutate_point(parent, &mut rng),
355 7..=8 => mutate_simplify(parent),
356 _ => random_expr(4, &mut rng), // fresh blood
357 };
358
359 // Skip overly large expressions
360 if child.size() > 50 {
361 continue;
362 }
363
364 let fit = fitness(&child, &data);
365 next_pop.push((child, fit));
366 }
367
368 population = next_pop;
369 }
370
371 // Final results
372 println!();
373 let best = best_expr.unwrap();
374 let mut g = ExprGraph::new();
375 let root = best.to_expr(&mut g);
376 let simplified = g.simplify(root);
377 let expr_str = g.fmt_expr(simplified);
378 println!("best expression: {}", expr_str);
379 println!("best fitness: {:.6}", best_fitness);
380
381 // Verify on test points
382 println!("\nverification:");
383 let compiled = g.compile(simplified);
384 for &x in &[-2.0, -1.0, 0.0, 1.0, 2.0] {
385 let pred = compiled(&[x]);
386 let exact = target(x);
387 println!(
388 " f({:>5.1}) = {:>8.4} (predicted: {:>8.4}, error: {:>8.4})",
389 x,
390 exact,
391 pred,
392 (pred - exact).abs()
393 );
394 }
395
396 // Symbolic derivative via ExprGraph
397 let dx = g.diff(simplified, 0);
398 let dx = g.simplify(dx);
399 println!(
400 "\nsymbolic derivative: d/dx [{}] = {}",
401 expr_str,
402 g.fmt_expr(dx)
403 );
404}Source§impl ExprGraph
impl ExprGraph
Sourcepub fn fmt_expr(&self, expr: ExprId) -> String
pub fn fmt_expr(&self, expr: ExprId) -> String
Format an expression as a human-readable string.
Examples found in repository?
84 fn format(&self) -> String {
85 let mut g = ExprGraph::new();
86 let root = self.to_expr(&mut g);
87 g.fmt_expr(root)
88 }
89
90 /// Evaluate at a point using ExprGraph's compiled eval.
91 fn eval_at(&self, x: f64) -> f64 {
92 let mut g = ExprGraph::new();
93 let root = self.to_expr(&mut g);
94 g.eval(root, &[x])
95 }
96}
97
98// --- Random expression generation -------------------------------------------
99
100fn random_expr(depth: usize, rng: &mut Lcg) -> Expr {
101 if depth == 0 || (depth < 3 && rng.uniform() < 0.3) {
102 return match rng.range(3) {
103 0 => Expr::X,
104 1 => Expr::Lit(
105 (rng.uniform() * 4.0 - 2.0) * 10.0_f64.powi(-((rng.uniform() * 2.0) as i32)),
106 ),
107 _ => Expr::X,
108 };
109 }
110
111 match rng.range(5) {
112 0 => Expr::Add(
113 Box::new(random_expr(depth - 1, rng)),
114 Box::new(random_expr(depth - 1, rng)),
115 ),
116 1 | 2 => Expr::Mul(
117 Box::new(random_expr(depth - 1, rng)),
118 Box::new(random_expr(depth - 1, rng)),
119 ),
120 3 => Expr::Sin(Box::new(random_expr(depth - 1, rng))),
121 _ => Expr::Neg(Box::new(random_expr(depth - 1, rng))),
122 }
123}
124
125// --- Mutation operators ------------------------------------------------------
126
127/// Grow mutation: replace a random subtree with a new random one.
128fn mutate_grow(expr: &Expr, rng: &mut Lcg) -> Expr {
129 let size = expr.size();
130 let target = rng.range(size);
131 grow_at(expr, target, &mut 0, rng)
132}
133
134fn grow_at(expr: &Expr, target: usize, counter: &mut usize, rng: &mut Lcg) -> Expr {
135 if *counter == target {
136 *counter += expr.size(); // skip subtree
137 return random_expr(3, rng);
138 }
139 *counter += 1;
140 match expr {
141 Expr::X => Expr::X,
142 Expr::Lit(v) => Expr::Lit(*v),
143 Expr::Add(a, b) => Expr::Add(
144 Box::new(grow_at(a, target, counter, rng)),
145 Box::new(grow_at(b, target, counter, rng)),
146 ),
147 Expr::Mul(a, b) => Expr::Mul(
148 Box::new(grow_at(a, target, counter, rng)),
149 Box::new(grow_at(b, target, counter, rng)),
150 ),
151 Expr::Sin(a) => Expr::Sin(Box::new(grow_at(a, target, counter, rng))),
152 Expr::Neg(a) => Expr::Neg(Box::new(grow_at(a, target, counter, rng))),
153 }
154}
155
156/// Point mutation: change a single node's operation.
157fn mutate_point(expr: &Expr, rng: &mut Lcg) -> Expr {
158 let size = expr.size();
159 let target = rng.range(size);
160 point_at(expr, target, &mut 0, rng)
161}
162
163fn point_at(expr: &Expr, target: usize, counter: &mut usize, rng: &mut Lcg) -> Expr {
164 if *counter == target {
165 *counter += 1;
166 return match expr {
167 Expr::X => Expr::Lit(rng.uniform() * 2.0 - 1.0),
168 Expr::Lit(_) => Expr::X,
169 Expr::Add(a, b) => Expr::Mul(a.clone(), b.clone()),
170 Expr::Mul(a, b) => Expr::Add(a.clone(), b.clone()),
171 Expr::Sin(a) => Expr::Neg(a.clone()),
172 Expr::Neg(a) => Expr::Sin(a.clone()),
173 };
174 }
175 *counter += 1;
176 match expr {
177 Expr::X => Expr::X,
178 Expr::Lit(v) => Expr::Lit(*v),
179 Expr::Add(a, b) => Expr::Add(
180 Box::new(point_at(a, target, counter, rng)),
181 Box::new(point_at(b, target, counter, rng)),
182 ),
183 Expr::Mul(a, b) => Expr::Mul(
184 Box::new(point_at(a, target, counter, rng)),
185 Box::new(point_at(b, target, counter, rng)),
186 ),
187 Expr::Sin(a) => Expr::Sin(Box::new(point_at(a, target, counter, rng))),
188 Expr::Neg(a) => Expr::Neg(Box::new(point_at(a, target, counter, rng))),
189 }
190}
191
192/// Simplify mutation: convert to ExprGraph, simplify, convert back.
193/// Falls back to identity if conversion back would be complex.
194fn mutate_simplify(expr: &Expr) -> Expr {
195 let mut g = ExprGraph::new();
196 let root = expr.to_expr(&mut g);
197 let simplified = g.simplify(root);
198 let s = g.fmt_expr(simplified);
199
200 // Quick check: if simplified form is much shorter, it's better
201 let orig = expr.format();
202 if s.len() < orig.len() {
203 // Re-evaluate to confirm it still works
204 let test = g.eval::<f64>(simplified, &[1.0]);
205 let orig_test = expr.eval_at(1.0);
206 if (test - orig_test).abs() < 1e-10 || (test.is_nan() && orig_test.is_nan()) {
207 // Return a new tree built from the simplified graph
208 return expr_from_graph(&g, simplified);
209 }
210 }
211 expr.clone()
212}
213
214/// Reconstruct an Expr tree from an ExprGraph node.
215fn expr_from_graph(g: &ExprGraph, id: ExprId) -> Expr {
216 match g.node(id) {
217 tang_expr::node::Node::Var(_) => Expr::X,
218 tang_expr::node::Node::Lit(bits) => {
219 let v = f64::from_bits(bits);
220 if v == 0.0 {
221 Expr::Lit(0.0)
222 } else {
223 Expr::Lit(v)
224 }
225 }
226 tang_expr::node::Node::Add(a, b) => Expr::Add(
227 Box::new(expr_from_graph(g, a)),
228 Box::new(expr_from_graph(g, b)),
229 ),
230 tang_expr::node::Node::Mul(a, b) => Expr::Mul(
231 Box::new(expr_from_graph(g, a)),
232 Box::new(expr_from_graph(g, b)),
233 ),
234 tang_expr::node::Node::Neg(a) => Expr::Neg(Box::new(expr_from_graph(g, a))),
235 tang_expr::node::Node::Sin(a) => Expr::Sin(Box::new(expr_from_graph(g, a))),
236 // For operations we don't represent in our AST, just evaluate as literal
237 _ => {
238 let v = g.eval::<f64>(id, &[1.0]); // fallback
239 Expr::Lit(v)
240 }
241 }
242}
243
244// --- Fitness evaluation ------------------------------------------------------
245
246fn target(x: f64) -> f64 {
247 x * x * x.sin()
248}
249
250fn generate_data(n: usize, rng: &mut Lcg) -> Vec<(f64, f64)> {
251 (0..n)
252 .map(|i| {
253 let x = -3.0 + 6.0 * i as f64 / (n - 1) as f64;
254 let noise = (rng.uniform() - 0.5) * 0.01;
255 (x, target(x) + noise)
256 })
257 .collect()
258}
259
260/// Evaluate fitness: MSE + complexity penalty.
261fn fitness(expr: &Expr, data: &[(f64, f64)]) -> f64 {
262 let mut g = ExprGraph::new();
263 let root = expr.to_expr(&mut g);
264 let compiled = g.compile(root);
265
266 let mut mse = 0.0;
267 for &(x, y) in data {
268 let pred = compiled(&[x]);
269 if pred.is_nan() || pred.is_infinite() {
270 return f64::INFINITY;
271 }
272 let err = pred - y;
273 mse += err * err;
274 }
275 mse /= data.len() as f64;
276
277 let penalty = 0.001 * expr.size() as f64;
278 mse + penalty
279}
280
281// --- Selection ---------------------------------------------------------------
282
283fn tournament<'a>(pop: &'a [(Expr, f64)], k: usize, rng: &mut Lcg) -> &'a Expr {
284 let mut best_idx = rng.range(pop.len());
285 for _ in 1..k {
286 let idx = rng.range(pop.len());
287 if pop[idx].1 < pop[best_idx].1 {
288 best_idx = idx;
289 }
290 }
291 &pop[best_idx].0
292}
293
294// --- Main --------------------------------------------------------------------
295
296fn main() {
297 println!("=== Symbolic Regression ===\n");
298 println!("target: f(x) = x^2 * sin(x)\n");
299
300 let mut rng = Lcg::new(42);
301 let data = generate_data(50, &mut rng);
302
303 const POP_SIZE: usize = 200;
304 const GENERATIONS: usize = 100;
305 const TOURNAMENT_K: usize = 5;
306
307 // Initialize population
308 let mut population: Vec<(Expr, f64)> = (0..POP_SIZE)
309 .map(|_| {
310 let expr = random_expr(4, &mut rng);
311 let fit = fitness(&expr, &data);
312 (expr, fit)
313 })
314 .collect();
315
316 let mut best_expr: Option<Expr> = None;
317 let mut best_fitness = f64::INFINITY;
318
319 for gen in 0..GENERATIONS {
320 // Track best
321 for (expr, fit) in &population {
322 if *fit < best_fitness {
323 best_fitness = *fit;
324 best_expr = Some(expr.clone());
325 }
326 }
327
328 if (gen + 1) % 10 == 0 || gen == 0 {
329 let b = best_expr.as_ref().unwrap();
330 println!(
331 "gen {:>3}: best fitness = {:.6} nodes = {:>3} expr = {}",
332 gen + 1,
333 best_fitness,
334 b.size(),
335 b.format(),
336 );
337 }
338
339 // Build next generation
340 let mut next_pop = Vec::with_capacity(POP_SIZE);
341
342 // Elitism: keep top 5
343 let mut indices: Vec<usize> = (0..population.len()).collect();
344 indices.sort_by(|&a, &b| population[a].1.partial_cmp(&population[b].1).unwrap());
345 for &i in indices.iter().take(5) {
346 next_pop.push(population[i].clone());
347 }
348
349 // Fill rest via mutation
350 while next_pop.len() < POP_SIZE {
351 let parent = tournament(&population, TOURNAMENT_K, &mut rng);
352 let child = match rng.range(10) {
353 0..=3 => mutate_grow(parent, &mut rng),
354 4..=6 => mutate_point(parent, &mut rng),
355 7..=8 => mutate_simplify(parent),
356 _ => random_expr(4, &mut rng), // fresh blood
357 };
358
359 // Skip overly large expressions
360 if child.size() > 50 {
361 continue;
362 }
363
364 let fit = fitness(&child, &data);
365 next_pop.push((child, fit));
366 }
367
368 population = next_pop;
369 }
370
371 // Final results
372 println!();
373 let best = best_expr.unwrap();
374 let mut g = ExprGraph::new();
375 let root = best.to_expr(&mut g);
376 let simplified = g.simplify(root);
377 let expr_str = g.fmt_expr(simplified);
378 println!("best expression: {}", expr_str);
379 println!("best fitness: {:.6}", best_fitness);
380
381 // Verify on test points
382 println!("\nverification:");
383 let compiled = g.compile(simplified);
384 for &x in &[-2.0, -1.0, 0.0, 1.0, 2.0] {
385 let pred = compiled(&[x]);
386 let exact = target(x);
387 println!(
388 " f({:>5.1}) = {:>8.4} (predicted: {:>8.4}, error: {:>8.4})",
389 x,
390 exact,
391 pred,
392 (pred - exact).abs()
393 );
394 }
395
396 // Symbolic derivative via ExprGraph
397 let dx = g.diff(simplified, 0);
398 let dx = g.simplify(dx);
399 println!(
400 "\nsymbolic derivative: d/dx [{}] = {}",
401 expr_str,
402 g.fmt_expr(dx)
403 );
404}Source§impl ExprGraph
impl ExprGraph
Sourcepub fn eval<S: Scalar>(&self, expr: ExprId, inputs: &[S]) -> S
pub fn eval<S: Scalar>(&self, expr: ExprId, inputs: &[S]) -> S
Evaluate an expression with concrete scalar inputs.
inputs[n] provides the value for Var(n). Walks the graph in
topological order (which is just index order, since children are
always created before parents).
Examples found in repository?
91 fn eval_at(&self, x: f64) -> f64 {
92 let mut g = ExprGraph::new();
93 let root = self.to_expr(&mut g);
94 g.eval(root, &[x])
95 }
96}
97
98// --- Random expression generation -------------------------------------------
99
100fn random_expr(depth: usize, rng: &mut Lcg) -> Expr {
101 if depth == 0 || (depth < 3 && rng.uniform() < 0.3) {
102 return match rng.range(3) {
103 0 => Expr::X,
104 1 => Expr::Lit(
105 (rng.uniform() * 4.0 - 2.0) * 10.0_f64.powi(-((rng.uniform() * 2.0) as i32)),
106 ),
107 _ => Expr::X,
108 };
109 }
110
111 match rng.range(5) {
112 0 => Expr::Add(
113 Box::new(random_expr(depth - 1, rng)),
114 Box::new(random_expr(depth - 1, rng)),
115 ),
116 1 | 2 => Expr::Mul(
117 Box::new(random_expr(depth - 1, rng)),
118 Box::new(random_expr(depth - 1, rng)),
119 ),
120 3 => Expr::Sin(Box::new(random_expr(depth - 1, rng))),
121 _ => Expr::Neg(Box::new(random_expr(depth - 1, rng))),
122 }
123}
124
125// --- Mutation operators ------------------------------------------------------
126
127/// Grow mutation: replace a random subtree with a new random one.
128fn mutate_grow(expr: &Expr, rng: &mut Lcg) -> Expr {
129 let size = expr.size();
130 let target = rng.range(size);
131 grow_at(expr, target, &mut 0, rng)
132}
133
134fn grow_at(expr: &Expr, target: usize, counter: &mut usize, rng: &mut Lcg) -> Expr {
135 if *counter == target {
136 *counter += expr.size(); // skip subtree
137 return random_expr(3, rng);
138 }
139 *counter += 1;
140 match expr {
141 Expr::X => Expr::X,
142 Expr::Lit(v) => Expr::Lit(*v),
143 Expr::Add(a, b) => Expr::Add(
144 Box::new(grow_at(a, target, counter, rng)),
145 Box::new(grow_at(b, target, counter, rng)),
146 ),
147 Expr::Mul(a, b) => Expr::Mul(
148 Box::new(grow_at(a, target, counter, rng)),
149 Box::new(grow_at(b, target, counter, rng)),
150 ),
151 Expr::Sin(a) => Expr::Sin(Box::new(grow_at(a, target, counter, rng))),
152 Expr::Neg(a) => Expr::Neg(Box::new(grow_at(a, target, counter, rng))),
153 }
154}
155
156/// Point mutation: change a single node's operation.
157fn mutate_point(expr: &Expr, rng: &mut Lcg) -> Expr {
158 let size = expr.size();
159 let target = rng.range(size);
160 point_at(expr, target, &mut 0, rng)
161}
162
163fn point_at(expr: &Expr, target: usize, counter: &mut usize, rng: &mut Lcg) -> Expr {
164 if *counter == target {
165 *counter += 1;
166 return match expr {
167 Expr::X => Expr::Lit(rng.uniform() * 2.0 - 1.0),
168 Expr::Lit(_) => Expr::X,
169 Expr::Add(a, b) => Expr::Mul(a.clone(), b.clone()),
170 Expr::Mul(a, b) => Expr::Add(a.clone(), b.clone()),
171 Expr::Sin(a) => Expr::Neg(a.clone()),
172 Expr::Neg(a) => Expr::Sin(a.clone()),
173 };
174 }
175 *counter += 1;
176 match expr {
177 Expr::X => Expr::X,
178 Expr::Lit(v) => Expr::Lit(*v),
179 Expr::Add(a, b) => Expr::Add(
180 Box::new(point_at(a, target, counter, rng)),
181 Box::new(point_at(b, target, counter, rng)),
182 ),
183 Expr::Mul(a, b) => Expr::Mul(
184 Box::new(point_at(a, target, counter, rng)),
185 Box::new(point_at(b, target, counter, rng)),
186 ),
187 Expr::Sin(a) => Expr::Sin(Box::new(point_at(a, target, counter, rng))),
188 Expr::Neg(a) => Expr::Neg(Box::new(point_at(a, target, counter, rng))),
189 }
190}
191
192/// Simplify mutation: convert to ExprGraph, simplify, convert back.
193/// Falls back to identity if conversion back would be complex.
194fn mutate_simplify(expr: &Expr) -> Expr {
195 let mut g = ExprGraph::new();
196 let root = expr.to_expr(&mut g);
197 let simplified = g.simplify(root);
198 let s = g.fmt_expr(simplified);
199
200 // Quick check: if simplified form is much shorter, it's better
201 let orig = expr.format();
202 if s.len() < orig.len() {
203 // Re-evaluate to confirm it still works
204 let test = g.eval::<f64>(simplified, &[1.0]);
205 let orig_test = expr.eval_at(1.0);
206 if (test - orig_test).abs() < 1e-10 || (test.is_nan() && orig_test.is_nan()) {
207 // Return a new tree built from the simplified graph
208 return expr_from_graph(&g, simplified);
209 }
210 }
211 expr.clone()
212}
213
214/// Reconstruct an Expr tree from an ExprGraph node.
215fn expr_from_graph(g: &ExprGraph, id: ExprId) -> Expr {
216 match g.node(id) {
217 tang_expr::node::Node::Var(_) => Expr::X,
218 tang_expr::node::Node::Lit(bits) => {
219 let v = f64::from_bits(bits);
220 if v == 0.0 {
221 Expr::Lit(0.0)
222 } else {
223 Expr::Lit(v)
224 }
225 }
226 tang_expr::node::Node::Add(a, b) => Expr::Add(
227 Box::new(expr_from_graph(g, a)),
228 Box::new(expr_from_graph(g, b)),
229 ),
230 tang_expr::node::Node::Mul(a, b) => Expr::Mul(
231 Box::new(expr_from_graph(g, a)),
232 Box::new(expr_from_graph(g, b)),
233 ),
234 tang_expr::node::Node::Neg(a) => Expr::Neg(Box::new(expr_from_graph(g, a))),
235 tang_expr::node::Node::Sin(a) => Expr::Sin(Box::new(expr_from_graph(g, a))),
236 // For operations we don't represent in our AST, just evaluate as literal
237 _ => {
238 let v = g.eval::<f64>(id, &[1.0]); // fallback
239 Expr::Lit(v)
240 }
241 }
242}Source§impl ExprGraph
impl ExprGraph
Sourcepub fn new() -> Self
pub fn new() -> Self
Create a new graph pre-populated with ZERO, ONE, TWO.
Examples found in repository?
84 fn format(&self) -> String {
85 let mut g = ExprGraph::new();
86 let root = self.to_expr(&mut g);
87 g.fmt_expr(root)
88 }
89
90 /// Evaluate at a point using ExprGraph's compiled eval.
91 fn eval_at(&self, x: f64) -> f64 {
92 let mut g = ExprGraph::new();
93 let root = self.to_expr(&mut g);
94 g.eval(root, &[x])
95 }
96}
97
98// --- Random expression generation -------------------------------------------
99
100fn random_expr(depth: usize, rng: &mut Lcg) -> Expr {
101 if depth == 0 || (depth < 3 && rng.uniform() < 0.3) {
102 return match rng.range(3) {
103 0 => Expr::X,
104 1 => Expr::Lit(
105 (rng.uniform() * 4.0 - 2.0) * 10.0_f64.powi(-((rng.uniform() * 2.0) as i32)),
106 ),
107 _ => Expr::X,
108 };
109 }
110
111 match rng.range(5) {
112 0 => Expr::Add(
113 Box::new(random_expr(depth - 1, rng)),
114 Box::new(random_expr(depth - 1, rng)),
115 ),
116 1 | 2 => Expr::Mul(
117 Box::new(random_expr(depth - 1, rng)),
118 Box::new(random_expr(depth - 1, rng)),
119 ),
120 3 => Expr::Sin(Box::new(random_expr(depth - 1, rng))),
121 _ => Expr::Neg(Box::new(random_expr(depth - 1, rng))),
122 }
123}
124
125// --- Mutation operators ------------------------------------------------------
126
127/// Grow mutation: replace a random subtree with a new random one.
128fn mutate_grow(expr: &Expr, rng: &mut Lcg) -> Expr {
129 let size = expr.size();
130 let target = rng.range(size);
131 grow_at(expr, target, &mut 0, rng)
132}
133
134fn grow_at(expr: &Expr, target: usize, counter: &mut usize, rng: &mut Lcg) -> Expr {
135 if *counter == target {
136 *counter += expr.size(); // skip subtree
137 return random_expr(3, rng);
138 }
139 *counter += 1;
140 match expr {
141 Expr::X => Expr::X,
142 Expr::Lit(v) => Expr::Lit(*v),
143 Expr::Add(a, b) => Expr::Add(
144 Box::new(grow_at(a, target, counter, rng)),
145 Box::new(grow_at(b, target, counter, rng)),
146 ),
147 Expr::Mul(a, b) => Expr::Mul(
148 Box::new(grow_at(a, target, counter, rng)),
149 Box::new(grow_at(b, target, counter, rng)),
150 ),
151 Expr::Sin(a) => Expr::Sin(Box::new(grow_at(a, target, counter, rng))),
152 Expr::Neg(a) => Expr::Neg(Box::new(grow_at(a, target, counter, rng))),
153 }
154}
155
156/// Point mutation: change a single node's operation.
157fn mutate_point(expr: &Expr, rng: &mut Lcg) -> Expr {
158 let size = expr.size();
159 let target = rng.range(size);
160 point_at(expr, target, &mut 0, rng)
161}
162
163fn point_at(expr: &Expr, target: usize, counter: &mut usize, rng: &mut Lcg) -> Expr {
164 if *counter == target {
165 *counter += 1;
166 return match expr {
167 Expr::X => Expr::Lit(rng.uniform() * 2.0 - 1.0),
168 Expr::Lit(_) => Expr::X,
169 Expr::Add(a, b) => Expr::Mul(a.clone(), b.clone()),
170 Expr::Mul(a, b) => Expr::Add(a.clone(), b.clone()),
171 Expr::Sin(a) => Expr::Neg(a.clone()),
172 Expr::Neg(a) => Expr::Sin(a.clone()),
173 };
174 }
175 *counter += 1;
176 match expr {
177 Expr::X => Expr::X,
178 Expr::Lit(v) => Expr::Lit(*v),
179 Expr::Add(a, b) => Expr::Add(
180 Box::new(point_at(a, target, counter, rng)),
181 Box::new(point_at(b, target, counter, rng)),
182 ),
183 Expr::Mul(a, b) => Expr::Mul(
184 Box::new(point_at(a, target, counter, rng)),
185 Box::new(point_at(b, target, counter, rng)),
186 ),
187 Expr::Sin(a) => Expr::Sin(Box::new(point_at(a, target, counter, rng))),
188 Expr::Neg(a) => Expr::Neg(Box::new(point_at(a, target, counter, rng))),
189 }
190}
191
192/// Simplify mutation: convert to ExprGraph, simplify, convert back.
193/// Falls back to identity if conversion back would be complex.
194fn mutate_simplify(expr: &Expr) -> Expr {
195 let mut g = ExprGraph::new();
196 let root = expr.to_expr(&mut g);
197 let simplified = g.simplify(root);
198 let s = g.fmt_expr(simplified);
199
200 // Quick check: if simplified form is much shorter, it's better
201 let orig = expr.format();
202 if s.len() < orig.len() {
203 // Re-evaluate to confirm it still works
204 let test = g.eval::<f64>(simplified, &[1.0]);
205 let orig_test = expr.eval_at(1.0);
206 if (test - orig_test).abs() < 1e-10 || (test.is_nan() && orig_test.is_nan()) {
207 // Return a new tree built from the simplified graph
208 return expr_from_graph(&g, simplified);
209 }
210 }
211 expr.clone()
212}
213
214/// Reconstruct an Expr tree from an ExprGraph node.
215fn expr_from_graph(g: &ExprGraph, id: ExprId) -> Expr {
216 match g.node(id) {
217 tang_expr::node::Node::Var(_) => Expr::X,
218 tang_expr::node::Node::Lit(bits) => {
219 let v = f64::from_bits(bits);
220 if v == 0.0 {
221 Expr::Lit(0.0)
222 } else {
223 Expr::Lit(v)
224 }
225 }
226 tang_expr::node::Node::Add(a, b) => Expr::Add(
227 Box::new(expr_from_graph(g, a)),
228 Box::new(expr_from_graph(g, b)),
229 ),
230 tang_expr::node::Node::Mul(a, b) => Expr::Mul(
231 Box::new(expr_from_graph(g, a)),
232 Box::new(expr_from_graph(g, b)),
233 ),
234 tang_expr::node::Node::Neg(a) => Expr::Neg(Box::new(expr_from_graph(g, a))),
235 tang_expr::node::Node::Sin(a) => Expr::Sin(Box::new(expr_from_graph(g, a))),
236 // For operations we don't represent in our AST, just evaluate as literal
237 _ => {
238 let v = g.eval::<f64>(id, &[1.0]); // fallback
239 Expr::Lit(v)
240 }
241 }
242}
243
244// --- Fitness evaluation ------------------------------------------------------
245
246fn target(x: f64) -> f64 {
247 x * x * x.sin()
248}
249
250fn generate_data(n: usize, rng: &mut Lcg) -> Vec<(f64, f64)> {
251 (0..n)
252 .map(|i| {
253 let x = -3.0 + 6.0 * i as f64 / (n - 1) as f64;
254 let noise = (rng.uniform() - 0.5) * 0.01;
255 (x, target(x) + noise)
256 })
257 .collect()
258}
259
260/// Evaluate fitness: MSE + complexity penalty.
261fn fitness(expr: &Expr, data: &[(f64, f64)]) -> f64 {
262 let mut g = ExprGraph::new();
263 let root = expr.to_expr(&mut g);
264 let compiled = g.compile(root);
265
266 let mut mse = 0.0;
267 for &(x, y) in data {
268 let pred = compiled(&[x]);
269 if pred.is_nan() || pred.is_infinite() {
270 return f64::INFINITY;
271 }
272 let err = pred - y;
273 mse += err * err;
274 }
275 mse /= data.len() as f64;
276
277 let penalty = 0.001 * expr.size() as f64;
278 mse + penalty
279}
280
281// --- Selection ---------------------------------------------------------------
282
283fn tournament<'a>(pop: &'a [(Expr, f64)], k: usize, rng: &mut Lcg) -> &'a Expr {
284 let mut best_idx = rng.range(pop.len());
285 for _ in 1..k {
286 let idx = rng.range(pop.len());
287 if pop[idx].1 < pop[best_idx].1 {
288 best_idx = idx;
289 }
290 }
291 &pop[best_idx].0
292}
293
294// --- Main --------------------------------------------------------------------
295
296fn main() {
297 println!("=== Symbolic Regression ===\n");
298 println!("target: f(x) = x^2 * sin(x)\n");
299
300 let mut rng = Lcg::new(42);
301 let data = generate_data(50, &mut rng);
302
303 const POP_SIZE: usize = 200;
304 const GENERATIONS: usize = 100;
305 const TOURNAMENT_K: usize = 5;
306
307 // Initialize population
308 let mut population: Vec<(Expr, f64)> = (0..POP_SIZE)
309 .map(|_| {
310 let expr = random_expr(4, &mut rng);
311 let fit = fitness(&expr, &data);
312 (expr, fit)
313 })
314 .collect();
315
316 let mut best_expr: Option<Expr> = None;
317 let mut best_fitness = f64::INFINITY;
318
319 for gen in 0..GENERATIONS {
320 // Track best
321 for (expr, fit) in &population {
322 if *fit < best_fitness {
323 best_fitness = *fit;
324 best_expr = Some(expr.clone());
325 }
326 }
327
328 if (gen + 1) % 10 == 0 || gen == 0 {
329 let b = best_expr.as_ref().unwrap();
330 println!(
331 "gen {:>3}: best fitness = {:.6} nodes = {:>3} expr = {}",
332 gen + 1,
333 best_fitness,
334 b.size(),
335 b.format(),
336 );
337 }
338
339 // Build next generation
340 let mut next_pop = Vec::with_capacity(POP_SIZE);
341
342 // Elitism: keep top 5
343 let mut indices: Vec<usize> = (0..population.len()).collect();
344 indices.sort_by(|&a, &b| population[a].1.partial_cmp(&population[b].1).unwrap());
345 for &i in indices.iter().take(5) {
346 next_pop.push(population[i].clone());
347 }
348
349 // Fill rest via mutation
350 while next_pop.len() < POP_SIZE {
351 let parent = tournament(&population, TOURNAMENT_K, &mut rng);
352 let child = match rng.range(10) {
353 0..=3 => mutate_grow(parent, &mut rng),
354 4..=6 => mutate_point(parent, &mut rng),
355 7..=8 => mutate_simplify(parent),
356 _ => random_expr(4, &mut rng), // fresh blood
357 };
358
359 // Skip overly large expressions
360 if child.size() > 50 {
361 continue;
362 }
363
364 let fit = fitness(&child, &data);
365 next_pop.push((child, fit));
366 }
367
368 population = next_pop;
369 }
370
371 // Final results
372 println!();
373 let best = best_expr.unwrap();
374 let mut g = ExprGraph::new();
375 let root = best.to_expr(&mut g);
376 let simplified = g.simplify(root);
377 let expr_str = g.fmt_expr(simplified);
378 println!("best expression: {}", expr_str);
379 println!("best fitness: {:.6}", best_fitness);
380
381 // Verify on test points
382 println!("\nverification:");
383 let compiled = g.compile(simplified);
384 for &x in &[-2.0, -1.0, 0.0, 1.0, 2.0] {
385 let pred = compiled(&[x]);
386 let exact = target(x);
387 println!(
388 " f({:>5.1}) = {:>8.4} (predicted: {:>8.4}, error: {:>8.4})",
389 x,
390 exact,
391 pred,
392 (pred - exact).abs()
393 );
394 }
395
396 // Symbolic derivative via ExprGraph
397 let dx = g.diff(simplified, 0);
398 let dx = g.simplify(dx);
399 println!(
400 "\nsymbolic derivative: d/dx [{}] = {}",
401 expr_str,
402 g.fmt_expr(dx)
403 );
404}Sourcepub fn node(&self, id: ExprId) -> Node
pub fn node(&self, id: ExprId) -> Node
Look up the node for an ExprId.
Examples found in repository?
215fn expr_from_graph(g: &ExprGraph, id: ExprId) -> Expr {
216 match g.node(id) {
217 tang_expr::node::Node::Var(_) => Expr::X,
218 tang_expr::node::Node::Lit(bits) => {
219 let v = f64::from_bits(bits);
220 if v == 0.0 {
221 Expr::Lit(0.0)
222 } else {
223 Expr::Lit(v)
224 }
225 }
226 tang_expr::node::Node::Add(a, b) => Expr::Add(
227 Box::new(expr_from_graph(g, a)),
228 Box::new(expr_from_graph(g, b)),
229 ),
230 tang_expr::node::Node::Mul(a, b) => Expr::Mul(
231 Box::new(expr_from_graph(g, a)),
232 Box::new(expr_from_graph(g, b)),
233 ),
234 tang_expr::node::Node::Neg(a) => Expr::Neg(Box::new(expr_from_graph(g, a))),
235 tang_expr::node::Node::Sin(a) => Expr::Sin(Box::new(expr_from_graph(g, a))),
236 // For operations we don't represent in our AST, just evaluate as literal
237 _ => {
238 let v = g.eval::<f64>(id, &[1.0]); // fallback
239 Expr::Lit(v)
240 }
241 }
242}Sourcepub fn nodes_slice(&self) -> &[Node]
pub fn nodes_slice(&self) -> &[Node]
Read-only access to the node arena for serialization.
Sourcepub fn var(&mut self, n: u16) -> ExprId
pub fn var(&mut self, n: u16) -> ExprId
Create a variable node.
Examples found in repository?
58 fn to_expr(&self, g: &mut ExprGraph) -> ExprId {
59 match self {
60 Expr::X => g.var(0),
61 Expr::Lit(v) => g.lit(*v),
62 Expr::Add(a, b) => {
63 let a = a.to_expr(g);
64 let b = b.to_expr(g);
65 g.add(a, b)
66 }
67 Expr::Mul(a, b) => {
68 let a = a.to_expr(g);
69 let b = b.to_expr(g);
70 g.mul(a, b)
71 }
72 Expr::Sin(a) => {
73 let a = a.to_expr(g);
74 g.sin(a)
75 }
76 Expr::Neg(a) => {
77 let a = a.to_expr(g);
78 g.neg(a)
79 }
80 }
81 }Sourcepub fn lit(&mut self, v: f64) -> ExprId
pub fn lit(&mut self, v: f64) -> ExprId
Create a literal node.
Examples found in repository?
58 fn to_expr(&self, g: &mut ExprGraph) -> ExprId {
59 match self {
60 Expr::X => g.var(0),
61 Expr::Lit(v) => g.lit(*v),
62 Expr::Add(a, b) => {
63 let a = a.to_expr(g);
64 let b = b.to_expr(g);
65 g.add(a, b)
66 }
67 Expr::Mul(a, b) => {
68 let a = a.to_expr(g);
69 let b = b.to_expr(g);
70 g.mul(a, b)
71 }
72 Expr::Sin(a) => {
73 let a = a.to_expr(g);
74 g.sin(a)
75 }
76 Expr::Neg(a) => {
77 let a = a.to_expr(g);
78 g.neg(a)
79 }
80 }
81 }Sourcepub fn add(&mut self, a: ExprId, b: ExprId) -> ExprId
pub fn add(&mut self, a: ExprId, b: ExprId) -> ExprId
Add two expressions.
Examples found in repository?
58 fn to_expr(&self, g: &mut ExprGraph) -> ExprId {
59 match self {
60 Expr::X => g.var(0),
61 Expr::Lit(v) => g.lit(*v),
62 Expr::Add(a, b) => {
63 let a = a.to_expr(g);
64 let b = b.to_expr(g);
65 g.add(a, b)
66 }
67 Expr::Mul(a, b) => {
68 let a = a.to_expr(g);
69 let b = b.to_expr(g);
70 g.mul(a, b)
71 }
72 Expr::Sin(a) => {
73 let a = a.to_expr(g);
74 g.sin(a)
75 }
76 Expr::Neg(a) => {
77 let a = a.to_expr(g);
78 g.neg(a)
79 }
80 }
81 }Sourcepub fn mul(&mut self, a: ExprId, b: ExprId) -> ExprId
pub fn mul(&mut self, a: ExprId, b: ExprId) -> ExprId
Multiply two expressions.
Examples found in repository?
58 fn to_expr(&self, g: &mut ExprGraph) -> ExprId {
59 match self {
60 Expr::X => g.var(0),
61 Expr::Lit(v) => g.lit(*v),
62 Expr::Add(a, b) => {
63 let a = a.to_expr(g);
64 let b = b.to_expr(g);
65 g.add(a, b)
66 }
67 Expr::Mul(a, b) => {
68 let a = a.to_expr(g);
69 let b = b.to_expr(g);
70 g.mul(a, b)
71 }
72 Expr::Sin(a) => {
73 let a = a.to_expr(g);
74 g.sin(a)
75 }
76 Expr::Neg(a) => {
77 let a = a.to_expr(g);
78 g.neg(a)
79 }
80 }
81 }Sourcepub fn neg(&mut self, a: ExprId) -> ExprId
pub fn neg(&mut self, a: ExprId) -> ExprId
Negate an expression.
Examples found in repository?
58 fn to_expr(&self, g: &mut ExprGraph) -> ExprId {
59 match self {
60 Expr::X => g.var(0),
61 Expr::Lit(v) => g.lit(*v),
62 Expr::Add(a, b) => {
63 let a = a.to_expr(g);
64 let b = b.to_expr(g);
65 g.add(a, b)
66 }
67 Expr::Mul(a, b) => {
68 let a = a.to_expr(g);
69 let b = b.to_expr(g);
70 g.mul(a, b)
71 }
72 Expr::Sin(a) => {
73 let a = a.to_expr(g);
74 g.sin(a)
75 }
76 Expr::Neg(a) => {
77 let a = a.to_expr(g);
78 g.neg(a)
79 }
80 }
81 }Sourcepub fn sin(&mut self, a: ExprId) -> ExprId
pub fn sin(&mut self, a: ExprId) -> ExprId
Sine.
Examples found in repository?
58 fn to_expr(&self, g: &mut ExprGraph) -> ExprId {
59 match self {
60 Expr::X => g.var(0),
61 Expr::Lit(v) => g.lit(*v),
62 Expr::Add(a, b) => {
63 let a = a.to_expr(g);
64 let b = b.to_expr(g);
65 g.add(a, b)
66 }
67 Expr::Mul(a, b) => {
68 let a = a.to_expr(g);
69 let b = b.to_expr(g);
70 g.mul(a, b)
71 }
72 Expr::Sin(a) => {
73 let a = a.to_expr(g);
74 g.sin(a)
75 }
76 Expr::Neg(a) => {
77 let a = a.to_expr(g);
78 g.neg(a)
79 }
80 }
81 }Source§impl ExprGraph
impl ExprGraph
Sourcepub fn simplify(&mut self, expr: ExprId) -> ExprId
pub fn simplify(&mut self, expr: ExprId) -> ExprId
Simplify an expression by applying rewrite rules to fixpoint.
Bottom-up: simplify children first, then match parent. Iterates until no more changes occur.
Examples found in repository?
194fn mutate_simplify(expr: &Expr) -> Expr {
195 let mut g = ExprGraph::new();
196 let root = expr.to_expr(&mut g);
197 let simplified = g.simplify(root);
198 let s = g.fmt_expr(simplified);
199
200 // Quick check: if simplified form is much shorter, it's better
201 let orig = expr.format();
202 if s.len() < orig.len() {
203 // Re-evaluate to confirm it still works
204 let test = g.eval::<f64>(simplified, &[1.0]);
205 let orig_test = expr.eval_at(1.0);
206 if (test - orig_test).abs() < 1e-10 || (test.is_nan() && orig_test.is_nan()) {
207 // Return a new tree built from the simplified graph
208 return expr_from_graph(&g, simplified);
209 }
210 }
211 expr.clone()
212}
213
214/// Reconstruct an Expr tree from an ExprGraph node.
215fn expr_from_graph(g: &ExprGraph, id: ExprId) -> Expr {
216 match g.node(id) {
217 tang_expr::node::Node::Var(_) => Expr::X,
218 tang_expr::node::Node::Lit(bits) => {
219 let v = f64::from_bits(bits);
220 if v == 0.0 {
221 Expr::Lit(0.0)
222 } else {
223 Expr::Lit(v)
224 }
225 }
226 tang_expr::node::Node::Add(a, b) => Expr::Add(
227 Box::new(expr_from_graph(g, a)),
228 Box::new(expr_from_graph(g, b)),
229 ),
230 tang_expr::node::Node::Mul(a, b) => Expr::Mul(
231 Box::new(expr_from_graph(g, a)),
232 Box::new(expr_from_graph(g, b)),
233 ),
234 tang_expr::node::Node::Neg(a) => Expr::Neg(Box::new(expr_from_graph(g, a))),
235 tang_expr::node::Node::Sin(a) => Expr::Sin(Box::new(expr_from_graph(g, a))),
236 // For operations we don't represent in our AST, just evaluate as literal
237 _ => {
238 let v = g.eval::<f64>(id, &[1.0]); // fallback
239 Expr::Lit(v)
240 }
241 }
242}
243
244// --- Fitness evaluation ------------------------------------------------------
245
246fn target(x: f64) -> f64 {
247 x * x * x.sin()
248}
249
250fn generate_data(n: usize, rng: &mut Lcg) -> Vec<(f64, f64)> {
251 (0..n)
252 .map(|i| {
253 let x = -3.0 + 6.0 * i as f64 / (n - 1) as f64;
254 let noise = (rng.uniform() - 0.5) * 0.01;
255 (x, target(x) + noise)
256 })
257 .collect()
258}
259
260/// Evaluate fitness: MSE + complexity penalty.
261fn fitness(expr: &Expr, data: &[(f64, f64)]) -> f64 {
262 let mut g = ExprGraph::new();
263 let root = expr.to_expr(&mut g);
264 let compiled = g.compile(root);
265
266 let mut mse = 0.0;
267 for &(x, y) in data {
268 let pred = compiled(&[x]);
269 if pred.is_nan() || pred.is_infinite() {
270 return f64::INFINITY;
271 }
272 let err = pred - y;
273 mse += err * err;
274 }
275 mse /= data.len() as f64;
276
277 let penalty = 0.001 * expr.size() as f64;
278 mse + penalty
279}
280
281// --- Selection ---------------------------------------------------------------
282
283fn tournament<'a>(pop: &'a [(Expr, f64)], k: usize, rng: &mut Lcg) -> &'a Expr {
284 let mut best_idx = rng.range(pop.len());
285 for _ in 1..k {
286 let idx = rng.range(pop.len());
287 if pop[idx].1 < pop[best_idx].1 {
288 best_idx = idx;
289 }
290 }
291 &pop[best_idx].0
292}
293
294// --- Main --------------------------------------------------------------------
295
296fn main() {
297 println!("=== Symbolic Regression ===\n");
298 println!("target: f(x) = x^2 * sin(x)\n");
299
300 let mut rng = Lcg::new(42);
301 let data = generate_data(50, &mut rng);
302
303 const POP_SIZE: usize = 200;
304 const GENERATIONS: usize = 100;
305 const TOURNAMENT_K: usize = 5;
306
307 // Initialize population
308 let mut population: Vec<(Expr, f64)> = (0..POP_SIZE)
309 .map(|_| {
310 let expr = random_expr(4, &mut rng);
311 let fit = fitness(&expr, &data);
312 (expr, fit)
313 })
314 .collect();
315
316 let mut best_expr: Option<Expr> = None;
317 let mut best_fitness = f64::INFINITY;
318
319 for gen in 0..GENERATIONS {
320 // Track best
321 for (expr, fit) in &population {
322 if *fit < best_fitness {
323 best_fitness = *fit;
324 best_expr = Some(expr.clone());
325 }
326 }
327
328 if (gen + 1) % 10 == 0 || gen == 0 {
329 let b = best_expr.as_ref().unwrap();
330 println!(
331 "gen {:>3}: best fitness = {:.6} nodes = {:>3} expr = {}",
332 gen + 1,
333 best_fitness,
334 b.size(),
335 b.format(),
336 );
337 }
338
339 // Build next generation
340 let mut next_pop = Vec::with_capacity(POP_SIZE);
341
342 // Elitism: keep top 5
343 let mut indices: Vec<usize> = (0..population.len()).collect();
344 indices.sort_by(|&a, &b| population[a].1.partial_cmp(&population[b].1).unwrap());
345 for &i in indices.iter().take(5) {
346 next_pop.push(population[i].clone());
347 }
348
349 // Fill rest via mutation
350 while next_pop.len() < POP_SIZE {
351 let parent = tournament(&population, TOURNAMENT_K, &mut rng);
352 let child = match rng.range(10) {
353 0..=3 => mutate_grow(parent, &mut rng),
354 4..=6 => mutate_point(parent, &mut rng),
355 7..=8 => mutate_simplify(parent),
356 _ => random_expr(4, &mut rng), // fresh blood
357 };
358
359 // Skip overly large expressions
360 if child.size() > 50 {
361 continue;
362 }
363
364 let fit = fitness(&child, &data);
365 next_pop.push((child, fit));
366 }
367
368 population = next_pop;
369 }
370
371 // Final results
372 println!();
373 let best = best_expr.unwrap();
374 let mut g = ExprGraph::new();
375 let root = best.to_expr(&mut g);
376 let simplified = g.simplify(root);
377 let expr_str = g.fmt_expr(simplified);
378 println!("best expression: {}", expr_str);
379 println!("best fitness: {:.6}", best_fitness);
380
381 // Verify on test points
382 println!("\nverification:");
383 let compiled = g.compile(simplified);
384 for &x in &[-2.0, -1.0, 0.0, 1.0, 2.0] {
385 let pred = compiled(&[x]);
386 let exact = target(x);
387 println!(
388 " f({:>5.1}) = {:>8.4} (predicted: {:>8.4}, error: {:>8.4})",
389 x,
390 exact,
391 pred,
392 (pred - exact).abs()
393 );
394 }
395
396 // Symbolic derivative via ExprGraph
397 let dx = g.diff(simplified, 0);
398 let dx = g.simplify(dx);
399 println!(
400 "\nsymbolic derivative: d/dx [{}] = {}",
401 expr_str,
402 g.fmt_expr(dx)
403 );
404}Source§impl ExprGraph
impl ExprGraph
Source§impl ExprGraph
impl ExprGraph
Sourcepub fn to_wgsl(&self, outputs: &[ExprId], n_inputs: usize) -> WgslKernel
pub fn to_wgsl(&self, outputs: &[ExprId], n_inputs: usize) -> WgslKernel
Generate a WGSL compute shader that evaluates expressions in parallel.
Each work item reads n_inputs values and writes outputs.len() values.
The generated shader uses f32 (GPU native). Shared subexpressions are
computed once per thread.
The caller handles device/pipeline/dispatch (no wgpu dependency here).