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 debug_hook: Option<std::rc::Rc<std::cell::RefCell<dyn crate::debug::DebugHook>>>,
137 restoration_factory_provider: Option<RestorationFactoryProvider>,
143 on_converged: Option<ConvergedCallback>,
147 record_iter_history: bool,
153 linsol_summary_sink: Arc<Mutex<LinearSolverSummary>>,
163 sqp_warm_start: Option<crate::sqp::SqpIterates>,
169 sqp_last_working_set: Option<pounce_qp::WorkingSet>,
174 warm_start_iterate: Option<crate::debug::IterateSnapshot>,
184}
185
186impl fmt::Debug for IpoptApplication {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 f.debug_struct("IpoptApplication")
189 .field("options", &self.options)
190 .field("statistics", &self.statistics)
191 .finish_non_exhaustive()
192 }
193}
194
195impl Default for IpoptApplication {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201impl IpoptApplication {
202 pub fn new() -> Self {
205 let reg = RegisteredOptions::default();
206 register_all_upstream_options(®)
209 .unwrap_or_else(|e| panic!("Upstream options registration failed: {e}"));
210 pounce_presolve::register_options(®)
211 .unwrap_or_else(|e| panic!("Presolve options registration failed: {e}"));
212 let reg = Rc::new(reg);
213 Self {
214 options: OptionsList::with_registered(Rc::clone(®)),
215 reg_options: reg,
216 journalist: Rc::new(Journalist::new()),
217 statistics: RefCell::new(SolveStatistics::new()),
218 timing: RefCell::new(Rc::new(TimingStatistics::new())),
219 linear_backend_factory: None,
220 restoration_factory: None,
221 diagnostics: None,
222 debug_hook: None,
223 restoration_factory_provider: None,
224 on_converged: None,
225 record_iter_history: false,
226 linsol_summary_sink: Arc::new(Mutex::new(LinearSolverSummary::default())),
227 sqp_warm_start: None,
228 sqp_last_working_set: None,
229 warm_start_iterate: None,
230 }
231 }
232
233 pub fn options(&self) -> &OptionsList {
234 &self.options
235 }
236
237 pub fn options_mut(&mut self) -> &mut OptionsList {
238 &mut self.options
239 }
240
241 pub fn registered_options(&self) -> &Rc<RegisteredOptions> {
242 &self.reg_options
243 }
244
245 pub fn journalist(&self) -> &Rc<Journalist> {
246 &self.journalist
247 }
248
249 pub fn set_linear_backend_factory(&mut self, factory: LinearBackendFactory) {
254 self.linear_backend_factory = Some(factory);
255 }
256
257 pub fn set_restoration_factory(&mut self, factory: RestorationFactory) {
266 self.restoration_factory = Some(factory);
267 }
268
269 pub fn set_diagnostics(&mut self, diag: Rc<DiagnosticsState>) {
274 self.diagnostics = Some(diag);
275 }
276
277 pub fn set_debug_hook(
281 &mut self,
282 hook: std::rc::Rc<std::cell::RefCell<dyn crate::debug::DebugHook>>,
283 ) {
284 self.debug_hook = Some(hook);
285 }
286
287 pub fn diagnostics(&self) -> Option<Rc<DiagnosticsState>> {
291 self.diagnostics.as_ref().map(Rc::clone)
292 }
293
294 pub fn set_restoration_factory_provider(&mut self, provider: RestorationFactoryProvider) {
303 self.restoration_factory_provider = Some(provider);
304 }
305
306 pub fn set_on_converged(&mut self, cb: ConvergedCallback) {
312 self.on_converged = Some(cb);
313 }
314
315 pub fn enable_iter_history(&mut self) {
322 self.record_iter_history = true;
323 }
324
325 pub fn initialize_with_options_file(&mut self, path: &Path) -> Result<(), SolverException> {
328 let txt = std::fs::read_to_string(path).map_err(|e| {
329 SolverException::new(
330 ExceptionKind::IPOPT_APPLICATION_ERROR,
331 format!("could not read options file {}: {}", path.display(), e),
332 file!(),
333 line!() as Index,
334 )
335 })?;
336 self.options.read_from_str(&txt, true)?;
337 self.open_output_file_journal();
338 Ok(())
339 }
340
341 pub fn initialize_with_options_str(&mut self, s: &str) -> Result<(), SolverException> {
344 self.options.read_from_str(s, true)?;
345 self.open_output_file_journal();
346 Ok(())
347 }
348
349 fn open_output_file_journal(&self) {
361 let fname = match self.options.get_string_value("output_file", "") {
362 Ok((v, true)) if !v.is_empty() => v,
363 _ => return,
364 };
365 let level_int = self
366 .options
367 .get_integer_value("file_print_level", "")
368 .ok()
369 .and_then(|(v, f)| f.then_some(v))
370 .unwrap_or(5);
371 let level = journal_level_from_int(level_int);
372 let append = self
373 .options
374 .get_bool_value("file_append", "")
375 .ok()
376 .and_then(|(v, f)| f.then_some(v))
377 .unwrap_or(false);
378 let jname = format!("OutputFile:{}", fname);
379 let _ = self
380 .journalist
381 .add_file_journal(&jname, &fname, level, append);
382 }
383
384 pub fn initialize(&mut self) -> Result<(), SolverException> {
388 Ok(())
389 }
390
391 pub fn open_output_file(&mut self, fname: &str, print_level: i32) -> bool {
397 if self
398 .options
399 .set_string_value("output_file", fname, true, false)
400 .is_err()
401 {
402 return false;
403 }
404 if self
405 .options
406 .set_integer_value("file_print_level", print_level as Index, true, false)
407 .is_err()
408 {
409 return false;
410 }
411 let level = journal_level_from_int(print_level);
412 let jname = format!("OutputFile:{}", fname);
413 self.journalist
418 .add_file_journal(&jname, fname, level, false)
419 .is_some()
420 }
421
422 pub fn problem_dimensions(&self, tnlp: &mut dyn TNLP) -> Option<NlpInfo> {
425 tnlp.get_nlp_info()
426 }
427
428 pub fn statistics(&self) -> SolveStatistics {
429 self.statistics.borrow().clone()
430 }
431
432 pub fn timing_stats(&self) -> Rc<TimingStatistics> {
439 Rc::clone(&self.timing.borrow())
440 }
441
442 pub fn linear_solver_summary(&self) -> Option<LinearSolverSummary> {
449 let guard = self.linsol_summary_sink.lock().ok()?;
450 if guard.is_empty() {
451 None
452 } else {
453 Some(guard.clone())
454 }
455 }
456
457 pub fn optimize_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
470 if self.is_sqp_algorithm_selected() {
475 return self.optimize_sqp_tnlp(tnlp);
476 }
477 let info = match tnlp.borrow_mut().get_nlp_info() {
478 Some(info) => info,
479 None => return ApplicationReturnStatus::InvalidProblemDefinition,
480 };
481 if info.m > 0 && self.is_l1_penalty_enabled() {
491 if let Some(status) = self.run_l1_penalty_outer_loop(Rc::clone(&tnlp)) {
492 return status;
493 }
494 }
498 if info.m > 0 && self.is_l1_fallback_enabled() && !self.is_l1_penalty_enabled() {
505 return self.run_with_l1_fallback(tnlp);
506 }
507 self.optimize_constrained(tnlp)
514 }
515
516 fn is_l1_penalty_enabled(&self) -> bool {
519 self.options
520 .get_bool_value("l1_exact_penalty_barrier", "")
521 .ok()
522 .and_then(|(v, found)| found.then_some(v))
523 .unwrap_or(false)
524 }
525
526 fn l1_penalty_init(&self) -> Number {
527 self.options
528 .get_numeric_value("l1_penalty_init", "")
529 .ok()
530 .and_then(|(v, found)| found.then_some(v))
531 .unwrap_or(1.0)
532 }
533 fn l1_penalty_max(&self) -> Number {
534 self.options
535 .get_numeric_value("l1_penalty_max", "")
536 .ok()
537 .and_then(|(v, found)| found.then_some(v))
538 .unwrap_or(1.0e6)
539 }
540 fn l1_penalty_increase_factor(&self) -> Number {
541 self.options
542 .get_numeric_value("l1_penalty_increase_factor", "")
543 .ok()
544 .and_then(|(v, found)| found.then_some(v))
545 .unwrap_or(8.0)
546 }
547 fn l1_penalty_max_outer_iter(&self) -> usize {
548 self.options
549 .get_integer_value("l1_penalty_max_outer_iter", "")
550 .ok()
551 .and_then(|(v, found)| found.then_some(v))
552 .unwrap_or(8) as usize
553 }
554 fn l1_slack_tol(&self) -> Number {
555 self.options
556 .get_numeric_value("l1_slack_tol", "")
557 .ok()
558 .and_then(|(v, found)| found.then_some(v))
559 .unwrap_or(1.0e-6)
560 }
561 fn l1_steering_factor(&self) -> Number {
562 self.options
563 .get_numeric_value("l1_steering_factor", "")
564 .ok()
565 .and_then(|(v, found)| found.then_some(v))
566 .unwrap_or(10.0)
567 }
568 fn is_l1_fallback_enabled(&self) -> bool {
569 self.options
570 .get_bool_value("l1_fallback_on_restoration_failure", "")
571 .ok()
572 .and_then(|(v, found)| found.then_some(v))
573 .unwrap_or(false)
574 }
575
576 pub fn set_sqp_warm_start(&mut self, warm: crate::sqp::SqpIterates) {
590 self.sqp_warm_start = Some(warm);
591 }
592
593 pub fn clear_sqp_warm_start(&mut self) {
595 self.sqp_warm_start = None;
596 }
597
598 pub fn set_warm_start_iterate(&mut self, snap: crate::debug::IterateSnapshot) {
606 self.warm_start_iterate = Some(snap);
607 }
608
609 pub fn last_sqp_working_set(&self) -> Option<&pounce_qp::WorkingSet> {
614 self.sqp_last_working_set.as_ref()
615 }
616
617 fn is_sqp_algorithm_selected(&self) -> bool {
618 match self.options.get_string_value("algorithm", "") {
619 Ok((v, true)) => v.eq_ignore_ascii_case("active-set-sqp"),
620 _ => false,
621 }
622 }
623
624 fn optimize_sqp_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
631 use pounce_nlp::orig_ipopt_nlp::OrigIpoptNlp;
632 use pounce_nlp::tnlp_adapter::TNLPAdapter;
633 use pounce_nlp::NoScaling;
634
635 let adapter = match TNLPAdapter::new(Rc::clone(&tnlp)) {
636 Ok(a) => Rc::new(RefCell::new(a)),
637 Err(_) => return ApplicationReturnStatus::InvalidProblemDefinition,
638 };
639 let orig_nlp = match OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)) {
640 Ok(n) => n,
641 Err(_) => return ApplicationReturnStatus::InternalError,
642 };
643 let nlp_rc: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(orig_nlp));
644
645 let mut sqp_adapter = crate::sqp::IpoptNlpAdapter::new(Rc::clone(&nlp_rc));
646
647 let mut builder = self.algorithm_builder_snapshot();
648 builder.algorithm = crate::alg_builder::AlgorithmChoice::ActiveSetSqp;
649 let factory = self.make_backend_factory();
650 let mut alg = match builder.build_sqp_with_backend(factory) {
651 Some(a) => a,
652 None => return ApplicationReturnStatus::InternalError,
653 };
654
655 let warm = self.sqp_warm_start.take();
659 let res = match alg.optimize_with_warm_start(&mut sqp_adapter, warm) {
660 Ok(r) => r,
661 Err(e) => {
662 if std::env::var_os("POUNCE_DBG_SQP").is_some() {
663 tracing::warn!(target: "pounce::sqp", "[SQP] optimize_with_warm_start error: {e:?}");
664 }
665 return ApplicationReturnStatus::InternalError;
666 }
667 };
668 self.sqp_last_working_set = res.working_set.clone();
671 {
680 let mut stats = self.statistics.borrow_mut();
681 stats.iteration_count = res.n_iter as Index;
682 stats.final_objective = res.obj;
683 stats.final_dual_inf = res.final_stationarity;
684 stats.final_constr_viol = res.final_constr_viol;
685 stats.final_compl = 0.0; }
687 let (app_status, solver_status) = match res.status {
688 crate::sqp::SqpStatus::Optimal => (
689 ApplicationReturnStatus::SolveSucceeded,
690 pounce_nlp::SolverReturn::Success,
691 ),
692 crate::sqp::SqpStatus::MaxIter => (
693 ApplicationReturnStatus::MaximumIterationsExceeded,
694 pounce_nlp::SolverReturn::MaxiterExceeded,
695 ),
696 crate::sqp::SqpStatus::InfeasibleSubproblem => (
697 ApplicationReturnStatus::InfeasibleProblemDetected,
698 pounce_nlp::SolverReturn::LocalInfeasibility,
699 ),
700 crate::sqp::SqpStatus::LineSearchFailed => (
701 ApplicationReturnStatus::SearchDirectionBecomesTooSmall,
702 pounce_nlp::SolverReturn::ErrorInStepComputation,
703 ),
704 };
705
706 let _ = finalize_via_sqp(&nlp_rc, &res, solver_status, &tnlp);
712
713 app_status
714 }
715
716 fn algorithm_builder_snapshot(&self) -> AlgorithmBuilder {
720 let mut builder = AlgorithmBuilder::default();
721 apply_sqp_options(&self.options, &mut builder.sqp);
722 builder
723 }
724
725 fn make_backend_factory(&self) -> LinearBackendFactory {
729 Box::new(
730 |_choice| -> Box<dyn pounce_linsol::SparseSymLinearSolverInterface> {
731 Box::new(pounce_feral::FeralSolverInterface::new())
732 },
733 )
734 }
735
736 fn run_with_l1_fallback(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
753 let first_status = self.optimize_constrained(Rc::clone(&tnlp));
756 if !is_l1_fallback_trigger(first_status) {
757 return first_status;
758 }
759 let prev = self
763 .options
764 .get_string_value("l1_exact_penalty_barrier", "")
765 .ok();
766 let _ = self
767 .options
768 .set_string_value("l1_exact_penalty_barrier", "yes", true, false);
769 let retry_status = self
770 .run_l1_penalty_outer_loop(Rc::clone(&tnlp))
771 .unwrap_or(ApplicationReturnStatus::InternalError);
772 let _ = self.options.set_string_value(
773 "l1_exact_penalty_barrier",
774 prev.as_ref().map(|(v, _)| v.as_str()).unwrap_or("no"),
775 true,
776 false,
777 );
778 if matches!(retry_status, ApplicationReturnStatus::SolveSucceeded) {
779 retry_status
780 } else {
781 first_status
782 }
783 }
784
785 fn run_l1_penalty_outer_loop(
806 &mut self,
807 tnlp: Rc<RefCell<dyn TNLP>>,
808 ) -> Option<ApplicationReturnStatus> {
809 let rho_init = self.l1_penalty_init();
810 let rho_max = self.l1_penalty_max().max(rho_init);
811 let factor = self.l1_penalty_increase_factor().max(1.0);
812 let tau = self.l1_steering_factor();
813 let slack_tol = self.l1_slack_tol();
814 let max_outer = self.l1_penalty_max_outer_iter().max(1);
815
816 let mut wrapper = pounce_l1penalty::L1PenaltyBarrierTnlp::new(Rc::clone(&tnlp), rho_init)?;
817 if wrapper.m_eq() == 0 {
818 return None;
821 }
822 wrapper.set_defer_inner_finalize(true);
823 let wrapper_rc = Rc::new(RefCell::new(wrapper));
824
825 let mut rho = rho_init;
826 let mut last_status = ApplicationReturnStatus::InternalError;
827 for _outer in 0..max_outer {
828 wrapper_rc.borrow_mut().set_rho(rho);
829 let dyn_tnlp: Rc<RefCell<dyn TNLP>> = wrapper_rc.clone();
830 last_status = self.optimize_constrained(dyn_tnlp);
831
832 let w = wrapper_rc.borrow();
833 if !w.has_solution() {
834 drop(w);
836 break;
837 }
838 let slack_sum = w.last_slack_sum();
839 let y_eq_inf = w.last_y_eq_inf_norm();
840 drop(w);
841
842 let inner_ok = matches!(
844 last_status,
845 ApplicationReturnStatus::SolveSucceeded
846 | ApplicationReturnStatus::SolvedToAcceptableLevel
847 );
848 if !inner_ok {
849 break;
850 }
851 if slack_sum.is_finite() && slack_sum <= slack_tol {
852 break;
853 }
854 if rho >= rho_max {
855 break;
856 }
857 let geom = rho * factor;
859 let steer = tau * y_eq_inf + 1.0e-12;
860 rho = geom.max(steer).min(rho_max);
861 }
862
863 let w = wrapper_rc.borrow();
865 if w.has_solution() {
866 let x_trunc: Vec<Number> = w.last_x_trunc().to_vec();
867 let lambda: Vec<Number> = w.last_lambda().to_vec();
868 let z_l: Vec<Number> = w.last_z_l_trunc().to_vec();
869 let z_u: Vec<Number> = w.last_z_u_trunc().to_vec();
870 let solver_status = w.last_status().unwrap_or(SolverReturn::InternalError);
871 let slack_sum = w.last_slack_sum();
872 drop(w);
873
874 let infeasible_certificate = matches!(
882 last_status,
883 ApplicationReturnStatus::SolveSucceeded
884 | ApplicationReturnStatus::SolvedToAcceptableLevel
885 ) && slack_sum.is_finite()
886 && slack_sum > slack_tol;
887 let final_app_status = if infeasible_certificate {
888 ApplicationReturnStatus::InfeasibleProblemDetected
889 } else {
890 last_status
891 };
892 let final_solver_status = if infeasible_certificate {
893 SolverReturn::LocalInfeasibility
894 } else {
895 solver_status
896 };
897
898 let f_inner = tnlp
900 .borrow_mut()
901 .eval_f(&x_trunc, true)
902 .unwrap_or(Number::NAN);
903 let m = tnlp
904 .borrow_mut()
905 .get_nlp_info()
906 .map(|i| i.m as usize)
907 .unwrap_or(0);
908 let mut g_inner = vec![0.0; m];
909 if m > 0 {
910 let _ = tnlp.borrow_mut().eval_g(&x_trunc, false, &mut g_inner);
911 }
912 tnlp.borrow_mut().finalize_solution(
913 Solution {
914 status: final_solver_status,
915 x: &x_trunc,
916 z_l: &z_l,
917 z_u: &z_u,
918 g: &g_inner,
919 lambda: &lambda,
920 obj_value: f_inner,
921 },
922 &TnlpIpoptData::default(),
923 &TnlpIpoptCq::default(),
924 );
925 return Some(final_app_status);
926 }
927 Some(last_status)
929 }
930
931 pub fn reoptimize_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
933 self.optimize_tnlp(tnlp)
936 }
937
938 fn optimize_constrained(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
942 let t_start = Instant::now();
943
944 let print_opts = self
948 .options
949 .get_bool_value("print_user_options", "")
950 .ok()
951 .and_then(|(v, f)| f.then_some(v))
952 .unwrap_or(false);
953 if print_opts {
954 print!(
955 "\nList of user-set options:\n\n{}",
956 self.options.print_user_options()
957 );
958 }
959
960 let print_doc = self
970 .options
971 .get_bool_value("print_options_documentation", "")
972 .ok()
973 .and_then(|(v, f)| f.then_some(v))
974 .unwrap_or(false);
975 if print_doc {
976 let mode = self
977 .options
978 .get_string_value("print_options_mode", "")
979 .ok()
980 .map(|(v, _)| PrintOptionsMode::from_tag(&v))
981 .unwrap_or(PrintOptionsMode::Text);
982 let advanced = self
983 .options
984 .get_bool_value("print_advanced_options", "")
985 .ok()
986 .map(|(v, _)| v)
987 .unwrap_or(false);
988 print!(
989 "\n# Pounce options registry\n\n{}",
990 self.reg_options.print_options_documentation(mode, advanced)
991 );
992 }
993
994 let timing = Rc::new(TimingStatistics::new());
1000 *self.timing.borrow_mut() = Rc::clone(&timing);
1001 timing.overall_alg.start();
1002
1003 if let Ok(mut guard) = self.linsol_summary_sink.lock() {
1009 *guard = LinearSolverSummary::default();
1010 } else {
1011 debug_assert!(false, "linsol summary sink mutex poisoned");
1012 }
1013
1014 let lo_inf = self
1020 .options
1021 .get_numeric_value("nlp_lower_bound_inf", "")
1022 .ok()
1023 .and_then(|(v, f)| f.then_some(v))
1024 .unwrap_or(DEFAULT_NLP_LOWER_BOUND_INF);
1025 let up_inf = self
1026 .options
1027 .get_numeric_value("nlp_upper_bound_inf", "")
1028 .ok()
1029 .and_then(|(v, f)| f.then_some(v))
1030 .unwrap_or(DEFAULT_NLP_UPPER_BOUND_INF);
1031 let fixed_treatment = match self
1032 .options
1033 .get_string_value("fixed_variable_treatment", "")
1034 .ok()
1035 .and_then(|(v, f)| f.then_some(v))
1036 .as_deref()
1037 {
1038 Some("relax_bounds") => FixedVarTreatment::RelaxBounds,
1039 _ => FixedVarTreatment::MakeParameter,
1043 };
1044 let adapter = match TNLPAdapter::new_with_options(
1045 Rc::clone(&tnlp),
1046 lo_inf,
1047 up_inf,
1048 fixed_treatment,
1049 ) {
1050 Ok(a) => Rc::new(RefCell::new(a)),
1051 Err(_) => {
1052 timing.overall_alg.end();
1053 return ApplicationReturnStatus::InvalidProblemDefinition;
1054 }
1055 };
1056 let mut orig_nlp = match OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)) {
1057 Ok(n) => n,
1058 Err(_) => {
1059 timing.overall_alg.end();
1060 return ApplicationReturnStatus::InternalError;
1061 }
1062 };
1063 orig_nlp.set_timing_stats(Rc::clone(&timing));
1064
1065 let n_x_var = orig_nlp.x_space().dim();
1071 let n_c = orig_nlp.c_space().dim();
1072 if n_x_var > 0 && n_x_var < n_c {
1073 timing.overall_alg.end();
1074 return ApplicationReturnStatus::NotEnoughDegreesOfFreedom;
1075 }
1076
1077 let bound_relax_factor = self
1081 .options
1082 .get_numeric_value("bound_relax_factor", "")
1083 .ok()
1084 .and_then(|(v, f)| f.then_some(v))
1085 .unwrap_or(1e-8);
1086 let constr_viol_tol = self
1087 .options
1088 .get_numeric_value("constr_viol_tol", "")
1089 .ok()
1090 .and_then(|(v, f)| f.then_some(v))
1091 .unwrap_or(1e-4);
1092 orig_nlp.relax_bounds(bound_relax_factor, constr_viol_tol);
1093
1094 let scaling_method = self
1099 .options
1100 .get_string_value("nlp_scaling_method", "")
1101 .ok()
1102 .and_then(|(v, f)| f.then_some(v))
1103 .unwrap_or_else(|| "gradient-based".to_string());
1104 let scaling_method = match scaling_method.as_str() {
1105 "none" => ScalingMethod::None,
1106 "gradient-based" => ScalingMethod::GradientBased,
1107 "user-scaling" => ScalingMethod::UserScaling,
1108 _ => ScalingMethod::GradientBased,
1112 };
1113 let max_gradient = self
1114 .options
1115 .get_numeric_value("nlp_scaling_max_gradient", "")
1116 .ok()
1117 .and_then(|(v, f)| f.then_some(v))
1118 .unwrap_or(100.0);
1119 let min_value = self
1120 .options
1121 .get_numeric_value("nlp_scaling_min_value", "")
1122 .ok()
1123 .and_then(|(v, f)| f.then_some(v))
1124 .unwrap_or(1e-8);
1125 let obj_target_gradient = self
1126 .options
1127 .get_numeric_value("nlp_scaling_obj_target_gradient", "")
1128 .ok()
1129 .and_then(|(v, f)| f.then_some(v))
1130 .unwrap_or(0.0);
1131 let constr_target_gradient = self
1132 .options
1133 .get_numeric_value("nlp_scaling_constr_target_gradient", "")
1134 .ok()
1135 .and_then(|(v, f)| f.then_some(v))
1136 .unwrap_or(0.0);
1137 orig_nlp.determine_scaling_from_starting_point(
1138 scaling_method,
1139 max_gradient,
1140 min_value,
1141 obj_target_gradient,
1142 constr_target_gradient,
1143 );
1144
1145 let nlp_handle: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(orig_nlp));
1146
1147 let builder = self.algorithm_builder_from_options();
1155
1156 let feral_cfg = feral_config_from_options(&self.options);
1162 let factory = self.linear_backend_factory.take().unwrap_or_else(|| {
1163 default_backend_factory_with_sink(feral_cfg, Arc::clone(&self.linsol_summary_sink))
1164 });
1165 let bundle = builder.build_with_backend(factory);
1166
1167 let data: crate::ipopt_data::IpoptDataHandle = Rc::new(RefCell::new(AlgIpoptData::new()));
1173 data.borrow_mut().timing = Rc::clone(&timing);
1174 let cq: crate::ipopt_cq::IpoptCqHandle = Rc::new(RefCell::new(
1175 IpoptCalculatedQuantities::new(Rc::clone(&data), Rc::clone(&nlp_handle)),
1176 ));
1177 if let Ok((v, true)) = self.options.get_numeric_value("slack_move", "") {
1180 cq.borrow_mut().slack_move = v;
1181 }
1182
1183 {
1188 let nlp_borrow = nlp_handle.borrow();
1189 let n_x = nlp_borrow.n();
1190 let n_s = nlp_borrow.m_ineq();
1191 let n_yc = nlp_borrow.m_eq();
1192 let n_yd = nlp_borrow.m_ineq();
1193 let n_zl = nlp_borrow.x_l().dim();
1194 let n_zu = nlp_borrow.x_u().dim();
1195 let n_vl = nlp_borrow.d_l().dim();
1196 let n_vu = nlp_borrow.d_u().dim();
1197 drop(nlp_borrow);
1198 let iv = IteratesVector::new(
1199 Rc::new(DenseVectorSpace::new(n_x).make_new_dense()),
1200 Rc::new(DenseVectorSpace::new(n_s).make_new_dense()),
1201 Rc::new(DenseVectorSpace::new(n_yc).make_new_dense()),
1202 Rc::new(DenseVectorSpace::new(n_yd).make_new_dense()),
1203 Rc::new(DenseVectorSpace::new(n_zl).make_new_dense()),
1204 Rc::new(DenseVectorSpace::new(n_zu).make_new_dense()),
1205 Rc::new(DenseVectorSpace::new(n_vl).make_new_dense()),
1206 Rc::new(DenseVectorSpace::new(n_vu).make_new_dense()),
1207 );
1208 data.borrow_mut().set_curr(iv);
1209 }
1210
1211 if let Some(snap) = self.warm_start_iterate.take() {
1219 let dims_match = {
1220 let borrow = data.borrow();
1221 borrow
1222 .curr
1223 .as_ref()
1224 .map(|c| iterates_dims(c) == iterates_dims(snap.iterates()))
1225 .unwrap_or(false)
1226 };
1227 if dims_match {
1228 data.borrow_mut().set_curr(snap.iterates().clone());
1229 data.borrow_mut().curr_mu = snap.mu();
1230 } else {
1231 tracing::warn!(
1232 target: "pounce::warm_start",
1233 "debugger warm-restart iterate dimensions differ from the fresh \
1234 solve; ignoring the captured iterate and seeding normally"
1235 );
1236 }
1237 }
1238
1239 let max_iter = self
1240 .options
1241 .get_integer_value("max_iter", "")
1242 .ok()
1243 .and_then(|(v, f)| f.then_some(v))
1244 .unwrap_or(3000);
1245 let tol = self
1246 .options
1247 .get_numeric_value("tol", "")
1248 .ok()
1249 .and_then(|(v, f)| f.then_some(v))
1250 .unwrap_or(1e-8);
1251 data.borrow_mut().tol = tol;
1252
1253 let mut alg = IpoptAlgorithm::new(data, cq, bundle)
1254 .with_nlp(Rc::clone(&nlp_handle))
1255 .with_tnlp(Rc::clone(&tnlp));
1256 if let Some(provider) = self.restoration_factory_provider.as_mut() {
1261 self.restoration_factory = Some(provider());
1262 }
1263 if let Some(factory) = self.restoration_factory.as_mut() {
1264 alg = alg.with_restoration(factory());
1265 }
1266 if let Some(diag) = self.diagnostics.as_ref() {
1267 alg = alg.with_diagnostics(Rc::clone(diag));
1268 }
1269 if let Some(hook) = self.debug_hook.take() {
1273 alg = alg.with_debug_hook(hook);
1274 }
1275 alg.max_iter = max_iter;
1276 if let Ok((v, found)) = self.options.get_integer_value("print_level", "") {
1281 if found && v <= 0 {
1282 alg.print_iter_output = false;
1283 if let Some(resto) = alg.restoration.as_mut() {
1287 resto.set_print_iter_output(false);
1288 }
1289 }
1290 }
1291
1292 let iter_capture = self
1303 .record_iter_history
1304 .then(pounce_observability::IterCaptureGuard::start);
1305
1306 let solver_status = alg.optimize();
1307
1308 let captured_iters = iter_capture.map(|g| g.finish()).unwrap_or_default();
1309 timing.overall_alg.end();
1315
1316 {
1318 let mut stats = self.statistics.borrow_mut();
1319 {
1320 let d = alg.data.borrow();
1321 stats.iteration_count = d.iter_count;
1322 stats.final_mu = d.curr_mu;
1326 }
1327 stats.total_wallclock_time_secs = t_start.elapsed().as_secs_f64();
1328 stats.restoration_calls = alg.resto_calls;
1332 stats.restoration_inner_iters = alg.resto_inner_iters;
1333 stats.restoration_outer_iters = alg.resto_outer_iters;
1334 stats.restoration_wall_secs = alg.resto_wall_secs;
1335 stats.iterations = captured_iters;
1336 let curr_x = alg.data.borrow().curr.as_ref().map(|c| c.x.clone());
1344 if let Some(x) = curr_x {
1345 if let Ok(f) = try_eval_curr_f(&nlp_handle, &x) {
1346 stats.final_objective = f;
1347 stats.final_scaled_objective = f;
1348 }
1349 }
1350 let cq = alg.cq.borrow();
1355 stats.final_dual_inf = cq.curr_dual_infeasibility_max();
1356 stats.final_constr_viol = cq.curr_primal_infeasibility_max();
1357 let compl = cq
1362 .curr_compl_x_l()
1363 .amax()
1364 .max(cq.curr_compl_x_u().amax())
1365 .max(cq.curr_compl_s_l().amax())
1366 .max(cq.curr_compl_s_u().amax());
1367 stats.final_compl = compl;
1368 stats.final_kkt_error = cq.curr_nlp_error();
1369 }
1370
1371 let app_status = solver_return_to_app_status(solver_status);
1374
1375 if matches!(
1380 app_status,
1381 ApplicationReturnStatus::SolveSucceeded
1382 | ApplicationReturnStatus::SolvedToAcceptableLevel
1383 ) {
1384 if let Some(cb) = self.on_converged.as_mut() {
1385 if let Some(sd) = alg.search_dir.as_mut() {
1386 let pd = sd.pd_solver_rc();
1387 cb(&alg.data, &alg.cq, &nlp_handle, pd);
1388 }
1389 }
1390 }
1391
1392 match finalize_via_orig_nlp(&nlp_handle, &alg, solver_status, app_status, &tnlp) {
1398 Ok(f_unscaled) => {
1399 self.statistics.borrow_mut().final_objective = f_unscaled;
1400 }
1401 Err(()) => {
1402 }
1405 }
1406
1407 let print_timing = self
1415 .options
1416 .get_bool_value("print_timing_statistics", "")
1417 .ok()
1418 .and_then(|(v, f)| f.then_some(v))
1419 .unwrap_or(false);
1420 if print_timing {
1421 let report = timing.report();
1422 print!("{}", report);
1423 use pounce_common::journalist::{JournalCategory, JournalLevel};
1424 self.journalist.print(
1425 JournalLevel::J_SUMMARY,
1426 JournalCategory::J_TIMING_STATISTICS,
1427 &report,
1428 );
1429 }
1430
1431 app_status
1432 }
1433
1434 pub fn algorithm_builder_from_options(&self) -> AlgorithmBuilder {
1442 let mut builder = AlgorithmBuilder::new();
1443
1444 let mut mehrotra_on = false;
1449 if let Ok((v, found)) = self.options.get_string_value("mehrotra_algorithm", "") {
1450 if found && v == "yes" {
1451 mehrotra_on = true;
1452 builder.mehrotra_algorithm = true;
1453 builder.mu_strategy = MuStrategyChoice::Adaptive;
1454 builder.mu_oracle = crate::mu::adaptive::MuOracleKind::Probing;
1455 builder.line_search.accept_every_trial_step = true;
1461 builder.init.bound_push = 10.0;
1465 builder.init.bound_frac = 0.2;
1466 builder.init.slack_bound_push = 10.0;
1467 builder.init.slack_bound_frac = 0.2;
1468 builder.init.bound_mult_init_val = 10.0;
1469 builder.init.constr_mult_init_max = 0.0;
1470 builder.line_search.alpha_for_y =
1475 crate::line_search::backtracking::AlphaForY::BoundMult;
1476 builder.mu.adaptive_mu_globalization =
1483 crate::mu::adaptive::AdaptiveMuGlobalization::NeverMonotoneMode;
1484 builder.init.least_square_init_primal = true;
1492 }
1493 }
1494
1495 if let Ok((v, found)) = self.options.get_string_value("mu_strategy", "") {
1496 if found {
1497 let parsed = match v.as_str() {
1498 "adaptive" => MuStrategyChoice::Adaptive,
1499 _ => MuStrategyChoice::Monotone,
1500 };
1501 if mehrotra_on && matches!(parsed, MuStrategyChoice::Monotone) {
1502 tracing::warn!(target: "pounce::algorithm",
1506 "pounce: mehrotra_algorithm=yes requires \
1507 mu_strategy=adaptive; ignoring \
1508 mu_strategy=monotone."
1509 );
1510 } else {
1511 builder.mu_strategy = parsed;
1512 }
1513 }
1514 }
1515 if let Ok((v, found)) = self.options.get_string_value("mu_oracle", "") {
1516 if found {
1517 builder.mu_oracle = match v.as_str() {
1518 "loqo" => crate::mu::adaptive::MuOracleKind::Loqo,
1519 "probing" => crate::mu::adaptive::MuOracleKind::Probing,
1520 _ => crate::mu::adaptive::MuOracleKind::QualityFunction,
1521 };
1522 }
1523 }
1524 if let Ok((v, found)) = self
1525 .options
1526 .get_string_value("adaptive_mu_globalization", "")
1527 {
1528 if found {
1529 use crate::mu::adaptive::AdaptiveMuGlobalization;
1530 builder.mu.adaptive_mu_globalization = match v.as_str() {
1531 "kkt-error" => AdaptiveMuGlobalization::KktError,
1532 "never-monotone-mode" => AdaptiveMuGlobalization::NeverMonotoneMode,
1533 _ => AdaptiveMuGlobalization::ObjConstrFilter,
1534 };
1535 }
1536 }
1537 if let Ok((v, found)) = self.options.get_string_value("hessian_approximation", "") {
1538 if found {
1539 builder.hessian_approximation = match v.as_str() {
1540 "limited-memory" => HessianApproxChoice::LimitedMemory,
1541 _ => HessianApproxChoice::Exact,
1542 };
1543 }
1544 }
1545 if let Ok((v, found)) = self.options.get_string_value("line_search_method", "") {
1546 if found {
1547 builder.line_search_method = match v.as_str() {
1548 "cg-penalty" => LineSearchChoice::CgPenalty,
1549 "penalty" => LineSearchChoice::Penalty,
1550 _ => LineSearchChoice::Filter,
1551 };
1552 }
1553 }
1554 if let Ok((v, found)) = self.options.get_string_value("accept_every_trial_step", "") {
1557 if found {
1558 builder.line_search.accept_every_trial_step = v == "yes";
1559 }
1560 }
1561 if let Ok((v, found)) = self.options.get_string_value("alpha_for_y", "") {
1564 if found {
1565 use crate::line_search::backtracking::AlphaForY;
1566 builder.line_search.alpha_for_y = match v.as_str() {
1567 "primal" => AlphaForY::Primal,
1568 "bound-mult" | "bound_mult" => AlphaForY::BoundMult,
1569 "full" => AlphaForY::Full,
1570 "min" => AlphaForY::Min,
1571 "max" => AlphaForY::Max,
1572 "primal-and-full" | "dual-and-full" => AlphaForY::Primal,
1573 _ => AlphaForY::Primal,
1574 };
1575 }
1576 }
1577 if let Ok((v, _found)) = self.options.get_string_value("linear_solver", "") {
1589 builder.linear_solver = match v.as_str() {
1590 "ma57" => LinearSolverChoice::Ma57,
1591 _ => LinearSolverChoice::Feral,
1592 };
1593 }
1594
1595 if let Ok((v, found)) = self.options.get_string_value("linear_system_scaling", "") {
1604 if found {
1605 builder.linear_system_scaling = match v.as_str() {
1606 "ruiz" => crate::alg_builder::LinearSystemScalingChoice::Ruiz,
1607 "mc19" => crate::alg_builder::LinearSystemScalingChoice::Mc19,
1608 _ => crate::alg_builder::LinearSystemScalingChoice::None,
1609 };
1610 }
1611 }
1612 if let Ok((v, found)) = self.options.get_bool_value("linear_scaling_on_demand", "") {
1613 if found {
1614 builder.linear_scaling_on_demand = v;
1615 }
1616 }
1617
1618 let read_num = |key: &str| -> Option<f64> {
1622 self.options
1623 .get_numeric_value(key, "")
1624 .ok()
1625 .and_then(|(v, f)| f.then_some(v))
1626 };
1627 let read_int = |key: &str| -> Option<i32> {
1628 self.options
1629 .get_integer_value(key, "")
1630 .ok()
1631 .and_then(|(v, f)| f.then_some(v))
1632 };
1633 if let Some(v) = read_num("tol") {
1634 builder.conv_check.tol = v;
1635 }
1636 if let Some(v) = read_num("dual_inf_tol") {
1637 builder.conv_check.dual_inf_tol = v;
1638 }
1639 if let Some(v) = read_num("constr_viol_tol") {
1640 builder.conv_check.constr_viol_tol = v;
1641 }
1642 if let Some(v) = read_num("compl_inf_tol") {
1643 builder.conv_check.compl_inf_tol = v;
1644 }
1645 if let Some(v) = read_int("max_iter") {
1646 builder.conv_check.max_iter = v;
1647 }
1648 if let Some(v) = read_num("max_cpu_time") {
1649 builder.conv_check.max_cpu_time = v;
1650 }
1651 if let Some(v) = read_num("max_wall_time") {
1652 builder.conv_check.max_wall_time = v;
1653 }
1654 if let Some(v) = read_num("acceptable_tol") {
1655 builder.conv_check.acceptable_tol = v;
1656 }
1657 if let Some(v) = read_num("acceptable_dual_inf_tol") {
1658 builder.conv_check.acceptable_dual_inf_tol = v;
1659 }
1660 if let Some(v) = read_num("acceptable_constr_viol_tol") {
1661 builder.conv_check.acceptable_constr_viol_tol = v;
1662 }
1663 if let Some(v) = read_num("acceptable_compl_inf_tol") {
1664 builder.conv_check.acceptable_compl_inf_tol = v;
1665 }
1666 if let Some(v) = read_num("acceptable_obj_change_tol") {
1667 builder.conv_check.acceptable_obj_change_tol = v;
1668 }
1669 if let Some(v) = read_int("acceptable_iter") {
1670 builder.conv_check.acceptable_iter = v;
1671 }
1672 if let Some(v) = read_num("infeas_stationarity_tol") {
1673 builder.conv_check.infeas_stationarity_tol = v;
1674 }
1675 if let Some(v) = read_num("infeas_viol_kappa") {
1676 builder.conv_check.infeas_viol_kappa = v;
1677 }
1678 if let Some(v) = read_int("infeas_max_streak") {
1679 builder.conv_check.infeas_max_streak = v;
1680 }
1681
1682 if let Some(v) = read_num("mu_init") {
1687 builder.mu.mu_init = v;
1688 }
1689 if let Some(v) = read_num("mu_max") {
1690 builder.mu.mu_max = v;
1691 }
1692 if let Some(v) = read_num("mu_max_fact") {
1693 builder.mu.mu_max_fact = v;
1694 }
1695 if let Some(v) = read_num("mu_min") {
1696 builder.mu.mu_min = v;
1697 }
1698 if let Some(v) = read_num("mu_target") {
1699 builder.mu.mu_target = v;
1700 }
1701 if let Some(v) = read_num("mu_linear_decrease_factor") {
1702 builder.mu.mu_linear_decrease_factor = v;
1703 }
1704 if let Some(v) = read_num("mu_superlinear_decrease_power") {
1705 builder.mu.mu_superlinear_decrease_power = v;
1706 }
1707 if let Ok((v, found)) = self
1708 .options
1709 .get_string_value("mu_allow_fast_monotone_decrease", "")
1710 {
1711 if found {
1712 builder.mu.mu_allow_fast_monotone_decrease = v == "yes";
1713 }
1714 }
1715 if let Some(v) = read_num("barrier_tol_factor") {
1716 builder.mu.barrier_tol_factor = v;
1717 }
1718 if let Some(v) = read_num("sigma_max") {
1719 builder.mu.sigma_max = v;
1720 }
1721 if let Some(v) = read_num("sigma_min") {
1722 builder.mu.sigma_min = v;
1723 }
1724
1725 if let Ok((v, found)) = self
1729 .options
1730 .get_string_value("quality_function_norm_type", "")
1731 {
1732 if found {
1733 use crate::mu::oracle::quality_function::NormType;
1734 builder.mu.quality_function_norm_type = match v.as_str() {
1735 "1-norm" => NormType::OneNorm,
1736 "2-norm" => NormType::TwoNorm,
1737 "max-norm" => NormType::MaxNorm,
1738 _ => NormType::TwoNormSquared,
1739 };
1740 }
1741 }
1742 if let Ok((v, found)) = self
1743 .options
1744 .get_string_value("quality_function_centrality", "")
1745 {
1746 if found {
1747 use crate::mu::oracle::quality_function::CentralityType;
1748 builder.mu.quality_function_centrality = match v.as_str() {
1749 "log" => CentralityType::LogCenter,
1750 "reciprocal" => CentralityType::ReciprocalCenter,
1751 "cubed-reciprocal" => CentralityType::CubedReciprocalCenter,
1752 _ => CentralityType::None,
1753 };
1754 }
1755 }
1756 if let Ok((v, found)) = self
1757 .options
1758 .get_string_value("quality_function_balancing_term", "")
1759 {
1760 if found {
1761 use crate::mu::oracle::quality_function::BalancingTermType;
1762 builder.mu.quality_function_balancing_term = match v.as_str() {
1763 "cubic" => BalancingTermType::CubicTerm,
1764 _ => BalancingTermType::None,
1765 };
1766 }
1767 }
1768 if let Some(v) = read_int("quality_function_max_section_steps") {
1769 builder.mu.quality_function_max_section_steps = v;
1770 }
1771 if let Some(v) = read_num("quality_function_section_sigma_tol") {
1772 builder.mu.quality_function_section_sigma_tol = v;
1773 }
1774 if let Some(v) = read_num("quality_function_section_qf_tol") {
1775 builder.mu.quality_function_section_qf_tol = v;
1776 }
1777
1778 if let Some(v) = read_num("probing_iterate_quality_factor") {
1786 builder.mu.probing_iterate_quality_factor = v;
1787 }
1788
1789 if let Some(v) = read_num("adaptive_mu_safeguard_factor") {
1793 builder.mu.adaptive_mu_safeguard_factor = v;
1794 }
1795 if let Some(v) = read_num("adaptive_mu_monotone_init_factor") {
1796 builder.mu.adaptive_mu_monotone_init_factor = v;
1797 }
1798 if let Ok((v, found)) = self
1799 .options
1800 .get_bool_value("adaptive_mu_restore_previous_iterate", "")
1801 {
1802 if found {
1803 builder.mu.adaptive_mu_restore_previous_iterate = v;
1804 }
1805 }
1806 if let Some(v) = read_int("adaptive_mu_kkterror_red_iters") {
1807 if v >= 0 {
1808 builder.mu.adaptive_mu_kkterror_red_iters = v as usize;
1809 }
1810 }
1811 if let Some(v) = read_num("adaptive_mu_kkterror_red_fact") {
1812 builder.mu.adaptive_mu_kkterror_red_fact = v;
1813 }
1814 if let Ok((v, found)) = self
1815 .options
1816 .get_string_value("adaptive_mu_kkt_norm_type", "")
1817 {
1818 if found {
1819 use crate::mu::adaptive::AdaptiveMuKktNorm;
1820 builder.mu.adaptive_mu_kkt_norm_type = match v.as_str() {
1821 "1-norm" => AdaptiveMuKktNorm::OneNorm,
1822 "2-norm" => AdaptiveMuKktNorm::TwoNorm,
1823 "max-norm" => AdaptiveMuKktNorm::MaxNorm,
1824 _ => AdaptiveMuKktNorm::TwoNormSquared,
1825 };
1826 }
1827 }
1828
1829 if let Some(v) = read_int("watchdog_shortened_iter_trigger") {
1833 builder.line_search.watchdog_shortened_iter_trigger = v;
1834 }
1835 if let Some(v) = read_int("watchdog_trial_iter_max") {
1836 builder.line_search.watchdog_trial_iter_max = v;
1837 }
1838 if let Some(v) = read_num("soft_resto_pderror_reduction_factor") {
1839 builder.line_search.soft_resto_pderror_reduction_factor = v;
1840 }
1841 if let Some(v) = read_int("max_soft_resto_iters") {
1842 builder.line_search.max_soft_resto_iters = v;
1843 }
1844
1845 if let Some(v) = read_int("print_frequency_iter") {
1847 builder.output.print_frequency_iter = v;
1848 }
1849 if let Some(v) = read_num("print_frequency_time") {
1850 builder.output.print_frequency_time = v;
1851 }
1852 if let Ok((v, found)) = self.options.get_bool_value("print_info_string", "") {
1853 if found {
1854 builder.output.print_info_string = v;
1855 }
1856 }
1857 if let Ok((v, found)) = self.options.get_string_value("inf_pr_output", "") {
1858 if found {
1859 builder.output.inf_pr_output_internal = v == "internal";
1860 }
1861 }
1862
1863 if let Ok((v, found)) = self.options.get_bool_value("warm_start_init_point", "") {
1869 if found {
1870 builder.warm_start_init_point = v;
1871 }
1872 }
1873 if let Ok((v, found)) = self.options.get_bool_value("warm_start_same_structure", "") {
1874 if found {
1875 builder.warm.same_structure = v;
1876 }
1877 }
1878 if let Some(v) = read_num("warm_start_bound_push") {
1879 builder.warm.bound_push = v;
1880 }
1881 if let Some(v) = read_num("warm_start_bound_frac") {
1882 builder.warm.bound_frac = v;
1883 }
1884 if let Some(v) = read_num("warm_start_slack_bound_push") {
1885 builder.warm.slack_bound_push = v;
1886 }
1887 if let Some(v) = read_num("warm_start_slack_bound_frac") {
1888 builder.warm.slack_bound_frac = v;
1889 }
1890 if let Some(v) = read_num("warm_start_mult_bound_push") {
1891 builder.warm.mult_bound_push = v;
1892 }
1893 if let Some(v) = read_num("warm_start_mult_init_max") {
1894 builder.warm.mult_init_max = v;
1895 }
1896 if let Some(v) = read_num("warm_start_target_mu") {
1897 builder.warm.target_mu = v;
1898 }
1899 if let Ok((v, found)) = self
1900 .options
1901 .get_string_value("warm_start_entire_iterate", "")
1902 {
1903 if found {
1904 builder.warm.entire_iterate = v == "yes";
1905 }
1906 }
1907
1908 if let Some(v) = read_num("bound_push") {
1912 builder.init.bound_push = v;
1913 }
1914 if let Some(v) = read_num("bound_frac") {
1915 builder.init.bound_frac = v;
1916 }
1917 if let Some(v) = read_num("slack_bound_push") {
1918 builder.init.slack_bound_push = v;
1919 }
1920 if let Some(v) = read_num("slack_bound_frac") {
1921 builder.init.slack_bound_frac = v;
1922 }
1923 if let Some(v) = read_num("constr_mult_init_max") {
1924 builder.init.constr_mult_init_max = v;
1925 }
1926 if let Some(v) = read_num("bound_mult_init_val") {
1927 builder.init.bound_mult_init_val = v;
1928 }
1929 if let Ok((v, found)) = self.options.get_string_value("bound_mult_init_method", "") {
1930 if found {
1931 builder.init.bound_mult_init_method = v;
1932 }
1933 }
1934 if let Ok((v, found)) = self
1935 .options
1936 .get_string_value("least_square_init_primal", "")
1937 {
1938 if found {
1939 builder.init.least_square_init_primal = v == "yes";
1940 }
1941 }
1942 builder
1943 }
1944}
1945
1946fn iterates_dims(c: &IteratesVector) -> [i32; 8] {
1953 [
1954 c.x.dim(),
1955 c.s.dim(),
1956 c.y_c.dim(),
1957 c.y_d.dim(),
1958 c.z_l.dim(),
1959 c.z_u.dim(),
1960 c.v_l.dim(),
1961 c.v_u.dim(),
1962 ]
1963}
1964
1965fn journal_level_from_int(v: i32) -> JournalLevel {
1966 match v.clamp(0, 12) {
1967 0 => JournalLevel::J_NONE,
1968 1 => JournalLevel::J_ERROR,
1969 2 => JournalLevel::J_STRONGWARNING,
1970 3 => JournalLevel::J_SUMMARY,
1971 4 => JournalLevel::J_WARNING,
1972 5 => JournalLevel::J_ITERSUMMARY,
1973 6 => JournalLevel::J_DETAILED,
1974 7 => JournalLevel::J_MOREDETAILED,
1975 8 => JournalLevel::J_VECTOR,
1976 9 => JournalLevel::J_MOREVECTOR,
1977 10 => JournalLevel::J_MATRIX,
1978 11 => JournalLevel::J_MOREMATRIX,
1979 _ => JournalLevel::J_ALL,
1980 }
1981}
1982
1983pub fn default_backend_factory(feral_cfg: pounce_feral::FeralConfig) -> LinearBackendFactory {
1992 Box::new(
1993 move |choice: LinearSolverChoice| -> Box<dyn SparseSymLinearSolverInterface> {
1994 match choice {
1995 LinearSolverChoice::Feral => Box::new(
1996 pounce_feral::FeralSolverInterface::with_config(feral_cfg.clone()),
1997 ),
1998 LinearSolverChoice::Ma57 => {
1999 #[cfg(feature = "ma57")]
2000 {
2001 Box::new(pounce_hsl::Ma57SolverInterface::new())
2002 }
2003 #[cfg(not(feature = "ma57"))]
2004 {
2005 Box::new(pounce_feral::FeralSolverInterface::with_config(
2007 feral_cfg.clone(),
2008 ))
2009 }
2010 }
2011 }
2012 },
2013 )
2014}
2015
2016pub fn default_backend_factory_with_sink(
2023 feral_cfg: pounce_feral::FeralConfig,
2024 sink: Arc<Mutex<LinearSolverSummary>>,
2025) -> LinearBackendFactory {
2026 Box::new(
2027 move |choice: LinearSolverChoice| -> Box<dyn SparseSymLinearSolverInterface> {
2028 match choice {
2029 LinearSolverChoice::Feral => Box::new(
2030 pounce_feral::FeralSolverInterface::with_config(feral_cfg.clone())
2031 .with_summary_sink(Arc::clone(&sink)),
2032 ),
2033 LinearSolverChoice::Ma57 => {
2034 #[cfg(feature = "ma57")]
2035 {
2036 Box::new(pounce_hsl::Ma57SolverInterface::new())
2037 }
2038 #[cfg(not(feature = "ma57"))]
2039 {
2040 Box::new(
2041 pounce_feral::FeralSolverInterface::with_config(feral_cfg.clone())
2042 .with_summary_sink(Arc::clone(&sink)),
2043 )
2044 }
2045 }
2046 }
2047 },
2048 )
2049}
2050
2051pub fn feral_config_from_options(
2057 options: &pounce_common::options_list::OptionsList,
2058) -> pounce_feral::FeralConfig {
2059 let mut cfg = pounce_feral::FeralConfig::from_env();
2060 if let Ok((v, true)) = options.get_bool_value("feral_cascade_break", "") {
2067 cfg.cascade_break = Some(v);
2068 }
2069 if let Ok((v, true)) = options.get_bool_value("feral_fma", "") {
2070 cfg.fma = v;
2071 }
2072 if let Ok((v, true)) = options.get_bool_value("feral_refine", "") {
2073 cfg.refine = v;
2074 }
2075 if let Ok((v, true)) = options.get_numeric_value("feral_singular_pivot_floor", "") {
2076 cfg.singular_pivot_floor = v;
2077 }
2078 if let Ok((v, true)) = options.get_numeric_value("feral_pivtol", "") {
2079 cfg.pivtol = v;
2080 }
2081 if let Ok((v, true)) = options.get_string_value("feral_ordering", "") {
2086 if let Some(m) = pounce_feral::parse_ordering_method(&v) {
2087 cfg.ordering = m;
2088 }
2089 }
2090 if let Ok((v, true)) = options.get_string_value("feral_scaling", "") {
2094 if let Some(s) = pounce_feral::parse_scaling_strategy(&v) {
2095 cfg.scaling = s;
2096 }
2097 }
2098 cfg
2099}
2100
2101fn solver_return_to_app_status(s: SolverReturn) -> ApplicationReturnStatus {
2107 match s {
2108 SolverReturn::Success => ApplicationReturnStatus::SolveSucceeded,
2109 SolverReturn::StopAtAcceptablePoint => ApplicationReturnStatus::SolvedToAcceptableLevel,
2110 SolverReturn::FeasiblePointFound => ApplicationReturnStatus::FeasiblePointFound,
2111 SolverReturn::MaxiterExceeded => ApplicationReturnStatus::MaximumIterationsExceeded,
2112 SolverReturn::CpuTimeExceeded => ApplicationReturnStatus::MaximumCpuTimeExceeded,
2113 SolverReturn::WallTimeExceeded => ApplicationReturnStatus::MaximumWallTimeExceeded,
2114 SolverReturn::StopAtTinyStep => ApplicationReturnStatus::SearchDirectionBecomesTooSmall,
2115 SolverReturn::LocalInfeasibility => ApplicationReturnStatus::InfeasibleProblemDetected,
2116 SolverReturn::UserRequestedStop => ApplicationReturnStatus::UserRequestedStop,
2117 SolverReturn::DivergingIterates => ApplicationReturnStatus::DivergingIterates,
2118 SolverReturn::RestorationFailure => ApplicationReturnStatus::RestorationFailed,
2119 SolverReturn::ErrorInStepComputation => ApplicationReturnStatus::ErrorInStepComputation,
2120 SolverReturn::InvalidNumberDetected => ApplicationReturnStatus::InvalidNumberDetected,
2121 SolverReturn::TooFewDegreesOfFreedom => ApplicationReturnStatus::NotEnoughDegreesOfFreedom,
2122 SolverReturn::InvalidOption => ApplicationReturnStatus::InvalidOption,
2123 SolverReturn::OutOfMemory => ApplicationReturnStatus::InsufficientMemory,
2124 SolverReturn::InternalError | SolverReturn::Unassigned => {
2125 ApplicationReturnStatus::InternalError
2126 }
2127 }
2128}
2129
2130fn try_eval_curr_f(
2134 nlp: &Rc<RefCell<dyn IpoptNlp>>,
2135 x: &Rc<dyn pounce_linalg::Vector>,
2136) -> Result<Number, ()> {
2137 let mut nlp_mut = nlp.borrow_mut();
2138 Ok(nlp_mut.eval_f(&**x))
2139}
2140
2141fn is_l1_fallback_trigger(status: ApplicationReturnStatus) -> bool {
2148 matches!(
2149 status,
2150 ApplicationReturnStatus::RestorationFailed
2151 | ApplicationReturnStatus::InfeasibleProblemDetected
2152 | ApplicationReturnStatus::SolvedToAcceptableLevel
2153 | ApplicationReturnStatus::MaximumIterationsExceeded
2154 | ApplicationReturnStatus::NotEnoughDegreesOfFreedom
2155 )
2156}
2157
2158fn finalize_via_orig_nlp(
2169 nlp: &Rc<RefCell<dyn IpoptNlp>>,
2170 alg: &IpoptAlgorithm,
2171 solver_status: SolverReturn,
2172 _app_status: ApplicationReturnStatus,
2173 tnlp: &Rc<RefCell<dyn TNLP>>,
2174) -> Result<Number, ()> {
2175 let curr = alg.data.borrow().curr.clone().ok_or(())?;
2176 let nlp_borrow = nlp.borrow();
2180 let x_vec: Vec<Number> = nlp_borrow.lift_x_to_full(&*curr.x);
2181 let info = tnlp.borrow_mut().get_nlp_info().ok_or(())?;
2182 let n = info.n as usize;
2183 let m = info.m as usize;
2184 debug_assert_eq!(x_vec.len(), n);
2185 let mut z_l = nlp_borrow.pack_z_l_for_user(&*curr.z_l);
2189 if z_l.is_empty() {
2190 z_l = vec![0.0; n];
2191 }
2192 let mut z_u = nlp_borrow.pack_z_u_for_user(&*curr.z_u);
2193 if z_u.is_empty() {
2194 z_u = vec![0.0; n];
2195 }
2196 let mut lambda = nlp_borrow.pack_lambda_for_user(&*curr.y_c, &*curr.y_d);
2197 if lambda.is_empty() {
2198 lambda = vec![0.0; m];
2199 }
2200 drop(nlp_borrow);
2201 let mut g_final = vec![0.0; m];
2204 let _ = tnlp.borrow_mut().eval_g(&x_vec, true, &mut g_final);
2205 let f_final = tnlp
2206 .borrow_mut()
2207 .eval_f(&x_vec, true)
2208 .unwrap_or(Number::NAN);
2209 tnlp.borrow_mut().finalize_solution(
2210 Solution {
2211 status: solver_status,
2212 x: &x_vec,
2213 z_l: &z_l,
2214 z_u: &z_u,
2215 g: &g_final,
2216 lambda: &lambda,
2217 obj_value: f_final,
2218 },
2219 &TnlpIpoptData::default(),
2220 &TnlpIpoptCq::default(),
2221 );
2222 Ok(f_final)
2223}
2224
2225fn apply_sqp_options(options: &OptionsList, opts: &mut crate::sqp::SqpOptions) {
2233 use crate::sqp::{SqpGlobalization, SqpHessianSource};
2234
2235 if let Ok((s, true)) = options.get_string_value("sqp_globalization", "") {
2236 opts.globalization = match s.as_str() {
2237 "filter" => SqpGlobalization::Filter,
2238 "l1-elastic" => SqpGlobalization::L1Elastic,
2239 _ => opts.globalization,
2240 };
2241 }
2242 if let Ok((s, true)) = options.get_string_value("sqp_hessian", "") {
2243 opts.hessian = match s.as_str() {
2244 "exact" => SqpHessianSource::Exact,
2245 "damped-bfgs" => SqpHessianSource::DampedBfgs,
2246 "lbfgs" => SqpHessianSource::Lbfgs,
2247 _ => opts.hessian,
2248 };
2249 }
2250 if let Ok((v, true)) = options.get_integer_value("sqp_max_iter", "") {
2251 if v >= 0 {
2252 opts.max_iter = v as u32;
2253 }
2254 }
2255 if let Ok((v, true)) = options.get_numeric_value("sqp_tol", "") {
2256 opts.tol = v;
2257 }
2258 if let Ok((v, true)) = options.get_numeric_value("sqp_constr_viol_tol", "") {
2259 opts.constr_viol_tol = v;
2260 }
2261 if let Ok((v, true)) = options.get_numeric_value("sqp_dual_inf_tol", "") {
2262 opts.dual_inf_tol = v;
2263 }
2264 if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty", "") {
2265 opts.l1_penalty = v;
2266 }
2267 if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty_safety", "") {
2268 opts.l1_penalty_safety = v;
2269 }
2270 if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty_max", "") {
2271 opts.l1_penalty_max = v;
2272 }
2273 if let Ok((v, true)) = options.get_numeric_value("sqp_bt_reduction", "") {
2274 opts.bt_reduction = v;
2275 }
2276 if let Ok((v, true)) = options.get_numeric_value("sqp_bt_min_alpha", "") {
2277 opts.bt_min_alpha = v;
2278 }
2279 if let Ok((v, true)) = options.get_integer_value("sqp_print_level", "") {
2280 opts.print_level = v.clamp(0, u8::MAX as i32) as u8;
2281 }
2282 if let Ok((v, true)) = options.get_integer_value("sqp_lbfgs_max_history", "") {
2283 if v >= 1 {
2284 opts.lbfgs_max_history = v as u32;
2285 }
2286 }
2287}
2288
2289fn finalize_via_sqp(
2297 nlp: &Rc<RefCell<dyn IpoptNlp>>,
2298 res: &crate::sqp::SqpResult,
2299 solver_status: pounce_nlp::SolverReturn,
2300 tnlp: &Rc<RefCell<dyn TNLP>>,
2301) -> Result<Number, ()> {
2302 use pounce_linalg::dense_vector::DenseVectorSpace;
2303
2304 let info = tnlp.borrow_mut().get_nlp_info().ok_or(())?;
2305 let n = info.n as usize;
2306 let m = info.m as usize;
2307
2308 let nlp_borrow = nlp.borrow();
2311 let n_alg = nlp_borrow.n() as usize;
2312 let m_eq = nlp_borrow.m_eq() as usize;
2313 let m_ineq = nlp_borrow.m_ineq() as usize;
2314 debug_assert_eq!(res.x.len(), n_alg);
2315 debug_assert_eq!(res.lambda_g.len(), m_eq + m_ineq);
2316 debug_assert_eq!(res.lambda_x.len(), n_alg);
2317
2318 let x_space = DenseVectorSpace::new(n_alg as Index);
2319 let c_space = DenseVectorSpace::new(m_eq as Index);
2320 let d_space = DenseVectorSpace::new(m_ineq as Index);
2321
2322 let mut x_dv = x_space.make_new_dense();
2323 x_dv.set_values(&res.x);
2324 let x_vec: Vec<Number> = nlp_borrow.lift_x_to_full(&x_dv);
2325 debug_assert_eq!(x_vec.len(), n);
2326
2327 let mut z_l_compressed = x_space.make_new_dense();
2329 let mut z_u_compressed = x_space.make_new_dense();
2330 let zl_vals: Vec<Number> = res.lambda_x.iter().map(|v| v.max(0.0)).collect();
2331 let zu_vals: Vec<Number> = res.lambda_x.iter().map(|v| (-v).max(0.0)).collect();
2332 z_l_compressed.set_values(&zl_vals);
2333 z_u_compressed.set_values(&zu_vals);
2334 let mut z_l = nlp_borrow.pack_z_l_for_user(&z_l_compressed);
2335 if z_l.is_empty() {
2336 z_l = vec![0.0; n];
2337 }
2338 let mut z_u = nlp_borrow.pack_z_u_for_user(&z_u_compressed);
2339 if z_u.is_empty() {
2340 z_u = vec![0.0; n];
2341 }
2342
2343 let mut y_c_dv = c_space.make_new_dense();
2345 let mut y_d_dv = d_space.make_new_dense();
2346 if m_eq > 0 {
2347 y_c_dv.set_values(&res.lambda_g[..m_eq]);
2348 }
2349 if m_ineq > 0 {
2350 y_d_dv.set_values(&res.lambda_g[m_eq..]);
2351 }
2352 let mut lambda = nlp_borrow.pack_lambda_for_user(&y_c_dv, &y_d_dv);
2353 if lambda.is_empty() {
2354 lambda = vec![0.0; m];
2355 }
2356 drop(nlp_borrow);
2357
2358 let mut g_final = vec![0.0; m];
2359 let _ = tnlp.borrow_mut().eval_g(&x_vec, true, &mut g_final);
2360 let f_final = tnlp
2361 .borrow_mut()
2362 .eval_f(&x_vec, true)
2363 .unwrap_or(Number::NAN);
2364 tnlp.borrow_mut().finalize_solution(
2365 pounce_nlp::tnlp::Solution {
2366 status: solver_status,
2367 x: &x_vec,
2368 z_l: &z_l,
2369 z_u: &z_u,
2370 g: &g_final,
2371 lambda: &lambda,
2372 obj_value: f_final,
2373 },
2374 &TnlpIpoptData::default(),
2375 &TnlpIpoptCq::default(),
2376 );
2377 Ok(f_final)
2378}
2379
2380#[cfg(test)]
2381mod tests {
2382 use super::*;
2383 use pounce_nlp::tnlp::{
2384 BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest,
2385 StartingPoint,
2386 };
2387
2388 struct Hs071Stub;
2389 impl TNLP for Hs071Stub {
2390 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
2391 Some(NlpInfo {
2394 n: 4,
2395 m: 2,
2396 nnz_jac_g: 8,
2397 nnz_h_lag: 10,
2398 index_style: IndexStyle::C,
2399 })
2400 }
2401 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
2402 b.x_l.copy_from_slice(&[1.0; 4]);
2403 b.x_u.copy_from_slice(&[5.0; 4]);
2404 b.g_l.copy_from_slice(&[25.0, 40.0]);
2405 b.g_u.copy_from_slice(&[2.0e19, 40.0]);
2406 true
2407 }
2408 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2409 sp.x.copy_from_slice(&[1.0, 5.0, 5.0, 1.0]);
2410 true
2411 }
2412 fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
2413 Some(x[0] * x[3] * (x[0] + x[1] + x[2]) + x[2])
2414 }
2415 fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
2416 grad.fill(0.0);
2417 true
2418 }
2419 fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
2420 g.fill(0.0);
2421 true
2422 }
2423 fn eval_jac_g(
2424 &mut self,
2425 _x: Option<&[Number]>,
2426 _new_x: bool,
2427 mode: SparsityRequest<'_>,
2428 ) -> bool {
2429 if let SparsityRequest::Structure { irow, jcol } = mode {
2430 irow.copy_from_slice(&[0, 0, 0, 0, 1, 1, 1, 1]);
2431 jcol.copy_from_slice(&[0, 1, 2, 3, 0, 1, 2, 3]);
2432 }
2433 true
2434 }
2435 fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
2436 }
2437
2438 #[test]
2439 fn application_default_does_not_select_sqp() {
2440 let mut app = IpoptApplication::new();
2441 app.initialize().unwrap();
2442 assert!(!app.is_sqp_algorithm_selected());
2443 }
2444
2445 #[test]
2446 fn application_routes_to_sqp_when_algorithm_option_set() {
2447 let mut app = IpoptApplication::new();
2448 app.initialize().unwrap();
2449 app.initialize_with_options_str("algorithm active-set-sqp\n")
2450 .unwrap();
2451 assert!(app.is_sqp_algorithm_selected());
2452 }
2453
2454 struct ConvexEqTnlp {
2461 finalize_called: std::rc::Rc<std::cell::RefCell<Option<(Vec<Number>, Number)>>>,
2462 }
2463 impl TNLP for ConvexEqTnlp {
2464 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
2465 Some(NlpInfo {
2466 n: 2,
2467 m: 1,
2468 nnz_jac_g: 2,
2469 nnz_h_lag: 2,
2470 index_style: IndexStyle::C,
2471 })
2472 }
2473 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
2474 b.x_l.copy_from_slice(&[-2.0e19; 2]);
2475 b.x_u.copy_from_slice(&[2.0e19; 2]);
2476 b.g_l.copy_from_slice(&[1.0]);
2477 b.g_u.copy_from_slice(&[1.0]);
2478 true
2479 }
2480 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
2481 sp.x.copy_from_slice(&[0.0, 0.0]);
2482 true
2483 }
2484 fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
2485 Some(0.5 * (x[0] * x[0] + x[1] * x[1]) - x[0] - 2.0 * x[1])
2486 }
2487 fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
2488 grad[0] = x[0] - 1.0;
2489 grad[1] = x[1] - 2.0;
2490 true
2491 }
2492 fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
2493 g[0] = x[0] + x[1];
2494 true
2495 }
2496 fn eval_jac_g(
2497 &mut self,
2498 _x: Option<&[Number]>,
2499 _new_x: bool,
2500 mode: SparsityRequest<'_>,
2501 ) -> bool {
2502 match mode {
2503 SparsityRequest::Structure { irow, jcol } => {
2504 irow.copy_from_slice(&[0, 0]);
2505 jcol.copy_from_slice(&[0, 1]);
2506 }
2507 SparsityRequest::Values { values, .. } => {
2508 values.copy_from_slice(&[1.0, 1.0]);
2509 }
2510 }
2511 true
2512 }
2513 fn eval_h(
2514 &mut self,
2515 _x: Option<&[Number]>,
2516 _new_x: bool,
2517 _obj_factor: Number,
2518 _lambda: Option<&[Number]>,
2519 _new_lambda: bool,
2520 mode: SparsityRequest<'_>,
2521 ) -> bool {
2522 match mode {
2523 SparsityRequest::Structure { irow, jcol } => {
2524 irow.copy_from_slice(&[0, 1]);
2525 jcol.copy_from_slice(&[0, 1]);
2526 }
2527 SparsityRequest::Values { values, .. } => {
2528 values.copy_from_slice(&[1.0, 1.0]);
2529 }
2530 }
2531 true
2532 }
2533 fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
2534 *self.finalize_called.borrow_mut() = Some((sol.x.to_vec(), sol.obj_value));
2535 }
2536 }
2537
2538 #[test]
2539 fn application_sqp_path_solves_convex_eq_nlp_and_finalizes() {
2540 let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
2541 let tnlp = std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
2542 finalize_called: std::rc::Rc::clone(&finalize_slot),
2543 }));
2544
2545 let mut app = IpoptApplication::new();
2546 app.initialize().unwrap();
2547 app.initialize_with_options_str("algorithm active-set-sqp\n")
2548 .unwrap();
2549 let status = app.optimize_tnlp(tnlp);
2550 assert_eq!(status, ApplicationReturnStatus::SolveSucceeded);
2551
2552 let recv = finalize_slot.borrow().clone();
2554 let (x_recv, obj_recv) = recv.expect("finalize_solution was not called");
2555 assert_eq!(x_recv.len(), 2);
2556 assert!((x_recv[0] - 0.0).abs() < 1e-6, "x[0] = {}", x_recv[0]);
2557 assert!((x_recv[1] - 1.0).abs() < 1e-6, "x[1] = {}", x_recv[1]);
2558 assert!(
2559 (obj_recv - (-1.5)).abs() < 1e-6,
2560 "obj = {} but expected -1.5",
2561 obj_recv
2562 );
2563 }
2564
2565 #[test]
2566 fn application_routes_to_sqp_case_insensitively() {
2567 let mut app = IpoptApplication::new();
2568 app.initialize().unwrap();
2569 app.initialize_with_options_str("algorithm Active-Set-SQP\n")
2570 .unwrap();
2571 assert!(app.is_sqp_algorithm_selected());
2575 }
2576
2577 #[test]
2578 fn application_constructs_and_loads_options() {
2579 let mut app = IpoptApplication::new();
2580 app.initialize().unwrap();
2581 app.initialize_with_options_str("print_level 5\nfile_print_level 7\n")
2584 .unwrap();
2585 let (level, found) = app.options().get_integer_value("print_level", "").unwrap();
2586 assert!(found);
2587 assert_eq!(level, 5);
2588 }
2589
2590 #[test]
2591 fn application_sqp_suboptions_propagate_to_builder() {
2592 let mut app = IpoptApplication::new();
2595 app.initialize().unwrap();
2596 app.initialize_with_options_str(
2597 "algorithm active-set-sqp\n\
2598 sqp_globalization l1-elastic\n\
2599 sqp_hessian lbfgs\n\
2600 sqp_max_iter 17\n\
2601 sqp_tol 1e-7\n\
2602 sqp_constr_viol_tol 1e-5\n\
2603 sqp_dual_inf_tol 1e-3\n\
2604 sqp_l1_penalty 2.5\n\
2605 sqp_bt_reduction 0.25\n\
2606 sqp_bt_min_alpha 1e-10\n\
2607 sqp_print_level 2\n\
2608 sqp_lbfgs_max_history 12\n",
2609 )
2610 .unwrap();
2611 let snap = app.algorithm_builder_snapshot();
2612 assert_eq!(
2613 snap.sqp.globalization,
2614 crate::sqp::SqpGlobalization::L1Elastic
2615 );
2616 assert_eq!(snap.sqp.hessian, crate::sqp::SqpHessianSource::Lbfgs);
2617 assert_eq!(snap.sqp.max_iter, 17);
2618 assert!((snap.sqp.tol - 1e-7).abs() < 1e-18);
2619 assert!((snap.sqp.constr_viol_tol - 1e-5).abs() < 1e-18);
2620 assert!((snap.sqp.dual_inf_tol - 1e-3).abs() < 1e-18);
2621 assert!((snap.sqp.l1_penalty - 2.5).abs() < 1e-18);
2622 assert!((snap.sqp.bt_reduction - 0.25).abs() < 1e-18);
2623 assert!((snap.sqp.bt_min_alpha - 1e-10).abs() < 1e-18);
2624 assert_eq!(snap.sqp.print_level, 2);
2625 assert_eq!(snap.sqp.lbfgs_max_history, 12);
2626 }
2627
2628 #[test]
2629 fn application_sqp_warm_start_round_trip() {
2630 let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
2636 let tnlp_rc: std::rc::Rc<std::cell::RefCell<dyn TNLP>> =
2637 std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
2638 finalize_called: std::rc::Rc::clone(&finalize_slot),
2639 }));
2640
2641 let mut app = IpoptApplication::new();
2642 app.initialize().unwrap();
2643 app.initialize_with_options_str("algorithm active-set-sqp\n")
2644 .unwrap();
2645
2646 let status_a = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
2648 assert_eq!(status_a, ApplicationReturnStatus::SolveSucceeded);
2649 let ws = app.last_sqp_working_set().cloned();
2650 assert!(ws.is_some(), "cold solve must yield a working set");
2651
2652 let (x_recv, _) = finalize_slot.borrow().clone().unwrap();
2656 let warm = crate::sqp::SqpIterates {
2657 x: x_recv,
2658 lambda_g: vec![1.0],
2659 lambda_x: vec![0.0, 0.0],
2660 working: ws,
2661 };
2662 app.set_sqp_warm_start(warm);
2663
2664 let status_b = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
2666 assert_eq!(status_b, ApplicationReturnStatus::SolveSucceeded);
2667 assert!(app.last_sqp_working_set().is_some());
2668 }
2669
2670 #[test]
2671 fn application_sqp_warm_start_auto_clears_after_use() {
2672 let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
2673 let tnlp_rc: std::rc::Rc<std::cell::RefCell<dyn TNLP>> =
2674 std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
2675 finalize_called: std::rc::Rc::clone(&finalize_slot),
2676 }));
2677 let mut app = IpoptApplication::new();
2678 app.initialize().unwrap();
2679 app.initialize_with_options_str("algorithm active-set-sqp\n")
2680 .unwrap();
2681 app.set_sqp_warm_start(crate::sqp::SqpIterates {
2682 x: vec![0.0, 1.0],
2683 lambda_g: vec![1.0],
2684 lambda_x: vec![0.0, 0.0],
2685 working: None,
2686 });
2687 assert!(app.sqp_warm_start.is_some());
2688 let _ = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
2689 assert!(
2690 app.sqp_warm_start.is_none(),
2691 "warm-start input must be auto-cleared after use"
2692 );
2693 }
2694
2695 #[test]
2696 fn application_sqp_suboptions_default_when_unset() {
2697 let mut app = IpoptApplication::new();
2700 app.initialize().unwrap();
2701 let snap = app.algorithm_builder_snapshot();
2702 let d = crate::sqp::SqpOptions::default();
2703 assert_eq!(snap.sqp.globalization, d.globalization);
2704 assert_eq!(snap.sqp.hessian, d.hessian);
2705 assert_eq!(snap.sqp.max_iter, d.max_iter);
2706 assert!((snap.sqp.tol - d.tol).abs() < 1e-18);
2707 assert!((snap.sqp.constr_viol_tol - d.constr_viol_tol).abs() < 1e-18);
2708 assert!((snap.sqp.dual_inf_tol - d.dual_inf_tol).abs() < 1e-18);
2709 assert!((snap.sqp.l1_penalty - d.l1_penalty).abs() < 1e-18);
2710 assert!((snap.sqp.bt_reduction - d.bt_reduction).abs() < 1e-18);
2711 assert!((snap.sqp.bt_min_alpha - d.bt_min_alpha).abs() < 1e-18);
2712 assert_eq!(snap.sqp.print_level, d.print_level);
2713 assert_eq!(snap.sqp.lbfgs_max_history, d.lbfgs_max_history);
2714 }
2715
2716 #[test]
2717 fn application_reports_problem_dimensions() {
2718 let app = IpoptApplication::new();
2719 let mut tnlp = Hs071Stub;
2720 let info = app.problem_dimensions(&mut tnlp).unwrap();
2721 assert_eq!(info.n, 4);
2722 assert_eq!(info.m, 2);
2723 assert_eq!(info.nnz_jac_g, 8);
2724 assert_eq!(info.nnz_h_lag, 10);
2725 }
2726}