Skip to main content

rssn/output/
plotting.rs

1use std::collections::HashMap;
2
3use ndarray::Array2;
4use num_traits::ToPrimitive;
5use plotters::prelude::*;
6
7use crate::symbolic::core::DagOp;
8use crate::symbolic::core::Expr;
9
10/// Configuration for plotting.
11#[derive(Clone, Debug)]
12pub struct PlotConfig {
13    /// The width of the plot in pixels.
14    pub width: u32,
15    /// The height of the plot in pixels.
16    pub height: u32,
17    /// The caption of the plot.
18    pub caption: String,
19    /// The color of the line in the plot.
20    pub line_color: RGBAColor,
21    /// The color of the mesh in the plot.
22    pub mesh_color: RGBAColor,
23    /// The number of samples to use when plotting.
24    pub samples: usize,
25    /// The pitch for 3D plots (in radians).
26    pub pitch: f64,
27    /// The yaw for 3D plots (in radians).
28    pub yaw: f64,
29    /// The scale for 3D plots.
30    pub scale: f64,
31    /// The font size for labels (axis, legend).
32    pub label_font_size: u32,
33    /// The font size for the caption.
34    pub caption_font_size: u32,
35}
36
37impl Default for PlotConfig {
38    fn default() -> Self {
39        Self {
40            width: 800,
41            height: 600,
42            caption: "Plot".to_string(),
43            line_color: RED.to_rgba(),
44            mesh_color: BLACK.mix(0.1),
45            samples: 500,
46            pitch: 0.5,
47            yaw: 0.5,
48            scale: 0.7,
49            label_font_size: 20,
50            caption_font_size: 40,
51        }
52    }
53}
54
55/// Evaluates a symbolic expression to a numerical f64 value.
56/// `vars` contains the numerical values for the variables in the expression.
57/// This function is iterative to avoid stack overflows.
58pub(crate) fn eval_expr(
59    root_expr: &Expr,
60    vars: &HashMap<String, f64>,
61) -> Result<f64, String> {
62    let mut results: HashMap<Expr, f64> = HashMap::new();
63
64    let mut stack: Vec<Expr> = vec![root_expr.clone()];
65
66    let mut visited = std::collections::HashSet::new();
67
68    while let Some(expr) = stack.last() {
69        if results.contains_key(expr) {
70            stack.pop();
71
72            continue;
73        }
74
75        let children = expr.children();
76
77        if children.is_empty() || visited.contains(expr) {
78            let current_expr = stack.pop().expect("Expr present");
79
80            let children = current_expr.children();
81
82            let get_child_val = |i: usize| -> f64 { results[&children[i]] };
83
84            let val_result = match current_expr.op() {
85                | DagOp::Constant(c) => Ok(c.into_inner()),
86                | DagOp::BigInt(i) => {
87                    i.to_f64()
88                        .ok_or_else(|| "BigInt conversion to f64 failed".to_string())
89                },
90                | DagOp::Rational(r) => {
91                    Ok(r.numer().to_f64().unwrap() / r.denom().to_f64().unwrap())
92                },
93                | DagOp::Variable(v) => {
94                    vars.get(&v)
95                        .copied()
96                        .ok_or_else(|| format!("Variable '{v}' not found"))
97                },
98                | DagOp::Add => Ok(get_child_val(0) + get_child_val(1)),
99                | DagOp::Sub => Ok(get_child_val(0) - get_child_val(1)),
100                | DagOp::Mul => Ok(get_child_val(0) * get_child_val(1)),
101                | DagOp::Div => Ok(get_child_val(0) / get_child_val(1)),
102                | DagOp::Power => Ok(get_child_val(0).powf(get_child_val(1))),
103                | DagOp::Neg => Ok(-get_child_val(0)),
104                | DagOp::Sqrt => Ok(get_child_val(0).sqrt()),
105                | DagOp::Abs => Ok(get_child_val(0).abs()),
106                | DagOp::Sin => Ok(get_child_val(0).sin()),
107                | DagOp::Cos => Ok(get_child_val(0).cos()),
108                | DagOp::Tan => Ok(get_child_val(0).tan()),
109                | DagOp::Csc => Ok(1.0 / get_child_val(0).sin()),
110                | DagOp::Sec => Ok(1.0 / get_child_val(0).cos()),
111                | DagOp::Cot => Ok(1.0 / get_child_val(0).tan()),
112                | DagOp::ArcSin => Ok(get_child_val(0).asin()),
113                | DagOp::ArcCos => Ok(get_child_val(0).acos()),
114                | DagOp::ArcTan => Ok(get_child_val(0).atan()),
115                | DagOp::Sinh => Ok(get_child_val(0).sinh()),
116                | DagOp::Cosh => Ok(get_child_val(0).cosh()),
117                | DagOp::Tanh => Ok(get_child_val(0).tanh()),
118                | DagOp::Log => Ok(get_child_val(0).ln()),
119                | DagOp::LogBase => Ok(get_child_val(1).log(get_child_val(0))),
120                | DagOp::Exp => Ok(get_child_val(0).exp()),
121                | DagOp::Floor => Ok(get_child_val(0).floor()),
122                | DagOp::Pi => Ok(std::f64::consts::PI),
123                | DagOp::E => Ok(std::f64::consts::E),
124                | _ => {
125                    Err(format!(
126                        "Numerical evaluation for operation {:?} is not implemented",
127                        current_expr.op()
128                    ))
129                },
130            };
131
132            let val = val_result?;
133
134            results.insert(current_expr, val);
135        } else {
136            visited.insert(expr.clone());
137
138            for child in children.iter().rev() {
139                stack.push(child.clone());
140            }
141        }
142    }
143
144    Ok(results[root_expr])
145}
146
147/// Plots a 2D function y = f(x) and saves it to a file.
148///
149/// # Errors
150///
151/// This function will return an error if the plot cannot be created or saved.
152pub fn plot_function_2d(
153    expr: &Expr,
154    var: &str,
155    range: (f64, f64),
156    path: &str,
157    config: Option<PlotConfig>,
158) -> Result<(), String> {
159    let conf = config.unwrap_or_default();
160
161    let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
162
163    root.fill(&WHITE).map_err(|e| e.to_string())?;
164
165    let y_min = (0..100)
166        .map(|i| {
167            let x = (range.1 - range.0).mul_add(f64::from(i) / 99.0, range.0);
168
169            eval_expr(expr, &HashMap::from([(var.to_string(), x)]))
170        })
171        .filter_map(Result::ok)
172        .fold(f64::INFINITY, f64::min);
173
174    let y_max = (0..100)
175        .map(|i| {
176            let x = (range.1 - range.0).mul_add(f64::from(i) / 99.0, range.0);
177
178            eval_expr(expr, &HashMap::from([(var.to_string(), x)]))
179        })
180        .filter_map(Result::ok)
181        .fold(f64::NEG_INFINITY, f64::max);
182
183    let mut chart = ChartBuilder::on(&root)
184        .caption(&conf.caption, ("sans-serif", 40).into_font())
185        .margin(5)
186        .x_label_area_size(30)
187        .y_label_area_size(30)
188        .build_cartesian_2d(range.0..range.1, y_min..y_max)
189        .map_err(|e| e.to_string())?;
190
191    chart
192        .configure_mesh()
193        .light_line_style(conf.mesh_color)
194        .draw()
195        .map_err(|e| e.to_string())?;
196
197    chart
198        .draw_series(LineSeries::new(
199            (0..=conf.samples).map(|i| {
200                let x = (range.1 - range.0).mul_add((i as f64) / conf.samples as f64, range.0);
201                let y = eval_expr(expr, &HashMap::from([(var.to_string(), x)])).unwrap_or(0.0);
202                (x, y)
203            }),
204            &conf.line_color,
205        ))
206        .map_err(|e| e.to_string())?;
207
208    root.present().map_err(|e| e.to_string())?;
209
210    Ok(())
211}
212
213/// Plots multiple data series on a single 2D plot and saves it to a file.
214///
215/// # Errors
216///
217/// This function will return an error if the plot cannot be created or saved.
218pub fn plot_series_2d(
219    series: &[(String, Vec<(f64, f64)>)],
220    path: &str,
221    config: Option<PlotConfig>,
222) -> Result<(), String> {
223    let conf = config.unwrap_or_default();
224
225    let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
226
227    root.fill(&WHITE).map_err(|e| e.to_string())?;
228
229    if series.is_empty() {
230        return Err("No data series \
231                    provided"
232            .to_string());
233    }
234
235    // Find x and y ranges
236    let mut x_min = f64::INFINITY;
237
238    let mut x_max = f64::NEG_INFINITY;
239
240    let mut y_min = f64::INFINITY;
241
242    let mut y_max = f64::NEG_INFINITY;
243
244    for (_, data) in series {
245        for &(x, y) in data {
246            x_min = x_min.min(x);
247
248            x_max = x_max.max(x);
249
250            y_min = y_min.min(y);
251
252            y_max = y_max.max(y);
253        }
254    }
255
256    // Add padding
257    let x_pad = (x_max - x_min) * 0.05;
258
259    let y_pad = (y_max - y_min) * 0.1;
260
261    let x_range = (x_min - x_pad)..(x_max + x_pad);
262
263    let y_range = (y_min - y_pad)..(y_max + y_pad);
264
265    let mut chart = ChartBuilder::on(&root)
266        .caption(
267            &conf.caption,
268            ("sans-serif", conf.caption_font_size).into_font(),
269        )
270        .margin(conf.label_font_size / 2)
271        .x_label_area_size(conf.label_font_size * 2)
272        .y_label_area_size(conf.label_font_size * 2)
273        .build_cartesian_2d(x_range, y_range)
274        .map_err(|e| e.to_string())?;
275
276    chart
277        .configure_mesh()
278        .light_line_style(conf.mesh_color)
279        .label_style(("sans-serif", conf.label_font_size).into_font())
280        .draw()
281        .map_err(|e| e.to_string())?;
282
283    let colors = [&RED, &BLUE, &GREEN, &CYAN, &MAGENTA, &YELLOW, &BLACK];
284
285    for (i, (label, data)) in series.iter().enumerate() {
286        let color = colors[i % colors.len()];
287
288        chart
289            .draw_series(LineSeries::new(data.clone(), color))
290            .map_err(|e| e.to_string())?
291            .label(label)
292            .legend(move |(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], color));
293    }
294
295    chart
296        .configure_series_labels()
297        .background_style(WHITE.mix(0.8))
298        .border_style(BLACK)
299        .label_font(("sans-serif", conf.label_font_size).into_font())
300        .draw()
301        .map_err(|e| e.to_string())?;
302
303    root.present().map_err(|e| e.to_string())?;
304
305    Ok(())
306}
307
308/// Plots a 2D vector field and saves it to a file.
309///
310/// # Errors
311///
312/// This function will return an error if the plot cannot be created or saved.
313pub fn plot_vector_field_2d(
314    comps: (&Expr, &Expr),
315    vars: (&str, &str),
316    x_range: (f64, f64),
317    y_range: (f64, f64),
318    path: &str,
319    config: Option<PlotConfig>,
320) -> Result<(), String> {
321    let conf = config.unwrap_or_default();
322
323    let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
324
325    root.fill(&WHITE).map_err(|e| e.to_string())?;
326
327    let mut chart = ChartBuilder::on(&root)
328        .caption(&conf.caption, ("sans-serif", 40).into_font())
329        .build_cartesian_2d(x_range.0..x_range.1, y_range.0..y_range.1)
330        .map_err(|e| e.to_string())?;
331
332    chart
333        .configure_mesh()
334        .light_line_style(conf.mesh_color)
335        .draw()
336        .map_err(|e| e.to_string())?;
337
338    let (vx_expr, vy_expr) = comps;
339
340    let (x_var, y_var) = vars;
341
342    let mut arrows = Vec::new();
343
344    let steps: usize = ((conf.samples as f64).sqrt().round() as i64)
345        .try_into()
346        .unwrap_or(0);
347
348    for i in 0..steps {
349        for j in 0..steps {
350            let x = (x_range.1 - x_range.0).mul_add((i as f64) / (steps - 1) as f64, x_range.0);
351
352            let y = (y_range.1 - y_range.0).mul_add((j as f64) / (steps - 1) as f64, y_range.0);
353
354            let mut vars_map = HashMap::new();
355
356            vars_map.insert(x_var.to_string(), x);
357
358            vars_map.insert(y_var.to_string(), y);
359
360            if let (Ok(vx), Ok(vy)) = (eval_expr(vx_expr, &vars_map), eval_expr(vy_expr, &vars_map))
361            {
362                let magnitude = vx.hypot(vy);
363
364                if magnitude > 1e-9 {
365                    let scale = (x_range.1 - x_range.0) * 0.05;
366
367                    let end_x = (vx / magnitude).mul_add(scale, x);
368
369                    let end_y = (vy / magnitude).mul_add(scale, y);
370
371                    arrows.push(PathElement::new(
372                        vec![(x, y), (end_x, end_y)],
373                        conf.line_color,
374                    ));
375                }
376            }
377        }
378    }
379
380    chart.draw_series(arrows).map_err(|e| e.to_string())?;
381
382    root.present().map_err(|e| e.to_string())?;
383
384    Ok(())
385}
386
387/// Plots a 3D surface z = f(x, y) and saves it to a file.
388///
389/// # Errors
390///
391/// This function will return an error if the plot cannot be created or saved.
392pub fn plot_surface_3d(
393    expr: &Expr,
394    vars: (&str, &str),
395    x_range: (f64, f64),
396    y_range: (f64, f64),
397    path: &str,
398    config: Option<PlotConfig>,
399) -> Result<(), String> {
400    let conf = config.unwrap_or_default();
401
402    let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
403
404    root.fill(&WHITE).map_err(|e| e.to_string())?;
405
406    let mut chart = ChartBuilder::on(&root)
407        .caption(&conf.caption, ("sans-serif", 40).into_font())
408        .build_cartesian_3d(x_range.0..x_range.1, -1.0..1.0, y_range.0..y_range.1)
409        .map_err(|e| e.to_string())?;
410
411    chart.configure_axes().draw().map_err(|e| e.to_string())?;
412
413    let (x_var, y_var) = vars;
414
415    let steps: usize = ((conf.samples as f64).sqrt().round() as i64)
416        .try_into()
417        .unwrap_or(0);
418
419    let _ = chart.draw_series(
420        SurfaceSeries::xoz(
421            (0..steps)
422                .map(|i| x_range.0 + (x_range.1 - x_range.0) * (i as f64) / (steps - 1) as f64),
423            (0..steps)
424                .map(|i| y_range.0 + (y_range.1 - y_range.0) * (i as f64) / (steps - 1) as f64),
425            |x, z| {
426                let mut vars_map = HashMap::new();
427
428                vars_map.insert(x_var.to_string(), x);
429
430                vars_map.insert(y_var.to_string(), z);
431
432                eval_expr(expr, &vars_map).unwrap_or(0.0)
433            },
434        )
435        .style(conf.line_color.mix(0.5).filled()),
436    );
437
438    root.present().map_err(|e| e.to_string())?;
439
440    Ok(())
441}
442
443/// Plots a 2D array as a 3D surface plot and saves it to a file.
444///
445/// # Errors
446///
447/// This function will return an error if the plot cannot be created or saved.
448pub fn plot_surface_2d(
449    data: &Array2<f64>,
450    path: &str,
451    config: Option<PlotConfig>,
452) -> Result<(), String> {
453    let conf = config.unwrap_or_default();
454
455    let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
456
457    root.fill(&WHITE).map_err(|e| e.to_string())?;
458
459    let (height, width) = data.dim();
460
461    let mut min_val = f64::INFINITY;
462
463    let mut max_val = f64::NEG_INFINITY;
464
465    for &val in data {
466        min_val = min_val.min(val);
467
468        max_val = max_val.max(val);
469    }
470
471    // Add a bit of padding to the Z axis
472    let z_min = if (max_val - min_val).abs() < 1e-9 {
473        min_val - 1.0
474    } else {
475        (max_val - min_val).mul_add(-0.1, min_val)
476    };
477
478    let z_max = if (max_val - min_val).abs() < 1e-9 {
479        max_val + 1.0
480    } else {
481        (max_val - min_val).mul_add(0.1, max_val)
482    };
483
484    let mut chart = ChartBuilder::on(&root)
485        .caption(&conf.caption, ("sans-serif", 40).into_font())
486        .build_cartesian_3d(0.0..width as f64, z_min..z_max, 0.0..height as f64)
487        .map_err(|e| e.to_string())?;
488
489    chart.configure_axes().draw().map_err(|e| e.to_string())?;
490
491    chart
492        .draw_series(
493            SurfaceSeries::xoz(
494                (0..width).map(|x| x as f64),
495                (0..height).map(|y| y as f64),
496                |x, y| {
497                    let ix = usize::try_from(x.round() as isize)
498                        .unwrap_or(0)
499                        .min(width - 1);
500
501                    let iy = usize::try_from(y.round() as isize)
502                        .unwrap_or(0)
503                        .min(height - 1);
504
505                    data[[iy, ix]]
506                },
507            )
508            .style(conf.line_color.mix(0.5).filled()),
509        )
510        .map_err(|e| e.to_string())?;
511
512    root.present().map_err(|e| e.to_string())?;
513
514    Ok(())
515}
516
517/// Plots a 3D parametric curve (x(t), y(t), z(t)) and saves it to a file.
518///
519/// # Errors
520///
521/// This function will return an error if the plot cannot be created or saved.
522pub fn plot_parametric_curve_3d(
523    comps: (&Expr, &Expr, &Expr),
524    var: &str,
525    range: (f64, f64),
526    path: &str,
527    config: Option<PlotConfig>,
528) -> Result<(), String> {
529    let conf = config.unwrap_or_default();
530
531    let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
532
533    root.fill(&WHITE).map_err(|e| e.to_string())?;
534
535    let mut chart = ChartBuilder::on(&root)
536        .caption(&conf.caption, ("sans-serif", 40).into_font())
537        .build_cartesian_3d(-3.0..3.0, -3.0..3.0, -3.0..3.0)
538        .map_err(|e| e.to_string())?;
539
540    chart.configure_axes().draw().map_err(|e| e.to_string())?;
541
542    let (x_expr, y_expr, z_expr) = comps;
543
544    chart
545        .draw_series(LineSeries::new(
546            (0..=conf.samples).map(|i| {
547                let t = (range.1 - range.0).mul_add((i as f64) / conf.samples as f64, range.0);
548                let mut vars_map = HashMap::new();
549                vars_map.insert(var.to_string(), t);
550
551                let x = eval_expr(x_expr, &vars_map).unwrap_or(0.0);
552                let y = eval_expr(y_expr, &vars_map).unwrap_or(0.0);
553                let z = eval_expr(z_expr, &vars_map).unwrap_or(0.0);
554                (x, y, z)
555            }),
556            &conf.line_color,
557        ))
558        .map_err(|e| e.to_string())?;
559
560    root.present().map_err(|e| e.to_string())?;
561
562    Ok(())
563}
564
565/// Plots a 3D vector field and saves it to a file.
566///
567/// # Errors
568///
569/// This function will return an error if the plot cannot be created or saved.
570pub fn plot_vector_field_3d(
571    comps: (&Expr, &Expr, &Expr),
572    vars: (&str, &str, &str),
573    ranges: ((f64, f64), (f64, f64), (f64, f64)),
574    path: &str,
575    config: Option<PlotConfig>,
576) -> Result<(), String> {
577    let conf = config.unwrap_or_default();
578
579    let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
580
581    root.fill(&WHITE).map_err(|e| e.to_string())?;
582
583    let (x_range, y_range, z_range) = ranges;
584
585    let mut chart = ChartBuilder::on(&root)
586        .caption(&conf.caption, ("sans-serif", 40).into_font())
587        .build_cartesian_3d(
588            x_range.0..x_range.1,
589            y_range.0..y_range.1,
590            z_range.0..z_range.1,
591        )
592        .map_err(|e| e.to_string())?;
593
594    chart.configure_axes().draw().map_err(|e| e.to_string())?;
595
596    let (vx_expr, vy_expr, vz_expr) = comps;
597
598    let (x_var, y_var, z_var) = vars;
599
600    let mut arrows = Vec::new();
601
602    let steps: usize = ((conf.samples as f64).cbrt().round() as i64)
603        .try_into()
604        .unwrap_or(0);
605
606    for i in 0..steps {
607        for j in 0..steps {
608            for k in 0..steps {
609                let x = (x_range.1 - x_range.0).mul_add((i as f64) / (steps - 1) as f64, x_range.0);
610
611                let y = (y_range.1 - y_range.0).mul_add((j as f64) / (steps - 1) as f64, y_range.0);
612
613                let z = (z_range.1 - z_range.0).mul_add((k as f64) / (steps - 1) as f64, z_range.0);
614
615                let mut vars_map = HashMap::new();
616
617                vars_map.insert(x_var.to_string(), x);
618
619                vars_map.insert(y_var.to_string(), y);
620
621                vars_map.insert(z_var.to_string(), z);
622
623                if let (Ok(vx), Ok(vy), Ok(vz)) = (
624                    eval_expr(vx_expr, &vars_map),
625                    eval_expr(vy_expr, &vars_map),
626                    eval_expr(vz_expr, &vars_map),
627                ) {
628                    let magnitude = vz.mul_add(vz, vx.mul_add(vx, vy * vy)).sqrt();
629
630                    if magnitude > 1e-6 {
631                        let scale = (x_range.1 - x_range.0) * 0.05;
632
633                        let end_x = (vx / magnitude).mul_add(scale, x);
634
635                        let end_y = (vy / magnitude).mul_add(scale, y);
636
637                        let end_z = (vz / magnitude).mul_add(scale, z);
638
639                        arrows.push(PathElement::new(
640                            vec![(x, y, z), (end_x, end_y, end_z)],
641                            conf.line_color,
642                        ));
643                    }
644                }
645            }
646        }
647    }
648
649    chart.draw_series(arrows).map_err(|e| e.to_string())?;
650
651    root.present().map_err(|e| e.to_string())?;
652
653    Ok(())
654}
655
656#[cfg(test)]
657mod tests {
658
659    use std::collections::HashMap;
660
661    use super::*;
662    use crate::prelude::Expr;
663
664    #[test]
665    fn test_eval_basic() {
666        let x = Expr::Variable("x".to_string());
667
668        let expr = Expr::new_add(x, Expr::Constant(2.0));
669
670        let mut vars = HashMap::new();
671
672        vars.insert("x".to_string(), 3.0);
673
674        let res = eval_expr(&expr, &vars).unwrap();
675
676        assert_eq!(res, 5.0);
677    }
678
679    #[test]
680    fn test_eval_trig() {
681        let x = Expr::Variable("x".to_string());
682
683        let expr = Expr::new_sin(x);
684
685        let mut vars = HashMap::new();
686
687        vars.insert("x".to_string(), std::f64::consts::PI / 2.0);
688
689        let res = eval_expr(&expr, &vars).unwrap();
690
691        assert!((res - 1.0).abs() < 1e-10);
692    }
693
694    #[test]
695    fn test_eval_log_power() {
696        let x = Expr::Variable("x".to_string());
697
698        // e^(ln(x)) = x
699        let expr = Expr::new_exp(Expr::new_log(x.clone()));
700
701        let mut vars = HashMap::new();
702
703        vars.insert("x".to_string(), 5.0);
704
705        let res = eval_expr(&expr, &vars).unwrap();
706
707        assert!((res - 5.0).abs() < 1e-10);
708    }
709
710    #[test]
711    fn test_eval_error() {
712        let x = Expr::Variable("x".to_string());
713
714        let vars = HashMap::new(); // Missing x
715        let res = eval_expr(&x, &vars);
716
717        assert!(res.is_err());
718    }
719
720    use proptest::prelude::*;
721
722    proptest! {
723    #[test]
724            fn prop_no_panic_eval(
725                val in -100.0..100.0f64,
726                depth in 1..4usize,
727            ) {
728                let x = Expr::Variable("x".to_string());
729                let mut expr = x.clone();
730                for _ in 0..depth {
731                    expr = Expr::new_add(expr.clone(), Expr::Constant(1.0));
732                    expr = Expr::new_mul(expr.clone(), Expr::Constant(0.5));
733                }
734                let mut vars = HashMap::new();
735                vars.insert("x".to_string(), val);
736                let _ = eval_expr(&expr, &vars);
737            }
738        }
739}
740
741/// Plots a 3D path from a series of points (x, y, z) and saves it to a file.
742///
743/// # Errors
744///
745/// This function will return an error if the plot cannot be created or saved.
746pub fn plot_3d_path_from_points(
747    data: &[Vec<f64>],
748    path: &str,
749    config: Option<PlotConfig>,
750) -> Result<(), String> {
751    let conf = config.unwrap_or_default();
752
753    let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
754
755    root.fill(&WHITE).map_err(|e| e.to_string())?;
756
757    // Find the ranges for x, y, and z axes
758    let (mut x_min, mut x_max) = (f64::INFINITY, f64::NEG_INFINITY);
759
760    let (mut y_min, mut y_max) = (f64::INFINITY, f64::NEG_INFINITY);
761
762    let (mut z_min, mut z_max) = (f64::INFINITY, f64::NEG_INFINITY);
763
764    for point in data {
765        if point.len() >= 3 {
766            x_min = x_min.min(point[0]);
767
768            x_max = x_max.max(point[0]);
769
770            y_min = y_min.min(point[1]);
771
772            y_max = y_max.max(point[1]);
773
774            z_min = z_min.min(point[2]);
775
776            z_max = z_max.max(point[2]);
777        }
778    }
779
780    let mut chart = ChartBuilder::on(&root)
781        .caption(&conf.caption, ("sans-serif", 40).into_font())
782        .build_cartesian_3d(x_min..x_max, z_min..z_max, y_min..y_max) // Note: y and z are swapped in plotters' 3D coordinate system
783        .map_err(|e| e.to_string())?;
784
785    chart.configure_axes().draw().map_err(|e| e.to_string())?;
786
787    chart
788        .draw_series(LineSeries::new(
789            data.iter().filter_map(|p| {
790                if p.len() >= 3 {
791                    Some((p[0], p[2], p[1]))
792                } else {
793                    None
794                }
795            }), // Note: y and z are swapped here as well
796            &conf.line_color,
797        ))
798        .map_err(|e| e.to_string())?;
799
800    root.present().map_err(|e| e.to_string())?;
801
802    Ok(())
803}
804
805/// Plots a 2D heat map from a 2D array of data and saves it to a file.
806///
807/// # Errors
808///
809/// This function will return an error if the plot cannot be created or saved.
810pub fn plot_heatmap_2d(
811    data: &Array2<f64>,
812    path: &str,
813    config: Option<PlotConfig>,
814) -> Result<(), String> {
815    let conf = config.unwrap_or_default();
816
817    let root = BitMapBackend::new(path, (conf.width, conf.height)).into_drawing_area();
818
819    root.fill(&WHITE).map_err(|e| e.to_string())?;
820
821    let (height, width) = data.dim();
822
823    // Find min and max values for color mapping
824    let mut min_val = f64::INFINITY;
825
826    let mut max_val = f64::NEG_INFINITY;
827
828    for &val in data {
829        min_val = min_val.min(val);
830
831        println!("min_val: {min_val}");
832
833        max_val = max_val.max(val);
834
835        println!("max_val: {max_val}");
836    }
837
838    let mut chart = ChartBuilder::on(&root)
839        .caption(&conf.caption, ("sans-serif", 40).into_font())
840        .build_cartesian_2d(0..width as u32, 0..height as u32)
841        .map_err(|e| e.to_string())?;
842
843    chart.configure_mesh().draw().map_err(|e| e.to_string())?;
844
845    chart
846        .draw_series((0..width).flat_map(move |x| {
847            (0..height).map(move |y| {
848                let val = data[[y, x]]; // ndarray is (row, col) which corresponds to (y, x)
849                let normalized = if max_val - min_val > 0.0 {
850                    (val - min_val) / (max_val - min_val)
851                } else {
852                    0.5 // Avoid division by zero if all values are the same
853                };
854                // Use a blue-to-red color gradient
855                // let color = HSLColor(240.0 - 240.0 * normalized, 1.0, 0.5);
856                let color = HSLColor(240.0 * normalized, 1.0, 0.5);
857                Rectangle::new(
858                    [(x as u32, y as u32), (x as u32 + 1, y as u32 + 1)],
859                    color.filled(),
860                )
861            })
862        }))
863        .map_err(|e| e.to_string())?;
864
865    root.present().map_err(|e| e.to_string())?;
866
867    Ok(())
868}