1use std::time::Instant;
5
6use oximo_core::{Model, ModelKind};
7use oximo_solver::{Solver, SolverError, SolverResult};
8
9use crate::convex::{self, Route};
10use crate::options::PounceOptions;
11use crate::translate::{WarmStart, assemble, run_nlp_with_retries, setup};
12
13#[cfg(feature = "enzyme")]
14use crate::exact as backend;
15#[cfg(not(feature = "enzyme"))]
16use crate::stable as backend;
17
18struct NlpState {
19 oracle: backend::Oracle,
20 warm: Option<WarmStart>,
21}
22
23struct ConvexState {
24 route: Route,
25 problem: convex::Problem,
26 warm: Option<pounce_rs::convex::QpWarmStart>,
27 active: Option<convex::ActivePersistent>,
28}
29
30enum State {
31 Nlp(NlpState),
32 Convex(Box<ConvexState>),
33}
34
35struct CachedValidation {
36 options: PounceOptions,
37 result: Result<(), String>,
38}
39
40#[derive(Default)]
52pub struct PouncePersistent {
53 state: Option<State>,
54 validation: Option<CachedValidation>,
55}
56
57impl std::fmt::Debug for PouncePersistent {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.debug_struct("PouncePersistent").field("resident", &self.state.is_some()).finish()
60 }
61}
62
63impl PouncePersistent {
64 #[must_use]
66 pub fn new() -> Self {
67 Self::default()
68 }
69
70 pub fn reset(&mut self) {
73 self.state = None;
74 self.validation = None;
75 }
76
77 fn solve_resident(
78 &mut self,
79 model: &Model,
80 opts: &PounceOptions,
81 ) -> Result<SolverResult, SolverError> {
82 let route = convex::route(model, opts)?;
83 if route == Route::Nlp {
84 return self.solve_nlp(model, opts);
85 }
86 self.solve_convex(model, opts, route)
87 }
88
89 fn solve_nlp(
90 &mut self,
91 model: &Model,
92 opts: &PounceOptions,
93 ) -> Result<SolverResult, SolverError> {
94 self.solve_nlp_since(model, opts, Instant::now())
95 }
96
97 fn solve_nlp_since(
98 &mut self,
99 model: &Model,
100 opts: &PounceOptions,
101 started: Instant,
102 ) -> Result<SolverResult, SolverError> {
103 let prep = setup(model, opts)?;
104 let state = match &mut self.state {
105 Some(State::Nlp(state)) if backend::try_reuse(&state.oracle, model) => state,
106 slot => {
107 *slot = Some(State::Nlp(NlpState { oracle: backend::build(model)?, warm: None }));
108 let Some(State::Nlp(state)) = slot else { unreachable!() };
109 state
110 }
111 };
112 let mut outcome = run_nlp_with_retries(&state.oracle, &prep, opts, state.warm.as_ref())?;
113 let elapsed = started.elapsed();
114 state.warm = outcome.warm.take();
115 Ok(assemble(prep.sign, outcome, elapsed))
116 }
117
118 fn solve_convex(
119 &mut self,
120 model: &Model,
121 opts: &PounceOptions,
122 route: Route,
123 ) -> Result<SolverResult, SolverError> {
124 self.validate_convex_options(opts)?;
125 let problem = convex::build_problem(model)?;
126 let started = Instant::now();
127 let state = match &mut self.state {
128 Some(State::Convex(state))
129 if state.route == route && state.problem.same_structure(&problem) =>
130 {
131 state.problem = problem;
132 state
133 }
134 slot => {
135 let active = (route == Route::QpActiveSet).then(convex::ActivePersistent::new);
136 *slot = Some(State::Convex(Box::new(ConvexState {
137 route,
138 problem,
139 warm: None,
140 active,
141 })));
142 let Some(State::Convex(state)) = slot else { unreachable!() };
143 state
144 }
145 };
146 let solution = if route == Route::QpActiveSet {
147 state
148 .active
149 .as_mut()
150 .expect("active-set state exists for active-set route")
151 .solve(&state.problem, opts)?
152 } else {
153 convex::run(&state.problem, opts, route, state.warm.as_ref())
154 };
155 if convex::should_fallback_to_nlp(model, opts, &solution)? {
156 return self.solve_nlp_since(model, opts, started);
157 }
158 let elapsed = started.elapsed();
159 let mut outcome = convex::outcome(&state.problem, opts, route, &solution);
160 state.warm = (route != Route::QpActiveSet && outcome.termination.admits_primal())
161 .then(|| convex::warm_from_solution(route, &state.problem, &solution));
162 let sign = state.problem.sign();
163 outcome.warm = None;
164 Ok(assemble(sign, outcome, elapsed))
165 }
166
167 fn validate_convex_options(&mut self, opts: &PounceOptions) -> Result<(), SolverError> {
168 if let Some(cached) = &self.validation
169 && cached.options == *opts
170 {
171 return cached.result.clone().map_err(SolverError::Backend);
172 }
173 self.validation = None;
174 let result = convex::validate_options(opts);
175 let cached = match &result {
176 Ok(()) => Ok(()),
177 Err(SolverError::Backend(message)) => Err(message.clone()),
178 Err(_) => return result,
179 };
180 self.validation = Some(CachedValidation { options: opts.clone(), result: cached });
181 result
182 }
183}
184
185impl Solver for PouncePersistent {
186 type Options = PounceOptions;
187
188 fn name(&self) -> &str {
189 "pounce"
190 }
191
192 fn supports(&self, kind: ModelKind) -> bool {
193 matches!(
194 kind,
195 ModelKind::LP | ModelKind::QP | ModelKind::QCP | ModelKind::SOCP | ModelKind::NLP
196 )
197 }
198
199 fn solve(&mut self, model: &Model, opts: &PounceOptions) -> Result<SolverResult, SolverError> {
200 match self.solve_resident(model, opts) {
201 Ok(result) => Ok(result),
202 Err(e) => {
203 self.state = None;
204 Err(e)
205 }
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn convex_validation_cache_tracks_options_and_errors() {
216 let mut solver = PouncePersistent::new();
217 let valid = PounceOptions::default().qp_tau(0.9);
218 solver.validate_convex_options(&valid).unwrap();
219 assert!(
220 solver
221 .validation
222 .as_ref()
223 .is_some_and(|cached| { cached.options == valid && cached.result.is_ok() })
224 );
225 solver.validate_convex_options(&valid).unwrap();
226
227 let invalid = PounceOptions::default().set("not_a_real_option", true);
228 let first = solver.validate_convex_options(&invalid).unwrap_err().to_string();
229 let second = solver.validate_convex_options(&invalid).unwrap_err().to_string();
230 assert_eq!(first, second);
231 assert!(
232 solver
233 .validation
234 .as_ref()
235 .is_some_and(|cached| { cached.options == invalid && cached.result.is_err() })
236 );
237
238 solver.reset();
239 assert!(solver.validation.is_none());
240 }
241}