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() || !curr_f.is_finite() {
125 return false;
126 }
127 let component_ok = overall <= self.acceptable_tol
128 && dual_inf <= self.acceptable_dual_inf_tol
129 && constr_viol <= self.acceptable_constr_viol_tol
130 && compl_inf <= self.acceptable_compl_inf_tol;
131 if !component_ok {
132 return false;
133 }
134 if self.acceptable_obj_change_tol < 1e20 {
142 if let Some(prev) = self.last_acceptable_obj {
143 let denom = curr_f.abs().max(1.0);
144 if (prev - curr_f).abs() >= self.acceptable_obj_change_tol * denom {
145 return false;
146 }
147 }
148 }
149 true
150 }
151
152 fn is_infeasible_stationary(&self, constr_viol: Number, stationarity: Number) -> bool {
159 if self.infeas_stationarity_tol <= 0.0 || self.infeas_max_streak <= 0 {
160 return false;
161 }
162 constr_viol > self.infeas_viol_kappa * self.constr_viol_tol
163 && stationarity <= self.infeas_stationarity_tol
164 }
165
166 fn note_infeasible_stationary(&mut self, constr_viol: Number, stationarity: Number) -> bool {
174 if self.is_infeasible_stationary(constr_viol, stationarity) {
175 self.infeas_streak += 1;
176 self.infeas_streak >= self.infeas_max_streak
177 } else {
178 self.infeas_streak = 0;
179 false
180 }
181 }
182}
183
184impl ConvCheck for OptErrorConvCheck {
185 fn check_convergence(&mut self, nlp_err: Number, iter_count: Index) -> ConvergenceStatus {
186 if nlp_err <= self.tol {
187 return ConvergenceStatus::Converged;
188 }
189 if self.acceptable_iter > 0 && nlp_err <= self.acceptable_tol {
195 self.acceptable_count += 1;
196 if self.acceptable_count >= self.acceptable_iter {
197 return ConvergenceStatus::ConvergedToAcceptable;
198 }
199 } else {
200 self.acceptable_count = 0;
201 }
202 if iter_count >= self.max_iter {
203 return ConvergenceStatus::MaxIterExceeded;
204 }
205 ConvergenceStatus::Continue
206 }
207
208 fn check_convergence_with_state(
209 &mut self,
210 nlp_err: Number,
211 iter_count: Index,
212 data: &IpoptDataHandle,
213 cq: &IpoptCqHandle,
214 ) -> ConvergenceStatus {
215 let cq_ref = cq.borrow();
229 let dual_inf = cq_ref.curr_unscaled_dual_infeasibility_max();
230 let constr_viol = cq_ref.curr_unscaled_primal_infeasibility_max();
231 let compl_inf = cq_ref.curr_unscaled_complementarity_max();
232 let curr_f = cq_ref.curr_f();
233 drop(cq_ref);
234
235 if self.passes_component_tols(nlp_err, dual_inf, constr_viol, compl_inf) {
236 return ConvergenceStatus::Converged;
237 }
238 if self.acceptable_iter > 0
241 && self.passes_acceptable_tols(nlp_err, dual_inf, constr_viol, compl_inf, curr_f)
242 {
243 self.acceptable_count += 1;
244 if self.acceptable_count >= self.acceptable_iter {
245 return ConvergenceStatus::ConvergedToAcceptable;
246 }
247 } else {
248 self.acceptable_count = 0;
249 }
250 if iter_count >= self.max_iter {
251 return ConvergenceStatus::MaxIterExceeded;
252 }
253 if self.infeas_stationarity_tol > 0.0 && self.infeas_max_streak > 0 {
262 let stationarity = cq.borrow().curr_infeasibility_stationarity();
263 if self.note_infeasible_stationary(constr_viol, stationarity) {
264 return ConvergenceStatus::LocallyInfeasible;
265 }
266 }
267 let timing = &data.borrow().timing;
274 if timing.overall_alg.live_cpu_time() >= self.max_cpu_time {
275 return ConvergenceStatus::CpuTimeExceeded;
276 }
277 if timing.overall_alg.live_wallclock_time() >= self.max_wall_time {
278 return ConvergenceStatus::WallTimeExceeded;
279 }
280 ConvergenceStatus::Continue
281 }
282
283 fn tol_or_default(&self) -> Number {
284 self.tol
285 }
286
287 fn set_tolerance(&mut self, name: &str, value: Number) -> bool {
288 match name {
289 "tol" => self.tol = value,
290 "dual_inf_tol" => self.dual_inf_tol = value,
291 "constr_viol_tol" => self.constr_viol_tol = value,
292 "compl_inf_tol" => self.compl_inf_tol = value,
293 "acceptable_tol" => self.acceptable_tol = value,
294 "acceptable_dual_inf_tol" => self.acceptable_dual_inf_tol = value,
295 "acceptable_constr_viol_tol" => self.acceptable_constr_viol_tol = value,
296 "acceptable_compl_inf_tol" => self.acceptable_compl_inf_tol = value,
297 "acceptable_obj_change_tol" => self.acceptable_obj_change_tol = value,
298 _ => return false,
299 }
300 true
301 }
302
303 fn current_is_acceptable(&self, nlp_err: Number) -> bool {
304 nlp_err.is_finite() && nlp_err <= self.acceptable_tol
310 }
311
312 fn current_is_acceptable_with_state(
313 &self,
314 nlp_err: Number,
315 _data: &IpoptDataHandle,
316 cq: &IpoptCqHandle,
317 ) -> bool {
318 let cq_ref = cq.borrow();
319 let dual_inf = cq_ref.curr_unscaled_dual_infeasibility_max();
323 let constr_viol = cq_ref.curr_unscaled_primal_infeasibility_max();
324 let compl_inf = cq_ref.curr_unscaled_complementarity_max();
325 let curr_f = cq_ref.curr_f();
326 drop(cq_ref);
327 self.passes_acceptable_tols(nlp_err, dual_inf, constr_viol, compl_inf, curr_f)
328 }
329
330 fn set_curr_acceptable_obj(&mut self, obj: Number) {
331 self.last_acceptable_obj = Some(obj);
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn converges_at_tol() {
341 let mut c = OptErrorConvCheck::new();
342 assert_eq!(c.check_convergence(1e-9, 0), ConvergenceStatus::Converged);
343 }
344
345 #[test]
346 fn acceptable_iter_count_threshold() {
347 let mut c = OptErrorConvCheck {
348 acceptable_iter: 3,
349 ..Default::default()
350 };
351 assert_eq!(c.check_convergence(1e-7, 0), ConvergenceStatus::Continue);
353 assert_eq!(c.check_convergence(1e-7, 1), ConvergenceStatus::Continue);
354 assert_eq!(
355 c.check_convergence(1e-7, 2),
356 ConvergenceStatus::ConvergedToAcceptable
357 );
358 }
359
360 #[test]
361 fn acceptable_iter_zero_disables_acceptable_termination() {
362 let mut c = OptErrorConvCheck {
367 acceptable_iter: 0,
368 ..Default::default()
369 };
370 for k in 0..50 {
374 assert_eq!(
375 c.check_convergence(1e-7, k),
376 ConvergenceStatus::Continue,
377 "acceptable_iter=0 must not stop at the acceptable level (iter {k})"
378 );
379 }
380 assert_eq!(c.check_convergence(1e-9, 51), ConvergenceStatus::Converged);
382 }
383
384 #[test]
385 fn streak_resets_when_above_acceptable() {
386 let mut c = OptErrorConvCheck {
387 acceptable_iter: 3,
388 ..Default::default()
389 };
390 assert_eq!(c.check_convergence(1e-7, 0), ConvergenceStatus::Continue);
391 assert_eq!(c.check_convergence(1e-3, 1), ConvergenceStatus::Continue);
393 assert_eq!(c.check_convergence(1e-7, 2), ConvergenceStatus::Continue);
394 assert_eq!(c.check_convergence(1e-7, 3), ConvergenceStatus::Continue);
395 assert_eq!(
396 c.check_convergence(1e-7, 4),
397 ConvergenceStatus::ConvergedToAcceptable
398 );
399 }
400
401 #[test]
402 fn passes_acceptable_tols_gates_on_per_component_triplet() {
403 let c = OptErrorConvCheck {
404 acceptable_tol: 1e-6,
405 acceptable_dual_inf_tol: 1e-3,
406 acceptable_constr_viol_tol: 1e-3,
407 acceptable_compl_inf_tol: 1e-3,
408 ..Default::default()
409 };
410 assert!(c.passes_acceptable_tols(1e-7, 1e-4, 1e-4, 1e-4, 0.0));
411 assert!(!c.passes_acceptable_tols(1e-7, 1.0, 1e-4, 1e-4, 0.0));
413 assert!(!c.passes_acceptable_tols(1e-5, 1e-4, 1e-4, 1e-4, 0.0));
415 }
416
417 #[test]
418 fn passes_acceptable_tols_honors_obj_change_tol() {
419 let mut c = OptErrorConvCheck {
420 acceptable_tol: 1e-6,
421 acceptable_dual_inf_tol: 1.0,
422 acceptable_constr_viol_tol: 1.0,
423 acceptable_compl_inf_tol: 1.0,
424 acceptable_obj_change_tol: 0.1,
425 ..Default::default()
426 };
427 assert!(c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 10.0));
429 c.set_curr_acceptable_obj(10.0);
430 assert!(c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 10.0));
432 assert!(c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 11.0));
435 assert!(!c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 15.0));
438 }
439
440 use crate::conv_check::r#trait::ConvCheck;
441
442 #[test]
443 fn set_curr_acceptable_obj_records_for_cross_check() {
444 let mut c = OptErrorConvCheck::new();
445 assert!(c.last_acceptable_obj.is_none());
446 ConvCheck::set_curr_acceptable_obj(&mut c, 4.2);
447 assert_eq!(c.last_acceptable_obj, Some(4.2));
448 }
449
450 #[test]
451 fn passes_component_tols_requires_all_under_threshold() {
452 let c = OptErrorConvCheck {
453 tol: 1e-8,
454 dual_inf_tol: 1.0,
455 constr_viol_tol: 1e-4,
456 compl_inf_tol: 1e-4,
457 ..Default::default()
458 };
459 assert!(c.passes_component_tols(1e-9, 0.5, 1e-5, 1e-5));
461 assert!(!c.passes_component_tols(1e-12, 2.0, 1e-5, 1e-5));
463 assert!(!c.passes_component_tols(1e-12, 0.0, 0.0, 1e-2));
465 assert!(!c.passes_component_tols(1e-12, 0.0, 1e-2, 0.0));
467 }
468
469 #[test]
470 fn infeasible_stationary_requires_violation_and_flat_gradient() {
471 let c = OptErrorConvCheck {
472 constr_viol_tol: 1e-4,
473 infeas_viol_kappa: 1e2, infeas_stationarity_tol: 1e-8,
475 infeas_max_streak: 5,
476 ..Default::default()
477 };
478 assert!(c.is_infeasible_stationary(1e-1, 1e-9));
481 assert!(!c.is_infeasible_stationary(1e-1, 1e-3));
484 assert!(!c.is_infeasible_stationary(1e-3, 1e-9));
487 }
488
489 #[test]
490 fn infeasible_stationary_disabled_by_nonpositive_knobs() {
491 let off_tol = OptErrorConvCheck {
492 infeas_stationarity_tol: 0.0,
493 infeas_max_streak: 5,
494 ..Default::default()
495 };
496 assert!(!off_tol.is_infeasible_stationary(1e9, 0.0));
497 let off_streak = OptErrorConvCheck {
498 infeas_stationarity_tol: 1e-8,
499 infeas_max_streak: 0,
500 ..Default::default()
501 };
502 assert!(!off_streak.is_infeasible_stationary(1e9, 0.0));
503 }
504
505 #[test]
506 fn infeasible_stationary_streak_fires_only_after_max_streak() {
507 let mut c = OptErrorConvCheck {
508 constr_viol_tol: 1e-4,
509 infeas_viol_kappa: 1e2, infeas_stationarity_tol: 1e-8,
511 infeas_max_streak: 3,
512 ..Default::default()
513 };
514 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
517 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
518 assert!(c.note_infeasible_stationary(1e-1, 1e-9));
519 }
520
521 #[test]
522 fn infeasible_stationary_streak_resets_on_feasibility_progress() {
523 let mut c = OptErrorConvCheck {
524 constr_viol_tol: 1e-4,
525 infeas_viol_kappa: 1e2,
526 infeas_stationarity_tol: 1e-8,
527 infeas_max_streak: 3,
528 ..Default::default()
529 };
530 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
531 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
532 assert!(!c.note_infeasible_stationary(1e-1, 1e-3));
534 assert_eq!(c.infeas_streak, 0);
535 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
537 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
538 assert!(c.note_infeasible_stationary(1e-1, 1e-9));
539 }
540
541 #[test]
542 fn infeasible_stationary_streak_never_fires_when_disabled() {
543 let mut c = OptErrorConvCheck {
544 infeas_stationarity_tol: 0.0,
545 infeas_max_streak: 5,
546 ..Default::default()
547 };
548 for _ in 0..20 {
549 assert!(!c.note_infeasible_stationary(1e9, 0.0));
550 }
551 assert_eq!(c.infeas_streak, 0);
552 }
553
554 #[test]
555 fn max_iter_exceeded() {
556 let mut c = OptErrorConvCheck {
557 max_iter: 5,
558 ..Default::default()
559 };
560 assert_eq!(
561 c.check_convergence(1.0, 5),
562 ConvergenceStatus::MaxIterExceeded
563 );
564 }
565}