Skip to main content

diffsol_c/
ode_diffsl.rs

1//! DiffSL equations: the [OdeWrapper] constructors, the solver factories, and the
2//! [SolveEqn] impls for each code generation backend.
3
4#[cfg(feature = "diffsl-external-dynamic")]
5use std::path::PathBuf;
6
7use diffsol::{
8    error::DiffsolError, matrix::Matrix, DefaultDenseMatrix, DiffSl, ObjectModule, Vector,
9};
10#[cfg(any(feature = "diffsl-cranelift", feature = "diffsl-llvm"))]
11use diffsol::{CodegenModule, CodegenModuleCompile, CodegenModuleJit, OdeBuilder};
12
13use crate::error::DiffsolRtError;
14#[cfg(any(feature = "diffsl-cranelift", feature = "diffsl-llvm"))]
15use crate::jit::JitBackendType;
16use crate::linear_solver_type::LinearSolverType;
17use crate::matrix_type::MatrixType;
18use crate::ode::OdeWrapper;
19use crate::ode_solver_type::OdeSolverType;
20#[cfg(feature = "external")]
21use crate::scalar_type::ExternalScalar;
22use crate::scalar_type::{Scalar, ScalarType};
23use crate::solve::{GenericSolve, Solve};
24use crate::solve_eqn::{unsupported, EqnRequirement, SolveEqn};
25
26/// A [`GenericSolve`] over DiffSL equations compiled with the code generation backend `CG`.
27pub(crate) type DiffSlSolve<M, CG> = GenericSolve<M, DiffSl<M, CG>>;
28
29impl OdeWrapper {
30    /// Construct an ODE solver backed by externally-provided DiffSL symbols.
31    #[cfg(feature = "external")]
32    pub fn new_external(
33        rhs_state_deps: Vec<(usize, usize)>,
34        rhs_input_deps: Vec<(usize, usize)>,
35        mass_state_deps: Vec<(usize, usize)>,
36        scalar_type: ScalarType,
37        matrix_type: MatrixType,
38        linear_solver: LinearSolverType,
39        ode_solver: OdeSolverType,
40    ) -> Result<Self, DiffsolRtError> {
41        let solve = solve_factory_external(
42            rhs_state_deps,
43            rhs_input_deps,
44            mass_state_deps,
45            matrix_type,
46            scalar_type,
47        )?;
48        Self::build(
49            String::new(),
50            scalar_type,
51            solve,
52            None,
53            linear_solver,
54            ode_solver,
55        )
56    }
57
58    /// Construct an ODE solver backed by DiffSL symbols loaded from a dynamic library.
59    #[cfg(feature = "diffsl-external-dynamic")]
60    #[allow(clippy::too_many_arguments)]
61    pub fn new_external_dynamic(
62        path: impl Into<std::path::PathBuf>,
63        rhs_state_deps: Vec<(usize, usize)>,
64        rhs_input_deps: Vec<(usize, usize)>,
65        mass_state_deps: Vec<(usize, usize)>,
66        scalar_type: ScalarType,
67        matrix_type: MatrixType,
68        linear_solver: LinearSolverType,
69        ode_solver: OdeSolverType,
70    ) -> Result<Self, DiffsolRtError> {
71        let solve = solve_factory_external_dynamic(
72            path.into(),
73            rhs_state_deps,
74            rhs_input_deps,
75            mass_state_deps,
76            matrix_type,
77            scalar_type,
78        )?;
79        Self::build(
80            String::new(),
81            scalar_type,
82            solve,
83            None,
84            linear_solver,
85            ode_solver,
86        )
87    }
88
89    /// Construct an ODE solver by JIT-compiling DiffSL code immediately.
90    #[cfg(any(feature = "diffsl-cranelift", feature = "diffsl-llvm"))]
91    pub fn new_jit(
92        code: &str,
93        jit_backend: JitBackendType,
94        scalar_type: ScalarType,
95        matrix_type: MatrixType,
96        linear_solver: LinearSolverType,
97        ode_solver: OdeSolverType,
98    ) -> Result<Self, DiffsolRtError> {
99        let solve = solve_factory_jit(code, jit_backend, matrix_type, scalar_type)?;
100        Self::build(
101            code.to_owned(),
102            scalar_type,
103            solve,
104            Some(jit_backend),
105            linear_solver,
106            ode_solver,
107        )
108    }
109}
110
111// Public factory method for generating an instance based on matrix type
112#[cfg(feature = "external")]
113pub(crate) fn solve_factory_external(
114    rhs_state_deps: Vec<(usize, usize)>,
115    rhs_input_deps: Vec<(usize, usize)>,
116    mass_state_deps: Vec<(usize, usize)>,
117    matrix_type: MatrixType,
118    scalar_type: ScalarType,
119) -> Result<Box<dyn Solve>, DiffsolRtError> {
120    // the C ABI only carries the rhs and mass patterns; the out, reset and stop operators' are
121    // left empty, which is the same "no non-zeros" contract the other lists already use
122    let deps = diffsol::DiffSlDeps {
123        rhs_state: rhs_state_deps,
124        rhs_input: rhs_input_deps,
125        mass_state: mass_state_deps,
126        ..Default::default()
127    };
128    let solve: Box<dyn Solve> = match matrix_type {
129        MatrixType::NalgebraDense => match scalar_type {
130            #[cfg(feature = "diffsl-external-f32")]
131            ScalarType::F32 => Box::new(DiffSlSolve::<
132                diffsol::NalgebraMat<f32>,
133                diffsl::ExternalModule<f32>,
134            >::from_external(deps, false)?),
135            #[cfg(feature = "diffsl-external-f64")]
136            ScalarType::F64 => Box::new(DiffSlSolve::<
137                diffsol::NalgebraMat<f64>,
138                diffsl::ExternalModule<f64>,
139            >::from_external(deps, false)?),
140            _ => {
141                return Err(DiffsolRtError::from(DiffsolError::Other(
142                    "Unsupported scalar type for NalgebraDense".to_string(),
143                )));
144            }
145        },
146        MatrixType::FaerDense => match scalar_type {
147            #[cfg(feature = "diffsl-external-f32")]
148            ScalarType::F32 => Box::new(DiffSlSolve::<
149                diffsol::FaerMat<f32>,
150                diffsl::ExternalModule<f32>,
151            >::from_external(deps, false)?),
152            #[cfg(feature = "diffsl-external-f64")]
153            ScalarType::F64 => Box::new(DiffSlSolve::<
154                diffsol::FaerMat<f64>,
155                diffsl::ExternalModule<f64>,
156            >::from_external(deps, false)?),
157            _ => {
158                return Err(DiffsolRtError::from(DiffsolError::Other(
159                    "Unsupported scalar type for FaerDense".to_string(),
160                )));
161            }
162        },
163        MatrixType::FaerSparse => match scalar_type {
164            #[cfg(feature = "diffsl-external-f32")]
165            ScalarType::F32 => Box::new(DiffSlSolve::<
166                diffsol::FaerSparseMat<f32>,
167                diffsl::ExternalModule<f32>,
168            >::from_external(deps, false)?),
169            #[cfg(feature = "diffsl-external-f64")]
170            ScalarType::F64 => Box::new(DiffSlSolve::<
171                diffsol::FaerSparseMat<f64>,
172                diffsl::ExternalModule<f64>,
173            >::from_external(deps, false)?),
174            _ => {
175                return Err(DiffsolRtError::from(DiffsolError::Other(
176                    "Unsupported scalar type for FaerSparse".to_string(),
177                )));
178            }
179        },
180    };
181    Ok(solve)
182}
183
184#[cfg(feature = "diffsl-external-dynamic")]
185pub(crate) fn solve_factory_external_dynamic(
186    path: PathBuf,
187    rhs_state_deps: Vec<(usize, usize)>,
188    rhs_input_deps: Vec<(usize, usize)>,
189    mass_state_deps: Vec<(usize, usize)>,
190    matrix_type: MatrixType,
191    scalar_type: ScalarType,
192) -> Result<Box<dyn Solve>, DiffsolRtError> {
193    // see `solve_factory_external`: the C ABI carries only the rhs and mass patterns
194    let deps = diffsol::DiffSlDeps {
195        rhs_state: rhs_state_deps,
196        rhs_input: rhs_input_deps,
197        mass_state: mass_state_deps,
198        ..Default::default()
199    };
200    let solve: Box<dyn Solve> = match matrix_type {
201        MatrixType::NalgebraDense => match scalar_type {
202            ScalarType::F32 => Box::new(DiffSlSolve::<
203                diffsol::NalgebraMat<f32>,
204                diffsl::ExternalDynModule<f32>,
205            >::from_external_dynamic(path, deps, false)?),
206            ScalarType::F64 => Box::new(DiffSlSolve::<
207                diffsol::NalgebraMat<f64>,
208                diffsl::ExternalDynModule<f64>,
209            >::from_external_dynamic(path, deps, false)?),
210        },
211        MatrixType::FaerDense => match scalar_type {
212            ScalarType::F32 => Box::new(DiffSlSolve::<
213                diffsol::FaerMat<f32>,
214                diffsl::ExternalDynModule<f32>,
215            >::from_external_dynamic(path, deps, false)?),
216            ScalarType::F64 => Box::new(DiffSlSolve::<
217                diffsol::FaerMat<f64>,
218                diffsl::ExternalDynModule<f64>,
219            >::from_external_dynamic(path, deps, false)?),
220        },
221        MatrixType::FaerSparse => match scalar_type {
222            ScalarType::F32 => Box::new(DiffSlSolve::<
223                diffsol::FaerSparseMat<f32>,
224                diffsl::ExternalDynModule<f32>,
225            >::from_external_dynamic(path, deps, false)?),
226            ScalarType::F64 => Box::new(DiffSlSolve::<
227                diffsol::FaerSparseMat<f64>,
228                diffsl::ExternalDynModule<f64>,
229            >::from_external_dynamic(path, deps, false)?),
230        },
231    };
232    Ok(solve)
233}
234
235#[cfg(any(feature = "diffsl-cranelift", feature = "diffsl-llvm"))]
236pub(crate) fn solve_factory_jit(
237    code: &str,
238    jit_backend: JitBackendType,
239    matrix_type: MatrixType,
240    scalar_type: ScalarType,
241) -> Result<Box<dyn Solve>, DiffsolRtError> {
242    match jit_backend {
243        #[cfg(feature = "diffsl-cranelift")]
244        JitBackendType::Cranelift => solve_factory_with_jit_backend::<diffsol::CraneliftJitModule>(
245            code,
246            matrix_type,
247            scalar_type,
248        ),
249        #[cfg(feature = "diffsl-llvm")]
250        JitBackendType::Llvm => {
251            solve_factory_with_jit_backend::<diffsol::LlvmModule>(code, matrix_type, scalar_type)
252        }
253    }
254}
255
256#[cfg(any(feature = "diffsl-cranelift", feature = "diffsl-llvm"))]
257fn solve_factory_with_jit_backend<CG>(
258    code: &str,
259    matrix_type: MatrixType,
260    scalar_type: ScalarType,
261) -> Result<Box<dyn Solve>, DiffsolRtError>
262where
263    CG: CodegenModule + CodegenModuleJit + CodegenModuleCompile,
264    DiffSl<diffsol::NalgebraMat<f32>, CG>: SolveEqn,
265    DiffSl<diffsol::NalgebraMat<f64>, CG>: SolveEqn,
266    DiffSl<diffsol::FaerMat<f32>, CG>: SolveEqn,
267    DiffSl<diffsol::FaerMat<f64>, CG>: SolveEqn,
268    DiffSl<diffsol::FaerSparseMat<f32>, CG>: SolveEqn,
269    DiffSl<diffsol::FaerSparseMat<f64>, CG>: SolveEqn,
270{
271    let solve: Box<dyn Solve> = match matrix_type {
272        MatrixType::NalgebraDense => match scalar_type {
273            ScalarType::F32 => {
274                let problem =
275                    OdeBuilder::<diffsol::NalgebraMat<f32>>::new().build_from_diffsl::<CG>(code)?;
276                Box::new(GenericSolve { problem })
277            }
278            ScalarType::F64 => {
279                let problem =
280                    OdeBuilder::<diffsol::NalgebraMat<f64>>::new().build_from_diffsl::<CG>(code)?;
281                Box::new(GenericSolve { problem })
282            }
283        },
284        MatrixType::FaerDense => match scalar_type {
285            ScalarType::F32 => {
286                let problem =
287                    OdeBuilder::<diffsol::FaerMat<f32>>::new().build_from_diffsl::<CG>(code)?;
288                Box::new(GenericSolve { problem })
289            }
290            ScalarType::F64 => {
291                let problem =
292                    OdeBuilder::<diffsol::FaerMat<f64>>::new().build_from_diffsl::<CG>(code)?;
293                Box::new(GenericSolve { problem })
294            }
295        },
296        MatrixType::FaerSparse => match scalar_type {
297            ScalarType::F32 => {
298                let problem = OdeBuilder::<diffsol::FaerSparseMat<f32>>::new()
299                    .build_from_diffsl::<CG>(code)?;
300                Box::new(GenericSolve { problem })
301            }
302            ScalarType::F64 => {
303                let problem = OdeBuilder::<diffsol::FaerSparseMat<f64>>::new()
304                    .build_from_diffsl::<CG>(code)?;
305                Box::new(GenericSolve { problem })
306            }
307        },
308    };
309    Ok(solve)
310}
311
312pub(crate) fn solve_factory_from_serialized_diffsl(
313    serialized_diffsl: &[u8],
314    matrix_type: MatrixType,
315    scalar_type: ScalarType,
316) -> Result<Box<dyn Solve>, DiffsolRtError> {
317    let solve: Box<dyn Solve> = match matrix_type {
318        MatrixType::NalgebraDense => match scalar_type {
319            ScalarType::F32 => Box::new(
320                DiffSlSolve::<diffsol::NalgebraMat<f32>, ObjectModule>::from_serialized_diffsl(
321                    serialized_diffsl,
322                )?,
323            ),
324            ScalarType::F64 => Box::new(
325                DiffSlSolve::<diffsol::NalgebraMat<f64>, ObjectModule>::from_serialized_diffsl(
326                    serialized_diffsl,
327                )?,
328            ),
329        },
330        MatrixType::FaerDense => match scalar_type {
331            ScalarType::F32 => Box::new(
332                DiffSlSolve::<diffsol::FaerMat<f32>, ObjectModule>::from_serialized_diffsl(
333                    serialized_diffsl,
334                )?,
335            ),
336            ScalarType::F64 => Box::new(
337                DiffSlSolve::<diffsol::FaerMat<f64>, ObjectModule>::from_serialized_diffsl(
338                    serialized_diffsl,
339                )?,
340            ),
341        },
342        MatrixType::FaerSparse => match scalar_type {
343            ScalarType::F32 => Box::new(
344                DiffSlSolve::<diffsol::FaerSparseMat<f32>, ObjectModule>::from_serialized_diffsl(
345                    serialized_diffsl,
346                )?,
347            ),
348            ScalarType::F64 => Box::new(
349                DiffSlSolve::<diffsol::FaerSparseMat<f64>, ObjectModule>::from_serialized_diffsl(
350                    serialized_diffsl,
351                )?,
352            ),
353        },
354    };
355    Ok(solve)
356}
357
358#[cfg(feature = "external")]
359impl<M> DiffSlSolve<M, diffsl::ExternalModule<M::T>>
360where
361    M: Matrix<T: ExternalScalar>,
362    M::V: Vector + DefaultDenseMatrix,
363{
364    pub fn from_external(
365        deps: diffsol::DiffSlDeps,
366        include_sensitivities: bool,
367    ) -> Result<Self, DiffsolRtError> {
368        let eqn = DiffSl::<M, diffsl::ExternalModule<M::T>>::from_external(
369            M::C::default(),
370            deps,
371            include_sensitivities,
372        )?;
373        Self::from_eqn(eqn)
374    }
375}
376
377#[cfg(feature = "diffsl-external-dynamic")]
378impl<M> DiffSlSolve<M, diffsl::ExternalDynModule<M::T>>
379where
380    M: Matrix<T: Scalar>,
381    M::V: Vector + DefaultDenseMatrix,
382{
383    pub fn from_external_dynamic(
384        path: impl Into<PathBuf>,
385        deps: diffsol::DiffSlDeps,
386        include_sensitivities: bool,
387    ) -> Result<Self, DiffsolRtError> {
388        let eqn = DiffSl::<M, diffsl::ExternalDynModule<M::T>>::from_external_dynamic(
389            path,
390            M::C::default(),
391            deps,
392            include_sensitivities,
393        )?;
394        Self::from_eqn(eqn)
395    }
396}
397
398impl<M> DiffSlSolve<M, ObjectModule>
399where
400    M: Matrix<T: Scalar>,
401    M::V: Vector + DefaultDenseMatrix,
402{
403    pub fn from_serialized_diffsl(serialized_diffsl: &[u8]) -> Result<Self, DiffsolRtError> {
404        let eqn = serde_json::from_slice::<DiffSl<M, ObjectModule>>(serialized_diffsl)
405            .map_err(|e| DiffsolRtError::from(DiffsolError::Other(e.to_string())))?;
406        Self::from_eqn(eqn)
407    }
408}
409
410fn serialize_json<T: serde::Serialize>(eqn: &T) -> Result<Vec<u8>, DiffsolRtError> {
411    serde_json::to_vec(eqn).map_err(|e| DiffsolRtError::from(DiffsolError::Other(e.to_string())))
412}
413
414/// The requirement check of DiffSL equations, which is the same for each backend.
415fn check_diffsl_requirement(requirement: EqnRequirement) -> Result<(), DiffsolRtError> {
416    // sensitivity analysis of DiffSL equations is not supported on Windows
417    let needs_sens = matches!(
418        requirement,
419        EqnRequirement::FwdSens | EqnRequirement::Adjoint(_) | EqnRequirement::Vjp(_)
420    );
421    if needs_sens && cfg!(target_os = "windows") {
422        return Err(DiffsolError::Other(
423            "Sensitivity analysis is not supported on Windows, please use a linux or macOS system."
424                .to_string(),
425        )
426        .into());
427    }
428    Ok(())
429}
430
431/// Implement [`SolveEqn`] for `DiffSl<M, $cg>`, where the scalar type of `M` satisfies
432/// `$scalar`. The serialization depends on the backend `$cg`, so each backend gives its own
433/// `serialized` body, with `$eqn` the equations.
434macro_rules! impl_solve_eqn_for_diffsl {
435    ($cg:ty, $scalar:path, |$eqn:ident| $serialized:expr) => {
436        impl<M> SolveEqn for DiffSl<M, $cg>
437        where
438            M: Matrix<T: $scalar>,
439            M::V: Vector + DefaultDenseMatrix,
440        {
441            fn serialized(&self) -> Result<Vec<u8>, DiffsolRtError> {
442                let $eqn = self;
443                $serialized
444            }
445
446            fn check_requirement(&self, requirement: EqnRequirement) -> Result<(), DiffsolRtError> {
447                check_diffsl_requirement(requirement)
448            }
449        }
450    };
451}
452
453#[cfg(feature = "diffsl-llvm")]
454impl_solve_eqn_for_diffsl!(diffsol::LlvmModule, Scalar, |eqn| serialize_json(eqn));
455
456impl_solve_eqn_for_diffsl!(diffsol::ObjectModule, Scalar, |eqn| serialize_json(eqn));
457
458#[cfg(feature = "diffsl-cranelift")]
459impl_solve_eqn_for_diffsl!(diffsol::CraneliftJitModule, Scalar, |_eqn| unsupported(
460    "ODE serialization is not supported for Cranelift-backed solvers"
461));
462
463#[cfg(feature = "external")]
464impl_solve_eqn_for_diffsl!(diffsl::ExternalModule<M::T>, ExternalScalar, |_eqn| {
465    unsupported("ODE serialization is only supported for JIT-backed solvers")
466});
467
468#[cfg(feature = "diffsl-external-dynamic")]
469impl_solve_eqn_for_diffsl!(diffsl::ExternalDynModule<M::T>, Scalar, |_eqn| unsupported(
470    "ODE serialization is only supported for JIT-backed solvers"
471));
472
473#[cfg(all(test, any(feature = "diffsl-cranelift", feature = "diffsl-llvm")))]
474mod tests {
475    use diffsol::MatrixCommon;
476    use diffsol::{
477        CodegenModuleCompile, CodegenModuleJit, Context, OdeBuilder, OdeEquations, Vector,
478    };
479
480    #[cfg(feature = "diffsl-llvm")]
481    use crate::test_support::{hybrid_logistic_state_dr, logistic_state_dr};
482    use crate::{
483        linear_solver_type::LinearSolverType,
484        matrix_type::MatrixType,
485        ode_solver_type::OdeSolverType,
486        scalar_type::ScalarType,
487        test_support::{
488            assert_close, hybrid_logistic_diffsl_code, hybrid_logistic_state, lane_f64,
489            lane_matrix, logistic_diffsl_code, logistic_state, two_param_logistic_diffsl_code,
490            vector_tensor, LOGISTIC_X0,
491        },
492    };
493
494    use super::{solve_factory_with_jit_backend, DiffSlSolve};
495
496    use crate::solve::{GenericSolve, Solve};
497    use crate::solve_eqn::SolveEqn;
498
499    fn make_generic_solve<T, CG>() -> DiffSlSolve<diffsol::NalgebraMat<T>, CG>
500    where
501        T: crate::scalar_type::Scalar + diffsol::NalgebraScalar,
502        CG: diffsol::CodegenModule + CodegenModuleJit + CodegenModuleCompile,
503        diffsol::DiffSl<diffsol::NalgebraMat<T>, CG>: SolveEqn,
504        diffsol::NalgebraMat<T>: diffsol::matrix::Matrix,
505        <diffsol::NalgebraMat<T> as MatrixCommon>::T: crate::scalar_type::Scalar,
506        <diffsol::NalgebraMat<T> as MatrixCommon>::V: diffsol::DefaultDenseMatrix,
507    {
508        let problem = OdeBuilder::<diffsol::NalgebraMat<T>>::new()
509            .build_from_diffsl::<CG>(logistic_diffsl_code())
510            .unwrap();
511        GenericSolve { problem }
512    }
513
514    fn assert_factory_supports_all_matrix_and_scalar_types<CG>()
515    where
516        CG: diffsol::CodegenModule + CodegenModuleJit + CodegenModuleCompile,
517        diffsol::DiffSl<diffsol::NalgebraMat<f32>, CG>: SolveEqn,
518        diffsol::DiffSl<diffsol::NalgebraMat<f64>, CG>: SolveEqn,
519        diffsol::DiffSl<diffsol::FaerMat<f32>, CG>: SolveEqn,
520        diffsol::DiffSl<diffsol::FaerMat<f64>, CG>: SolveEqn,
521        diffsol::DiffSl<diffsol::FaerSparseMat<f32>, CG>: SolveEqn,
522        diffsol::DiffSl<diffsol::FaerSparseMat<f64>, CG>: SolveEqn,
523    {
524        for matrix_type in [
525            MatrixType::NalgebraDense,
526            MatrixType::FaerDense,
527            MatrixType::FaerSparse,
528        ] {
529            for scalar_type in [ScalarType::F32, ScalarType::F64] {
530                assert!(solve_factory_with_jit_backend::<CG>(
531                    logistic_diffsl_code(),
532                    matrix_type,
533                    scalar_type,
534                )
535                .is_ok());
536            }
537        }
538    }
539
540    fn assert_solve_metadata_and_helpers<T, CG>(tol: f64)
541    where
542        T: crate::scalar_type::Scalar + diffsol::NalgebraScalar,
543        CG: diffsol::CodegenModule + CodegenModuleJit + CodegenModuleCompile,
544        diffsol::DiffSl<diffsol::NalgebraMat<T>, CG>: SolveEqn,
545        diffsol::NalgebraMat<T>: diffsol::matrix::Matrix,
546        <diffsol::NalgebraMat<T> as MatrixCommon>::T: crate::scalar_type::Scalar,
547        <diffsol::NalgebraMat<T> as MatrixCommon>::V: diffsol::DefaultDenseMatrix,
548        DiffSlSolve<diffsol::NalgebraMat<T>, CG>: Solve,
549    {
550        let mut solve = make_generic_solve::<T, CG>();
551        assert_eq!(solve.matrix_type(), MatrixType::NalgebraDense);
552        assert_eq!(solve.nstates(), 1);
553        assert_eq!(solve.nparams(), 1);
554        assert_eq!(solve.nout(), 1);
555        assert!(!solve.has_stop());
556        assert!(solve.check(LinearSolverType::Default).is_ok());
557        assert!(solve.check(LinearSolverType::Lu).is_ok());
558        assert!(solve.check(LinearSolverType::Klu).is_err());
559
560        solve.set_atol(1e-5);
561        solve.set_rtol(1e-4);
562        assert_close(solve.atol(), 1e-5, tol, "solve atol");
563        assert_close(solve.rtol(), 1e-4, tol, "solve rtol");
564        solve.set_t0(0.125);
565        solve.set_h0(0.25);
566        solve.set_integrate_out(true);
567        assert_close(solve.t0(), 0.125, tol, "solve t0");
568        assert_close(solve.h0(), 0.25, tol, "solve h0");
569        assert!(solve.integrate_out());
570
571        solve.set_sens_rtol(Some(1e-3));
572        solve.set_sens_atol(Some(1e-4));
573        solve.set_out_rtol(Some(2e-3));
574        solve.set_out_atol(Some(2e-4));
575        solve.set_param_rtol(Some(3e-3));
576        solve.set_param_atol(Some(3e-4));
577        assert_close(solve.sens_rtol().unwrap(), 1e-3, tol, "solve sens rtol");
578        assert_close(solve.sens_atol().unwrap(), 1e-4, tol, "solve sens atol");
579        assert_close(solve.out_rtol().unwrap(), 2e-3, tol, "solve out rtol");
580        assert_close(solve.out_atol().unwrap(), 2e-4, tol, "solve out atol");
581        assert_close(solve.param_rtol().unwrap(), 3e-3, tol, "solve param rtol");
582        assert_close(solve.param_atol().unwrap(), 3e-4, tol, "solve param atol");
583
584        solve.set_sens_rtol(None);
585        solve.set_sens_atol(None);
586        solve.set_out_rtol(None);
587        solve.set_out_atol(None);
588        solve.set_param_rtol(None);
589        solve.set_param_atol(None);
590        assert_eq!(solve.sens_rtol(), None);
591        assert_eq!(solve.sens_atol(), None);
592        assert_eq!(solve.out_rtol(), None);
593        assert_eq!(solve.out_atol(), None);
594        assert_eq!(solve.param_rtol(), None);
595        assert_eq!(solve.param_atol(), None);
596
597        let y0 = lane_f64(&solve.y0(vector_tensor(&[2.0]).view()).unwrap(), 0);
598        assert_eq!(y0.len(), 1);
599        assert_close(y0[0], LOGISTIC_X0, tol, "y0[0]");
600
601        let rhs = lane_f64(
602            &solve
603                .rhs(
604                    vector_tensor(&[2.0]).view(),
605                    0.0,
606                    vector_tensor(&[0.25]).view(),
607                )
608                .unwrap(),
609            0,
610        );
611        assert_eq!(rhs.len(), 1);
612        assert_close(rhs[0], 0.375, tol, "solve rhs");
613
614        let jac_mul = lane_f64(
615            &solve
616                .rhs_jac_mul(
617                    vector_tensor(&[2.0]).view(),
618                    0.0,
619                    vector_tensor(&[0.25]).view(),
620                    vector_tensor(&[3.0]).view(),
621                )
622                .unwrap(),
623            0,
624        );
625        assert_eq!(jac_mul.len(), 1);
626        assert_close(jac_mul[0], 3.0, tol, "solve rhs jac mul");
627    }
628
629    fn assert_solve_runtime_paths<T, CG>(tol: f64)
630    where
631        T: crate::scalar_type::Scalar + diffsol::NalgebraScalar,
632        CG: diffsol::CodegenModule + CodegenModuleJit + CodegenModuleCompile,
633        diffsol::DiffSl<diffsol::NalgebraMat<T>, CG>: SolveEqn,
634        diffsol::NalgebraMat<T>: diffsol::matrix::Matrix,
635        <diffsol::NalgebraMat<T> as MatrixCommon>::T: crate::scalar_type::Scalar,
636        <diffsol::NalgebraMat<T> as MatrixCommon>::V: diffsol::DefaultDenseMatrix,
637        DiffSlSolve<diffsol::NalgebraMat<T>, CG>: Solve,
638    {
639        let mut solve = make_generic_solve::<T, CG>();
640        let soln = solve
641            .solve(
642                OdeSolverType::Bdf,
643                LinearSolverType::Lu,
644                vector_tensor(&[2.0]).view(),
645                1.0,
646            )
647            .unwrap();
648        let ts = lane_f64(&soln.clone().get_ts(), 0);
649        let ys = lane_matrix(&soln.clone().get_ys(), 0);
650        assert_close(*ts.last().unwrap(), 1.0, tol, "solve final time");
651        assert_close(
652            ys[(0, ts.len() - 1)],
653            logistic_state(LOGISTIC_X0, 2.0, 1.0),
654            tol,
655            "solve final value",
656        );
657
658        let mut solve = make_generic_solve::<T, CG>();
659        let dense = solve
660            .solve_dense(
661                OdeSolverType::Tsit45,
662                LinearSolverType::Lu,
663                vector_tensor(&[2.0]).view(),
664                vector_tensor(&[0.25, 0.5, 1.0]).view(),
665            )
666            .unwrap();
667        let ts = lane_f64(&dense.clone().get_ts(), 0);
668        let ys = lane_matrix(&dense.clone().get_ys(), 0);
669        assert_eq!(ts, vec![0.25, 0.5, 1.0]);
670        for (i, &t) in ts.iter().enumerate() {
671            assert_close(
672                ys[(0, i)],
673                logistic_state(LOGISTIC_X0, 2.0, t),
674                tol,
675                &format!("solve_dense[{i}]"),
676            );
677        }
678
679        let mut solve = make_generic_solve::<T, CG>();
680        let err = match solve.solve(
681            OdeSolverType::Bdf,
682            LinearSolverType::Default,
683            vector_tensor(&[]).view(),
684            1.0,
685        ) {
686            Ok(_) => panic!("expected parameter count mismatch"),
687            Err(err) => err,
688        };
689        assert!(err
690            .to_string()
691            .contains("params: Expected 1 values in each lane, got 0"));
692
693        let hybrid_problem = OdeBuilder::<diffsol::NalgebraMat<T>>::new()
694            .build_from_diffsl::<CG>(hybrid_logistic_diffsl_code())
695            .unwrap();
696        let mut hybrid_solve = GenericSolve {
697            problem: hybrid_problem,
698        };
699        let hybrid = hybrid_solve
700            .solve(
701                OdeSolverType::Bdf,
702                LinearSolverType::Lu,
703                vector_tensor(&[2.0]).view(),
704                2.0,
705            )
706            .unwrap();
707        let hybrid_ts = lane_f64(&hybrid.clone().get_ts(), 0);
708        let hybrid_ys = lane_matrix(&hybrid.clone().get_ys(), 0);
709        assert_close(*hybrid_ts.last().unwrap(), 2.0, tol, "solve final time");
710        assert_close(
711            hybrid_ys[(0, hybrid_ts.len() - 1)],
712            hybrid_logistic_state(2.0, 2.0),
713            tol,
714            "solve final value",
715        );
716
717        let hybrid_problem = OdeBuilder::<diffsol::NalgebraMat<T>>::new()
718            .build_from_diffsl::<CG>(hybrid_logistic_diffsl_code())
719            .unwrap();
720        let mut hybrid_solve = GenericSolve {
721            problem: hybrid_problem,
722        };
723        let hybrid_dense = hybrid_solve
724            .solve_dense(
725                OdeSolverType::Tsit45,
726                LinearSolverType::Lu,
727                vector_tensor(&[2.0]).view(),
728                vector_tensor(&[0.5, 1.0, 1.5, 2.0]).view(),
729            )
730            .unwrap();
731        let hybrid_dense_ts = lane_f64(&hybrid_dense.clone().get_ts(), 0);
732        let hybrid_dense_ys = lane_matrix(&hybrid_dense.clone().get_ys(), 0);
733        assert_eq!(hybrid_dense_ts, vec![0.5, 1.0, 1.5, 2.0]);
734        for (i, &t) in hybrid_dense_ts.iter().enumerate() {
735            assert_close(
736                hybrid_dense_ys[(0, i)],
737                hybrid_logistic_state(2.0, t),
738                tol,
739                &format!("solve_dense[{i}]"),
740            );
741        }
742    }
743
744    #[cfg(feature = "diffsl-llvm")]
745    fn assert_solve_sensitivity_paths() {
746        let t_eval = [0.25, 0.5, 1.0];
747
748        let mut solve = make_generic_solve::<f64, diffsol::LlvmModule>();
749        let sens = solve
750            .solve_fwd_sens(
751                OdeSolverType::Bdf,
752                LinearSolverType::Lu,
753                vector_tensor(&[2.0]).view(),
754                vector_tensor(&t_eval).view(),
755            )
756            .unwrap();
757        let sens_values = sens.clone().get_sens();
758        assert_eq!(sens_values.len(), 1);
759        let sens_matrix = lane_matrix(&sens_values.into_iter().next().unwrap(), 0);
760        for (i, &t) in t_eval.iter().enumerate() {
761            assert_close(
762                sens_matrix[(0, i)],
763                logistic_state_dr(LOGISTIC_X0, 2.0, t),
764                5e-4,
765                &format!("solve_fwd_sens[{i}]"),
766            );
767        }
768
769        let hybrid_problem = OdeBuilder::<diffsol::NalgebraMat<f64>>::new()
770            .build_from_diffsl::<diffsol::LlvmModule>(hybrid_logistic_diffsl_code())
771            .unwrap();
772        let mut solve = GenericSolve {
773            problem: hybrid_problem,
774        };
775        let hybrid_sens = solve
776            .solve_fwd_sens(
777                OdeSolverType::Bdf,
778                LinearSolverType::Lu,
779                vector_tensor(&[2.0]).view(),
780                vector_tensor(&t_eval).view(),
781            )
782            .unwrap();
783        let sens_values = hybrid_sens.clone().get_sens();
784        let sens_matrix = lane_matrix(&sens_values.into_iter().next().unwrap(), 0);
785        for (i, &t) in t_eval.iter().enumerate() {
786            assert_close(
787                sens_matrix[(0, i)],
788                hybrid_logistic_state_dr(2.0, t),
789                5e-4,
790                &format!("solve_fwd_sens[{i}]"),
791            );
792        }
793    }
794
795    #[cfg(feature = "diffsl-cranelift")]
796    fn assert_sens_atol_round_trips_with_multiple_params() {
797        // two params make sens_atol batched (nbatch = 2), which get_index rejects
798        let problem = OdeBuilder::<diffsol::NalgebraMat<f64>>::new()
799            .build_from_diffsl::<diffsol::CraneliftJitModule>(two_param_logistic_diffsl_code())
800            .unwrap();
801        let mut solve = GenericSolve { problem };
802        assert_eq!(solve.nparams(), 2);
803
804        solve.set_sens_atol(Some(1e-4));
805        let sens_atol = solve.problem.sens_atol.as_ref().unwrap();
806        assert_eq!(sens_atol.context().nbatch(), 2);
807        assert_eq!(sens_atol.len(), solve.nstates());
808        assert_close(solve.sens_atol().unwrap(), 1e-4, 1e-12, "sens atol");
809    }
810
811    #[cfg(feature = "diffsl-cranelift")]
812    #[test]
813    fn solve_factory_supports_all_jit_matrix_and_scalar_types_for_cranelift() {
814        assert_factory_supports_all_matrix_and_scalar_types::<diffsol::CraneliftJitModule>();
815    }
816
817    #[cfg(feature = "diffsl-cranelift")]
818    #[test]
819    fn solve_trait_helpers_and_runtime_paths_for_cranelift() {
820        assert_solve_metadata_and_helpers::<f64, diffsol::CraneliftJitModule>(1e-12);
821        assert_solve_runtime_paths::<f64, diffsol::CraneliftJitModule>(5e-4);
822        assert_solve_metadata_and_helpers::<f32, diffsol::CraneliftJitModule>(5e-3);
823        assert_solve_runtime_paths::<f32, diffsol::CraneliftJitModule>(5e-3);
824    }
825
826    #[cfg(feature = "diffsl-cranelift")]
827    #[test]
828    fn sens_atol_round_trips_with_multiple_params_for_cranelift() {
829        assert_sens_atol_round_trips_with_multiple_params();
830    }
831
832    #[cfg(feature = "diffsl-llvm")]
833    #[test]
834    fn solve_trait_helpers_and_runtime_paths_for_llvm() {
835        assert_solve_metadata_and_helpers::<f64, diffsol::LlvmModule>(1e-12);
836        assert_solve_runtime_paths::<f64, diffsol::LlvmModule>(5e-4);
837        assert_solve_metadata_and_helpers::<f32, diffsol::LlvmModule>(5e-3);
838        assert_solve_runtime_paths::<f32, diffsol::LlvmModule>(5e-3);
839    }
840
841    #[cfg(feature = "diffsl-llvm")]
842    #[test]
843    fn solve_trait_sensitivity_paths_for_llvm() {
844        assert_solve_sensitivity_paths();
845    }
846
847    #[cfg(feature = "diffsl-cranelift")]
848    #[test]
849    fn setup_problem_validates_parameter_count_for_cranelift() {
850        let mut solve = make_generic_solve::<f64, diffsol::CraneliftJitModule>();
851        let err = solve.setup_problem(vector_tensor(&[]).view()).unwrap_err();
852        assert!(err
853            .to_string()
854            .contains("params: Expected 1 values in each lane, got 0"));
855
856        solve.setup_problem(vector_tensor(&[2.0]).view()).unwrap();
857        let mut params = solve.problem.context().vector_zeros(1);
858        solve.problem.eqn.get_params(&mut params);
859        assert_eq!(params.get_index(0), 2.0);
860    }
861
862    #[cfg(feature = "diffsl-llvm")]
863    #[test]
864    fn setup_problem_validates_parameter_count_for_llvm() {
865        let mut solve = make_generic_solve::<f64, diffsol::LlvmModule>();
866        let err = solve.setup_problem(vector_tensor(&[]).view()).unwrap_err();
867        assert!(err
868            .to_string()
869            .contains("params: Expected 1 values in each lane, got 0"));
870
871        solve.setup_problem(vector_tensor(&[2.0]).view()).unwrap();
872        let mut params = solve.problem.context().vector_zeros(1);
873        solve.problem.eqn.get_params(&mut params);
874        assert_eq!(params.get_index(0), 2.0);
875    }
876}