1use crate::alg_builder::{
21 AlgorithmBuilder, HessianApproxChoice, LineSearchChoice, LinearBackendFactory,
22 LinearSolverChoice, MuStrategyChoice,
23};
24use crate::ipopt_alg::IpoptAlgorithm;
25use crate::ipopt_cq::IpoptCalculatedQuantities;
26use crate::ipopt_data::IpoptData as AlgIpoptData;
27use crate::ipopt_nlp::IpoptNlp;
28use crate::iterates_vector::IteratesVector;
29use crate::restoration::RestorationPhase;
30use crate::upstream_options::register_all_upstream_options;
31
32pub type RestorationFactory = Box<dyn FnMut() -> Box<dyn RestorationPhase>>;
38
39pub type RestorationFactoryProvider = Box<dyn FnMut() -> RestorationFactory>;
47
48pub type ConvergedCallback = Box<
67 dyn FnMut(
68 &crate::ipopt_data::IpoptDataHandle,
69 &crate::ipopt_cq::IpoptCqHandle,
70 &Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
71 Rc<RefCell<crate::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
72 ),
73>;
74use pounce_common::diagnostics::DiagnosticsState;
75use pounce_common::exception::{ExceptionKind, SolverException};
76use pounce_common::journalist::{JournalLevel, Journalist};
77use pounce_common::options_list::OptionsList;
78use pounce_common::reg_options::{PrintOptionsMode, RegisteredOptions};
79use pounce_common::timing::TimingStatistics;
80use pounce_common::types::{Index, Number};
81use pounce_linalg::dense_vector::DenseVectorSpace;
82use pounce_linsol::summary::LinearSolverSummary;
83use pounce_linsol::SparseSymLinearSolverInterface;
84use pounce_nlp::alg_types::SolverReturn;
85use pounce_nlp::orig_ipopt_nlp::{NoScaling, OrigIpoptNlp, ScalingMethod};
86use pounce_nlp::return_codes::ApplicationReturnStatus;
87use pounce_nlp::solve_statistics::SolveStatistics;
88use pounce_nlp::tnlp::{
89 IpoptCq as TnlpIpoptCq, IpoptData as TnlpIpoptData, NlpInfo, Solution, TNLP,
90};
91use pounce_nlp::tnlp_adapter::{
92 FixedVarTreatment, TNLPAdapter, DEFAULT_NLP_LOWER_BOUND_INF, DEFAULT_NLP_UPPER_BOUND_INF,
93};
94use std::cell::RefCell;
95use std::fmt;
96use std::path::Path;
97use std::rc::Rc;
98use std::sync::{Arc, Mutex};
99use std::time::Instant;
100
101pub struct IpoptApplication {
102 options: OptionsList,
103 reg_options: Rc<RegisteredOptions>,
104 journalist: Rc<Journalist>,
105 statistics: RefCell<SolveStatistics>,
106 timing: RefCell<Rc<TimingStatistics>>,
112 linear_backend_factory: Option<LinearBackendFactory>,
116 restoration_factory: Option<RestorationFactory>,
125 diagnostics: Option<Rc<DiagnosticsState>>,
131 restoration_factory_provider: Option<RestorationFactoryProvider>,
137 on_converged: Option<ConvergedCallback>,
141 record_iter_history: bool,
147 linsol_summary_sink: Arc<Mutex<LinearSolverSummary>>,
157 sqp_warm_start: Option<crate::sqp::SqpIterates>,
163 sqp_last_working_set: Option<pounce_qp::WorkingSet>,
168}
169
170impl fmt::Debug for IpoptApplication {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 f.debug_struct("IpoptApplication")
173 .field("options", &self.options)
174 .field("statistics", &self.statistics)
175 .finish_non_exhaustive()
176 }
177}
178
179impl Default for IpoptApplication {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185impl IpoptApplication {
186 pub fn new() -> Self {
189 let reg = RegisteredOptions::default();
190 register_all_upstream_options(®)
193 .unwrap_or_else(|e| panic!("Upstream options registration failed: {e}"));
194 pounce_presolve::register_options(®)
195 .unwrap_or_else(|e| panic!("Presolve options registration failed: {e}"));
196 let reg = Rc::new(reg);
197 Self {
198 options: OptionsList::with_registered(Rc::clone(®)),
199 reg_options: reg,
200 journalist: Rc::new(Journalist::new()),
201 statistics: RefCell::new(SolveStatistics::new()),
202 timing: RefCell::new(Rc::new(TimingStatistics::new())),
203 linear_backend_factory: None,
204 restoration_factory: None,
205 diagnostics: None,
206 restoration_factory_provider: None,
207 on_converged: None,
208 record_iter_history: false,
209 linsol_summary_sink: Arc::new(Mutex::new(LinearSolverSummary::default())),
210 sqp_warm_start: None,
211 sqp_last_working_set: None,
212 }
213 }
214
215 pub fn options(&self) -> &OptionsList {
216 &self.options
217 }
218
219 pub fn options_mut(&mut self) -> &mut OptionsList {
220 &mut self.options
221 }
222
223 pub fn registered_options(&self) -> &Rc<RegisteredOptions> {
224 &self.reg_options
225 }
226
227 pub fn journalist(&self) -> &Rc<Journalist> {
228 &self.journalist
229 }
230
231 pub fn set_linear_backend_factory(&mut self, factory: LinearBackendFactory) {
236 self.linear_backend_factory = Some(factory);
237 }
238
239 pub fn set_restoration_factory(&mut self, factory: RestorationFactory) {
248 self.restoration_factory = Some(factory);
249 }
250
251 pub fn set_diagnostics(&mut self, diag: Rc<DiagnosticsState>) {
256 self.diagnostics = Some(diag);
257 }
258
259 pub fn diagnostics(&self) -> Option<Rc<DiagnosticsState>> {
263 self.diagnostics.as_ref().map(Rc::clone)
264 }
265
266 pub fn set_restoration_factory_provider(&mut self, provider: RestorationFactoryProvider) {
275 self.restoration_factory_provider = Some(provider);
276 }
277
278 pub fn set_on_converged(&mut self, cb: ConvergedCallback) {
284 self.on_converged = Some(cb);
285 }
286
287 pub fn enable_iter_history(&mut self) {
294 self.record_iter_history = true;
295 }
296
297 pub fn initialize_with_options_file(&mut self, path: &Path) -> Result<(), SolverException> {
300 let txt = std::fs::read_to_string(path).map_err(|e| {
301 SolverException::new(
302 ExceptionKind::IPOPT_APPLICATION_ERROR,
303 format!("could not read options file {}: {}", path.display(), e),
304 file!(),
305 line!() as Index,
306 )
307 })?;
308 self.options.read_from_str(&txt, true)?;
309 self.open_output_file_journal();
310 Ok(())
311 }
312
313 pub fn initialize_with_options_str(&mut self, s: &str) -> Result<(), SolverException> {
316 self.options.read_from_str(s, true)?;
317 self.open_output_file_journal();
318 Ok(())
319 }
320
321 fn open_output_file_journal(&self) {
333 let fname = match self.options.get_string_value("output_file", "") {
334 Ok((v, true)) if !v.is_empty() => v,
335 _ => return,
336 };
337 let level_int = self
338 .options
339 .get_integer_value("file_print_level", "")
340 .ok()
341 .and_then(|(v, f)| f.then_some(v))
342 .unwrap_or(5);
343 let level = journal_level_from_int(level_int);
344 let append = self
345 .options
346 .get_bool_value("file_append", "")
347 .ok()
348 .and_then(|(v, f)| f.then_some(v))
349 .unwrap_or(false);
350 let jname = format!("OutputFile:{}", fname);
351 let _ = self
352 .journalist
353 .add_file_journal(&jname, &fname, level, append);
354 }
355
356 pub fn initialize(&mut self) -> Result<(), SolverException> {
360 Ok(())
361 }
362
363 pub fn open_output_file(&mut self, fname: &str, print_level: i32) -> bool {
369 if self
370 .options
371 .set_string_value("output_file", fname, true, false)
372 .is_err()
373 {
374 return false;
375 }
376 if self
377 .options
378 .set_integer_value("file_print_level", print_level as Index, true, false)
379 .is_err()
380 {
381 return false;
382 }
383 let level = journal_level_from_int(print_level);
384 let jname = format!("OutputFile:{}", fname);
385 self.journalist
390 .add_file_journal(&jname, fname, level, false)
391 .is_some()
392 }
393
394 pub fn problem_dimensions(&self, tnlp: &mut dyn TNLP) -> Option<NlpInfo> {
397 tnlp.get_nlp_info()
398 }
399
400 pub fn statistics(&self) -> SolveStatistics {
401 self.statistics.borrow().clone()
402 }
403
404 pub fn timing_stats(&self) -> Rc<TimingStatistics> {
411 Rc::clone(&self.timing.borrow())
412 }
413
414 pub fn linear_solver_summary(&self) -> Option<LinearSolverSummary> {
421 let guard = self.linsol_summary_sink.lock().ok()?;
422 if guard.is_empty() {
423 None
424 } else {
425 Some(guard.clone())
426 }
427 }
428
429 pub fn optimize_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
442 if self.is_sqp_algorithm_selected() {
447 return self.optimize_sqp_tnlp(tnlp);
448 }
449 let info = match tnlp.borrow_mut().get_nlp_info() {
450 Some(info) => info,
451 None => return ApplicationReturnStatus::InvalidProblemDefinition,
452 };
453 if info.m > 0 && self.is_l1_penalty_enabled() {
463 if let Some(status) = self.run_l1_penalty_outer_loop(Rc::clone(&tnlp)) {
464 return status;
465 }
466 }
470 if info.m > 0 && self.is_l1_fallback_enabled() && !self.is_l1_penalty_enabled() {
477 return self.run_with_l1_fallback(tnlp);
478 }
479 self.optimize_constrained(tnlp)
486 }
487
488 fn is_l1_penalty_enabled(&self) -> bool {
491 self.options
492 .get_bool_value("l1_exact_penalty_barrier", "")
493 .ok()
494 .and_then(|(v, found)| found.then_some(v))
495 .unwrap_or(false)
496 }
497
498 fn l1_penalty_init(&self) -> Number {
499 self.options
500 .get_numeric_value("l1_penalty_init", "")
501 .ok()
502 .and_then(|(v, found)| found.then_some(v))
503 .unwrap_or(1.0)
504 }
505 fn l1_penalty_max(&self) -> Number {
506 self.options
507 .get_numeric_value("l1_penalty_max", "")
508 .ok()
509 .and_then(|(v, found)| found.then_some(v))
510 .unwrap_or(1.0e6)
511 }
512 fn l1_penalty_increase_factor(&self) -> Number {
513 self.options
514 .get_numeric_value("l1_penalty_increase_factor", "")
515 .ok()
516 .and_then(|(v, found)| found.then_some(v))
517 .unwrap_or(8.0)
518 }
519 fn l1_penalty_max_outer_iter(&self) -> usize {
520 self.options
521 .get_integer_value("l1_penalty_max_outer_iter", "")
522 .ok()
523 .and_then(|(v, found)| found.then_some(v))
524 .unwrap_or(8) as usize
525 }
526 fn l1_slack_tol(&self) -> Number {
527 self.options
528 .get_numeric_value("l1_slack_tol", "")
529 .ok()
530 .and_then(|(v, found)| found.then_some(v))
531 .unwrap_or(1.0e-6)
532 }
533 fn l1_steering_factor(&self) -> Number {
534 self.options
535 .get_numeric_value("l1_steering_factor", "")
536 .ok()
537 .and_then(|(v, found)| found.then_some(v))
538 .unwrap_or(10.0)
539 }
540 fn is_l1_fallback_enabled(&self) -> bool {
541 self.options
542 .get_bool_value("l1_fallback_on_restoration_failure", "")
543 .ok()
544 .and_then(|(v, found)| found.then_some(v))
545 .unwrap_or(false)
546 }
547
548 pub fn set_sqp_warm_start(&mut self, warm: crate::sqp::SqpIterates) {
562 self.sqp_warm_start = Some(warm);
563 }
564
565 pub fn clear_sqp_warm_start(&mut self) {
567 self.sqp_warm_start = None;
568 }
569
570 pub fn last_sqp_working_set(&self) -> Option<&pounce_qp::WorkingSet> {
575 self.sqp_last_working_set.as_ref()
576 }
577
578 fn is_sqp_algorithm_selected(&self) -> bool {
579 match self.options.get_string_value("algorithm", "") {
580 Ok((v, true)) => v.eq_ignore_ascii_case("active-set-sqp"),
581 _ => false,
582 }
583 }
584
585 fn optimize_sqp_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
592 use pounce_nlp::orig_ipopt_nlp::OrigIpoptNlp;
593 use pounce_nlp::tnlp_adapter::TNLPAdapter;
594 use pounce_nlp::NoScaling;
595
596 let adapter = match TNLPAdapter::new(Rc::clone(&tnlp)) {
597 Ok(a) => Rc::new(RefCell::new(a)),
598 Err(_) => return ApplicationReturnStatus::InvalidProblemDefinition,
599 };
600 let orig_nlp = match OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)) {
601 Ok(n) => n,
602 Err(_) => return ApplicationReturnStatus::InternalError,
603 };
604 let nlp_rc: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(orig_nlp));
605
606 let mut sqp_adapter = crate::sqp::IpoptNlpAdapter::new(Rc::clone(&nlp_rc));
607
608 let mut builder = self.algorithm_builder_snapshot();
609 builder.algorithm = crate::alg_builder::AlgorithmChoice::ActiveSetSqp;
610 let factory = self.make_backend_factory();
611 let mut alg = match builder.build_sqp_with_backend(factory) {
612 Some(a) => a,
613 None => return ApplicationReturnStatus::InternalError,
614 };
615
616 let warm = self.sqp_warm_start.take();
620 let res = match alg.optimize_with_warm_start(&mut sqp_adapter, warm) {
621 Ok(r) => r,
622 Err(e) => {
623 if std::env::var_os("POUNCE_DBG_SQP").is_some() {
624 tracing::warn!(target: "pounce::sqp", "[SQP] optimize_with_warm_start error: {e:?}");
625 }
626 return ApplicationReturnStatus::InternalError;
627 }
628 };
629 self.sqp_last_working_set = res.working_set.clone();
632 {
641 let mut stats = self.statistics.borrow_mut();
642 stats.iteration_count = res.n_iter as Index;
643 stats.final_objective = res.obj;
644 stats.final_dual_inf = res.final_stationarity;
645 stats.final_constr_viol = res.final_constr_viol;
646 stats.final_compl = 0.0; }
648 let (app_status, solver_status) = match res.status {
649 crate::sqp::SqpStatus::Optimal => (
650 ApplicationReturnStatus::SolveSucceeded,
651 pounce_nlp::SolverReturn::Success,
652 ),
653 crate::sqp::SqpStatus::MaxIter => (
654 ApplicationReturnStatus::MaximumIterationsExceeded,
655 pounce_nlp::SolverReturn::MaxiterExceeded,
656 ),
657 crate::sqp::SqpStatus::InfeasibleSubproblem => (
658 ApplicationReturnStatus::InfeasibleProblemDetected,
659 pounce_nlp::SolverReturn::LocalInfeasibility,
660 ),
661 crate::sqp::SqpStatus::LineSearchFailed => (
662 ApplicationReturnStatus::SearchDirectionBecomesTooSmall,
663 pounce_nlp::SolverReturn::ErrorInStepComputation,
664 ),
665 };
666
667 let _ = finalize_via_sqp(&nlp_rc, &res, solver_status, &tnlp);
673
674 app_status
675 }
676
677 fn algorithm_builder_snapshot(&self) -> AlgorithmBuilder {
681 let mut builder = AlgorithmBuilder::default();
682 apply_sqp_options(&self.options, &mut builder.sqp);
683 builder
684 }
685
686 fn make_backend_factory(&self) -> LinearBackendFactory {
690 Box::new(
691 |_choice| -> Box<dyn pounce_linsol::SparseSymLinearSolverInterface> {
692 Box::new(pounce_feral::FeralSolverInterface::new())
693 },
694 )
695 }
696
697 fn run_with_l1_fallback(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
714 let first_status = self.optimize_constrained(Rc::clone(&tnlp));
717 if !is_l1_fallback_trigger(first_status) {
718 return first_status;
719 }
720 let prev = self
724 .options
725 .get_string_value("l1_exact_penalty_barrier", "")
726 .ok();
727 let _ = self
728 .options
729 .set_string_value("l1_exact_penalty_barrier", "yes", true, false);
730 let retry_status = self
731 .run_l1_penalty_outer_loop(Rc::clone(&tnlp))
732 .unwrap_or(ApplicationReturnStatus::InternalError);
733 let _ = self.options.set_string_value(
734 "l1_exact_penalty_barrier",
735 prev.as_ref().map(|(v, _)| v.as_str()).unwrap_or("no"),
736 true,
737 false,
738 );
739 if matches!(retry_status, ApplicationReturnStatus::SolveSucceeded) {
740 retry_status
741 } else {
742 first_status
743 }
744 }
745
746 fn run_l1_penalty_outer_loop(
767 &mut self,
768 tnlp: Rc<RefCell<dyn TNLP>>,
769 ) -> Option<ApplicationReturnStatus> {
770 let rho_init = self.l1_penalty_init();
771 let rho_max = self.l1_penalty_max().max(rho_init);
772 let factor = self.l1_penalty_increase_factor().max(1.0);
773 let tau = self.l1_steering_factor();
774 let slack_tol = self.l1_slack_tol();
775 let max_outer = self.l1_penalty_max_outer_iter().max(1);
776
777 let mut wrapper = pounce_l1penalty::L1PenaltyBarrierTnlp::new(Rc::clone(&tnlp), rho_init)?;
778 if wrapper.m_eq() == 0 {
779 return None;
782 }
783 wrapper.set_defer_inner_finalize(true);
784 let wrapper_rc = Rc::new(RefCell::new(wrapper));
785
786 let mut rho = rho_init;
787 let mut last_status = ApplicationReturnStatus::InternalError;
788 for _outer in 0..max_outer {
789 wrapper_rc.borrow_mut().set_rho(rho);
790 let dyn_tnlp: Rc<RefCell<dyn TNLP>> = wrapper_rc.clone();
791 last_status = self.optimize_constrained(dyn_tnlp);
792
793 let w = wrapper_rc.borrow();
794 if !w.has_solution() {
795 drop(w);
797 break;
798 }
799 let slack_sum = w.last_slack_sum();
800 let y_eq_inf = w.last_y_eq_inf_norm();
801 drop(w);
802
803 let inner_ok = matches!(
805 last_status,
806 ApplicationReturnStatus::SolveSucceeded
807 | ApplicationReturnStatus::SolvedToAcceptableLevel
808 );
809 if !inner_ok {
810 break;
811 }
812 if slack_sum.is_finite() && slack_sum <= slack_tol {
813 break;
814 }
815 if rho >= rho_max {
816 break;
817 }
818 let geom = rho * factor;
820 let steer = tau * y_eq_inf + 1.0e-12;
821 rho = geom.max(steer).min(rho_max);
822 }
823
824 let w = wrapper_rc.borrow();
826 if w.has_solution() {
827 let x_trunc: Vec<Number> = w.last_x_trunc().to_vec();
828 let lambda: Vec<Number> = w.last_lambda().to_vec();
829 let z_l: Vec<Number> = w.last_z_l_trunc().to_vec();
830 let z_u: Vec<Number> = w.last_z_u_trunc().to_vec();
831 let solver_status = w.last_status().unwrap_or(SolverReturn::InternalError);
832 let slack_sum = w.last_slack_sum();
833 drop(w);
834
835 let infeasible_certificate = matches!(
843 last_status,
844 ApplicationReturnStatus::SolveSucceeded
845 | ApplicationReturnStatus::SolvedToAcceptableLevel
846 ) && slack_sum.is_finite()
847 && slack_sum > slack_tol;
848 let final_app_status = if infeasible_certificate {
849 ApplicationReturnStatus::InfeasibleProblemDetected
850 } else {
851 last_status
852 };
853 let final_solver_status = if infeasible_certificate {
854 SolverReturn::LocalInfeasibility
855 } else {
856 solver_status
857 };
858
859 let f_inner = tnlp
861 .borrow_mut()
862 .eval_f(&x_trunc, true)
863 .unwrap_or(Number::NAN);
864 let m = tnlp
865 .borrow_mut()
866 .get_nlp_info()
867 .map(|i| i.m as usize)
868 .unwrap_or(0);
869 let mut g_inner = vec![0.0; m];
870 if m > 0 {
871 let _ = tnlp.borrow_mut().eval_g(&x_trunc, false, &mut g_inner);
872 }
873 tnlp.borrow_mut().finalize_solution(
874 Solution {
875 status: final_solver_status,
876 x: &x_trunc,
877 z_l: &z_l,
878 z_u: &z_u,
879 g: &g_inner,
880 lambda: &lambda,
881 obj_value: f_inner,
882 },
883 &TnlpIpoptData::default(),
884 &TnlpIpoptCq::default(),
885 );
886 return Some(final_app_status);
887 }
888 Some(last_status)
890 }
891
892 pub fn reoptimize_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
894 self.optimize_tnlp(tnlp)
897 }
898
899 fn optimize_constrained(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
903 let t_start = Instant::now();
904
905 let print_opts = self
909 .options
910 .get_bool_value("print_user_options", "")
911 .ok()
912 .and_then(|(v, f)| f.then_some(v))
913 .unwrap_or(false);
914 if print_opts {
915 print!(
916 "\nList of user-set options:\n\n{}",
917 self.options.print_user_options()
918 );
919 }
920
921 let print_doc = self
931 .options
932 .get_bool_value("print_options_documentation", "")
933 .ok()
934 .and_then(|(v, f)| f.then_some(v))
935 .unwrap_or(false);
936 if print_doc {
937 let mode = self
938 .options
939 .get_string_value("print_options_mode", "")
940 .ok()
941 .map(|(v, _)| PrintOptionsMode::from_tag(&v))
942 .unwrap_or(PrintOptionsMode::Text);
943 let advanced = self
944 .options
945 .get_bool_value("print_advanced_options", "")
946 .ok()
947 .map(|(v, _)| v)
948 .unwrap_or(false);
949 print!(
950 "\n# Pounce options registry\n\n{}",
951 self.reg_options.print_options_documentation(mode, advanced)
952 );
953 }
954
955 let timing = Rc::new(TimingStatistics::new());
961 *self.timing.borrow_mut() = Rc::clone(&timing);
962 timing.overall_alg.start();
963
964 if let Ok(mut guard) = self.linsol_summary_sink.lock() {
970 *guard = LinearSolverSummary::default();
971 } else {
972 debug_assert!(false, "linsol summary sink mutex poisoned");
973 }
974
975 let lo_inf = self
981 .options
982 .get_numeric_value("nlp_lower_bound_inf", "")
983 .ok()
984 .and_then(|(v, f)| f.then_some(v))
985 .unwrap_or(DEFAULT_NLP_LOWER_BOUND_INF);
986 let up_inf = self
987 .options
988 .get_numeric_value("nlp_upper_bound_inf", "")
989 .ok()
990 .and_then(|(v, f)| f.then_some(v))
991 .unwrap_or(DEFAULT_NLP_UPPER_BOUND_INF);
992 let fixed_treatment = match self
993 .options
994 .get_string_value("fixed_variable_treatment", "")
995 .ok()
996 .and_then(|(v, f)| f.then_some(v))
997 .as_deref()
998 {
999 Some("relax_bounds") => FixedVarTreatment::RelaxBounds,
1000 _ => FixedVarTreatment::MakeParameter,
1004 };
1005 let adapter = match TNLPAdapter::new_with_options(
1006 Rc::clone(&tnlp),
1007 lo_inf,
1008 up_inf,
1009 fixed_treatment,
1010 ) {
1011 Ok(a) => Rc::new(RefCell::new(a)),
1012 Err(_) => {
1013 timing.overall_alg.end();
1014 return ApplicationReturnStatus::InvalidProblemDefinition;
1015 }
1016 };
1017 let mut orig_nlp = match OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)) {
1018 Ok(n) => n,
1019 Err(_) => {
1020 timing.overall_alg.end();
1021 return ApplicationReturnStatus::InternalError;
1022 }
1023 };
1024 orig_nlp.set_timing_stats(Rc::clone(&timing));
1025
1026 let n_x_var = orig_nlp.x_space().dim();
1032 let n_c = orig_nlp.c_space().dim();
1033 if n_x_var > 0 && n_x_var < n_c {
1034 timing.overall_alg.end();
1035 return ApplicationReturnStatus::NotEnoughDegreesOfFreedom;
1036 }
1037
1038 let bound_relax_factor = self
1042 .options
1043 .get_numeric_value("bound_relax_factor", "")
1044 .ok()
1045 .and_then(|(v, f)| f.then_some(v))
1046 .unwrap_or(1e-8);
1047 let constr_viol_tol = self
1048 .options
1049 .get_numeric_value("constr_viol_tol", "")
1050 .ok()
1051 .and_then(|(v, f)| f.then_some(v))
1052 .unwrap_or(1e-4);
1053 orig_nlp.relax_bounds(bound_relax_factor, constr_viol_tol);
1054
1055 let scaling_method = self
1060 .options
1061 .get_string_value("nlp_scaling_method", "")
1062 .ok()
1063 .and_then(|(v, f)| f.then_some(v))
1064 .unwrap_or_else(|| "gradient-based".to_string());
1065 let scaling_method = match scaling_method.as_str() {
1066 "none" => ScalingMethod::None,
1067 "gradient-based" => ScalingMethod::GradientBased,
1068 "user-scaling" => ScalingMethod::UserScaling,
1069 _ => ScalingMethod::GradientBased,
1073 };
1074 let max_gradient = self
1075 .options
1076 .get_numeric_value("nlp_scaling_max_gradient", "")
1077 .ok()
1078 .and_then(|(v, f)| f.then_some(v))
1079 .unwrap_or(100.0);
1080 let min_value = self
1081 .options
1082 .get_numeric_value("nlp_scaling_min_value", "")
1083 .ok()
1084 .and_then(|(v, f)| f.then_some(v))
1085 .unwrap_or(1e-8);
1086 let obj_target_gradient = self
1087 .options
1088 .get_numeric_value("nlp_scaling_obj_target_gradient", "")
1089 .ok()
1090 .and_then(|(v, f)| f.then_some(v))
1091 .unwrap_or(0.0);
1092 let constr_target_gradient = self
1093 .options
1094 .get_numeric_value("nlp_scaling_constr_target_gradient", "")
1095 .ok()
1096 .and_then(|(v, f)| f.then_some(v))
1097 .unwrap_or(0.0);
1098 orig_nlp.determine_scaling_from_starting_point(
1099 scaling_method,
1100 max_gradient,
1101 min_value,
1102 obj_target_gradient,
1103 constr_target_gradient,
1104 );
1105
1106 let nlp_handle: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(orig_nlp));
1107
1108 let builder = self.algorithm_builder_from_options();
1116
1117 let feral_cfg = feral_config_from_options(&self.options);
1123 let factory = self.linear_backend_factory.take().unwrap_or_else(|| {
1124 default_backend_factory_with_sink(feral_cfg, Arc::clone(&self.linsol_summary_sink))
1125 });
1126 let bundle = builder.build_with_backend(factory);
1127
1128 let data: crate::ipopt_data::IpoptDataHandle = Rc::new(RefCell::new(AlgIpoptData::new()));
1134 data.borrow_mut().timing = Rc::clone(&timing);
1135 let cq: crate::ipopt_cq::IpoptCqHandle = Rc::new(RefCell::new(
1136 IpoptCalculatedQuantities::new(Rc::clone(&data), Rc::clone(&nlp_handle)),
1137 ));
1138
1139 {
1144 let nlp_borrow = nlp_handle.borrow();
1145 let n_x = nlp_borrow.n();
1146 let n_s = nlp_borrow.m_ineq();
1147 let n_yc = nlp_borrow.m_eq();
1148 let n_yd = nlp_borrow.m_ineq();
1149 let n_zl = nlp_borrow.x_l().dim();
1150 let n_zu = nlp_borrow.x_u().dim();
1151 let n_vl = nlp_borrow.d_l().dim();
1152 let n_vu = nlp_borrow.d_u().dim();
1153 drop(nlp_borrow);
1154 let iv = IteratesVector::new(
1155 Rc::new(DenseVectorSpace::new(n_x).make_new_dense()),
1156 Rc::new(DenseVectorSpace::new(n_s).make_new_dense()),
1157 Rc::new(DenseVectorSpace::new(n_yc).make_new_dense()),
1158 Rc::new(DenseVectorSpace::new(n_yd).make_new_dense()),
1159 Rc::new(DenseVectorSpace::new(n_zl).make_new_dense()),
1160 Rc::new(DenseVectorSpace::new(n_zu).make_new_dense()),
1161 Rc::new(DenseVectorSpace::new(n_vl).make_new_dense()),
1162 Rc::new(DenseVectorSpace::new(n_vu).make_new_dense()),
1163 );
1164 data.borrow_mut().set_curr(iv);
1165 }
1166
1167 let max_iter = self
1168 .options
1169 .get_integer_value("max_iter", "")
1170 .ok()
1171 .and_then(|(v, f)| f.then_some(v))
1172 .unwrap_or(3000);
1173 let tol = self
1174 .options
1175 .get_numeric_value("tol", "")
1176 .ok()
1177 .and_then(|(v, f)| f.then_some(v))
1178 .unwrap_or(1e-8);
1179 data.borrow_mut().tol = tol;
1180
1181 let mut alg = IpoptAlgorithm::new(data, cq, bundle)
1182 .with_nlp(Rc::clone(&nlp_handle))
1183 .with_tnlp(Rc::clone(&tnlp));
1184 if let Some(provider) = self.restoration_factory_provider.as_mut() {
1189 self.restoration_factory = Some(provider());
1190 }
1191 if let Some(factory) = self.restoration_factory.as_mut() {
1192 alg = alg.with_restoration(factory());
1193 }
1194 if let Some(diag) = self.diagnostics.as_ref() {
1195 alg = alg.with_diagnostics(Rc::clone(diag));
1196 }
1197 alg.max_iter = max_iter;
1198 if let Ok((v, found)) = self.options.get_integer_value("print_level", "") {
1203 if found && v <= 0 {
1204 alg.print_iter_output = false;
1205 if let Some(resto) = alg.restoration.as_mut() {
1209 resto.set_print_iter_output(false);
1210 }
1211 }
1212 }
1213
1214 let iter_capture = self
1225 .record_iter_history
1226 .then(pounce_observability::IterCaptureGuard::start);
1227
1228 let solver_status = alg.optimize();
1229
1230 let captured_iters = iter_capture.map(|g| g.finish()).unwrap_or_default();
1231 timing.overall_alg.end();
1237
1238 {
1240 let mut stats = self.statistics.borrow_mut();
1241 {
1242 let d = alg.data.borrow();
1243 stats.iteration_count = d.iter_count;
1244 stats.final_mu = d.curr_mu;
1248 }
1249 stats.total_wallclock_time_secs = t_start.elapsed().as_secs_f64();
1250 stats.restoration_calls = alg.resto_calls;
1254 stats.restoration_inner_iters = alg.resto_inner_iters;
1255 stats.restoration_outer_iters = alg.resto_outer_iters;
1256 stats.restoration_wall_secs = alg.resto_wall_secs;
1257 stats.iterations = captured_iters;
1258 let curr_x = alg.data.borrow().curr.as_ref().map(|c| c.x.clone());
1266 if let Some(x) = curr_x {
1267 if let Ok(f) = try_eval_curr_f(&nlp_handle, &x) {
1268 stats.final_objective = f;
1269 stats.final_scaled_objective = f;
1270 }
1271 }
1272 let cq = alg.cq.borrow();
1277 stats.final_dual_inf = cq.curr_dual_infeasibility_max();
1278 stats.final_constr_viol = cq.curr_primal_infeasibility_max();
1279 let compl = cq
1284 .curr_compl_x_l()
1285 .amax()
1286 .max(cq.curr_compl_x_u().amax())
1287 .max(cq.curr_compl_s_l().amax())
1288 .max(cq.curr_compl_s_u().amax());
1289 stats.final_compl = compl;
1290 stats.final_kkt_error = cq.curr_nlp_error();
1291 }
1292
1293 let app_status = solver_return_to_app_status(solver_status);
1296
1297 if matches!(
1302 app_status,
1303 ApplicationReturnStatus::SolveSucceeded
1304 | ApplicationReturnStatus::SolvedToAcceptableLevel
1305 ) {
1306 if let Some(cb) = self.on_converged.as_mut() {
1307 if let Some(sd) = alg.search_dir.as_mut() {
1308 let pd = sd.pd_solver_rc();
1309 cb(&alg.data, &alg.cq, &nlp_handle, pd);
1310 }
1311 }
1312 }
1313
1314 match finalize_via_orig_nlp(&nlp_handle, &alg, solver_status, app_status, &tnlp) {
1320 Ok(f_unscaled) => {
1321 self.statistics.borrow_mut().final_objective = f_unscaled;
1322 }
1323 Err(()) => {
1324 }
1327 }
1328
1329 let print_timing = self
1337 .options
1338 .get_bool_value("print_timing_statistics", "")
1339 .ok()
1340 .and_then(|(v, f)| f.then_some(v))
1341 .unwrap_or(false);
1342 if print_timing {
1343 let report = timing.report();
1344 print!("{}", report);
1345 use pounce_common::journalist::{JournalCategory, JournalLevel};
1346 self.journalist.print(
1347 JournalLevel::J_SUMMARY,
1348 JournalCategory::J_TIMING_STATISTICS,
1349 &report,
1350 );
1351 }
1352
1353 app_status
1354 }
1355
1356 pub fn algorithm_builder_from_options(&self) -> AlgorithmBuilder {
1364 let mut builder = AlgorithmBuilder::new();
1365
1366 let mut mehrotra_on = false;
1371 if let Ok((v, found)) = self.options.get_string_value("mehrotra_algorithm", "") {
1372 if found && v == "yes" {
1373 mehrotra_on = true;
1374 builder.mehrotra_algorithm = true;
1375 builder.mu_strategy = MuStrategyChoice::Adaptive;
1376 builder.mu_oracle = crate::mu::adaptive::MuOracleKind::Probing;
1377 builder.line_search.accept_every_trial_step = true;
1383 builder.init.bound_push = 10.0;
1387 builder.init.bound_frac = 0.2;
1388 builder.init.slack_bound_push = 10.0;
1389 builder.init.slack_bound_frac = 0.2;
1390 builder.init.bound_mult_init_val = 10.0;
1391 builder.init.constr_mult_init_max = 0.0;
1392 builder.line_search.alpha_for_y =
1397 crate::line_search::backtracking::AlphaForY::BoundMult;
1398 builder.mu.adaptive_mu_globalization =
1405 crate::mu::adaptive::AdaptiveMuGlobalization::NeverMonotoneMode;
1406 builder.init.least_square_init_primal = true;
1414 }
1415 }
1416
1417 if let Ok((v, found)) = self.options.get_string_value("mu_strategy", "") {
1418 if found {
1419 let parsed = match v.as_str() {
1420 "adaptive" => MuStrategyChoice::Adaptive,
1421 _ => MuStrategyChoice::Monotone,
1422 };
1423 if mehrotra_on && matches!(parsed, MuStrategyChoice::Monotone) {
1424 tracing::warn!(target: "pounce::algorithm",
1428 "pounce: mehrotra_algorithm=yes requires \
1429 mu_strategy=adaptive; ignoring \
1430 mu_strategy=monotone."
1431 );
1432 } else {
1433 builder.mu_strategy = parsed;
1434 }
1435 }
1436 }
1437 if let Ok((v, found)) = self.options.get_string_value("mu_oracle", "") {
1438 if found {
1439 builder.mu_oracle = match v.as_str() {
1440 "loqo" => crate::mu::adaptive::MuOracleKind::Loqo,
1441 "probing" => crate::mu::adaptive::MuOracleKind::Probing,
1442 _ => crate::mu::adaptive::MuOracleKind::QualityFunction,
1443 };
1444 }
1445 }
1446 if let Ok((v, found)) = self
1447 .options
1448 .get_string_value("adaptive_mu_globalization", "")
1449 {
1450 if found {
1451 use crate::mu::adaptive::AdaptiveMuGlobalization;
1452 builder.mu.adaptive_mu_globalization = match v.as_str() {
1453 "kkt-error" => AdaptiveMuGlobalization::KktError,
1454 "never-monotone-mode" => AdaptiveMuGlobalization::NeverMonotoneMode,
1455 _ => AdaptiveMuGlobalization::ObjConstrFilter,
1456 };
1457 }
1458 }
1459 if let Ok((v, found)) = self.options.get_string_value("hessian_approximation", "") {
1460 if found {
1461 builder.hessian_approximation = match v.as_str() {
1462 "limited-memory" => HessianApproxChoice::LimitedMemory,
1463 _ => HessianApproxChoice::Exact,
1464 };
1465 }
1466 }
1467 if let Ok((v, found)) = self.options.get_string_value("line_search_method", "") {
1468 if found {
1469 builder.line_search_method = match v.as_str() {
1470 "cg-penalty" => LineSearchChoice::CgPenalty,
1471 "penalty" => LineSearchChoice::Penalty,
1472 _ => LineSearchChoice::Filter,
1473 };
1474 }
1475 }
1476 if let Ok((v, found)) = self.options.get_string_value("accept_every_trial_step", "") {
1479 if found {
1480 builder.line_search.accept_every_trial_step = v == "yes";
1481 }
1482 }
1483 if let Ok((v, found)) = self.options.get_string_value("alpha_for_y", "") {
1486 if found {
1487 use crate::line_search::backtracking::AlphaForY;
1488 builder.line_search.alpha_for_y = match v.as_str() {
1489 "primal" => AlphaForY::Primal,
1490 "bound-mult" | "bound_mult" => AlphaForY::BoundMult,
1491 "full" => AlphaForY::Full,
1492 "min" => AlphaForY::Min,
1493 "max" => AlphaForY::Max,
1494 "primal-and-full" | "dual-and-full" => AlphaForY::Primal,
1495 _ => AlphaForY::Primal,
1496 };
1497 }
1498 }
1499 if let Ok((v, _found)) = self.options.get_string_value("linear_solver", "") {
1511 builder.linear_solver = match v.as_str() {
1512 "ma57" => LinearSolverChoice::Ma57,
1513 _ => LinearSolverChoice::Feral,
1514 };
1515 }
1516
1517 if let Ok((v, found)) = self.options.get_string_value("linear_system_scaling", "") {
1526 if found {
1527 builder.linear_system_scaling = match v.as_str() {
1528 "ruiz" => crate::alg_builder::LinearSystemScalingChoice::Ruiz,
1529 "mc19" => crate::alg_builder::LinearSystemScalingChoice::Mc19,
1530 _ => crate::alg_builder::LinearSystemScalingChoice::None,
1531 };
1532 }
1533 }
1534 if let Ok((v, found)) = self.options.get_bool_value("linear_scaling_on_demand", "") {
1535 if found {
1536 builder.linear_scaling_on_demand = v;
1537 }
1538 }
1539
1540 let read_num = |key: &str| -> Option<f64> {
1544 self.options
1545 .get_numeric_value(key, "")
1546 .ok()
1547 .and_then(|(v, f)| f.then_some(v))
1548 };
1549 let read_int = |key: &str| -> Option<i32> {
1550 self.options
1551 .get_integer_value(key, "")
1552 .ok()
1553 .and_then(|(v, f)| f.then_some(v))
1554 };
1555 if let Some(v) = read_num("tol") {
1556 builder.conv_check.tol = v;
1557 }
1558 if let Some(v) = read_num("dual_inf_tol") {
1559 builder.conv_check.dual_inf_tol = v;
1560 }
1561 if let Some(v) = read_num("constr_viol_tol") {
1562 builder.conv_check.constr_viol_tol = v;
1563 }
1564 if let Some(v) = read_num("compl_inf_tol") {
1565 builder.conv_check.compl_inf_tol = v;
1566 }
1567 if let Some(v) = read_int("max_iter") {
1568 builder.conv_check.max_iter = v;
1569 }
1570 if let Some(v) = read_num("max_cpu_time") {
1571 builder.conv_check.max_cpu_time = v;
1572 }
1573 if let Some(v) = read_num("max_wall_time") {
1574 builder.conv_check.max_wall_time = v;
1575 }
1576 if let Some(v) = read_num("acceptable_tol") {
1577 builder.conv_check.acceptable_tol = v;
1578 }
1579 if let Some(v) = read_num("acceptable_dual_inf_tol") {
1580 builder.conv_check.acceptable_dual_inf_tol = v;
1581 }
1582 if let Some(v) = read_num("acceptable_constr_viol_tol") {
1583 builder.conv_check.acceptable_constr_viol_tol = v;
1584 }
1585 if let Some(v) = read_num("acceptable_compl_inf_tol") {
1586 builder.conv_check.acceptable_compl_inf_tol = v;
1587 }
1588 if let Some(v) = read_num("acceptable_obj_change_tol") {
1589 builder.conv_check.acceptable_obj_change_tol = v;
1590 }
1591 if let Some(v) = read_int("acceptable_iter") {
1592 builder.conv_check.acceptable_iter = v;
1593 }
1594 if let Some(v) = read_num("infeas_stationarity_tol") {
1595 builder.conv_check.infeas_stationarity_tol = v;
1596 }
1597 if let Some(v) = read_num("infeas_viol_kappa") {
1598 builder.conv_check.infeas_viol_kappa = v;
1599 }
1600 if let Some(v) = read_int("infeas_max_streak") {
1601 builder.conv_check.infeas_max_streak = v;
1602 }
1603
1604 if let Some(v) = read_num("mu_init") {
1609 builder.mu.mu_init = v;
1610 }
1611 if let Some(v) = read_num("mu_max") {
1612 builder.mu.mu_max = v;
1613 }
1614 if let Some(v) = read_num("mu_max_fact") {
1615 builder.mu.mu_max_fact = v;
1616 }
1617 if let Some(v) = read_num("mu_min") {
1618 builder.mu.mu_min = v;
1619 }
1620 if let Some(v) = read_num("mu_target") {
1621 builder.mu.mu_target = v;
1622 }
1623 if let Some(v) = read_num("mu_linear_decrease_factor") {
1624 builder.mu.mu_linear_decrease_factor = v;
1625 }
1626 if let Some(v) = read_num("mu_superlinear_decrease_power") {
1627 builder.mu.mu_superlinear_decrease_power = v;
1628 }
1629 if let Ok((v, found)) = self
1630 .options
1631 .get_string_value("mu_allow_fast_monotone_decrease", "")
1632 {
1633 if found {
1634 builder.mu.mu_allow_fast_monotone_decrease = v == "yes";
1635 }
1636 }
1637 if let Some(v) = read_num("barrier_tol_factor") {
1638 builder.mu.barrier_tol_factor = v;
1639 }
1640 if let Some(v) = read_num("sigma_max") {
1641 builder.mu.sigma_max = v;
1642 }
1643 if let Some(v) = read_num("sigma_min") {
1644 builder.mu.sigma_min = v;
1645 }
1646
1647 if let Ok((v, found)) = self
1651 .options
1652 .get_string_value("quality_function_norm_type", "")
1653 {
1654 if found {
1655 use crate::mu::oracle::quality_function::NormType;
1656 builder.mu.quality_function_norm_type = match v.as_str() {
1657 "1-norm" => NormType::OneNorm,
1658 "2-norm" => NormType::TwoNorm,
1659 "max-norm" => NormType::MaxNorm,
1660 _ => NormType::TwoNormSquared,
1661 };
1662 }
1663 }
1664 if let Ok((v, found)) = self
1665 .options
1666 .get_string_value("quality_function_centrality", "")
1667 {
1668 if found {
1669 use crate::mu::oracle::quality_function::CentralityType;
1670 builder.mu.quality_function_centrality = match v.as_str() {
1671 "log" => CentralityType::LogCenter,
1672 "reciprocal" => CentralityType::ReciprocalCenter,
1673 "cubed-reciprocal" => CentralityType::CubedReciprocalCenter,
1674 _ => CentralityType::None,
1675 };
1676 }
1677 }
1678 if let Ok((v, found)) = self
1679 .options
1680 .get_string_value("quality_function_balancing_term", "")
1681 {
1682 if found {
1683 use crate::mu::oracle::quality_function::BalancingTermType;
1684 builder.mu.quality_function_balancing_term = match v.as_str() {
1685 "cubic" => BalancingTermType::CubicTerm,
1686 _ => BalancingTermType::None,
1687 };
1688 }
1689 }
1690 if let Some(v) = read_int("quality_function_max_section_steps") {
1691 builder.mu.quality_function_max_section_steps = v;
1692 }
1693 if let Some(v) = read_num("quality_function_section_sigma_tol") {
1694 builder.mu.quality_function_section_sigma_tol = v;
1695 }
1696 if let Some(v) = read_num("quality_function_section_qf_tol") {
1697 builder.mu.quality_function_section_qf_tol = v;
1698 }
1699
1700 if let Some(v) = read_num("probing_iterate_quality_factor") {
1708 builder.mu.probing_iterate_quality_factor = v;
1709 }
1710
1711 if let Some(v) = read_num("adaptive_mu_safeguard_factor") {
1715 builder.mu.adaptive_mu_safeguard_factor = v;
1716 }
1717 if let Some(v) = read_num("adaptive_mu_monotone_init_factor") {
1718 builder.mu.adaptive_mu_monotone_init_factor = v;
1719 }
1720 if let Ok((v, found)) = self
1721 .options
1722 .get_bool_value("adaptive_mu_restore_previous_iterate", "")
1723 {
1724 if found {
1725 builder.mu.adaptive_mu_restore_previous_iterate = v;
1726 }
1727 }
1728 if let Some(v) = read_int("adaptive_mu_kkterror_red_iters") {
1729 if v >= 0 {
1730 builder.mu.adaptive_mu_kkterror_red_iters = v as usize;
1731 }
1732 }
1733 if let Some(v) = read_num("adaptive_mu_kkterror_red_fact") {
1734 builder.mu.adaptive_mu_kkterror_red_fact = v;
1735 }
1736 if let Ok((v, found)) = self
1737 .options
1738 .get_string_value("adaptive_mu_kkt_norm_type", "")
1739 {
1740 if found {
1741 use crate::mu::adaptive::AdaptiveMuKktNorm;
1742 builder.mu.adaptive_mu_kkt_norm_type = match v.as_str() {
1743 "1-norm" => AdaptiveMuKktNorm::OneNorm,
1744 "2-norm" => AdaptiveMuKktNorm::TwoNorm,
1745 "max-norm" => AdaptiveMuKktNorm::MaxNorm,
1746 _ => AdaptiveMuKktNorm::TwoNormSquared,
1747 };
1748 }
1749 }
1750
1751 if let Some(v) = read_int("watchdog_shortened_iter_trigger") {
1755 builder.line_search.watchdog_shortened_iter_trigger = v;
1756 }
1757 if let Some(v) = read_int("watchdog_trial_iter_max") {
1758 builder.line_search.watchdog_trial_iter_max = v;
1759 }
1760 if let Some(v) = read_num("soft_resto_pderror_reduction_factor") {
1761 builder.line_search.soft_resto_pderror_reduction_factor = v;
1762 }
1763 if let Some(v) = read_int("max_soft_resto_iters") {
1764 builder.line_search.max_soft_resto_iters = v;
1765 }
1766
1767 if let Some(v) = read_int("print_frequency_iter") {
1769 builder.output.print_frequency_iter = v;
1770 }
1771 if let Some(v) = read_num("print_frequency_time") {
1772 builder.output.print_frequency_time = v;
1773 }
1774 if let Ok((v, found)) = self.options.get_bool_value("print_info_string", "") {
1775 if found {
1776 builder.output.print_info_string = v;
1777 }
1778 }
1779 if let Ok((v, found)) = self.options.get_string_value("inf_pr_output", "") {
1780 if found {
1781 builder.output.inf_pr_output_internal = v == "internal";
1782 }
1783 }
1784
1785 if let Ok((v, found)) = self.options.get_bool_value("warm_start_init_point", "") {
1791 if found {
1792 builder.warm_start_init_point = v;
1793 }
1794 }
1795 if let Ok((v, found)) = self.options.get_bool_value("warm_start_same_structure", "") {
1796 if found {
1797 builder.warm.same_structure = v;
1798 }
1799 }
1800 if let Some(v) = read_num("warm_start_bound_push") {
1801 builder.warm.bound_push = v;
1802 }
1803 if let Some(v) = read_num("warm_start_bound_frac") {
1804 builder.warm.bound_frac = v;
1805 }
1806 if let Some(v) = read_num("warm_start_slack_bound_push") {
1807 builder.warm.slack_bound_push = v;
1808 }
1809 if let Some(v) = read_num("warm_start_slack_bound_frac") {
1810 builder.warm.slack_bound_frac = v;
1811 }
1812 if let Some(v) = read_num("warm_start_mult_bound_push") {
1813 builder.warm.mult_bound_push = v;
1814 }
1815 if let Some(v) = read_num("warm_start_mult_init_max") {
1816 builder.warm.mult_init_max = v;
1817 }
1818 if let Some(v) = read_num("warm_start_target_mu") {
1819 builder.warm.target_mu = v;
1820 }
1821 if let Ok((v, found)) = self
1822 .options
1823 .get_string_value("warm_start_entire_iterate", "")
1824 {
1825 if found {
1826 builder.warm.entire_iterate = v == "yes";
1827 }
1828 }
1829
1830 if let Some(v) = read_num("bound_push") {
1834 builder.init.bound_push = v;
1835 }
1836 if let Some(v) = read_num("bound_frac") {
1837 builder.init.bound_frac = v;
1838 }
1839 if let Some(v) = read_num("slack_bound_push") {
1840 builder.init.slack_bound_push = v;
1841 }
1842 if let Some(v) = read_num("slack_bound_frac") {
1843 builder.init.slack_bound_frac = v;
1844 }
1845 if let Some(v) = read_num("constr_mult_init_max") {
1846 builder.init.constr_mult_init_max = v;
1847 }
1848 if let Some(v) = read_num("bound_mult_init_val") {
1849 builder.init.bound_mult_init_val = v;
1850 }
1851 if let Ok((v, found)) = self.options.get_string_value("bound_mult_init_method", "") {
1852 if found {
1853 builder.init.bound_mult_init_method = v;
1854 }
1855 }
1856 if let Ok((v, found)) = self
1857 .options
1858 .get_string_value("least_square_init_primal", "")
1859 {
1860 if found {
1861 builder.init.least_square_init_primal = v == "yes";
1862 }
1863 }
1864 builder
1865 }
1866}
1867
1868fn journal_level_from_int(v: i32) -> JournalLevel {
1872 match v.clamp(0, 12) {
1873 0 => JournalLevel::J_NONE,
1874 1 => JournalLevel::J_ERROR,
1875 2 => JournalLevel::J_STRONGWARNING,
1876 3 => JournalLevel::J_SUMMARY,
1877 4 => JournalLevel::J_WARNING,
1878 5 => JournalLevel::J_ITERSUMMARY,
1879 6 => JournalLevel::J_DETAILED,
1880 7 => JournalLevel::J_MOREDETAILED,
1881 8 => JournalLevel::J_VECTOR,
1882 9 => JournalLevel::J_MOREVECTOR,
1883 10 => JournalLevel::J_MATRIX,
1884 11 => JournalLevel::J_MOREMATRIX,
1885 _ => JournalLevel::J_ALL,
1886 }
1887}
1888
1889pub fn default_backend_factory(feral_cfg: pounce_feral::FeralConfig) -> LinearBackendFactory {
1898 Box::new(
1899 move |choice: LinearSolverChoice| -> Box<dyn SparseSymLinearSolverInterface> {
1900 match choice {
1901 LinearSolverChoice::Feral => {
1902 Box::new(pounce_feral::FeralSolverInterface::with_config(feral_cfg))
1903 }
1904 LinearSolverChoice::Ma57 => {
1905 #[cfg(feature = "ma57")]
1906 {
1907 Box::new(pounce_hsl::Ma57SolverInterface::new())
1908 }
1909 #[cfg(not(feature = "ma57"))]
1910 {
1911 Box::new(pounce_feral::FeralSolverInterface::with_config(feral_cfg))
1913 }
1914 }
1915 }
1916 },
1917 )
1918}
1919
1920pub fn default_backend_factory_with_sink(
1927 feral_cfg: pounce_feral::FeralConfig,
1928 sink: Arc<Mutex<LinearSolverSummary>>,
1929) -> LinearBackendFactory {
1930 Box::new(
1931 move |choice: LinearSolverChoice| -> Box<dyn SparseSymLinearSolverInterface> {
1932 match choice {
1933 LinearSolverChoice::Feral => Box::new(
1934 pounce_feral::FeralSolverInterface::with_config(feral_cfg)
1935 .with_summary_sink(Arc::clone(&sink)),
1936 ),
1937 LinearSolverChoice::Ma57 => {
1938 #[cfg(feature = "ma57")]
1939 {
1940 Box::new(pounce_hsl::Ma57SolverInterface::new())
1941 }
1942 #[cfg(not(feature = "ma57"))]
1943 {
1944 Box::new(
1945 pounce_feral::FeralSolverInterface::with_config(feral_cfg)
1946 .with_summary_sink(Arc::clone(&sink)),
1947 )
1948 }
1949 }
1950 }
1951 },
1952 )
1953}
1954
1955pub fn feral_config_from_options(
1961 options: &pounce_common::options_list::OptionsList,
1962) -> pounce_feral::FeralConfig {
1963 let mut cfg = pounce_feral::FeralConfig::from_env();
1964 if let Ok((v, true)) = options.get_bool_value("feral_cascade_break", "") {
1971 cfg.cascade_break = Some(v);
1972 }
1973 if let Ok((v, true)) = options.get_bool_value("feral_fma", "") {
1974 cfg.fma = v;
1975 }
1976 if let Ok((v, true)) = options.get_bool_value("feral_refine", "") {
1977 cfg.refine = v;
1978 }
1979 if let Ok((v, true)) = options.get_numeric_value("feral_singular_pivot_floor", "") {
1980 cfg.singular_pivot_floor = v;
1981 }
1982 if let Ok((v, true)) = options.get_numeric_value("feral_pivtol", "") {
1983 cfg.pivtol = v;
1984 }
1985 if let Ok((v, true)) = options.get_string_value("feral_ordering", "") {
1990 if let Some(m) = pounce_feral::parse_ordering_method(&v) {
1991 cfg.ordering = m;
1992 }
1993 }
1994 cfg
1995}
1996
1997fn solver_return_to_app_status(s: SolverReturn) -> ApplicationReturnStatus {
2003 match s {
2004 SolverReturn::Success => ApplicationReturnStatus::SolveSucceeded,
2005 SolverReturn::StopAtAcceptablePoint => ApplicationReturnStatus::SolvedToAcceptableLevel,
2006 SolverReturn::FeasiblePointFound => ApplicationReturnStatus::FeasiblePointFound,
2007 SolverReturn::MaxiterExceeded => ApplicationReturnStatus::MaximumIterationsExceeded,
2008 SolverReturn::CpuTimeExceeded => ApplicationReturnStatus::MaximumCpuTimeExceeded,
2009 SolverReturn::WallTimeExceeded => ApplicationReturnStatus::MaximumWallTimeExceeded,
2010 SolverReturn::StopAtTinyStep => ApplicationReturnStatus::SearchDirectionBecomesTooSmall,
2011 SolverReturn::LocalInfeasibility => ApplicationReturnStatus::InfeasibleProblemDetected,
2012 SolverReturn::UserRequestedStop => ApplicationReturnStatus::UserRequestedStop,
2013 SolverReturn::DivergingIterates => ApplicationReturnStatus::DivergingIterates,
2014 SolverReturn::RestorationFailure => ApplicationReturnStatus::RestorationFailed,
2015 SolverReturn::ErrorInStepComputation => ApplicationReturnStatus::ErrorInStepComputation,
2016 SolverReturn::InvalidNumberDetected => ApplicationReturnStatus::InvalidNumberDetected,
2017 SolverReturn::TooFewDegreesOfFreedom => ApplicationReturnStatus::NotEnoughDegreesOfFreedom,
2018 SolverReturn::InvalidOption => ApplicationReturnStatus::InvalidOption,
2019 SolverReturn::OutOfMemory => ApplicationReturnStatus::InsufficientMemory,
2020 SolverReturn::InternalError | SolverReturn::Unassigned => {
2021 ApplicationReturnStatus::InternalError
2022 }
2023 }
2024}
2025
2026fn try_eval_curr_f(
2030 nlp: &Rc<RefCell<dyn IpoptNlp>>,
2031 x: &Rc<dyn pounce_linalg::Vector>,
2032) -> Result<Number, ()> {
2033 let mut nlp_mut = nlp.borrow_mut();
2034 Ok(nlp_mut.eval_f(&**x))
2035}
2036
2037fn is_l1_fallback_trigger(status: ApplicationReturnStatus) -> bool {
2044 matches!(
2045 status,
2046 ApplicationReturnStatus::RestorationFailed
2047 | ApplicationReturnStatus::InfeasibleProblemDetected
2048 | ApplicationReturnStatus::SolvedToAcceptableLevel
2049 | ApplicationReturnStatus::MaximumIterationsExceeded
2050 | ApplicationReturnStatus::NotEnoughDegreesOfFreedom
2051 )
2052}
2053
2054fn finalize_via_orig_nlp(
2065 nlp: &Rc<RefCell<dyn IpoptNlp>>,
2066 alg: &IpoptAlgorithm,
2067 solver_status: SolverReturn,
2068 _app_status: ApplicationReturnStatus,
2069 tnlp: &Rc<RefCell<dyn TNLP>>,
2070) -> Result<Number, ()> {
2071 let curr = alg.data.borrow().curr.clone().ok_or(())?;
2072 let nlp_borrow = nlp.borrow();
2076 let x_vec: Vec<Number> = nlp_borrow.lift_x_to_full(&*curr.x);
2077 let info = tnlp.borrow_mut().get_nlp_info().ok_or(())?;
2078 let n = info.n as usize;
2079 let m = info.m as usize;
2080 debug_assert_eq!(x_vec.len(), n);
2081 let mut z_l = nlp_borrow.pack_z_l_for_user(&*curr.z_l);
2085 if z_l.is_empty() {
2086 z_l = vec![0.0; n];
2087 }
2088 let mut z_u = nlp_borrow.pack_z_u_for_user(&*curr.z_u);
2089 if z_u.is_empty() {
2090 z_u = vec![0.0; n];
2091 }
2092 let mut lambda = nlp_borrow.pack_lambda_for_user(&*curr.y_c, &*curr.y_d);
2093 if lambda.is_empty() {
2094 lambda = vec![0.0; m];
2095 }
2096 drop(nlp_borrow);
2097 let mut g_final = vec![0.0; m];
2100 let _ = tnlp.borrow_mut().eval_g(&x_vec, true, &mut g_final);
2101 let f_final = tnlp
2102 .borrow_mut()
2103 .eval_f(&x_vec, true)
2104 .unwrap_or(Number::NAN);
2105 tnlp.borrow_mut().finalize_solution(
2106 Solution {
2107 status: solver_status,
2108 x: &x_vec,
2109 z_l: &z_l,
2110 z_u: &z_u,
2111 g: &g_final,
2112 lambda: &lambda,
2113 obj_value: f_final,
2114 },
2115 &TnlpIpoptData::default(),
2116 &TnlpIpoptCq::default(),
2117 );
2118 Ok(f_final)
2119}
2120
2121fn apply_sqp_options(options: &OptionsList, opts: &mut crate::sqp::SqpOptions) {
2129 use crate::sqp::{SqpGlobalization, SqpHessianSource};
2130
2131 if let Ok((s, true)) = options.get_string_value("sqp_globalization", "") {
2132 opts.globalization = match s.as_str() {
2133 "filter" => SqpGlobalization::Filter,
2134 "l1-elastic" => SqpGlobalization::L1Elastic,
2135 _ => opts.globalization,
2136 };
2137 }
2138 if let Ok((s, true)) = options.get_string_value("sqp_hessian", "") {
2139 opts.hessian = match s.as_str() {
2140 "exact" => SqpHessianSource::Exact,
2141 "damped-bfgs" => SqpHessianSource::DampedBfgs,
2142 "lbfgs" => SqpHessianSource::Lbfgs,
2143 _ => opts.hessian,
2144 };
2145 }
2146 if let Ok((v, true)) = options.get_integer_value("sqp_max_iter", "") {
2147 if v >= 0 {
2148 opts.max_iter = v as u32;
2149 }
2150 }
2151 if let Ok((v, true)) = options.get_numeric_value("sqp_tol", "") {
2152 opts.tol = v;
2153 }
2154 if let Ok((v, true)) = options.get_numeric_value("sqp_constr_viol_tol", "") {
2155 opts.constr_viol_tol = v;
2156 }
2157 if let Ok((v, true)) = options.get_numeric_value("sqp_dual_inf_tol", "") {
2158 opts.dual_inf_tol = v;
2159 }
2160 if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty", "") {
2161 opts.l1_penalty = v;
2162 }
2163 if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty_safety", "") {
2164 opts.l1_penalty_safety = v;
2165 }
2166 if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty_max", "") {
2167 opts.l1_penalty_max = v;
2168 }
2169 if let Ok((v, true)) = options.get_numeric_value("sqp_bt_reduction", "") {
2170 opts.bt_reduction = v;
2171 }
2172 if let Ok((v, true)) = options.get_numeric_value("sqp_bt_min_alpha", "") {
2173 opts.bt_min_alpha = v;
2174 }
2175 if let Ok((v, true)) = options.get_integer_value("sqp_print_level", "") {
2176 opts.print_level = v.clamp(0, u8::MAX as i32) as u8;
2177 }
2178 if let Ok((v, true)) = options.get_integer_value("sqp_lbfgs_max_history", "") {
2179 if v >= 1 {
2180 opts.lbfgs_max_history = v as u32;
2181 }
2182 }
2183}
2184
2185fn finalize_via_sqp(
2193 nlp: &Rc<RefCell<dyn IpoptNlp>>,
2194 res: &crate::sqp::SqpResult,
2195 solver_status: pounce_nlp::SolverReturn,
2196 tnlp: &Rc<RefCell<dyn TNLP>>,
2197) -> Result<Number, ()> {
2198 use pounce_linalg::dense_vector::DenseVectorSpace;
2199
2200 let info = tnlp.borrow_mut().get_nlp_info().ok_or(())?;
2201 let n = info.n as usize;
2202 let m = info.m as usize;
2203
2204 let nlp_borrow = nlp.borrow();
2207 let n_alg = nlp_borrow.n() as usize;
2208 let m_eq = nlp_borrow.m_eq() as usize;
2209 let m_ineq = nlp_borrow.m_ineq() as usize;
2210 debug_assert_eq!(res.x.len(), n_alg);
2211 debug_assert_eq!(res.lambda_g.len(), m_eq + m_ineq);
2212 debug_assert_eq!(res.lambda_x.len(), n_alg);
2213
2214 let x_space = DenseVectorSpace::new(n_alg as Index);
2215 let c_space = DenseVectorSpace::new(m_eq as Index);
2216 let d_space = DenseVectorSpace::new(m_ineq as Index);
2217
2218 let mut x_dv = x_space.make_new_dense();
2219 x_dv.set_values(&res.x);
2220 let x_vec: Vec<Number> = nlp_borrow.lift_x_to_full(&x_dv);
2221 debug_assert_eq!(x_vec.len(), n);
2222
2223 let mut z_l_compressed = x_space.make_new_dense();
2225 let mut z_u_compressed = x_space.make_new_dense();
2226 let zl_vals: Vec<Number> = res.lambda_x.iter().map(|v| v.max(0.0)).collect();
2227 let zu_vals: Vec<Number> = res.lambda_x.iter().map(|v| (-v).max(0.0)).collect();
2228 z_l_compressed.set_values(&zl_vals);
2229 z_u_compressed.set_values(&zu_vals);
2230 let mut z_l = nlp_borrow.pack_z_l_for_user(&z_l_compressed);
2231 if z_l.is_empty() {
2232 z_l = vec![0.0; n];
2233 }
2234 let mut z_u = nlp_borrow.pack_z_u_for_user(&z_u_compressed);
2235 if z_u.is_empty() {
2236 z_u = vec![0.0; n];
2237 }
2238
2239 let mut y_c_dv = c_space.make_new_dense();
2241 let mut y_d_dv = d_space.make_new_dense();
2242 if m_eq > 0 {
2243 y_c_dv.set_values(&res.lambda_g[..m_eq]);
2244 }
2245 if m_ineq > 0 {
2246 y_d_dv.set_values(&res.lambda_g[m_eq..]);
2247 }
2248 let mut lambda = nlp_borrow.pack_lambda_for_user(&y_c_dv, &y_d_dv);
2249 if lambda.is_empty() {
2250 lambda = vec![0.0; m];
2251 }
2252 drop(nlp_borrow);
2253
2254 let mut g_final = vec![0.0; m];
2255 let _ = tnlp.borrow_mut().eval_g(&x_vec, true, &mut g_final);
2256 let f_final = tnlp
2257 .borrow_mut()
2258 .eval_f(&x_vec, true)
2259 .unwrap_or(Number::NAN);
2260 tnlp.borrow_mut().finalize_solution(
2261 pounce_nlp::tnlp::Solution {
2262 status: solver_status,
2263 x: &x_vec,
2264 z_l: &z_l,
2265 z_u: &z_u,
2266 g: &g_final,
2267 lambda: &lambda,
2268 obj_value: f_final,
2269 },
2270 &TnlpIpoptData::default(),
2271 &TnlpIpoptCq::default(),
2272 );
2273 Ok(f_final)
2274}
2275
2276#[cfg(test)]
2277mod tests {
2278 use super::*;
2279 use pounce_nlp::tnlp::{
2280 BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest,
2281 StartingPoint,
2282 };
2283
2284 struct Hs071Stub;
2285 impl TNLP for Hs071Stub {
2286 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
2287 Some(NlpInfo {
2290 n: 4,
2291 m: 2,
2292 nnz_jac_g: 8,
2293 nnz_h_lag: 10,
2294 index_style: IndexStyle::C,
2295 })
2296 }
2297 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
2298 b.x_l.copy_from_slice(&[1.0; 4]);
2299 b.x_u.copy_from_slice(&[5.0; 4]);
2300 b.g_l.copy_from_slice(&[25.0, 40.0]);
2301 b.g_u.copy_from_slice(&[2.0e19, 40.0]);
2302 true
2303 }
2304 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2305 sp.x.copy_from_slice(&[1.0, 5.0, 5.0, 1.0]);
2306 true
2307 }
2308 fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
2309 Some(x[0] * x[3] * (x[0] + x[1] + x[2]) + x[2])
2310 }
2311 fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
2312 grad.fill(0.0);
2313 true
2314 }
2315 fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
2316 g.fill(0.0);
2317 true
2318 }
2319 fn eval_jac_g(
2320 &mut self,
2321 _x: Option<&[Number]>,
2322 _new_x: bool,
2323 mode: SparsityRequest<'_>,
2324 ) -> bool {
2325 if let SparsityRequest::Structure { irow, jcol } = mode {
2326 irow.copy_from_slice(&[0, 0, 0, 0, 1, 1, 1, 1]);
2327 jcol.copy_from_slice(&[0, 1, 2, 3, 0, 1, 2, 3]);
2328 }
2329 true
2330 }
2331 fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
2332 }
2333
2334 #[test]
2335 fn application_default_does_not_select_sqp() {
2336 let mut app = IpoptApplication::new();
2337 app.initialize().unwrap();
2338 assert!(!app.is_sqp_algorithm_selected());
2339 }
2340
2341 #[test]
2342 fn application_routes_to_sqp_when_algorithm_option_set() {
2343 let mut app = IpoptApplication::new();
2344 app.initialize().unwrap();
2345 app.initialize_with_options_str("algorithm active-set-sqp\n")
2346 .unwrap();
2347 assert!(app.is_sqp_algorithm_selected());
2348 }
2349
2350 struct ConvexEqTnlp {
2357 finalize_called: std::rc::Rc<std::cell::RefCell<Option<(Vec<Number>, Number)>>>,
2358 }
2359 impl TNLP for ConvexEqTnlp {
2360 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
2361 Some(NlpInfo {
2362 n: 2,
2363 m: 1,
2364 nnz_jac_g: 2,
2365 nnz_h_lag: 2,
2366 index_style: IndexStyle::C,
2367 })
2368 }
2369 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
2370 b.x_l.copy_from_slice(&[-2.0e19; 2]);
2371 b.x_u.copy_from_slice(&[2.0e19; 2]);
2372 b.g_l.copy_from_slice(&[1.0]);
2373 b.g_u.copy_from_slice(&[1.0]);
2374 true
2375 }
2376 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2377 sp.x.copy_from_slice(&[0.0, 0.0]);
2378 true
2379 }
2380 fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
2381 Some(0.5 * (x[0] * x[0] + x[1] * x[1]) - x[0] - 2.0 * x[1])
2382 }
2383 fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
2384 grad[0] = x[0] - 1.0;
2385 grad[1] = x[1] - 2.0;
2386 true
2387 }
2388 fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
2389 g[0] = x[0] + x[1];
2390 true
2391 }
2392 fn eval_jac_g(
2393 &mut self,
2394 _x: Option<&[Number]>,
2395 _new_x: bool,
2396 mode: SparsityRequest<'_>,
2397 ) -> bool {
2398 match mode {
2399 SparsityRequest::Structure { irow, jcol } => {
2400 irow.copy_from_slice(&[0, 0]);
2401 jcol.copy_from_slice(&[0, 1]);
2402 }
2403 SparsityRequest::Values { values, .. } => {
2404 values.copy_from_slice(&[1.0, 1.0]);
2405 }
2406 }
2407 true
2408 }
2409 fn eval_h(
2410 &mut self,
2411 _x: Option<&[Number]>,
2412 _new_x: bool,
2413 _obj_factor: Number,
2414 _lambda: Option<&[Number]>,
2415 _new_lambda: bool,
2416 mode: SparsityRequest<'_>,
2417 ) -> bool {
2418 match mode {
2419 SparsityRequest::Structure { irow, jcol } => {
2420 irow.copy_from_slice(&[0, 1]);
2421 jcol.copy_from_slice(&[0, 1]);
2422 }
2423 SparsityRequest::Values { values, .. } => {
2424 values.copy_from_slice(&[1.0, 1.0]);
2425 }
2426 }
2427 true
2428 }
2429 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
2430 *self.finalize_called.borrow_mut() = Some((sol.x.to_vec(), sol.obj_value));
2431 }
2432 }
2433
2434 #[test]
2435 fn application_sqp_path_solves_convex_eq_nlp_and_finalizes() {
2436 let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
2437 let tnlp = std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
2438 finalize_called: std::rc::Rc::clone(&finalize_slot),
2439 }));
2440
2441 let mut app = IpoptApplication::new();
2442 app.initialize().unwrap();
2443 app.initialize_with_options_str("algorithm active-set-sqp\n")
2444 .unwrap();
2445 let status = app.optimize_tnlp(tnlp);
2446 assert_eq!(status, ApplicationReturnStatus::SolveSucceeded);
2447
2448 let recv = finalize_slot.borrow().clone();
2450 let (x_recv, obj_recv) = recv.expect("finalize_solution was not called");
2451 assert_eq!(x_recv.len(), 2);
2452 assert!((x_recv[0] - 0.0).abs() < 1e-6, "x[0] = {}", x_recv[0]);
2453 assert!((x_recv[1] - 1.0).abs() < 1e-6, "x[1] = {}", x_recv[1]);
2454 assert!(
2455 (obj_recv - (-1.5)).abs() < 1e-6,
2456 "obj = {} but expected -1.5",
2457 obj_recv
2458 );
2459 }
2460
2461 #[test]
2462 fn application_routes_to_sqp_case_insensitively() {
2463 let mut app = IpoptApplication::new();
2464 app.initialize().unwrap();
2465 app.initialize_with_options_str("algorithm Active-Set-SQP\n")
2466 .unwrap();
2467 assert!(app.is_sqp_algorithm_selected());
2471 }
2472
2473 #[test]
2474 fn application_constructs_and_loads_options() {
2475 let mut app = IpoptApplication::new();
2476 app.initialize().unwrap();
2477 app.initialize_with_options_str("print_level 5\nfile_print_level 7\n")
2480 .unwrap();
2481 let (level, found) = app.options().get_integer_value("print_level", "").unwrap();
2482 assert!(found);
2483 assert_eq!(level, 5);
2484 }
2485
2486 #[test]
2487 fn application_sqp_suboptions_propagate_to_builder() {
2488 let mut app = IpoptApplication::new();
2491 app.initialize().unwrap();
2492 app.initialize_with_options_str(
2493 "algorithm active-set-sqp\n\
2494 sqp_globalization l1-elastic\n\
2495 sqp_hessian lbfgs\n\
2496 sqp_max_iter 17\n\
2497 sqp_tol 1e-7\n\
2498 sqp_constr_viol_tol 1e-5\n\
2499 sqp_dual_inf_tol 1e-3\n\
2500 sqp_l1_penalty 2.5\n\
2501 sqp_bt_reduction 0.25\n\
2502 sqp_bt_min_alpha 1e-10\n\
2503 sqp_print_level 2\n\
2504 sqp_lbfgs_max_history 12\n",
2505 )
2506 .unwrap();
2507 let snap = app.algorithm_builder_snapshot();
2508 assert_eq!(
2509 snap.sqp.globalization,
2510 crate::sqp::SqpGlobalization::L1Elastic
2511 );
2512 assert_eq!(snap.sqp.hessian, crate::sqp::SqpHessianSource::Lbfgs);
2513 assert_eq!(snap.sqp.max_iter, 17);
2514 assert!((snap.sqp.tol - 1e-7).abs() < 1e-18);
2515 assert!((snap.sqp.constr_viol_tol - 1e-5).abs() < 1e-18);
2516 assert!((snap.sqp.dual_inf_tol - 1e-3).abs() < 1e-18);
2517 assert!((snap.sqp.l1_penalty - 2.5).abs() < 1e-18);
2518 assert!((snap.sqp.bt_reduction - 0.25).abs() < 1e-18);
2519 assert!((snap.sqp.bt_min_alpha - 1e-10).abs() < 1e-18);
2520 assert_eq!(snap.sqp.print_level, 2);
2521 assert_eq!(snap.sqp.lbfgs_max_history, 12);
2522 }
2523
2524 #[test]
2525 fn application_sqp_warm_start_round_trip() {
2526 let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
2532 let tnlp_rc: std::rc::Rc<std::cell::RefCell<dyn TNLP>> =
2533 std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
2534 finalize_called: std::rc::Rc::clone(&finalize_slot),
2535 }));
2536
2537 let mut app = IpoptApplication::new();
2538 app.initialize().unwrap();
2539 app.initialize_with_options_str("algorithm active-set-sqp\n")
2540 .unwrap();
2541
2542 let status_a = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
2544 assert_eq!(status_a, ApplicationReturnStatus::SolveSucceeded);
2545 let ws = app.last_sqp_working_set().cloned();
2546 assert!(ws.is_some(), "cold solve must yield a working set");
2547
2548 let (x_recv, _) = finalize_slot.borrow().clone().unwrap();
2552 let warm = crate::sqp::SqpIterates {
2553 x: x_recv,
2554 lambda_g: vec![1.0],
2555 lambda_x: vec![0.0, 0.0],
2556 working: ws,
2557 };
2558 app.set_sqp_warm_start(warm);
2559
2560 let status_b = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
2562 assert_eq!(status_b, ApplicationReturnStatus::SolveSucceeded);
2563 assert!(app.last_sqp_working_set().is_some());
2564 }
2565
2566 #[test]
2567 fn application_sqp_warm_start_auto_clears_after_use() {
2568 let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
2569 let tnlp_rc: std::rc::Rc<std::cell::RefCell<dyn TNLP>> =
2570 std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
2571 finalize_called: std::rc::Rc::clone(&finalize_slot),
2572 }));
2573 let mut app = IpoptApplication::new();
2574 app.initialize().unwrap();
2575 app.initialize_with_options_str("algorithm active-set-sqp\n")
2576 .unwrap();
2577 app.set_sqp_warm_start(crate::sqp::SqpIterates {
2578 x: vec![0.0, 1.0],
2579 lambda_g: vec![1.0],
2580 lambda_x: vec![0.0, 0.0],
2581 working: None,
2582 });
2583 assert!(app.sqp_warm_start.is_some());
2584 let _ = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
2585 assert!(
2586 app.sqp_warm_start.is_none(),
2587 "warm-start input must be auto-cleared after use"
2588 );
2589 }
2590
2591 #[test]
2592 fn application_sqp_suboptions_default_when_unset() {
2593 let mut app = IpoptApplication::new();
2596 app.initialize().unwrap();
2597 let snap = app.algorithm_builder_snapshot();
2598 let d = crate::sqp::SqpOptions::default();
2599 assert_eq!(snap.sqp.globalization, d.globalization);
2600 assert_eq!(snap.sqp.hessian, d.hessian);
2601 assert_eq!(snap.sqp.max_iter, d.max_iter);
2602 assert!((snap.sqp.tol - d.tol).abs() < 1e-18);
2603 assert!((snap.sqp.constr_viol_tol - d.constr_viol_tol).abs() < 1e-18);
2604 assert!((snap.sqp.dual_inf_tol - d.dual_inf_tol).abs() < 1e-18);
2605 assert!((snap.sqp.l1_penalty - d.l1_penalty).abs() < 1e-18);
2606 assert!((snap.sqp.bt_reduction - d.bt_reduction).abs() < 1e-18);
2607 assert!((snap.sqp.bt_min_alpha - d.bt_min_alpha).abs() < 1e-18);
2608 assert_eq!(snap.sqp.print_level, d.print_level);
2609 assert_eq!(snap.sqp.lbfgs_max_history, d.lbfgs_max_history);
2610 }
2611
2612 #[test]
2613 fn application_reports_problem_dimensions() {
2614 let app = IpoptApplication::new();
2615 let mut tnlp = Hs071Stub;
2616 let info = app.problem_dimensions(&mut tnlp).unwrap();
2617 assert_eq!(info.n, 4);
2618 assert_eq!(info.m, 2);
2619 assert_eq!(info.nnz_jac_g, 8);
2620 assert_eq!(info.nnz_h_lag, 10);
2621 }
2622}