Skip to main content

sim_lib_view_math/
sweep.rs

1//! Parameter sweeps: slider/knob-driven plots with snapshot and compare.
2//!
3//! A sweep is a parametric series `y = f(x; param)`. A slider or knob drives
4//! `param` through `intent/set-param`; each change recomputes the series and
5//! re-renders the plot live. Snapshots freeze the current series so several
6//! parameter settings can be compared in one overlaid plot.
7
8use sim_kernel::{Error, Expr, Result};
9use sim_lib_scene::{node, sym};
10use sim_value::access::field as intent_field;
11
12use crate::num::{as_f64, number};
13use crate::plot::{multi_plot_view, response_plot_view};
14
15/// A live parameter sweep over a built-in linear family `y = param * x`.
16pub struct Sweep {
17    param: f64,
18    samples: usize,
19    snapshots: Vec<(String, Vec<(f64, f64)>)>,
20}
21
22impl Sweep {
23    /// A sweep starting at `param` with `samples` points.
24    pub fn new(param: f64, samples: usize) -> Self {
25        Self {
26            param,
27            samples: samples.max(2),
28            snapshots: Vec::new(),
29        }
30    }
31
32    /// The current parameter value.
33    pub fn param(&self) -> f64 {
34        self.param
35    }
36
37    fn series(&self) -> Vec<(f64, f64)> {
38        (0..self.samples)
39            .map(|i| {
40                let x = i as f64;
41                (x, self.param * x)
42            })
43            .collect()
44    }
45
46    /// The plot for the current parameter, with the slider control bound to
47    /// `intent/set-param`.
48    pub fn plot(&self) -> Expr {
49        let plot = multi_plot_view(&[(format!("param={}", self.param), self.series())]);
50        node(
51            "stack",
52            vec![
53                ("role", sym("sweep")),
54                ("dir", sym("column")),
55                (
56                    "children",
57                    Expr::List(vec![
58                        node(
59                            "slider",
60                            vec![
61                                ("param", sym("slope")),
62                                ("min", number(-5.0)),
63                                ("max", number(5.0)),
64                                ("value", number(self.param)),
65                            ],
66                        ),
67                        plot,
68                    ]),
69                ),
70            ],
71        )
72    }
73
74    /// Apply an `intent/set-param`, updating the parameter and returning the new
75    /// plot. The plot updates live with the parameter.
76    pub fn set_param(&mut self, intent: &Expr) -> Result<Expr> {
77        match intent_field(intent, "kind") {
78            Some(Expr::Symbol(kind)) if &*kind.name == "set-param" => {}
79            _ => return Err(Error::HostError("expected an intent/set-param".to_owned())),
80        }
81        let value = intent_field(intent, "value")
82            .and_then(as_f64)
83            .ok_or_else(|| Error::HostError("set-param 'value' must be a number".to_owned()))?;
84        self.param = value;
85        Ok(self.plot())
86    }
87
88    /// Freeze the current series so it can be compared with later settings.
89    pub fn snapshot(&mut self) {
90        self.snapshots
91            .push((format!("param={}", self.param), self.series()));
92    }
93
94    /// The number of frozen snapshots.
95    pub fn snapshot_count(&self) -> usize {
96        self.snapshots.len()
97    }
98
99    /// An overlaid plot of every snapshot plus the current series, for compare.
100    pub fn compare(&self) -> Expr {
101        let mut series = self.snapshots.clone();
102        series.push((format!("param={} (current)", self.param), self.series()));
103        multi_plot_view(&series)
104    }
105}
106
107/// Build a labelled response sweep from precomputed series.
108pub fn response_sweep_view(
109    lens: &str,
110    role: &str,
111    param: SweepParam<'_>,
112    series_list: &[(String, Vec<(f64, f64)>)],
113) -> Expr {
114    node(
115        "stack",
116        vec![
117            ("lens", sym(lens)),
118            ("role", sym(role)),
119            ("dir", sym("column")),
120            (
121                "children",
122                Expr::List(vec![
123                    node(
124                        "slider",
125                        vec![
126                            ("param", sym(param.name)),
127                            ("min", number(param.min)),
128                            ("max", number(param.max)),
129                            ("value", number(param.value)),
130                        ],
131                    ),
132                    response_plot_view(lens, "response-plot", series_list),
133                ]),
134            ),
135        ],
136    )
137}
138
139/// The parameter controlled by a response sweep.
140pub struct SweepParam<'a> {
141    /// Parameter name used by `intent/set-param`.
142    pub name: &'a str,
143    /// Minimum slider value.
144    pub min: f64,
145    /// Maximum slider value.
146    pub max: f64,
147    /// Current slider value.
148    pub value: f64,
149}