Skip to main content

diffsol_c/
ode_callbacks.rs

1//! ODE equations defined by C callbacks.
2//!
3//! [`OdeCallbacks`] implements [`diffsol::OdeEquations`]. It also implements each
4//! flavour that the solvers need: implicit, forward sensitivities and adjoint. The equations
5//! come from a table of C function pointers, [`OdeCallbacksDesc`]. Thus a C host (or a different
6//! FFI host) can define its own equations.
7//!
8//! There is one callback for each closure of [`diffsol::OdeBuilder`]. Each callback has the
9//! arguments of its closure, plus a `user_data` pointer as the last argument. Only `rhs` and
10//! `init` are necessary.
11//!
12//! # Callback conventions
13//!
14//! The library calls a callback **one time for all the lanes** of the equations. Each vector
15//! argument is a pointer to a [`TensorView`] (an input) or a [`TensorViewMut`] (the output `y`).
16//! The views show the data of the solver with no copy. Each view has the C layout of
17//! [`RawLayout`](crate::tensor::RawLayout): the fields `ptr`, `dtype`, `ndim`, `shape` and
18//! `strides` (in bytes). `dtype` is the scalar type of the descriptor (`double` or `float`).
19//!
20//! Each view has 3 axes, `[n, P, B]`. The table gives `n`. The counts are fields of
21//! [`OdeCallbacksDesc`].
22//!
23//! | callback | `x` | `p` | `v` | `y` (output) |
24//! |---|---|---|---|---|
25//! | `rhs`, `reset` | nstates | nparams | - | nstates |
26//! | `rhs_jac`, `reset_jac` (`y = J v`) | nstates | nparams | nstates | nstates |
27//! | `rhs_sens`, `reset_sens` (`y = J_p v`) | nstates | nparams | nparams | nstates |
28//! | `rhs_adjoint`, `reset_adjoint` (`y = -J^T v`) | nstates | nparams | nstates | nstates |
29//! | `rhs_sens_adjoint`, `reset_sens_adjoint` (`y = -J_p^T v`) | nstates | nparams | nstates | nparams |
30//! | `root` | nstates | nparams | - | nroots |
31//! | `root_jac` / `root_sens` | nstates | nparams | nstates / nparams | nroots |
32//! | `root_adjoint` / `root_sens_adjoint` | nstates | nparams | nroots | nstates / nparams |
33//! | `out` | nstates | nparams | - | nout |
34//! | `out_jac` / `out_sens` | nstates | nparams | nstates / nparams | nout |
35//! | `out_adjoint` / `out_sens_adjoint` | nstates | nparams | nout | nstates / nparams |
36//! | `init` | - | nparams | - | nstates |
37//! | `init_sens` / `init_sens_adjoint` | - | nparams | nparams / nstates | nstates / nparams |
38//! | `mass` / `mass_adjoint` (`y = M v + beta y`, `M^T` for the adjoint) | - | nparams | nstates | nstates |
39//!
40//! `J` is the Jacobian of the function with respect to the state `x`. `J_p` is the Jacobian
41//! with respect to the parameters `p`.
42//!
43//! # Lanes
44//!
45//! The output `y` has `P * B` lanes, as the grid `[P, B]`. Lane `(k, b)` of each argument has
46//! the values that the callback uses to calculate lane `(k, b)` of `y`. Element `i` of lane
47//! `(k, b)` is at the byte offset `i * strides[0] + k * strides[1] + b * strides[2]` from `ptr`.
48//! The values of one lane are contiguous in memory: `strides[0]` is the size of one value.
49//!
50//! - `B` is `nbatch`, the number of lanes of the equations (independent copies of the system,
51//!   each with its own parameters and state).
52//! - `P` is 1 in a normal solve. In a sensitivity or an adjoint solve, the solver can calculate
53//!   more than one product for each lane `b` in one call (for example one product for each
54//!   parameter). Then `P > 1`.
55//!
56//! An argument that has the same values for each `k` (for example `x` and `p` in a forward
57//! sensitivity solve) has `strides[1] == 0`. An argument that has the same values for all lanes
58//! has `strides[1] == strides[2] == 0`. Thus the callback does not need to know which arguments
59//! are broadcast: it uses the strides. The output `y` never has a stride of 0. Do not write to
60//! an input.
61//!
62//! A simple callback loops over `b`, `k` and `i`. A callback for a GPU can calculate all the
63//! lanes in one kernel.
64//!
65//! The `root` and `reset` callbacks are not available with more than one lane (`nbatch > 1`).
66//!
67//! Each `*_adjoint` and `*_sens_adjoint` callback calculates the *negative* transposed product.
68//! For example, `out_adjoint` calculates `y = -J^T v`, and `init_sens_adjoint` calculates
69//! `y = -(dy0/dp)^T v`. `mass_adjoint` is the only exception: it calculates `y = M^T v + beta y`.
70//!
71//! Each callback has the same meaning as the related [`diffsol::OdeBuilder`] closure. For
72//! example, `rhs_adjoint` is the `rhs_adjoint` closure of
73//! [`diffsol::OdeBuilder::rhs_adjoint_implicit`].
74//!
75//! # Return codes
76//!
77//! Each callback returns an `int`. The value `0` means success. A different value means
78//! failure. When a callback fails, all the lanes of its output are filled with NaN.
79//!
80//! The solver can recover from a failure. For example, it can reject a step and try a smaller
81//! step. Thus `rhs` can return an error for a state that is not in its domain.
82//!
83//! If the solve succeeds, the callback failures are ignored.
84//!
85//! If the solve fails, its error is the error of the solver. If a callback failed during the
86//! solve, the message also gives the first failure, for example
87//! `(a callback failed during the solve: callback 'rhs' returned error code 7)`. This failure is
88//! often the cause. But it can also be an earlier failure that the solver recovered from.
89//!
90//! # Sparsity patterns
91//!
92//! For a sparse matrix type (`FaerSparse`), the construction finds the sparsity pattern of each
93//! matrix one time. It calls each derivative callback with NaN in the inputs `x` and `v`. Each
94//! output that is NaN is a non-zero. This is the same method that [`diffsol::OdeBuilder`] uses.
95//!
96//! The probe uses the initial state at `t = 0`, with all parameters zero. The patterns do not
97//! change after this, also when the parameters change. Thus, for a sparse matrix type:
98//! - each callback must accept NaN inputs, and the parameters `p = 0`;
99//! - the pattern of each derivative must not depend on the values of `x`, `p` or `t`. For
100//!   example, do not use an `if` on an input value to decide whether to write an output.
101//!
102//! If a callback does not obey these rules, the matrix can be incorrect. Then the solver can
103//! converge slowly or fail. The dense matrix types do not use a pattern.
104//!
105//! # Ownership of `user_data`
106//!
107//! Each callback gets `user_data` as its last argument, with no change.
108//!
109//! If `free_user_data` is set, [`OdeCallbacks`] owns `user_data` from the start of its
110//! construction. This is also true if the construction fails. [`OdeCallbacks`] calls
111//! `free_user_data(user_data)` one time, when it is dropped.
112//!
113//! If `free_user_data` is not set, `user_data` must stay valid until the equations are dropped.
114//!
115//! # Threads
116//!
117//! The callbacks are called one at a time. They are never called at the same time from two
118//! threads.
119//!
120//! But a solver can move between threads. An `OdeWrapper` is `Send` and `Sync`. Thus the
121//! callbacks run on the thread that uses the solver at that time. `free_user_data` runs on the
122//! thread that drops the solver. If `user_data` must stay on one thread (for example a Python
123//! object that needs the GIL, or a JNI local reference), use the solver and drop it only on that
124//! thread.
125use std::{
126    cell::{Cell, RefCell},
127    ffi::c_void,
128    fmt,
129};
130
131use diffsol::{
132    error::DiffsolError,
133    jacobian::{
134        find_adjoint_non_zeros, find_constant_sens_non_zeros, find_jacobian_non_zeros,
135        find_matrix_non_zeros, find_sens_adjoint_non_zeros, find_sens_non_zeros, JacobianColoring,
136    },
137    matrix::sparsity::MatrixSparsity,
138    op::OpStatistics,
139    ConstantOp, ConstantOpSens, ConstantOpSensAdjoint, Context, LinearOp, LinearOpTranspose,
140    Matrix, NonLinearOp, NonLinearOpAdjoint, NonLinearOpJacobian, NonLinearOpSens,
141    NonLinearOpSensAdjoint, OdeEquations, OdeEquationsRef, Op, Scalar, Vector,
142};
143
144use num_traits::Zero;
145
146use crate::{
147    error::DiffsolRtError,
148    linear_solver_type::LinearSolverType,
149    matrix_type::MatrixType,
150    ode::OdeWrapper,
151    ode_solver_type::OdeSolverType,
152    scalar_type::ToScalarType,
153    solve::{GenericSolve, Solve},
154    solve_eqn::{EqnRequirement, SolveEqn},
155    tensor::{TensorLayout, TensorLayoutMut, TensorView, TensorViewMut},
156};
157
158// Each callback type is a nullable function pointer, the same as a function pointer type in C:
159// `None` is `NULL`, and means that the callback is not given. Each vector argument is a view of
160// all the lanes; the module documentation gives the layout.
161
162/// `y = F(x, p, t)`: used for `rhs`, `root`, `out` and `reset`.
163pub type OdeCallbackFn<T> = Option<
164    unsafe extern "C" fn(
165        x: *const TensorView<'_>,
166        p: *const TensorView<'_>,
167        t: T,
168        y: *mut TensorViewMut<'_>,
169        user_data: *mut c_void,
170    ) -> i32,
171>;
172
173/// `y = A(x, p, t) v`: used for every `*_jac`, `*_sens`, `*_adjoint` and `*_sens_adjoint`
174/// callback of `rhs`, `root`, `out` and `reset`.
175pub type OdeCallbackMulFn<T> = Option<
176    unsafe extern "C" fn(
177        x: *const TensorView<'_>,
178        p: *const TensorView<'_>,
179        t: T,
180        v: *const TensorView<'_>,
181        y: *mut TensorViewMut<'_>,
182        user_data: *mut c_void,
183    ) -> i32,
184>;
185
186/// `y = y0(p, t)`: the initial state.
187pub type OdeCallbackInitFn<T> = Option<
188    unsafe extern "C" fn(
189        p: *const TensorView<'_>,
190        t: T,
191        y: *mut TensorViewMut<'_>,
192        user_data: *mut c_void,
193    ) -> i32,
194>;
195
196/// `y = A(p, t) v`: used for `init_sens` and `init_sens_adjoint`.
197pub type OdeCallbackInitMulFn<T> = Option<
198    unsafe extern "C" fn(
199        p: *const TensorView<'_>,
200        t: T,
201        v: *const TensorView<'_>,
202        y: *mut TensorViewMut<'_>,
203        user_data: *mut c_void,
204    ) -> i32,
205>;
206
207/// `y = M(p, t) v + beta y`: used for `mass` (and `mass_adjoint`, with `M^T`). `y` is an input
208/// and the output.
209pub type OdeCallbackMassFn<T> = Option<
210    unsafe extern "C" fn(
211        v: *const TensorView<'_>,
212        p: *const TensorView<'_>,
213        t: T,
214        beta: T,
215        y: *mut TensorViewMut<'_>,
216        user_data: *mut c_void,
217    ) -> i32,
218>;
219
220/// Frees `user_data`, see the [module documentation](self).
221pub type OdeCallbackFreeFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
222
223/// The C callbacks (and the problem dimensions) that define an [`OdeCallbacks`].
224///
225/// A `NULL` function pointer means the callback is not provided. Only `rhs` and `init` are
226/// required; see the [module documentation](self) for the meaning and argument lengths of each
227/// callback.
228#[repr(C)]
229#[derive(Clone, Copy, Debug)]
230pub struct OdeCallbacksDesc<T> {
231    /// Number of states.
232    pub nstates: usize,
233    /// Number of parameters.
234    pub nparams: usize,
235    /// Number of outputs of `out`, which must be 0 exactly when `out` is not provided.
236    pub nout: usize,
237    /// Number of root functions of `root`, which must be 0 exactly when `root` is not provided.
238    pub nroots: usize,
239    /// Passed as the last argument of every callback.
240    pub user_data: *mut c_void,
241    /// Called once with `user_data` when the equations are dropped. May be `NULL`.
242    pub free_user_data: OdeCallbackFreeFn,
243
244    pub rhs: OdeCallbackFn<T>,
245    pub rhs_jac: OdeCallbackMulFn<T>,
246    pub rhs_sens: OdeCallbackMulFn<T>,
247    pub rhs_adjoint: OdeCallbackMulFn<T>,
248    pub rhs_sens_adjoint: OdeCallbackMulFn<T>,
249
250    pub init: OdeCallbackInitFn<T>,
251    pub init_sens: OdeCallbackInitMulFn<T>,
252    pub init_sens_adjoint: OdeCallbackInitMulFn<T>,
253
254    pub mass: OdeCallbackMassFn<T>,
255    pub mass_adjoint: OdeCallbackMassFn<T>,
256
257    pub root: OdeCallbackFn<T>,
258    pub root_jac: OdeCallbackMulFn<T>,
259    pub root_sens: OdeCallbackMulFn<T>,
260    pub root_adjoint: OdeCallbackMulFn<T>,
261    pub root_sens_adjoint: OdeCallbackMulFn<T>,
262
263    pub out: OdeCallbackFn<T>,
264    pub out_jac: OdeCallbackMulFn<T>,
265    pub out_sens: OdeCallbackMulFn<T>,
266    pub out_adjoint: OdeCallbackMulFn<T>,
267    pub out_sens_adjoint: OdeCallbackMulFn<T>,
268
269    pub reset: OdeCallbackFn<T>,
270    pub reset_jac: OdeCallbackMulFn<T>,
271    pub reset_sens: OdeCallbackMulFn<T>,
272    pub reset_adjoint: OdeCallbackMulFn<T>,
273    pub reset_sens_adjoint: OdeCallbackMulFn<T>,
274}
275
276// SAFETY: `user_data` is the only field that is not `Send`. The caller of the C API agrees to
277// the thread contract in the module documentation: the callbacks and `free_user_data` can run
278// on any thread, but never at the same time. Thus it is safe to move the descriptor to a
279// different thread. The descriptor is not `Sync`, because two threads must not call the
280// callbacks at the same time.
281unsafe impl<T: Send> Send for OdeCallbacksDesc<T> {}
282
283/// [`OdeCallbacksDesc`] for `double` precision.
284pub type DiffsolOdeCallbacksF64 = OdeCallbacksDesc<f64>;
285/// [`OdeCallbacksDesc`] for `float` precision.
286pub type DiffsolOdeCallbacksF32 = OdeCallbacksDesc<f32>;
287
288impl<T> OdeCallbacksDesc<T> {
289    /// Check that `nbatch` is at least 1, and that the largest buffer of the solver (one
290    /// vector for each lane and each parameter) has a size that does not overflow.
291    pub(crate) fn check_nbatch(&self, nbatch: usize) -> Result<(), String> {
292        if nbatch == 0 {
293            return Err("nbatch must be greater than 0".to_string());
294        }
295        let len = self.nstates.max(self.nout).max(self.nparams).max(1);
296        let bytes = nbatch
297            .checked_mul(self.nparams.max(1))
298            .and_then(|n| n.checked_mul(len))
299            .and_then(|n| n.checked_mul(std::mem::size_of::<T>()));
300        if bytes.is_none_or(|bytes| bytes > isize::MAX as usize) {
301            return Err(format!("nbatch {nbatch} is too large for this ODE"));
302        }
303        Ok(())
304    }
305
306    /// A descriptor with only the required callbacks set, and no `user_data`.
307    pub fn new(
308        nstates: usize,
309        nparams: usize,
310        rhs: unsafe extern "C" fn(
311            x: *const TensorView<'_>,
312            p: *const TensorView<'_>,
313            t: T,
314            y: *mut TensorViewMut<'_>,
315            user_data: *mut c_void,
316        ) -> i32,
317        init: unsafe extern "C" fn(
318            p: *const TensorView<'_>,
319            t: T,
320            y: *mut TensorViewMut<'_>,
321            user_data: *mut c_void,
322        ) -> i32,
323    ) -> Self {
324        Self {
325            nstates,
326            nparams,
327            nout: 0,
328            nroots: 0,
329            user_data: std::ptr::null_mut(),
330            free_user_data: None,
331            rhs: Some(rhs),
332            rhs_jac: None,
333            rhs_sens: None,
334            rhs_adjoint: None,
335            rhs_sens_adjoint: None,
336            init: Some(init),
337            init_sens: None,
338            init_sens_adjoint: None,
339            mass: None,
340            mass_adjoint: None,
341            root: None,
342            root_jac: None,
343            root_sens: None,
344            root_adjoint: None,
345            root_sens_adjoint: None,
346            out: None,
347            out_jac: None,
348            out_sens: None,
349            out_adjoint: None,
350            out_sens_adjoint: None,
351            reset: None,
352            reset_jac: None,
353            reset_sens: None,
354            reset_adjoint: None,
355            reset_sens_adjoint: None,
356        }
357    }
358
359    /// Check the descriptor is self-consistent, returning a description of the first problem.
360    fn validate(&self) -> Result<(), String> {
361        if self.rhs.is_none() {
362            return Err("the 'rhs' callback is required".to_string());
363        }
364        if self.init.is_none() {
365            return Err("the 'init' callback is required".to_string());
366        }
367        if self.nstates == 0 {
368            return Err("nstates must be greater than 0".to_string());
369        }
370        if self.root.is_some() != (self.nroots > 0) {
371            return Err(
372                "nroots must be greater than 0 exactly when the 'root' callback is provided"
373                    .to_string(),
374            );
375        }
376        if self.out.is_some() != (self.nout > 0) {
377            return Err(
378                "nout must be greater than 0 exactly when the 'out' callback is provided"
379                    .to_string(),
380            );
381        }
382        if self.reset.is_some() && self.root.is_none() {
383            return Err("the 'reset' callback requires the 'root' callback".to_string());
384        }
385        if self.mass_adjoint.is_some() && self.mass.is_none() {
386            return Err("the 'mass_adjoint' callback requires the 'mass' callback".to_string());
387        }
388        // the derivative callbacks of root, out and reset, in the order of `SUFFIXES`
389        const SUFFIXES: [&str; 4] = ["jac", "sens", "adjoint", "sens_adjoint"];
390        let derived = [
391            (
392                "root",
393                self.root.is_some(),
394                [
395                    self.root_jac.is_some(),
396                    self.root_sens.is_some(),
397                    self.root_adjoint.is_some(),
398                    self.root_sens_adjoint.is_some(),
399                ],
400            ),
401            (
402                "out",
403                self.out.is_some(),
404                [
405                    self.out_jac.is_some(),
406                    self.out_sens.is_some(),
407                    self.out_adjoint.is_some(),
408                    self.out_sens_adjoint.is_some(),
409                ],
410            ),
411            (
412                "reset",
413                self.reset.is_some(),
414                [
415                    self.reset_jac.is_some(),
416                    self.reset_sens.is_some(),
417                    self.reset_adjoint.is_some(),
418                    self.reset_sens_adjoint.is_some(),
419                ],
420            ),
421        ];
422        for (base, has_base, has_derived) in derived {
423            for (suffix, has) in SUFFIXES.into_iter().zip(has_derived) {
424                if has && !has_base {
425                    return Err(format!(
426                        "the '{base}_{suffix}' callback requires the '{base}' callback"
427                    ));
428                }
429            }
430        }
431        Ok(())
432    }
433
434    /// Free `user_data` with `free_user_data`, if set.
435    ///
436    /// # Safety
437    /// Must be called at most once per `user_data`, and only if nothing else owns it.
438    pub(crate) unsafe fn free_user_data(&self) {
439        if let Some(free) = self.free_user_data {
440            unsafe { free(self.user_data) };
441        }
442    }
443}
444
445/// A scalar type that C callbacks can use: `f64` or `f32`.
446///
447/// This trait is sealed. Users cannot implement it for other types.
448pub trait CallbackScalar: crate::scalar_type::Scalar + sealed::Sealed {}
449
450mod sealed {
451    use crate::{
452        error::DiffsolRtError, linear_solver_type::LinearSolverType, matrix_type::MatrixType,
453        ode::OdeWrapper, ode_callbacks::OdeCallbacksDesc, ode_solver_type::OdeSolverType,
454    };
455
456    pub trait Sealed: Sized {
457        /// Construct a solver from `callbacks`, with the scalar type `Self`.
458        ///
459        /// # Safety
460        /// `callbacks` must satisfy the conditions of
461        /// [`OdeCallbacks::new`](crate::ode_callbacks::OdeCallbacks::new).
462        unsafe fn new_ode(
463            callbacks: OdeCallbacksDesc<Self>,
464            matrix_type: MatrixType,
465            linear_solver: LinearSolverType,
466            ode_solver: OdeSolverType,
467            nbatch: usize,
468        ) -> Result<OdeWrapper, DiffsolRtError>;
469    }
470}
471
472/// Return the default context of `C` with `nbatch` lanes.
473fn batch_context<C: Context>(nbatch: usize) -> Result<C, DiffsolError> {
474    Ok(C::default().clone_with_nbatch(nbatch)?)
475}
476
477/// Build a [Solve] for the matrix type `matrix_type` and the scalar type `$t` from `callbacks`
478/// with `nbatch` lanes.
479///
480/// The caller must satisfy the safety conditions of [OdeCallbacks::new].
481macro_rules! solve_for_matrix_type {
482    ($t:ty, $callbacks:expr, $matrix_type:expr, $nbatch:expr) => {{
483        let callbacks = $callbacks;
484        let nbatch = $nbatch;
485        // SAFETY: the caller guarantees the conditions of `OdeCallbacks::new`
486        match $matrix_type {
487            MatrixType::NalgebraDense => {
488                let eqn = unsafe {
489                    OdeCallbacks::<diffsol::NalgebraMat<$t>>::new(callbacks, batch_context(nbatch)?)
490                };
491                Box::new(GenericSolve::from_eqn(eqn?)?) as Box<dyn Solve>
492            }
493            MatrixType::FaerDense => {
494                let eqn = unsafe {
495                    OdeCallbacks::<diffsol::FaerMat<$t>>::new(callbacks, batch_context(nbatch)?)
496                };
497                Box::new(GenericSolve::from_eqn(eqn?)?)
498            }
499            MatrixType::FaerSparse => {
500                let eqn = unsafe {
501                    OdeCallbacks::<diffsol::FaerSparseMat<$t>>::new(
502                        callbacks,
503                        batch_context(nbatch)?,
504                    )
505                };
506                Box::new(GenericSolve::from_eqn(eqn?)?)
507            }
508        }
509    }};
510}
511
512macro_rules! impl_callback_scalar {
513    ($t:ty) => {
514        impl sealed::Sealed for $t {
515            unsafe fn new_ode(
516                callbacks: OdeCallbacksDesc<$t>,
517                matrix_type: MatrixType,
518                linear_solver: LinearSolverType,
519                ode_solver: OdeSolverType,
520                nbatch: usize,
521            ) -> Result<OdeWrapper, DiffsolRtError> {
522                let solve = solve_for_matrix_type!($t, callbacks, matrix_type, nbatch);
523                OdeWrapper::build(
524                    String::new(),
525                    <$t as ToScalarType>::scalar_type(),
526                    solve,
527                    None,
528                    linear_solver,
529                    ode_solver,
530                )
531            }
532        }
533
534        impl CallbackScalar for $t {}
535    };
536}
537
538impl_callback_scalar!(f64);
539impl_callback_scalar!(f32);
540
541impl OdeWrapper {
542    /// Construct an ODE solver from equations that C callbacks give.
543    ///
544    /// The scalar type `T` (`f64` or `f32`) of the callbacks is also the scalar type of the
545    /// solver. [crate::ode_callbacks] gives the callback conventions.
546    ///
547    /// The solver integrates `nbatch` lanes (independent copies of the system, each with its
548    /// own parameters and state) together. `nbatch` must be at least 1.
549    ///
550    /// The solver takes ownership of `callbacks.user_data`. If `callbacks.free_user_data` is
551    /// set, the solver calls it one time. It calls it when the solver is dropped. If this
552    /// function fails, it calls it before it returns.
553    ///
554    /// # Safety
555    /// `callbacks` must satisfy the conditions of [OdeCallbacks::new].
556    pub unsafe fn new_callbacks<T: CallbackScalar>(
557        callbacks: OdeCallbacksDesc<T>,
558        matrix_type: MatrixType,
559        linear_solver: LinearSolverType,
560        ode_solver: OdeSolverType,
561        nbatch: usize,
562    ) -> Result<Self, DiffsolRtError> {
563        if let Err(message) = callbacks.check_nbatch(nbatch) {
564            // ownership of `user_data` has passed to us, so free it on every failure path
565            // SAFETY: nothing else has taken ownership of `user_data` yet
566            unsafe { callbacks.free_user_data() };
567            return Err(DiffsolError::Other(message).into());
568        }
569        // SAFETY: the caller guarantees the conditions of `OdeCallbacks::new`
570        unsafe { T::new_ode(callbacks, matrix_type, linear_solver, ode_solver, nbatch) }
571    }
572}
573
574/// An error recorded while evaluating the callbacks of an [`OdeCallbacks`].
575#[derive(Clone, Copy, Debug, PartialEq, Eq)]
576pub enum OdeCallbackError {
577    /// The callback returned a non-zero status code.
578    Failed { callback: &'static str, code: i32 },
579    /// The solver needed a callback that was not provided.
580    Missing { callback: &'static str },
581}
582
583impl fmt::Display for OdeCallbackError {
584    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
585        match self {
586            OdeCallbackError::Failed { callback, code } => {
587                write!(f, "callback '{callback}' returned error code {code}")
588            }
589            OdeCallbackError::Missing { callback } => {
590                f.write_str(&missing_message("evaluating the equations", callback))
591            }
592        }
593    }
594}
595
596/// The message for a missing callback `name`, the same whether the missing callback is found
597/// by the check before a solve or when the solver calls it.
598fn missing_message(operation: &str, name: &str) -> String {
599    format!("OdeCallbacks: {operation} requires the '{name}' callback")
600}
601
602/// A matrix type that [`OdeCallbacks`] can use: its vectors give views of their data
603/// ([`TensorLayoutMut`]), so the callbacks can read and write all the lanes with no copy.
604pub trait CallbackMatrix: Matrix<V: TensorLayoutMut> {}
605
606impl<M: Matrix<V: TensorLayoutMut>> CallbackMatrix for M {}
607
608/// A sparsity pattern together with the coloring used to evaluate a matrix with that pattern.
609struct Pattern<M: Matrix> {
610    sparsity: M::Sparsity,
611    coloring: JacobianColoring<M>,
612}
613
614impl<M: Matrix> Pattern<M> {
615    fn new(nrows: usize, ncols: usize, non_zeros: Vec<(usize, usize)>, ctx: &M::C) -> Self {
616        let sparsity = MatrixSparsity::try_from_indices(nrows, ncols, non_zeros.clone())
617            .expect("invalid sparsity pattern");
618        let coloring = JacobianColoring::new(&sparsity, &non_zeros, ctx.clone());
619        Self { sparsity, coloring }
620    }
621}
622
623/// The patterns of the matrices of one non-linear operator (`rhs`, `root`, `out` or `reset`).
624struct NonLinearPatterns<M: Matrix> {
625    jac: Option<Pattern<M>>,
626    adjoint: Option<Pattern<M>>,
627    sens: Option<Pattern<M>>,
628    sens_adjoint: Option<Pattern<M>>,
629}
630
631impl<M: Matrix> Default for NonLinearPatterns<M> {
632    fn default() -> Self {
633        Self {
634            jac: None,
635            adjoint: None,
636            sens: None,
637            sens_adjoint: None,
638        }
639    }
640}
641
642/// Every sparsity pattern of an [`OdeCallbacks`]. Only computed for sparse matrix types, where
643/// the matrices cannot be built without them.
644struct Patterns<M: Matrix> {
645    rhs: NonLinearPatterns<M>,
646    root: NonLinearPatterns<M>,
647    out: NonLinearPatterns<M>,
648    reset: NonLinearPatterns<M>,
649    init_sens: Option<Pattern<M>>,
650    mass: Option<Pattern<M>>,
651    mass_transpose: Option<Pattern<M>>,
652}
653
654impl<M: Matrix> Default for Patterns<M> {
655    fn default() -> Self {
656        Self {
657            rhs: NonLinearPatterns::default(),
658            root: NonLinearPatterns::default(),
659            out: NonLinearPatterns::default(),
660            reset: NonLinearPatterns::default(),
661            init_sens: None,
662            mass: None,
663            mass_transpose: None,
664        }
665    }
666}
667
668/// ODE equations defined by C callbacks, see the [module documentation](self).
669pub struct OdeCallbacks<M: Matrix> {
670    callbacks: OdeCallbacksDesc<M::T>,
671    p: M::V,
672    ctx: M::C,
673    patterns: Patterns<M>,
674    statistics: RefCell<OpStatistics>,
675    error: Cell<Option<OdeCallbackError>>,
676}
677
678impl<M: Matrix> Drop for OdeCallbacks<M> {
679    fn drop(&mut self) {
680        // SAFETY: `OdeCallbacks` took ownership of `user_data` on construction, and is only
681        // dropped once
682        unsafe { self.callbacks.free_user_data() };
683    }
684}
685
686impl<M: CallbackMatrix> OdeCallbacks<M> {
687    /// Create the equations from `callbacks`. All parameters are zero at the start.
688    ///
689    /// This function takes ownership of `callbacks.user_data`, also if it returns an error. See
690    /// the [module documentation](self).
691    ///
692    /// For sparse matrix types, this function finds the sparsity pattern of each derivative
693    /// callback. To do this, it calls the callback with NaN inputs at the initial state and
694    /// `t = 0`.
695    ///
696    /// # Safety
697    /// - Each function pointer in `callbacks` must be null or a valid function with the
698    ///   signature that the [module documentation](self) gives.
699    /// - Each function must stay valid until the equations are dropped.
700    /// - Each function must read and write only the values that the views show, and must not
701    ///   write to an input view.
702    /// - If `free_user_data` is set, the equations own `user_data`. Do not free `user_data`
703    ///   yourself, and do not give it to other equations.
704    /// - If `free_user_data` is not set, `user_data` must stay valid until the equations are
705    ///   dropped.
706    pub unsafe fn new(callbacks: OdeCallbacksDesc<M::T>, ctx: M::C) -> Result<Self, DiffsolError> {
707        let mut eqn = Self {
708            p: M::V::zeros(callbacks.nparams, ctx.clone()),
709            callbacks,
710            ctx,
711            patterns: Patterns::default(),
712            statistics: RefCell::new(OpStatistics::default()),
713            error: Cell::new(None),
714        };
715        // `eqn` owns `user_data` from here on, so it is freed even if validation fails
716        eqn.callbacks
717            .validate()
718            .map_err(|msg| DiffsolError::Other(format!("Invalid ODE callbacks: {msg}")))?;
719        // the solver needs all lanes to find the same root at the same time, and it cannot
720        // report an error if they do not
721        if eqn.ctx.nbatch() > 1 && (eqn.callbacks.root.is_some() || eqn.callbacks.reset.is_some()) {
722            return Err(DiffsolError::Other(
723                "Invalid ODE callbacks: the 'root' and 'reset' callbacks are not available with \
724                 more than one lane (nbatch > 1)"
725                    .to_string(),
726            ));
727        }
728        if M::is_sparse() {
729            eqn.patterns = eqn.find_patterns();
730            if let Some(err) = eqn.take_callback_error() {
731                return Err(DiffsolError::Other(format!(
732                    "Failed to find the sparsity pattern of the ODE callbacks: {err}"
733                )));
734            }
735        }
736        Ok(eqn)
737    }
738
739    /// Take (and clear) the first error recorded while evaluating the callbacks.
740    pub fn take_callback_error(&self) -> Option<OdeCallbackError> {
741        self.error.take()
742    }
743
744    /// Record `err` (if it is the first error) and fill all the lanes of `y` with NaN.
745    fn record(&self, err: OdeCallbackError, y: &mut M::V) {
746        if self.error.get().is_none() {
747            self.error.set(Some(err));
748        }
749        y.fill(M::T::NAN);
750    }
751
752    /// Call the callback `f` one time for all the lanes of `y`, with `call`. `call` gets a view
753    /// of each vector of `inputs` and a view of `y`, all with the `[n, P, B]` layout of the module
754    /// documentation. If `f` is missing or fails, record the error and fill `y` with NaN.
755    fn eval_lanes<F, const N: usize>(
756        &self,
757        name: &'static str,
758        f: Option<F>,
759        inputs: [&M::V; N],
760        y: &mut M::V,
761        call: impl FnOnce(F, [*const TensorView<'_>; N], *mut TensorViewMut<'_>) -> i32,
762    ) {
763        let Some(f) = f else {
764            return self.record(OdeCallbackError::Missing { callback: name }, y);
765        };
766        // `y` has `P * B` lanes. The inputs have `P * B`, `B` or 1 lanes, and diffsol repeats
767        // each lane of an input with `B` lanes over `P` adjacent lanes of `y`
768        let nlanes = y.context().nbatch();
769        let nbatch = self.ctx.nbatch();
770        let (ninner, nouter) = if nlanes.is_multiple_of(nbatch) {
771            (nlanes / nbatch, nbatch)
772        } else {
773            (nlanes, 1)
774        };
775        // SAFETY: each layout shows the data of a vector that stays borrowed until the callback
776        // returns. The inputs are only read, and `y` is mutably borrowed, so nothing else reads
777        // or writes its data during the call
778        let views = inputs.map(|v| unsafe {
779            TensorView::from_layout(v.tensor_layout().batch_lanes(ninner, nouter))
780        });
781        let code = {
782            let y_layout = y.tensor_layout_mut().batch_lanes(ninner, nouter);
783            let mut y_view = unsafe { TensorViewMut::from_layout(y_layout) };
784            // the view goes out of scope before `y` is used again
785            call(f, views.each_ref().map(|v| v as *const _), &mut y_view)
786        };
787        if code != 0 {
788            self.record(
789                OdeCallbackError::Failed {
790                    callback: name,
791                    code,
792                },
793                y,
794            );
795        }
796    }
797
798    // SAFETY (for each `unsafe` call below): the views have the layouts in the module
799    // documentation, and `user_data` is the pointer the caller gave with the callbacks.
800
801    /// Evaluate `y = f(x, p, t)` for all the lanes.
802    fn eval(&self, name: &'static str, f: OdeCallbackFn<M::T>, x: &M::V, t: M::T, y: &mut M::V) {
803        let ud = self.callbacks.user_data;
804        self.eval_lanes(name, f, [x, &self.p], y, |f, [x, p], y| unsafe {
805            f(x, p, t, y, ud)
806        });
807    }
808
809    /// Evaluate `y = f(x, p, t, v)` for all the lanes.
810    fn eval_mul(
811        &self,
812        name: &'static str,
813        f: OdeCallbackMulFn<M::T>,
814        x: &M::V,
815        t: M::T,
816        v: &M::V,
817        y: &mut M::V,
818    ) {
819        let ud = self.callbacks.user_data;
820        self.eval_lanes(name, f, [x, &self.p, v], y, |f, [x, p, v], y| unsafe {
821            f(x, p, t, v, y, ud)
822        });
823    }
824
825    /// Evaluate `y = f(p, t)` for all the lanes.
826    fn eval_init(&self, name: &'static str, f: OdeCallbackInitFn<M::T>, t: M::T, y: &mut M::V) {
827        let ud = self.callbacks.user_data;
828        self.eval_lanes(name, f, [&self.p], y, |f, [p], y| unsafe { f(p, t, y, ud) });
829    }
830
831    /// Evaluate `y = f(p, t, v)` for all the lanes.
832    fn eval_init_mul(
833        &self,
834        name: &'static str,
835        f: OdeCallbackInitMulFn<M::T>,
836        t: M::T,
837        v: &M::V,
838        y: &mut M::V,
839    ) {
840        let ud = self.callbacks.user_data;
841        self.eval_lanes(name, f, [&self.p, v], y, |f, [p, v], y| unsafe {
842            f(p, t, v, y, ud)
843        });
844    }
845
846    /// Evaluate `y = f(v, p, t, beta, y)` for all the lanes.
847    fn eval_mass(
848        &self,
849        name: &'static str,
850        f: OdeCallbackMassFn<M::T>,
851        v: &M::V,
852        t: M::T,
853        beta: M::T,
854        y: &mut M::V,
855    ) {
856        let ud = self.callbacks.user_data;
857        self.eval_lanes(name, f, [v, &self.p], y, |f, [v, p], y| unsafe {
858            f(v, p, t, beta, y, ud)
859        });
860    }
861
862    /// Find the sparsity patterns of the matrices that diffsol builds, for each provided
863    /// derivative callback. These are the same matrices that `OdeBuilder` finds patterns for:
864    /// all four of the rhs, the sensitivities of root and reset, and the sensitivities and
865    /// adjoints of out.
866    fn find_patterns(&self) -> Patterns<M> {
867        let t0 = M::T::zero();
868        let y0 = self.init().call(t0);
869        let cb = &self.callbacks;
870        let ctx = &self.ctx;
871        let rhs = nonlinear_patterns(
872            &self.rhs(),
873            &y0,
874            t0,
875            [cb.rhs_jac, cb.rhs_adjoint, cb.rhs_sens, cb.rhs_sens_adjoint],
876            ctx,
877        );
878        let root = self
879            .root()
880            .map(|root| nonlinear_patterns(&root, &y0, t0, [None, None, cb.root_sens, None], ctx));
881        let out = self.out().map(|out| {
882            let callbacks = [None, cb.out_adjoint, cb.out_sens, cb.out_sens_adjoint];
883            nonlinear_patterns(&out, &y0, t0, callbacks, ctx)
884        });
885        let reset = self.reset().map(|reset| {
886            nonlinear_patterns(&reset, &y0, t0, [None, None, cb.reset_sens, None], ctx)
887        });
888        let init_sens = (cb.init_sens.is_some() && cb.nparams > 0).then(|| {
889            let init = self.init();
890            let non_zeros = find_constant_sens_non_zeros(&init, t0);
891            Pattern::new(init.nout(), cb.nparams, non_zeros, ctx)
892        });
893        let n = cb.nstates;
894        let (mass, mass_transpose) = match self.mass() {
895            Some(mass) => {
896                let non_zeros = find_matrix_non_zeros(&mass, t0);
897                let transpose = cb
898                    .mass_adjoint
899                    .map(|_| Pattern::new(n, n, transposed(&non_zeros), ctx));
900                (Some(Pattern::new(n, n, non_zeros, ctx)), transpose)
901            }
902            None => (None, None),
903        };
904        Patterns {
905            rhs,
906            root: root.unwrap_or_default(),
907            out: out.unwrap_or_default(),
908            reset: reset.unwrap_or_default(),
909            init_sens,
910            mass,
911            mass_transpose,
912        }
913    }
914
915    /// Check the callbacks needed by `requirement` were provided. Each row of the tables below is
916    /// `(needed, name, provided)`: the callback `name` is needed if `needed` is true.
917    fn check(&self, requirement: EqnRequirement) -> Result<(), DiffsolRtError> {
918        let cb = &self.callbacks;
919        let (has_out, has_root, has_reset) =
920            (cb.out.is_some(), cb.root.is_some(), cb.reset.is_some());
921        // implicit methods need the rhs Jacobian. Tsit45, the only explicit method, does not
922        // support a mass matrix, so a mass matrix needs no separate row.
923        let needs_rhs_jac = |method| !matches!(method, OdeSolverType::Tsit45);
924        let (operation, rows) = match requirement {
925            EqnRequirement::Solve(method) => (
926                "this solver",
927                vec![(needs_rhs_jac(method), "rhs_jac", cb.rhs_jac.is_some())],
928            ),
929            EqnRequirement::FwdSens => (
930                "a forward sensitivity solve",
931                vec![
932                    (true, "rhs_jac", cb.rhs_jac.is_some()),
933                    (true, "rhs_sens", cb.rhs_sens.is_some()),
934                    (true, "init_sens", cb.init_sens.is_some()),
935                    (has_out, "out_jac", cb.out_jac.is_some()),
936                    (has_out, "out_sens", cb.out_sens.is_some()),
937                    (has_root, "root_jac", cb.root_jac.is_some()),
938                    (has_root, "root_sens", cb.root_sens.is_some()),
939                    (has_reset, "reset_jac", cb.reset_jac.is_some()),
940                    (has_reset, "reset_sens", cb.reset_sens.is_some()),
941                ],
942            ),
943            EqnRequirement::Adjoint(method) => (
944                "an adjoint solve",
945                vec![
946                    (needs_rhs_jac(method), "rhs_jac", cb.rhs_jac.is_some()),
947                    (true, "rhs_adjoint", cb.rhs_adjoint.is_some()),
948                    (true, "rhs_sens_adjoint", cb.rhs_sens_adjoint.is_some()),
949                    (true, "init_sens_adjoint", cb.init_sens_adjoint.is_some()),
950                    (cb.mass.is_some(), "mass_adjoint", cb.mass_adjoint.is_some()),
951                    (has_out, "out_adjoint", cb.out_adjoint.is_some()),
952                    (has_out, "out_sens_adjoint", cb.out_sens_adjoint.is_some()),
953                    (has_root, "root_jac", cb.root_jac.is_some()),
954                    (has_root, "root_adjoint", cb.root_adjoint.is_some()),
955                    (
956                        has_root,
957                        "root_sens_adjoint",
958                        cb.root_sens_adjoint.is_some(),
959                    ),
960                    (has_reset, "reset_jac", cb.reset_jac.is_some()),
961                    (has_reset, "reset_adjoint", cb.reset_adjoint.is_some()),
962                    (
963                        has_reset,
964                        "reset_sens_adjoint",
965                        cb.reset_sens_adjoint.is_some(),
966                    ),
967                ],
968            ),
969            // roots and resets are not supported, see GenericSolve::solve_vjp_fwd
970            EqnRequirement::Vjp(method) => (
971                "a VJP solve",
972                vec![
973                    (needs_rhs_jac(method), "rhs_jac", cb.rhs_jac.is_some()),
974                    (true, "rhs_adjoint", cb.rhs_adjoint.is_some()),
975                    (true, "rhs_sens_adjoint", cb.rhs_sens_adjoint.is_some()),
976                    (true, "init_sens_adjoint", cb.init_sens_adjoint.is_some()),
977                    (cb.mass.is_some(), "mass_adjoint", cb.mass_adjoint.is_some()),
978                    (has_out, "out_adjoint", cb.out_adjoint.is_some()),
979                    (has_out, "out_sens_adjoint", cb.out_sens_adjoint.is_some()),
980                ],
981            ),
982        };
983        match rows
984            .into_iter()
985            .find(|&(needed, _, provided)| needed && !provided)
986        {
987            Some((_, name, _)) => Err(DiffsolError::Other(missing_message(operation, name)).into()),
988            None => Ok(()),
989        }
990    }
991}
992
993/// The pattern of the transpose of a matrix with the non-zeros `non_zeros`.
994fn transposed(non_zeros: &[(usize, usize)]) -> Vec<(usize, usize)> {
995    non_zeros.iter().map(|&(i, j)| (j, i)).collect()
996}
997
998/// Find the patterns of the matrices of a non-linear operator, for each of the
999/// `[jac, adjoint, sens, sens_adjoint]` callbacks that is provided. The pattern of an adjoint
1000/// matrix is the transpose of the forward pattern when that was found, otherwise it is probed.
1001fn nonlinear_patterns<M, F>(
1002    op: &F,
1003    y0: &M::V,
1004    t0: M::T,
1005    callbacks: [OdeCallbackMulFn<M::T>; 4],
1006    ctx: &M::C,
1007) -> NonLinearPatterns<M>
1008where
1009    M: Matrix,
1010    F: NonLinearOpJacobian<M = M, V = M::V, T = M::T, C = M::C>
1011        + NonLinearOpAdjoint<M = M, V = M::V, T = M::T, C = M::C>
1012        + NonLinearOpSens<M = M, V = M::V, T = M::T, C = M::C>
1013        + NonLinearOpSensAdjoint<M = M, V = M::V, T = M::T, C = M::C>,
1014{
1015    let [has_jac, has_adjoint, has_sens, has_sens_adjoint] = callbacks.map(|f| f.is_some());
1016    let (nstates, nout, nparams) = (op.nstates(), op.nout(), op.nparams());
1017    let has_params = nparams > 0;
1018    let jac = has_jac.then(|| find_jacobian_non_zeros(op, y0, t0));
1019    let sens = (has_sens && has_params).then(|| find_sens_non_zeros(op, y0, t0));
1020    let adjoint = has_adjoint.then(|| match &jac {
1021        Some(jac) => transposed(jac),
1022        None => find_adjoint_non_zeros(op, y0, t0),
1023    });
1024    let sens_adjoint = (has_sens_adjoint && has_params).then(|| match &sens {
1025        Some(sens) => transposed(sens),
1026        None => find_sens_adjoint_non_zeros(op, y0, t0),
1027    });
1028    NonLinearPatterns {
1029        jac: jac.map(|nz| Pattern::new(nout, nstates, nz, ctx)),
1030        adjoint: adjoint.map(|nz| Pattern::new(nstates, nout, nz, ctx)),
1031        sens: sens.map(|nz| Pattern::new(nout, nparams, nz, ctx)),
1032        sens_adjoint: sens_adjoint.map(|nz| Pattern::new(nparams, nout, nz, ctx)),
1033    }
1034}
1035
1036impl<M: Matrix> Op for OdeCallbacks<M> {
1037    type T = M::T;
1038    type V = M::V;
1039    type M = M;
1040    type C = M::C;
1041
1042    fn context(&self) -> &Self::C {
1043        &self.ctx
1044    }
1045    fn nstates(&self) -> usize {
1046        self.callbacks.nstates
1047    }
1048    fn nout(&self) -> usize {
1049        if self.callbacks.out.is_some() {
1050            self.callbacks.nout
1051        } else {
1052            self.callbacks.nstates
1053        }
1054    }
1055    fn nparams(&self) -> usize {
1056        self.callbacks.nparams
1057    }
1058    fn statistics(&self) -> OpStatistics {
1059        self.statistics.borrow().clone()
1060    }
1061}
1062
1063/// The right-hand side `F(x, p, t)` of an [`OdeCallbacks`].
1064pub struct OdeCallbacksRhs<'a, M: Matrix>(&'a OdeCallbacks<M>);
1065/// The root function of an [`OdeCallbacks`].
1066pub struct OdeCallbacksRoot<'a, M: Matrix>(&'a OdeCallbacks<M>);
1067/// The output function of an [`OdeCallbacks`].
1068pub struct OdeCallbacksOut<'a, M: Matrix>(&'a OdeCallbacks<M>);
1069/// The reset function of an [`OdeCallbacks`].
1070pub struct OdeCallbacksReset<'a, M: Matrix>(&'a OdeCallbacks<M>);
1071/// The initial condition of an [`OdeCallbacks`].
1072pub struct OdeCallbacksInit<'a, M: Matrix>(&'a OdeCallbacks<M>);
1073/// The mass matrix of an [`OdeCallbacks`].
1074pub struct OdeCallbacksMass<'a, M: Matrix>(&'a OdeCallbacks<M>);
1075
1076macro_rules! impl_op_for_view {
1077    ($view:ident, |$eqn:ident| $nout:expr $(, $stats:ident)?) => {
1078        impl<M: Matrix> Op for $view<'_, M> {
1079            type T = M::T;
1080            type V = M::V;
1081            type M = M;
1082            type C = M::C;
1083
1084            fn context(&self) -> &Self::C {
1085                &self.0.ctx
1086            }
1087            fn nstates(&self) -> usize {
1088                self.0.callbacks.nstates
1089            }
1090            #[allow(clippy::misnamed_getters)]
1091            fn nout(&self) -> usize {
1092                let $eqn = &self.0.callbacks;
1093                $nout
1094            }
1095            fn nparams(&self) -> usize {
1096                self.0.callbacks.nparams
1097            }
1098            $(
1099                fn $stats(&self) -> OpStatistics {
1100                    self.0.statistics.borrow().clone()
1101                }
1102            )?
1103        }
1104    };
1105}
1106
1107impl_op_for_view!(OdeCallbacksRhs, |cb| cb.nstates, statistics);
1108impl_op_for_view!(OdeCallbacksRoot, |cb| cb.nroots);
1109impl_op_for_view!(OdeCallbacksOut, |cb| cb.nout);
1110impl_op_for_view!(OdeCallbacksReset, |cb| cb.nstates);
1111impl_op_for_view!(OdeCallbacksInit, |cb| cb.nstates);
1112impl_op_for_view!(OdeCallbacksMass, |cb| cb.nstates);
1113
1114/// Implement the non-linear operator traits for a view of `rhs`, `root`, `out` or `reset`.
1115/// `$stats` says whether to count evaluations in the equations' statistics (only the rhs does).
1116macro_rules! impl_nonlinear_view {
1117    ($view:ident, $f:ident, $stats:literal) => {
1118        paste::paste! {
1119        impl<M: CallbackMatrix> NonLinearOp for $view<'_, M> {
1120            fn call_inplace(&self, x: &M::V, t: M::T, y: &mut M::V) {
1121                if $stats {
1122                    self.0.statistics.borrow_mut().increment_call();
1123                }
1124                self.0.eval(stringify!($f), self.0.callbacks.$f, x, t, y);
1125            }
1126        }
1127
1128        impl<M: CallbackMatrix> NonLinearOpJacobian for $view<'_, M> {
1129            fn jac_mul_inplace(&self, x: &M::V, t: M::T, v: &M::V, y: &mut M::V) {
1130                if $stats {
1131                    self.0.statistics.borrow_mut().increment_jac_mul();
1132                }
1133                self.0
1134                    .eval_mul(stringify!([<$f _jac>]), self.0.callbacks.[<$f _jac>], x, t, v, y);
1135            }
1136            fn jacobian_inplace(&self, x: &M::V, t: M::T, y: &mut M) {
1137                if $stats {
1138                    self.0.statistics.borrow_mut().increment_matrix();
1139                }
1140                match self.0.patterns.$f.jac.as_ref() {
1141                    Some(pattern) => pattern.coloring.jacobian_inplace(self, x, t, y),
1142                    None => self._default_jacobian_inplace(x, t, y),
1143                }
1144            }
1145            fn jacobian_sparsity(&self) -> Option<M::Sparsity> {
1146                self.0
1147                    .patterns
1148                    .$f
1149                    .jac
1150                    .as_ref()
1151                    .map(|p| p.sparsity.clone())
1152            }
1153        }
1154
1155        impl<M: CallbackMatrix> NonLinearOpSens for $view<'_, M> {
1156            fn sens_mul_inplace(&self, x: &M::V, t: M::T, v: &M::V, y: &mut M::V) {
1157                self.0
1158                    .eval_mul(stringify!([<$f _sens>]), self.0.callbacks.[<$f _sens>], x, t, v, y);
1159            }
1160            fn sens_inplace(&self, x: &M::V, t: M::T, y: &mut M) {
1161                match self.0.patterns.$f.sens.as_ref() {
1162                    Some(pattern) => pattern.coloring.sens_inplace(self, x, t, y),
1163                    None => self._default_sens_inplace(x, t, y),
1164                }
1165            }
1166            fn sens_sparsity(&self) -> Option<M::Sparsity> {
1167                self.0
1168                    .patterns
1169                    .$f
1170                    .sens
1171                    .as_ref()
1172                    .map(|p| p.sparsity.clone())
1173            }
1174        }
1175
1176        impl<M: CallbackMatrix> NonLinearOpAdjoint for $view<'_, M> {
1177            fn jac_transpose_mul_inplace(&self, x: &M::V, t: M::T, v: &M::V, y: &mut M::V) {
1178                if $stats {
1179                    self.0.statistics.borrow_mut().increment_jac_adj_mul();
1180                }
1181                self.0
1182                    .eval_mul(stringify!([<$f _adjoint>]), self.0.callbacks.[<$f _adjoint>], x, t, v, y);
1183            }
1184            fn adjoint_inplace(&self, x: &M::V, t: M::T, y: &mut M) {
1185                match self.0.patterns.$f.adjoint.as_ref() {
1186                    Some(pattern) => pattern.coloring.adjoint_inplace(self, x, t, y),
1187                    None => self._default_adjoint_inplace(x, t, y),
1188                }
1189            }
1190            fn adjoint_sparsity(&self) -> Option<M::Sparsity> {
1191                self.0
1192                    .patterns
1193                    .$f
1194                    .adjoint
1195                    .as_ref()
1196                    .map(|p| p.sparsity.clone())
1197            }
1198        }
1199
1200        impl<M: CallbackMatrix> NonLinearOpSensAdjoint for $view<'_, M> {
1201            fn sens_transpose_mul_inplace(&self, x: &M::V, t: M::T, v: &M::V, y: &mut M::V) {
1202                self.0.eval_mul(
1203                    stringify!([<$f _sens_adjoint>]),
1204                    self.0.callbacks.[<$f _sens_adjoint>],
1205                    x,
1206                    t,
1207                    v,
1208                    y,
1209                );
1210            }
1211            fn sens_adjoint_inplace(&self, x: &M::V, t: M::T, y: &mut M) {
1212                match self.0.patterns.$f.sens_adjoint.as_ref() {
1213                    Some(pattern) => pattern.coloring.sens_adjoint_inplace(self, x, t, y),
1214                    None => self._default_sens_adjoint_inplace(x, t, y),
1215                }
1216            }
1217            fn sens_adjoint_sparsity(&self) -> Option<M::Sparsity> {
1218                self.0
1219                    .patterns
1220                    .$f
1221                    .sens_adjoint
1222                    .as_ref()
1223                    .map(|p| p.sparsity.clone())
1224            }
1225        }
1226        }
1227    };
1228}
1229
1230impl_nonlinear_view!(OdeCallbacksRhs, rhs, true);
1231impl_nonlinear_view!(OdeCallbacksRoot, root, false);
1232impl_nonlinear_view!(OdeCallbacksOut, out, false);
1233impl_nonlinear_view!(OdeCallbacksReset, reset, false);
1234
1235impl<M: CallbackMatrix> ConstantOp for OdeCallbacksInit<'_, M> {
1236    fn call_inplace(&self, t: M::T, y: &mut M::V) {
1237        self.0.eval_init("init", self.0.callbacks.init, t, y);
1238    }
1239}
1240
1241impl<M: CallbackMatrix> ConstantOpSens for OdeCallbacksInit<'_, M> {
1242    fn sens_mul_inplace(&self, t: M::T, v: &M::V, y: &mut M::V) {
1243        self.0
1244            .eval_init_mul("init_sens", self.0.callbacks.init_sens, t, v, y);
1245    }
1246    fn sens_inplace(&self, t: M::T, y: &mut M) {
1247        match self.0.patterns.init_sens.as_ref() {
1248            Some(pattern) => pattern.coloring.constant_sens_inplace(self, t, y),
1249            None => self._default_sens_inplace(t, y),
1250        }
1251    }
1252    fn sens_sparsity(&self) -> Option<M::Sparsity> {
1253        self.0
1254            .patterns
1255            .init_sens
1256            .as_ref()
1257            .map(|p| p.sparsity.clone())
1258    }
1259}
1260
1261impl<M: CallbackMatrix> ConstantOpSensAdjoint for OdeCallbacksInit<'_, M> {
1262    fn sens_transpose_mul_inplace(&self, t: M::T, v: &M::V, y: &mut M::V) {
1263        self.0.eval_init_mul(
1264            "init_sens_adjoint",
1265            self.0.callbacks.init_sens_adjoint,
1266            t,
1267            v,
1268            y,
1269        );
1270    }
1271}
1272
1273impl<M: CallbackMatrix> LinearOp for OdeCallbacksMass<'_, M> {
1274    fn gemv_inplace(&self, x: &M::V, t: M::T, beta: M::T, y: &mut M::V) {
1275        self.0
1276            .eval_mass("mass", self.0.callbacks.mass, x, t, beta, y);
1277    }
1278    fn matrix_inplace(&self, t: M::T, y: &mut M) {
1279        match self.0.patterns.mass.as_ref() {
1280            Some(pattern) => pattern.coloring.matrix_inplace(self, t, y),
1281            None => self._default_matrix_inplace(t, y),
1282        }
1283    }
1284    fn sparsity(&self) -> Option<M::Sparsity> {
1285        self.0.patterns.mass.as_ref().map(|p| p.sparsity.clone())
1286    }
1287}
1288
1289impl<M: CallbackMatrix> LinearOpTranspose for OdeCallbacksMass<'_, M> {
1290    fn gemv_transpose_inplace(&self, x: &M::V, t: M::T, beta: M::T, y: &mut M::V) {
1291        self.0
1292            .eval_mass("mass_adjoint", self.0.callbacks.mass_adjoint, x, t, beta, y);
1293    }
1294    fn transpose_inplace(&self, t: M::T, y: &mut M) {
1295        match self.0.patterns.mass_transpose.as_ref() {
1296            Some(pattern) => pattern.coloring.transpose_inplace(self, t, y),
1297            None => self._default_transpose_inplace(t, y),
1298        }
1299    }
1300    fn transpose_sparsity(&self) -> Option<M::Sparsity> {
1301        self.0
1302            .patterns
1303            .mass_transpose
1304            .as_ref()
1305            .map(|p| p.sparsity.clone())
1306    }
1307}
1308
1309impl<'a, M: CallbackMatrix> OdeEquationsRef<'a> for OdeCallbacks<M> {
1310    type Mass = OdeCallbacksMass<'a, M>;
1311    type Rhs = OdeCallbacksRhs<'a, M>;
1312    type Root = OdeCallbacksRoot<'a, M>;
1313    type Init = OdeCallbacksInit<'a, M>;
1314    type Out = OdeCallbacksOut<'a, M>;
1315    type Reset = OdeCallbacksReset<'a, M>;
1316}
1317
1318impl<M: CallbackMatrix> OdeEquations for OdeCallbacks<M> {
1319    fn rhs(&self) -> OdeCallbacksRhs<'_, M> {
1320        OdeCallbacksRhs(self)
1321    }
1322    fn mass(&self) -> Option<OdeCallbacksMass<'_, M>> {
1323        self.callbacks.mass.map(|_| OdeCallbacksMass(self))
1324    }
1325    fn root(&self) -> Option<OdeCallbacksRoot<'_, M>> {
1326        self.callbacks.root.map(|_| OdeCallbacksRoot(self))
1327    }
1328    fn out(&self) -> Option<OdeCallbacksOut<'_, M>> {
1329        self.callbacks.out.map(|_| OdeCallbacksOut(self))
1330    }
1331    fn reset(&self) -> Option<OdeCallbacksReset<'_, M>> {
1332        self.callbacks.reset.map(|_| OdeCallbacksReset(self))
1333    }
1334    fn init(&self) -> OdeCallbacksInit<'_, M> {
1335        OdeCallbacksInit(self)
1336    }
1337    fn set_params(&mut self, p: &M::V) {
1338        self.p.copy_from(p);
1339    }
1340    fn get_params(&self, p: &mut M::V) {
1341        p.copy_from(&self.p);
1342    }
1343}
1344
1345impl<M: CallbackMatrix> SolveEqn for OdeCallbacks<M> {
1346    fn check_requirement(&self, requirement: EqnRequirement) -> Result<(), DiffsolRtError> {
1347        self.check(requirement)
1348    }
1349
1350    fn take_error(&self) -> Option<String> {
1351        self.take_callback_error().map(|err| err.to_string())
1352    }
1353}
1354
1355/// Call `f` one time for each lane of the output `y` of a callback, with the same lane of each
1356/// input. This is a simple way to write a callback in Rust: `f` gets one lane of each view as a
1357/// slice, so it does not use the strides. It is the equivalent of
1358/// [`diffsol::Vector::for_each_batch`] for the views of a callback.
1359///
1360/// The last argument of `f` is the index of the lane of `y`, `b * P + k` for the lane `(k, b)`
1361/// (the lane index of diffsol). The batch lane is `lane / P`, where `P` is `y.shape()[1]`.
1362///
1363/// ```ignore
1364/// unsafe extern "C" fn rhs(
1365///     x: *const TensorView<'_>,
1366///     p: *const TensorView<'_>,
1367///     _t: f64,
1368///     y: *mut TensorViewMut<'_>,
1369///     _user_data: *mut c_void,
1370/// ) -> i32 {
1371///     unsafe { for_each_batch([x, p], y, |[x, p], y: &mut [f64], _lane| y[0] = -p[0] * x[0]) };
1372///     0
1373/// }
1374/// ```
1375///
1376/// # Panics
1377/// If a view does not have 3 axes, the scalar type of a view is not `T`, or an input does not
1378/// have the lanes `[P, B]` of `y`.
1379///
1380/// # Safety
1381/// The pointers must be the arguments that the library gave to the callback, and the call must
1382/// be during the callback.
1383pub unsafe fn for_each_batch<T: crate::scalar_type::Scalar, const N: usize>(
1384    inputs: [*const TensorView<'_>; N],
1385    y: *mut TensorViewMut<'_>,
1386    mut f: impl FnMut([&[T]; N], &mut [T], usize),
1387) {
1388    // SAFETY: the caller gives the views of the callback, which are valid during the call
1389    let (inputs, y) = unsafe { (inputs.map(|v| &*v), &mut *y) };
1390    let dtype = T::scalar_type();
1391    assert!(
1392        y.shape().len() == 3 && y.dtype() == dtype,
1393        "y must have 3 axes and the scalar type {dtype:?}"
1394    );
1395    let lanes = [y.shape()[1], y.shape()[2]];
1396    for (i, v) in inputs.iter().enumerate() {
1397        assert!(
1398            v.shape().len() == 3 && v.dtype() == dtype && v.shape()[1..] == lanes,
1399            "input {i} must have 3 axes, the scalar type {dtype:?} and the lanes {lanes:?} of y"
1400        );
1401    }
1402    for b in 0..lanes[1] {
1403        for k in 0..lanes[0] {
1404            // the checks above make the scalar types and the indices correct
1405            let lane_inputs = inputs.map(|v| v.column::<T>(&[k, b]).unwrap());
1406            f(
1407                lane_inputs,
1408                y.column_mut::<T>(&[k, b]).unwrap(),
1409                b * lanes[0] + k,
1410            );
1411        }
1412    }
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417    use std::cell::RefCell;
1418    use std::ffi::c_void;
1419    use std::sync::atomic::{AtomicUsize, Ordering};
1420
1421    use diffsol::{
1422        ConstantOp, Context, LinearOp, LinearOpTranspose, Matrix, NalgebraContext, NalgebraMat,
1423        NonLinearOp, NonLinearOpJacobian, OdeEquations, Vector,
1424    };
1425
1426    use super::{for_each_batch, OdeCallbackError, OdeCallbacks, OdeCallbacksDesc};
1427    use crate::tensor::{TensorView, TensorViewMut};
1428
1429    type M = NalgebraMat<f64>;
1430    type View<'a> = *const TensorView<'a>;
1431    type ViewMut<'a> = *mut TensorViewMut<'a>;
1432
1433    unsafe extern "C" fn rhs(
1434        x: View<'_>,
1435        p: View<'_>,
1436        _t: f64,
1437        y: ViewMut<'_>,
1438        _ud: *mut c_void,
1439    ) -> i32 {
1440        unsafe { for_each_batch([x, p], y, |[x, p], y: &mut [f64], _| y[0] = -p[0] * x[0]) };
1441        0
1442    }
1443
1444    unsafe extern "C" fn rhs_jac(
1445        _x: View<'_>,
1446        p: View<'_>,
1447        _t: f64,
1448        v: View<'_>,
1449        y: ViewMut<'_>,
1450        _ud: *mut c_void,
1451    ) -> i32 {
1452        unsafe { for_each_batch([p, v], y, |[p, v], y: &mut [f64], _| y[0] = -p[0] * v[0]) };
1453        0
1454    }
1455
1456    unsafe extern "C" fn failing_rhs(
1457        _x: View<'_>,
1458        _p: View<'_>,
1459        _t: f64,
1460        _y: ViewMut<'_>,
1461        _ud: *mut c_void,
1462    ) -> i32 {
1463        7
1464    }
1465
1466    unsafe extern "C" fn init(p: View<'_>, _t: f64, y: ViewMut<'_>, _ud: *mut c_void) -> i32 {
1467        unsafe { for_each_batch([p], y, |_, y: &mut [f64], _| y.fill(1.0)) };
1468        0
1469    }
1470
1471    unsafe extern "C" fn count_free(ud: *mut c_void) {
1472        unsafe { &*(ud as *const AtomicUsize) }.fetch_add(1, Ordering::SeqCst);
1473    }
1474
1475    // a 2 state system with the non-symmetric mass matrix [[1, 2], [0, 3]]
1476    unsafe extern "C" fn rhs2(
1477        x: View<'_>,
1478        _p: View<'_>,
1479        _t: f64,
1480        y: ViewMut<'_>,
1481        _ud: *mut c_void,
1482    ) -> i32 {
1483        unsafe {
1484            for_each_batch([x], y, |[x], y: &mut [f64], _| {
1485                y.copy_from_slice(&[-x[0], -x[1]])
1486            })
1487        };
1488        0
1489    }
1490
1491    unsafe extern "C" fn init2(p: View<'_>, _t: f64, y: ViewMut<'_>, _ud: *mut c_void) -> i32 {
1492        unsafe { for_each_batch([p], y, |_, y: &mut [f64], _| y.fill(1.0)) };
1493        0
1494    }
1495
1496    unsafe extern "C" fn mass2(
1497        v: View<'_>,
1498        _p: View<'_>,
1499        _t: f64,
1500        beta: f64,
1501        y: ViewMut<'_>,
1502        _ud: *mut c_void,
1503    ) -> i32 {
1504        unsafe {
1505            for_each_batch([v], y, |[v], y: &mut [f64], _| {
1506                y[0] = v[0] + 2.0 * v[1] + beta * y[0];
1507                y[1] = 3.0 * v[1] + beta * y[1];
1508            })
1509        };
1510        0
1511    }
1512
1513    unsafe extern "C" fn mass2_adjoint(
1514        v: View<'_>,
1515        _p: View<'_>,
1516        _t: f64,
1517        beta: f64,
1518        y: ViewMut<'_>,
1519        _ud: *mut c_void,
1520    ) -> i32 {
1521        unsafe {
1522            for_each_batch([v], y, |[v], y: &mut [f64], _| {
1523                y[0] = v[0] + beta * y[0];
1524                y[1] = 2.0 * v[0] + 3.0 * v[1] + beta * y[1];
1525            })
1526        };
1527        0
1528    }
1529
1530    fn desc() -> OdeCallbacksDesc<f64> {
1531        OdeCallbacksDesc::new(1, 1, rhs, init)
1532    }
1533
1534    fn build(desc: OdeCallbacksDesc<f64>) -> Result<OdeCallbacks<M>, diffsol::DiffsolError> {
1535        // SAFETY: the test callbacks are valid and use the documented lengths
1536        unsafe { OdeCallbacks::<M>::new(desc, NalgebraContext::default()) }
1537    }
1538
1539    fn build_err(desc: OdeCallbacksDesc<f64>) -> String {
1540        match build(desc) {
1541            Ok(_) => panic!("expected the descriptor to be rejected"),
1542            Err(err) => err.to_string(),
1543        }
1544    }
1545
1546    #[test]
1547    fn evaluates_callbacks() {
1548        let mut d = desc();
1549        d.rhs_jac = Some(rhs_jac);
1550        let mut eqn = build(d).unwrap();
1551        let ctx = NalgebraContext::default();
1552        eqn.set_params(&ctx.vector_from_vec(vec![2.0]));
1553        let x = ctx.vector_from_vec(vec![3.0]);
1554        eqn.rhs()
1555            .call(&x, 0.0)
1556            .assert_eq_st(&ctx.vector_from_vec(vec![-6.0]), 1e-12);
1557        eqn.rhs()
1558            .jac_mul(&x, 0.0, &x)
1559            .assert_eq_st(&ctx.vector_from_vec(vec![-6.0]), 1e-12);
1560        eqn.init()
1561            .call(0.0)
1562            .assert_eq_st(&ctx.vector_from_vec(vec![1.0]), 1e-12);
1563        assert!(eqn.mass().is_none());
1564        assert!(eqn.root().is_none());
1565        assert!(eqn.out().is_none());
1566        assert!(eqn.reset().is_none());
1567        assert!(eqn.take_callback_error().is_none());
1568    }
1569
1570    #[test]
1571    fn records_failures_and_missing_callbacks() {
1572        let mut d = desc();
1573        d.rhs = Some(failing_rhs);
1574        let eqn = build(d).unwrap();
1575        let x = NalgebraContext::default().vector_from_vec(vec![3.0]);
1576        let y = eqn.rhs().call(&x, 0.0);
1577        assert!(y.get_index(0).is_nan());
1578        assert_eq!(
1579            eqn.take_callback_error(),
1580            Some(OdeCallbackError::Failed {
1581                callback: "rhs",
1582                code: 7
1583            })
1584        );
1585        assert!(eqn.take_callback_error().is_none());
1586
1587        let y = eqn.rhs().jac_mul(&x, 0.0, &x);
1588        assert!(y.get_index(0).is_nan());
1589        assert_eq!(
1590            eqn.take_callback_error(),
1591            Some(OdeCallbackError::Missing {
1592                callback: "rhs_jac"
1593            })
1594        );
1595    }
1596
1597    /// A failure fills all the lanes of the output with NaN.
1598    #[test]
1599    fn failures_fill_all_lanes_with_nan() {
1600        let mut d = desc();
1601        d.rhs = Some(failing_rhs);
1602        let ctx = NalgebraContext::default().clone_with_nbatch(3).unwrap();
1603        // SAFETY: the test callbacks are valid and use the documented lengths
1604        let eqn = unsafe { OdeCallbacks::<M>::new(d, ctx) }.unwrap();
1605        let x = ctx.vector_from_vec(vec![1.0, 2.0, 3.0]);
1606        let y = eqn.rhs().call(&x, 0.0);
1607        assert!(y.clone_as_vec().iter().all(|y| y.is_nan()));
1608    }
1609
1610    /// The layouts that a callback saw: `(shape, strides)` of each argument, `y` last.
1611    type Seen = Vec<([usize; 3], [usize; 3])>;
1612
1613    thread_local! {
1614        static SEEN: RefCell<Seen> = const { RefCell::new(Vec::new()) };
1615    }
1616
1617    fn layout(shape: &[usize], strides: &[usize]) -> ([usize; 3], [usize; 3]) {
1618        (shape.try_into().unwrap(), strides.try_into().unwrap())
1619    }
1620
1621    /// `y = diag(p) v` with 2 states and 2 parameters, and record the layouts.
1622    unsafe extern "C" fn record_jac(
1623        x: View<'_>,
1624        p: View<'_>,
1625        _t: f64,
1626        v: View<'_>,
1627        y: ViewMut<'_>,
1628        _ud: *mut c_void,
1629    ) -> i32 {
1630        let seen = [x, p, v].map(|v| unsafe { layout((*v).shape(), (*v).strides()) });
1631        let y_seen = unsafe { layout((*y).shape(), (*y).strides()) };
1632        SEEN.with(|s| *s.borrow_mut() = [&seen[..], &[y_seen]].concat());
1633        unsafe {
1634            for_each_batch([p, v], y, |[p, v], y: &mut [f64], _| {
1635                y[0] = p[0] * v[0];
1636                y[1] = p[1] * v[1];
1637            })
1638        };
1639        0
1640    }
1641
1642    /// In a forward sensitivity solve, the state and the parameters have `nbatch` lanes, and
1643    /// `v` and `y` have one lane for each batch lane and each parameter. The callback sees the
1644    /// state and the parameters with a stride of 0 on the parameter axis.
1645    #[test]
1646    fn grouped_broadcast_gives_strides_of_zero() {
1647        let (nbatch, nparams) = (2, 2);
1648        let mut d = OdeCallbacksDesc::new(2, nparams, rhs2, init2);
1649        d.rhs_jac = Some(record_jac);
1650        let ctx = NalgebraContext::default()
1651            .clone_with_nbatch(nbatch)
1652            .unwrap();
1653        let aug_ctx = ctx.clone_with_nbatch(nbatch * nparams).unwrap();
1654        // SAFETY: the test callbacks are valid and use the documented lengths
1655        let mut eqn = unsafe { OdeCallbacks::<M>::new(d, ctx) }.unwrap();
1656        // lane `b` has the parameters `[10 b + 1, 10 b + 2]`
1657        eqn.set_params(&ctx.vector_from_vec(vec![1.0, 2.0, 11.0, 12.0]));
1658        let x = ctx.vector_from_vec(vec![0.0; 4]);
1659        // lane `b * nparams + k` of `v` is `[b, k]`
1660        let v_values: Vec<f64> = (0..nbatch)
1661            .flat_map(|b| (0..nparams).flat_map(move |k| [b as f64, k as f64]))
1662            .collect();
1663        let v = aug_ctx.vector_from_vec(v_values);
1664        let mut y = aug_ctx.vector_from_vec(vec![0.0; 8]);
1665        eqn.rhs().jac_mul_inplace(&x, 0.0, &v, &mut y);
1666        assert!(eqn.take_callback_error().is_none());
1667
1668        let s = std::mem::size_of::<f64>();
1669        let c = 2 * s;
1670        let seen = SEEN.with(|s| s.borrow().clone());
1671        let shape = [2, nparams, nbatch];
1672        assert_eq!(
1673            seen,
1674            vec![
1675                (shape, [s, 0, c]),
1676                (shape, [s, 0, c]),
1677                (shape, [s, c, nparams * c]),
1678                (shape, [s, c, nparams * c]),
1679            ]
1680        );
1681        let expected: Vec<f64> = (0..nbatch)
1682            .flat_map(|b| {
1683                let p = [10.0 * b as f64 + 1.0, 10.0 * b as f64 + 2.0];
1684                (0..nparams).flat_map(move |k| [p[0] * b as f64, p[1] * k as f64])
1685            })
1686            .collect();
1687        assert_eq!(y.clone_as_vec(), expected);
1688    }
1689
1690    #[test]
1691    fn rejects_invalid_descriptors() {
1692        let mut d = desc();
1693        d.rhs = None;
1694        assert!(build_err(d).contains("'rhs' callback is required"));
1695
1696        let mut d = desc();
1697        d.init = None;
1698        assert!(build_err(d).contains("'init' callback is required"));
1699
1700        let mut d = desc();
1701        d.nstates = 0;
1702        assert!(build_err(d).contains("nstates"));
1703
1704        let mut d = desc();
1705        d.nout = 2;
1706        assert!(build_err(d).contains("nout"));
1707
1708        let mut d = desc();
1709        d.out = Some(rhs);
1710        assert!(build_err(d).contains("nout"));
1711
1712        let mut d = desc();
1713        d.nroots = 1;
1714        assert!(build_err(d).contains("nroots"));
1715
1716        let mut d = desc();
1717        d.out_jac = Some(rhs_jac);
1718        assert!(build_err(d).contains("'out_jac' callback requires the 'out' callback"));
1719
1720        let mut d = desc();
1721        d.root = Some(rhs);
1722        d.nroots = 1;
1723        d.reset_jac = Some(rhs_jac);
1724        assert!(build_err(d).contains("'reset_jac' callback requires the 'reset' callback"));
1725    }
1726
1727    #[test]
1728    fn frees_user_data_exactly_once() {
1729        let count = AtomicUsize::new(0);
1730        let mut d = desc();
1731        d.user_data = &count as *const AtomicUsize as *mut c_void;
1732        d.free_user_data = Some(count_free);
1733
1734        drop(build(d).unwrap());
1735        assert_eq!(count.load(Ordering::SeqCst), 1);
1736
1737        d.rhs = None;
1738        assert!(build(d).is_err());
1739        assert_eq!(count.load(Ordering::SeqCst), 2);
1740    }
1741
1742    /// The mass matrix and its transpose are built correctly from their colorings for a
1743    /// sparse matrix type, also when the mass matrix is not symmetric.
1744    #[test]
1745    fn sparse_mass_and_transpose_matrices() {
1746        type S = diffsol::FaerSparseMat<f64>;
1747        let mut desc = OdeCallbacksDesc::new(2, 0, rhs2, init2);
1748        desc.mass = Some(mass2);
1749        desc.mass_adjoint = Some(mass2_adjoint);
1750        // SAFETY: the test callbacks are valid and use the documented lengths
1751        let eqn = unsafe { OdeCallbacks::<S>::new(desc, Default::default()) }.unwrap();
1752        let mass = eqn.mass().unwrap();
1753        let dense = |m: &S| {
1754            let mut d = [[0.0; 2]; 2];
1755            let (indices, values) = m.triplet_iter();
1756            for ((i, j), v) in indices.zip(values) {
1757                d[i][j] += v;
1758            }
1759            d
1760        };
1761        let m = mass.matrix(0.0);
1762        assert_eq!(dense(&m), [[1.0, 2.0], [0.0, 3.0]]);
1763        let mut mt = S::new_from_sparsity(2, 2, mass.transpose_sparsity(), Default::default());
1764        mass.transpose_inplace(0.0, &mut mt);
1765        assert_eq!(dense(&mt), [[1.0, 0.0], [2.0, 3.0]]);
1766    }
1767}