1use 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
158pub 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
173pub 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
186pub 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
196pub 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
207pub 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
220pub type OdeCallbackFreeFn = Option<unsafe extern "C" fn(user_data: *mut c_void)>;
222
223#[repr(C)]
229#[derive(Clone, Copy, Debug)]
230pub struct OdeCallbacksDesc<T> {
231 pub nstates: usize,
233 pub nparams: usize,
235 pub nout: usize,
237 pub nroots: usize,
239 pub user_data: *mut c_void,
241 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
276unsafe impl<T: Send> Send for OdeCallbacksDesc<T> {}
282
283pub type DiffsolOdeCallbacksF64 = OdeCallbacksDesc<f64>;
285pub type DiffsolOdeCallbacksF32 = OdeCallbacksDesc<f32>;
287
288impl<T> OdeCallbacksDesc<T> {
289 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 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 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 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 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
445pub 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 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
472fn batch_context<C: Context>(nbatch: usize) -> Result<C, DiffsolError> {
474 Ok(C::default().clone_with_nbatch(nbatch)?)
475}
476
477macro_rules! solve_for_matrix_type {
482 ($t:ty, $callbacks:expr, $matrix_type:expr, $nbatch:expr) => {{
483 let callbacks = $callbacks;
484 let nbatch = $nbatch;
485 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 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 unsafe { callbacks.free_user_data() };
567 return Err(DiffsolError::Other(message).into());
568 }
569 unsafe { T::new_ode(callbacks, matrix_type, linear_solver, ode_solver, nbatch) }
571 }
572}
573
574#[derive(Clone, Copy, Debug, PartialEq, Eq)]
576pub enum OdeCallbackError {
577 Failed { callback: &'static str, code: i32 },
579 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
596fn missing_message(operation: &str, name: &str) -> String {
599 format!("OdeCallbacks: {operation} requires the '{name}' callback")
600}
601
602pub trait CallbackMatrix: Matrix<V: TensorLayoutMut> {}
605
606impl<M: Matrix<V: TensorLayoutMut>> CallbackMatrix for M {}
607
608struct 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
623struct 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
642struct 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
668pub 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 unsafe { self.callbacks.free_user_data() };
683 }
684}
685
686impl<M: CallbackMatrix> OdeCallbacks<M> {
687 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.callbacks
717 .validate()
718 .map_err(|msg| DiffsolError::Other(format!("Invalid ODE callbacks: {msg}")))?;
719 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 pub fn take_callback_error(&self) -> Option<OdeCallbackError> {
741 self.error.take()
742 }
743
744 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 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 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 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 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 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 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 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 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 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 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 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 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 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
993fn transposed(non_zeros: &[(usize, usize)]) -> Vec<(usize, usize)> {
995 non_zeros.iter().map(|&(i, j)| (j, i)).collect()
996}
997
998fn 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
1063pub struct OdeCallbacksRhs<'a, M: Matrix>(&'a OdeCallbacks<M>);
1065pub struct OdeCallbacksRoot<'a, M: Matrix>(&'a OdeCallbacks<M>);
1067pub struct OdeCallbacksOut<'a, M: Matrix>(&'a OdeCallbacks<M>);
1069pub struct OdeCallbacksReset<'a, M: Matrix>(&'a OdeCallbacks<M>);
1071pub struct OdeCallbacksInit<'a, M: Matrix>(&'a OdeCallbacks<M>);
1073pub 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
1114macro_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
1355pub 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 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 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 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 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 #[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 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 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 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 #[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 let mut eqn = unsafe { OdeCallbacks::<M>::new(d, ctx) }.unwrap();
1656 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 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 #[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 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}