Skip to main content

diffsol/ode_equations/
adjoint_equations.rs

1use num_traits::{One, Signed, Zero};
2use std::{
3    cell::RefCell,
4    ops::{AddAssign, SubAssign},
5    rc::Rc,
6};
7
8use crate::{
9    error::DiffsolError, op::nonlinear_op::NonLinearOpJacobian, AugmentedOdeEquations,
10    CheckpointingPath, ConstantOp, ConstantOpSensAdjoint, LinearOp, LinearOpTranspose, Matrix,
11    NonLinearOp, NonLinearOpAdjoint, NonLinearOpSensAdjoint, OdeEquations, OdeEquationsAdjoint,
12    OdeEquationsRef, OdeSolverMethod, OdeSolverProblem, OdeSolverState, Op, Scalar, Vector,
13};
14
15pub struct AdjointContext<'a, Eqn, State, Method>
16where
17    Eqn: OdeEquations,
18    State: OdeSolverState<Eqn::V>,
19    Method: OdeSolverMethod<'a, Eqn, State = State>,
20{
21    eqn: &'a Eqn,
22    t0: Eqn::T,
23    checkpointers: CheckpointingPath<Eqn, State>,
24    active_checkpointer: usize,
25    solver: Option<RefCell<Method>>,
26    x: Eqn::V,
27    index: usize,
28    max_index: usize,
29    last_t: Option<Eqn::T>,
30    col: Eqn::V,
31}
32
33impl<'a, Eqn, State, Method> AdjointContext<'a, Eqn, State, Method>
34where
35    Eqn: OdeEquations,
36    State: OdeSolverState<Eqn::V>,
37    Method: OdeSolverMethod<'a, Eqn, State = State>,
38{
39    pub fn new(
40        eqn: &'a Eqn,
41        t0: Eqn::T,
42        checkpointers: CheckpointingPath<Eqn, State>,
43        solver: Option<Method>,
44        max_index: usize,
45    ) -> Self {
46        let active_checkpointer = checkpointers
47            .len()
48            .checked_sub(1)
49            .expect("adjoint checkpointing path must not be empty");
50        let ctx = eqn.context();
51        let x = <Eqn::V as Vector>::zeros(eqn.rhs().nstates(), ctx.clone());
52        let mut col = <Eqn::V as Vector>::zeros(max_index, ctx.clone());
53        let index = 0;
54        col.set_index(0, Eqn::T::one());
55        Self {
56            eqn,
57            t0,
58            checkpointers,
59            active_checkpointer,
60            solver: solver.map(RefCell::new),
61            x,
62            index,
63            max_index,
64            col,
65            last_t: None,
66        }
67    }
68
69    fn active_checkpointer(&self) -> &crate::Checkpointing<Eqn, State> {
70        &self.checkpointers[self.active_checkpointer]
71    }
72
73    pub(crate) fn pop_last_checkpointing(&mut self) -> Option<crate::Checkpointing<Eqn, State>> {
74        if self.active_checkpointer == self.checkpointers.len() - 1 {
75            self.active_checkpointer = self.active_checkpointer.saturating_sub(1);
76        }
77        self.checkpointers.pop()
78    }
79
80    pub fn set_state(&mut self, t: Eqn::T) {
81        if let Some(last_t) = self.last_t {
82            if last_t == t {
83                return;
84            }
85        }
86        // clamp tiny boundary overshoots to the boundary values to avoid interpolation errors
87        let t0 = self.t0;
88        let t1 = self.checkpointers[self.checkpointers.len() - 1].end_t();
89        let boundary_tol = Eqn::T::EPSILON.sqrt() * (t.abs() + t1.abs() + Eqn::T::one());
90        let t_interp = if t > t1 && t - t1 <= boundary_tol {
91            t1
92        } else if t < t0 && t0 - t <= boundary_tol {
93            t0
94        } else {
95            t
96        };
97        self.last_t = Some(t_interp);
98        while self.active_checkpointer > 0
99            && t_interp + boundary_tol < self.active_checkpointer().first_t()
100        {
101            self.active_checkpointer -= 1;
102        }
103        while self.active_checkpointer + 1 < self.checkpointers.len()
104            && t_interp > self.active_checkpointer().end_t() + boundary_tol
105        {
106            self.active_checkpointer += 1;
107        }
108        let active_checkpointer = self.active_checkpointer;
109        let x = &mut self.x;
110        match self.solver.as_ref() {
111            Some(solver) => {
112                let mut solver = solver.borrow_mut();
113                self.checkpointers[active_checkpointer]
114                    .interpolate(Some(&mut *solver), t_interp, x)
115                    .unwrap();
116            }
117            None => self.checkpointers[active_checkpointer]
118                .interpolate::<Method>(None, t_interp, x)
119                .unwrap(),
120        }
121        // for diffsl, we need to set data for the adjoint state!
122        // basically just involves calling the normal rhs function with the new self.x
123        // todo: this seems a bit hacky, perhaps a dedicated function on the trait for this?
124        self.eqn.rhs().call(&self.x, t_interp);
125    }
126
127    pub fn state(&self) -> &Eqn::V {
128        &self.x
129    }
130
131    pub fn col(&self) -> &Eqn::V {
132        &self.col
133    }
134
135    pub fn set_index(&mut self, index: usize) {
136        self.col.set_index(self.index, Eqn::T::zero());
137        self.index = index;
138        self.col.set_index(self.index, Eqn::T::one());
139    }
140}
141
142pub struct AdjointMass<'a, Eqn>
143where
144    Eqn: OdeEquations,
145{
146    eqn: &'a Eqn,
147}
148
149impl<'a, Eqn> AdjointMass<'a, Eqn>
150where
151    Eqn: OdeEquations,
152{
153    pub fn new(eqn: &'a Eqn) -> Self {
154        Self { eqn }
155    }
156}
157
158impl<Eqn> Op for AdjointMass<'_, Eqn>
159where
160    Eqn: OdeEquations,
161{
162    type T = Eqn::T;
163    type V = Eqn::V;
164    type M = Eqn::M;
165    type C = Eqn::C;
166
167    fn nstates(&self) -> usize {
168        self.eqn.rhs().nstates()
169    }
170    fn nout(&self) -> usize {
171        self.eqn.rhs().nstates()
172    }
173    fn nparams(&self) -> usize {
174        self.eqn.rhs().nparams()
175    }
176    fn context(&self) -> &Self::C {
177        self.eqn.context()
178    }
179}
180
181impl<Eqn> LinearOp for AdjointMass<'_, Eqn>
182where
183    Eqn: OdeEquationsAdjoint,
184{
185    fn gemv_inplace(&self, x: &Self::V, t: Self::T, beta: Self::T, y: &mut Self::V) {
186        self.eqn
187            .mass()
188            .unwrap()
189            .gemv_transpose_inplace(x, t, beta, y);
190    }
191
192    fn matrix_inplace(&self, t: Self::T, y: &mut Self::M) {
193        self.eqn.mass().unwrap().transpose_inplace(t, y);
194    }
195
196    fn sparsity(&self) -> Option<<Self::M as Matrix>::Sparsity> {
197        self.eqn.mass().unwrap().transpose_sparsity()
198    }
199}
200
201pub struct AdjointInit<'a, Eqn, Method>
202where
203    Eqn: OdeEquations,
204    Method: OdeSolverMethod<'a, Eqn>,
205{
206    eqn: &'a Eqn,
207    _marker: std::marker::PhantomData<Method>,
208}
209
210impl<'a, Eqn, Method> AdjointInit<'a, Eqn, Method>
211where
212    Eqn: OdeEquations,
213    Method: OdeSolverMethod<'a, Eqn>,
214{
215    pub fn new(
216        eqn: &'a Eqn,
217        _context: Rc<RefCell<AdjointContext<'a, Eqn, Method::State, Method>>>,
218        _with_out: bool,
219    ) -> Self {
220        Self {
221            eqn,
222            _marker: std::marker::PhantomData,
223        }
224    }
225}
226
227impl<'a, Eqn, Method> Op for AdjointInit<'a, Eqn, Method>
228where
229    Eqn: OdeEquations,
230    Method: OdeSolverMethod<'a, Eqn>,
231{
232    type T = Eqn::T;
233    type V = Eqn::V;
234    type M = Eqn::M;
235    type C = Eqn::C;
236
237    fn nstates(&self) -> usize {
238        self.eqn.rhs().nstates()
239    }
240    fn nout(&self) -> usize {
241        self.eqn.rhs().nstates()
242    }
243    fn nparams(&self) -> usize {
244        self.eqn.rhs().nparams()
245    }
246    fn context(&self) -> &Self::C {
247        self.eqn.context()
248    }
249}
250
251impl<'a, Eqn, Method> ConstantOp for AdjointInit<'a, Eqn, Method>
252where
253    Eqn: OdeEquations,
254    Method: OdeSolverMethod<'a, Eqn>,
255{
256    fn call_inplace(&self, _t: Self::T, y: &mut Self::V) {
257        y.fill(Eqn::T::zero());
258    }
259}
260
261/// Right-hand side of the adjoint equations is:
262///
263/// F(λ, x, t) = -f^T_x(x, t) λ - g^T_x(x,t)
264///
265/// f_x is the partial derivative of the right-hand side with respect to the state vector.
266/// g_x is the partial derivative of the functional g with respect to the state vector.
267///
268/// We need the current state x(t), which is obtained from the checkpointed forward solve at the current time step.
269pub struct AdjointRhs<'a, Eqn, Method>
270where
271    Eqn: OdeEquations,
272    Method: OdeSolverMethod<'a, Eqn>,
273{
274    eqn: &'a Eqn,
275    context: Rc<RefCell<AdjointContext<'a, Eqn, Method::State, Method>>>,
276    tmp: RefCell<Eqn::V>,
277    with_out: bool,
278}
279
280impl<'a, Eqn, Method> AdjointRhs<'a, Eqn, Method>
281where
282    Eqn: OdeEquations,
283    Method: OdeSolverMethod<'a, Eqn>,
284{
285    pub fn new(
286        eqn: &'a Eqn,
287        context: Rc<RefCell<AdjointContext<'a, Eqn, Method::State, Method>>>,
288        with_out: bool,
289    ) -> Self {
290        let tmp_n = if with_out { eqn.rhs().nstates() } else { 0 };
291        let tmp = RefCell::new(<Eqn::V as Vector>::zeros(tmp_n, eqn.context().clone()));
292        Self {
293            eqn,
294            context,
295            tmp,
296            with_out,
297        }
298    }
299}
300
301impl<'a, Eqn, Method> Op for AdjointRhs<'a, Eqn, Method>
302where
303    Eqn: OdeEquations,
304    Method: OdeSolverMethod<'a, Eqn>,
305{
306    type T = Eqn::T;
307    type V = Eqn::V;
308    type M = Eqn::M;
309    type C = Eqn::C;
310
311    fn nstates(&self) -> usize {
312        self.eqn.rhs().nstates()
313    }
314    fn nout(&self) -> usize {
315        self.eqn.rhs().nstates()
316    }
317    fn nparams(&self) -> usize {
318        self.eqn.rhs().nparams()
319    }
320    fn context(&self) -> &Self::C {
321        self.eqn.context()
322    }
323}
324
325impl<'a, Eqn, Method> NonLinearOp for AdjointRhs<'a, Eqn, Method>
326where
327    Eqn: OdeEquationsAdjoint,
328    Method: OdeSolverMethod<'a, Eqn>,
329{
330    /// F(λ, x, t) = -f^T_x(x, t) λ - g^T_x(x,t)
331    fn call_inplace(&self, lambda: &Self::V, t: Self::T, y: &mut Self::V) {
332        self.context.borrow_mut().set_state(t);
333        let context = self.context.borrow();
334        let x = context.state();
335
336        // y = -f^T_x(x, t) λ
337        self.eqn.rhs().jac_transpose_mul_inplace(x, t, lambda, y);
338
339        // y = -f^T_x(x, t) λ - g^T_x(x,t)
340        if self.with_out {
341            let col = context.col();
342            if let Some(out) = self.eqn.out() {
343                let mut tmp = self.tmp.borrow_mut();
344                out.jac_transpose_mul_inplace(x, t, col, &mut tmp);
345                y.add_assign(&*tmp);
346            } else {
347                // Default output is identity on state when no explicit out is defined.
348                y.add_assign(col);
349            }
350        }
351    }
352}
353
354impl<'a, Eqn, Method> NonLinearOpJacobian for AdjointRhs<'a, Eqn, Method>
355where
356    Eqn: OdeEquationsAdjoint,
357    Method: OdeSolverMethod<'a, Eqn>,
358{
359    // J = -f^T_x(x, t)
360    fn jac_mul_inplace(&self, _x: &Self::V, t: Self::T, v: &Self::V, y: &mut Self::V) {
361        self.context.borrow_mut().set_state(t);
362        let context = self.context.borrow();
363        let x = context.state();
364        self.eqn.rhs().jac_transpose_mul_inplace(x, t, v, y);
365    }
366    fn jacobian_inplace(&self, _x: &Self::V, t: Self::T, y: &mut Self::M) {
367        self.context.borrow_mut().set_state(t);
368        let context = self.context.borrow();
369        let x = context.state();
370        self.eqn.rhs().adjoint_inplace(x, t, y);
371    }
372    fn jacobian_sparsity(&self) -> Option<<Self::M as Matrix>::Sparsity> {
373        self.eqn.rhs().adjoint_sparsity()
374    }
375}
376
377/// Output of the adjoint equations is:
378///
379/// F(λ, x, t) = -g_p^T(x, t) - f_p^T(x, t) λ
380///
381/// f_p is the partial derivative of the right-hand side with respect to the parameter vector
382/// g_p is the partial derivative of the functional g with respect to the parameter vector
383///
384/// We need the current state x(t), which is obtained from the checkpointed forward solve at the current time step.
385pub struct AdjointOut<'a, Eqn, Method>
386where
387    Eqn: OdeEquations,
388    Method: OdeSolverMethod<'a, Eqn>,
389{
390    eqn: &'a Eqn,
391    context: Rc<RefCell<AdjointContext<'a, Eqn, Method::State, Method>>>,
392    tmp: RefCell<Eqn::V>,
393    with_out: bool,
394}
395
396impl<'a, Eqn, Method> AdjointOut<'a, Eqn, Method>
397where
398    Eqn: OdeEquations,
399    Method: OdeSolverMethod<'a, Eqn>,
400{
401    pub fn new(
402        eqn: &'a Eqn,
403        context: Rc<RefCell<AdjointContext<'a, Eqn, Method::State, Method>>>,
404        with_out: bool,
405    ) -> Self {
406        let tmp_n = if with_out { eqn.rhs().nparams() } else { 0 };
407        let tmp = RefCell::new(<Eqn::V as Vector>::zeros(tmp_n, eqn.context().clone()));
408        Self {
409            eqn,
410            context,
411            tmp,
412            with_out,
413        }
414    }
415}
416
417impl<'a, Eqn, Method> Op for AdjointOut<'a, Eqn, Method>
418where
419    Eqn: OdeEquations,
420    Method: OdeSolverMethod<'a, Eqn>,
421{
422    type T = Eqn::T;
423    type V = Eqn::V;
424    type M = Eqn::M;
425    type C = Eqn::C;
426
427    fn nstates(&self) -> usize {
428        self.eqn.rhs().nstates()
429    }
430    fn nout(&self) -> usize {
431        self.eqn.rhs().nparams()
432    }
433    fn nparams(&self) -> usize {
434        self.eqn.rhs().nparams()
435    }
436    fn context(&self) -> &Self::C {
437        self.eqn.context()
438    }
439}
440
441impl<'a, Eqn, Method> NonLinearOp for AdjointOut<'a, Eqn, Method>
442where
443    Eqn: OdeEquationsAdjoint,
444    Method: OdeSolverMethod<'a, Eqn>,
445{
446    /// F(λ, x, t) = -g_p(x, t) - λ^T f_p(x, t)
447    fn call_inplace(&self, lambda: &Self::V, t: Self::T, y: &mut Self::V) {
448        self.context.borrow_mut().set_state(t);
449        let context = self.context.borrow();
450        let x = context.state();
451        self.eqn.rhs().sens_transpose_mul_inplace(x, t, lambda, y);
452
453        if self.with_out {
454            let col = context.col();
455            if let Some(out) = self.eqn.out() {
456                let mut tmp = self.tmp.borrow_mut();
457                out.sens_transpose_mul_inplace(x, t, col, &mut tmp);
458                y.add_assign(&*tmp);
459            }
460        }
461    }
462}
463
464impl<'a, Eqn, Method> NonLinearOpJacobian for AdjointOut<'a, Eqn, Method>
465where
466    Eqn: OdeEquationsAdjoint,
467    Method: OdeSolverMethod<'a, Eqn>,
468{
469    // J = -f_p(x, t)
470    fn jac_mul_inplace(&self, _x: &Self::V, t: Self::T, v: &Self::V, y: &mut Self::V) {
471        self.context.borrow_mut().set_state(t);
472        let context = self.context.borrow();
473        let x = context.state();
474        self.eqn.rhs().sens_transpose_mul_inplace(x, t, v, y);
475    }
476    fn jacobian_inplace(&self, _x: &Self::V, t: Self::T, y: &mut Self::M) {
477        self.context.borrow_mut().set_state(t);
478        let context = self.context.borrow();
479        let x = context.state();
480        self.eqn.rhs().sens_adjoint_inplace(x, t, y);
481    }
482    fn jacobian_sparsity(&self) -> Option<<Self::M as Matrix>::Sparsity> {
483        self.eqn.rhs().sens_adjoint_sparsity()
484    }
485}
486
487/// Adjoint equations for ODEs
488///
489/// M * dλ/dt = -f^T_x(x, t) λ - g^T_x(x,t)
490/// λ(T) = 0
491/// g(λ, x, t) = -g_p(x, t) - λ^T f_p(x, t)
492///
493pub struct AdjointEquations<'a, Eqn, Method>
494where
495    Eqn: OdeEquations,
496    Method: OdeSolverMethod<'a, Eqn>,
497{
498    eqn: &'a Eqn,
499    rhs: AdjointRhs<'a, Eqn, Method>,
500    out: AdjointOut<'a, Eqn, Method>,
501    mass: Option<AdjointMass<'a, Eqn>>,
502    context: Rc<RefCell<AdjointContext<'a, Eqn, Method::State, Method>>>,
503    tmp: RefCell<Eqn::V>,
504    tmp2: RefCell<Eqn::V>,
505    init: AdjointInit<'a, Eqn, Method>,
506    atol: Option<&'a Eqn::V>,
507    rtol: Option<Eqn::T>,
508    out_rtol: Option<Eqn::T>,
509    out_atol: Option<&'a Eqn::V>,
510}
511
512impl<'a, Eqn, Method> Clone for AdjointEquations<'a, Eqn, Method>
513where
514    Eqn: OdeEquations,
515    Method: OdeSolverMethod<'a, Eqn>,
516{
517    fn clone(&self) -> Self {
518        let context_ref = self.context.borrow();
519        let context = Rc::new(RefCell::new(AdjointContext::new(
520            context_ref.eqn,
521            context_ref.t0,
522            context_ref.checkpointers.clone(),
523            context_ref
524                .solver
525                .as_ref()
526                .map(|solver| solver.borrow().clone()),
527            context_ref.max_index,
528        )));
529        let rhs = AdjointRhs::new(self.eqn, context.clone(), self.rhs.with_out);
530        let init = AdjointInit::new(self.eqn, context.clone(), self.rhs.with_out);
531        let out = AdjointOut::new(self.eqn, context.clone(), self.out.with_out);
532        let tmp = self.tmp.clone();
533        let tmp2 = self.tmp2.clone();
534        let atol = self.atol;
535        let rtol = self.rtol;
536        let out_atol = self.out_atol;
537        let out_rtol = self.out_rtol;
538        let mass = self.eqn.mass().map(|_m| AdjointMass::new(self.eqn));
539        Self {
540            rhs,
541            init,
542            mass,
543            context,
544            out,
545            tmp,
546            tmp2,
547            eqn: self.eqn,
548            atol,
549            rtol,
550            out_rtol,
551            out_atol,
552        }
553    }
554}
555
556impl<'a, Eqn, Method> AdjointEquations<'a, Eqn, Method>
557where
558    Eqn: OdeEquationsAdjoint,
559    Method: OdeSolverMethod<'a, Eqn>,
560{
561    pub(crate) fn new(
562        problem: &'a OdeSolverProblem<Eqn>,
563        context: Rc<RefCell<AdjointContext<'a, Eqn, Method::State, Method>>>,
564        with_out: bool,
565    ) -> Self {
566        let eqn = &problem.eqn;
567        let rhs = AdjointRhs::new(eqn, context.clone(), with_out);
568        let init = AdjointInit::new(eqn, context.clone(), with_out);
569        let out = AdjointOut::new(eqn, context.clone(), with_out);
570        let tmp = RefCell::new(<Eqn::V as Vector>::zeros(
571            eqn.rhs().nparams(),
572            eqn.context().clone(),
573        ));
574        let tmp2 = RefCell::new(<Eqn::V as Vector>::zeros(
575            eqn.rhs().nstates(),
576            eqn.context().clone(),
577        ));
578        let atol = problem.sens_atol.as_ref().or(Some(&problem.atol));
579        let rtol = problem.sens_rtol.or(Some(problem.rtol));
580        let out_atol = problem.param_atol.as_ref();
581        let out_rtol = problem.param_rtol;
582        let mass = eqn.mass().map(|_m| AdjointMass::new(eqn));
583        Self {
584            rhs,
585            init,
586            mass,
587            context,
588            out,
589            tmp,
590            tmp2,
591            eqn,
592            atol,
593            rtol,
594            out_rtol,
595            out_atol,
596        }
597    }
598
599    pub fn eqn(&self) -> &'a Eqn {
600        self.eqn
601    }
602
603    pub fn last_t(&self) -> Eqn::T {
604        self.context.borrow().checkpointers.last().unwrap().last_t()
605    }
606
607    pub fn last_h(&self) -> Option<Eqn::T> {
608        self.context.borrow().checkpointers.last().unwrap().last_h()
609    }
610
611    pub(crate) fn checkpointing_len(&self) -> usize {
612        self.context.borrow().checkpointers.len()
613    }
614
615    pub(crate) fn checkpointing_bounds(&self, index: usize) -> (Eqn::T, Eqn::T) {
616        let context = self.context.borrow();
617        let checkpointer = &context.checkpointers[index];
618        (checkpointer.first_t(), checkpointer.end_t())
619    }
620
621    pub(crate) fn checkpointing_terminal_reset_root_idx(&self, index: usize) -> Option<usize> {
622        self.context.borrow().checkpointers[index].terminal_reset_root_idx()
623    }
624
625    pub fn with_out(&self) -> bool {
626        self.rhs.with_out
627    }
628
629    pub fn correct_sg_for_init(&self, t: Eqn::T, s: &[Eqn::V], sg: &mut [Eqn::V]) {
630        let mut tmp = self.tmp.borrow_mut();
631        for (s_i, sg_i) in s.iter().zip(sg.iter_mut()) {
632            if let Some(mass) = self.eqn.mass() {
633                let mut tmp2 = self.tmp2.borrow_mut();
634                mass.call_transpose_inplace(s_i, t, &mut tmp2);
635                self.eqn
636                    .init()
637                    .sens_transpose_mul_inplace(t, &tmp2, &mut tmp);
638                sg_i.sub_assign(&*tmp);
639            } else {
640                self.eqn.init().sens_transpose_mul_inplace(t, s_i, &mut tmp);
641                sg_i.sub_assign(&*tmp);
642            }
643        }
644    }
645
646    pub fn interpolate_forward_state(&self, t: Eqn::T, y: &mut Eqn::V) -> Result<(), DiffsolError> {
647        let mut context = self.context.borrow_mut();
648        context.set_state(t);
649        y.copy_from(context.state());
650        Ok(())
651    }
652
653    pub fn checkpointing_last_state(&self, index: usize) -> Method::State {
654        self.context.borrow().checkpointers[index]
655            .last_checkpoint()
656            .clone()
657    }
658
659    pub fn checkpointing_first_state(&self, index: usize) -> Method::State {
660        self.context.borrow().checkpointers[index]
661            .first_checkpoint()
662            .clone()
663    }
664
665    pub fn pop_last_checkpointing(
666        &mut self,
667    ) -> Result<crate::Checkpointing<Eqn, Method::State>, DiffsolError> {
668        let mut context = self.context.borrow_mut();
669        context
670            .pop_last_checkpointing()
671            .ok_or_else(|| DiffsolError::Other("No more checkpointing to pop".to_string()))
672    }
673
674    pub fn into_checkpointing(self) -> CheckpointingPath<Eqn, Method::State> {
675        let Self {
676            rhs,
677            out,
678            context,
679            eqn: _,
680            mass: _,
681            tmp: _,
682            tmp2: _,
683            init: _,
684            atol: _,
685            rtol: _,
686            out_rtol: _,
687            out_atol: _,
688        } = self;
689
690        drop(rhs);
691        drop(out);
692
693        match Rc::try_unwrap(context) {
694            Ok(context) => context.into_inner().checkpointers,
695            Err(_) => {
696                panic!("adjoint context should be uniquely owned after consuming AdjointEquations")
697            }
698        }
699    }
700}
701
702impl<'a, Eqn, Method> std::fmt::Debug for AdjointEquations<'a, Eqn, Method>
703where
704    Eqn: OdeEquations,
705    Method: OdeSolverMethod<'a, Eqn>,
706{
707    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
708        f.debug_struct("AdjointEquations").finish()
709    }
710}
711
712impl<'a, Eqn, Method> Op for AdjointEquations<'a, Eqn, Method>
713where
714    Eqn: OdeEquations,
715    Method: OdeSolverMethod<'a, Eqn>,
716{
717    type T = Eqn::T;
718    type V = Eqn::V;
719    type M = Eqn::M;
720    type C = Eqn::C;
721
722    fn nstates(&self) -> usize {
723        self.eqn.rhs().nstates()
724    }
725    fn nout(&self) -> usize {
726        self.eqn.rhs().nout()
727    }
728    fn nparams(&self) -> usize {
729        self.eqn.rhs().nparams()
730    }
731    fn context(&self) -> &Self::C {
732        self.eqn.context()
733    }
734}
735
736impl<'a, 'b, Eqn, Method> OdeEquationsRef<'a> for AdjointEquations<'b, Eqn, Method>
737where
738    Eqn: OdeEquationsAdjoint,
739    Method: OdeSolverMethod<'b, Eqn>,
740{
741    type Rhs = &'a AdjointRhs<'b, Eqn, Method>;
742    type Mass = &'a AdjointMass<'b, Eqn>;
743    type Root = <Eqn as OdeEquationsRef<'a>>::Root;
744    type Init = &'a AdjointInit<'b, Eqn, Method>;
745    type Out = &'a AdjointOut<'b, Eqn, Method>;
746    type Reset = <Eqn as OdeEquationsRef<'a>>::Reset;
747}
748
749impl<'a, Eqn, Method> OdeEquations for AdjointEquations<'a, Eqn, Method>
750where
751    Eqn: OdeEquationsAdjoint,
752    Method: OdeSolverMethod<'a, Eqn>,
753{
754    fn rhs(&self) -> &AdjointRhs<'a, Eqn, Method> {
755        &self.rhs
756    }
757    fn mass(&self) -> Option<&AdjointMass<'a, Eqn>> {
758        self.mass.as_ref()
759    }
760    fn root(&self) -> Option<<Eqn as OdeEquationsRef<'_>>::Root> {
761        None
762    }
763    fn init(&self) -> &AdjointInit<'a, Eqn, Method> {
764        &self.init
765    }
766    fn out(&self) -> Option<&AdjointOut<'a, Eqn, Method>> {
767        Some(&self.out)
768    }
769    fn set_params(&mut self, p: &Self::V) {
770        self.eqn.set_params(p);
771    }
772    fn set_model_index(&mut self, m: usize) {
773        self.eqn.set_model_index(m);
774    }
775    fn get_params(&self, p: &mut Self::V) {
776        self.eqn.get_params(p);
777    }
778}
779
780impl<'a, Eqn, Method> AugmentedOdeEquations<Eqn> for AdjointEquations<'a, Eqn, Method>
781where
782    Eqn: OdeEquationsAdjoint,
783    Method: OdeSolverMethod<'a, Eqn>,
784{
785    fn include_in_error_control(&self) -> bool {
786        self.atol.is_some() && self.rtol.is_some()
787    }
788    fn include_out_in_error_control(&self) -> bool {
789        self.out().is_some() && self.out_atol.is_some() && self.out_rtol.is_some()
790    }
791
792    fn atol(&self) -> Option<&Eqn::V> {
793        self.atol
794    }
795    fn out_atol(&self) -> Option<&Eqn::V> {
796        self.out_atol
797    }
798    fn out_rtol(&self) -> Option<Eqn::T> {
799        self.out_rtol
800    }
801    fn rtol(&self) -> Option<Eqn::T> {
802        self.rtol
803    }
804
805    fn max_index(&self) -> usize {
806        self.context.borrow().max_index
807    }
808
809    fn set_index(&mut self, index: usize) {
810        self.context.borrow_mut().set_index(index);
811    }
812
813    fn update_rhs_out_state(&mut self, _y: &Eqn::V, _dy: &Eqn::V, _t: Eqn::T) {}
814
815    fn integrate_main_eqn(&self) -> bool {
816        false
817    }
818}
819
820#[cfg(test)]
821mod tests {
822    use std::{cell::RefCell, rc::Rc};
823
824    use crate::{
825        matrix::dense_nalgebra_serial::NalgebraMat,
826        ode_equations::{
827            adjoint_equations::AdjointEquations,
828            test_models::exponential_decay::exponential_decay_problem_adjoint,
829        },
830        AdjointContext, AugmentedOdeEquations, Checkpointing, DenseMatrix, FaerSparseLU,
831        FaerSparseMat, FaerVec, Matrix, MatrixCommon, NalgebraVec, NonLinearOp,
832        NonLinearOpJacobian, OdeEquations, Op, RkState, Vector,
833    };
834    type Mcpu = NalgebraMat<f64>;
835    type Vcpu = NalgebraVec<f64>;
836    type LS = crate::NalgebraLU<f64>;
837
838    #[test]
839    fn test_rhs_exponential() {
840        // dy/dt = -ay (p = [a])
841        // a = 0.1
842        let (problem, _soln) = exponential_decay_problem_adjoint::<Mcpu>(true, true);
843        let ctx = problem.eqn.context();
844        let state = RkState {
845            t: 0.0,
846            y: Vcpu::from_vec(vec![1.0, 1.0], *ctx),
847            dy: Vcpu::from_vec(vec![1.0, 1.0], *ctx),
848            g: Vcpu::zeros(0, *ctx),
849            dg: Vcpu::zeros(0, *ctx),
850            sg: Vec::new(),
851            dsg: Vec::new(),
852            s: Vec::new(),
853            ds: Vec::new(),
854            h: 0.0,
855        };
856        let nout = problem.eqn.out().unwrap().nout();
857        let mut solver = problem.esdirk34_solver::<LS>(state.clone()).unwrap();
858        let checkpointer = Checkpointing::new(
859            Some(&mut solver),
860            0,
861            vec![state.clone(), state.clone()],
862            None,
863        );
864        let context = Rc::new(RefCell::new(AdjointContext::new(
865            &problem.eqn,
866            problem.t0,
867            vec![checkpointer],
868            Some(solver.clone()),
869            nout,
870        )));
871        let adj_eqn = AdjointEquations::new(&problem, context.clone(), false);
872        // F(λ, x, t) = -f^T_x(x, t) λ
873        // f_x = |-a 0|
874        //       |0 -a|
875        // F(s, t)_0 =  |a 0| |1| = |a| = |0.1|
876        //              |0 a| |2|   |2a| = |0.2|
877        let v = Vcpu::from_vec(vec![1.0, 2.0], *ctx);
878        let f = adj_eqn.rhs.call(&v, state.t);
879        let f_expect = Vcpu::from_vec(vec![0.1, 0.2], *ctx);
880        f.assert_eq_st(&f_expect, 1e-10);
881
882        let mut adj_eqn = AdjointEquations::new(&problem, context, true);
883
884        // f_x^T = |-a 0|
885        //         |0 -a|
886        // J = -f_x^T
887        let adjoint = adj_eqn.rhs.jacobian(&state.y, state.t);
888        assert_eq!(adjoint.nrows(), 2);
889        assert_eq!(adjoint.ncols(), 2);
890        assert_eq!(adjoint.get_index(0, 0), 0.1);
891        assert_eq!(adjoint.get_index(1, 1), 0.1);
892
893        // g_x = |1 2|
894        //       |3 4|
895        // S = -g^T_x(x,t)
896        // so S = |-1 -3|
897        //        |-2 -4|
898
899        // f_p^T = |-x_1 -x_2 |
900        //         |0   0 |
901        // g_p = |0 0|
902        //       |0 0|
903        // g(λ, x, t) = -g_p(x, t) - λ^T f_p(x, t)
904        //            = |1  1| |1| + |0| = |3|
905        //              |0  0| |2|  |0|  = |0|
906        adj_eqn.set_index(0);
907        let out = adj_eqn.out.call(&v, state.t);
908        let out_expect = Vcpu::from_vec(vec![3.0, 0.0], *ctx);
909        out.assert_eq_st(&out_expect, 1e-10);
910
911        // F(λ, x, t) = -f^T_x(x, t) λ - g^T_x(x,t)
912        // f_x = |-a 0|
913        //       |0 -a|
914        // F(s, t)_0 =  |a 0| |1| - |1.0| = | a - 1| = |-0.9|
915        //              |0 a| |2|   |2.0|   |2a - 2| = |-1.8|
916        let f = adj_eqn.rhs.call(&v, state.t);
917        let f_expect = Vcpu::from_vec(vec![-0.9, -1.8], *ctx);
918        f.assert_eq_st(&f_expect, 1e-10);
919    }
920
921    #[test]
922    fn test_rhs_exponential_sparse() {
923        // dy/dt = -ay (p = [a])
924        // a = 0.1
925        let (problem, _soln) = exponential_decay_problem_adjoint::<FaerSparseMat<f64>>(true, true);
926        let ctx = problem.eqn.context();
927        let state = RkState {
928            t: 0.0,
929            y: FaerVec::from_vec(vec![1.0, 1.0], *ctx),
930            dy: FaerVec::from_vec(vec![1.0, 1.0], *ctx),
931            g: FaerVec::zeros(0, *ctx),
932            dg: FaerVec::zeros(0, *ctx),
933            sg: Vec::new(),
934            dsg: Vec::new(),
935            s: Vec::new(),
936            ds: Vec::new(),
937            h: 0.0,
938        };
939        let nout = problem.eqn.out().unwrap().nout();
940        let mut solver = problem
941            .esdirk34_solver::<FaerSparseLU<f64>>(state.clone())
942            .unwrap();
943        let checkpointer = Checkpointing::new(
944            Some(&mut solver),
945            0,
946            vec![state.clone(), state.clone()],
947            None,
948        );
949        let context = Rc::new(RefCell::new(AdjointContext::new(
950            &problem.eqn,
951            problem.t0,
952            vec![checkpointer],
953            Some(solver.clone()),
954            nout,
955        )));
956        let mut adj_eqn = AdjointEquations::new(&problem, context, true);
957
958        // f_x^T = |-a 0|
959        //         |0 -a|
960        // J = -f_x^T
961        let adjoint = adj_eqn.rhs.jacobian(&state.y, state.t);
962        assert_eq!(adjoint.nrows(), 2);
963        assert_eq!(adjoint.ncols(), 2);
964        let (idx, vals) = adjoint.triplet_iter();
965        for ((i, j), v) in idx.zip(vals) {
966            if i == j {
967                assert_eq!(v, 0.1);
968            } else {
969                assert_eq!(v, 0.0);
970            }
971        }
972
973        // g_x = |1 2|
974        //       |3 4|
975        // S = -g^T_x(x,t)
976        // so S = |-1 -3|
977        //        |-2 -4|
978
979        // F(λ, x, t) = -f^T_x(x, t) λ - g^T_x(x,t)
980        // f_x = |-a 0|
981        //       |0 -a|
982        // F(s, t)_0 =  |a 0| |1| - |1.0| = |a - 1| = |-0.9|
983        //              |0 a| |2|   |2.0|   |2a - 2| = |-1.8|
984        adj_eqn.set_index(0);
985        let v = FaerVec::from_vec(vec![1.0, 2.0], *ctx);
986        let f = adj_eqn.rhs.call(&v, state.t);
987        let f_expect = FaerVec::from_vec(vec![-0.9, -1.8], *ctx);
988        f.assert_eq_st(&f_expect, 1e-10);
989    }
990}