1use crate::alg_builder::{
21 AlgorithmBuilder, HessianApproxChoice, LineSearchChoice, LinearBackendFactory,
22 LinearSolverChoice, MuStrategyChoice,
23};
24use crate::hess::lim_mem_quasi_newton::UpdateType;
25use crate::ipopt_alg::IpoptAlgorithm;
26use crate::ipopt_cq::IpoptCalculatedQuantities;
27use crate::ipopt_data::IpoptData as AlgIpoptData;
28use crate::ipopt_nlp::IpoptNlp;
29use crate::iterates_vector::IteratesVector;
30use crate::restoration::RestorationPhase;
31use crate::upstream_options::register_all_upstream_options;
32
33pub type RestorationFactory = Box<dyn FnMut() -> Box<dyn RestorationPhase>>;
39
40pub type RestorationFactoryProvider = Box<dyn FnMut() -> RestorationFactory>;
48
49pub type ConvergedCallback = Box<
68 dyn FnMut(
69 &crate::ipopt_data::IpoptDataHandle,
70 &crate::ipopt_cq::IpoptCqHandle,
71 &Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
72 Rc<RefCell<crate::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
73 ),
74>;
75use pounce_common::diagnostics::DiagnosticsState;
76use pounce_common::exception::{ExceptionKind, SolverException};
77use pounce_common::journalist::{JournalLevel, Journalist};
78use pounce_common::options_list::OptionsList;
79use pounce_common::reg_options::{PrintOptionsMode, RegisteredOptions};
80use pounce_common::timing::TimingStatistics;
81use pounce_common::types::{Index, Number};
82use pounce_linalg::dense_vector::DenseVectorSpace;
83use pounce_linsol::summary::LinearSolverSummary;
84use pounce_linsol::SparseSymLinearSolverInterface;
85use pounce_nlp::alg_types::SolverReturn;
86use pounce_nlp::orig_ipopt_nlp::{ConstObjScaling, OrigIpoptNlp, ScalingMethod};
87use pounce_nlp::return_codes::ApplicationReturnStatus;
88use pounce_nlp::solve_statistics::SolveStatistics;
89use pounce_nlp::tnlp::{
90 IpoptCq as TnlpIpoptCq, IpoptData as TnlpIpoptData, NlpInfo, Solution, TNLP,
91};
92use pounce_nlp::tnlp_adapter::{
93 FixedVarTreatment, TNLPAdapter, DEFAULT_NLP_LOWER_BOUND_INF, DEFAULT_NLP_UPPER_BOUND_INF,
94};
95use std::cell::RefCell;
96use std::fmt;
97use std::path::Path;
98use std::rc::Rc;
99use std::sync::{Arc, Mutex};
100use std::time::Instant;
101
102pub struct IpoptApplication {
103 options: OptionsList,
104 reg_options: Rc<RegisteredOptions>,
105 journalist: Rc<Journalist>,
106 statistics: RefCell<SolveStatistics>,
107 timing: RefCell<Rc<TimingStatistics>>,
113 linear_backend_factory: Option<LinearBackendFactory>,
117 restoration_factory: Option<RestorationFactory>,
126 diagnostics: Option<Rc<DiagnosticsState>>,
132 debug_hook: Option<std::rc::Rc<std::cell::RefCell<dyn crate::debug::DebugHook>>>,
138 restoration_factory_provider: Option<RestorationFactoryProvider>,
144 on_converged: Option<ConvergedCallback>,
148 record_iter_history: bool,
154 linsol_summary_sink: Arc<Mutex<LinearSolverSummary>>,
164 sqp_warm_start: Option<crate::sqp::SqpIterates>,
170 sqp_last_working_set: Option<pounce_qp::WorkingSet>,
175 warm_start_iterate: Option<crate::debug::IterateSnapshot>,
185}
186
187impl fmt::Debug for IpoptApplication {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 f.debug_struct("IpoptApplication")
190 .field("options", &self.options)
191 .field("statistics", &self.statistics)
192 .finish_non_exhaustive()
193 }
194}
195
196impl Default for IpoptApplication {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202impl IpoptApplication {
203 pub fn new() -> Self {
206 let reg = RegisteredOptions::default();
207 register_all_upstream_options(®)
210 .unwrap_or_else(|e| panic!("Upstream options registration failed: {e}"));
211 pounce_presolve::register_options(®)
212 .unwrap_or_else(|e| panic!("Presolve options registration failed: {e}"));
213 let reg = Rc::new(reg);
214 Self {
215 options: OptionsList::with_registered(Rc::clone(®)),
216 reg_options: reg,
217 journalist: Rc::new(Journalist::new()),
218 statistics: RefCell::new(SolveStatistics::new()),
219 timing: RefCell::new(Rc::new(TimingStatistics::new())),
220 linear_backend_factory: None,
221 restoration_factory: None,
222 diagnostics: None,
223 debug_hook: None,
224 restoration_factory_provider: None,
225 on_converged: None,
226 record_iter_history: false,
227 linsol_summary_sink: Arc::new(Mutex::new(LinearSolverSummary::default())),
228 sqp_warm_start: None,
229 sqp_last_working_set: None,
230 warm_start_iterate: None,
231 }
232 }
233
234 pub fn options(&self) -> &OptionsList {
235 &self.options
236 }
237
238 pub fn options_mut(&mut self) -> &mut OptionsList {
239 &mut self.options
240 }
241
242 pub fn registered_options(&self) -> &Rc<RegisteredOptions> {
243 &self.reg_options
244 }
245
246 pub fn journalist(&self) -> &Rc<Journalist> {
247 &self.journalist
248 }
249
250 pub fn set_linear_backend_factory(&mut self, factory: LinearBackendFactory) {
255 self.linear_backend_factory = Some(factory);
256 }
257
258 pub fn set_restoration_factory(&mut self, factory: RestorationFactory) {
267 self.restoration_factory = Some(factory);
268 }
269
270 pub fn set_diagnostics(&mut self, diag: Rc<DiagnosticsState>) {
275 self.diagnostics = Some(diag);
276 }
277
278 pub fn set_debug_hook(
282 &mut self,
283 hook: std::rc::Rc<std::cell::RefCell<dyn crate::debug::DebugHook>>,
284 ) {
285 self.debug_hook = Some(hook);
286 }
287
288 pub fn diagnostics(&self) -> Option<Rc<DiagnosticsState>> {
292 self.diagnostics.as_ref().map(Rc::clone)
293 }
294
295 pub fn set_restoration_factory_provider(&mut self, provider: RestorationFactoryProvider) {
304 self.restoration_factory_provider = Some(provider);
305 }
306
307 pub fn set_on_converged(&mut self, cb: ConvergedCallback) {
313 self.on_converged = Some(cb);
314 }
315
316 pub fn enable_iter_history(&mut self) {
323 self.record_iter_history = true;
324 }
325
326 pub fn initialize_with_options_file(&mut self, path: &Path) -> Result<(), SolverException> {
329 let txt = std::fs::read_to_string(path).map_err(|e| {
330 SolverException::new(
331 ExceptionKind::IPOPT_APPLICATION_ERROR,
332 format!("could not read options file {}: {}", path.display(), e),
333 file!(),
334 line!() as Index,
335 )
336 })?;
337 self.options.read_from_str(&txt, true)?;
338 self.open_output_file_journal();
339 Ok(())
340 }
341
342 pub fn initialize_with_options_str(&mut self, s: &str) -> Result<(), SolverException> {
345 self.options.read_from_str(s, true)?;
346 self.open_output_file_journal();
347 Ok(())
348 }
349
350 fn open_output_file_journal(&self) {
362 let fname = match self.options.get_string_value("output_file", "") {
363 Ok((v, true)) if !v.is_empty() => v,
364 _ => return,
365 };
366 let level_int = self
367 .options
368 .get_integer_value("file_print_level", "")
369 .ok()
370 .and_then(|(v, f)| f.then_some(v))
371 .unwrap_or(5);
372 let level = journal_level_from_int(level_int);
373 let append = self
374 .options
375 .get_bool_value("file_append", "")
376 .ok()
377 .and_then(|(v, f)| f.then_some(v))
378 .unwrap_or(false);
379 let jname = format!("OutputFile:{}", fname);
380 let _ = self
381 .journalist
382 .add_file_journal(&jname, &fname, level, append);
383 }
384
385 pub fn initialize(&mut self) -> Result<(), SolverException> {
389 Ok(())
390 }
391
392 pub fn open_output_file(&mut self, fname: &str, print_level: i32) -> bool {
398 if self
399 .options
400 .set_string_value("output_file", fname, true, false)
401 .is_err()
402 {
403 return false;
404 }
405 if self
406 .options
407 .set_integer_value("file_print_level", print_level as Index, true, false)
408 .is_err()
409 {
410 return false;
411 }
412 let level = journal_level_from_int(print_level);
413 let jname = format!("OutputFile:{}", fname);
414 self.journalist
419 .add_file_journal(&jname, fname, level, false)
420 .is_some()
421 }
422
423 pub fn problem_dimensions(&self, tnlp: &mut dyn TNLP) -> Option<NlpInfo> {
426 tnlp.get_nlp_info()
427 }
428
429 pub fn statistics(&self) -> SolveStatistics {
430 self.statistics.borrow().clone()
431 }
432
433 pub fn timing_stats(&self) -> Rc<TimingStatistics> {
440 Rc::clone(&self.timing.borrow())
441 }
442
443 pub fn linear_solver_summary(&self) -> Option<LinearSolverSummary> {
450 let guard = self.linsol_summary_sink.lock().ok()?;
451 if guard.is_empty() {
452 None
453 } else {
454 Some(guard.clone())
455 }
456 }
457
458 pub fn optimize_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
471 if self.is_sqp_algorithm_selected() {
476 return self.optimize_sqp_tnlp(tnlp);
477 }
478 let info = match tnlp.borrow_mut().get_nlp_info() {
479 Some(info) => info,
480 None => return ApplicationReturnStatus::InvalidProblemDefinition,
481 };
482 if info.m > 0 && self.is_l1_penalty_enabled() {
492 if let Some(status) = self.run_l1_penalty_outer_loop(Rc::clone(&tnlp)) {
493 return status;
494 }
495 }
499 if info.m > 0 && self.is_l1_fallback_enabled() && !self.is_l1_penalty_enabled() {
506 return self.run_with_l1_fallback(tnlp);
507 }
508 self.optimize_constrained(tnlp)
515 }
516
517 fn is_l1_penalty_enabled(&self) -> bool {
520 self.options
521 .get_bool_value("l1_exact_penalty_barrier", "")
522 .ok()
523 .and_then(|(v, found)| found.then_some(v))
524 .unwrap_or(false)
525 }
526
527 fn l1_penalty_init(&self) -> Number {
528 self.options
529 .get_numeric_value("l1_penalty_init", "")
530 .ok()
531 .and_then(|(v, found)| found.then_some(v))
532 .unwrap_or(1.0)
533 }
534 fn l1_penalty_max(&self) -> Number {
535 self.options
536 .get_numeric_value("l1_penalty_max", "")
537 .ok()
538 .and_then(|(v, found)| found.then_some(v))
539 .unwrap_or(1.0e6)
540 }
541 fn l1_penalty_increase_factor(&self) -> Number {
542 self.options
543 .get_numeric_value("l1_penalty_increase_factor", "")
544 .ok()
545 .and_then(|(v, found)| found.then_some(v))
546 .unwrap_or(8.0)
547 }
548 fn l1_penalty_max_outer_iter(&self) -> usize {
549 self.options
550 .get_integer_value("l1_penalty_max_outer_iter", "")
551 .ok()
552 .and_then(|(v, found)| found.then_some(v))
553 .unwrap_or(8) as usize
554 }
555 fn l1_slack_tol(&self) -> Number {
556 self.options
557 .get_numeric_value("l1_slack_tol", "")
558 .ok()
559 .and_then(|(v, found)| found.then_some(v))
560 .unwrap_or(1.0e-6)
561 }
562 fn l1_steering_factor(&self) -> Number {
563 self.options
564 .get_numeric_value("l1_steering_factor", "")
565 .ok()
566 .and_then(|(v, found)| found.then_some(v))
567 .unwrap_or(10.0)
568 }
569 fn is_l1_fallback_enabled(&self) -> bool {
570 self.options
571 .get_bool_value("l1_fallback_on_restoration_failure", "")
572 .ok()
573 .and_then(|(v, found)| found.then_some(v))
574 .unwrap_or(false)
575 }
576
577 pub fn set_sqp_warm_start(&mut self, warm: crate::sqp::SqpIterates) {
591 self.sqp_warm_start = Some(warm);
592 }
593
594 pub fn clear_sqp_warm_start(&mut self) {
596 self.sqp_warm_start = None;
597 }
598
599 pub fn set_warm_start_iterate(&mut self, snap: crate::debug::IterateSnapshot) {
607 self.warm_start_iterate = Some(snap);
608 }
609
610 pub fn last_sqp_working_set(&self) -> Option<&pounce_qp::WorkingSet> {
615 self.sqp_last_working_set.as_ref()
616 }
617
618 fn is_sqp_algorithm_selected(&self) -> bool {
619 match self.options.get_string_value("algorithm", "") {
620 Ok((v, true)) => v.eq_ignore_ascii_case("active-set-sqp"),
621 _ => false,
622 }
623 }
624
625 fn optimize_sqp_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
632 use pounce_nlp::orig_ipopt_nlp::OrigIpoptNlp;
633 use pounce_nlp::tnlp_adapter::TNLPAdapter;
634 use pounce_nlp::ConstObjScaling;
635
636 let adapter = match TNLPAdapter::new(Rc::clone(&tnlp)) {
637 Ok(a) => Rc::new(RefCell::new(a)),
638 Err(_) => return ApplicationReturnStatus::InvalidProblemDefinition,
639 };
640 let obj_scaling_factor = self
644 .options
645 .get_numeric_value("obj_scaling_factor", "")
646 .ok()
647 .and_then(|(v, f)| f.then_some(v))
648 .unwrap_or(1.0);
649 let orig_nlp = match OrigIpoptNlp::new(
650 Rc::clone(&adapter),
651 Rc::new(ConstObjScaling(obj_scaling_factor)),
652 ) {
653 Ok(n) => n,
654 Err(_) => return ApplicationReturnStatus::InternalError,
655 };
656 let nlp_rc: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(orig_nlp));
657
658 let mut sqp_adapter = crate::sqp::IpoptNlpAdapter::new(Rc::clone(&nlp_rc));
659
660 let mut builder = self.algorithm_builder_snapshot();
661 builder.algorithm = crate::alg_builder::AlgorithmChoice::ActiveSetSqp;
662 let factory = self.make_backend_factory();
663 let mut alg = match builder.build_sqp_with_backend(factory) {
664 Some(a) => a,
665 None => return ApplicationReturnStatus::InternalError,
666 };
667
668 let warm = self.sqp_warm_start.take();
672 let res = match alg.optimize_with_warm_start(&mut sqp_adapter, warm) {
673 Ok(r) => r,
674 Err(e) => {
675 if std::env::var_os("POUNCE_DBG_SQP").is_some() {
676 tracing::warn!(target: "pounce::sqp", "[SQP] optimize_with_warm_start error: {e:?}");
677 }
678 return ApplicationReturnStatus::InternalError;
679 }
680 };
681 self.sqp_last_working_set = res.working_set.clone();
684 {
693 let mut stats = self.statistics.borrow_mut();
694 stats.iteration_count = res.n_iter as Index;
695 stats.final_objective = res.obj;
696 stats.final_dual_inf = res.final_stationarity;
697 stats.final_constr_viol = res.final_constr_viol;
698 stats.final_compl = 0.0; }
700 let (app_status, solver_status) = match res.status {
701 crate::sqp::SqpStatus::Optimal => (
702 ApplicationReturnStatus::SolveSucceeded,
703 pounce_nlp::SolverReturn::Success,
704 ),
705 crate::sqp::SqpStatus::MaxIter => (
706 ApplicationReturnStatus::MaximumIterationsExceeded,
707 pounce_nlp::SolverReturn::MaxiterExceeded,
708 ),
709 crate::sqp::SqpStatus::InfeasibleSubproblem => (
710 ApplicationReturnStatus::InfeasibleProblemDetected,
711 pounce_nlp::SolverReturn::LocalInfeasibility,
712 ),
713 crate::sqp::SqpStatus::LineSearchFailed => (
714 ApplicationReturnStatus::SearchDirectionBecomesTooSmall,
715 pounce_nlp::SolverReturn::ErrorInStepComputation,
716 ),
717 };
718
719 let _ = finalize_via_sqp(&nlp_rc, &res, solver_status, &tnlp);
725
726 app_status
727 }
728
729 fn algorithm_builder_snapshot(&self) -> AlgorithmBuilder {
733 let mut builder = AlgorithmBuilder::default();
734 apply_sqp_options(&self.options, &mut builder.sqp);
735 apply_qp_subproblem_options(&self.options, &mut builder.sqp_qp);
736 builder
737 }
738
739 fn make_backend_factory(&self) -> LinearBackendFactory {
743 Box::new(
744 |_choice| -> Box<dyn pounce_linsol::SparseSymLinearSolverInterface> {
745 Box::new(pounce_feral::FeralSolverInterface::new())
746 },
747 )
748 }
749
750 fn run_with_l1_fallback(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
767 let first_status = self.optimize_constrained(Rc::clone(&tnlp));
770 if !is_l1_fallback_trigger(first_status) {
771 return first_status;
772 }
773 let prev = self
777 .options
778 .get_string_value("l1_exact_penalty_barrier", "")
779 .ok();
780 let _ = self
781 .options
782 .set_string_value("l1_exact_penalty_barrier", "yes", true, false);
783 let retry_status = self
784 .run_l1_penalty_outer_loop(Rc::clone(&tnlp))
785 .unwrap_or(ApplicationReturnStatus::InternalError);
786 let _ = self.options.set_string_value(
787 "l1_exact_penalty_barrier",
788 prev.as_ref().map(|(v, _)| v.as_str()).unwrap_or("no"),
789 true,
790 false,
791 );
792 if matches!(retry_status, ApplicationReturnStatus::SolveSucceeded) {
793 retry_status
794 } else {
795 first_status
796 }
797 }
798
799 fn run_l1_penalty_outer_loop(
820 &mut self,
821 tnlp: Rc<RefCell<dyn TNLP>>,
822 ) -> Option<ApplicationReturnStatus> {
823 let rho_init = self.l1_penalty_init();
824 let rho_max = self.l1_penalty_max().max(rho_init);
825 let factor = self.l1_penalty_increase_factor().max(1.0);
826 let tau = self.l1_steering_factor();
827 let slack_tol = self.l1_slack_tol();
828 let max_outer = self.l1_penalty_max_outer_iter().max(1);
829
830 let mut wrapper = pounce_l1penalty::L1PenaltyBarrierTnlp::new(Rc::clone(&tnlp), rho_init)?;
831 if wrapper.m_eq() == 0 {
832 return None;
835 }
836 wrapper.set_defer_inner_finalize(true);
837 let wrapper_rc = Rc::new(RefCell::new(wrapper));
838
839 let mut rho = rho_init;
840 let mut last_status = ApplicationReturnStatus::InternalError;
841 for _outer in 0..max_outer {
842 wrapper_rc.borrow_mut().set_rho(rho);
843 let dyn_tnlp: Rc<RefCell<dyn TNLP>> = wrapper_rc.clone();
844 last_status = self.optimize_constrained(dyn_tnlp);
845
846 let w = wrapper_rc.borrow();
847 if !w.has_solution() {
848 drop(w);
850 break;
851 }
852 let slack_sum = w.last_slack_sum();
853 let y_eq_inf = w.last_y_eq_inf_norm();
854 drop(w);
855
856 let inner_ok = matches!(
858 last_status,
859 ApplicationReturnStatus::SolveSucceeded
860 | ApplicationReturnStatus::SolvedToAcceptableLevel
861 );
862 if !inner_ok {
863 break;
864 }
865 if slack_sum.is_finite() && slack_sum <= slack_tol {
866 break;
867 }
868 if rho >= rho_max {
869 break;
870 }
871 let geom = rho * factor;
873 let steer = tau * y_eq_inf + 1.0e-12;
874 rho = geom.max(steer).min(rho_max);
875 }
876
877 let w = wrapper_rc.borrow();
879 if w.has_solution() {
880 let x_trunc: Vec<Number> = w.last_x_trunc().to_vec();
881 let lambda: Vec<Number> = w.last_lambda().to_vec();
882 let z_l: Vec<Number> = w.last_z_l_trunc().to_vec();
883 let z_u: Vec<Number> = w.last_z_u_trunc().to_vec();
884 let solver_status = w.last_status().unwrap_or(SolverReturn::InternalError);
885 let slack_sum = w.last_slack_sum();
886 drop(w);
887
888 let infeasible_certificate = matches!(
896 last_status,
897 ApplicationReturnStatus::SolveSucceeded
898 | ApplicationReturnStatus::SolvedToAcceptableLevel
899 ) && slack_sum.is_finite()
900 && slack_sum > slack_tol;
901 let final_app_status = if infeasible_certificate {
902 ApplicationReturnStatus::InfeasibleProblemDetected
903 } else {
904 last_status
905 };
906 let final_solver_status = if infeasible_certificate {
907 SolverReturn::LocalInfeasibility
908 } else {
909 solver_status
910 };
911
912 let f_inner = tnlp
914 .borrow_mut()
915 .eval_f(&x_trunc, true)
916 .unwrap_or(Number::NAN);
917 let m = tnlp
918 .borrow_mut()
919 .get_nlp_info()
920 .map(|i| i.m as usize)
921 .unwrap_or(0);
922 let mut g_inner = vec![0.0; m];
923 if m > 0 {
924 let _ = tnlp.borrow_mut().eval_g(&x_trunc, false, &mut g_inner);
925 }
926 tnlp.borrow_mut().finalize_solution(
927 Solution {
928 status: final_solver_status,
929 x: &x_trunc,
930 z_l: &z_l,
931 z_u: &z_u,
932 g: &g_inner,
933 lambda: &lambda,
934 obj_value: f_inner,
935 },
936 &TnlpIpoptData::default(),
937 &TnlpIpoptCq::default(),
938 );
939 return Some(final_app_status);
940 }
941 Some(last_status)
943 }
944
945 fn optimize_constrained(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
949 let t_start = Instant::now();
950
951 let print_opts = self
955 .options
956 .get_bool_value("print_user_options", "")
957 .ok()
958 .and_then(|(v, f)| f.then_some(v))
959 .unwrap_or(false);
960 if print_opts {
961 print!(
962 "\nList of user-set options:\n\n{}",
963 self.options.print_user_options()
964 );
965 }
966
967 let print_doc = self
977 .options
978 .get_bool_value("print_options_documentation", "")
979 .ok()
980 .and_then(|(v, f)| f.then_some(v))
981 .unwrap_or(false);
982 if print_doc {
983 let mode = self
984 .options
985 .get_string_value("print_options_mode", "")
986 .ok()
987 .map(|(v, _)| PrintOptionsMode::from_tag(&v))
988 .unwrap_or(PrintOptionsMode::Text);
989 let advanced = self
990 .options
991 .get_bool_value("print_advanced_options", "")
992 .ok()
993 .map(|(v, _)| v)
994 .unwrap_or(false);
995 print!(
996 "\n# Pounce options registry\n\n{}",
997 self.reg_options.print_options_documentation(mode, advanced)
998 );
999 }
1000
1001 let timing = Rc::new(TimingStatistics::new());
1007 *self.timing.borrow_mut() = Rc::clone(&timing);
1008 timing.overall_alg.start();
1009
1010 if let Ok(mut guard) = self.linsol_summary_sink.lock() {
1016 *guard = LinearSolverSummary::default();
1017 } else {
1018 debug_assert!(false, "linsol summary sink mutex poisoned");
1019 }
1020
1021 let lo_inf = self
1027 .options
1028 .get_numeric_value("nlp_lower_bound_inf", "")
1029 .ok()
1030 .and_then(|(v, f)| f.then_some(v))
1031 .unwrap_or(DEFAULT_NLP_LOWER_BOUND_INF);
1032 let up_inf = self
1033 .options
1034 .get_numeric_value("nlp_upper_bound_inf", "")
1035 .ok()
1036 .and_then(|(v, f)| f.then_some(v))
1037 .unwrap_or(DEFAULT_NLP_UPPER_BOUND_INF);
1038 let fixed_treatment = match self
1039 .options
1040 .get_string_value("fixed_variable_treatment", "")
1041 .ok()
1042 .and_then(|(v, f)| f.then_some(v))
1043 .as_deref()
1044 {
1045 Some("relax_bounds") => FixedVarTreatment::RelaxBounds,
1046 _ => FixedVarTreatment::MakeParameter,
1050 };
1051 let adapter = match TNLPAdapter::new_with_options(
1052 Rc::clone(&tnlp),
1053 lo_inf,
1054 up_inf,
1055 fixed_treatment,
1056 ) {
1057 Ok(a) => Rc::new(RefCell::new(a)),
1058 Err(_) => {
1059 timing.overall_alg.end();
1060 return ApplicationReturnStatus::InvalidProblemDefinition;
1061 }
1062 };
1063 let obj_scaling_factor = self
1069 .options
1070 .get_numeric_value("obj_scaling_factor", "")
1071 .ok()
1072 .and_then(|(v, f)| f.then_some(v))
1073 .unwrap_or(1.0);
1074 let mut orig_nlp = match OrigIpoptNlp::new(
1075 Rc::clone(&adapter),
1076 Rc::new(ConstObjScaling(obj_scaling_factor)),
1077 ) {
1078 Ok(n) => n,
1079 Err(_) => {
1080 timing.overall_alg.end();
1081 return ApplicationReturnStatus::InternalError;
1082 }
1083 };
1084 orig_nlp.set_timing_stats(Rc::clone(&timing));
1085
1086 let n_x_var = orig_nlp.x_space().dim();
1092 let n_c = orig_nlp.c_space().dim();
1093 if n_x_var > 0 && n_x_var < n_c {
1094 timing.overall_alg.end();
1095 return ApplicationReturnStatus::NotEnoughDegreesOfFreedom;
1096 }
1097
1098 let bound_relax_factor = self
1102 .options
1103 .get_numeric_value("bound_relax_factor", "")
1104 .ok()
1105 .and_then(|(v, f)| f.then_some(v))
1106 .unwrap_or(1e-8);
1107 let constr_viol_tol = self
1108 .options
1109 .get_numeric_value("constr_viol_tol", "")
1110 .ok()
1111 .and_then(|(v, f)| f.then_some(v))
1112 .unwrap_or(1e-4);
1113 orig_nlp.relax_bounds(bound_relax_factor, constr_viol_tol);
1114
1115 let scaling_method = self
1120 .options
1121 .get_string_value("nlp_scaling_method", "")
1122 .ok()
1123 .and_then(|(v, f)| f.then_some(v))
1124 .unwrap_or_else(|| "gradient-based".to_string());
1125 let scaling_method = match scaling_method.as_str() {
1126 "none" => ScalingMethod::None,
1127 "gradient-based" => ScalingMethod::GradientBased,
1128 "user-scaling" => ScalingMethod::UserScaling,
1129 _ => ScalingMethod::GradientBased,
1133 };
1134 let max_gradient = self
1135 .options
1136 .get_numeric_value("nlp_scaling_max_gradient", "")
1137 .ok()
1138 .and_then(|(v, f)| f.then_some(v))
1139 .unwrap_or(100.0);
1140 let min_value = self
1141 .options
1142 .get_numeric_value("nlp_scaling_min_value", "")
1143 .ok()
1144 .and_then(|(v, f)| f.then_some(v))
1145 .unwrap_or(1e-8);
1146 let obj_target_gradient = self
1147 .options
1148 .get_numeric_value("nlp_scaling_obj_target_gradient", "")
1149 .ok()
1150 .and_then(|(v, f)| f.then_some(v))
1151 .unwrap_or(0.0);
1152 let constr_target_gradient = self
1153 .options
1154 .get_numeric_value("nlp_scaling_constr_target_gradient", "")
1155 .ok()
1156 .and_then(|(v, f)| f.then_some(v))
1157 .unwrap_or(0.0);
1158 orig_nlp.determine_scaling_from_starting_point(
1159 scaling_method,
1160 max_gradient,
1161 min_value,
1162 obj_target_gradient,
1163 constr_target_gradient,
1164 );
1165
1166 let nlp_handle: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(orig_nlp));
1167
1168 let builder = self.algorithm_builder_from_options();
1176
1177 let feral_cfg = feral_config_from_options(&self.options);
1183 let factory = self.linear_backend_factory.take().unwrap_or_else(|| {
1184 default_backend_factory_with_sink(feral_cfg, Arc::clone(&self.linsol_summary_sink))
1185 });
1186 let bundle = builder.build_with_backend(factory);
1187
1188 let data: crate::ipopt_data::IpoptDataHandle = Rc::new(RefCell::new(AlgIpoptData::new()));
1194 data.borrow_mut().timing = Rc::clone(&timing);
1195 let cq: crate::ipopt_cq::IpoptCqHandle = Rc::new(RefCell::new(
1196 IpoptCalculatedQuantities::new(Rc::clone(&data), Rc::clone(&nlp_handle)),
1197 ));
1198 if let Ok((v, true)) = self.options.get_numeric_value("slack_move", "") {
1201 cq.borrow_mut().slack_move = v;
1202 }
1203
1204 {
1209 let nlp_borrow = nlp_handle.borrow();
1210 let n_x = nlp_borrow.n();
1211 let n_s = nlp_borrow.m_ineq();
1212 let n_yc = nlp_borrow.m_eq();
1213 let n_yd = nlp_borrow.m_ineq();
1214 let n_zl = nlp_borrow.x_l().dim();
1215 let n_zu = nlp_borrow.x_u().dim();
1216 let n_vl = nlp_borrow.d_l().dim();
1217 let n_vu = nlp_borrow.d_u().dim();
1218 drop(nlp_borrow);
1219 let iv = IteratesVector::new(
1220 Rc::new(DenseVectorSpace::new(n_x).make_new_dense()),
1221 Rc::new(DenseVectorSpace::new(n_s).make_new_dense()),
1222 Rc::new(DenseVectorSpace::new(n_yc).make_new_dense()),
1223 Rc::new(DenseVectorSpace::new(n_yd).make_new_dense()),
1224 Rc::new(DenseVectorSpace::new(n_zl).make_new_dense()),
1225 Rc::new(DenseVectorSpace::new(n_zu).make_new_dense()),
1226 Rc::new(DenseVectorSpace::new(n_vl).make_new_dense()),
1227 Rc::new(DenseVectorSpace::new(n_vu).make_new_dense()),
1228 );
1229 data.borrow_mut().set_curr(iv);
1230 }
1231
1232 if let Some(snap) = self.warm_start_iterate.take() {
1240 let dims_match = {
1241 let borrow = data.borrow();
1242 borrow
1243 .curr
1244 .as_ref()
1245 .map(|c| iterates_dims(c) == iterates_dims(snap.iterates()))
1246 .unwrap_or(false)
1247 };
1248 if dims_match {
1249 data.borrow_mut().set_curr(snap.iterates().clone());
1250 data.borrow_mut().curr_mu = snap.mu();
1251 } else {
1252 tracing::warn!(
1253 target: "pounce::warm_start",
1254 "debugger warm-restart iterate dimensions differ from the fresh \
1255 solve; ignoring the captured iterate and seeding normally"
1256 );
1257 }
1258 }
1259
1260 let max_iter = self
1261 .options
1262 .get_integer_value("max_iter", "")
1263 .ok()
1264 .and_then(|(v, f)| f.then_some(v))
1265 .unwrap_or(3000);
1266 let tol = self
1267 .options
1268 .get_numeric_value("tol", "")
1269 .ok()
1270 .and_then(|(v, f)| f.then_some(v))
1271 .unwrap_or(1e-8);
1272 data.borrow_mut().tol = tol;
1273
1274 let mut alg = IpoptAlgorithm::new(data, cq, bundle)
1275 .with_nlp(Rc::clone(&nlp_handle))
1276 .with_tnlp(Rc::clone(&tnlp));
1277 if let Some(provider) = self.restoration_factory_provider.as_mut() {
1282 self.restoration_factory = Some(provider());
1283 }
1284 if let Some(factory) = self.restoration_factory.as_mut() {
1285 alg = alg.with_restoration(factory());
1286 }
1287 if let Some(diag) = self.diagnostics.as_ref() {
1288 alg = alg.with_diagnostics(Rc::clone(diag));
1289 }
1290 if let Some(hook) = self.debug_hook.take() {
1294 alg = alg.with_debug_hook(hook);
1295 }
1296 alg.max_iter = max_iter;
1297 if let Ok((v, found)) = self.options.get_integer_value("print_level", "") {
1302 if found && v <= 0 {
1303 alg.print_iter_output = false;
1304 if let Some(resto) = alg.restoration.as_mut() {
1308 resto.set_print_iter_output(false);
1309 }
1310 }
1311 }
1312
1313 let iter_capture = self
1324 .record_iter_history
1325 .then(pounce_observability::IterCaptureGuard::start);
1326
1327 let solver_status = alg.optimize();
1328
1329 let captured_iters = iter_capture.map(|g| g.finish()).unwrap_or_default();
1330 timing.overall_alg.end();
1336
1337 {
1339 let mut stats = self.statistics.borrow_mut();
1340 {
1341 let d = alg.data.borrow();
1342 stats.iteration_count = d.iter_count;
1343 stats.final_mu = d.curr_mu;
1347 }
1348 stats.total_wallclock_time_secs = t_start.elapsed().as_secs_f64();
1349 stats.restoration_calls = alg.resto_calls;
1353 stats.restoration_inner_iters = alg.resto_inner_iters;
1354 stats.restoration_outer_iters = alg.resto_outer_iters;
1355 stats.restoration_wall_secs = alg.resto_wall_secs;
1356 stats.iterations = captured_iters;
1357 let curr_x = alg.data.borrow().curr.as_ref().map(|c| c.x.clone());
1365 if let Some(x) = curr_x {
1366 if let Ok(f) = try_eval_curr_f(&nlp_handle, &x) {
1367 stats.final_objective = f;
1368 stats.final_scaled_objective = f;
1369 }
1370 }
1371 let cq = alg.cq.borrow();
1376 stats.final_dual_inf = cq.curr_dual_infeasibility_max();
1377 stats.final_constr_viol = cq.curr_primal_infeasibility_max();
1378 let compl = cq
1383 .curr_compl_x_l()
1384 .amax()
1385 .max(cq.curr_compl_x_u().amax())
1386 .max(cq.curr_compl_s_l().amax())
1387 .max(cq.curr_compl_s_u().amax());
1388 stats.final_compl = compl;
1389 stats.final_kkt_error = cq.curr_nlp_error();
1390 }
1391
1392 let app_status = solver_return_to_app_status(solver_status);
1395
1396 if matches!(
1401 app_status,
1402 ApplicationReturnStatus::SolveSucceeded
1403 | ApplicationReturnStatus::SolvedToAcceptableLevel
1404 ) {
1405 if let Some(cb) = self.on_converged.as_mut() {
1406 if let Some(sd) = alg.search_dir.as_mut() {
1407 let pd = sd.pd_solver_rc();
1408 cb(&alg.data, &alg.cq, &nlp_handle, pd);
1409 }
1410 }
1411 }
1412
1413 match finalize_via_orig_nlp(&nlp_handle, &alg, solver_status, app_status, &tnlp) {
1419 Ok(f_unscaled) => {
1420 self.statistics.borrow_mut().final_objective = f_unscaled;
1421 }
1422 Err(()) => {
1423 }
1426 }
1427
1428 let print_timing = self
1436 .options
1437 .get_bool_value("print_timing_statistics", "")
1438 .ok()
1439 .and_then(|(v, f)| f.then_some(v))
1440 .unwrap_or(false);
1441 if print_timing {
1442 let report = timing.report();
1443 print!("{}", report);
1444 use pounce_common::journalist::{JournalCategory, JournalLevel};
1445 self.journalist.print(
1446 JournalLevel::J_SUMMARY,
1447 JournalCategory::J_TIMING_STATISTICS,
1448 &report,
1449 );
1450 }
1451
1452 app_status
1453 }
1454
1455 pub fn algorithm_builder_from_options(&self) -> AlgorithmBuilder {
1463 let mut builder = AlgorithmBuilder::new();
1464
1465 let mut mehrotra_on = false;
1470 if let Ok((v, found)) = self.options.get_string_value("mehrotra_algorithm", "") {
1471 if found && v == "yes" {
1472 mehrotra_on = true;
1473 builder.mehrotra_algorithm = true;
1474 builder.mu_strategy = MuStrategyChoice::Adaptive;
1475 builder.mu_oracle = crate::mu::adaptive::MuOracleKind::Probing;
1476 builder.line_search.accept_every_trial_step = true;
1482 builder.init.bound_push = 10.0;
1486 builder.init.bound_frac = 0.2;
1487 builder.init.slack_bound_push = 10.0;
1488 builder.init.slack_bound_frac = 0.2;
1489 builder.init.bound_mult_init_val = 10.0;
1490 builder.init.constr_mult_init_max = 0.0;
1491 builder.line_search.alpha_for_y =
1496 crate::line_search::backtracking::AlphaForY::BoundMult;
1497 builder.mu.adaptive_mu_globalization =
1504 crate::mu::adaptive::AdaptiveMuGlobalization::NeverMonotoneMode;
1505 builder.init.least_square_init_primal = true;
1513 }
1514 }
1515
1516 if let Ok((v, found)) = self.options.get_string_value("mu_strategy", "") {
1517 if found {
1518 let parsed = match v.as_str() {
1519 "adaptive" => MuStrategyChoice::Adaptive,
1520 _ => MuStrategyChoice::Monotone,
1521 };
1522 if mehrotra_on && matches!(parsed, MuStrategyChoice::Monotone) {
1523 tracing::warn!(target: "pounce::algorithm",
1527 "pounce: mehrotra_algorithm=yes requires \
1528 mu_strategy=adaptive; ignoring \
1529 mu_strategy=monotone."
1530 );
1531 } else {
1532 builder.mu_strategy = parsed;
1533 }
1534 }
1535 }
1536 if let Ok((v, found)) = self.options.get_string_value("mu_oracle", "") {
1537 if found {
1538 builder.mu_oracle = match v.as_str() {
1539 "loqo" => crate::mu::adaptive::MuOracleKind::Loqo,
1540 "probing" => crate::mu::adaptive::MuOracleKind::Probing,
1541 _ => crate::mu::adaptive::MuOracleKind::QualityFunction,
1542 };
1543 }
1544 }
1545 if let Ok((v, found)) = self
1546 .options
1547 .get_string_value("adaptive_mu_globalization", "")
1548 {
1549 if found {
1550 use crate::mu::adaptive::AdaptiveMuGlobalization;
1551 builder.mu.adaptive_mu_globalization = match v.as_str() {
1552 "kkt-error" => AdaptiveMuGlobalization::KktError,
1553 "never-monotone-mode" => AdaptiveMuGlobalization::NeverMonotoneMode,
1554 _ => AdaptiveMuGlobalization::ObjConstrFilter,
1555 };
1556 }
1557 }
1558 if let Ok((v, found)) = self.options.get_string_value("hessian_approximation", "") {
1559 if found {
1560 builder.hessian_approximation = match v.as_str() {
1561 "limited-memory" => HessianApproxChoice::LimitedMemory,
1562 _ => HessianApproxChoice::Exact,
1563 };
1564 }
1565 }
1566 if let Ok((v, found)) = self
1572 .options
1573 .get_string_value("limited_memory_update_type", "")
1574 {
1575 if found {
1576 builder.limited_memory_update_type = match v.as_str() {
1577 "sr1" => UpdateType::Sr1,
1578 _ => UpdateType::Bfgs,
1579 };
1580 }
1581 }
1582 if let Ok((v, found)) = self
1584 .options
1585 .get_integer_value("limited_memory_max_history", "")
1586 {
1587 if found && v >= 0 {
1588 builder.limited_memory_max_history = v as Index;
1589 }
1590 }
1591 if let Ok((v, found)) = self.options.get_string_value("line_search_method", "") {
1592 if found {
1593 builder.line_search_method = match v.as_str() {
1594 "cg-penalty" => LineSearchChoice::CgPenalty,
1595 "penalty" => LineSearchChoice::Penalty,
1596 _ => LineSearchChoice::Filter,
1597 };
1598 }
1599 }
1600 if let Ok((v, found)) = self.options.get_string_value("accept_every_trial_step", "") {
1603 if found {
1604 builder.line_search.accept_every_trial_step = v == "yes";
1605 }
1606 }
1607 if let Ok((v, found)) = self.options.get_string_value("alpha_for_y", "") {
1610 if found {
1611 use crate::line_search::backtracking::AlphaForY;
1612 builder.line_search.alpha_for_y = match v.as_str() {
1613 "primal" => AlphaForY::Primal,
1614 "bound-mult" | "bound_mult" => AlphaForY::BoundMult,
1615 "full" => AlphaForY::Full,
1616 "min" => AlphaForY::Min,
1617 "max" => AlphaForY::Max,
1618 "primal-and-full" | "dual-and-full" => AlphaForY::Primal,
1619 _ => AlphaForY::Primal,
1620 };
1621 }
1622 }
1623 if let Ok((v, _found)) = self.options.get_string_value("linear_solver", "") {
1635 builder.linear_solver = match v.as_str() {
1636 "ma57" => LinearSolverChoice::Ma57,
1637 _ => LinearSolverChoice::Feral,
1638 };
1639 }
1640
1641 if let Ok((v, found)) = self.options.get_string_value("linear_system_scaling", "") {
1650 if found {
1651 builder.linear_system_scaling = match v.as_str() {
1652 "ruiz" => crate::alg_builder::LinearSystemScalingChoice::Ruiz,
1653 "mc19" => crate::alg_builder::LinearSystemScalingChoice::Mc19,
1654 _ => crate::alg_builder::LinearSystemScalingChoice::None,
1655 };
1656 }
1657 }
1658 if let Ok((v, found)) = self.options.get_bool_value("linear_scaling_on_demand", "") {
1659 if found {
1660 builder.linear_scaling_on_demand = v;
1661 }
1662 }
1663
1664 let read_num = |key: &str| -> Option<f64> {
1668 self.options
1669 .get_numeric_value(key, "")
1670 .ok()
1671 .and_then(|(v, f)| f.then_some(v))
1672 };
1673 let read_int = |key: &str| -> Option<i32> {
1674 self.options
1675 .get_integer_value(key, "")
1676 .ok()
1677 .and_then(|(v, f)| f.then_some(v))
1678 };
1679 if let Some(v) = read_num("tol") {
1680 builder.conv_check.tol = v;
1681 }
1682 if let Some(v) = read_num("dual_inf_tol") {
1683 builder.conv_check.dual_inf_tol = v;
1684 }
1685 if let Some(v) = read_num("constr_viol_tol") {
1686 builder.conv_check.constr_viol_tol = v;
1687 }
1688 if let Some(v) = read_num("compl_inf_tol") {
1689 builder.conv_check.compl_inf_tol = v;
1690 }
1691 if let Some(v) = read_int("max_iter") {
1692 builder.conv_check.max_iter = v;
1693 }
1694 if let Some(v) = read_num("max_cpu_time") {
1695 builder.conv_check.max_cpu_time = v;
1696 }
1697 if let Some(v) = read_num("max_wall_time") {
1698 builder.conv_check.max_wall_time = v;
1699 }
1700 if let Some(v) = read_num("acceptable_tol") {
1701 builder.conv_check.acceptable_tol = v;
1702 }
1703 if let Some(v) = read_num("acceptable_dual_inf_tol") {
1704 builder.conv_check.acceptable_dual_inf_tol = v;
1705 }
1706 if let Some(v) = read_num("acceptable_constr_viol_tol") {
1707 builder.conv_check.acceptable_constr_viol_tol = v;
1708 }
1709 if let Some(v) = read_num("acceptable_compl_inf_tol") {
1710 builder.conv_check.acceptable_compl_inf_tol = v;
1711 }
1712 if let Some(v) = read_num("acceptable_obj_change_tol") {
1713 builder.conv_check.acceptable_obj_change_tol = v;
1714 }
1715 if let Some(v) = read_int("acceptable_iter") {
1716 builder.conv_check.acceptable_iter = v;
1717 }
1718 if let Some(v) = read_num("infeas_stationarity_tol") {
1719 builder.conv_check.infeas_stationarity_tol = v;
1720 }
1721 if let Some(v) = read_num("infeas_viol_kappa") {
1722 builder.conv_check.infeas_viol_kappa = v;
1723 }
1724 if let Some(v) = read_int("infeas_max_streak") {
1725 builder.conv_check.infeas_max_streak = v;
1726 }
1727
1728 if let Some(v) = read_num("mu_init") {
1733 builder.mu.mu_init = v;
1734 }
1735 if let Some(v) = read_num("mu_max") {
1736 builder.mu.mu_max = v;
1737 }
1738 if let Some(v) = read_num("mu_max_fact") {
1739 builder.mu.mu_max_fact = v;
1740 }
1741 if let Some(v) = read_num("mu_min") {
1742 builder.mu.mu_min = v;
1743 }
1744 if let Some(v) = read_num("mu_target") {
1745 builder.mu.mu_target = v;
1746 }
1747 if let Some(v) = read_num("mu_linear_decrease_factor") {
1748 builder.mu.mu_linear_decrease_factor = v;
1749 }
1750 if let Some(v) = read_num("mu_superlinear_decrease_power") {
1751 builder.mu.mu_superlinear_decrease_power = v;
1752 }
1753 if let Ok((v, found)) = self
1754 .options
1755 .get_string_value("mu_allow_fast_monotone_decrease", "")
1756 {
1757 if found {
1758 builder.mu.mu_allow_fast_monotone_decrease = v == "yes";
1759 }
1760 }
1761 if let Some(v) = read_num("barrier_tol_factor") {
1762 builder.mu.barrier_tol_factor = v;
1763 }
1764 if let Some(v) = read_num("sigma_max") {
1765 builder.mu.sigma_max = v;
1766 }
1767 if let Some(v) = read_num("sigma_min") {
1768 builder.mu.sigma_min = v;
1769 }
1770
1771 if let Ok((v, found)) = self
1775 .options
1776 .get_string_value("quality_function_norm_type", "")
1777 {
1778 if found {
1779 use crate::mu::oracle::quality_function::NormType;
1780 builder.mu.quality_function_norm_type = match v.as_str() {
1781 "1-norm" => NormType::OneNorm,
1782 "2-norm" => NormType::TwoNorm,
1783 "max-norm" => NormType::MaxNorm,
1784 _ => NormType::TwoNormSquared,
1785 };
1786 }
1787 }
1788 if let Ok((v, found)) = self
1789 .options
1790 .get_string_value("quality_function_centrality", "")
1791 {
1792 if found {
1793 use crate::mu::oracle::quality_function::CentralityType;
1794 builder.mu.quality_function_centrality = match v.as_str() {
1795 "log" => CentralityType::LogCenter,
1796 "reciprocal" => CentralityType::ReciprocalCenter,
1797 "cubed-reciprocal" => CentralityType::CubedReciprocalCenter,
1798 _ => CentralityType::None,
1799 };
1800 }
1801 }
1802 if let Ok((v, found)) = self
1803 .options
1804 .get_string_value("quality_function_balancing_term", "")
1805 {
1806 if found {
1807 use crate::mu::oracle::quality_function::BalancingTermType;
1808 builder.mu.quality_function_balancing_term = match v.as_str() {
1809 "cubic" => BalancingTermType::CubicTerm,
1810 _ => BalancingTermType::None,
1811 };
1812 }
1813 }
1814 if let Some(v) = read_int("quality_function_max_section_steps") {
1815 builder.mu.quality_function_max_section_steps = v;
1816 }
1817 if let Some(v) = read_num("quality_function_section_sigma_tol") {
1818 builder.mu.quality_function_section_sigma_tol = v;
1819 }
1820 if let Some(v) = read_num("quality_function_section_qf_tol") {
1821 builder.mu.quality_function_section_qf_tol = v;
1822 }
1823
1824 if let Some(v) = read_num("probing_iterate_quality_factor") {
1832 builder.mu.probing_iterate_quality_factor = v;
1833 }
1834
1835 if let Some(v) = read_num("adaptive_mu_safeguard_factor") {
1839 builder.mu.adaptive_mu_safeguard_factor = v;
1840 }
1841 if let Some(v) = read_num("adaptive_mu_monotone_init_factor") {
1842 builder.mu.adaptive_mu_monotone_init_factor = v;
1843 }
1844 if let Ok((v, found)) = self
1845 .options
1846 .get_bool_value("adaptive_mu_restore_previous_iterate", "")
1847 {
1848 if found {
1849 builder.mu.adaptive_mu_restore_previous_iterate = v;
1850 }
1851 }
1852 if let Some(v) = read_int("adaptive_mu_kkterror_red_iters") {
1853 if v >= 0 {
1854 builder.mu.adaptive_mu_kkterror_red_iters = v as usize;
1855 }
1856 }
1857 if let Some(v) = read_num("adaptive_mu_kkterror_red_fact") {
1858 builder.mu.adaptive_mu_kkterror_red_fact = v;
1859 }
1860 if let Ok((v, found)) = self
1861 .options
1862 .get_string_value("adaptive_mu_kkt_norm_type", "")
1863 {
1864 if found {
1865 use crate::mu::adaptive::AdaptiveMuKktNorm;
1866 builder.mu.adaptive_mu_kkt_norm_type = match v.as_str() {
1867 "1-norm" => AdaptiveMuKktNorm::OneNorm,
1868 "2-norm" => AdaptiveMuKktNorm::TwoNorm,
1869 "max-norm" => AdaptiveMuKktNorm::MaxNorm,
1870 _ => AdaptiveMuKktNorm::TwoNormSquared,
1871 };
1872 }
1873 }
1874
1875 if let Some(v) = read_int("watchdog_shortened_iter_trigger") {
1879 builder.line_search.watchdog_shortened_iter_trigger = v;
1880 }
1881 if let Some(v) = read_int("watchdog_trial_iter_max") {
1882 builder.line_search.watchdog_trial_iter_max = v;
1883 }
1884 if let Some(v) = read_num("soft_resto_pderror_reduction_factor") {
1885 builder.line_search.soft_resto_pderror_reduction_factor = v;
1886 }
1887 if let Some(v) = read_int("max_soft_resto_iters") {
1888 builder.line_search.max_soft_resto_iters = v;
1889 }
1890
1891 if let Some(v) = read_int("print_frequency_iter") {
1893 builder.output.print_frequency_iter = v;
1894 }
1895 if let Some(v) = read_num("print_frequency_time") {
1896 builder.output.print_frequency_time = v;
1897 }
1898 if let Ok((v, found)) = self.options.get_bool_value("print_info_string", "") {
1899 if found {
1900 builder.output.print_info_string = v;
1901 }
1902 }
1903 if let Ok((v, found)) = self.options.get_string_value("inf_pr_output", "") {
1904 if found {
1905 builder.output.inf_pr_output_internal = v == "internal";
1906 }
1907 }
1908
1909 if let Ok((v, found)) = self.options.get_bool_value("warm_start_init_point", "") {
1915 if found {
1916 builder.warm_start_init_point = v;
1917 }
1918 }
1919 if let Ok((v, found)) = self.options.get_bool_value("warm_start_same_structure", "") {
1920 if found {
1921 builder.warm.same_structure = v;
1922 }
1923 }
1924 if let Some(v) = read_num("warm_start_bound_push") {
1925 builder.warm.bound_push = v;
1926 }
1927 if let Some(v) = read_num("warm_start_bound_frac") {
1928 builder.warm.bound_frac = v;
1929 }
1930 if let Some(v) = read_num("warm_start_slack_bound_push") {
1931 builder.warm.slack_bound_push = v;
1932 }
1933 if let Some(v) = read_num("warm_start_slack_bound_frac") {
1934 builder.warm.slack_bound_frac = v;
1935 }
1936 if let Some(v) = read_num("warm_start_mult_bound_push") {
1937 builder.warm.mult_bound_push = v;
1938 }
1939 if let Some(v) = read_num("warm_start_mult_init_max") {
1940 builder.warm.mult_init_max = v;
1941 }
1942 if let Some(v) = read_num("warm_start_target_mu") {
1943 builder.warm.target_mu = v;
1944 }
1945 if let Ok((v, found)) = self
1946 .options
1947 .get_string_value("warm_start_entire_iterate", "")
1948 {
1949 if found {
1950 builder.warm.entire_iterate = v == "yes";
1951 }
1952 }
1953
1954 if let Some(v) = read_num("bound_push") {
1958 builder.init.bound_push = v;
1959 }
1960 if let Some(v) = read_num("bound_frac") {
1961 builder.init.bound_frac = v;
1962 }
1963 if let Some(v) = read_num("slack_bound_push") {
1964 builder.init.slack_bound_push = v;
1965 }
1966 if let Some(v) = read_num("slack_bound_frac") {
1967 builder.init.slack_bound_frac = v;
1968 }
1969 if let Some(v) = read_num("constr_mult_init_max") {
1970 builder.init.constr_mult_init_max = v;
1971 }
1972 if let Some(v) = read_num("bound_mult_init_val") {
1973 builder.init.bound_mult_init_val = v;
1974 }
1975 if let Ok((v, found)) = self.options.get_string_value("bound_mult_init_method", "") {
1976 if found {
1977 builder.init.bound_mult_init_method = v;
1978 }
1979 }
1980 if let Ok((v, found)) = self
1981 .options
1982 .get_string_value("least_square_init_primal", "")
1983 {
1984 if found {
1985 builder.init.least_square_init_primal = v == "yes";
1986 }
1987 }
1988 builder
1989 }
1990}
1991
1992fn iterates_dims(c: &IteratesVector) -> [i32; 8] {
1999 [
2000 c.x.dim(),
2001 c.s.dim(),
2002 c.y_c.dim(),
2003 c.y_d.dim(),
2004 c.z_l.dim(),
2005 c.z_u.dim(),
2006 c.v_l.dim(),
2007 c.v_u.dim(),
2008 ]
2009}
2010
2011fn journal_level_from_int(v: i32) -> JournalLevel {
2012 match v.clamp(0, 12) {
2013 0 => JournalLevel::J_NONE,
2014 1 => JournalLevel::J_ERROR,
2015 2 => JournalLevel::J_STRONGWARNING,
2016 3 => JournalLevel::J_SUMMARY,
2017 4 => JournalLevel::J_WARNING,
2018 5 => JournalLevel::J_ITERSUMMARY,
2019 6 => JournalLevel::J_DETAILED,
2020 7 => JournalLevel::J_MOREDETAILED,
2021 8 => JournalLevel::J_VECTOR,
2022 9 => JournalLevel::J_MOREVECTOR,
2023 10 => JournalLevel::J_MATRIX,
2024 11 => JournalLevel::J_MOREMATRIX,
2025 _ => JournalLevel::J_ALL,
2026 }
2027}
2028
2029pub fn default_backend_factory(feral_cfg: pounce_feral::FeralConfig) -> LinearBackendFactory {
2038 Box::new(
2039 move |choice: LinearSolverChoice| -> Box<dyn SparseSymLinearSolverInterface> {
2040 match choice {
2041 LinearSolverChoice::Feral => Box::new(
2042 pounce_feral::FeralSolverInterface::with_config(feral_cfg.clone()),
2043 ),
2044 LinearSolverChoice::Ma57 => {
2045 #[cfg(feature = "ma57")]
2046 {
2047 Box::new(pounce_hsl::Ma57SolverInterface::new())
2048 }
2049 #[cfg(not(feature = "ma57"))]
2050 {
2051 Box::new(pounce_feral::FeralSolverInterface::with_config(
2053 feral_cfg.clone(),
2054 ))
2055 }
2056 }
2057 }
2058 },
2059 )
2060}
2061
2062pub fn default_backend_factory_with_sink(
2069 feral_cfg: pounce_feral::FeralConfig,
2070 sink: Arc<Mutex<LinearSolverSummary>>,
2071) -> LinearBackendFactory {
2072 Box::new(
2073 move |choice: LinearSolverChoice| -> Box<dyn SparseSymLinearSolverInterface> {
2074 match choice {
2075 LinearSolverChoice::Feral => Box::new(
2076 pounce_feral::FeralSolverInterface::with_config(feral_cfg.clone())
2077 .with_summary_sink(Arc::clone(&sink)),
2078 ),
2079 LinearSolverChoice::Ma57 => {
2080 #[cfg(feature = "ma57")]
2081 {
2082 Box::new(pounce_hsl::Ma57SolverInterface::new())
2083 }
2084 #[cfg(not(feature = "ma57"))]
2085 {
2086 Box::new(
2087 pounce_feral::FeralSolverInterface::with_config(feral_cfg.clone())
2088 .with_summary_sink(Arc::clone(&sink)),
2089 )
2090 }
2091 }
2092 }
2093 },
2094 )
2095}
2096
2097pub fn feral_config_from_options(
2103 options: &pounce_common::options_list::OptionsList,
2104) -> pounce_feral::FeralConfig {
2105 let mut cfg = pounce_feral::FeralConfig::from_env();
2106 if let Ok((v, true)) = options.get_bool_value("feral_cascade_break", "") {
2113 cfg.cascade_break = Some(v);
2114 }
2115 if let Ok((v, true)) = options.get_bool_value("feral_fma", "") {
2116 cfg.fma = v;
2117 }
2118 if let Ok((v, true)) = options.get_bool_value("feral_refine", "") {
2119 cfg.refine = v;
2120 }
2121 if let Ok((v, true)) = options.get_numeric_value("feral_singular_pivot_floor", "") {
2122 cfg.singular_pivot_floor = v;
2123 }
2124 if let Ok((v, true)) = options.get_numeric_value("feral_pivtol", "") {
2125 cfg.pivtol = v;
2126 }
2127 if let Ok((v, true)) = options.get_string_value("feral_ordering", "") {
2132 if let Some(m) = pounce_feral::parse_ordering_method(&v) {
2133 cfg.ordering = m;
2134 }
2135 }
2136 if let Ok((v, true)) = options.get_string_value("feral_scaling", "") {
2140 if let Some(s) = pounce_feral::parse_scaling_strategy(&v) {
2141 cfg.scaling = s;
2142 }
2143 }
2144 cfg
2145}
2146
2147fn solver_return_to_app_status(s: SolverReturn) -> ApplicationReturnStatus {
2153 match s {
2154 SolverReturn::Success => ApplicationReturnStatus::SolveSucceeded,
2155 SolverReturn::StopAtAcceptablePoint => ApplicationReturnStatus::SolvedToAcceptableLevel,
2156 SolverReturn::FeasiblePointFound => ApplicationReturnStatus::FeasiblePointFound,
2157 SolverReturn::MaxiterExceeded => ApplicationReturnStatus::MaximumIterationsExceeded,
2158 SolverReturn::CpuTimeExceeded => ApplicationReturnStatus::MaximumCpuTimeExceeded,
2159 SolverReturn::WallTimeExceeded => ApplicationReturnStatus::MaximumWallTimeExceeded,
2160 SolverReturn::StopAtTinyStep => ApplicationReturnStatus::SearchDirectionBecomesTooSmall,
2161 SolverReturn::LocalInfeasibility => ApplicationReturnStatus::InfeasibleProblemDetected,
2162 SolverReturn::UserRequestedStop => ApplicationReturnStatus::UserRequestedStop,
2163 SolverReturn::DivergingIterates => ApplicationReturnStatus::DivergingIterates,
2164 SolverReturn::RestorationFailure => ApplicationReturnStatus::RestorationFailed,
2165 SolverReturn::ErrorInStepComputation => ApplicationReturnStatus::ErrorInStepComputation,
2166 SolverReturn::InvalidNumberDetected => ApplicationReturnStatus::InvalidNumberDetected,
2167 SolverReturn::TooFewDegreesOfFreedom => ApplicationReturnStatus::NotEnoughDegreesOfFreedom,
2168 SolverReturn::InvalidOption => ApplicationReturnStatus::InvalidOption,
2169 SolverReturn::OutOfMemory => ApplicationReturnStatus::InsufficientMemory,
2170 SolverReturn::InternalError | SolverReturn::Unassigned => {
2171 ApplicationReturnStatus::InternalError
2172 }
2173 }
2174}
2175
2176fn try_eval_curr_f(
2180 nlp: &Rc<RefCell<dyn IpoptNlp>>,
2181 x: &Rc<dyn pounce_linalg::Vector>,
2182) -> Result<Number, ()> {
2183 let mut nlp_mut = nlp.borrow_mut();
2184 Ok(nlp_mut.eval_f(&**x))
2185}
2186
2187fn is_l1_fallback_trigger(status: ApplicationReturnStatus) -> bool {
2194 matches!(
2195 status,
2196 ApplicationReturnStatus::RestorationFailed
2197 | ApplicationReturnStatus::InfeasibleProblemDetected
2198 | ApplicationReturnStatus::SolvedToAcceptableLevel
2199 | ApplicationReturnStatus::MaximumIterationsExceeded
2200 | ApplicationReturnStatus::NotEnoughDegreesOfFreedom
2201 )
2202}
2203
2204fn finalize_via_orig_nlp(
2215 nlp: &Rc<RefCell<dyn IpoptNlp>>,
2216 alg: &IpoptAlgorithm,
2217 solver_status: SolverReturn,
2218 _app_status: ApplicationReturnStatus,
2219 tnlp: &Rc<RefCell<dyn TNLP>>,
2220) -> Result<Number, ()> {
2221 let curr = alg.data.borrow().curr.clone().ok_or(())?;
2222 let nlp_borrow = nlp.borrow();
2226 let x_vec: Vec<Number> = nlp_borrow.lift_x_to_full(&*curr.x);
2227 let info = tnlp.borrow_mut().get_nlp_info().ok_or(())?;
2228 let n = info.n as usize;
2229 let m = info.m as usize;
2230 debug_assert_eq!(x_vec.len(), n);
2231 let mut z_l = nlp_borrow.finalize_solution_z_l(&*curr.z_l);
2242 if z_l.is_empty() {
2243 z_l = vec![0.0; n];
2244 }
2245 let mut z_u = nlp_borrow.finalize_solution_z_u(&*curr.z_u);
2246 if z_u.is_empty() {
2247 z_u = vec![0.0; n];
2248 }
2249 let mut lambda = nlp_borrow.finalize_solution_lambda(&*curr.y_c, &*curr.y_d);
2250 if lambda.is_empty() {
2251 lambda = vec![0.0; m];
2252 }
2253 drop(nlp_borrow);
2254 let mut g_final = vec![0.0; m];
2257 let _ = tnlp.borrow_mut().eval_g(&x_vec, true, &mut g_final);
2258 let f_final = tnlp
2259 .borrow_mut()
2260 .eval_f(&x_vec, true)
2261 .unwrap_or(Number::NAN);
2262 tnlp.borrow_mut().finalize_solution(
2263 Solution {
2264 status: solver_status,
2265 x: &x_vec,
2266 z_l: &z_l,
2267 z_u: &z_u,
2268 g: &g_final,
2269 lambda: &lambda,
2270 obj_value: f_final,
2271 },
2272 &TnlpIpoptData::default(),
2273 &TnlpIpoptCq::default(),
2274 );
2275 Ok(f_final)
2276}
2277
2278fn apply_sqp_options(options: &OptionsList, opts: &mut crate::sqp::SqpOptions) {
2286 use crate::sqp::{SqpGlobalization, SqpHessianSource};
2287
2288 if let Ok((s, true)) = options.get_string_value("sqp_globalization", "") {
2289 opts.globalization = match s.as_str() {
2290 "filter" => SqpGlobalization::Filter,
2291 "l1-elastic" => SqpGlobalization::L1Elastic,
2292 _ => opts.globalization,
2293 };
2294 }
2295 if let Ok((s, true)) = options.get_string_value("sqp_hessian", "") {
2296 opts.hessian = match s.as_str() {
2297 "exact" => SqpHessianSource::Exact,
2298 "damped-bfgs" => SqpHessianSource::DampedBfgs,
2299 "lbfgs" => SqpHessianSource::Lbfgs,
2300 _ => opts.hessian,
2301 };
2302 }
2303 if let Ok((v, true)) = options.get_integer_value("sqp_max_iter", "") {
2304 if v >= 0 {
2305 opts.max_iter = v as u32;
2306 }
2307 }
2308 if let Ok((v, true)) = options.get_numeric_value("sqp_tol", "") {
2309 opts.tol = v;
2310 }
2311 if let Ok((v, true)) = options.get_numeric_value("sqp_constr_viol_tol", "") {
2312 opts.constr_viol_tol = v;
2313 }
2314 if let Ok((v, true)) = options.get_numeric_value("sqp_dual_inf_tol", "") {
2315 opts.dual_inf_tol = v;
2316 }
2317 if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty", "") {
2318 opts.l1_penalty = v;
2319 }
2320 if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty_safety", "") {
2321 opts.l1_penalty_safety = v;
2322 }
2323 if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty_max", "") {
2324 opts.l1_penalty_max = v;
2325 }
2326 if let Ok((v, true)) = options.get_numeric_value("sqp_bt_reduction", "") {
2327 opts.bt_reduction = v;
2328 }
2329 if let Ok((v, true)) = options.get_numeric_value("sqp_bt_min_alpha", "") {
2330 opts.bt_min_alpha = v;
2331 }
2332 if let Ok((v, true)) = options.get_integer_value("sqp_print_level", "") {
2333 opts.print_level = v.clamp(0, u8::MAX as i32) as u8;
2334 }
2335 if let Ok((v, true)) = options.get_integer_value("sqp_lbfgs_max_history", "") {
2336 if v >= 1 {
2337 opts.lbfgs_max_history = v as u32;
2338 }
2339 }
2340}
2341
2342fn apply_qp_subproblem_options(options: &OptionsList, opts: &mut pounce_qp::QpOptions) {
2352 use pounce_qp::AntiCyclingChoice;
2353
2354 if let Ok((v, true)) = options.get_integer_value("sqp_qp_max_iter", "") {
2355 if v >= 0 {
2356 opts.max_iter = v as u32;
2357 }
2358 }
2359 if let Ok((v, true)) = options.get_numeric_value("sqp_qp_feas_tol", "") {
2360 opts.feas_tol = v;
2361 }
2362 if let Ok((v, true)) = options.get_numeric_value("sqp_qp_opt_tol", "") {
2363 opts.opt_tol = v;
2364 }
2365 if let Ok((v, true)) = options.get_numeric_value("sqp_qp_elastic_gamma", "") {
2366 opts.elastic_gamma = v;
2367 }
2368 if let Ok((s, true)) = options.get_string_value("sqp_qp_anti_cycling", "") {
2369 opts.anti_cycling = match s.as_str() {
2370 "expand" => AntiCyclingChoice::Expand,
2371 "bland" => AntiCyclingChoice::Bland,
2372 "none" => AntiCyclingChoice::None,
2373 _ => opts.anti_cycling,
2374 };
2375 }
2376}
2377
2378fn finalize_via_sqp(
2386 nlp: &Rc<RefCell<dyn IpoptNlp>>,
2387 res: &crate::sqp::SqpResult,
2388 solver_status: pounce_nlp::SolverReturn,
2389 tnlp: &Rc<RefCell<dyn TNLP>>,
2390) -> Result<Number, ()> {
2391 use pounce_linalg::dense_vector::DenseVectorSpace;
2392
2393 let info = tnlp.borrow_mut().get_nlp_info().ok_or(())?;
2394 let n = info.n as usize;
2395 let m = info.m as usize;
2396
2397 let nlp_borrow = nlp.borrow();
2400 let n_alg = nlp_borrow.n() as usize;
2401 let m_eq = nlp_borrow.m_eq() as usize;
2402 let m_ineq = nlp_borrow.m_ineq() as usize;
2403 debug_assert_eq!(res.x.len(), n_alg);
2404 debug_assert_eq!(res.lambda_g.len(), m_eq + m_ineq);
2405 debug_assert_eq!(res.lambda_x.len(), n_alg);
2406
2407 let x_space = DenseVectorSpace::new(n_alg as Index);
2408 let c_space = DenseVectorSpace::new(m_eq as Index);
2409 let d_space = DenseVectorSpace::new(m_ineq as Index);
2410
2411 let mut x_dv = x_space.make_new_dense();
2412 x_dv.set_values(&res.x);
2413 let x_vec: Vec<Number> = nlp_borrow.lift_x_to_full(&x_dv);
2414 debug_assert_eq!(x_vec.len(), n);
2415
2416 let mut z_l_compressed = x_space.make_new_dense();
2418 let mut z_u_compressed = x_space.make_new_dense();
2419 let zl_vals: Vec<Number> = res.lambda_x.iter().map(|v| v.max(0.0)).collect();
2420 let zu_vals: Vec<Number> = res.lambda_x.iter().map(|v| (-v).max(0.0)).collect();
2421 z_l_compressed.set_values(&zl_vals);
2422 z_u_compressed.set_values(&zu_vals);
2423 let mut z_l = nlp_borrow.finalize_solution_z_l(&z_l_compressed);
2426 if z_l.is_empty() {
2427 z_l = vec![0.0; n];
2428 }
2429 let mut z_u = nlp_borrow.finalize_solution_z_u(&z_u_compressed);
2430 if z_u.is_empty() {
2431 z_u = vec![0.0; n];
2432 }
2433
2434 let mut y_c_dv = c_space.make_new_dense();
2436 let mut y_d_dv = d_space.make_new_dense();
2437 if m_eq > 0 {
2438 y_c_dv.set_values(&res.lambda_g[..m_eq]);
2439 }
2440 if m_ineq > 0 {
2441 y_d_dv.set_values(&res.lambda_g[m_eq..]);
2442 }
2443 let mut lambda = nlp_borrow.finalize_solution_lambda(&y_c_dv, &y_d_dv);
2444 if lambda.is_empty() {
2445 lambda = vec![0.0; m];
2446 }
2447 drop(nlp_borrow);
2448
2449 let mut g_final = vec![0.0; m];
2450 let _ = tnlp.borrow_mut().eval_g(&x_vec, true, &mut g_final);
2451 let f_final = tnlp
2452 .borrow_mut()
2453 .eval_f(&x_vec, true)
2454 .unwrap_or(Number::NAN);
2455 tnlp.borrow_mut().finalize_solution(
2456 pounce_nlp::tnlp::Solution {
2457 status: solver_status,
2458 x: &x_vec,
2459 z_l: &z_l,
2460 z_u: &z_u,
2461 g: &g_final,
2462 lambda: &lambda,
2463 obj_value: f_final,
2464 },
2465 &TnlpIpoptData::default(),
2466 &TnlpIpoptCq::default(),
2467 );
2468 Ok(f_final)
2469}
2470
2471#[cfg(test)]
2472mod tests {
2473 use super::*;
2474 use pounce_nlp::tnlp::{
2475 BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest,
2476 StartingPoint,
2477 };
2478
2479 struct Hs071Stub;
2480 impl TNLP for Hs071Stub {
2481 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
2482 Some(NlpInfo {
2485 n: 4,
2486 m: 2,
2487 nnz_jac_g: 8,
2488 nnz_h_lag: 10,
2489 index_style: IndexStyle::C,
2490 })
2491 }
2492 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
2493 b.x_l.copy_from_slice(&[1.0; 4]);
2494 b.x_u.copy_from_slice(&[5.0; 4]);
2495 b.g_l.copy_from_slice(&[25.0, 40.0]);
2496 b.g_u.copy_from_slice(&[2.0e19, 40.0]);
2497 true
2498 }
2499 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2500 sp.x.copy_from_slice(&[1.0, 5.0, 5.0, 1.0]);
2501 true
2502 }
2503 fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
2504 Some(x[0] * x[3] * (x[0] + x[1] + x[2]) + x[2])
2505 }
2506 fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
2507 grad.fill(0.0);
2508 true
2509 }
2510 fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
2511 g.fill(0.0);
2512 true
2513 }
2514 fn eval_jac_g(
2515 &mut self,
2516 _x: Option<&[Number]>,
2517 _new_x: bool,
2518 mode: SparsityRequest<'_>,
2519 ) -> bool {
2520 if let SparsityRequest::Structure { irow, jcol } = mode {
2521 irow.copy_from_slice(&[0, 0, 0, 0, 1, 1, 1, 1]);
2522 jcol.copy_from_slice(&[0, 1, 2, 3, 0, 1, 2, 3]);
2523 }
2524 true
2525 }
2526 fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
2527 }
2528
2529 #[test]
2530 fn application_default_does_not_select_sqp() {
2531 let mut app = IpoptApplication::new();
2532 app.initialize().unwrap();
2533 assert!(!app.is_sqp_algorithm_selected());
2534 }
2535
2536 #[test]
2537 fn application_routes_to_sqp_when_algorithm_option_set() {
2538 let mut app = IpoptApplication::new();
2539 app.initialize().unwrap();
2540 app.initialize_with_options_str("algorithm active-set-sqp\n")
2541 .unwrap();
2542 assert!(app.is_sqp_algorithm_selected());
2543 }
2544
2545 struct ConvexEqTnlp {
2552 finalize_called: std::rc::Rc<std::cell::RefCell<Option<(Vec<Number>, Number)>>>,
2553 }
2554 impl TNLP for ConvexEqTnlp {
2555 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
2556 Some(NlpInfo {
2557 n: 2,
2558 m: 1,
2559 nnz_jac_g: 2,
2560 nnz_h_lag: 2,
2561 index_style: IndexStyle::C,
2562 })
2563 }
2564 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
2565 b.x_l.copy_from_slice(&[-2.0e19; 2]);
2566 b.x_u.copy_from_slice(&[2.0e19; 2]);
2567 b.g_l.copy_from_slice(&[1.0]);
2568 b.g_u.copy_from_slice(&[1.0]);
2569 true
2570 }
2571 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2572 sp.x.copy_from_slice(&[0.0, 0.0]);
2573 true
2574 }
2575 fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
2576 Some(0.5 * (x[0] * x[0] + x[1] * x[1]) - x[0] - 2.0 * x[1])
2577 }
2578 fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
2579 grad[0] = x[0] - 1.0;
2580 grad[1] = x[1] - 2.0;
2581 true
2582 }
2583 fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
2584 g[0] = x[0] + x[1];
2585 true
2586 }
2587 fn eval_jac_g(
2588 &mut self,
2589 _x: Option<&[Number]>,
2590 _new_x: bool,
2591 mode: SparsityRequest<'_>,
2592 ) -> bool {
2593 match mode {
2594 SparsityRequest::Structure { irow, jcol } => {
2595 irow.copy_from_slice(&[0, 0]);
2596 jcol.copy_from_slice(&[0, 1]);
2597 }
2598 SparsityRequest::Values { values, .. } => {
2599 values.copy_from_slice(&[1.0, 1.0]);
2600 }
2601 }
2602 true
2603 }
2604 fn eval_h(
2605 &mut self,
2606 _x: Option<&[Number]>,
2607 _new_x: bool,
2608 _obj_factor: Number,
2609 _lambda: Option<&[Number]>,
2610 _new_lambda: bool,
2611 mode: SparsityRequest<'_>,
2612 ) -> bool {
2613 match mode {
2614 SparsityRequest::Structure { irow, jcol } => {
2615 irow.copy_from_slice(&[0, 1]);
2616 jcol.copy_from_slice(&[0, 1]);
2617 }
2618 SparsityRequest::Values { values, .. } => {
2619 values.copy_from_slice(&[1.0, 1.0]);
2620 }
2621 }
2622 true
2623 }
2624 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
2625 *self.finalize_called.borrow_mut() = Some((sol.x.to_vec(), sol.obj_value));
2626 }
2627 }
2628
2629 #[test]
2630 fn application_sqp_path_solves_convex_eq_nlp_and_finalizes() {
2631 let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
2632 let tnlp = std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
2633 finalize_called: std::rc::Rc::clone(&finalize_slot),
2634 }));
2635
2636 let mut app = IpoptApplication::new();
2637 app.initialize().unwrap();
2638 app.initialize_with_options_str("algorithm active-set-sqp\n")
2639 .unwrap();
2640 let status = app.optimize_tnlp(tnlp);
2641 assert_eq!(status, ApplicationReturnStatus::SolveSucceeded);
2642
2643 let recv = finalize_slot.borrow().clone();
2645 let (x_recv, obj_recv) = recv.expect("finalize_solution was not called");
2646 assert_eq!(x_recv.len(), 2);
2647 assert!((x_recv[0] - 0.0).abs() < 1e-6, "x[0] = {}", x_recv[0]);
2648 assert!((x_recv[1] - 1.0).abs() < 1e-6, "x[1] = {}", x_recv[1]);
2649 assert!(
2650 (obj_recv - (-1.5)).abs() < 1e-6,
2651 "obj = {} but expected -1.5",
2652 obj_recv
2653 );
2654 }
2655
2656 #[test]
2657 fn application_routes_to_sqp_case_insensitively() {
2658 let mut app = IpoptApplication::new();
2659 app.initialize().unwrap();
2660 app.initialize_with_options_str("algorithm Active-Set-SQP\n")
2661 .unwrap();
2662 assert!(app.is_sqp_algorithm_selected());
2666 }
2667
2668 #[test]
2669 fn application_constructs_and_loads_options() {
2670 let mut app = IpoptApplication::new();
2671 app.initialize().unwrap();
2672 app.initialize_with_options_str("print_level 5\nfile_print_level 7\n")
2675 .unwrap();
2676 let (level, found) = app.options().get_integer_value("print_level", "").unwrap();
2677 assert!(found);
2678 assert_eq!(level, 5);
2679 }
2680
2681 #[test]
2682 fn application_sqp_suboptions_propagate_to_builder() {
2683 let mut app = IpoptApplication::new();
2686 app.initialize().unwrap();
2687 app.initialize_with_options_str(
2688 "algorithm active-set-sqp\n\
2689 sqp_globalization l1-elastic\n\
2690 sqp_hessian lbfgs\n\
2691 sqp_max_iter 17\n\
2692 sqp_tol 1e-7\n\
2693 sqp_constr_viol_tol 1e-5\n\
2694 sqp_dual_inf_tol 1e-3\n\
2695 sqp_l1_penalty 2.5\n\
2696 sqp_bt_reduction 0.25\n\
2697 sqp_bt_min_alpha 1e-10\n\
2698 sqp_print_level 2\n\
2699 sqp_lbfgs_max_history 12\n",
2700 )
2701 .unwrap();
2702 let snap = app.algorithm_builder_snapshot();
2703 assert_eq!(
2704 snap.sqp.globalization,
2705 crate::sqp::SqpGlobalization::L1Elastic
2706 );
2707 assert_eq!(snap.sqp.hessian, crate::sqp::SqpHessianSource::Lbfgs);
2708 assert_eq!(snap.sqp.max_iter, 17);
2709 assert!((snap.sqp.tol - 1e-7).abs() < 1e-18);
2710 assert!((snap.sqp.constr_viol_tol - 1e-5).abs() < 1e-18);
2711 assert!((snap.sqp.dual_inf_tol - 1e-3).abs() < 1e-18);
2712 assert!((snap.sqp.l1_penalty - 2.5).abs() < 1e-18);
2713 assert!((snap.sqp.bt_reduction - 0.25).abs() < 1e-18);
2714 assert!((snap.sqp.bt_min_alpha - 1e-10).abs() < 1e-18);
2715 assert_eq!(snap.sqp.print_level, 2);
2716 assert_eq!(snap.sqp.lbfgs_max_history, 12);
2717 }
2718
2719 #[test]
2720 fn application_limited_memory_options_propagate_to_builder() {
2721 use crate::hess::lim_mem_quasi_newton::UpdateType;
2722
2723 let mut app = IpoptApplication::new();
2727 app.initialize().unwrap();
2728 let def = app.algorithm_builder_from_options();
2729 assert_eq!(def.limited_memory_update_type, UpdateType::Bfgs);
2730 assert_eq!(def.limited_memory_max_history, 6);
2731
2732 let mut app = IpoptApplication::new();
2737 app.initialize().unwrap();
2738 app.initialize_with_options_str(
2739 "hessian_approximation limited-memory\n\
2740 limited_memory_update_type sr1\n\
2741 limited_memory_max_history 9\n",
2742 )
2743 .unwrap();
2744 let snap = app.algorithm_builder_from_options();
2745 assert_eq!(snap.limited_memory_update_type, UpdateType::Sr1);
2746 assert_eq!(snap.limited_memory_max_history, 9);
2747 }
2748
2749 #[test]
2750 fn application_sqp_warm_start_round_trip() {
2751 let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
2757 let tnlp_rc: std::rc::Rc<std::cell::RefCell<dyn TNLP>> =
2758 std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
2759 finalize_called: std::rc::Rc::clone(&finalize_slot),
2760 }));
2761
2762 let mut app = IpoptApplication::new();
2763 app.initialize().unwrap();
2764 app.initialize_with_options_str("algorithm active-set-sqp\n")
2765 .unwrap();
2766
2767 let status_a = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
2769 assert_eq!(status_a, ApplicationReturnStatus::SolveSucceeded);
2770 let ws = app.last_sqp_working_set().cloned();
2771 assert!(ws.is_some(), "cold solve must yield a working set");
2772
2773 let (x_recv, _) = finalize_slot.borrow().clone().unwrap();
2777 let warm = crate::sqp::SqpIterates {
2778 x: x_recv,
2779 lambda_g: vec![1.0],
2780 lambda_x: vec![0.0, 0.0],
2781 working: ws,
2782 };
2783 app.set_sqp_warm_start(warm);
2784
2785 let status_b = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
2787 assert_eq!(status_b, ApplicationReturnStatus::SolveSucceeded);
2788 assert!(app.last_sqp_working_set().is_some());
2789 }
2790
2791 #[test]
2792 fn application_sqp_warm_start_auto_clears_after_use() {
2793 let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
2794 let tnlp_rc: std::rc::Rc<std::cell::RefCell<dyn TNLP>> =
2795 std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
2796 finalize_called: std::rc::Rc::clone(&finalize_slot),
2797 }));
2798 let mut app = IpoptApplication::new();
2799 app.initialize().unwrap();
2800 app.initialize_with_options_str("algorithm active-set-sqp\n")
2801 .unwrap();
2802 app.set_sqp_warm_start(crate::sqp::SqpIterates {
2803 x: vec![0.0, 1.0],
2804 lambda_g: vec![1.0],
2805 lambda_x: vec![0.0, 0.0],
2806 working: None,
2807 });
2808 assert!(app.sqp_warm_start.is_some());
2809 let _ = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
2810 assert!(
2811 app.sqp_warm_start.is_none(),
2812 "warm-start input must be auto-cleared after use"
2813 );
2814 }
2815
2816 #[test]
2817 fn application_sqp_suboptions_default_when_unset() {
2818 let mut app = IpoptApplication::new();
2821 app.initialize().unwrap();
2822 let snap = app.algorithm_builder_snapshot();
2823 let d = crate::sqp::SqpOptions::default();
2824 assert_eq!(snap.sqp.globalization, d.globalization);
2825 assert_eq!(snap.sqp.hessian, d.hessian);
2826 assert_eq!(snap.sqp.max_iter, d.max_iter);
2827 assert!((snap.sqp.tol - d.tol).abs() < 1e-18);
2828 assert!((snap.sqp.constr_viol_tol - d.constr_viol_tol).abs() < 1e-18);
2829 assert!((snap.sqp.dual_inf_tol - d.dual_inf_tol).abs() < 1e-18);
2830 assert!((snap.sqp.l1_penalty - d.l1_penalty).abs() < 1e-18);
2831 assert!((snap.sqp.bt_reduction - d.bt_reduction).abs() < 1e-18);
2832 assert!((snap.sqp.bt_min_alpha - d.bt_min_alpha).abs() < 1e-18);
2833 assert_eq!(snap.sqp.print_level, d.print_level);
2834 assert_eq!(snap.sqp.lbfgs_max_history, d.lbfgs_max_history);
2835 }
2836
2837 #[test]
2838 fn application_reports_problem_dimensions() {
2839 let app = IpoptApplication::new();
2840 let mut tnlp = Hs071Stub;
2841 let info = app.problem_dimensions(&mut tnlp).unwrap();
2842 assert_eq!(info.n, 4);
2843 assert_eq!(info.m, 2);
2844 assert_eq!(info.nnz_jac_g, 8);
2845 assert_eq!(info.nnz_h_lag, 10);
2846 }
2847}