Skip to main content

sim_lib_numbers_numeric/
traits.rs

1//! Backend plugin traits and option types: `Differentiator`, `Quadrature`, and
2//! `OdeSolver`, plus the callable adapter, options, and problem records.
3
4use sim_kernel::{Args, Cx, Error, Result, Symbol, Value};
5use sim_lib_numbers_cas::CasExpr;
6use sim_lib_numbers_func::Func;
7
8/// Common interface for every numeric backend plugin: it reports its method
9/// name and the kind of operation it implements.
10pub trait NumericPlugin: Send + Sync + 'static {
11    /// The method name this plugin registers under (for example, `central` or `rk4`).
12    fn name(&self) -> Symbol;
13    /// The numeric operation kind this plugin implements.
14    fn kind(&self) -> NumericKind;
15}
16
17/// The category of numeric backend, used to route a plugin to the right slot in
18/// the registry.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum NumericKind {
21    /// A differentiator backend (numeric derivative at a point).
22    Differentiator,
23    /// A fixed-rule quadrature backend (definite integral with a set rule).
24    QuadratureFixed,
25    /// An adaptive quadrature backend (definite integral to a tolerance).
26    QuadratureAdaptive,
27    /// A fixed-step ODE solver backend.
28    OdeFixed,
29    /// An adaptive-step ODE solver backend.
30    OdeAdaptive,
31    /// An implicit differential-algebraic equation solver backend.
32    DaeImplicit,
33}
34
35/// Options controlling a numeric-differentiation call.
36///
37/// `method = auto` first uses a symbolic CAS body when the function has one,
38/// then a registered differentiator named by the function's
39/// `differentiator_hint`, then central finite difference.
40#[derive(Clone, Debug)]
41pub struct DiffOpts {
42    /// The differentiator method to use, or `auto` to follow the automatic
43    /// symbolic, hinted-exact, then finite-difference order.
44    pub method: Symbol,
45    /// The finite-difference step size.
46    pub h: f64,
47}
48
49impl DiffOpts {
50    /// Returns default options: the `auto` method with a small default step.
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use sim_lib_numbers_numeric::DiffOpts;
56    ///
57    /// let opts = DiffOpts::auto();
58    /// assert_eq!(opts.method.to_string(), "auto");
59    /// assert!(opts.h > 0.0);
60    /// ```
61    pub fn auto() -> Self {
62        Self {
63            method: Symbol::new("auto"),
64            h: 1.0e-6,
65        }
66    }
67}
68
69/// Options controlling an integration (quadrature) call.
70#[derive(Clone, Debug)]
71pub struct QuadOpts {
72    /// The quadrature method to use, or `auto` to let the registry choose.
73    pub method: Symbol,
74    /// The number of subdivisions, for fixed-rule quadrature.
75    pub n: Option<usize>,
76    /// The error tolerance, for adaptive quadrature.
77    pub tol: Option<f64>,
78}
79
80impl QuadOpts {
81    /// Returns default options for fixed-rule integration (`auto`, no tolerance).
82    pub fn fixed_default() -> Self {
83        Self {
84            method: Symbol::new("auto"),
85            n: None,
86            tol: None,
87        }
88    }
89
90    /// Returns default options for adaptive integration (`auto` with a tolerance).
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use sim_lib_numbers_numeric::QuadOpts;
96    ///
97    /// let fixed = QuadOpts::fixed_default();
98    /// assert!(fixed.tol.is_none());
99    ///
100    /// let adaptive = QuadOpts::adaptive_default();
101    /// assert!(adaptive.tol.is_some());
102    /// ```
103    pub fn adaptive_default() -> Self {
104        Self {
105            method: Symbol::new("auto"),
106            n: None,
107            tol: Some(1.0e-10),
108        }
109    }
110}
111
112/// Options controlling an ODE-solve call.
113#[derive(Clone, Debug)]
114pub struct OdeOpts {
115    /// The ODE solver method to use, or `auto` to let the registry choose.
116    pub method: Symbol,
117    /// The fixed step size, for fixed-step solvers.
118    pub h: Option<f64>,
119    /// The error tolerance, for adaptive solvers.
120    pub tol: Option<f64>,
121    /// An optional cap on the number of integration steps.
122    pub max_steps: Option<usize>,
123}
124
125impl OdeOpts {
126    /// Returns default options for an adaptive ODE solve (`auto` with a tolerance).
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// use sim_lib_numbers_numeric::OdeOpts;
132    ///
133    /// let opts = OdeOpts::default_adaptive();
134    /// assert_eq!(opts.method.to_string(), "auto");
135    /// assert!(opts.tol.is_some());
136    /// assert!(opts.h.is_none());
137    /// ```
138    pub fn default_adaptive() -> Self {
139        Self {
140            method: Symbol::new("auto"),
141            h: None,
142            tol: Some(1.0e-8),
143            max_steps: None,
144        }
145    }
146}
147
148/// A numeric-method callable, either a `Func` value or any ordinary callable
149/// runtime value with the variable symbols supplied by the numeric surface.
150#[derive(Clone)]
151pub struct NumericCallable {
152    value: Value,
153    vars: Vec<Symbol>,
154    body_cas: Option<CasExpr>,
155    differentiator_hint: Option<Symbol>,
156}
157
158enum FuncVarPolicy {
159    Exact,
160    UseFuncVarsWithArity,
161}
162
163impl NumericCallable {
164    /// Builds a unary callable whose `Func` parameter, when present, must match
165    /// the requested variable.
166    pub fn unary(value: Value, var: Symbol) -> Result<Self> {
167        Self::from_value(
168            value,
169            vec![var],
170            FuncVarPolicy::Exact,
171            "numeric methods require the Func parameter to match the requested variable",
172            "numeric methods expect a Func or ordinary callable",
173        )
174    }
175
176    /// Builds a binary callable whose `Func` parameters, when present, must
177    /// match the requested independent and dependent variables.
178    pub fn binary(value: Value, var: Symbol, y_var: Symbol) -> Result<Self> {
179        Self::from_value(
180            value,
181            vec![var, y_var],
182            FuncVarPolicy::Exact,
183            "ode-solve requires the Func parameters to match the requested x and y variables",
184            "ode-solve expects a Func or ordinary callable",
185        )
186    }
187
188    /// Builds a unary sampling callable. `Func` values keep their own variable
189    /// name; ordinary callables use `fallback_var`.
190    pub fn sampled_unary(value: Value, fallback_var: Symbol) -> Result<Self> {
191        Self::from_value(
192            value,
193            vec![fallback_var],
194            FuncVarPolicy::UseFuncVarsWithArity,
195            "numeric sampling requires a unary Func",
196            "numeric sampling expects a Func or ordinary callable",
197        )
198    }
199
200    /// Builds a binary sampling callable. `Func` values keep their own variable
201    /// names; ordinary callables use the supplied fallback names.
202    pub fn sampled_binary(
203        value: Value,
204        fallback_var: Symbol,
205        fallback_y_var: Symbol,
206    ) -> Result<Self> {
207        Self::from_value(
208            value,
209            vec![fallback_var, fallback_y_var],
210            FuncVarPolicy::UseFuncVarsWithArity,
211            "numeric sampling requires a binary Func",
212            "numeric sampling expects a Func or ordinary callable",
213        )
214    }
215
216    fn from_value(
217        value: Value,
218        requested_vars: Vec<Symbol>,
219        policy: FuncVarPolicy,
220        func_mismatch: &str,
221        callable_mismatch: &str,
222    ) -> Result<Self> {
223        if let Some(func) = value.object().downcast_ref::<Func>() {
224            let vars = match policy {
225                FuncVarPolicy::Exact => {
226                    if func.vars.as_slice() != requested_vars.as_slice() {
227                        return Err(Error::Eval(func_mismatch.to_owned()));
228                    }
229                    requested_vars
230                }
231                FuncVarPolicy::UseFuncVarsWithArity => {
232                    if func.vars.len() != requested_vars.len() {
233                        return Err(Error::Eval(func_mismatch.to_owned()));
234                    }
235                    func.vars.clone()
236                }
237            };
238            let body_cas = func.body_cas().cloned();
239            let differentiator_hint = func.metadata.differentiator_hint.clone();
240            return Ok(Self {
241                value,
242                vars,
243                body_cas,
244                differentiator_hint,
245            });
246        }
247        value
248            .object()
249            .as_callable()
250            .ok_or_else(|| Error::Eval(callable_mismatch.to_owned()))?;
251        Ok(Self {
252            value,
253            vars: requested_vars,
254            body_cas: None,
255            differentiator_hint: None,
256        })
257    }
258
259    /// Calls the wrapped value with numeric sample arguments.
260    pub fn call(&self, cx: &mut Cx, args: Vec<Value>) -> Result<Value> {
261        cx.call_value(self.value.clone(), Args::new(args))
262    }
263
264    /// The wrapped runtime value.
265    pub fn value(&self) -> &Value {
266        &self.value
267    }
268
269    /// The wrapped `Func`, when this callable is backed by one.
270    pub fn as_func(&self) -> Option<&Func> {
271        self.value.object().downcast_ref::<Func>()
272    }
273
274    /// Variable symbols associated with this numeric callable.
275    pub fn vars(&self) -> &[Symbol] {
276        &self.vars
277    }
278
279    /// The symbolic CAS body when the wrapped value is a symbolic `Func`.
280    pub fn body_cas(&self) -> Option<&CasExpr> {
281        self.body_cas.as_ref()
282    }
283
284    /// The exact differentiator hint when the wrapped value is a hinted `Func`.
285    pub fn differentiator_hint(&self) -> Option<&Symbol> {
286        self.differentiator_hint.as_ref()
287    }
288}
289
290/// A first-order initial-value ODE problem `dy/dx = f(x, y)` handed to an
291/// [`OdeSolver`].
292pub struct OdeProblem<'a> {
293    /// The right-hand-side function giving the derivative.
294    pub dy: &'a NumericCallable,
295    /// The independent-variable symbol (typically `x`).
296    pub var: &'a Symbol,
297    /// The dependent-variable symbol (typically `y`).
298    pub y_var: &'a Symbol,
299    /// The initial value of the independent variable.
300    pub x0: &'a Value,
301    /// The initial value of the dependent variable.
302    pub y0: &'a Value,
303    /// The end value of the independent variable to integrate toward.
304    pub x_end: &'a Value,
305}
306
307/// A numeric differentiation backend: computes `df/dvar` at a point.
308pub trait Differentiator: NumericPlugin {
309    /// Evaluates the numeric derivative of `f` with respect to `var` at `point`.
310    fn diff_at(
311        &self,
312        cx: &mut Cx,
313        f: &Func,
314        var: &Symbol,
315        point: &Value,
316        opt: DiffOpts,
317    ) -> Result<Value>;
318
319    /// Evaluates the numeric derivative for any numeric callable.
320    ///
321    /// Differentiators that only sample their input should override this hook.
322    /// Exact differentiators that require `Func` metadata inherit the fail-closed
323    /// `Func` adapter.
324    fn diff_callable_at(
325        &self,
326        cx: &mut Cx,
327        f: &NumericCallable,
328        var: &Symbol,
329        point: &Value,
330        opt: DiffOpts,
331    ) -> Result<Value> {
332        let func = f
333            .as_func()
334            .ok_or_else(|| Error::Eval("differentiator requires a Func value".to_owned()))?;
335        self.diff_at(cx, func, var, point, opt)
336    }
337}
338
339/// A numeric integration (quadrature) backend: computes a definite integral.
340pub trait Quadrature: NumericPlugin {
341    /// Integrates `f` over `var` from `lo` to `hi`.
342    fn integrate(
343        &self,
344        cx: &mut Cx,
345        f: &NumericCallable,
346        var: &Symbol,
347        lo: &Value,
348        hi: &Value,
349        opt: QuadOpts,
350    ) -> Result<Value>;
351}
352
353/// A numeric ODE-solving backend: integrates an initial-value problem.
354pub trait OdeSolver: NumericPlugin {
355    /// Solves `problem`, returning the sampled `(x, y)` points of the trajectory.
356    fn solve(
357        &self,
358        cx: &mut Cx,
359        problem: OdeProblem<'_>,
360        opt: OdeOpts,
361    ) -> Result<Vec<(Value, Value)>>;
362}