use crate::alg_builder::{
AlgorithmBuilder, HessianApproxChoice, LineSearchChoice, LinearBackendFactory,
LinearSolverChoice, MuStrategyChoice,
};
use crate::ipopt_alg::IpoptAlgorithm;
use crate::ipopt_cq::IpoptCalculatedQuantities;
use crate::ipopt_data::IpoptData as AlgIpoptData;
use crate::ipopt_nlp::IpoptNlp;
use crate::iterates_vector::IteratesVector;
use crate::restoration::RestorationPhase;
use crate::upstream_options::register_all_upstream_options;
pub type RestorationFactory = Box<dyn FnMut() -> Box<dyn RestorationPhase>>;
pub type RestorationFactoryProvider = Box<dyn FnMut() -> RestorationFactory>;
pub type ConvergedCallback = Box<
dyn FnMut(
&crate::ipopt_data::IpoptDataHandle,
&crate::ipopt_cq::IpoptCqHandle,
&Rc<RefCell<dyn pounce_nlp::ipopt_nlp::IpoptNlp>>,
Rc<RefCell<crate::kkt::pd_full_space_solver::PdFullSpaceSolver>>,
),
>;
use pounce_common::diagnostics::DiagnosticsState;
use pounce_common::exception::{ExceptionKind, SolverException};
use pounce_common::journalist::{JournalLevel, Journalist};
use pounce_common::options_list::OptionsList;
use pounce_common::reg_options::{PrintOptionsMode, RegisteredOptions};
use pounce_common::timing::TimingStatistics;
use pounce_common::types::{Index, Number};
use pounce_linalg::dense_vector::DenseVectorSpace;
use pounce_linsol::summary::LinearSolverSummary;
use pounce_linsol::SparseSymLinearSolverInterface;
use pounce_nlp::alg_types::SolverReturn;
use pounce_nlp::orig_ipopt_nlp::{NoScaling, OrigIpoptNlp, ScalingMethod};
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::solve_statistics::SolveStatistics;
use pounce_nlp::tnlp::{
IpoptCq as TnlpIpoptCq, IpoptData as TnlpIpoptData, NlpInfo, Solution, TNLP,
};
use pounce_nlp::tnlp_adapter::{
FixedVarTreatment, TNLPAdapter, DEFAULT_NLP_LOWER_BOUND_INF, DEFAULT_NLP_UPPER_BOUND_INF,
};
use std::cell::RefCell;
use std::fmt;
use std::path::Path;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Instant;
pub struct IpoptApplication {
options: OptionsList,
reg_options: Rc<RegisteredOptions>,
journalist: Rc<Journalist>,
statistics: RefCell<SolveStatistics>,
timing: RefCell<Rc<TimingStatistics>>,
linear_backend_factory: Option<LinearBackendFactory>,
restoration_factory: Option<RestorationFactory>,
diagnostics: Option<Rc<DiagnosticsState>>,
restoration_factory_provider: Option<RestorationFactoryProvider>,
on_converged: Option<ConvergedCallback>,
record_iter_history: bool,
linsol_summary_sink: Arc<Mutex<LinearSolverSummary>>,
sqp_warm_start: Option<crate::sqp::SqpIterates>,
sqp_last_working_set: Option<pounce_qp::WorkingSet>,
}
impl fmt::Debug for IpoptApplication {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("IpoptApplication")
.field("options", &self.options)
.field("statistics", &self.statistics)
.finish_non_exhaustive()
}
}
impl Default for IpoptApplication {
fn default() -> Self {
Self::new()
}
}
impl IpoptApplication {
pub fn new() -> Self {
let reg = RegisteredOptions::default();
register_all_upstream_options(®)
.unwrap_or_else(|e| panic!("Upstream options registration failed: {e}"));
pounce_presolve::register_options(®)
.unwrap_or_else(|e| panic!("Presolve options registration failed: {e}"));
let reg = Rc::new(reg);
Self {
options: OptionsList::with_registered(Rc::clone(®)),
reg_options: reg,
journalist: Rc::new(Journalist::new()),
statistics: RefCell::new(SolveStatistics::new()),
timing: RefCell::new(Rc::new(TimingStatistics::new())),
linear_backend_factory: None,
restoration_factory: None,
diagnostics: None,
restoration_factory_provider: None,
on_converged: None,
record_iter_history: false,
linsol_summary_sink: Arc::new(Mutex::new(LinearSolverSummary::default())),
sqp_warm_start: None,
sqp_last_working_set: None,
}
}
pub fn options(&self) -> &OptionsList {
&self.options
}
pub fn options_mut(&mut self) -> &mut OptionsList {
&mut self.options
}
pub fn registered_options(&self) -> &Rc<RegisteredOptions> {
&self.reg_options
}
pub fn journalist(&self) -> &Rc<Journalist> {
&self.journalist
}
pub fn set_linear_backend_factory(&mut self, factory: LinearBackendFactory) {
self.linear_backend_factory = Some(factory);
}
pub fn set_restoration_factory(&mut self, factory: RestorationFactory) {
self.restoration_factory = Some(factory);
}
pub fn set_diagnostics(&mut self, diag: Rc<DiagnosticsState>) {
self.diagnostics = Some(diag);
}
pub fn diagnostics(&self) -> Option<Rc<DiagnosticsState>> {
self.diagnostics.as_ref().map(Rc::clone)
}
pub fn set_restoration_factory_provider(&mut self, provider: RestorationFactoryProvider) {
self.restoration_factory_provider = Some(provider);
}
pub fn set_on_converged(&mut self, cb: ConvergedCallback) {
self.on_converged = Some(cb);
}
pub fn enable_iter_history(&mut self) {
self.record_iter_history = true;
}
pub fn initialize_with_options_file(&mut self, path: &Path) -> Result<(), SolverException> {
let txt = std::fs::read_to_string(path).map_err(|e| {
SolverException::new(
ExceptionKind::IPOPT_APPLICATION_ERROR,
format!("could not read options file {}: {}", path.display(), e),
file!(),
line!() as Index,
)
})?;
self.options.read_from_str(&txt, true)?;
self.open_output_file_journal();
Ok(())
}
pub fn initialize_with_options_str(&mut self, s: &str) -> Result<(), SolverException> {
self.options.read_from_str(s, true)?;
self.open_output_file_journal();
Ok(())
}
fn open_output_file_journal(&self) {
let fname = match self.options.get_string_value("output_file", "") {
Ok((v, true)) if !v.is_empty() => v,
_ => return,
};
let level_int = self
.options
.get_integer_value("file_print_level", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(5);
let level = journal_level_from_int(level_int);
let append = self
.options
.get_bool_value("file_append", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(false);
let jname = format!("OutputFile:{}", fname);
let _ = self
.journalist
.add_file_journal(&jname, &fname, level, append);
}
pub fn initialize(&mut self) -> Result<(), SolverException> {
Ok(())
}
pub fn open_output_file(&mut self, fname: &str, print_level: i32) -> bool {
if self
.options
.set_string_value("output_file", fname, true, false)
.is_err()
{
return false;
}
if self
.options
.set_integer_value("file_print_level", print_level as Index, true, false)
.is_err()
{
return false;
}
let level = journal_level_from_int(print_level);
let jname = format!("OutputFile:{}", fname);
self.journalist
.add_file_journal(&jname, fname, level, false)
.is_some()
}
pub fn problem_dimensions(&self, tnlp: &mut dyn TNLP) -> Option<NlpInfo> {
tnlp.get_nlp_info()
}
pub fn statistics(&self) -> SolveStatistics {
self.statistics.borrow().clone()
}
pub fn timing_stats(&self) -> Rc<TimingStatistics> {
Rc::clone(&self.timing.borrow())
}
pub fn linear_solver_summary(&self) -> Option<LinearSolverSummary> {
let guard = self.linsol_summary_sink.lock().ok()?;
if guard.is_empty() {
None
} else {
Some(guard.clone())
}
}
pub fn optimize_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
if self.is_sqp_algorithm_selected() {
return self.optimize_sqp_tnlp(tnlp);
}
let info = match tnlp.borrow_mut().get_nlp_info() {
Some(info) => info,
None => return ApplicationReturnStatus::InvalidProblemDefinition,
};
if info.m > 0 && self.is_l1_penalty_enabled() {
if let Some(status) = self.run_l1_penalty_outer_loop(Rc::clone(&tnlp)) {
return status;
}
}
if info.m > 0 && self.is_l1_fallback_enabled() && !self.is_l1_penalty_enabled() {
return self.run_with_l1_fallback(tnlp);
}
self.optimize_constrained(tnlp)
}
fn is_l1_penalty_enabled(&self) -> bool {
self.options
.get_bool_value("l1_exact_penalty_barrier", "")
.ok()
.and_then(|(v, found)| found.then_some(v))
.unwrap_or(false)
}
fn l1_penalty_init(&self) -> Number {
self.options
.get_numeric_value("l1_penalty_init", "")
.ok()
.and_then(|(v, found)| found.then_some(v))
.unwrap_or(1.0)
}
fn l1_penalty_max(&self) -> Number {
self.options
.get_numeric_value("l1_penalty_max", "")
.ok()
.and_then(|(v, found)| found.then_some(v))
.unwrap_or(1.0e6)
}
fn l1_penalty_increase_factor(&self) -> Number {
self.options
.get_numeric_value("l1_penalty_increase_factor", "")
.ok()
.and_then(|(v, found)| found.then_some(v))
.unwrap_or(8.0)
}
fn l1_penalty_max_outer_iter(&self) -> usize {
self.options
.get_integer_value("l1_penalty_max_outer_iter", "")
.ok()
.and_then(|(v, found)| found.then_some(v))
.unwrap_or(8) as usize
}
fn l1_slack_tol(&self) -> Number {
self.options
.get_numeric_value("l1_slack_tol", "")
.ok()
.and_then(|(v, found)| found.then_some(v))
.unwrap_or(1.0e-6)
}
fn l1_steering_factor(&self) -> Number {
self.options
.get_numeric_value("l1_steering_factor", "")
.ok()
.and_then(|(v, found)| found.then_some(v))
.unwrap_or(10.0)
}
fn is_l1_fallback_enabled(&self) -> bool {
self.options
.get_bool_value("l1_fallback_on_restoration_failure", "")
.ok()
.and_then(|(v, found)| found.then_some(v))
.unwrap_or(false)
}
pub fn set_sqp_warm_start(&mut self, warm: crate::sqp::SqpIterates) {
self.sqp_warm_start = Some(warm);
}
pub fn clear_sqp_warm_start(&mut self) {
self.sqp_warm_start = None;
}
pub fn last_sqp_working_set(&self) -> Option<&pounce_qp::WorkingSet> {
self.sqp_last_working_set.as_ref()
}
fn is_sqp_algorithm_selected(&self) -> bool {
match self.options.get_string_value("algorithm", "") {
Ok((v, true)) => v.eq_ignore_ascii_case("active-set-sqp"),
_ => false,
}
}
fn optimize_sqp_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
use pounce_nlp::orig_ipopt_nlp::OrigIpoptNlp;
use pounce_nlp::tnlp_adapter::TNLPAdapter;
use pounce_nlp::NoScaling;
let adapter = match TNLPAdapter::new(Rc::clone(&tnlp)) {
Ok(a) => Rc::new(RefCell::new(a)),
Err(_) => return ApplicationReturnStatus::InvalidProblemDefinition,
};
let orig_nlp = match OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)) {
Ok(n) => n,
Err(_) => return ApplicationReturnStatus::InternalError,
};
let nlp_rc: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(orig_nlp));
let mut sqp_adapter = crate::sqp::IpoptNlpAdapter::new(Rc::clone(&nlp_rc));
let mut builder = self.algorithm_builder_snapshot();
builder.algorithm = crate::alg_builder::AlgorithmChoice::ActiveSetSqp;
let factory = self.make_backend_factory();
let mut alg = match builder.build_sqp_with_backend(factory) {
Some(a) => a,
None => return ApplicationReturnStatus::InternalError,
};
let warm = self.sqp_warm_start.take();
let res = match alg.optimize_with_warm_start(&mut sqp_adapter, warm) {
Ok(r) => r,
Err(e) => {
if std::env::var_os("POUNCE_DBG_SQP").is_some() {
tracing::warn!(target: "pounce::sqp", "[SQP] optimize_with_warm_start error: {e:?}");
}
return ApplicationReturnStatus::InternalError;
}
};
self.sqp_last_working_set = res.working_set.clone();
{
let mut stats = self.statistics.borrow_mut();
stats.iteration_count = res.n_iter as Index;
stats.final_objective = res.obj;
stats.final_dual_inf = res.final_stationarity;
stats.final_constr_viol = res.final_constr_viol;
stats.final_compl = 0.0; }
let (app_status, solver_status) = match res.status {
crate::sqp::SqpStatus::Optimal => (
ApplicationReturnStatus::SolveSucceeded,
pounce_nlp::SolverReturn::Success,
),
crate::sqp::SqpStatus::MaxIter => (
ApplicationReturnStatus::MaximumIterationsExceeded,
pounce_nlp::SolverReturn::MaxiterExceeded,
),
crate::sqp::SqpStatus::InfeasibleSubproblem => (
ApplicationReturnStatus::InfeasibleProblemDetected,
pounce_nlp::SolverReturn::LocalInfeasibility,
),
crate::sqp::SqpStatus::LineSearchFailed => (
ApplicationReturnStatus::SearchDirectionBecomesTooSmall,
pounce_nlp::SolverReturn::ErrorInStepComputation,
),
};
let _ = finalize_via_sqp(&nlp_rc, &res, solver_status, &tnlp);
app_status
}
fn algorithm_builder_snapshot(&self) -> AlgorithmBuilder {
let mut builder = AlgorithmBuilder::default();
apply_sqp_options(&self.options, &mut builder.sqp);
builder
}
fn make_backend_factory(&self) -> LinearBackendFactory {
Box::new(
|_choice| -> Box<dyn pounce_linsol::SparseSymLinearSolverInterface> {
Box::new(pounce_feral::FeralSolverInterface::new())
},
)
}
fn run_with_l1_fallback(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
let first_status = self.optimize_constrained(Rc::clone(&tnlp));
if !is_l1_fallback_trigger(first_status) {
return first_status;
}
let prev = self
.options
.get_string_value("l1_exact_penalty_barrier", "")
.ok();
let _ = self
.options
.set_string_value("l1_exact_penalty_barrier", "yes", true, false);
let retry_status = self
.run_l1_penalty_outer_loop(Rc::clone(&tnlp))
.unwrap_or(ApplicationReturnStatus::InternalError);
let _ = self.options.set_string_value(
"l1_exact_penalty_barrier",
prev.as_ref().map(|(v, _)| v.as_str()).unwrap_or("no"),
true,
false,
);
if matches!(retry_status, ApplicationReturnStatus::SolveSucceeded) {
retry_status
} else {
first_status
}
}
fn run_l1_penalty_outer_loop(
&mut self,
tnlp: Rc<RefCell<dyn TNLP>>,
) -> Option<ApplicationReturnStatus> {
let rho_init = self.l1_penalty_init();
let rho_max = self.l1_penalty_max().max(rho_init);
let factor = self.l1_penalty_increase_factor().max(1.0);
let tau = self.l1_steering_factor();
let slack_tol = self.l1_slack_tol();
let max_outer = self.l1_penalty_max_outer_iter().max(1);
let mut wrapper = pounce_l1penalty::L1PenaltyBarrierTnlp::new(Rc::clone(&tnlp), rho_init)?;
if wrapper.m_eq() == 0 {
return None;
}
wrapper.set_defer_inner_finalize(true);
let wrapper_rc = Rc::new(RefCell::new(wrapper));
let mut rho = rho_init;
let mut last_status = ApplicationReturnStatus::InternalError;
for _outer in 0..max_outer {
wrapper_rc.borrow_mut().set_rho(rho);
let dyn_tnlp: Rc<RefCell<dyn TNLP>> = wrapper_rc.clone();
last_status = self.optimize_constrained(dyn_tnlp);
let w = wrapper_rc.borrow();
if !w.has_solution() {
drop(w);
break;
}
let slack_sum = w.last_slack_sum();
let y_eq_inf = w.last_y_eq_inf_norm();
drop(w);
let inner_ok = matches!(
last_status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
);
if !inner_ok {
break;
}
if slack_sum.is_finite() && slack_sum <= slack_tol {
break;
}
if rho >= rho_max {
break;
}
let geom = rho * factor;
let steer = tau * y_eq_inf + 1.0e-12;
rho = geom.max(steer).min(rho_max);
}
let w = wrapper_rc.borrow();
if w.has_solution() {
let x_trunc: Vec<Number> = w.last_x_trunc().to_vec();
let lambda: Vec<Number> = w.last_lambda().to_vec();
let z_l: Vec<Number> = w.last_z_l_trunc().to_vec();
let z_u: Vec<Number> = w.last_z_u_trunc().to_vec();
let solver_status = w.last_status().unwrap_or(SolverReturn::InternalError);
let slack_sum = w.last_slack_sum();
drop(w);
let infeasible_certificate = matches!(
last_status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
) && slack_sum.is_finite()
&& slack_sum > slack_tol;
let final_app_status = if infeasible_certificate {
ApplicationReturnStatus::InfeasibleProblemDetected
} else {
last_status
};
let final_solver_status = if infeasible_certificate {
SolverReturn::LocalInfeasibility
} else {
solver_status
};
let f_inner = tnlp
.borrow_mut()
.eval_f(&x_trunc, true)
.unwrap_or(Number::NAN);
let m = tnlp
.borrow_mut()
.get_nlp_info()
.map(|i| i.m as usize)
.unwrap_or(0);
let mut g_inner = vec![0.0; m];
if m > 0 {
let _ = tnlp.borrow_mut().eval_g(&x_trunc, false, &mut g_inner);
}
tnlp.borrow_mut().finalize_solution(
Solution {
status: final_solver_status,
x: &x_trunc,
z_l: &z_l,
z_u: &z_u,
g: &g_inner,
lambda: &lambda,
obj_value: f_inner,
},
&TnlpIpoptData::default(),
&TnlpIpoptCq::default(),
);
return Some(final_app_status);
}
Some(last_status)
}
pub fn reoptimize_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
self.optimize_tnlp(tnlp)
}
fn optimize_constrained(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
let t_start = Instant::now();
let print_opts = self
.options
.get_bool_value("print_user_options", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(false);
if print_opts {
print!(
"\nList of user-set options:\n\n{}",
self.options.print_user_options()
);
}
let print_doc = self
.options
.get_bool_value("print_options_documentation", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(false);
if print_doc {
let mode = self
.options
.get_string_value("print_options_mode", "")
.ok()
.map(|(v, _)| PrintOptionsMode::from_tag(&v))
.unwrap_or(PrintOptionsMode::Text);
let advanced = self
.options
.get_bool_value("print_advanced_options", "")
.ok()
.map(|(v, _)| v)
.unwrap_or(false);
print!(
"\n# Pounce options registry\n\n{}",
self.reg_options.print_options_documentation(mode, advanced)
);
}
let timing = Rc::new(TimingStatistics::new());
*self.timing.borrow_mut() = Rc::clone(&timing);
timing.overall_alg.start();
if let Ok(mut guard) = self.linsol_summary_sink.lock() {
*guard = LinearSolverSummary::default();
} else {
debug_assert!(false, "linsol summary sink mutex poisoned");
}
let lo_inf = self
.options
.get_numeric_value("nlp_lower_bound_inf", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(DEFAULT_NLP_LOWER_BOUND_INF);
let up_inf = self
.options
.get_numeric_value("nlp_upper_bound_inf", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(DEFAULT_NLP_UPPER_BOUND_INF);
let fixed_treatment = match self
.options
.get_string_value("fixed_variable_treatment", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.as_deref()
{
Some("relax_bounds") => FixedVarTreatment::RelaxBounds,
_ => FixedVarTreatment::MakeParameter,
};
let adapter = match TNLPAdapter::new_with_options(
Rc::clone(&tnlp),
lo_inf,
up_inf,
fixed_treatment,
) {
Ok(a) => Rc::new(RefCell::new(a)),
Err(_) => {
timing.overall_alg.end();
return ApplicationReturnStatus::InvalidProblemDefinition;
}
};
let mut orig_nlp = match OrigIpoptNlp::new(Rc::clone(&adapter), Rc::new(NoScaling)) {
Ok(n) => n,
Err(_) => {
timing.overall_alg.end();
return ApplicationReturnStatus::InternalError;
}
};
orig_nlp.set_timing_stats(Rc::clone(&timing));
let n_x_var = orig_nlp.x_space().dim();
let n_c = orig_nlp.c_space().dim();
if n_x_var > 0 && n_x_var < n_c {
timing.overall_alg.end();
return ApplicationReturnStatus::NotEnoughDegreesOfFreedom;
}
let bound_relax_factor = self
.options
.get_numeric_value("bound_relax_factor", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1e-8);
let constr_viol_tol = self
.options
.get_numeric_value("constr_viol_tol", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1e-4);
orig_nlp.relax_bounds(bound_relax_factor, constr_viol_tol);
let scaling_method = self
.options
.get_string_value("nlp_scaling_method", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or_else(|| "gradient-based".to_string());
let scaling_method = match scaling_method.as_str() {
"none" => ScalingMethod::None,
"gradient-based" => ScalingMethod::GradientBased,
"user-scaling" => ScalingMethod::UserScaling,
_ => ScalingMethod::GradientBased,
};
let max_gradient = self
.options
.get_numeric_value("nlp_scaling_max_gradient", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(100.0);
let min_value = self
.options
.get_numeric_value("nlp_scaling_min_value", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1e-8);
let obj_target_gradient = self
.options
.get_numeric_value("nlp_scaling_obj_target_gradient", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(0.0);
let constr_target_gradient = self
.options
.get_numeric_value("nlp_scaling_constr_target_gradient", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(0.0);
orig_nlp.determine_scaling_from_starting_point(
scaling_method,
max_gradient,
min_value,
obj_target_gradient,
constr_target_gradient,
);
let nlp_handle: Rc<RefCell<dyn IpoptNlp>> = Rc::new(RefCell::new(orig_nlp));
let builder = self.algorithm_builder_from_options();
let feral_cfg = feral_config_from_options(&self.options);
let factory = self.linear_backend_factory.take().unwrap_or_else(|| {
default_backend_factory_with_sink(feral_cfg, Arc::clone(&self.linsol_summary_sink))
});
let bundle = builder.build_with_backend(factory);
let data: crate::ipopt_data::IpoptDataHandle = Rc::new(RefCell::new(AlgIpoptData::new()));
data.borrow_mut().timing = Rc::clone(&timing);
let cq: crate::ipopt_cq::IpoptCqHandle = Rc::new(RefCell::new(
IpoptCalculatedQuantities::new(Rc::clone(&data), Rc::clone(&nlp_handle)),
));
{
let nlp_borrow = nlp_handle.borrow();
let n_x = nlp_borrow.n();
let n_s = nlp_borrow.m_ineq();
let n_yc = nlp_borrow.m_eq();
let n_yd = nlp_borrow.m_ineq();
let n_zl = nlp_borrow.x_l().dim();
let n_zu = nlp_borrow.x_u().dim();
let n_vl = nlp_borrow.d_l().dim();
let n_vu = nlp_borrow.d_u().dim();
drop(nlp_borrow);
let iv = IteratesVector::new(
Rc::new(DenseVectorSpace::new(n_x).make_new_dense()),
Rc::new(DenseVectorSpace::new(n_s).make_new_dense()),
Rc::new(DenseVectorSpace::new(n_yc).make_new_dense()),
Rc::new(DenseVectorSpace::new(n_yd).make_new_dense()),
Rc::new(DenseVectorSpace::new(n_zl).make_new_dense()),
Rc::new(DenseVectorSpace::new(n_zu).make_new_dense()),
Rc::new(DenseVectorSpace::new(n_vl).make_new_dense()),
Rc::new(DenseVectorSpace::new(n_vu).make_new_dense()),
);
data.borrow_mut().set_curr(iv);
}
let max_iter = self
.options
.get_integer_value("max_iter", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(3000);
let tol = self
.options
.get_numeric_value("tol", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1e-8);
data.borrow_mut().tol = tol;
let mut alg = IpoptAlgorithm::new(data, cq, bundle)
.with_nlp(Rc::clone(&nlp_handle))
.with_tnlp(Rc::clone(&tnlp));
if let Some(provider) = self.restoration_factory_provider.as_mut() {
self.restoration_factory = Some(provider());
}
if let Some(factory) = self.restoration_factory.as_mut() {
alg = alg.with_restoration(factory());
}
if let Some(diag) = self.diagnostics.as_ref() {
alg = alg.with_diagnostics(Rc::clone(diag));
}
alg.max_iter = max_iter;
if let Ok((v, found)) = self.options.get_integer_value("print_level", "") {
if found && v <= 0 {
alg.print_iter_output = false;
if let Some(resto) = alg.restoration.as_mut() {
resto.set_print_iter_output(false);
}
}
}
let iter_capture = self
.record_iter_history
.then(pounce_observability::IterCaptureGuard::start);
let solver_status = alg.optimize();
let captured_iters = iter_capture.map(|g| g.finish()).unwrap_or_default();
timing.overall_alg.end();
{
let mut stats = self.statistics.borrow_mut();
{
let d = alg.data.borrow();
stats.iteration_count = d.iter_count;
stats.final_mu = d.curr_mu;
}
stats.total_wallclock_time_secs = t_start.elapsed().as_secs_f64();
stats.restoration_calls = alg.resto_calls;
stats.restoration_inner_iters = alg.resto_inner_iters;
stats.restoration_outer_iters = alg.resto_outer_iters;
stats.restoration_wall_secs = alg.resto_wall_secs;
stats.iterations = captured_iters;
let curr_x = alg.data.borrow().curr.as_ref().map(|c| c.x.clone());
if let Some(x) = curr_x {
if let Ok(f) = try_eval_curr_f(&nlp_handle, &x) {
stats.final_objective = f;
stats.final_scaled_objective = f;
}
}
let cq = alg.cq.borrow();
stats.final_dual_inf = cq.curr_dual_infeasibility_max();
stats.final_constr_viol = cq.curr_primal_infeasibility_max();
let compl = cq
.curr_compl_x_l()
.amax()
.max(cq.curr_compl_x_u().amax())
.max(cq.curr_compl_s_l().amax())
.max(cq.curr_compl_s_u().amax());
stats.final_compl = compl;
stats.final_kkt_error = cq.curr_nlp_error();
}
let app_status = solver_return_to_app_status(solver_status);
if matches!(
app_status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
) {
if let Some(cb) = self.on_converged.as_mut() {
if let Some(sd) = alg.search_dir.as_mut() {
let pd = sd.pd_solver_rc();
cb(&alg.data, &alg.cq, &nlp_handle, pd);
}
}
}
match finalize_via_orig_nlp(&nlp_handle, &alg, solver_status, app_status, &tnlp) {
Ok(f_unscaled) => {
self.statistics.borrow_mut().final_objective = f_unscaled;
}
Err(()) => {
}
}
let print_timing = self
.options
.get_bool_value("print_timing_statistics", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(false);
if print_timing {
let report = timing.report();
print!("{}", report);
use pounce_common::journalist::{JournalCategory, JournalLevel};
self.journalist.print(
JournalLevel::J_SUMMARY,
JournalCategory::J_TIMING_STATISTICS,
&report,
);
}
app_status
}
pub fn algorithm_builder_from_options(&self) -> AlgorithmBuilder {
let mut builder = AlgorithmBuilder::new();
let mut mehrotra_on = false;
if let Ok((v, found)) = self.options.get_string_value("mehrotra_algorithm", "") {
if found && v == "yes" {
mehrotra_on = true;
builder.mehrotra_algorithm = true;
builder.mu_strategy = MuStrategyChoice::Adaptive;
builder.mu_oracle = crate::mu::adaptive::MuOracleKind::Probing;
builder.line_search.accept_every_trial_step = true;
builder.init.bound_push = 10.0;
builder.init.bound_frac = 0.2;
builder.init.slack_bound_push = 10.0;
builder.init.slack_bound_frac = 0.2;
builder.init.bound_mult_init_val = 10.0;
builder.init.constr_mult_init_max = 0.0;
builder.line_search.alpha_for_y =
crate::line_search::backtracking::AlphaForY::BoundMult;
builder.mu.adaptive_mu_globalization =
crate::mu::adaptive::AdaptiveMuGlobalization::NeverMonotoneMode;
builder.init.least_square_init_primal = true;
}
}
if let Ok((v, found)) = self.options.get_string_value("mu_strategy", "") {
if found {
let parsed = match v.as_str() {
"adaptive" => MuStrategyChoice::Adaptive,
_ => MuStrategyChoice::Monotone,
};
if mehrotra_on && matches!(parsed, MuStrategyChoice::Monotone) {
tracing::warn!(target: "pounce::algorithm",
"pounce: mehrotra_algorithm=yes requires \
mu_strategy=adaptive; ignoring \
mu_strategy=monotone."
);
} else {
builder.mu_strategy = parsed;
}
}
}
if let Ok((v, found)) = self.options.get_string_value("mu_oracle", "") {
if found {
builder.mu_oracle = match v.as_str() {
"loqo" => crate::mu::adaptive::MuOracleKind::Loqo,
"probing" => crate::mu::adaptive::MuOracleKind::Probing,
_ => crate::mu::adaptive::MuOracleKind::QualityFunction,
};
}
}
if let Ok((v, found)) = self
.options
.get_string_value("adaptive_mu_globalization", "")
{
if found {
use crate::mu::adaptive::AdaptiveMuGlobalization;
builder.mu.adaptive_mu_globalization = match v.as_str() {
"kkt-error" => AdaptiveMuGlobalization::KktError,
"never-monotone-mode" => AdaptiveMuGlobalization::NeverMonotoneMode,
_ => AdaptiveMuGlobalization::ObjConstrFilter,
};
}
}
if let Ok((v, found)) = self.options.get_string_value("hessian_approximation", "") {
if found {
builder.hessian_approximation = match v.as_str() {
"limited-memory" => HessianApproxChoice::LimitedMemory,
_ => HessianApproxChoice::Exact,
};
}
}
if let Ok((v, found)) = self.options.get_string_value("line_search_method", "") {
if found {
builder.line_search_method = match v.as_str() {
"cg-penalty" => LineSearchChoice::CgPenalty,
"penalty" => LineSearchChoice::Penalty,
_ => LineSearchChoice::Filter,
};
}
}
if let Ok((v, found)) = self.options.get_string_value("accept_every_trial_step", "") {
if found {
builder.line_search.accept_every_trial_step = v == "yes";
}
}
if let Ok((v, found)) = self.options.get_string_value("alpha_for_y", "") {
if found {
use crate::line_search::backtracking::AlphaForY;
builder.line_search.alpha_for_y = match v.as_str() {
"primal" => AlphaForY::Primal,
"bound-mult" | "bound_mult" => AlphaForY::BoundMult,
"full" => AlphaForY::Full,
"min" => AlphaForY::Min,
"max" => AlphaForY::Max,
"primal-and-full" | "dual-and-full" => AlphaForY::Primal,
_ => AlphaForY::Primal,
};
}
}
if let Ok((v, _found)) = self.options.get_string_value("linear_solver", "") {
builder.linear_solver = match v.as_str() {
"ma57" => LinearSolverChoice::Ma57,
_ => LinearSolverChoice::Feral,
};
}
if let Ok((v, found)) = self.options.get_string_value("linear_system_scaling", "") {
if found {
builder.linear_system_scaling = match v.as_str() {
"ruiz" => crate::alg_builder::LinearSystemScalingChoice::Ruiz,
"mc19" => crate::alg_builder::LinearSystemScalingChoice::Mc19,
_ => crate::alg_builder::LinearSystemScalingChoice::None,
};
}
}
if let Ok((v, found)) = self.options.get_bool_value("linear_scaling_on_demand", "") {
if found {
builder.linear_scaling_on_demand = v;
}
}
let read_num = |key: &str| -> Option<f64> {
self.options
.get_numeric_value(key, "")
.ok()
.and_then(|(v, f)| f.then_some(v))
};
let read_int = |key: &str| -> Option<i32> {
self.options
.get_integer_value(key, "")
.ok()
.and_then(|(v, f)| f.then_some(v))
};
if let Some(v) = read_num("tol") {
builder.conv_check.tol = v;
}
if let Some(v) = read_num("dual_inf_tol") {
builder.conv_check.dual_inf_tol = v;
}
if let Some(v) = read_num("constr_viol_tol") {
builder.conv_check.constr_viol_tol = v;
}
if let Some(v) = read_num("compl_inf_tol") {
builder.conv_check.compl_inf_tol = v;
}
if let Some(v) = read_int("max_iter") {
builder.conv_check.max_iter = v;
}
if let Some(v) = read_num("max_cpu_time") {
builder.conv_check.max_cpu_time = v;
}
if let Some(v) = read_num("max_wall_time") {
builder.conv_check.max_wall_time = v;
}
if let Some(v) = read_num("acceptable_tol") {
builder.conv_check.acceptable_tol = v;
}
if let Some(v) = read_num("acceptable_dual_inf_tol") {
builder.conv_check.acceptable_dual_inf_tol = v;
}
if let Some(v) = read_num("acceptable_constr_viol_tol") {
builder.conv_check.acceptable_constr_viol_tol = v;
}
if let Some(v) = read_num("acceptable_compl_inf_tol") {
builder.conv_check.acceptable_compl_inf_tol = v;
}
if let Some(v) = read_num("acceptable_obj_change_tol") {
builder.conv_check.acceptable_obj_change_tol = v;
}
if let Some(v) = read_int("acceptable_iter") {
builder.conv_check.acceptable_iter = v;
}
if let Some(v) = read_num("infeas_stationarity_tol") {
builder.conv_check.infeas_stationarity_tol = v;
}
if let Some(v) = read_num("infeas_viol_kappa") {
builder.conv_check.infeas_viol_kappa = v;
}
if let Some(v) = read_int("infeas_max_streak") {
builder.conv_check.infeas_max_streak = v;
}
if let Some(v) = read_num("mu_init") {
builder.mu.mu_init = v;
}
if let Some(v) = read_num("mu_max") {
builder.mu.mu_max = v;
}
if let Some(v) = read_num("mu_max_fact") {
builder.mu.mu_max_fact = v;
}
if let Some(v) = read_num("mu_min") {
builder.mu.mu_min = v;
}
if let Some(v) = read_num("mu_target") {
builder.mu.mu_target = v;
}
if let Some(v) = read_num("mu_linear_decrease_factor") {
builder.mu.mu_linear_decrease_factor = v;
}
if let Some(v) = read_num("mu_superlinear_decrease_power") {
builder.mu.mu_superlinear_decrease_power = v;
}
if let Ok((v, found)) = self
.options
.get_string_value("mu_allow_fast_monotone_decrease", "")
{
if found {
builder.mu.mu_allow_fast_monotone_decrease = v == "yes";
}
}
if let Some(v) = read_num("barrier_tol_factor") {
builder.mu.barrier_tol_factor = v;
}
if let Some(v) = read_num("sigma_max") {
builder.mu.sigma_max = v;
}
if let Some(v) = read_num("sigma_min") {
builder.mu.sigma_min = v;
}
if let Ok((v, found)) = self
.options
.get_string_value("quality_function_norm_type", "")
{
if found {
use crate::mu::oracle::quality_function::NormType;
builder.mu.quality_function_norm_type = match v.as_str() {
"1-norm" => NormType::OneNorm,
"2-norm" => NormType::TwoNorm,
"max-norm" => NormType::MaxNorm,
_ => NormType::TwoNormSquared,
};
}
}
if let Ok((v, found)) = self
.options
.get_string_value("quality_function_centrality", "")
{
if found {
use crate::mu::oracle::quality_function::CentralityType;
builder.mu.quality_function_centrality = match v.as_str() {
"log" => CentralityType::LogCenter,
"reciprocal" => CentralityType::ReciprocalCenter,
"cubed-reciprocal" => CentralityType::CubedReciprocalCenter,
_ => CentralityType::None,
};
}
}
if let Ok((v, found)) = self
.options
.get_string_value("quality_function_balancing_term", "")
{
if found {
use crate::mu::oracle::quality_function::BalancingTermType;
builder.mu.quality_function_balancing_term = match v.as_str() {
"cubic" => BalancingTermType::CubicTerm,
_ => BalancingTermType::None,
};
}
}
if let Some(v) = read_int("quality_function_max_section_steps") {
builder.mu.quality_function_max_section_steps = v;
}
if let Some(v) = read_num("quality_function_section_sigma_tol") {
builder.mu.quality_function_section_sigma_tol = v;
}
if let Some(v) = read_num("quality_function_section_qf_tol") {
builder.mu.quality_function_section_qf_tol = v;
}
if let Some(v) = read_num("probing_iterate_quality_factor") {
builder.mu.probing_iterate_quality_factor = v;
}
if let Some(v) = read_num("adaptive_mu_safeguard_factor") {
builder.mu.adaptive_mu_safeguard_factor = v;
}
if let Some(v) = read_num("adaptive_mu_monotone_init_factor") {
builder.mu.adaptive_mu_monotone_init_factor = v;
}
if let Ok((v, found)) = self
.options
.get_bool_value("adaptive_mu_restore_previous_iterate", "")
{
if found {
builder.mu.adaptive_mu_restore_previous_iterate = v;
}
}
if let Some(v) = read_int("adaptive_mu_kkterror_red_iters") {
if v >= 0 {
builder.mu.adaptive_mu_kkterror_red_iters = v as usize;
}
}
if let Some(v) = read_num("adaptive_mu_kkterror_red_fact") {
builder.mu.adaptive_mu_kkterror_red_fact = v;
}
if let Ok((v, found)) = self
.options
.get_string_value("adaptive_mu_kkt_norm_type", "")
{
if found {
use crate::mu::adaptive::AdaptiveMuKktNorm;
builder.mu.adaptive_mu_kkt_norm_type = match v.as_str() {
"1-norm" => AdaptiveMuKktNorm::OneNorm,
"2-norm" => AdaptiveMuKktNorm::TwoNorm,
"max-norm" => AdaptiveMuKktNorm::MaxNorm,
_ => AdaptiveMuKktNorm::TwoNormSquared,
};
}
}
if let Some(v) = read_int("watchdog_shortened_iter_trigger") {
builder.line_search.watchdog_shortened_iter_trigger = v;
}
if let Some(v) = read_int("watchdog_trial_iter_max") {
builder.line_search.watchdog_trial_iter_max = v;
}
if let Some(v) = read_num("soft_resto_pderror_reduction_factor") {
builder.line_search.soft_resto_pderror_reduction_factor = v;
}
if let Some(v) = read_int("max_soft_resto_iters") {
builder.line_search.max_soft_resto_iters = v;
}
if let Some(v) = read_int("print_frequency_iter") {
builder.output.print_frequency_iter = v;
}
if let Some(v) = read_num("print_frequency_time") {
builder.output.print_frequency_time = v;
}
if let Ok((v, found)) = self.options.get_bool_value("print_info_string", "") {
if found {
builder.output.print_info_string = v;
}
}
if let Ok((v, found)) = self.options.get_string_value("inf_pr_output", "") {
if found {
builder.output.inf_pr_output_internal = v == "internal";
}
}
if let Ok((v, found)) = self.options.get_bool_value("warm_start_init_point", "") {
if found {
builder.warm_start_init_point = v;
}
}
if let Ok((v, found)) = self.options.get_bool_value("warm_start_same_structure", "") {
if found {
builder.warm.same_structure = v;
}
}
if let Some(v) = read_num("warm_start_bound_push") {
builder.warm.bound_push = v;
}
if let Some(v) = read_num("warm_start_bound_frac") {
builder.warm.bound_frac = v;
}
if let Some(v) = read_num("warm_start_slack_bound_push") {
builder.warm.slack_bound_push = v;
}
if let Some(v) = read_num("warm_start_slack_bound_frac") {
builder.warm.slack_bound_frac = v;
}
if let Some(v) = read_num("warm_start_mult_bound_push") {
builder.warm.mult_bound_push = v;
}
if let Some(v) = read_num("warm_start_mult_init_max") {
builder.warm.mult_init_max = v;
}
if let Some(v) = read_num("warm_start_target_mu") {
builder.warm.target_mu = v;
}
if let Ok((v, found)) = self
.options
.get_string_value("warm_start_entire_iterate", "")
{
if found {
builder.warm.entire_iterate = v == "yes";
}
}
if let Some(v) = read_num("bound_push") {
builder.init.bound_push = v;
}
if let Some(v) = read_num("bound_frac") {
builder.init.bound_frac = v;
}
if let Some(v) = read_num("slack_bound_push") {
builder.init.slack_bound_push = v;
}
if let Some(v) = read_num("slack_bound_frac") {
builder.init.slack_bound_frac = v;
}
if let Some(v) = read_num("constr_mult_init_max") {
builder.init.constr_mult_init_max = v;
}
if let Some(v) = read_num("bound_mult_init_val") {
builder.init.bound_mult_init_val = v;
}
if let Ok((v, found)) = self.options.get_string_value("bound_mult_init_method", "") {
if found {
builder.init.bound_mult_init_method = v;
}
}
if let Ok((v, found)) = self
.options
.get_string_value("least_square_init_primal", "")
{
if found {
builder.init.least_square_init_primal = v == "yes";
}
}
builder
}
}
fn journal_level_from_int(v: i32) -> JournalLevel {
match v.clamp(0, 12) {
0 => JournalLevel::J_NONE,
1 => JournalLevel::J_ERROR,
2 => JournalLevel::J_STRONGWARNING,
3 => JournalLevel::J_SUMMARY,
4 => JournalLevel::J_WARNING,
5 => JournalLevel::J_ITERSUMMARY,
6 => JournalLevel::J_DETAILED,
7 => JournalLevel::J_MOREDETAILED,
8 => JournalLevel::J_VECTOR,
9 => JournalLevel::J_MOREVECTOR,
10 => JournalLevel::J_MATRIX,
11 => JournalLevel::J_MOREMATRIX,
_ => JournalLevel::J_ALL,
}
}
pub fn default_backend_factory(feral_cfg: pounce_feral::FeralConfig) -> LinearBackendFactory {
Box::new(
move |choice: LinearSolverChoice| -> Box<dyn SparseSymLinearSolverInterface> {
match choice {
LinearSolverChoice::Feral => {
Box::new(pounce_feral::FeralSolverInterface::with_config(feral_cfg))
}
LinearSolverChoice::Ma57 => {
#[cfg(feature = "ma57")]
{
Box::new(pounce_hsl::Ma57SolverInterface::new())
}
#[cfg(not(feature = "ma57"))]
{
Box::new(pounce_feral::FeralSolverInterface::with_config(feral_cfg))
}
}
}
},
)
}
pub fn default_backend_factory_with_sink(
feral_cfg: pounce_feral::FeralConfig,
sink: Arc<Mutex<LinearSolverSummary>>,
) -> LinearBackendFactory {
Box::new(
move |choice: LinearSolverChoice| -> Box<dyn SparseSymLinearSolverInterface> {
match choice {
LinearSolverChoice::Feral => Box::new(
pounce_feral::FeralSolverInterface::with_config(feral_cfg)
.with_summary_sink(Arc::clone(&sink)),
),
LinearSolverChoice::Ma57 => {
#[cfg(feature = "ma57")]
{
Box::new(pounce_hsl::Ma57SolverInterface::new())
}
#[cfg(not(feature = "ma57"))]
{
Box::new(
pounce_feral::FeralSolverInterface::with_config(feral_cfg)
.with_summary_sink(Arc::clone(&sink)),
)
}
}
}
},
)
}
pub fn feral_config_from_options(
options: &pounce_common::options_list::OptionsList,
) -> pounce_feral::FeralConfig {
let mut cfg = pounce_feral::FeralConfig::from_env();
if let Ok((v, true)) = options.get_bool_value("feral_cascade_break", "") {
cfg.cascade_break = Some(v);
}
if let Ok((v, true)) = options.get_bool_value("feral_fma", "") {
cfg.fma = v;
}
if let Ok((v, true)) = options.get_bool_value("feral_refine", "") {
cfg.refine = v;
}
if let Ok((v, true)) = options.get_numeric_value("feral_singular_pivot_floor", "") {
cfg.singular_pivot_floor = v;
}
if let Ok((v, true)) = options.get_numeric_value("feral_pivtol", "") {
cfg.pivtol = v;
}
if let Ok((v, true)) = options.get_string_value("feral_ordering", "") {
if let Some(m) = pounce_feral::parse_ordering_method(&v) {
cfg.ordering = m;
}
}
cfg
}
fn solver_return_to_app_status(s: SolverReturn) -> ApplicationReturnStatus {
match s {
SolverReturn::Success => ApplicationReturnStatus::SolveSucceeded,
SolverReturn::StopAtAcceptablePoint => ApplicationReturnStatus::SolvedToAcceptableLevel,
SolverReturn::FeasiblePointFound => ApplicationReturnStatus::FeasiblePointFound,
SolverReturn::MaxiterExceeded => ApplicationReturnStatus::MaximumIterationsExceeded,
SolverReturn::CpuTimeExceeded => ApplicationReturnStatus::MaximumCpuTimeExceeded,
SolverReturn::WallTimeExceeded => ApplicationReturnStatus::MaximumWallTimeExceeded,
SolverReturn::StopAtTinyStep => ApplicationReturnStatus::SearchDirectionBecomesTooSmall,
SolverReturn::LocalInfeasibility => ApplicationReturnStatus::InfeasibleProblemDetected,
SolverReturn::UserRequestedStop => ApplicationReturnStatus::UserRequestedStop,
SolverReturn::DivergingIterates => ApplicationReturnStatus::DivergingIterates,
SolverReturn::RestorationFailure => ApplicationReturnStatus::RestorationFailed,
SolverReturn::ErrorInStepComputation => ApplicationReturnStatus::ErrorInStepComputation,
SolverReturn::InvalidNumberDetected => ApplicationReturnStatus::InvalidNumberDetected,
SolverReturn::TooFewDegreesOfFreedom => ApplicationReturnStatus::NotEnoughDegreesOfFreedom,
SolverReturn::InvalidOption => ApplicationReturnStatus::InvalidOption,
SolverReturn::OutOfMemory => ApplicationReturnStatus::InsufficientMemory,
SolverReturn::InternalError | SolverReturn::Unassigned => {
ApplicationReturnStatus::InternalError
}
}
}
fn try_eval_curr_f(
nlp: &Rc<RefCell<dyn IpoptNlp>>,
x: &Rc<dyn pounce_linalg::Vector>,
) -> Result<Number, ()> {
let mut nlp_mut = nlp.borrow_mut();
Ok(nlp_mut.eval_f(&**x))
}
fn is_l1_fallback_trigger(status: ApplicationReturnStatus) -> bool {
matches!(
status,
ApplicationReturnStatus::RestorationFailed
| ApplicationReturnStatus::InfeasibleProblemDetected
| ApplicationReturnStatus::SolvedToAcceptableLevel
| ApplicationReturnStatus::MaximumIterationsExceeded
| ApplicationReturnStatus::NotEnoughDegreesOfFreedom
)
}
fn finalize_via_orig_nlp(
nlp: &Rc<RefCell<dyn IpoptNlp>>,
alg: &IpoptAlgorithm,
solver_status: SolverReturn,
_app_status: ApplicationReturnStatus,
tnlp: &Rc<RefCell<dyn TNLP>>,
) -> Result<Number, ()> {
let curr = alg.data.borrow().curr.clone().ok_or(())?;
let nlp_borrow = nlp.borrow();
let x_vec: Vec<Number> = nlp_borrow.lift_x_to_full(&*curr.x);
let info = tnlp.borrow_mut().get_nlp_info().ok_or(())?;
let n = info.n as usize;
let m = info.m as usize;
debug_assert_eq!(x_vec.len(), n);
let mut z_l = nlp_borrow.pack_z_l_for_user(&*curr.z_l);
if z_l.is_empty() {
z_l = vec![0.0; n];
}
let mut z_u = nlp_borrow.pack_z_u_for_user(&*curr.z_u);
if z_u.is_empty() {
z_u = vec![0.0; n];
}
let mut lambda = nlp_borrow.pack_lambda_for_user(&*curr.y_c, &*curr.y_d);
if lambda.is_empty() {
lambda = vec![0.0; m];
}
drop(nlp_borrow);
let mut g_final = vec![0.0; m];
let _ = tnlp.borrow_mut().eval_g(&x_vec, true, &mut g_final);
let f_final = tnlp
.borrow_mut()
.eval_f(&x_vec, true)
.unwrap_or(Number::NAN);
tnlp.borrow_mut().finalize_solution(
Solution {
status: solver_status,
x: &x_vec,
z_l: &z_l,
z_u: &z_u,
g: &g_final,
lambda: &lambda,
obj_value: f_final,
},
&TnlpIpoptData::default(),
&TnlpIpoptCq::default(),
);
Ok(f_final)
}
fn apply_sqp_options(options: &OptionsList, opts: &mut crate::sqp::SqpOptions) {
use crate::sqp::{SqpGlobalization, SqpHessianSource};
if let Ok((s, true)) = options.get_string_value("sqp_globalization", "") {
opts.globalization = match s.as_str() {
"filter" => SqpGlobalization::Filter,
"l1-elastic" => SqpGlobalization::L1Elastic,
_ => opts.globalization,
};
}
if let Ok((s, true)) = options.get_string_value("sqp_hessian", "") {
opts.hessian = match s.as_str() {
"exact" => SqpHessianSource::Exact,
"damped-bfgs" => SqpHessianSource::DampedBfgs,
"lbfgs" => SqpHessianSource::Lbfgs,
_ => opts.hessian,
};
}
if let Ok((v, true)) = options.get_integer_value("sqp_max_iter", "") {
if v >= 0 {
opts.max_iter = v as u32;
}
}
if let Ok((v, true)) = options.get_numeric_value("sqp_tol", "") {
opts.tol = v;
}
if let Ok((v, true)) = options.get_numeric_value("sqp_constr_viol_tol", "") {
opts.constr_viol_tol = v;
}
if let Ok((v, true)) = options.get_numeric_value("sqp_dual_inf_tol", "") {
opts.dual_inf_tol = v;
}
if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty", "") {
opts.l1_penalty = v;
}
if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty_safety", "") {
opts.l1_penalty_safety = v;
}
if let Ok((v, true)) = options.get_numeric_value("sqp_l1_penalty_max", "") {
opts.l1_penalty_max = v;
}
if let Ok((v, true)) = options.get_numeric_value("sqp_bt_reduction", "") {
opts.bt_reduction = v;
}
if let Ok((v, true)) = options.get_numeric_value("sqp_bt_min_alpha", "") {
opts.bt_min_alpha = v;
}
if let Ok((v, true)) = options.get_integer_value("sqp_print_level", "") {
opts.print_level = v.clamp(0, u8::MAX as i32) as u8;
}
if let Ok((v, true)) = options.get_integer_value("sqp_lbfgs_max_history", "") {
if v >= 1 {
opts.lbfgs_max_history = v as u32;
}
}
}
fn finalize_via_sqp(
nlp: &Rc<RefCell<dyn IpoptNlp>>,
res: &crate::sqp::SqpResult,
solver_status: pounce_nlp::SolverReturn,
tnlp: &Rc<RefCell<dyn TNLP>>,
) -> Result<Number, ()> {
use pounce_linalg::dense_vector::DenseVectorSpace;
let info = tnlp.borrow_mut().get_nlp_info().ok_or(())?;
let n = info.n as usize;
let m = info.m as usize;
let nlp_borrow = nlp.borrow();
let n_alg = nlp_borrow.n() as usize;
let m_eq = nlp_borrow.m_eq() as usize;
let m_ineq = nlp_borrow.m_ineq() as usize;
debug_assert_eq!(res.x.len(), n_alg);
debug_assert_eq!(res.lambda_g.len(), m_eq + m_ineq);
debug_assert_eq!(res.lambda_x.len(), n_alg);
let x_space = DenseVectorSpace::new(n_alg as Index);
let c_space = DenseVectorSpace::new(m_eq as Index);
let d_space = DenseVectorSpace::new(m_ineq as Index);
let mut x_dv = x_space.make_new_dense();
x_dv.set_values(&res.x);
let x_vec: Vec<Number> = nlp_borrow.lift_x_to_full(&x_dv);
debug_assert_eq!(x_vec.len(), n);
let mut z_l_compressed = x_space.make_new_dense();
let mut z_u_compressed = x_space.make_new_dense();
let zl_vals: Vec<Number> = res.lambda_x.iter().map(|v| v.max(0.0)).collect();
let zu_vals: Vec<Number> = res.lambda_x.iter().map(|v| (-v).max(0.0)).collect();
z_l_compressed.set_values(&zl_vals);
z_u_compressed.set_values(&zu_vals);
let mut z_l = nlp_borrow.pack_z_l_for_user(&z_l_compressed);
if z_l.is_empty() {
z_l = vec![0.0; n];
}
let mut z_u = nlp_borrow.pack_z_u_for_user(&z_u_compressed);
if z_u.is_empty() {
z_u = vec![0.0; n];
}
let mut y_c_dv = c_space.make_new_dense();
let mut y_d_dv = d_space.make_new_dense();
if m_eq > 0 {
y_c_dv.set_values(&res.lambda_g[..m_eq]);
}
if m_ineq > 0 {
y_d_dv.set_values(&res.lambda_g[m_eq..]);
}
let mut lambda = nlp_borrow.pack_lambda_for_user(&y_c_dv, &y_d_dv);
if lambda.is_empty() {
lambda = vec![0.0; m];
}
drop(nlp_borrow);
let mut g_final = vec![0.0; m];
let _ = tnlp.borrow_mut().eval_g(&x_vec, true, &mut g_final);
let f_final = tnlp
.borrow_mut()
.eval_f(&x_vec, true)
.unwrap_or(Number::NAN);
tnlp.borrow_mut().finalize_solution(
pounce_nlp::tnlp::Solution {
status: solver_status,
x: &x_vec,
z_l: &z_l,
z_u: &z_u,
g: &g_final,
lambda: &lambda,
obj_value: f_final,
},
&TnlpIpoptData::default(),
&TnlpIpoptCq::default(),
);
Ok(f_final)
}
#[cfg(test)]
mod tests {
use super::*;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, Solution, SparsityRequest,
StartingPoint,
};
struct Hs071Stub;
impl TNLP for Hs071Stub {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 4,
m: 2,
nnz_jac_g: 8,
nnz_h_lag: 10,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[1.0; 4]);
b.x_u.copy_from_slice(&[5.0; 4]);
b.g_l.copy_from_slice(&[25.0, 40.0]);
b.g_u.copy_from_slice(&[2.0e19, 40.0]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[1.0, 5.0, 5.0, 1.0]);
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
Some(x[0] * x[3] * (x[0] + x[1] + x[2]) + x[2])
}
fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
grad.fill(0.0);
true
}
fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g.fill(0.0);
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
mode: SparsityRequest<'_>,
) -> bool {
if let SparsityRequest::Structure { irow, jcol } = mode {
irow.copy_from_slice(&[0, 0, 0, 0, 1, 1, 1, 1]);
jcol.copy_from_slice(&[0, 1, 2, 3, 0, 1, 2, 3]);
}
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn application_default_does_not_select_sqp() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
assert!(!app.is_sqp_algorithm_selected());
}
#[test]
fn application_routes_to_sqp_when_algorithm_option_set() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("algorithm active-set-sqp\n")
.unwrap();
assert!(app.is_sqp_algorithm_selected());
}
struct ConvexEqTnlp {
finalize_called: std::rc::Rc<std::cell::RefCell<Option<(Vec<Number>, Number)>>>,
}
impl TNLP for ConvexEqTnlp {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 2,
m: 1,
nnz_jac_g: 2,
nnz_h_lag: 2,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[-2.0e19; 2]);
b.x_u.copy_from_slice(&[2.0e19; 2]);
b.g_l.copy_from_slice(&[1.0]);
b.g_u.copy_from_slice(&[1.0]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[0.0, 0.0]);
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
Some(0.5 * (x[0] * x[0] + x[1] * x[1]) - x[0] - 2.0 * x[1])
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
grad[0] = x[0] - 1.0;
grad[1] = x[1] - 2.0;
true
}
fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g[0] = x[0] + x[1];
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow.copy_from_slice(&[0, 0]);
jcol.copy_from_slice(&[0, 1]);
}
SparsityRequest::Values { values, .. } => {
values.copy_from_slice(&[1.0, 1.0]);
}
}
true
}
fn eval_h(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_obj_factor: Number,
_lambda: Option<&[Number]>,
_new_lambda: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow.copy_from_slice(&[0, 1]);
jcol.copy_from_slice(&[0, 1]);
}
SparsityRequest::Values { values, .. } => {
values.copy_from_slice(&[1.0, 1.0]);
}
}
true
}
fn finalize_solution(&mut self, sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {
*self.finalize_called.borrow_mut() = Some((sol.x.to_vec(), sol.obj_value));
}
}
#[test]
fn application_sqp_path_solves_convex_eq_nlp_and_finalizes() {
let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
let tnlp = std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
finalize_called: std::rc::Rc::clone(&finalize_slot),
}));
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("algorithm active-set-sqp\n")
.unwrap();
let status = app.optimize_tnlp(tnlp);
assert_eq!(status, ApplicationReturnStatus::SolveSucceeded);
let recv = finalize_slot.borrow().clone();
let (x_recv, obj_recv) = recv.expect("finalize_solution was not called");
assert_eq!(x_recv.len(), 2);
assert!((x_recv[0] - 0.0).abs() < 1e-6, "x[0] = {}", x_recv[0]);
assert!((x_recv[1] - 1.0).abs() < 1e-6, "x[1] = {}", x_recv[1]);
assert!(
(obj_recv - (-1.5)).abs() < 1e-6,
"obj = {} but expected -1.5",
obj_recv
);
}
#[test]
fn application_routes_to_sqp_case_insensitively() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("algorithm Active-Set-SQP\n")
.unwrap();
assert!(app.is_sqp_algorithm_selected());
}
#[test]
fn application_constructs_and_loads_options() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("print_level 5\nfile_print_level 7\n")
.unwrap();
let (level, found) = app.options().get_integer_value("print_level", "").unwrap();
assert!(found);
assert_eq!(level, 5);
}
#[test]
fn application_sqp_suboptions_propagate_to_builder() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"algorithm active-set-sqp\n\
sqp_globalization l1-elastic\n\
sqp_hessian lbfgs\n\
sqp_max_iter 17\n\
sqp_tol 1e-7\n\
sqp_constr_viol_tol 1e-5\n\
sqp_dual_inf_tol 1e-3\n\
sqp_l1_penalty 2.5\n\
sqp_bt_reduction 0.25\n\
sqp_bt_min_alpha 1e-10\n\
sqp_print_level 2\n\
sqp_lbfgs_max_history 12\n",
)
.unwrap();
let snap = app.algorithm_builder_snapshot();
assert_eq!(
snap.sqp.globalization,
crate::sqp::SqpGlobalization::L1Elastic
);
assert_eq!(snap.sqp.hessian, crate::sqp::SqpHessianSource::Lbfgs);
assert_eq!(snap.sqp.max_iter, 17);
assert!((snap.sqp.tol - 1e-7).abs() < 1e-18);
assert!((snap.sqp.constr_viol_tol - 1e-5).abs() < 1e-18);
assert!((snap.sqp.dual_inf_tol - 1e-3).abs() < 1e-18);
assert!((snap.sqp.l1_penalty - 2.5).abs() < 1e-18);
assert!((snap.sqp.bt_reduction - 0.25).abs() < 1e-18);
assert!((snap.sqp.bt_min_alpha - 1e-10).abs() < 1e-18);
assert_eq!(snap.sqp.print_level, 2);
assert_eq!(snap.sqp.lbfgs_max_history, 12);
}
#[test]
fn application_sqp_warm_start_round_trip() {
let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
let tnlp_rc: std::rc::Rc<std::cell::RefCell<dyn TNLP>> =
std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
finalize_called: std::rc::Rc::clone(&finalize_slot),
}));
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("algorithm active-set-sqp\n")
.unwrap();
let status_a = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
assert_eq!(status_a, ApplicationReturnStatus::SolveSucceeded);
let ws = app.last_sqp_working_set().cloned();
assert!(ws.is_some(), "cold solve must yield a working set");
let (x_recv, _) = finalize_slot.borrow().clone().unwrap();
let warm = crate::sqp::SqpIterates {
x: x_recv,
lambda_g: vec![1.0],
lambda_x: vec![0.0, 0.0],
working: ws,
};
app.set_sqp_warm_start(warm);
let status_b = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
assert_eq!(status_b, ApplicationReturnStatus::SolveSucceeded);
assert!(app.last_sqp_working_set().is_some());
}
#[test]
fn application_sqp_warm_start_auto_clears_after_use() {
let finalize_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
let tnlp_rc: std::rc::Rc<std::cell::RefCell<dyn TNLP>> =
std::rc::Rc::new(std::cell::RefCell::new(ConvexEqTnlp {
finalize_called: std::rc::Rc::clone(&finalize_slot),
}));
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("algorithm active-set-sqp\n")
.unwrap();
app.set_sqp_warm_start(crate::sqp::SqpIterates {
x: vec![0.0, 1.0],
lambda_g: vec![1.0],
lambda_x: vec![0.0, 0.0],
working: None,
});
assert!(app.sqp_warm_start.is_some());
let _ = app.optimize_tnlp(std::rc::Rc::clone(&tnlp_rc));
assert!(
app.sqp_warm_start.is_none(),
"warm-start input must be auto-cleared after use"
);
}
#[test]
fn application_sqp_suboptions_default_when_unset() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
let snap = app.algorithm_builder_snapshot();
let d = crate::sqp::SqpOptions::default();
assert_eq!(snap.sqp.globalization, d.globalization);
assert_eq!(snap.sqp.hessian, d.hessian);
assert_eq!(snap.sqp.max_iter, d.max_iter);
assert!((snap.sqp.tol - d.tol).abs() < 1e-18);
assert!((snap.sqp.constr_viol_tol - d.constr_viol_tol).abs() < 1e-18);
assert!((snap.sqp.dual_inf_tol - d.dual_inf_tol).abs() < 1e-18);
assert!((snap.sqp.l1_penalty - d.l1_penalty).abs() < 1e-18);
assert!((snap.sqp.bt_reduction - d.bt_reduction).abs() < 1e-18);
assert!((snap.sqp.bt_min_alpha - d.bt_min_alpha).abs() < 1e-18);
assert_eq!(snap.sqp.print_level, d.print_level);
assert_eq!(snap.sqp.lbfgs_max_history, d.lbfgs_max_history);
}
#[test]
fn application_reports_problem_dimensions() {
let app = IpoptApplication::new();
let mut tnlp = Hs071Stub;
let info = app.problem_dimensions(&mut tnlp).unwrap();
assert_eq!(info.n, 4);
assert_eq!(info.m, 2);
assert_eq!(info.nnz_jac_g, 8);
assert_eq!(info.nnz_h_lag, 10);
}
}