1use std::sync::{Arc, Mutex};
2
3use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
4
5use crate::jit::JitBackendType;
6use crate::{
7 adjoint_checkpoint::AdjointCheckpointWrapper,
8 error::DiffsolRtError,
9 host_array::{FromHostArray, HostArray},
10 initial_condition_options::{
11 InitialConditionSolverOptions, InitialConditionSolverOptionsSnapshot,
12 },
13 linear_solver_type::LinearSolverType,
14 matrix_type::MatrixType,
15 ode_options::{OdeSolverOptions, OdeSolverOptionsSnapshot},
16 ode_solver_type::OdeSolverType,
17 scalar_type::ScalarType,
18 solution_wrapper::SolutionWrapper,
19 solve::Solve,
20};
21
22pub struct Ode {
23 pub(crate) solve: Box<dyn Solve>,
24 code: String,
25 scalar_type: ScalarType,
26 jit_backend: Option<JitBackendType>,
27 linear_solver: LinearSolverType,
28 ode_solver: OdeSolverType,
29}
30
31#[derive(Clone)]
32pub struct OdeWrapper(Arc<Mutex<Ode>>);
33
34const _: () = {
36 const fn assert_send_sync<T: Send + Sync>() {}
37 assert_send_sync::<OdeWrapper>();
38};
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
41struct OdeWrapperSnapshot {
42 code: String,
43 equation: Vec<u8>,
44 jit_backend: JitBackendType,
45 scalar_type: ScalarType,
46 matrix_type: MatrixType,
47 linear_solver: LinearSolverType,
48 ode_solver: OdeSolverType,
49 rtol: f64,
50 atol: f64,
51 t0: f64,
52 h0: f64,
53 integrate_out: bool,
54 sens_rtol: Option<f64>,
55 sens_atol: Option<f64>,
56 out_rtol: Option<f64>,
57 out_atol: Option<f64>,
58 param_rtol: Option<f64>,
59 param_atol: Option<f64>,
60 ic_options: InitialConditionSolverOptionsSnapshot,
61 ode_options: OdeSolverOptionsSnapshot,
62}
63
64impl OdeWrapper {
65 fn guard(&self) -> Result<std::sync::MutexGuard<'_, Ode>, DiffsolRtError> {
66 self.0.lock().map_err(|_| {
67 DiffsolRtError::from(diffsol::error::DiffsolError::Other(
68 "Failed to acquire lock on ODE solver".to_string(),
69 ))
70 })
71 }
72}
73
74impl OdeWrapper {
75 fn snapshot(&self) -> Result<OdeWrapperSnapshot, DiffsolRtError> {
76 let ode = self.guard()?;
77 let jit_backend = ode.jit_backend.ok_or_else(|| {
78 DiffsolRtError::from(diffsol::error::DiffsolError::Other(
79 "OdeWrapper serialization is only supported for JIT-backed solvers".to_string(),
80 ))
81 })?;
82 Ok(OdeWrapperSnapshot {
83 code: ode.code.clone(),
84 equation: ode.solve.serialized_diffsl()?,
85 jit_backend,
86 scalar_type: ode.scalar_type,
87 matrix_type: ode.solve.matrix_type(),
88 linear_solver: ode.linear_solver,
89 ode_solver: ode.ode_solver,
90 rtol: ode.solve.rtol(),
91 atol: ode.solve.atol(),
92 t0: ode.solve.t0(),
93 h0: ode.solve.h0(),
94 integrate_out: ode.solve.integrate_out(),
95 sens_rtol: ode.solve.sens_rtol(),
96 sens_atol: ode.solve.sens_atol(),
97 out_rtol: ode.solve.out_rtol(),
98 out_atol: ode.solve.out_atol(),
99 param_rtol: ode.solve.param_rtol(),
100 param_atol: ode.solve.param_atol(),
101 ic_options: InitialConditionSolverOptionsSnapshot::from_solve(ode.solve.as_ref()),
102 ode_options: OdeSolverOptionsSnapshot::from_solve(ode.solve.as_ref()),
103 })
104 }
105
106 pub(crate) fn build(
107 code: String,
108 scalar_type: ScalarType,
109 solve: Box<dyn Solve>,
110 jit_backend: Option<JitBackendType>,
111 linear_solver: LinearSolverType,
112 ode_solver: OdeSolverType,
113 ) -> Result<Self, DiffsolRtError> {
114 solve.check(linear_solver)?;
115 Ok(OdeWrapper(Arc::new(Mutex::new(Ode {
116 code,
117 scalar_type,
118 solve,
119 jit_backend,
120 linear_solver,
121 ode_solver,
122 }))))
123 }
124
125 fn from_snapshot(snapshot: OdeWrapperSnapshot) -> Result<Self, DiffsolRtError> {
126 let solve = crate::ode_diffsl::solve_factory_from_serialized_diffsl(
127 snapshot.equation.as_slice(),
128 snapshot.matrix_type,
129 snapshot.scalar_type,
130 )?;
131 let wrapper = Self::build(
132 snapshot.code,
133 snapshot.scalar_type,
134 solve,
135 Some(snapshot.jit_backend),
136 snapshot.linear_solver,
137 snapshot.ode_solver,
138 )?;
139 {
140 let mut ode = wrapper.guard()?;
141 ode.solve.set_rtol(snapshot.rtol);
142 ode.solve.set_atol(snapshot.atol);
143 ode.solve.set_t0(snapshot.t0);
144 ode.solve.set_h0(snapshot.h0);
145 ode.solve.set_integrate_out(snapshot.integrate_out);
146 ode.solve.set_sens_rtol(snapshot.sens_rtol);
147 ode.solve.set_sens_atol(snapshot.sens_atol);
148 ode.solve.set_out_rtol(snapshot.out_rtol);
149 ode.solve.set_out_atol(snapshot.out_atol);
150 ode.solve.set_param_rtol(snapshot.param_rtol);
151 ode.solve.set_param_atol(snapshot.param_atol);
152 snapshot.ic_options.apply_to_solve(ode.solve.as_mut());
153 snapshot.ode_options.apply_to_solve(ode.solve.as_mut());
154 }
155 Ok(wrapper)
156 }
157
158 pub fn get_matrix_type(&self) -> Result<MatrixType, DiffsolRtError> {
160 Ok(self.guard()?.solve.matrix_type())
161 }
162
163 pub fn get_nstates(&self) -> Result<usize, DiffsolRtError> {
164 Ok(self.guard()?.solve.nstates())
165 }
166
167 pub fn get_nparams(&self) -> Result<usize, DiffsolRtError> {
168 Ok(self.guard()?.solve.nparams())
169 }
170
171 pub fn get_nout(&self) -> Result<usize, DiffsolRtError> {
172 Ok(self.guard()?.solve.nout())
173 }
174
175 pub fn has_stop(&self) -> Result<bool, DiffsolRtError> {
176 Ok(self.guard()?.solve.has_stop())
177 }
178
179 pub fn get_ode_solver(&self) -> Result<OdeSolverType, DiffsolRtError> {
181 Ok(self.guard()?.ode_solver)
182 }
183
184 pub fn set_ode_solver(&self, value: OdeSolverType) -> Result<(), DiffsolRtError> {
185 self.guard()?.ode_solver = value;
186 Ok(())
187 }
188
189 pub fn get_linear_solver(&self) -> Result<LinearSolverType, DiffsolRtError> {
192 Ok(self.guard()?.linear_solver)
193 }
194
195 pub fn set_linear_solver(&self, value: LinearSolverType) -> Result<(), DiffsolRtError> {
196 self.guard()?.solve.check(value)?;
197 self.guard()?.linear_solver = value;
198 Ok(())
199 }
200
201 pub fn get_rtol(&self) -> Result<f64, DiffsolRtError> {
203 Ok(self.guard()?.solve.rtol())
204 }
205
206 pub fn set_rtol(&self, value: f64) -> Result<(), DiffsolRtError> {
207 self.guard()?.solve.set_rtol(value);
208 Ok(())
209 }
210
211 pub fn get_atol(&self) -> Result<f64, DiffsolRtError> {
213 Ok(self.guard()?.solve.atol())
214 }
215
216 pub fn set_atol(&self, value: f64) -> Result<(), DiffsolRtError> {
217 self.guard()?.solve.set_atol(value);
218 Ok(())
219 }
220
221 pub fn get_t0(&self) -> Result<f64, DiffsolRtError> {
223 Ok(self.guard()?.solve.t0())
224 }
225
226 pub fn set_t0(&self, value: f64) -> Result<(), DiffsolRtError> {
227 self.guard()?.solve.set_t0(value);
228 Ok(())
229 }
230
231 pub fn get_h0(&self) -> Result<f64, DiffsolRtError> {
233 Ok(self.guard()?.solve.h0())
234 }
235
236 pub fn set_h0(&self, value: f64) -> Result<(), DiffsolRtError> {
237 self.guard()?.solve.set_h0(value);
238 Ok(())
239 }
240
241 pub fn get_integrate_out(&self) -> Result<bool, DiffsolRtError> {
243 Ok(self.guard()?.solve.integrate_out())
244 }
245
246 pub fn set_integrate_out(&self, value: bool) -> Result<(), DiffsolRtError> {
247 self.guard()?.solve.set_integrate_out(value);
248 Ok(())
249 }
250
251 pub fn get_sens_rtol(&self) -> Result<Option<f64>, DiffsolRtError> {
253 Ok(self.guard()?.solve.sens_rtol())
254 }
255
256 pub fn set_sens_rtol(&self, value: Option<f64>) -> Result<(), DiffsolRtError> {
257 self.guard()?.solve.set_sens_rtol(value);
258 Ok(())
259 }
260
261 pub fn get_sens_atol(&self) -> Result<Option<f64>, DiffsolRtError> {
263 Ok(self.guard()?.solve.sens_atol())
264 }
265
266 pub fn set_sens_atol(&self, value: Option<f64>) -> Result<(), DiffsolRtError> {
267 self.guard()?.solve.set_sens_atol(value);
268 Ok(())
269 }
270
271 pub fn get_out_rtol(&self) -> Result<Option<f64>, DiffsolRtError> {
273 Ok(self.guard()?.solve.out_rtol())
274 }
275
276 pub fn set_out_rtol(&self, value: Option<f64>) -> Result<(), DiffsolRtError> {
277 self.guard()?.solve.set_out_rtol(value);
278 Ok(())
279 }
280
281 pub fn get_out_atol(&self) -> Result<Option<f64>, DiffsolRtError> {
283 Ok(self.guard()?.solve.out_atol())
284 }
285
286 pub fn set_out_atol(&self, value: Option<f64>) -> Result<(), DiffsolRtError> {
287 self.guard()?.solve.set_out_atol(value);
288 Ok(())
289 }
290
291 pub fn get_param_rtol(&self) -> Result<Option<f64>, DiffsolRtError> {
293 Ok(self.guard()?.solve.param_rtol())
294 }
295
296 pub fn set_param_rtol(&self, value: Option<f64>) -> Result<(), DiffsolRtError> {
297 self.guard()?.solve.set_param_rtol(value);
298 Ok(())
299 }
300
301 pub fn get_param_atol(&self) -> Result<Option<f64>, DiffsolRtError> {
303 Ok(self.guard()?.solve.param_atol())
304 }
305
306 pub fn set_param_atol(&self, value: Option<f64>) -> Result<(), DiffsolRtError> {
307 self.guard()?.solve.set_param_atol(value);
308 Ok(())
309 }
310
311 pub fn get_code(&self) -> Result<String, DiffsolRtError> {
312 Ok(self.guard()?.code.clone())
313 }
314
315 pub fn get_scalar_type(&self) -> Result<ScalarType, DiffsolRtError> {
316 Ok(self.guard()?.scalar_type)
317 }
318
319 pub fn get_jit_backend(&self) -> Result<Option<JitBackendType>, DiffsolRtError> {
320 Ok(self.guard()?.jit_backend)
321 }
322
323 pub fn get_ic_options(&self) -> InitialConditionSolverOptions {
324 InitialConditionSolverOptions::new(self.0.clone())
325 }
326
327 pub fn get_options(&self) -> OdeSolverOptions {
328 OdeSolverOptions::new(self.0.clone())
329 }
330
331 pub fn y0(&self, params: HostArray) -> Result<HostArray, DiffsolRtError> {
333 let mut self_guard = self.guard()?;
334 self_guard.solve.y0(params.as_slice()?)
335 }
336
337 pub fn rhs(
339 &self,
340 params: HostArray,
341 t: f64,
342 y: HostArray,
343 ) -> Result<HostArray, DiffsolRtError> {
344 let mut self_guard = self.guard()?;
345 self_guard.solve.rhs(params.as_slice()?, t, y.as_slice()?)
346 }
347
348 pub fn rhs_jac_mul(
350 &self,
351 params: HostArray,
352 t: f64,
353 y: HostArray,
354 v: HostArray,
355 ) -> Result<HostArray, DiffsolRtError> {
356 let mut self_guard = self.guard()?;
357 self_guard
358 .solve
359 .rhs_jac_mul(params.as_slice()?, t, y.as_slice()?, v.as_slice()?)
360 }
361
362 #[allow(clippy::type_complexity)]
380 pub fn solve(
381 &self,
382 params: HostArray,
383 final_time: f64,
384 ) -> Result<SolutionWrapper, DiffsolRtError> {
385 let mut self_guard = self.guard()?;
386 let params = params.as_slice()?;
387 let linear_solver = self_guard.linear_solver;
388 let method = self_guard.ode_solver;
389 let solution = self_guard
390 .solve
391 .solve(method, linear_solver, params, final_time)?;
392 Ok(SolutionWrapper::new(solution))
393 }
394
395 pub fn solve_dense(
409 &self,
410 params: HostArray,
411 t_eval: HostArray,
412 ) -> Result<SolutionWrapper, DiffsolRtError> {
413 let mut self_guard = self.guard()?;
414 let params = params.as_slice()?;
415 let t_eval = t_eval.as_slice()?;
416 let linear_solver = self_guard.linear_solver;
417 let method = self_guard.ode_solver;
418 let solution = self_guard
419 .solve
420 .solve_dense(method, linear_solver, params, t_eval)?;
421 Ok(SolutionWrapper::new(solution))
422 }
423
424 #[allow(clippy::type_complexity)]
437 pub fn solve_fwd_sens(
438 &self,
439 params: HostArray,
440 t_eval: HostArray,
441 ) -> Result<SolutionWrapper, DiffsolRtError> {
442 let mut self_guard = self.guard()?;
443 let params = params.as_slice()?;
444 let t_eval = t_eval.as_slice()?;
445 let linear_solver = self_guard.linear_solver;
446 let method = self_guard.ode_solver;
447 let solution = self_guard
448 .solve
449 .solve_fwd_sens(method, linear_solver, params, t_eval)?;
450 Ok(SolutionWrapper::new(solution))
451 }
452
453 pub fn solve_continuous_adjoint(
459 &self,
460 params: HostArray,
461 final_time: f64,
462 ) -> Result<(HostArray, HostArray), DiffsolRtError> {
463 let mut self_guard = self.guard()?;
464 let linear_solver = self_guard.linear_solver;
465 let ode_solver = self_guard.ode_solver;
466 self_guard.solve.solve_continuous_adjoint(
467 ode_solver,
468 linear_solver,
469 params.as_slice()?,
470 final_time,
471 )
472 }
473
474 pub fn solve_adjoint_fwd(
477 &self,
478 params: HostArray,
479 t_eval: HostArray,
480 ) -> Result<(SolutionWrapper, AdjointCheckpointWrapper), DiffsolRtError> {
481 let mut self_guard = self.guard()?;
482 let params = params.as_slice()?;
483 let t_eval = t_eval.as_slice()?;
484 let linear_solver = self_guard.linear_solver;
485 let method = self_guard.ode_solver;
486 let (solution, checkpoint) =
487 self_guard
488 .solve
489 .solve_adjoint_fwd(method, linear_solver, params, t_eval)?;
490 Ok((SolutionWrapper::new(solution), checkpoint))
491 }
492
493 pub fn solve_adjoint_bkwd(
499 &self,
500 solution: &SolutionWrapper,
501 checkpoint: &AdjointCheckpointWrapper,
502 dgdu_eval: HostArray,
503 ) -> Result<HostArray, DiffsolRtError> {
504 let t_eval_host = solution.get_ts()?;
505 let t_eval = Vec::<f64>::from_host_array(t_eval_host)?;
506 let mut self_guard = self.guard()?;
507 let linear_solver = self_guard.linear_solver;
508 let method = self_guard.ode_solver;
509 self_guard
510 .solve
511 .solve_adjoint_bkwd(method, linear_solver, checkpoint, &t_eval, dgdu_eval)
512 }
513}
514
515impl Serialize for OdeWrapper {
516 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
517 where
518 S: Serializer,
519 {
520 self.snapshot()
521 .map_err(serde::ser::Error::custom)?
522 .serialize(serializer)
523 }
524}
525
526impl<'de> Deserialize<'de> for OdeWrapper {
527 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
528 where
529 D: Deserializer<'de>,
530 {
531 let snapshot = OdeWrapperSnapshot::deserialize(deserializer)?;
532 Self::from_snapshot(snapshot).map_err(DeError::custom)
533 }
534}
535
536#[cfg(all(test, feature = "diffsl-external-f64"))]
537mod tests {
538 use super::*;
539 use crate::host_array::FromHostArray;
540 use crate::linear_solver_type::LinearSolverType;
541 use crate::scalar_type::ScalarType;
542 use crate::test_support::{
543 assert_close, assert_solution_tail, logistic_state, logistic_state_dr, mass_state_deps,
544 rhs_input_deps, rhs_state_deps, vector_host, ASSERT_TOL, LOGISTIC_X0,
545 };
546
547 fn all_ode_solvers() -> [OdeSolverType; 4] {
548 [
549 OdeSolverType::Bdf,
550 OdeSolverType::Esdirk34,
551 OdeSolverType::TrBdf2,
552 OdeSolverType::Tsit45,
553 ]
554 }
555
556 fn make_ode(matrix_type: MatrixType, ode_solver: OdeSolverType) -> OdeWrapper {
557 OdeWrapper::new_external(
558 rhs_state_deps(),
559 rhs_input_deps(),
560 mass_state_deps(),
561 ScalarType::F64,
562 matrix_type,
563 LinearSolverType::Default,
564 ode_solver,
565 )
566 .unwrap()
567 }
568
569 fn assert_runtime_dispatch(matrix_type: MatrixType) {
570 let ode = make_ode(matrix_type, OdeSolverType::Bdf);
571 assert_eq!(ode.get_matrix_type().unwrap(), matrix_type);
572 assert_eq!(ode.get_nstates().unwrap(), 1);
573 assert_eq!(ode.get_nparams().unwrap(), 1);
574 assert_eq!(ode.get_nout().unwrap(), 1);
575 assert!(ode.has_stop().unwrap());
576
577 let y0 = ode.y0(vector_host(&[2.0])).unwrap();
578 assert_eq!(Vec::<f64>::from_host_array(y0).unwrap(), vec![LOGISTIC_X0]);
579
580 let rhs = ode
581 .rhs(vector_host(&[2.0]), 0.0, vector_host(&[0.25]))
582 .unwrap();
583 assert_close(
584 Vec::<f64>::from_host_array(rhs).unwrap()[0],
585 0.375,
586 ASSERT_TOL,
587 "rhs(0.25)",
588 );
589
590 let rhs_jac_mul = ode
591 .rhs_jac_mul(
592 vector_host(&[2.0]),
593 0.0,
594 vector_host(&[0.25]),
595 vector_host(&[3.0]),
596 )
597 .unwrap();
598 assert_close(
599 Vec::<f64>::from_host_array(rhs_jac_mul).unwrap()[0],
600 3.0,
601 ASSERT_TOL,
602 "rhs_jac_mul(0.25, 3.0)",
603 );
604 }
605
606 fn assert_solver_dense_solution(matrix_type: MatrixType, ode_solver: OdeSolverType) {
607 let ode = make_ode(matrix_type, ode_solver);
608 ode.set_rtol(1e-8).unwrap();
609 ode.set_atol(1e-8).unwrap();
610
611 let t_eval = [0.25, 0.5, 1.0];
612 let solution = ode
613 .solve_dense(vector_host(&[2.0]), vector_host(&t_eval))
614 .unwrap();
615
616 assert_solution_tail(&solution, &t_eval, LOGISTIC_X0, 2.0, 5e-4);
617 }
618
619 fn hybrid_root_time() -> f64 {
620 0.5 * 9.0_f64.ln()
621 }
622
623 fn assert_hybrid_solution_applies_reset_after_root(ode_solver: OdeSolverType) {
624 let ode = make_ode(MatrixType::NalgebraDense, ode_solver);
625 ode.set_rtol(1e-8).unwrap();
626 ode.set_atol(1e-8).unwrap();
627
628 let final_time = 2.0;
629 let solution = ode.solve(vector_host(&[2.0]), final_time).unwrap();
630 let ys = solution.get_ys().unwrap();
631 let ys = ys.as_array::<f64>().unwrap();
632 let ts = Vec::<f64>::from_host_array(solution.get_ts().unwrap()).unwrap();
633 let root_time = hybrid_root_time();
634
635 assert_eq!(ys.nrows(), 1);
636 assert_eq!(ys.ncols(), ts.len());
637 assert!(!ts.is_empty(), "expected hybrid solve to produce output");
638 assert_close(
639 *ts.last().unwrap(),
640 final_time,
641 ASSERT_TOL,
642 "hybrid final time",
643 );
644 assert_close(ys[(0, ys.ncols() - 1)], 1.0, 5e-4, "hybrid final value");
645 assert!(
646 ts.iter().any(|&t| t < root_time),
647 "expected pre-root samples"
648 );
649 assert!(
650 ts.iter().any(|&t| t > root_time),
651 "expected post-root samples after reset"
652 );
653 }
654
655 fn assert_hybrid_dense_solution_continues_after_reset(ode_solver: OdeSolverType) {
656 let ode = make_ode(MatrixType::NalgebraDense, ode_solver);
657 ode.set_rtol(1e-8).unwrap();
658 ode.set_atol(1e-8).unwrap();
659
660 let t_eval = [0.5, 1.0, 1.25, 1.5, 2.0];
661 let solution = ode
662 .solve_dense(vector_host(&[2.0]), vector_host(&t_eval))
663 .unwrap();
664 let ys = solution.get_ys().unwrap();
665 let ys = ys.as_array::<f64>().unwrap();
666
667 assert_eq!(ys.nrows(), 1);
668 assert_eq!(ys.ncols(), t_eval.len());
669 assert_close(
670 ys[(0, 0)],
671 logistic_state(LOGISTIC_X0, 2.0, t_eval[0]),
672 5e-4,
673 "hybrid dense pre-root value",
674 );
675 assert_close(
676 ys[(0, 1)],
677 logistic_state(LOGISTIC_X0, 2.0, t_eval[1]),
678 5e-4,
679 "hybrid dense near-root value",
680 );
681 for col in 2..t_eval.len() {
682 assert_close(ys[(0, col)], 1.0, 5e-4, "hybrid dense post-root value");
683 }
684 }
685
686 fn assert_hybrid_forward_sensitivities_complete_across_reset(ode_solver: OdeSolverType) {
687 let ode = make_ode(MatrixType::NalgebraDense, ode_solver);
688 ode.set_rtol(1e-8).unwrap();
689 ode.set_atol(1e-8).unwrap();
690
691 let t_eval = [0.5, 1.0, 1.25, 1.5, 2.0];
692 let solution = ode
693 .solve_fwd_sens(vector_host(&[2.0]), vector_host(&t_eval))
694 .unwrap();
695 let ys = solution.get_ys().unwrap();
696 let ys = ys.as_array::<f64>().unwrap();
697 let sens = solution.get_sens().unwrap();
698
699 assert_eq!(ys.nrows(), 1);
700 assert_eq!(ys.ncols(), t_eval.len());
701 assert_eq!(sens.len(), 1);
702 let sens_values = sens[0].as_array::<f64>().unwrap();
703 assert_eq!(sens_values.nrows(), 1);
704 assert_eq!(sens_values.ncols(), t_eval.len());
705 assert_close(
706 ys[(0, 0)],
707 logistic_state(LOGISTIC_X0, 2.0, t_eval[0]),
708 5e-4,
709 "hybrid sens pre-root value",
710 );
711 for col in 2..t_eval.len() {
712 assert_close(ys[(0, col)], 1.0, 5e-4, "hybrid sens post-root value");
713 assert!(
714 sens_values[(0, col)].is_finite(),
715 "expected finite post-root sensitivity at column {col}"
716 );
717 }
718 }
719
720 #[test]
721 fn runtime_dispatch_matches_requested_matrix_type() {
722 for matrix_type in [
723 MatrixType::NalgebraDense,
724 MatrixType::FaerDense,
725 MatrixType::FaerSparse,
726 ] {
727 assert_runtime_dispatch(matrix_type);
728 }
729 }
730
731 #[test]
732 fn bdf_dense_solution_matches_logistic_solution() {
733 let ode = make_ode(MatrixType::NalgebraDense, OdeSolverType::Bdf);
734 ode.set_rtol(1e-8).unwrap();
735 ode.set_atol(1e-8).unwrap();
736
737 let t_eval = [0.25, 0.5, 1.0];
738 let solution = ode
739 .solve_dense(vector_host(&[2.0]), vector_host(&t_eval))
740 .unwrap();
741
742 assert_solution_tail(&solution, &t_eval, LOGISTIC_X0, 2.0, 5e-4);
743 }
744
745 #[test]
746 fn esdirk34_dense_solution_matches_logistic_solution() {
747 assert_solver_dense_solution(MatrixType::FaerDense, OdeSolverType::Esdirk34);
748 }
749
750 #[test]
751 fn tr_bdf2_sparse_solution_matches_logistic_solution() {
752 assert_solver_dense_solution(MatrixType::FaerSparse, OdeSolverType::TrBdf2);
753 }
754
755 #[test]
756 fn tsit45_dense_solution_matches_logistic_solution() {
757 assert_solver_dense_solution(MatrixType::NalgebraDense, OdeSolverType::Tsit45);
758 }
759
760 #[test]
761 fn bdf_forward_sensitivities_match_logistic_derivative() {
762 let ode = make_ode(MatrixType::NalgebraDense, OdeSolverType::Bdf);
763 ode.set_rtol(1e-8).unwrap();
764 ode.set_atol(1e-8).unwrap();
765 ode.set_sens_rtol(Some(1e-6)).unwrap();
766 ode.set_sens_atol(Some(1e-6)).unwrap();
767
768 let t_eval = [0.25, 0.5, 1.0];
769 let solution = ode
770 .solve_fwd_sens(vector_host(&[2.0]), vector_host(&t_eval))
771 .unwrap();
772
773 assert_solution_tail(&solution, &t_eval, LOGISTIC_X0, 2.0, 5e-4);
774 let sens = solution.get_sens().unwrap();
775 assert_eq!(sens.len(), 1);
776 let sens_values = sens[0].as_array::<f64>().unwrap();
777 assert_eq!(sens_values.nrows(), 1);
778 assert_eq!(sens_values.ncols(), t_eval.len());
779 for (i, &t) in t_eval.iter().enumerate() {
780 assert_close(
781 sens_values[(0, i)],
782 logistic_state_dr(LOGISTIC_X0, 2.0, t),
783 ASSERT_TOL,
784 &format!("sensitivity[{i}]"),
785 );
786 }
787 }
788
789 #[test]
790 fn hybrid_solution_applies_reset_after_root_for_all_solvers() {
791 for ode_solver in all_ode_solvers() {
792 assert_hybrid_solution_applies_reset_after_root(ode_solver);
793 }
794 }
795
796 #[test]
797 fn hybrid_dense_solution_continues_after_reset_for_all_solvers() {
798 for ode_solver in all_ode_solvers() {
799 assert_hybrid_dense_solution_continues_after_reset(ode_solver);
800 }
801 }
802
803 #[test]
804 fn hybrid_forward_sensitivities_complete_across_reset_for_all_solvers() {
805 for ode_solver in all_ode_solvers() {
806 assert_hybrid_forward_sensitivities_complete_across_reset(ode_solver);
807 }
808 }
809}
810
811#[cfg(all(test, feature = "diffsl-external-dynamic"))]
812mod dynamic_tests {
813 use crate::host_array::FromHostArray;
814 use crate::linear_solver_type::LinearSolverType;
815 use crate::scalar_type::ScalarType;
816 use crate::test_support::{
817 assert_close, assert_solution_tail, external_dynamic_fixture_path, mass_state_deps,
818 rhs_input_deps, rhs_state_deps, vector_host, ASSERT_TOL, LOGISTIC_X0,
819 };
820
821 use super::*;
822
823 fn make_ode(matrix_type: MatrixType, ode_solver: OdeSolverType) -> OdeWrapper {
824 OdeWrapper::new_external_dynamic(
825 external_dynamic_fixture_path(),
826 rhs_state_deps(),
827 rhs_input_deps(),
828 mass_state_deps(),
829 ScalarType::F64,
830 matrix_type,
831 LinearSolverType::Default,
832 ode_solver,
833 )
834 .unwrap()
835 }
836
837 #[test]
838 fn runtime_dispatch_matches_requested_matrix_type() {
839 for matrix_type in [
840 MatrixType::NalgebraDense,
841 MatrixType::FaerDense,
842 MatrixType::FaerSparse,
843 ] {
844 let ode = make_ode(matrix_type, OdeSolverType::Bdf);
845 assert_eq!(ode.get_matrix_type().unwrap(), matrix_type);
846 assert_eq!(ode.get_code().unwrap(), "");
847 assert_eq!(ode.get_jit_backend().unwrap(), None);
848 assert_eq!(ode.get_nstates().unwrap(), 1);
849 assert_eq!(ode.get_nparams().unwrap(), 1);
850 assert_eq!(ode.get_nout().unwrap(), 1);
851 assert!(ode.has_stop().unwrap());
852
853 let y0 = ode.y0(vector_host(&[2.0])).unwrap();
854 assert_eq!(Vec::<f64>::from_host_array(y0).unwrap(), vec![LOGISTIC_X0]);
855
856 let rhs = ode
857 .rhs(vector_host(&[2.0]), 0.0, vector_host(&[0.25]))
858 .unwrap();
859 assert_close(
860 Vec::<f64>::from_host_array(rhs).unwrap()[0],
861 0.375,
862 ASSERT_TOL,
863 "rhs(0.25)",
864 );
865
866 let rhs_jac_mul = ode
867 .rhs_jac_mul(
868 vector_host(&[2.0]),
869 0.0,
870 vector_host(&[0.25]),
871 vector_host(&[3.0]),
872 )
873 .unwrap();
874 assert_close(
875 Vec::<f64>::from_host_array(rhs_jac_mul).unwrap()[0],
876 3.0,
877 ASSERT_TOL,
878 "rhs_jac_mul(0.25, 3.0)",
879 );
880 }
881 }
882
883 #[test]
884 fn dense_solution_matches_logistic_solution() {
885 let ode = make_ode(MatrixType::NalgebraDense, OdeSolverType::Bdf);
886 ode.set_rtol(1e-8).unwrap();
887 ode.set_atol(1e-8).unwrap();
888
889 let t_eval = [0.25, 0.5, 1.0];
890 let solution = ode
891 .solve_dense(vector_host(&[2.0]), vector_host(&t_eval))
892 .unwrap();
893
894 assert_solution_tail(&solution, &t_eval, LOGISTIC_X0, 2.0, 5e-4);
895 }
896
897 #[test]
898 fn non_jit_serialization_is_rejected() {
899 let ode = make_ode(MatrixType::NalgebraDense, OdeSolverType::Bdf);
900 let err = serde_json::to_string(&ode).unwrap_err().to_string();
901 assert!(err.contains("JIT-backed"));
902 }
903}