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::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>>,
}
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())),
}
}
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 {
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)
}
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 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,
_ => 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);
orig_nlp.determine_scaling_from_starting_point(scaling_method, max_gradient, min_value);
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));
alg.record_iter_history = self.record_iter_history;
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 solver_status = alg.optimize();
timing.overall_alg.end();
{
let mut stats = self.statistics.borrow_mut();
stats.iteration_count = alg.data.borrow().iter_count;
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 = std::mem::take(&mut alg.iter_history);
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
}
fn algorithm_builder_from_options(&self) -> AlgorithmBuilder {
let mut builder = AlgorithmBuilder::new();
if let Ok((v, found)) = self.options.get_string_value("mu_strategy", "") {
if found {
builder.mu_strategy = match v.as_str() {
"adaptive" => MuStrategyChoice::Adaptive,
_ => MuStrategyChoice::Monotone,
};
}
}
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("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("linear_solver", "") {
builder.linear_solver = match v.as_str() {
"ma57" => LinearSolverChoice::Ma57,
_ => LinearSolverChoice::Feral,
};
}
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 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";
}
}
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 = 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;
}
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)
}
#[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_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_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);
}
}