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 nlp_err <= self.acceptable_tol {
190 self.acceptable_count += 1;
191 if self.acceptable_count >= self.acceptable_iter {
192 return ConvergenceStatus::ConvergedToAcceptable;
193 }
194 } else {
195 self.acceptable_count = 0;
196 }
197 if iter_count >= self.max_iter {
198 return ConvergenceStatus::MaxIterExceeded;
199 }
200 ConvergenceStatus::Continue
201 }
202
203 fn check_convergence_with_state(
204 &mut self,
205 nlp_err: Number,
206 iter_count: Index,
207 data: &IpoptDataHandle,
208 cq: &IpoptCqHandle,
209 ) -> ConvergenceStatus {
210 let cq_ref = cq.borrow();
216 let dual_inf = cq_ref.curr_dual_infeasibility_max();
217 let constr_viol = cq_ref.curr_primal_infeasibility_max();
218 let compl_inf = cq_ref.curr_complementarity_max();
219 let curr_f = cq_ref.curr_f();
220 drop(cq_ref);
221
222 if self.passes_component_tols(nlp_err, dual_inf, constr_viol, compl_inf) {
223 return ConvergenceStatus::Converged;
224 }
225 if self.passes_acceptable_tols(nlp_err, dual_inf, constr_viol, compl_inf, curr_f) {
226 self.acceptable_count += 1;
227 if self.acceptable_count >= self.acceptable_iter {
228 return ConvergenceStatus::ConvergedToAcceptable;
229 }
230 } else {
231 self.acceptable_count = 0;
232 }
233 if iter_count >= self.max_iter {
234 return ConvergenceStatus::MaxIterExceeded;
235 }
236 if self.infeas_stationarity_tol > 0.0 && self.infeas_max_streak > 0 {
245 let stationarity = cq.borrow().curr_infeasibility_stationarity();
246 if self.note_infeasible_stationary(constr_viol, stationarity) {
247 return ConvergenceStatus::LocallyInfeasible;
248 }
249 }
250 let timing = &data.borrow().timing;
257 if timing.overall_alg.live_cpu_time() >= self.max_cpu_time {
258 return ConvergenceStatus::CpuTimeExceeded;
259 }
260 if timing.overall_alg.live_wallclock_time() >= self.max_wall_time {
261 return ConvergenceStatus::WallTimeExceeded;
262 }
263 ConvergenceStatus::Continue
264 }
265
266 fn tol_or_default(&self) -> Number {
267 self.tol
268 }
269
270 fn current_is_acceptable(&self, nlp_err: Number) -> bool {
271 nlp_err.is_finite() && nlp_err <= self.acceptable_tol
277 }
278
279 fn current_is_acceptable_with_state(
280 &self,
281 nlp_err: Number,
282 _data: &IpoptDataHandle,
283 cq: &IpoptCqHandle,
284 ) -> bool {
285 let cq_ref = cq.borrow();
286 let dual_inf = cq_ref.curr_dual_infeasibility_max();
287 let constr_viol = cq_ref.curr_primal_infeasibility_max();
288 let compl_inf = cq_ref.curr_complementarity_max();
289 let curr_f = cq_ref.curr_f();
290 drop(cq_ref);
291 self.passes_acceptable_tols(nlp_err, dual_inf, constr_viol, compl_inf, curr_f)
292 }
293
294 fn set_curr_acceptable_obj(&mut self, obj: Number) {
295 self.last_acceptable_obj = Some(obj);
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn converges_at_tol() {
305 let mut c = OptErrorConvCheck::new();
306 assert_eq!(c.check_convergence(1e-9, 0), ConvergenceStatus::Converged);
307 }
308
309 #[test]
310 fn acceptable_iter_count_threshold() {
311 let mut c = OptErrorConvCheck {
312 acceptable_iter: 3,
313 ..Default::default()
314 };
315 assert_eq!(c.check_convergence(1e-7, 0), ConvergenceStatus::Continue);
317 assert_eq!(c.check_convergence(1e-7, 1), ConvergenceStatus::Continue);
318 assert_eq!(
319 c.check_convergence(1e-7, 2),
320 ConvergenceStatus::ConvergedToAcceptable
321 );
322 }
323
324 #[test]
325 fn streak_resets_when_above_acceptable() {
326 let mut c = OptErrorConvCheck {
327 acceptable_iter: 3,
328 ..Default::default()
329 };
330 assert_eq!(c.check_convergence(1e-7, 0), ConvergenceStatus::Continue);
331 assert_eq!(c.check_convergence(1e-3, 1), ConvergenceStatus::Continue);
333 assert_eq!(c.check_convergence(1e-7, 2), ConvergenceStatus::Continue);
334 assert_eq!(c.check_convergence(1e-7, 3), ConvergenceStatus::Continue);
335 assert_eq!(
336 c.check_convergence(1e-7, 4),
337 ConvergenceStatus::ConvergedToAcceptable
338 );
339 }
340
341 #[test]
342 fn passes_acceptable_tols_gates_on_per_component_triplet() {
343 let c = OptErrorConvCheck {
344 acceptable_tol: 1e-6,
345 acceptable_dual_inf_tol: 1e-3,
346 acceptable_constr_viol_tol: 1e-3,
347 acceptable_compl_inf_tol: 1e-3,
348 ..Default::default()
349 };
350 assert!(c.passes_acceptable_tols(1e-7, 1e-4, 1e-4, 1e-4, 0.0));
351 assert!(!c.passes_acceptable_tols(1e-7, 1.0, 1e-4, 1e-4, 0.0));
353 assert!(!c.passes_acceptable_tols(1e-5, 1e-4, 1e-4, 1e-4, 0.0));
355 }
356
357 #[test]
358 fn passes_acceptable_tols_honors_obj_change_tol() {
359 let mut c = OptErrorConvCheck {
360 acceptable_tol: 1e-6,
361 acceptable_dual_inf_tol: 1.0,
362 acceptable_constr_viol_tol: 1.0,
363 acceptable_compl_inf_tol: 1.0,
364 acceptable_obj_change_tol: 0.1,
365 ..Default::default()
366 };
367 assert!(c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 10.0));
369 c.set_curr_acceptable_obj(10.0);
370 assert!(c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 10.0));
372 assert!(c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 11.0));
375 assert!(!c.passes_acceptable_tols(1e-7, 0.0, 0.0, 0.0, 15.0));
378 }
379
380 use crate::conv_check::r#trait::ConvCheck;
381
382 #[test]
383 fn set_curr_acceptable_obj_records_for_cross_check() {
384 let mut c = OptErrorConvCheck::new();
385 assert!(c.last_acceptable_obj.is_none());
386 ConvCheck::set_curr_acceptable_obj(&mut c, 4.2);
387 assert_eq!(c.last_acceptable_obj, Some(4.2));
388 }
389
390 #[test]
391 fn passes_component_tols_requires_all_under_threshold() {
392 let c = OptErrorConvCheck {
393 tol: 1e-8,
394 dual_inf_tol: 1.0,
395 constr_viol_tol: 1e-4,
396 compl_inf_tol: 1e-4,
397 ..Default::default()
398 };
399 assert!(c.passes_component_tols(1e-9, 0.5, 1e-5, 1e-5));
401 assert!(!c.passes_component_tols(1e-12, 2.0, 1e-5, 1e-5));
403 assert!(!c.passes_component_tols(1e-12, 0.0, 0.0, 1e-2));
405 assert!(!c.passes_component_tols(1e-12, 0.0, 1e-2, 0.0));
407 }
408
409 #[test]
410 fn infeasible_stationary_requires_violation_and_flat_gradient() {
411 let c = OptErrorConvCheck {
412 constr_viol_tol: 1e-4,
413 infeas_viol_kappa: 1e2, infeas_stationarity_tol: 1e-8,
415 infeas_max_streak: 5,
416 ..Default::default()
417 };
418 assert!(c.is_infeasible_stationary(1e-1, 1e-9));
421 assert!(!c.is_infeasible_stationary(1e-1, 1e-3));
424 assert!(!c.is_infeasible_stationary(1e-3, 1e-9));
427 }
428
429 #[test]
430 fn infeasible_stationary_disabled_by_nonpositive_knobs() {
431 let off_tol = OptErrorConvCheck {
432 infeas_stationarity_tol: 0.0,
433 infeas_max_streak: 5,
434 ..Default::default()
435 };
436 assert!(!off_tol.is_infeasible_stationary(1e9, 0.0));
437 let off_streak = OptErrorConvCheck {
438 infeas_stationarity_tol: 1e-8,
439 infeas_max_streak: 0,
440 ..Default::default()
441 };
442 assert!(!off_streak.is_infeasible_stationary(1e9, 0.0));
443 }
444
445 #[test]
446 fn infeasible_stationary_streak_fires_only_after_max_streak() {
447 let mut c = OptErrorConvCheck {
448 constr_viol_tol: 1e-4,
449 infeas_viol_kappa: 1e2, infeas_stationarity_tol: 1e-8,
451 infeas_max_streak: 3,
452 ..Default::default()
453 };
454 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
457 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
458 assert!(c.note_infeasible_stationary(1e-1, 1e-9));
459 }
460
461 #[test]
462 fn infeasible_stationary_streak_resets_on_feasibility_progress() {
463 let mut c = OptErrorConvCheck {
464 constr_viol_tol: 1e-4,
465 infeas_viol_kappa: 1e2,
466 infeas_stationarity_tol: 1e-8,
467 infeas_max_streak: 3,
468 ..Default::default()
469 };
470 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-3));
474 assert_eq!(c.infeas_streak, 0);
475 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
477 assert!(!c.note_infeasible_stationary(1e-1, 1e-9));
478 assert!(c.note_infeasible_stationary(1e-1, 1e-9));
479 }
480
481 #[test]
482 fn infeasible_stationary_streak_never_fires_when_disabled() {
483 let mut c = OptErrorConvCheck {
484 infeas_stationarity_tol: 0.0,
485 infeas_max_streak: 5,
486 ..Default::default()
487 };
488 for _ in 0..20 {
489 assert!(!c.note_infeasible_stationary(1e9, 0.0));
490 }
491 assert_eq!(c.infeas_streak, 0);
492 }
493
494 #[test]
495 fn max_iter_exceeded() {
496 let mut c = OptErrorConvCheck {
497 max_iter: 5,
498 ..Default::default()
499 };
500 assert_eq!(
501 c.check_convergence(1.0, 5),
502 ConvergenceStatus::MaxIterExceeded
503 );
504 }
505}