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}