Skip to main content

oximo_pounce/
persistent.rs

1//! A resident POUNCE handle that keeps the built derivative oracle alive across
2//! solves and warm-starts each solve from the previous iterate.
3
4use std::time::Instant;
5
6use oximo_core::{Model, ModelKind};
7use oximo_solver::{Solver, SolverError, SolverResult};
8
9use crate::options::PounceOptions;
10use crate::translate::{WarmStart, assemble, setup};
11
12#[cfg(feature = "enzyme")]
13use crate::exact as backend;
14#[cfg(not(feature = "enzyme"))]
15use crate::stable as backend;
16
17struct State {
18    oracle: backend::Oracle,
19    warm: Option<WarmStart>,
20}
21
22/// A stateful POUNCE handle that keeps the derivative build resident across
23/// solves. Created by [`Pounce::persistent`](crate::Pounce).
24///
25/// When the next model has the same variables, objective, and constraint
26/// expressions with an unchanged sparsity pattern, the resident oracle
27/// is refreshed in place, reusing the compiled tapes (and, on the `enzyme`
28/// path, the exact jacobians/Hessians structure) instead of rebuilding.
29/// Also, the solve is warm-started from the previous iterate.
30/// Any structural change rebuilds.
31///
32/// A failed solve clears the resident state. The next call rebuilds from scratch.
33#[derive(Default)]
34pub struct PouncePersistent {
35    state: Option<State>,
36}
37
38impl std::fmt::Debug for PouncePersistent {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("PouncePersistent").field("resident", &self.state.is_some()).finish()
41    }
42}
43
44impl PouncePersistent {
45    /// A fresh handle with no model loaded. The first solve builds it.
46    #[must_use]
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Drop the resident oracle so the next [`solve`](Solver::solve) rebuilds
52    /// from scratch (and starts from the model's initial point).
53    pub fn reset(&mut self) {
54        self.state = None;
55    }
56
57    fn solve_resident(
58        &mut self,
59        model: &Model,
60        opts: &PounceOptions,
61    ) -> Result<SolverResult, SolverError> {
62        let prep = setup(model, opts)?;
63        let started = Instant::now();
64
65        let state = match &mut self.state {
66            Some(state) if backend::try_reuse(&state.oracle, model) => state,
67            state => state.insert(State { oracle: backend::build(model)?, warm: None }),
68        };
69        let mut outcome = backend::run(&state.oracle, &prep, opts, state.warm.as_ref())?;
70        let elapsed = started.elapsed();
71
72        state.warm = outcome.warm.take();
73        Ok(assemble(prep.sign, outcome, elapsed))
74    }
75}
76
77impl Solver for PouncePersistent {
78    type Options = PounceOptions;
79
80    fn name(&self) -> &str {
81        "pounce"
82    }
83
84    fn supports(&self, kind: ModelKind) -> bool {
85        matches!(kind, ModelKind::LP | ModelKind::QP | ModelKind::QCP | ModelKind::NLP)
86    }
87
88    fn solve(&mut self, model: &Model, opts: &PounceOptions) -> Result<SolverResult, SolverError> {
89        match self.solve_resident(model, opts) {
90            Ok(result) => Ok(result),
91            Err(e) => {
92                self.state = None;
93                Err(e)
94            }
95        }
96    }
97}