Skip to main content

ExprGraph

Struct ExprGraph 

Source
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

Source

pub fn to_kernel( &self, outputs: &[ExprId], n_inputs: usize, dialect: Dialect, ) -> ComputeKernel

Generate a compute kernel for the given dialect.

Each work item reads n_inputs f32 values and writes outputs.len() f32 values. Shared subexpressions are computed once per thread.

Source§

impl ExprGraph

Source

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?
examples/symbolic_regression.rs (line 264)
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

pub fn compile_many(&self, exprs: &[ExprId]) -> CompiledMany

Compile multiple output expressions to a single closure.

Writes results into the output slice.

Source

pub fn live_set(&self, outputs: &[ExprId]) -> HashSet<usize>

Find all node indices reachable from the given outputs.

Source§

impl ExprGraph

Source

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?
examples/symbolic_regression.rs (line 397)
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

Source

pub fn fmt_expr(&self, expr: ExprId) -> String

Format an expression as a human-readable string.

Examples found in repository?
examples/symbolic_regression.rs (line 87)
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

Source

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?
examples/symbolic_regression.rs (line 94)
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

pub fn eval_many<S: Scalar>(&self, exprs: &[ExprId], inputs: &[S]) -> Vec<S>

Evaluate multiple output expressions, sharing intermediate values.

Source§

impl ExprGraph

Source

pub fn new() -> Self

Create a new graph pre-populated with ZERO, ONE, TWO.

Examples found in repository?
examples/symbolic_regression.rs (line 85)
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

pub fn len(&self) -> usize

Total number of nodes in the graph.

Source

pub fn is_empty(&self) -> bool

Whether the graph is empty (it never is after construction).

Source

pub fn node(&self, id: ExprId) -> Node

Look up the node for an ExprId.

Examples found in repository?
examples/symbolic_regression.rs (line 216)
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

pub fn nodes_slice(&self) -> &[Node]

Read-only access to the node arena for serialization.

Source

pub fn var(&mut self, n: u16) -> ExprId

Create a variable node.

Examples found in repository?
examples/symbolic_regression.rs (line 60)
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

pub fn lit(&mut self, v: f64) -> ExprId

Create a literal node.

Examples found in repository?
examples/symbolic_regression.rs (line 61)
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

pub fn add(&mut self, a: ExprId, b: ExprId) -> ExprId

Add two expressions.

Examples found in repository?
examples/symbolic_regression.rs (line 65)
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

pub fn mul(&mut self, a: ExprId, b: ExprId) -> ExprId

Multiply two expressions.

Examples found in repository?
examples/symbolic_regression.rs (line 70)
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

pub fn neg(&mut self, a: ExprId) -> ExprId

Negate an expression.

Examples found in repository?
examples/symbolic_regression.rs (line 78)
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

pub fn recip(&mut self, a: ExprId) -> ExprId

Reciprocal (1/x).

Source

pub fn sqrt(&mut self, a: ExprId) -> ExprId

Square root.

Source

pub fn sin(&mut self, a: ExprId) -> ExprId

Sine.

Examples found in repository?
examples/symbolic_regression.rs (line 74)
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

pub fn atan2(&mut self, y: ExprId, x: ExprId) -> ExprId

atan2(y, x).

Source

pub fn exp2(&mut self, a: ExprId) -> ExprId

Base-2 exponential.

Source

pub fn log2(&mut self, a: ExprId) -> ExprId

Base-2 logarithm.

Source

pub fn select(&mut self, cond: ExprId, a: ExprId, b: ExprId) -> ExprId

Branchless select: returns a if cond > 0, else b.

Source§

impl ExprGraph

Source

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?
examples/symbolic_regression.rs (line 197)
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

Source

pub fn deps(&self, expr: ExprId) -> u64

Compute a bitmask of which Var(n) indices appear in expr.

Bit n is set if Var(n) is reachable from expr. Supports up to 64 variables.

Source

pub fn jacobian_sparsity(&self, outputs: &[ExprId], n_vars: usize) -> Vec<u64>

Compute the Jacobian sparsity pattern.

Returns one u64 bitmask per output expression. Bit j of result[i] is set if outputs[i] depends on Var(j).

Source§

impl ExprGraph

Source

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).

Trait Implementations§

Source§

impl Clone for ExprGraph

Source§

fn clone(&self) -> ExprGraph

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Default for ExprGraph

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.