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