1use crate::conv_check::r#trait::{ConvCheck, ConvergenceStatus};
15use crate::ipopt_cq::IpoptCqHandle;
16use crate::ipopt_data::IpoptDataHandle;
17use pounce_common::types::{Index, Number};
18
19pub struct OptErrorConvCheck {
20 pub tol: Number,
21 pub dual_inf_tol: Number,
22 pub constr_viol_tol: Number,
23 pub compl_inf_tol: Number,
24 pub acceptable_tol: Number,
25 pub acceptable_dual_inf_tol: Number,
26 pub acceptable_constr_viol_tol: Number,
27 pub acceptable_compl_inf_tol: Number,
28 pub acceptable_obj_change_tol: Number,
29 pub acceptable_iter: Index,
30 pub max_iter: Index,
31 pub max_cpu_time: Number,
32 pub max_wall_time: Number,
33 pub acceptable_count: Index,
34 pub last_acceptable_obj: Option<Number>,
39 pub infeas_stationarity_tol: Number,
45 pub infeas_viol_kappa: Number,
49 pub infeas_max_streak: Index,
53 pub infeas_streak: Index,
55}
56
57impl Default for OptErrorConvCheck {
58 fn default() -> Self {
59 Self {
61 tol: 1e-8,
62 dual_inf_tol: 1.0,
63 constr_viol_tol: 1e-4,
64 compl_inf_tol: 1e-4,
65 acceptable_tol: 1e-6,
66 acceptable_dual_inf_tol: 1e10,
67 acceptable_constr_viol_tol: 1e-2,
68 acceptable_compl_inf_tol: 1e-2,
69 acceptable_obj_change_tol: 1e20,
70 acceptable_iter: 15,
71 max_iter: 3000,
72 max_cpu_time: 1e6,
73 max_wall_time: 1e6,
74 acceptable_count: 0,
75 last_acceptable_obj: None,
76 infeas_stationarity_tol: 1e-8,
77 infeas_viol_kappa: 1e2,
78 infeas_max_streak: 5,
79 infeas_streak: 0,
80 }
81 }
82}
83
84impl OptErrorConvCheck {
85 pub fn new() -> Self {
86 Self::default()
87 }
88
89 fn passes_component_tols(
94 &self,
95 overall: Number,
96 dual_inf: Number,
97 constr_viol: Number,
98 compl_inf: Number,
99 ) -> bool {
100 overall <= self.tol
101 && dual_inf <= self.dual_inf_tol
102 && constr_viol <= self.constr_viol_tol
103 && compl_inf <= self.compl_inf_tol
104 }
105
106 fn passes_acceptable_tols(
111 &self,
112 overall: Number,
113 dual_inf: Number,
114 constr_viol: Number,
115 compl_inf: Number,
116 curr_f: Number,
117 ) -> bool {
118 if !overall.is_finite() {
119 return false;
120 }
121 let component_ok = overall <= self.acceptable_tol
122 && dual_inf <= self.acceptable_dual_inf_tol
123 && constr_viol <= self.acceptable_constr_viol_tol
124 && compl_inf <= self.acceptable_compl_inf_tol;
125 if !component_ok {
126 return false;
127 }
128 if self.acceptable_obj_change_tol < 1e20 {
136 if let Some(prev) = self.last_acceptable_obj {
137 let denom = curr_f.abs().max(1.0);
138 if (prev - curr_f).abs() >= self.acceptable_obj_change_tol * denom {
139 return false;
140 }
141 }
142 }
143 true
144 }
145
146 fn is_infeasible_stationary(&self, constr_viol: Number, stationarity: Number) -> bool {
153 if self.infeas_stationarity_tol <= 0.0 || self.infeas_max_streak <= 0 {
154 return false;
155 }
156 constr_viol > self.infeas_viol_kappa * self.constr_viol_tol
157 && stationarity <= self.infeas_stationarity_tol
158 }
159
160 fn note_infeasible_stationary(&mut self, constr_viol: Number, stationarity: Number) -> bool {
168 if self.is_infeasible_stationary(constr_viol, stationarity) {
169 self.infeas_streak += 1;
170 self.infeas_streak >= self.infeas_max_streak
171 } else {
172 self.infeas_streak = 0;
173 false
174 }
175 }
176}
177
178impl ConvCheck for OptErrorConvCheck {
179 fn check_convergence(&mut self, nlp_err: Number, iter_count: Index) -> ConvergenceStatus {
180 if nlp_err <= self.tol {
181 return ConvergenceStatus::Converged;
182 }
183 if nlp_err <= self.acceptable_tol {
184 self.acceptable_count += 1;
185 if self.acceptable_count >= self.acceptable_iter {
186 return ConvergenceStatus::ConvergedToAcceptable;
187 }
188 } else {
189 self.acceptable_count = 0;
190 }
191 if iter_count >= self.max_iter {
192 return ConvergenceStatus::MaxIterExceeded;
193 }
194 ConvergenceStatus::Continue
195 }
196
197 fn check_convergence_with_state(
198 &mut self,
199 nlp_err: Number,
200 iter_count: Index,
201 data: &IpoptDataHandle,
202 cq: &IpoptCqHandle,
203 ) -> ConvergenceStatus {
204 let cq_ref = cq.borrow();
210 let dual_inf = cq_ref.curr_dual_infeasibility_max();
211 let constr_viol = cq_ref.curr_primal_infeasibility_max();
212 let compl_inf = cq_ref.curr_complementarity_max();
213 let curr_f = cq_ref.curr_f();
214 drop(cq_ref);
215
216 if self.passes_component_tols(nlp_err, dual_inf, constr_viol, compl_inf) {
217 return ConvergenceStatus::Converged;
218 }
219 if self.passes_acceptable_tols(nlp_err, dual_inf, constr_viol, compl_inf, curr_f) {
220 self.acceptable_count += 1;
221 if self.acceptable_count >= self.acceptable_iter {
222 return ConvergenceStatus::ConvergedToAcceptable;
223 }
224 } else {
225 self.acceptable_count = 0;
226 }
227 if iter_count >= self.max_iter {
228 return ConvergenceStatus::MaxIterExceeded;
229 }
230 if self.infeas_stationarity_tol > 0.0 && self.infeas_max_streak > 0 {
239 let stationarity = cq.borrow().curr_infeasibility_stationarity();
240 if self.note_infeasible_stationary(constr_viol, stationarity) {
241 return ConvergenceStatus::LocallyInfeasible;
242 }
243 }
244 let timing = &data.borrow().timing;
251 if timing.overall_alg.live_cpu_time() >= self.max_cpu_time {
252 return ConvergenceStatus::CpuTimeExceeded;
253 }
254 if timing.overall_alg.live_wallclock_time() >= self.max_wall_time {
255 return ConvergenceStatus::WallTimeExceeded;
256 }
257 ConvergenceStatus::Continue
258 }
259
260 fn tol_or_default(&self) -> Number {
261 self.tol
262 }
263
264 fn current_is_acceptable(&self, nlp_err: Number) -> bool {
265 nlp_err.is_finite() && nlp_err <= self.acceptable_tol
271 }
272
273 fn current_is_acceptable_with_state(
274 &self,
275 nlp_err: Number,
276 _data: &IpoptDataHandle,
277 cq: &IpoptCqHandle,
278 ) -> bool {
279 let cq_ref = cq.borrow();
280 let dual_inf = cq_ref.curr_dual_infeasibility_max();
281 let constr_viol = cq_ref.curr_primal_infeasibility_max();
282 let compl_inf = cq_ref.curr_complementarity_max();
283 let curr_f = cq_ref.curr_f();
284 drop(cq_ref);
285 self.passes_acceptable_tols(nlp_err, dual_inf, constr_viol, compl_inf, curr_f)
286 }
287
288 fn set_curr_acceptable_obj(&mut self, obj: Number) {
289 self.last_acceptable_obj = Some(obj);
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn converges_at_tol() {
299 let mut c = OptErrorConvCheck::new();
300 assert_eq!(c.check_convergence(1e-9, 0), ConvergenceStatus::Converged);
301 }
302
303 #[test]
304 fn acceptable_iter_count_threshold() {
305 let mut c = OptErrorConvCheck {
306 acceptable_iter: 3,
307 ..Default::default()
308 };
309 assert_eq!(c.check_convergence(1e-7, 0), ConvergenceStatus::Continue);
311 assert_eq!(c.check_convergence(1e-7, 1), ConvergenceStatus::Continue);
312 assert_eq!(
313 c.check_convergence(1e-7, 2),
314 ConvergenceStatus::ConvergedToAcceptable
315 );
316 }
317
318 #[test]
319 fn streak_resets_when_above_acceptable() {
320 let mut c = OptErrorConvCheck {
321 acceptable_iter: 3,
322 ..Default::default()
323 };
324 assert_eq!(c.check_convergence(1e-7, 0), ConvergenceStatus::Continue);
325 assert_eq!(c.check_convergence(1e-3, 1), ConvergenceStatus::Continue);
327 assert_eq!(c.check_convergence(1e-7, 2), ConvergenceStatus::Continue);
328 assert_eq!(c.check_convergence(1e-7, 3), ConvergenceStatus::Continue);
329 assert_eq!(
330 c.check_convergence(1e-7, 4),
331 ConvergenceStatus::ConvergedToAcceptable
332 );
333 }
334
335 #[test]
336 fn passes_acceptable_tols_gates_on_per_component_triplet() {
337 let c = OptErrorConvCheck {
338 acceptable_tol: 1e-6,
339 acceptable_dual_inf_tol: 1e-3,
340 acceptable_constr_viol_tol: 1e-3,
341 acceptable_compl_inf_tol: 1e-3,
342 ..Default::default()
343 };
344 assert!(c.passes_acceptable_tols(1e-7, 1e-4, 1e-4, 1e-4, 0.0));
345 assert!(!c.passes_acceptable_tols(1e-7, 1.0, 1e-4, 1e-4, 0.0));
347 assert!(!c.passes_acceptable_tols(1e-5, 1e-4, 1e-4, 1e-4, 0.0));
349 }
350
351 #[test]
352 fn passes_acceptable_tols_honors_obj_change_tol() {
353 let mut c = OptErrorConvCheck {
354 acceptable_tol: 1e-6,
355 acceptable_dual_inf_tol: 1.0,
356 acceptable_constr_viol_tol: 1.0,
357 acceptable_compl_inf_tol: 1.0,
358 acceptable_obj_change_tol: 0.1,
359 ..Default::default()
360 };
361 assert!(c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 10.0));
363 c.set_curr_acceptable_obj(10.0);
364 assert!(c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 10.0));
366 assert!(c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 11.0));
369 assert!(!c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 15.0));
372 }
373
374 use crate::conv_check::r#trait::ConvCheck;
375
376 #[test]
377 fn set_curr_acceptable_obj_records_for_cross_check() {
378 let mut c = OptErrorConvCheck::new();
379 assert!(c.last_acceptable_obj.is_none());
380 ConvCheck::set_curr_acceptable_obj(&mut c, 4.2);
381 assert_eq!(c.last_acceptable_obj, Some(4.2));
382 }
383
384 #[test]
385 fn passes_component_tols_requires_all_under_threshold() {
386 let c = OptErrorConvCheck {
387 tol: 1e-8,
388 dual_inf_tol: 1.0,
389 constr_viol_tol: 1e-4,
390 compl_inf_tol: 1e-4,
391 ..Default::default()
392 };
393 assert!(c.passes_component_tols(1e-9, 0.5, 1e-5, 1e-5));
395 assert!(!c.passes_component_tols(1e-12, 2.0, 1e-5, 1e-5));
397 assert!(!c.passes_component_tols(1e-12, 0.0, 0.0, 1e-2));
399 assert!(!c.passes_component_tols(1e-12, 0.0, 1e-2, 0.0));
401 }
402
403 #[test]
404 fn infeasible_stationary_requires_violation_and_flat_gradient() {
405 let c = OptErrorConvCheck {
406 constr_viol_tol: 1e-4,
407 infeas_viol_kappa: 1e2, infeas_stationarity_tol: 1e-8,
409 infeas_max_streak: 5,
410 ..Default::default()
411 };
412 assert!(c.is_infeasible_stationary(1e-1, 1e-9));
415 assert!(!c.is_infeasible_stationary(1e-1, 1e-3));
418 assert!(!c.is_infeasible_stationary(1e-3, 1e-9));
421 }
422
423 #[test]
424 fn infeasible_stationary_disabled_by_nonpositive_knobs() {
425 let off_tol = OptErrorConvCheck {
426 infeas_stationarity_tol: 0.0,
427 infeas_max_streak: 5,
428 ..Default::default()
429 };
430 assert!(!off_tol.is_infeasible_stationary(1e9, 0.0));
431 let off_streak = OptErrorConvCheck {
432 infeas_stationarity_tol: 1e-8,
433 infeas_max_streak: 0,
434 ..Default::default()
435 };
436 assert!(!off_streak.is_infeasible_stationary(1e9, 0.0));
437 }
438
439 #[test]
440 fn infeasible_stationary_streak_fires_only_after_max_streak() {
441 let mut c = OptErrorConvCheck {
442 constr_viol_tol: 1e-4,
443 infeas_viol_kappa: 1e2, infeas_stationarity_tol: 1e-8,
445 infeas_max_streak: 3,
446 ..Default::default()
447 };
448 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
451 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
452 assert!(c.note_infeasible_stationary(1e-1, 1e-9));
453 }
454
455 #[test]
456 fn infeasible_stationary_streak_resets_on_feasibility_progress() {
457 let mut c = OptErrorConvCheck {
458 constr_viol_tol: 1e-4,
459 infeas_viol_kappa: 1e2,
460 infeas_stationarity_tol: 1e-8,
461 infeas_max_streak: 3,
462 ..Default::default()
463 };
464 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
465 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
466 assert!(!c.note_infeasible_stationary(1e-1, 1e-3));
468 assert_eq!(c.infeas_streak, 0);
469 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
471 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
472 assert!(c.note_infeasible_stationary(1e-1, 1e-9));
473 }
474
475 #[test]
476 fn infeasible_stationary_streak_never_fires_when_disabled() {
477 let mut c = OptErrorConvCheck {
478 infeas_stationarity_tol: 0.0,
479 infeas_max_streak: 5,
480 ..Default::default()
481 };
482 for _ in 0..20 {
483 assert!(!c.note_infeasible_stationary(1e9, 0.0));
484 }
485 assert_eq!(c.infeas_streak, 0);
486 }
487
488 #[test]
489 fn max_iter_exceeded() {
490 let mut c = OptErrorConvCheck {
491 max_iter: 5,
492 ..Default::default()
493 };
494 assert_eq!(
495 c.check_convergence(1.0, 5),
496 ConvergenceStatus::MaxIterExceeded
497 );
498 }
499}