use crate::alg_builder::{
AlgorithmBuilder, HessianApproxChoice, LineSearchChoice, LinearBackendFactory,
LinearSolverChoice, MuStrategyChoice,
};
use crate::hess::lim_mem_quasi_newton::UpdateType;
use crate::ipopt_alg::{DUAL_DIV_RETRY_DU_FLOOR, 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 const DEFAULT_OPTION_FILE_NAMES: &[&str] = &["pounce.opt", "ipopt.opt"];
const DUAL_DIV_RETRY_DOMINANCE: Number = 1e-6;
fn runaway_is_the_whole_residual(
dual_inf: Number,
viol: Number,
compl: Number,
du_floor: Number,
) -> bool {
dual_inf.is_finite()
&& dual_inf > 0.0
&& dual_inf >= du_floor
&& viol.is_finite()
&& compl.is_finite()
&& viol.max(compl) <= DUAL_DIV_RETRY_DOMINANCE * dual_inf
}
fn retry_answer_is_admissible(
base_obj: Number,
base_viol: Number,
retry_obj: Number,
retry_viol: Number,
accept_tol: Number,
sense: Number,
) -> bool {
if !base_obj.is_finite() || !base_viol.is_finite() || base_viol > accept_tol {
return true;
}
if !retry_obj.is_finite() {
return false;
}
let base_obj = sense * base_obj;
let retry_obj = sense * retry_obj;
let tol = accept_tol * base_obj.abs().max(1.0);
if retry_obj > base_obj + tol {
return false; }
if retry_obj < base_obj - tol {
return retry_viol.is_finite() && retry_viol <= base_viol;
}
true
}
#[derive(Debug, Default, Clone)]
pub struct OptionFileLoad {
pub path: Option<PathBuf>,
pub explicit: bool,
pub warnings: Vec<String>,
}
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::SparseSymLinearSolverInterface;
use pounce_linsol::summary::LinearSolverSummary;
use pounce_nlp::alg_types::SolverReturn;
use pounce_nlp::derivative_test::{DerivativeTest, DerivativeTestOptions};
use pounce_nlp::orig_ipopt_nlp::{ConstObjScaling, OrigIpoptNlp, ScalingMethod};
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::solve_statistics::SolveStatistics;
use pounce_nlp::tnlp::{
BoundsInfo, IpoptCq as TnlpIpoptCq, IpoptData as TnlpIpoptData, NlpInfo, Solution, TNLP,
};
use pounce_nlp::tnlp_adapter::{
DEFAULT_NLP_LOWER_BOUND_INF, DEFAULT_NLP_UPPER_BOUND_INF, FixedVarTreatment, TNLPAdapter,
};
use std::cell::RefCell;
use std::fmt;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Instant;
pub struct IpoptApplication {
options: OptionsList,
least_square_init_report: Option<crate::init::default::LeastSquareInitReport>,
variable_scaling: RefCell<Option<Vec<Number>>>,
row_scaling_active: std::cell::Cell<Option<bool>>,
presolve_already_applied: bool,
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>>,
debug_hook: Option<std::rc::Rc<std::cell::RefCell<dyn crate::debug::DebugHook>>>,
restoration_factory_provider: Option<RestorationFactoryProvider>,
on_converged: Option<ConvergedCallback>,
record_iter_history: bool,
option_file_resolved: bool,
convex_routing_available: bool,
backend_warnings_emitted: bool,
linsol_summary_sink: Arc<Mutex<LinearSolverSummary>>,
quality_escalations: Rc<std::cell::Cell<u64>>,
dual_divergence_signature: std::cell::Cell<bool>,
dual_divergence_retry_promoted: std::cell::Cell<bool>,
answer_restored_from_floor: std::cell::Cell<bool>,
last_finalize: RefCell<Option<FinalizeSnapshot>>,
last_iter_stats: Rc<RefCell<Option<pounce_nlp::tnlp::IterStats>>>,
sqp_warm_start: Option<crate::sqp::SqpIterates>,
sqp_last_working_set: Option<pounce_qp::WorkingSet>,
crossover_report: Option<crate::crossover::CrossoverReport>,
warm_start_iterate: Option<crate::debug::IterateSnapshot>,
warm_start_diag: RefCell<Option<crate::init::warm_start::WarmStartDiagnostics>>,
external_ordering: Option<Vec<usize>>,
kkt_schur_block: Option<Vec<usize>>,
last_printed_problem_stats: RefCell<Option<pounce_solve_report::console::ProblemStats>>,
in_retry_sequence: std::cell::Cell<bool>,
end_verdict_deferrals: std::cell::Cell<u32>,
}
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(®)),
least_square_init_report: None,
variable_scaling: RefCell::new(None),
row_scaling_active: std::cell::Cell::new(None),
presolve_already_applied: false,
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,
debug_hook: None,
restoration_factory_provider: None,
on_converged: None,
record_iter_history: false,
option_file_resolved: false,
convex_routing_available: false,
backend_warnings_emitted: false,
linsol_summary_sink: Arc::new(Mutex::new(LinearSolverSummary::default())),
quality_escalations: Rc::new(std::cell::Cell::new(0)),
dual_divergence_signature: std::cell::Cell::new(false),
dual_divergence_retry_promoted: std::cell::Cell::new(false),
answer_restored_from_floor: std::cell::Cell::new(false),
last_finalize: RefCell::new(None),
last_iter_stats: Rc::new(RefCell::new(None)),
sqp_warm_start: None,
sqp_last_working_set: None,
crossover_report: None,
warm_start_iterate: None,
warm_start_diag: RefCell::new(None),
external_ordering: None,
kkt_schur_block: None,
last_printed_problem_stats: RefCell::new(None),
in_retry_sequence: std::cell::Cell::new(false),
end_verdict_deferrals: std::cell::Cell::new(0),
}
}
pub fn options(&self) -> &OptionsList {
&self.options
}
pub fn options_mut(&mut self) -> &mut OptionsList {
&mut self.options
}
pub fn set_presolve_already_applied(&mut self, applied: bool) {
self.presolve_already_applied = applied;
}
pub fn optimize_tnlp_without_presolve(
&mut self,
tnlp: Rc<RefCell<dyn TNLP>>,
) -> ApplicationReturnStatus {
let explicit_wrapper = self.presolve_already_applied;
self.presolve_already_applied = true;
let status = self.optimize_tnlp(tnlp);
self.presolve_already_applied = explicit_wrapper;
status
}
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 set_debug_hook(
&mut self,
hook: std::rc::Rc<std::cell::RefCell<dyn crate::debug::DebugHook>>,
) {
self.debug_hook = Some(hook);
}
pub fn diagnostics(&self) -> Option<Rc<DiagnosticsState>> {
self.diagnostics.as_ref().map(Rc::clone)
}
fn adapter_options(&self) -> (Number, Number, FixedVarTreatment) {
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,
};
(lo_inf, up_inf, fixed_treatment)
}
pub fn set_in_retry_sequence(&self, active: bool) {
self.in_retry_sequence.set(active);
}
pub fn defer_end_verdict(&self) {
self.end_verdict_deferrals
.set(self.end_verdict_deferrals.get().saturating_add(1));
}
#[must_use]
pub fn release_end_verdict(&self) -> bool {
let next = self.end_verdict_deferrals.get().saturating_sub(1);
self.end_verdict_deferrals.set(next);
next == 0
}
pub fn print_end_verdict(&self, status: ApplicationReturnStatus) {
let console_output = match self.options.get_integer_value("print_level", "") {
Ok((v, true)) => v >= 1,
_ => true,
};
if console_output {
pounce_solve_report::console::print_exit_verdict(status);
}
}
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 answer_restored_from_floor(&self) -> bool {
self.answer_restored_from_floor.get()
}
pub fn enable_iter_history(&mut self) {
self.record_iter_history = true;
}
pub fn initialize_with_option_file(
&mut self,
explicit: Option<&Path>,
) -> Result<OptionFileLoad, SolverException> {
let mut load = OptionFileLoad::default();
self.option_file_resolved = true;
let path = match explicit {
Some(p) => {
if !p.is_file() {
return Err(SolverException::new(
ExceptionKind::IPOPT_APPLICATION_ERROR,
format!(
"options file \"{}\" does not exist. It was named by \
--options-file / option_file_name, so the run would \
otherwise proceed at stock defaults with none of its \
settings applied.",
p.display()
),
file!(),
line!() as Index,
));
}
load.explicit = true;
p.to_path_buf()
}
None => {
let present: Vec<&&str> = DEFAULT_OPTION_FILE_NAMES
.iter()
.filter(|n| Path::new(n).is_file())
.collect();
let Some(first) = present.first() else {
return Ok(load);
};
for other in &present[1..] {
load.warnings.push(format!(
"`{first}` and `{other}` are both present; reading `{first}` \
only (pounce's own name wins). Pass \
`option_file_name={other}` to read that one instead."
));
}
PathBuf::from(**first)
}
};
self.initialize_with_options_file(&path)?;
if let Ok((named, true)) = self.options.get_string_value("option_file_name", "")
&& !named.is_empty()
&& Path::new(&named) != path
{
load.warnings.push(format!(
"`{}` sets option_file_name to `{named}`, which has no effect: \
the options file is chosen before it is read. Pass \
`option_file_name={named}` on the command line to read that file.",
path.display()
));
}
load.path = Some(path);
Ok(load)
}
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 least_square_init_report(&self) -> Option<crate::init::default::LeastSquareInitReport> {
self.least_square_init_report.clone()
}
pub fn statistics(&self) -> SolveStatistics {
self.statistics.borrow().clone()
}
pub fn warm_start_diagnostics(&self) -> Option<crate::init::warm_start::WarmStartDiagnostics> {
self.warm_start_diag.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())
}
}
fn install_variable_scaling(
&self,
tnlp: Rc<RefCell<dyn TNLP>>,
) -> Result<Rc<RefCell<dyn TNLP>>, String> {
*self.variable_scaling.borrow_mut() = None;
let 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());
if method != "user-scaling" && method != "curvature-based" {
return Ok(tnlp);
}
match pounce_nlp::scaling_tnlp::wrap_with_scaling(
Rc::clone(&tnlp),
self.nlp_lower_bound_inf(),
self.nlp_upper_bound_inf(),
) {
Ok(Some(wrapped)) => {
*self.variable_scaling.borrow_mut() =
pounce_nlp::scaling_tnlp::factors_of(&wrapped);
Ok(wrapped)
}
Ok(None) => Ok(tnlp),
Err(why) => Err(format!(
"pounce: nlp_scaling_method={method} supplied per-variable \
scaling factors that cannot be applied. {why}. Correct the \
factors, or drop nlp_scaling_method={method}.\n"
)),
}
}
pub fn variable_scaling(&self) -> Option<Vec<Number>> {
self.variable_scaling.borrow().clone()
}
fn install_start_conditioner(&self, tnlp: Rc<RefCell<dyn TNLP>>) -> Rc<RefCell<dyn TNLP>> {
use pounce_nlp::start_conditioner::{AdamConfig, ConditionedStartTnlp, StartConditioner};
let perturbation = self
.options
.get_numeric_value("start_point_perturbation", "")
.map(|(v, _found)| v)
.unwrap_or(0.0);
let conditioner = if perturbation > 0.0 {
let seed = self
.options
.get_integer_value("start_point_perturbation_seed", "")
.map(|(v, _found)| v)
.unwrap_or(0);
StartConditioner::Jitter {
seed: seed.max(0) as u64,
scale: perturbation,
}
} else {
let which = self
.options
.get_string_value("start_point_conditioner", "")
.map(|(v, _found)| v)
.unwrap_or_else(|_| "none".to_string());
if which != "adam" {
return tnlp;
}
let d = AdamConfig::default();
StartConditioner::Adam(AdamConfig {
iters: self
.options
.get_integer_value("adam_warmup_iters", "")
.map(|(v, _found)| v.max(0) as usize)
.unwrap_or(d.iters),
lr: self
.options
.get_numeric_value("adam_warmup_learning_rate", "")
.map(|(v, _found)| v)
.unwrap_or(d.lr),
rho: self
.options
.get_numeric_value("adam_warmup_penalty", "")
.map(|(v, _found)| v)
.unwrap_or(d.rho),
..d
})
};
let lower = self.nlp_lower_bound_inf();
let upper = self.nlp_upper_bound_inf();
let wrapped = ConditionedStartTnlp::new(tnlp, conditioner).with_bound_inf(lower, upper);
Rc::new(RefCell::new(wrapped))
}
pub fn optimize_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
self.optimize_tnlp_with_derivative_test_tnlp(tnlp, None)
}
pub fn optimize_tnlp_with_derivative_test_tnlp(
&mut self,
tnlp: Rc<RefCell<dyn TNLP>>,
derivative_test_tnlp: Option<Rc<RefCell<dyn TNLP>>>,
) -> ApplicationReturnStatus {
self.dual_divergence_signature.set(false);
self.dual_divergence_retry_promoted.set(false);
self.answer_restored_from_floor.set(false);
if !self.in_retry_sequence.get() {
*self.last_printed_problem_stats.borrow_mut() = None;
}
let tnlp = match self.install_variable_scaling(tnlp) {
Ok(t) => t,
Err(msg) => {
use pounce_common::journalist::JournalCategory;
eprint!("{msg}");
self.journalist
.print(JournalLevel::J_ERROR, JournalCategory::J_MAIN, &msg);
return ApplicationReturnStatus::InvalidOption;
}
};
let tnlp = self.install_start_conditioner(tnlp);
let derivative_test_tnlp = derivative_test_tnlp.as_ref().unwrap_or(&tnlp);
if let Some(value) = self.unsupported_library_solver_selection() {
use pounce_common::journalist::JournalCategory;
self.journalist.print(
JournalLevel::J_ERROR,
JournalCategory::J_MAIN,
&format!(
"pounce: solver_selection={value} routing is only available \
through the pounce CLI (.nl input); library consumers can use \
qp-active-set, nlp, or auto.\n"
),
);
return ApplicationReturnStatus::InvalidOption;
}
if let Some(value) = self.unimplemented_linear_solver() {
use pounce_common::journalist::JournalCategory;
let msg = format!("{}\n", Self::unimplemented_linear_solver_message(&value));
eprint!("{msg}");
self.journalist
.print(JournalLevel::J_ERROR, JournalCategory::J_MAIN, &msg);
return ApplicationReturnStatus::InvalidOption;
}
if let Some(msg) = self
.unimplemented_option_refusal()
.or_else(|| self.unimplemented_option_value_refusal())
.or_else(|| self.unhonored_option_file_name())
.or_else(|| self.unhonored_convex_option())
{
use pounce_common::journalist::JournalCategory;
eprintln!("{msg}");
self.journalist.print(
JournalLevel::J_ERROR,
JournalCategory::J_MAIN,
&format!("{msg}\n"),
);
return ApplicationReturnStatus::InvalidOption;
}
if let Some(msg) = self.ma57_pivtol_bracket_refusal() {
use pounce_common::journalist::JournalCategory;
eprintln!("{msg}");
self.journalist.print(
JournalLevel::J_ERROR,
JournalCategory::J_MAIN,
&format!("{msg}\n"),
);
return ApplicationReturnStatus::InvalidOption;
}
let backend_warnings = self.take_unimplemented_backend_warnings();
for warning in self
.unexploited_hint_warnings()
.into_iter()
.chain(backend_warnings)
{
eprintln!("{warning}");
}
self.run_derivative_test(derivative_test_tnlp);
let tnlp = if self.presolve_already_applied {
tnlp
} else {
match pounce_presolve::wrap_from_options(tnlp, &self.options) {
Ok(tnlp) => tnlp,
Err(err) => {
use pounce_common::journalist::JournalCategory;
self.journalist.print(
JournalLevel::J_ERROR,
JournalCategory::J_MAIN,
&format!("pounce: could not materialize presolve options: {err}\n"),
);
return ApplicationReturnStatus::InvalidOption;
}
}
};
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 let Some(proof) = tnlp.borrow().presolve_infeasibility_proof() {
use pounce_common::journalist::JournalCategory;
let detail = match proof {
pounce_nlp::tnlp::InfeasibilityProof::BoundPropagation => {
"bound propagation crossed a variable's bounds".to_string()
}
pounce_nlp::tnlp::InfeasibilityProof::IntervalArithmetic { witness } => {
format!("interval arithmetic emptied constraint {witness}'s range")
}
};
self.journalist.print(
JournalLevel::J_SUMMARY,
JournalCategory::J_MAIN,
&format!(
"\nEXIT: Presolve detected the feasible region is empty ({detail}).\n\
No feasible point exists; the solve was not run.\n"
),
);
return ApplicationReturnStatus::InfeasibleProblemDetected;
}
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);
}
if self.is_dual_divergence_retry_enabled() {
return self.run_with_dual_divergence_retry(tnlp);
}
self.dispatch_standard_solve(tnlp)
}
fn dispatch_standard_solve(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
if self.is_mu_strategy_fallback_enabled() {
return self.run_with_mu_strategy_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 mu_strategy_was_set(&self) -> bool {
matches!(
self.options.get_string_value("mu_strategy", ""),
Ok((_, true))
)
}
fn mu_strategy_fallback_was_set(&self) -> bool {
matches!(
self.options.get_bool_value("mu_strategy_fallback", ""),
Ok((_, true))
)
}
const TERMINATION_POLICY_OPTIONS: &'static [&'static str] = &[
"tol",
"dual_inf_tol",
"constr_viol_tol",
"compl_inf_tol",
"acceptable_tol",
"acceptable_iter",
"acceptable_dual_inf_tol",
"acceptable_constr_viol_tol",
"acceptable_compl_inf_tol",
"acceptable_obj_change_tol",
"kkt_fidelity_tol",
"obj_scale_certificate_threshold",
"dual_inf_scale_kappa",
"primal_noise_floor_kappa",
"dual_diverging_streak",
"infeas_max_streak",
"resto_decline_deferrals",
"resto_decline_progress_ratio",
"neg_curv_escapes",
"limited_memory_ls_failure_restarts",
];
fn caller_set_termination_policy(&self) -> bool {
Self::TERMINATION_POLICY_OPTIONS.iter().any(|name| {
matches!(self.options.get_numeric_value(name, ""), Ok((_, true)))
|| matches!(self.options.get_integer_value(name, ""), Ok((_, true)))
|| matches!(self.options.get_bool_value(name, ""), Ok((_, true)))
|| matches!(self.options.get_string_value(name, ""), Ok((_, true)))
})
}
fn effective_mu_strategy_is_adaptive(&self) -> bool {
if let Ok((v, true)) = self.options.get_string_value("mu_strategy", "") {
return v == "adaptive";
}
matches!(
self.options.get_string_value("hessian_approximation", ""),
Ok((ref v, true)) if v == "limited-memory"
)
}
fn is_mu_strategy_fallback_enabled(&self) -> bool {
match self.options.get_bool_value("mu_strategy_fallback", "") {
Ok((v, true)) => v,
_ => !self.mu_strategy_was_set(),
}
}
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 crossover_report(&self) -> Option<&crate::crossover::CrossoverReport> {
self.crossover_report.as_ref()
}
pub fn set_warm_start_iterate(&mut self, snap: crate::debug::IterateSnapshot) {
self.warm_start_iterate = Some(snap);
}
pub fn set_external_ordering(&mut self, perm: Vec<usize>) {
self.external_ordering = Some(perm);
}
pub fn clear_external_ordering(&mut self) {
self.external_ordering = None;
}
pub fn external_ordering(&self) -> Option<&[usize]> {
self.external_ordering.as_deref()
}
pub fn set_kkt_schur_block(&mut self, indices: Vec<usize>) {
self.kkt_schur_block = Some(indices);
}
pub fn clear_kkt_schur_block(&mut self) {
self.kkt_schur_block = None;
}
pub fn kkt_schur_block(&self) -> Option<&[usize]> {
self.kkt_schur_block.as_deref()
}
pub fn last_sqp_working_set(&self) -> Option<&pounce_qp::WorkingSet> {
self.sqp_last_working_set.as_ref()
}
fn unsupported_library_solver_selection(&self) -> Option<&'static str> {
let (v, found) = self.options.get_string_value("solver_selection", "").ok()?;
if !found {
return None;
}
["lp-ipm", "qp-ipm", "socp"]
.into_iter()
.find(|c| v.eq_ignore_ascii_case(c))
}
pub fn unimplemented_linear_solver(&self) -> Option<String> {
let (v, _) = self.options.get_string_value("linear_solver", "").ok()?;
["feral", "ma57"]
.iter()
.all(|ok| !v.eq_ignore_ascii_case(ok))
.then_some(v)
}
pub fn unimplemented_option_refusal(&self) -> Option<String> {
crate::unimplemented_options::refusal(&self.options, &self.reg_options).or_else(|| {
crate::unimplemented_options::backend_only_refusal(&self.options, &self.reg_options)
})
}
pub fn unimplemented_option_value_refusal(&self) -> Option<String> {
crate::unimplemented_options::value_refusal(&self.options)
}
pub fn unhonored_option_file_name(&self) -> Option<String> {
if self.option_file_resolved {
return None;
}
if !crate::unimplemented_options::set_to_a_non_default(
&self.options,
&self.reg_options,
"option_file_name",
) {
return None;
}
match self.options.get_string_value("option_file_name", "") {
Ok((name, true)) if !name.is_empty() => Some(format!(
"pounce: `option_file_name` was set to `{name}`, but this entry \
point does not read options files — it would configure nothing. \
The `pounce` CLI honors it (and `./pounce.opt` / `./ipopt.opt`); \
from a library, read the file yourself and pass its contents to \
`initialize_with_options_str`, or set the options directly. \
Tracking issue: https://github.com/jkitchin/pounce/issues/518"
)),
_ => None,
}
}
pub fn set_convex_routing_available(&mut self, available: bool) {
self.convex_routing_available = available;
}
pub fn unhonored_convex_option(&self) -> Option<String> {
if self.convex_routing_available {
return None;
}
const CONVEX_ONLY: &[&str] = &[
"qp_presolve",
"qp_tau",
"qp_tau_max",
"qp_reg",
"qp_gondzio_corr",
"qp_infeas_tol",
"qp_hsde",
"qp_equilibrate",
"qp_crossover",
];
let name = CONVEX_ONLY.iter().find(|name| {
crate::unimplemented_options::set_to_a_non_default(
&self.options,
&self.reg_options,
name,
)
})?;
Some(format!(
"pounce: `{name}` tunes the convex LP/QP interior-point engine, but \
this entry point cannot route a model to it — the option would \
configure nothing. The `pounce` CLI reaches that engine on `.nl` \
input (`solver_selection=lp-ipm` / `qp-ipm` / `socp`, or `auto` on \
a model that classifies as one); from Python, `pounce.solve_qp` / \
`pounce.solve_cone` drive it directly and take the same knobs as \
typed arguments. On this path, `solver_selection=qp-active-set` \
(or `algorithm=active-set-sqp`) is the nearest thing, tuned by the \
`sqp_qp_*` options. Tracking issue: \
https://github.com/jkitchin/pounce/issues/604"
))
}
pub fn unexploited_hint_warnings(&self) -> Vec<String> {
crate::unimplemented_options::hint_warnings(&self.options, &self.reg_options)
}
pub fn convex_unexploited_hint_warnings(&self) -> Vec<String> {
crate::unimplemented_options::convex_hint_warnings(&self.options, &self.reg_options)
}
fn asserted_constant_derivative_hints(&self) -> [bool; 4] {
use pounce_nlp::constant_derivatives::HINT_OPTIONS;
let read_yes = |key: &str| matches!(self.options.get_bool_value(key, ""), Ok((true, true)));
HINT_OPTIONS.map(|name| match name {
"grad_f_constant" => read_yes("grad_f_constant"),
"hessian_constant" => read_yes("hessian_constant"),
"jac_c_constant" => read_yes("jac_c_constant"),
"jac_d_constant" => read_yes("jac_d_constant"),
other => unreachable!("`{other}` is in HINT_OPTIONS with no read site"),
})
}
fn install_constant_derivative_hints(&self, orig_nlp: &mut OrigIpoptNlp) {
use pounce_common::journalist::JournalCategory;
use pounce_nlp::constant_derivatives::reconcile;
let asserted = self.asserted_constant_derivative_hints();
let proofs = orig_nlp.derivative_proofs();
let (outcomes, enabled) = reconcile(proofs, asserted);
for outcome in &outcomes {
if let Some(warning) = outcome.warning() {
eprintln!("{warning}");
self.journalist.print(
JournalLevel::J_STRONGWARNING,
JournalCategory::J_MAIN,
&format!("{warning}\n"),
);
}
}
if std::env::var("POUNCE_DBG_CONSTDERIV").is_ok() {
for outcome in &outcomes {
eprintln!(
"[const deriv] {:<15} proof={:?} asserted={} reused={}",
outcome.name, outcome.proof, outcome.asserted, outcome.honoured,
);
}
}
orig_nlp.set_constant_derivatives(enabled);
}
pub fn unimplemented_backend_warnings(&self) -> Vec<String> {
crate::unimplemented_options::backend_warnings(&self.options, &self.reg_options)
}
pub fn take_unimplemented_backend_warnings(&mut self) -> Vec<String> {
if self.backend_warnings_emitted {
return Vec::new();
}
self.backend_warnings_emitted = true;
self.unimplemented_backend_warnings()
}
fn derivative_test_options(&self) -> DerivativeTestOptions {
let read_num = |key: &str, default: Number| -> Number {
self.options
.get_numeric_value(key, "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(default)
};
DerivativeTestOptions {
mode: self
.options
.get_string_value("derivative_test", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.map(|v| DerivativeTest::from_option(&v))
.unwrap_or_default(),
perturbation: read_num("derivative_test_perturbation", 1e-8),
tol: read_num("derivative_test_tol", 1e-4),
first_index: self
.options
.get_integer_value("derivative_test_first_index", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(-2),
print_all: self
.options
.get_bool_value("derivative_test_print_all", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(false),
}
}
pub fn run_derivative_test(&self, tnlp: &Rc<RefCell<dyn TNLP>>) {
let opts = self.derivative_test_options();
if matches!(opts.mode, DerivativeTest::None) {
return;
}
let report = {
let mut borrowed = tnlp.borrow_mut();
pounce_nlp::derivative_test::run(&mut *borrowed, &opts)
};
let Some(report) = report else {
eprintln!(
"pounce: derivative_test was requested but the TNLP declined to \
supply the information the check needs (dimensions, bounds, or \
a starting point); no test was run."
);
return;
};
use pounce_common::journalist::JournalCategory;
for line in &report.lines {
eprintln!("{line}");
self.journalist.print(
JournalLevel::J_SUMMARY,
JournalCategory::J_MAIN,
&format!("{line}\n"),
);
}
}
pub fn unimplemented_linear_solver_message(value: &str) -> String {
format!(
"pounce: linear_solver={value} is not implemented. pounce provides \
`feral` (pure-Rust sparse symmetric, the default) and `ma57` (HSL, \
in a `--features ma57` build); the other names in the option's \
list come from the upstream Ipopt registry so an ipopt.opt written \
for Ipopt still parses. Selecting one used to run FERAL silently, \
which makes a backend comparison measure nothing — so it is \
refused instead. Use linear_solver=feral or linear_solver=ma57."
)
}
pub fn is_sqp_algorithm_selected(&self) -> bool {
let algo_sqp = matches!(
self.options.get_string_value("algorithm", ""),
Ok((v, true)) if v.eq_ignore_ascii_case("active-set-sqp")
);
let selection_sqp = matches!(
self.options.get_string_value("solver_selection", ""),
Ok((v, true)) if v.eq_ignore_ascii_case("qp-active-set")
);
algo_sqp || selection_sqp
}
fn optimize_sqp_tnlp(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
use pounce_nlp::ConstObjScaling;
use pounce_nlp::orig_ipopt_nlp::OrigIpoptNlp;
use pounce_nlp::tnlp_adapter::TNLPAdapter;
let t_start = std::time::Instant::now();
let (lo_inf, up_inf, fixed_treatment) = self.adapter_options();
let adapter = match TNLPAdapter::new_with_options(
Rc::clone(&tnlp),
lo_inf,
up_inf,
fixed_treatment,
) {
Ok(a) => Rc::new(RefCell::new(a)),
Err(_) => return ApplicationReturnStatus::InvalidProblemDefinition,
};
let obj_scaling_factor = self
.options
.get_numeric_value("obj_scaling_factor", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1.0);
let mut orig_nlp = match OrigIpoptNlp::new(
Rc::clone(&adapter),
Rc::new(ConstObjScaling(obj_scaling_factor)),
) {
Ok(n) => n,
Err(_) => return ApplicationReturnStatus::InternalError,
};
self.install_constant_derivative_hints(&mut orig_nlp);
let requested_relax = self
.options
.get_numeric_value("bound_relax_factor", "")
.ok()
.and_then(|(v, set)| set.then_some(v));
match requested_relax {
Some(factor) => {
let cap = self
.options
.get_numeric_value("constr_viol_tol", "")
.ok()
.and_then(|(v, set)| set.then_some(v))
.unwrap_or(1e-4);
orig_nlp.relax_bounds(factor, cap);
}
None => orig_nlp.snapshot_declared_bounds(),
}
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
.linear_backend_factory
.take()
.unwrap_or_else(|| self.make_backend_factory());
let mut alg = match builder.build_sqp_with_backend(factory) {
Some(a) => a,
None => return ApplicationReturnStatus::InternalError,
};
let console_output = match self.options.get_integer_value("print_level", "") {
Ok((v, true)) => v >= 1,
_ => true,
};
self.emit_problem_stats(&tnlp, console_output);
let warm = self.sqp_warm_start.take();
let res = match alg.optimize_with_warm_start(&mut sqp_adapter, warm) {
Ok(r) => r,
Err(e) => {
tracing::warn!(
target: "pounce::sqp",
"SQP solve failed: {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.sqp_qp_solves = res.n_qp_solves as Index;
stats.sqp_qp_working_set_changes = res.n_qp_working_set_changes as Index;
stats.final_objective = res.obj;
stats.final_scaled_objective = res.obj;
stats.final_dual_inf = res.final_stationarity;
stats.final_constr_viol = res.final_constr_viol;
stats.final_compl = 0.0; stats.final_kkt_error = res.final_stationarity.max(res.final_constr_viol);
stats.final_unscaled_dual_inf = res.final_stationarity;
stats.final_unscaled_constr_viol = res.final_constr_viol;
stats.final_unscaled_compl = 0.0;
stats.final_unscaled_kkt_error = res.final_stationarity.max(res.final_constr_viol);
stats.final_declared_box_viol = {
use pounce_linalg::dense_vector::DenseVectorSpace;
let nlp_borrow = nlp_rc.borrow();
let x_space = DenseVectorSpace::new(nlp_borrow.n());
let mut x_dv = x_space.make_new_dense();
x_dv.set_values(&res.x);
nlp_borrow
.declared_box_violation(&x_dv)
.unwrap_or(Number::NAN)
};
stats.total_wallclock_time_secs = t_start.elapsed().as_secs_f64();
}
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,
),
crate::sqp::SqpStatus::QpStepFailed => (
ApplicationReturnStatus::SearchDirectionBecomesTooSmall,
pounce_nlp::SolverReturn::ErrorInStepComputation,
),
crate::sqp::SqpStatus::QpIterationLimit => (
ApplicationReturnStatus::MaximumIterationsExceeded,
pounce_nlp::SolverReturn::MaxiterExceeded,
),
crate::sqp::SqpStatus::Unbounded => (
ApplicationReturnStatus::DivergingIterates,
pounce_nlp::SolverReturn::DivergingIterates,
),
crate::sqp::SqpStatus::InvalidNumber => (
ApplicationReturnStatus::InvalidNumberDetected,
pounce_nlp::SolverReturn::InvalidNumberDetected,
),
};
let refuted = withdraw_infeasibility_if_refuted(
&tnlp,
solver_status,
self.nlp_lower_bound_inf(),
self.nlp_upper_bound_inf(),
self.user_tol(),
);
let (app_status, solver_status) = if refuted == solver_status {
(app_status, solver_status)
} else {
(solver_return_to_app_status(refuted), refuted)
};
let _ = finalize_via_sqp(&nlp_rc, &res, solver_status, &tnlp, &self.last_finalize);
let final_status = self.apply_kkt_fidelity_gate(app_status);
self.emit_end_summary(final_status, &nlp_rc, console_output);
final_status
}
fn apply_kkt_fidelity_gate(
&self,
app_status: ApplicationReturnStatus,
) -> ApplicationReturnStatus {
if !matches!(app_status, ApplicationReturnStatus::SolveSucceeded) {
return app_status;
}
if let Ok((ftol, true)) = self.options.get_numeric_value("kkt_fidelity_tol", "") {
if ftol > 0.0 {
let unscaled_kkt = self.statistics.borrow().final_unscaled_kkt_error;
if unscaled_kkt > ftol {
tracing::info!(target: "pounce::diagnostics",
"kkt_fidelity_tol={ftol:.3e}: unscaled KKT error {unscaled_kkt:.3e} \
exceeds it — downgrading Solve_Succeeded → \
Solved_To_Acceptable_Level (pounce#173)");
return ApplicationReturnStatus::SolvedToAcceptableLevel;
}
}
}
app_status
}
fn nlp_lower_bound_inf(&self) -> Number {
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)
}
fn nlp_upper_bound_inf(&self) -> Number {
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)
}
fn user_tol(&self) -> Number {
self.options
.get_numeric_value("tol", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1e-8)
}
fn user_acceptable_tol(&self) -> Number {
self.options
.get_numeric_value("acceptable_tol", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1e-6)
}
fn user_constr_viol_tol(&self) -> Number {
self.options
.get_numeric_value("constr_viol_tol", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1e-4)
}
fn user_acceptable_constr_viol_tol(&self) -> Number {
self.options
.get_numeric_value("acceptable_constr_viol_tol", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1e-2)
}
fn user_primal_noise_floor_kappa(&self) -> Number {
self.options
.get_numeric_value("primal_noise_floor_kappa", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(64.0)
}
fn emit_problem_stats(&self, tnlp: &Rc<RefCell<dyn TNLP>>, console_output: bool) {
if !console_output {
return;
}
let (lo_inf, up_inf, fixed_treatment) = self.adapter_options();
if let Some(stats) =
pounce_solve_report::console::collect_stats(tnlp, lo_inf, up_inf, fixed_treatment)
{
let mut memo = self.last_printed_problem_stats.borrow_mut();
if memo.as_ref() == Some(&stats) {
if !self.in_retry_sequence.get() {
println!("Re-solving the same problem (a further attempt follows).");
println!();
}
} else {
pounce_solve_report::console::print_problem_stats(&stats);
*memo = Some(stats);
}
}
}
fn emit_end_summary(
&self,
app_status: ApplicationReturnStatus,
nlp: &Rc<RefCell<dyn IpoptNlp>>,
console_output: bool,
) {
{
let ec = nlp.borrow().eval_counts();
let mut stats = self.statistics.borrow_mut();
stats.num_obj_evals = ec[0];
stats.num_obj_grad_evals = ec[1];
stats.num_constr_evals = ec[2].max(ec[3]);
stats.num_constr_jac_evals = ec[4].max(ec[5]);
stats.num_hess_evals = ec[6];
}
if !console_output {
return;
}
let stats = self.statistics.borrow();
let counts = pounce_solve_report::console::EvalCounts {
n_obj: stats.num_obj_evals as u64,
n_grad_f: stats.num_obj_grad_evals as u64,
n_g: stats.num_constr_evals as u64,
n_jac_g: stats.num_constr_jac_evals as u64,
n_h: stats.num_hess_evals as u64,
};
pounce_solve_report::console::print_summary(app_status, &stats, &counts);
if self.end_verdict_deferrals.get() == 0 {
pounce_solve_report::console::print_exit_verdict(app_status);
}
}
fn crossover_options(&self) -> crate::crossover::CrossoverOptions {
let mut o = crate::crossover::CrossoverOptions::default();
if let Ok((v, true)) = self.options.get_bool_value("crossover", "") {
o.enabled = v;
}
if let Ok((v, true)) = self.options.get_integer_value("crossover_max_iter", "") {
o.max_iter = v.max(0) as u32;
}
if let Ok((v, true)) = self.options.get_numeric_value("crossover_mult_tol", "") {
o.mult_tol = v;
}
if let Ok((v, true)) = self.options.get_numeric_value("crossover_primal_tol", "") {
o.primal_tol = v;
}
o
}
fn maybe_crossover(
&mut self,
alg: &mut IpoptAlgorithm,
nlp_handle: &Rc<RefCell<dyn IpoptNlp>>,
solver_status: SolverReturn,
) {
self.crossover_report = None;
alg.data.borrow_mut().curr_from_crossover = false;
let xopts = self.crossover_options();
if !xopts.enabled {
return;
}
if !matches!(
solver_status,
SolverReturn::Success | SolverReturn::StopAtAcceptablePoint
) {
return;
}
let Some(curr) = alg.data.borrow().curr.clone() else {
return;
};
let seed = crate::crossover::CrossoverSeed {
x: dense_values(&*curr.x),
lambda_g: {
let mut v = dense_values(&*curr.y_c);
v.extend(dense_values(&*curr.y_d));
v
},
lambda_x: crate::sqp::ipopt_adapter::pack_bound_multipliers(
nlp_handle,
&dense_values(&*curr.z_l),
&dense_values(&*curr.z_u),
),
};
let snapshot = self.algorithm_builder_snapshot();
let sqp_opts = snapshot.sqp.clone();
let qp_opts = snapshot.sqp_qp.clone();
let mut adapter =
crate::sqp::IpoptNlpAdapter::new_with_declared_bounds(Rc::clone(nlp_handle));
let (report, accepted) = crate::crossover::run(
&mut adapter,
&seed,
&xopts,
&sqp_opts,
&qp_opts,
|| {
let mut f = self.make_backend_factory();
f(crate::alg_builder::LinearSolverChoice::Feral)
},
|step4_opts| {
let mut b = self.algorithm_builder_snapshot();
b.algorithm = crate::alg_builder::AlgorithmChoice::ActiveSetSqp;
b.sqp = step4_opts;
b.build_sqp_with_backend(self.make_backend_factory())
},
);
if let Some(res) = accepted {
self.install_crossover_iterate(alg, nlp_handle, &curr, &res);
self.sqp_last_working_set = res.working_set.clone();
}
tracing::debug!(target: "pounce::crossover", "crossover: {report:?}");
self.crossover_report = Some(report);
}
fn install_crossover_iterate(
&self,
alg: &mut IpoptAlgorithm,
nlp_handle: &Rc<RefCell<dyn IpoptNlp>>,
curr: &crate::iterates_vector::IteratesVector,
res: &crate::sqp::SqpResult,
) {
let (m_c, m_d) = {
let b = nlp_handle.borrow();
(b.m_eq() as usize, b.m_ineq() as usize)
};
let y_c = &res.lambda_g[..m_c];
let y_d = &res.lambda_g[m_c..];
let mut adapter = crate::sqp::IpoptNlpAdapter::new(Rc::clone(nlp_handle));
let c_all = crate::sqp::SqpProblemSpec::eval_c(&mut adapter, &res.x);
let s_new = &c_all[m_c..];
debug_assert_eq!(s_new.len(), m_d);
let (z_l, z_u) =
crate::sqp::ipopt_adapter::split_bound_multipliers(nlp_handle, &res.lambda_x);
let (v_l, v_u) = crate::sqp::ipopt_adapter::split_slack_multipliers(nlp_handle, y_d);
let mut out = curr.deep_copy();
let ok = set_dense(&mut *out.x, &res.x)
&& set_dense(&mut *out.s, s_new)
&& set_dense(&mut *out.y_c, y_c)
&& set_dense(&mut *out.y_d, y_d)
&& set_dense(&mut *out.z_l, &z_l)
&& set_dense(&mut *out.z_u, &z_u)
&& set_dense(&mut *out.v_l, &v_l)
&& set_dense(&mut *out.v_u, &v_u);
if !ok {
tracing::warn!(
target: "pounce::crossover",
"crossover result did not fit the iterate; keeping the interior point"
);
return;
}
let mut d = alg.data.borrow_mut();
d.set_curr(out.freeze());
d.curr_from_crossover = true;
}
fn algorithm_builder_snapshot(&self) -> AlgorithmBuilder {
let mut builder = AlgorithmBuilder {
quality_escalation_counter: Some(Rc::clone(&self.quality_escalations)),
..AlgorithmBuilder::default()
};
apply_sqp_options(&self.options, &mut builder.sqp);
apply_qp_subproblem_options(&self.options, &mut builder.sqp_qp);
builder
}
fn ma57_pivtol_bracket_refusal(&self) -> Option<String> {
for prefix in ["", "resto."] {
let Ok((pivtolmax, true)) = self.options.get_numeric_value("ma57_pivtolmax", prefix)
else {
continue;
};
let pivtol = self
.options
.get_numeric_value("ma57_pivtol", prefix)
.map(|(v, _)| v)
.unwrap_or(1e-8);
if pivtolmax < pivtol {
return Some(format!(
"pounce: {prefix}ma57_pivtolmax ({pivtolmax:e}) is below \
{prefix}ma57_pivtol ({pivtol:e}). ma57_pivtolmax is the ceiling MA57 \
may raise the pivot tolerance to when it escalates for accuracy, so it \
cannot sit below the tolerance it starts from. Raise \
{prefix}ma57_pivtolmax to at least {pivtol:e}, or lower \
{prefix}ma57_pivtol."
));
}
}
None
}
fn make_backend_factory(&self) -> LinearBackendFactory {
let mut feral_cfg = feral_config_from_options_scoped(&self.options, RefineCarveOut::None);
if let Some(perm) = &self.external_ordering {
feral_cfg.ordering = pounce_feral::OrderingMethod::External(perm.clone());
}
let ma57_cfg = ma57_config_from_options(&self.options, "");
default_backend_factory_with_sink(
feral_cfg,
ma57_cfg,
Arc::clone(&self.linsol_summary_sink),
)
}
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_with_mu_strategy_fallback(
&mut self,
tnlp: Rc<RefCell<dyn TNLP>>,
) -> ApplicationReturnStatus {
let first_status = self.optimize_constrained(Rc::clone(&tnlp));
let retry_worthy = match first_status {
ApplicationReturnStatus::MaximumIterationsExceeded => true,
ApplicationReturnStatus::SolvedToAcceptableLevel => {
self.mu_strategy_fallback_was_set() || !self.caller_set_termination_policy()
}
_ => false,
};
if !retry_worthy {
return first_status;
}
if matches!(
first_status,
ApplicationReturnStatus::MaximumIterationsExceeded
) && self.quality_escalations.get() >= 1
&& self
.options
.get_bool_value("feral_increase_quality_retry", "")
.map(|(v, _found)| v)
.unwrap_or(true)
&& self
.options
.get_bool_value("feral_increase_quality", "")
.map(|(v, _found)| v)
.unwrap_or(true)
{
return first_status;
}
let prev = self.options.get_string_value("mu_strategy", "").ok();
let was_adaptive = self.effective_mu_strategy_is_adaptive();
let flipped = if was_adaptive { "monotone" } else { "adaptive" };
let _ = self
.options
.set_string_value("mu_strategy", flipped, true, false);
let solution_floor = self.last_finalize.borrow().clone();
let certificate_floor = SolutionCertificate::of(&self.statistics.borrow());
let trace_floor = *self.last_iter_stats.borrow();
let retry_status = self.optimize_constrained(Rc::clone(&tnlp));
let _ = self.options.set_string_value(
"mu_strategy",
prev.as_ref()
.filter(|(_, found)| *found)
.map(|(v, _)| v.as_str())
.unwrap_or(if was_adaptive { "adaptive" } else { "monotone" }),
true,
false,
);
if matches!(retry_status, ApplicationReturnStatus::SolveSucceeded) {
return retry_status;
}
if let Some(floor) = solution_floor {
tracing::debug!(target: "pounce::algorithm",
"[POUNCE] the mu_strategy_fallback retry did not promote \
({:?} is not Solve_Succeeded); restoring the first attempt's \
solution and statistics alongside its status (pounce#870).",
retry_status);
floor.replay(&tnlp);
self.answer_restored_from_floor.set(true);
certificate_floor.restore_into(&mut self.statistics.borrow_mut());
if let Some(stats) = trace_floor {
let _ = tnlp.borrow_mut().intermediate_callback(
stats,
&TnlpIpoptData::default(),
&TnlpIpoptCq::default(),
);
}
}
first_status
}
fn is_dual_divergence_retry_enabled(&self) -> bool {
self.options
.get_bool_value("dual_divergence_retry", "")
.map(|(v, _found)| v)
.unwrap_or(true)
}
fn run_with_dual_divergence_retry(
&mut self,
tnlp: Rc<RefCell<dyn TNLP>>,
) -> ApplicationReturnStatus {
let first_status = self.dispatch_standard_solve(Rc::clone(&tnlp));
if !self.dual_divergence_signature.get() {
return first_status;
}
let retry_worthy = matches!(
first_status,
ApplicationReturnStatus::SolvedToAcceptableLevel
| ApplicationReturnStatus::RestorationFailed
);
if !retry_worthy {
return first_status;
}
let base_unscaled_kkt = self.statistics.borrow().final_unscaled_kkt_error;
let (base_viol, base_compl) = {
let st = self.statistics.borrow();
(st.final_unscaled_constr_viol, st.final_unscaled_compl)
};
let base_dual_inf = self.statistics.borrow().final_unscaled_dual_inf;
if !runaway_is_the_whole_residual(
base_dual_inf,
base_viol,
base_compl,
self.options
.get_numeric_value("dual_divergence_retry_du_floor", "")
.map(|(v, _)| v)
.unwrap_or(DUAL_DIV_RETRY_DU_FLOOR),
) {
tracing::debug!(target: "pounce::algorithm",
"[POUNCE] gh#884: the signature fired mid-trajectory, but the \
answer being reported is not a converged point with a runaway \
multiplier — unscaled dual {:.3e} against viol {:.3e} and \
complementarity {:.3e}. Nothing here for perturb_always_cd to \
repair, so no retry (gh#887).",
base_dual_inf, base_viol, base_compl);
return first_status;
}
let solution_floor = self.last_finalize.borrow().clone();
let certificate_floor = SolutionCertificate::of(&self.statistics.borrow());
let trace_floor = *self.last_iter_stats.borrow();
tracing::debug!(target: "pounce::algorithm",
"[POUNCE] gh#884: the primal settled while the multipliers ran away \
(base {:?}, unscaled KKT {:.3e}); re-solving from scratch with \
perturb_always_cd=yes.",
first_status, base_unscaled_kkt);
let prev = self.options.get_string_value("perturb_always_cd", "").ok();
let _ = self
.options
.set_string_value("perturb_always_cd", "yes", true, false);
let retry_status = self.dispatch_standard_solve(Rc::clone(&tnlp));
let _ = self.options.set_string_value(
"perturb_always_cd",
prev.as_ref()
.filter(|(_, found)| *found)
.map(|(v, _)| v.as_str())
.unwrap_or("no"),
true,
false,
);
let retry_unscaled_kkt = self.statistics.borrow().final_unscaled_kkt_error;
let retry_viol = self.statistics.borrow().final_unscaled_constr_viol;
let claimed_success_is_real = retry_unscaled_kkt <= self.dual_divergence_retry_accept_tol()
&& retry_viol <= self.dual_divergence_retry_accept_tol();
let retry_obj = self.statistics.borrow().final_objective;
let sense = if self
.options
.get_numeric_value("obj_scaling_factor", "")
.map(|(v, _)| v)
.unwrap_or(1.0)
< 0.0
{
-1.0
} else {
1.0
};
let answer_is_admissible = retry_answer_is_admissible(
certificate_floor.objective,
certificate_floor.unscaled_constr_viol,
retry_obj,
retry_viol,
self.dual_divergence_retry_accept_tol(),
sense,
);
let promote = matches!(retry_status, ApplicationReturnStatus::SolveSucceeded)
&& claimed_success_is_real
&& retry_unscaled_kkt < base_unscaled_kkt
&& answer_is_admissible;
let console_output = match self.options.get_integer_value("print_level", "") {
Ok((v, true)) => v >= 1,
_ => true,
};
if console_output {
println!();
if promote {
println!(
"gh#884 dual-divergence retry: promoted — unscaled KKT error \
{base_unscaled_kkt:.4e} -> {retry_unscaled_kkt:.4e}."
);
} else if !answer_is_admissible {
println!(
"gh#884 dual-divergence retry: declined on the ANSWER, not the \
certificate — the retry converged (unscaled KKT error \
{retry_unscaled_kkt:.4e} against the base attempt's \
{base_unscaled_kkt:.4e}) but its objective {retry_obj:.8e} at \
constraint violation {retry_viol:.4e} is not admissible next to \
the base attempt's {:.8e} at {:.4e}; the base attempt's answer \
is the one reported.",
certificate_floor.objective, certificate_floor.unscaled_constr_viol
);
} else {
println!(
"gh#884 dual-divergence retry: declined ({retry_status:?}, \
unscaled KKT error {retry_unscaled_kkt:.4e} against the base \
attempt's {base_unscaled_kkt:.4e}); the base attempt's answer \
is the one reported."
);
}
}
if promote {
self.dual_divergence_retry_promoted.set(true);
self.statistics.borrow_mut().dual_divergence_retry_promoted = true;
tracing::debug!(target: "pounce::algorithm",
"[POUNCE] gh#884: the retry promoted — unscaled KKT {:.3e} \
(base {:.3e}).",
retry_unscaled_kkt, base_unscaled_kkt);
return retry_status;
}
if let Some(floor) = solution_floor {
tracing::debug!(target: "pounce::algorithm",
"[POUNCE] gh#884: the retry did not promote ({:?}, unscaled KKT \
{:.3e} vs base {:.3e}); restoring the first attempt's solution \
and statistics alongside its status.",
retry_status, retry_unscaled_kkt, base_unscaled_kkt);
floor.replay(&tnlp);
self.answer_restored_from_floor.set(true);
certificate_floor.restore_into(&mut self.statistics.borrow_mut());
self.statistics.borrow_mut().dual_divergence_signature = true;
if let Some(stats) = trace_floor {
let _ = tnlp.borrow_mut().intermediate_callback(
stats,
&TnlpIpoptData::default(),
&TnlpIpoptCq::default(),
);
}
}
first_status
}
fn dual_divergence_retry_accept_tol(&self) -> Number {
self.options
.get_numeric_value("acceptable_tol", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1e-6)
}
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();
let x_here: Vec<Number> = w.last_x_trunc().to_vec();
drop(w);
let inner_ok = matches!(
last_status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
);
if !inner_ok {
break;
}
let feasible_here = {
let m_inner = tnlp
.borrow_mut()
.get_nlp_info()
.map(|i| i.m.max(0) as usize)
.unwrap_or(0);
let mut g_here = vec![0.0; m_inner];
let evaluated =
m_inner == 0 || tnlp.borrow_mut().eval_g(&x_here, true, &mut g_here);
evaluated
.then(|| {
original_space_feasibility(
&tnlp,
&x_here,
&g_here,
self.nlp_lower_bound_inf(),
self.nlp_upper_bound_inf(),
self.user_tol(),
self.user_acceptable_tol(),
self.user_constr_viol_tol(),
self.user_acceptable_constr_viol_tol(),
self.user_primal_noise_floor_kappa(),
)
})
.flatten()
.map(|f| f.negligible_at_tol)
};
match feasible_here {
Some(true) => break,
Some(false) => {}
None if slack_sum.is_finite() && slack_sum <= slack_tol => break,
None => {}
}
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 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];
let g_evaluated = m == 0 || tnlp.borrow_mut().eval_g(&x_trunc, false, &mut g_inner);
let feas = g_evaluated
.then(|| {
original_space_feasibility(
&tnlp,
&x_trunc,
&g_inner,
self.nlp_lower_bound_inf(),
self.nlp_upper_bound_inf(),
self.user_tol(),
self.user_acceptable_tol(),
self.user_constr_viol_tol(),
self.user_acceptable_constr_viol_tol(),
self.user_primal_noise_floor_kappa(),
)
})
.flatten();
if let Some(f) = feas.as_ref() {
let scaled_may_mirror = self.row_scaling_active.get() == Some(false);
let mut stats = self.statistics.borrow_mut();
stats.final_unscaled_constr_viol = f.max_violation;
stats.final_unscaled_kkt_error =
stats.final_unscaled_kkt_error.max(f.max_violation);
if scaled_may_mirror {
stats.final_constr_viol = f.max_violation;
stats.final_kkt_error = stats.final_kkt_error.max(f.max_violation);
stats.final_kkt_error_above_noise =
stats.final_kkt_error_above_noise.max(f.max_violation);
}
}
let inner_claimed_success = matches!(
last_status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
);
let downgrade_to_acceptable = inner_claimed_success
&& feas
.as_ref()
.is_some_and(|f| !f.negligible_at_tol && f.negligible_at_acceptable);
let infeasible_certificate = inner_claimed_success
&& match feas.as_ref() {
Some(f) => !f.negligible_at_acceptable,
None => slack_sum.is_finite() && slack_sum > slack_tol,
};
if let Some(f) = feas.as_ref()
&& inner_claimed_success
&& !f.negligible_at_tol
{
tracing::info!(
target: "pounce::application",
"l1 penalty-barrier: the inner solve converged the augmented NLP, \
but the returned point violates the model's own constraints by \
{:.3e}, which does not meet tol; reporting {} rather than success \
(gh#794)",
f.max_violation,
if f.negligible_at_acceptable {
"Solved_To_Acceptable_Level"
} else {
"an infeasibility verdict"
},
);
}
let refuted = infeasible_certificate
&& withdraw_infeasibility_if_refuted(
&tnlp,
SolverReturn::LocalInfeasibility,
self.nlp_lower_bound_inf(),
self.nlp_upper_bound_inf(),
self.user_tol(),
) != SolverReturn::LocalInfeasibility;
let final_solver_status = match (infeasible_certificate, refuted) {
(true, false) => SolverReturn::LocalInfeasibility,
(true, true) => SolverReturn::ErrorInStepComputation,
(false, _) if downgrade_to_acceptable => SolverReturn::StopAtAcceptablePoint,
(false, _) => solver_status,
};
let final_app_status = match (infeasible_certificate, refuted) {
(true, false) => ApplicationReturnStatus::InfeasibleProblemDetected,
(true, true) => ApplicationReturnStatus::ErrorInStepComputation,
(false, _) if downgrade_to_acceptable => {
ApplicationReturnStatus::SolvedToAcceptableLevel
}
(false, _) => last_status,
};
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)
}
fn overdetermined_model_certified_infeasible(&self, tnlp: &Rc<RefCell<dyn TNLP>>) -> bool {
let mut opts = pounce_presolve::PresolveOptions::from_options_list(&self.options)
.unwrap_or_else(|_| pounce_presolve::PresolveOptions::defaults());
opts.enabled = true;
opts.bound_tightening = true;
opts.auxiliary = false;
opts.fbbt = false;
opts.redundant_constraint_removal = false;
opts.licq_check = false;
opts.warm_z_bounds = false;
let mut probe =
pounce_presolve::PresolveTnlp::new(Rc::clone(tnlp), opts).probing_without_a_solve();
if probe.get_nlp_info().is_none() {
return false;
}
probe.certified_infeasible().is_some()
}
fn optimize_constrained(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
let t_start = Instant::now();
self.row_scaling_active.set(None);
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);
*self.warm_start_diag.borrow_mut() = None;
let read_yes = |key: &str| -> bool {
self.options
.get_bool_value(key, "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(false)
};
let timing_enabled = read_yes("timing_statistics") || read_yes("print_timing_statistics");
timing.set_detailed_enabled(timing_enabled);
timing.overall_alg.start();
match self.linsol_summary_sink.lock() {
Ok(mut guard) => {
*guard = LinearSolverSummary::default();
}
_ => {
debug_assert!(false, "linsol summary sink mutex poisoned");
}
}
self.quality_escalations.set(0);
let (lo_inf, up_inf, fixed_treatment) = self.adapter_options();
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 obj_scaling_factor = self
.options
.get_numeric_value("obj_scaling_factor", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(1.0);
let mut orig_nlp = match OrigIpoptNlp::new(
Rc::clone(&adapter),
Rc::new(ConstObjScaling(obj_scaling_factor)),
) {
Ok(n) => n,
Err(_) => {
timing.overall_alg.end();
return ApplicationReturnStatus::InternalError;
}
};
orig_nlp.set_timing_stats(Rc::clone(&timing));
self.install_constant_derivative_hints(&mut orig_nlp);
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();
if self.overdetermined_model_certified_infeasible(&tnlp) {
use pounce_common::journalist::JournalCategory;
self.journalist.print(
JournalLevel::J_SUMMARY,
JournalCategory::J_MAIN,
"\nEXIT: Problem has too few degrees of freedom, and bound \
propagation proves its constraints inconsistent.\n\
No feasible point exists; the solve was not run.\n",
);
return ApplicationReturnStatus::InfeasibleProblemDetected;
}
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 honor_original_bounds = self
.options
.get_bool_value("honor_original_bounds", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(false);
orig_nlp.set_honor_original_bounds(honor_original_bounds);
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" | "curvature-based" => 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 mut builder = self.algorithm_builder_from_options();
if matches!(
builder.hessian_approximation,
HessianApproxChoice::Partitioned | HessianApproxChoice::FiniteDifference
) {
builder.objective_nonlinear_vars = adapter.borrow().objective_nonlinear_vars();
}
if matches!(
builder.hessian_approximation,
HessianApproxChoice::LimitedMemory | HessianApproxChoice::FiniteDifference
) {
let num_linear_variables = self
.options
.get_integer_value("num_linear_variables", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(0);
match adapter
.borrow()
.quasi_newton_nonlinear_vars(num_linear_variables)
{
Ok(mask) => builder.limited_memory_nonlinear_vars = mask,
Err(e) => {
use pounce_common::journalist::JournalCategory;
self.journalist.print(
JournalLevel::J_ERROR,
JournalCategory::J_MAIN,
&format!("\nEXIT: Invalid nonlinear-variable list: {}\n", e.message),
);
timing.overall_alg.end();
return ApplicationReturnStatus::InvalidProblemDefinition;
}
}
}
let mut feral_cfg = feral_config_from_options(&self.options);
if let Some(indices) = &self.kkt_schur_block {
builder.set_kkt_schur(indices.clone(), feral_cfg.clone());
}
if let Some(perm) = &self.external_ordering {
feral_cfg.ordering = pounce_feral::OrderingMethod::External(perm.clone());
}
let ma57_cfg = ma57_config_from_options(&self.options, "");
let factory = self.linear_backend_factory.take().unwrap_or_else(|| {
default_backend_factory_with_sink(
feral_cfg,
ma57_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);
data.borrow_mut().deadline = Some(pounce_common::timing::Deadline::new(
builder.conv_check.max_wall_time,
builder.conv_check.max_cpu_time,
));
let cq: crate::ipopt_cq::IpoptCqHandle = Rc::new(RefCell::new(
IpoptCalculatedQuantities::new(Rc::clone(&data), Rc::clone(&nlp_handle)),
));
if let Ok((v, true)) = self.options.get_numeric_value("slack_move", "") {
cq.borrow_mut().slack_move = v;
}
cq.borrow_mut().kappa_d = builder.kappa_d;
cq.borrow_mut().s_max = builder.s_max;
{
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);
}
if let Some(snap) = self.warm_start_iterate.take() {
let dims_match = {
let borrow = data.borrow();
borrow
.curr
.as_ref()
.map(|c| iterates_dims(c) == iterates_dims(snap.iterates()))
.unwrap_or(false)
};
if dims_match {
data.borrow_mut().set_curr(snap.iterates().clone());
data.borrow_mut().curr_mu = snap.mu();
} else {
tracing::warn!(
target: "pounce::warm_start",
"debugger warm-restart iterate dimensions differ from the fresh \
solve; ignoring the captured iterate and seeding normally"
);
}
}
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.last_iter_stats_sink = Some(Rc::clone(&self.last_iter_stats));
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));
}
if let Some(hook) = self.debug_hook.take() {
alg = alg.with_debug_hook(hook);
}
alg.max_iter = max_iter;
alg.kappa_sigma = builder.kappa_sigma;
alg.slack_based_scaling = matches!(
builder.linear_system_scaling,
crate::alg_builder::LinearSystemScalingChoice::SlackBased
);
alg.recalc_y = builder.recalc_y;
alg.recalc_y_feas_tol = builder.recalc_y_feas_tol;
alg.start_with_resto = builder.resto.start_with_resto;
alg.tiny_step_tol = builder.tiny_step_tol;
alg.tiny_step_y_tol = builder.tiny_step_y_tol;
alg.diverging_iterates_tol = builder.diverging_iterates_tol;
alg.dual_diverging_streak = builder.dual_diverging_streak.max(0) as usize;
alg.dual_divergence_retry_step_tol = builder.dual_divergence_retry_step_tol;
alg.dual_divergence_retry_du_floor = builder.dual_divergence_retry_du_floor;
alg.resto_decline_deferrals = builder.resto_decline_deferrals.max(0) as usize;
alg.resto_decline_progress_ratio = builder.resto_decline_progress_ratio;
alg.neg_curv_escapes = builder.neg_curv_escapes.max(0) as usize;
alg.lbfgs_ls_failure_restarts = builder.limited_memory_ls_failure_restarts.max(0) as usize;
alg.kkt_fidelity_tol = builder.kkt_fidelity_tol;
let console_output = match self.options.get_integer_value("print_level", "") {
Ok((v, true)) => v >= 1,
_ => true,
};
if !console_output {
alg.print_iter_output = false;
if let Some(resto) = alg.restoration.as_mut() {
resto.set_print_iter_output(false);
}
}
self.emit_problem_stats(&tnlp, console_output);
let iter_capture = self
.record_iter_history
.then(pounce_observability::IterCaptureGuard::start);
let solver_status = alg.optimize();
self.least_square_init_report = alg.least_square_init_report();
let captured_iters = iter_capture.map(|g| g.finish()).unwrap_or_default();
pounce_observability::extend_active_capture(&captured_iters);
timing.overall_alg.end();
self.maybe_crossover(&mut alg, &nlp_handle, solver_status);
{
let mut stats = self.statistics.borrow_mut();
{
let d = alg.data.borrow();
stats.iteration_count = d.iter_count;
stats.final_mu = d.curr_mu;
}
*self.warm_start_diag.borrow_mut() = alg.data.borrow().warm_start_diagnostics.clone();
stats.total_wallclock_time_secs = t_start.elapsed().as_secs_f64();
if let Some(fd) = alg.bundle.hess.fd_hessian_stats() {
use crate::hess::fd_hessian::FdPatternSource;
stats.fd_hessian_pattern_used = match fd.pattern_used {
Some(FdPatternSource::Declared) => 0,
Some(FdPatternSource::Jacobian) => 1,
None => -1,
};
stats.fd_hessian_nnz = fd.nnz as Index;
stats.fd_hessian_n = fd.n as Index;
stats.fd_hessian_groups = fd.groups as Index;
stats.fd_hessian_rho_max = fd.rho_max as Index;
stats.fd_hessian_coloring_fell_back = fd.coloring_fell_back;
stats.fd_hessian_objective_clique_widened = fd.objective_clique_widened;
}
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.quality_escalations = self.quality_escalations.get() as Index;
self.dual_divergence_signature
.set(self.dual_divergence_signature.get() || alg.dual_divergence_signature());
stats.dual_divergence_signature = self.dual_divergence_signature.get();
stats.dual_divergence_retry_promoted = self.dual_divergence_retry_promoted.get();
stats.iterations = captured_iters;
if solver_status != SolverReturn::InvalidProblemDefinition {
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();
stats.final_declared_constr_viol = if bound_relax_factor > 0.0 {
cq.curr_declared_primal_violation_max()
} else {
Number::NAN
};
stats.final_declared_box_viol = cq.curr_declared_box_violation_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();
stats.final_kkt_error_above_noise = cq
.curr_nlp_error_above_primal_noise(builder.conv_check.primal_noise_floor_kappa);
stats.final_unscaled_dual_inf = cq.curr_unscaled_dual_infeasibility_max();
stats.final_unscaled_constr_viol = cq.curr_unscaled_primal_infeasibility_max();
{
let nlp_ref = nlp_handle.borrow();
self.row_scaling_active.set(Some(
nlp_ref.c_scale_vec().is_some() || nlp_ref.d_scale_vec().is_some(),
));
}
stats.final_unscaled_compl = cq.curr_unscaled_complementarity_max();
stats.final_unscaled_kkt_error = cq.curr_unscaled_nlp_error();
if let Some(report) = self.crossover_report.as_ref()
&& report.accepted()
&& report.compl_after.is_finite()
{
let compl_declared = report.compl_after;
stats.final_compl = compl_declared;
stats.final_kkt_error =
cq.curr_nlp_error_with_complementarity(compl_declared, 0.0);
stats.final_kkt_error_above_noise = cq.curr_nlp_error_with_complementarity(
compl_declared,
builder.conv_check.primal_noise_floor_kappa,
);
let df = cq.obj_scaling_factor().abs();
stats.final_unscaled_compl = if df == 0.0 || df == 1.0 {
compl_declared
} else {
compl_declared / df
};
stats.final_unscaled_kkt_error = stats
.final_unscaled_dual_inf
.max(stats.final_unscaled_constr_viol)
.max(stats.final_unscaled_compl);
}
}
}
let solver_status =
withdraw_infeasibility_if_refuted(&tnlp, solver_status, lo_inf, up_inf, tol);
let app_status = self.apply_kkt_fidelity_gate(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);
}
}
}
if solver_status != SolverReturn::InvalidProblemDefinition {
match finalize_via_orig_nlp(
&nlp_handle,
&alg,
solver_status,
app_status,
&tnlp,
&self.last_finalize,
) {
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,
);
}
self.emit_end_summary(app_status, &nlp_handle, console_output);
app_status
}
pub fn algorithm_builder_from_options(&self) -> AlgorithmBuilder {
let mut builder = AlgorithmBuilder::new();
builder.quality_escalation_counter = Some(Rc::clone(&self.quality_escalations));
if let Ok((v, true)) = self.options.get_string_value("fast_step_computation", "") {
builder.fast_step_computation = v.eq_ignore_ascii_case("yes");
}
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,
"partitioned" => HessianApproxChoice::Partitioned,
"finite-difference" => HessianApproxChoice::FiniteDifference,
_ => HessianApproxChoice::Exact,
};
}
}
if builder.hessian_approximation == HessianApproxChoice::LimitedMemory
&& !self.mu_strategy_was_set()
{
builder.mu_strategy = MuStrategyChoice::Adaptive;
}
if let Ok((v, found)) = self.options.get_string_value("partitioned_update_type", "") {
if found {
builder.partitioned_update_type = match v.as_str() {
"bfgs" => UpdateType::Bfgs,
_ => UpdateType::Sr1,
};
builder.partitioned_update_type_was_set = true;
}
}
if let Ok((v, found)) = self
.options
.get_integer_value("partitioned_max_element", "")
{
if found && v > 0 {
builder.partitioned_max_element = v as usize;
}
}
if let Ok((v, found)) = self.options.get_string_value("fd_hessian_pattern", "") {
if found {
builder.fd_hessian_pattern = match v.as_str() {
"jacobian" => crate::hess::fd_hessian::FdPatternSource::Jacobian,
_ => crate::hess::fd_hessian::FdPatternSource::Declared,
};
}
}
if let Ok((v, found)) = self.options.get_string_value("fd_hessian_coloring", "") {
if found {
builder.fd_hessian_coloring = match v.as_str() {
"cpr" => crate::hess::fd_hessian::FdColoring::Cpr,
_ => crate::hess::fd_hessian::FdColoring::Star,
};
}
}
if let Ok((v, found)) = self.options.get_numeric_value("fd_hessian_reuse_tol", "") {
if found && v >= 0.0 {
builder.fd_hessian_reuse_tol = v;
}
}
if let Ok((v, found)) = self.options.get_string_value("partitioned_elements", "") {
if found {
builder.partitioned_elements = match v.as_str() {
"blocks" => crate::hess::partitioned_quasi_newton::ElementMode::PrimalBlock,
_ => crate::hess::partitioned_quasi_newton::ElementMode::PerConstraint,
};
}
}
if let Ok((v, found)) = self.options.get_integer_value("partitioned_block_size", "") {
if found && v > 0 {
builder.partitioned_block_size = v as usize;
}
}
if let Ok((v, found)) = self
.options
.get_numeric_value("partitioned_curvature_cap", "")
{
if found && v > 0.0 {
builder.partitioned_curvature_cap = v;
}
}
if let Ok((v, found)) = self
.options
.get_string_value("limited_memory_update_type", "")
{
if found {
builder.limited_memory_update_type = match v.as_str() {
"sr1" => UpdateType::Sr1,
_ => UpdateType::Bfgs,
};
}
}
if let Ok((v, found)) = self
.options
.get_integer_value("limited_memory_max_history", "")
{
if found && v >= 0 {
builder.limited_memory_max_history = v as Index;
}
}
if let Ok((v, found)) = self
.options
.get_string_value("limited_memory_initialization", "")
{
if found {
use crate::hess::lim_mem_quasi_newton::InitialApprox;
builder.limited_memory_initialization = match v.as_str() {
"scalar1" => InitialApprox::Scalar1,
"scalar2" => InitialApprox::Scalar2,
"scalar3" => InitialApprox::Scalar3,
"scalar4" => InitialApprox::Scalar4,
"constant" => InitialApprox::Constant,
"history-max" => InitialApprox::HistoryMax,
_ => InitialApprox::Scalar2,
};
}
}
if let Ok((v, true)) = self.options.get_string_value("recalc_y", "") {
builder.recalc_y = v == "yes";
}
if let Ok((v, true)) = self.options.get_numeric_value("recalc_y_feas_tol", "") {
builder.recalc_y_feas_tol = v;
}
if let Ok((v, true)) = self
.options
.get_numeric_value("limited_memory_init_val", "")
{
builder.limited_memory_init_val = v;
}
if let Ok((v, true)) = self
.options
.get_integer_value("limited_memory_max_skipping", "")
{
if v >= 0 {
builder.limited_memory_max_skipping = v as Index;
}
}
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, true)) = self
.options
.get_numeric_value("limited_memory_init_val_max", "")
{
builder.limited_memory_init_val_max = v;
}
if let Ok((v, true)) = self
.options
.get_numeric_value("limited_memory_init_val_min", "")
{
builder.limited_memory_init_val_min = v;
}
if let Ok((v, _found)) = self.options.get_string_value("linear_solver", "") {
let requested = if v.eq_ignore_ascii_case("ma57") {
LinearSolverChoice::Ma57
} else {
LinearSolverChoice::Feral
};
builder.linear_solver =
if matches!(requested, LinearSolverChoice::Ma57) && !cfg!(feature = "ma57") {
LinearSolverChoice::Feral
} else {
requested
};
}
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,
"slack-based" => crate::alg_builder::LinearSystemScalingChoice::SlackBased,
_ => 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("obj_scale_certificate_threshold") {
builder.conv_check.obj_scale_certificate_threshold = v;
}
if let Some(v) = read_num("primal_noise_floor_kappa") {
builder.conv_check.primal_noise_floor_kappa = v;
}
if let Some(v) = read_num("acceptable_progress_kappa") {
builder.conv_check.acceptable_progress_kappa = v;
}
if let Some(v) = read_num("dual_inf_scale_kappa") {
builder.conv_check.dual_inf_scale_kappa = v;
}
if let Some(v) = read_num("kkt_fidelity_tol") {
builder.kkt_fidelity_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("kappa_sigma") {
builder.kappa_sigma = v;
}
if let Some(v) = read_num("kappa_d") {
builder.kappa_d = v;
}
if let Some(v) = read_num("s_max") {
builder.s_max = v;
}
if let Some(v) = read_num("tiny_step_tol") {
builder.tiny_step_tol = v;
}
if let Some(v) = read_num("tiny_step_y_tol") {
builder.tiny_step_y_tol = v;
}
if let Some(v) = read_num("diverging_iterates_tol") {
builder.diverging_iterates_tol = v;
}
if let Some(v) = read_int("dual_diverging_streak") {
builder.dual_diverging_streak = v;
}
if let Some(v) = read_num("dual_divergence_retry_step_tol") {
builder.dual_divergence_retry_step_tol = v;
}
if let Some(v) = read_num("dual_divergence_retry_du_floor") {
builder.dual_divergence_retry_du_floor = v;
}
if let Some(v) = read_int("resto_decline_deferrals") {
builder.resto_decline_deferrals = v;
}
if let Some(v) = read_num("resto_decline_progress_ratio") {
builder.resto_decline_progress_ratio = v;
}
if let Some(v) = read_int("neg_curv_escapes") {
builder.neg_curv_escapes = v;
}
if let Some(v) = read_int("limited_memory_ls_failure_restarts") {
builder.limited_memory_ls_failure_restarts = 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("tau_min") {
builder.mu.tau_min = 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_max_free_returns") {
builder.mu.adaptive_mu_max_free_returns = v;
}
if let Some(v) = read_num("adaptive_mu_budget_pin_fraction") {
builder.mu.adaptive_mu_budget_pin_fraction = 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 Some(v) = read_num("filter_margin_fact") {
builder.mu.filter_margin_fact = v;
}
if let Some(v) = read_num("filter_max_margin") {
builder.mu.filter_max_margin = 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_num("alpha_red_factor") {
builder.line_search.alpha_red_factor = v;
}
if let Some(v) = read_num("alpha_red_factor_min") {
builder.line_search.alpha_red_factor_min = Some(v);
}
if let Some(v) = read_int("accept_after_max_steps") {
builder.line_search.accept_after_max_steps = v;
}
if let Some(v) = read_num("eta_phi") {
builder.line_search.eta_phi = v;
}
if let Some(v) = read_num("delta") {
builder.line_search.delta = v;
}
if let Some(v) = read_num("theta_min_fact") {
builder.line_search.theta_min_fact = v;
}
if let Some(v) = read_num("theta_max_row_scale_kappa") {
builder.line_search.theta_max_row_scale_kappa = v;
}
if let Some(v) = read_int("theta_max_adaptive_trigger") {
builder.line_search.theta_max_adaptive_trigger = v.max(0) as u32;
}
if let Some(v) = read_num("theta_max_adaptive_factor") {
builder.line_search.theta_max_adaptive_factor = v;
}
if let Some(v) = read_int("theta_max_adaptive_max_raises") {
builder.line_search.theta_max_adaptive_max_raises = v.max(0) as u32;
}
if let Some(v) = read_num("theta_max_fact") {
builder.line_search.theta_max_fact = v;
}
if let Some(v) = read_num("gamma_phi") {
builder.line_search.gamma_phi = v;
}
if let Some(v) = read_num("gamma_theta") {
builder.line_search.gamma_theta = v;
}
if let Some(v) = read_num("s_phi") {
builder.line_search.s_phi = v;
}
if let Some(v) = read_num("s_theta") {
builder.line_search.s_theta = v;
}
if let Some(v) = read_num("alpha_min_frac") {
builder.line_search.alpha_min_frac = v;
}
if let Some(v) = read_num("obj_max_inc") {
builder.line_search.obj_max_inc = v;
}
if let Some(v) = read_int("max_filter_resets") {
builder.line_search.max_filter_resets = v;
}
if let Some(v) = read_int("filter_reset_trigger") {
builder.line_search.filter_reset_trigger = v;
}
if let Ok((v, true)) = self
.options
.get_bool_value("filter_theta_roundoff_retry", "")
{
builder.line_search.filter_theta_roundoff_retry = v;
}
if let Some(v) = read_num("nu_init") {
builder.line_search.nu_init = v;
}
if let Some(v) = read_num("nu_inc") {
builder.line_search.nu_inc = v;
}
if let Some(v) = read_num("rho") {
builder.line_search.rho = v;
}
if let Some(v) = read_num("eta_penalty") {
builder.line_search.eta_penalty = v;
}
if let Some(v) = read_int("max_soc") {
builder.line_search.max_soc = v;
}
if let Some(v) = read_num("kappa_soc") {
builder.line_search.kappa_soc = v;
}
if let Some(v) = read_int("soc_method") {
builder.line_search.soc_method = v;
}
if let Some(v) = read_num("max_hessian_perturbation") {
builder.perturbation.max_hessian_perturbation = v;
}
if let Some(v) = read_num("min_hessian_perturbation") {
builder.perturbation.min_hessian_perturbation = v;
}
if let Some(v) = read_num("perturb_inc_fact_first") {
builder.perturbation.perturb_inc_fact_first = v;
}
if let Some(v) = read_num("perturb_inc_fact") {
builder.perturbation.perturb_inc_fact = v;
}
if let Some(v) = read_num("perturb_dec_fact") {
builder.perturbation.perturb_dec_fact = v;
}
if let Some(v) = read_num("first_hessian_perturbation") {
builder.perturbation.first_hessian_perturbation = v;
}
if let Some(v) = read_num("jacobian_regularization_value") {
builder.perturbation.jacobian_regularization_value = v;
}
if let Some(v) = read_num("jacobian_regularization_exponent") {
builder.perturbation.jacobian_regularization_exponent = v;
}
if let Ok((v, true)) = self.options.get_bool_value("perturb_always_cd", "") {
builder.perturbation.perturb_always_cd = v;
}
if let Some(v) = read_int("perturb_delta_c_max_rungs") {
builder.perturbation.perturb_delta_c_max_rungs = v;
}
if let Some(v) = read_int("min_refinement_steps") {
builder.refinement.min_refinement_steps = v;
}
if let Some(v) = read_int("max_refinement_steps") {
builder.refinement.max_refinement_steps = v;
}
if let Some(v) = read_num("residual_ratio_max") {
builder.refinement.residual_ratio_max = v;
}
if let Some(v) = read_num("residual_ratio_singular") {
builder.refinement.residual_ratio_singular = v;
}
if let Some(v) = read_num("residual_improvement_factor") {
builder.refinement.residual_improvement_factor = v;
}
if let Some(v) = read_num("neg_curv_test_tol") {
builder.refinement.neg_curv_test_tol = v;
}
if let Ok((v, true)) = self.options.get_bool_value("neg_curv_test_reg", "") {
builder.refinement.neg_curv_test_reg = v;
}
if let Some(v) = read_num("bound_mult_reset_threshold") {
builder.resto.bound_mult_reset_threshold = v;
}
if let Some(v) = read_num("constr_mult_reset_threshold") {
builder.resto.constr_mult_reset_threshold = v;
}
if let Some(v) = read_num("resto_penalty_parameter") {
builder.resto.resto_penalty_parameter = v;
}
if let Some(v) = read_num("resto_proximity_weight") {
builder.resto.resto_proximity_weight = v;
}
if let Some(v) = read_num("required_infeasibility_reduction") {
builder.resto.required_infeasibility_reduction = v;
}
let read_yes = |key: &str| -> Option<bool> {
match self.options.get_string_value(key, "") {
Ok((v, true)) => Some(v.eq_ignore_ascii_case("yes")),
_ => None,
}
};
if let Some(v) = read_yes("evaluate_orig_obj_at_resto_trial") {
builder.resto.evaluate_orig_obj_at_resto_trial = v;
}
if let Some(v) = read_yes("expect_infeasible_problem") {
builder.resto.expect_infeasible_problem = v;
}
if let Some(v) = read_yes("start_with_resto") {
builder.resto.start_with_resto = v;
}
if let Some(v) = read_int("max_resto_iter") {
builder.resto.max_resto_iter = 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 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_recentering", "") {
if found {
builder.warm.recentering = if v.eq_ignore_ascii_case("none") {
crate::alg_builder::WarmStartRecentering::None
} else {
crate::alg_builder::WarmStartRecentering::Residual
};
}
}
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 iterates_dims(c: &IteratesVector) -> [i32; 8] {
[
c.x.dim(),
c.s.dim(),
c.y_c.dim(),
c.y_d.dim(),
c.z_l.dim(),
c.z_u.dim(),
c.v_l.dim(),
c.v_u.dim(),
]
}
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,
}
}
#[derive(Debug, Clone, Default)]
pub struct Ma57Config {
#[cfg(feature = "ma57")]
opts: pounce_hsl::Ma57Options,
}
impl Ma57Config {
#[cfg(feature = "ma57")]
pub fn options(&self) -> &pounce_hsl::Ma57Options {
&self.opts
}
}
pub fn ma57_config_from_options(
options: &pounce_common::options_list::OptionsList,
prefix: &str,
) -> Ma57Config {
#[cfg(feature = "ma57")]
{
Ma57Config {
opts: pounce_hsl::Ma57Options::from_options_list(options, prefix),
}
}
#[cfg(not(feature = "ma57"))]
{
let _ = (options, prefix);
Ma57Config::default()
}
}
fn make_ma57_backend(
ma57_cfg: &Ma57Config,
feral_fallback: impl FnOnce() -> Box<dyn SparseSymLinearSolverInterface>,
) -> Box<dyn SparseSymLinearSolverInterface> {
#[cfg(feature = "ma57")]
{
let _ = feral_fallback;
Box::new(pounce_hsl::Ma57SolverInterface::with_options(
*ma57_cfg.options(),
))
}
#[cfg(not(feature = "ma57"))]
{
let _ = ma57_cfg;
feral_fallback()
}
}
pub fn default_backend_factory(
feral_cfg: pounce_feral::FeralConfig,
ma57_cfg: Ma57Config,
) -> LinearBackendFactory {
Box::new(
move |choice: LinearSolverChoice| -> Box<dyn SparseSymLinearSolverInterface> {
match choice {
LinearSolverChoice::Feral => Box::new(
pounce_feral::FeralSolverInterface::with_config(feral_cfg.clone()),
),
LinearSolverChoice::Ma57 => make_ma57_backend(&ma57_cfg, || {
Box::new(pounce_feral::FeralSolverInterface::with_config(
feral_cfg.clone(),
))
}),
}
},
)
}
pub fn default_backend_factory_with_sink(
feral_cfg: pounce_feral::FeralConfig,
ma57_cfg: Ma57Config,
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.clone())
.with_summary_sink(Arc::clone(&sink)),
),
LinearSolverChoice::Ma57 => make_ma57_backend(&ma57_cfg, || {
Box::new(
pounce_feral::FeralSolverInterface::with_config(feral_cfg.clone())
.with_summary_sink(Arc::clone(&sink)),
)
}),
}
},
)
}
pub fn feral_config_from_options(
options: &pounce_common::options_list::OptionsList,
) -> pounce_feral::FeralConfig {
feral_config_from_options_scoped(options, RefineCarveOut::InteriorPoint)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefineCarveOut {
InteriorPoint,
None,
}
pub fn feral_config_from_options_scoped(
options: &pounce_common::options_list::OptionsList,
carve_out: RefineCarveOut,
) -> 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;
}
let limited_memory = carve_out == RefineCarveOut::InteriorPoint
&& matches!(
options.get_string_value("hessian_approximation", ""),
Ok((ref s, true)) if s == "limited-memory"
);
if std::env::var_os("POUNCE_FERAL_REFINE").is_none() {
if let Ok((v, _)) = options.get_bool_value("feral_refine", "") {
cfg.refine = v;
}
if limited_memory {
cfg.refine = false;
}
}
if let Ok((v, true)) = options.get_bool_value("feral_refine", "") {
cfg.refine = v;
}
if let Ok((v, true)) = options.get_bool_value("feral_increase_quality", "") {
cfg.increase_quality = v;
}
if let Ok((v, true)) = options.get_integer_value("feral_refine_steps", "") {
cfg.refine_max_steps = v.max(0) as usize;
}
if let Ok((v, true)) = options.get_numeric_value("feral_refine_target", "") {
cfg.refine_target = v.max(0.0);
}
if let Ok((v, true)) = options.get_bool_value("feral_static_pivoting", "") {
cfg.static_pivoting = Some(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_inertia_pivot_floor", "") {
cfg.inertia_pivot_floor = Some(v);
}
if let Ok((v, true)) = options.get_numeric_value("feral_min_par_flops", "") {
cfg.min_par_flops = Some(v as u64);
}
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;
}
}
if let Ok((v, true)) = options.get_string_value("feral_scaling", "") {
if let Some(s) = pounce_feral::parse_scaling_strategy(&v) {
cfg.scaling = s;
}
}
cfg
}
fn withdraw_infeasibility_if_refuted(
tnlp: &Rc<RefCell<dyn TNLP>>,
solver_status: SolverReturn,
lo_inf: Number,
up_inf: Number,
tol: Number,
) -> SolverReturn {
if solver_status != SolverReturn::LocalInfeasibility {
return solver_status;
}
if tnlp.borrow().presolve_infeasibility_proof().is_some() {
return solver_status;
}
match crate::infeasibility_refutation::starting_point_refutes_infeasibility(
tnlp, lo_inf, up_inf, tol,
) {
Some(w) => {
tracing::debug!(
target: "pounce::application",
"[PN_INFEAS_REFUTED] the model's starting point satisfies every constraint \
(max violation {:.3e}) — withdrawing Infeasible_Problem_Detected",
w.max_violation
);
SolverReturn::ErrorInStepComputation
}
None => solver_status,
}
}
struct OriginalSpaceFeasibility {
max_violation: Number,
negligible_at_tol: bool,
negligible_at_acceptable: bool,
}
fn original_space_feasibility(
tnlp: &Rc<RefCell<dyn TNLP>>,
x: &[Number],
g: &[Number],
lower_bound_inf: Number,
upper_bound_inf: Number,
tol: Number,
acceptable_tol: Number,
constr_viol_tol: Number,
acceptable_constr_viol_tol: Number,
noise_floor_kappa: Number,
) -> Option<OriginalSpaceFeasibility> {
use pounce_common::tolerance::is_negligible;
let info = tnlp.borrow_mut().get_nlp_info()?;
let n = info.n.max(0) as usize;
let m = info.m.max(0) as usize;
if x.len() < n || g.len() < m {
return None;
}
let mut x_l = vec![0.0; n];
let mut x_u = vec![0.0; n];
let mut g_l = vec![0.0; m];
let mut g_u = vec![0.0; m];
if !tnlp.borrow_mut().get_bounds_info(BoundsInfo {
x_l: &mut x_l,
x_u: &mut x_u,
g_l: &mut g_l,
g_u: &mut g_u,
}) {
return None;
}
let mut max_violation: Number = 0.0;
let mut ok_tol = true;
let mut ok_acceptable = true;
let present = |b: Number, is_lower: bool| -> Option<Number> {
let absent = if is_lower {
b <= lower_bound_inf
} else {
b >= upper_bound_inf
};
(b.is_finite() && !absent).then_some(b)
};
let mut judge = |viol: Number, scale: Number| {
if viol <= 0.0 {
return;
}
max_violation = max_violation.max(viol);
let noise = noise_floor_kappa * Number::EPSILON * scale.abs();
let absolute_ok = |bound: Number| viol <= bound || viol <= noise;
ok_tol &= is_negligible(viol, scale, tol) && absolute_ok(constr_viol_tol);
ok_acceptable &=
is_negligible(viol, scale, acceptable_tol) && absolute_ok(acceptable_constr_viol_tol);
};
for i in 0..m {
let v = g[i];
if !v.is_finite() {
return None;
}
let lo = present(g_l[i], true);
let hi = present(g_u[i], false);
let scale = v
.abs()
.max(lo.map_or(0.0, Number::abs))
.max(hi.map_or(0.0, Number::abs));
judge(lo.map_or(0.0, |b| b - v), scale);
judge(hi.map_or(0.0, |b| v - b), scale);
}
for j in 0..n {
let v = x[j];
if !v.is_finite() {
return None;
}
let lo = present(x_l[j], true);
let hi = present(x_u[j], false);
let scale = v
.abs()
.max(lo.map_or(0.0, Number::abs))
.max(hi.map_or(0.0, Number::abs));
judge(lo.map_or(0.0, |b| b - v), scale);
judge(hi.map_or(0.0, |b| v - b), scale);
}
Some(OriginalSpaceFeasibility {
max_violation,
negligible_at_tol: ok_tol,
negligible_at_acceptable: ok_acceptable,
})
}
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::InvalidProblemDefinition => ApplicationReturnStatus::InvalidProblemDefinition,
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 dense_values(v: &dyn pounce_linalg::Vector) -> Vec<Number> {
v.as_any()
.downcast_ref::<pounce_linalg::dense_vector::DenseVector>()
.map(|d| d.expanded_values())
.unwrap_or_default()
}
fn set_dense(v: &mut dyn pounce_linalg::Vector, vals: &[Number]) -> bool {
match v
.as_any_mut()
.downcast_mut::<pounce_linalg::dense_vector::DenseVector>()
{
Some(d) if pounce_linalg::Vector::dim(d) as usize == vals.len() => {
d.set_values(vals);
true
}
_ => false,
}
}
#[derive(Debug, Clone)]
struct FinalizeSnapshot {
status: SolverReturn,
x: Vec<Number>,
z_l: Vec<Number>,
z_u: Vec<Number>,
g: Vec<Number>,
lambda: Vec<Number>,
obj_value: Number,
}
#[derive(Debug, Clone, Copy)]
struct SolutionCertificate {
objective: Number,
scaled_objective: Number,
dual_inf: Number,
constr_viol: Number,
compl: Number,
kkt_error: Number,
unscaled_dual_inf: Number,
unscaled_constr_viol: Number,
unscaled_compl: Number,
unscaled_kkt_error: Number,
kkt_error_above_noise: Number,
mu: Number,
}
impl SolutionCertificate {
fn of(s: &pounce_nlp::solve_statistics::SolveStatistics) -> Self {
Self {
objective: s.final_objective,
scaled_objective: s.final_scaled_objective,
dual_inf: s.final_dual_inf,
constr_viol: s.final_constr_viol,
compl: s.final_compl,
kkt_error: s.final_kkt_error,
unscaled_dual_inf: s.final_unscaled_dual_inf,
unscaled_constr_viol: s.final_unscaled_constr_viol,
unscaled_compl: s.final_unscaled_compl,
unscaled_kkt_error: s.final_unscaled_kkt_error,
kkt_error_above_noise: s.final_kkt_error_above_noise,
mu: s.final_mu,
}
}
fn restore_into(&self, s: &mut pounce_nlp::solve_statistics::SolveStatistics) {
s.final_objective = self.objective;
s.final_scaled_objective = self.scaled_objective;
s.final_dual_inf = self.dual_inf;
s.final_constr_viol = self.constr_viol;
s.final_compl = self.compl;
s.final_kkt_error = self.kkt_error;
s.final_unscaled_dual_inf = self.unscaled_dual_inf;
s.final_unscaled_constr_viol = self.unscaled_constr_viol;
s.final_unscaled_compl = self.unscaled_compl;
s.final_unscaled_kkt_error = self.unscaled_kkt_error;
s.final_kkt_error_above_noise = self.kkt_error_above_noise;
s.final_mu = self.mu;
}
}
impl FinalizeSnapshot {
fn replay(&self, tnlp: &Rc<RefCell<dyn TNLP>>) {
tnlp.borrow_mut().finalize_solution(
Solution {
status: self.status,
x: &self.x,
z_l: &self.z_l,
z_u: &self.z_u,
g: &self.g,
lambda: &self.lambda,
obj_value: self.obj_value,
},
&TnlpIpoptData::default(),
&TnlpIpoptCq::default(),
);
}
}
fn finalize_via_orig_nlp(
nlp: &Rc<RefCell<dyn IpoptNlp>>,
alg: &IpoptAlgorithm,
solver_status: SolverReturn,
_app_status: ApplicationReturnStatus,
tnlp: &Rc<RefCell<dyn TNLP>>,
sink: &RefCell<Option<FinalizeSnapshot>>,
) -> Result<Number, ()> {
let curr = alg.data.borrow().curr.clone().ok_or(())?;
let nlp_borrow = nlp.borrow();
let x_vec: Vec<Number> = nlp_borrow.finalize_solution_x(&*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.finalize_solution_z_l(&*curr.z_l);
if z_l.is_empty() {
z_l = vec![0.0; n];
}
let mut z_u = nlp_borrow.finalize_solution_z_u(&*curr.z_u);
if z_u.is_empty() {
z_u = vec![0.0; n];
}
let mut lambda = nlp_borrow.finalize_solution_lambda(&*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);
let snap = FinalizeSnapshot {
status: solver_status,
x: x_vec,
z_l,
z_u,
g: g_final,
lambda,
obj_value: f_final,
};
snap.replay(tnlp);
*sink.borrow_mut() = Some(snap);
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("hessian_approximation", "") {
if s == "limited-memory" {
opts.hessian = SqpHessianSource::DampedBfgs;
}
}
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 apply_qp_subproblem_options(options: &OptionsList, opts: &mut pounce_qp::QpOptions) {
match pounce_qp::ActiveSetOverrides::try_from_options_list(options) {
Ok(overrides) => overrides.apply(opts),
Err(error) => tracing::error!(
target: "pounce::options",
%error,
"sqp_qp_* options were rejected after the registry accepted them; \
the QP subproblem is running on pounce-qp defaults"
),
}
}
fn finalize_via_sqp(
nlp: &Rc<RefCell<dyn IpoptNlp>>,
res: &crate::sqp::SqpResult,
solver_status: pounce_nlp::SolverReturn,
tnlp: &Rc<RefCell<dyn TNLP>>,
sink: &RefCell<Option<FinalizeSnapshot>>,
) -> 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.finalize_solution_x(&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.finalize_solution_z_l(&z_l_compressed);
if z_l.is_empty() {
z_l = vec![0.0; n];
}
let mut z_u = nlp_borrow.finalize_solution_z_u(&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.finalize_solution_lambda(&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);
let snap = FinalizeSnapshot {
status: solver_status,
x: x_vec,
z_l,
z_u,
g: g_final,
lambda,
obj_value: f_final,
};
snap.replay(tnlp);
*sink.borrow_mut() = Some(snap);
Ok(f_final)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mu_strategy_fallback_default_defers_to_an_explicit_strategy() {
let app = IpoptApplication::new();
assert!(app.is_mu_strategy_fallback_enabled());
let mut app = IpoptApplication::new();
app.options_mut()
.set_string_value("mu_strategy", "monotone", true, false)
.unwrap();
assert!(!app.is_mu_strategy_fallback_enabled());
app.options_mut()
.set_string_value("mu_strategy_fallback", "yes", true, false)
.unwrap();
assert!(app.is_mu_strategy_fallback_enabled());
let mut app = IpoptApplication::new();
app.options_mut()
.set_string_value("mu_strategy_fallback", "no", true, false)
.unwrap();
assert!(!app.is_mu_strategy_fallback_enabled());
}
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, NlpInfo, ScalingRequest, 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());
}
#[test]
fn feral_min_par_flops_option_reaches_config() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
assert_eq!(
feral_config_from_options(app.options()).min_par_flops,
None,
"unset feral_min_par_flops should not force an override"
);
app.initialize_with_options_str("feral_min_par_flops 0\n")
.unwrap();
assert_eq!(
feral_config_from_options(app.options()).min_par_flops,
Some(0)
);
app.initialize_with_options_str("feral_min_par_flops 5e8\n")
.unwrap();
assert_eq!(
feral_config_from_options(app.options()).min_par_flops,
Some(500_000_000)
);
}
#[test]
fn feral_refine_steps_option_reaches_config() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
assert_eq!(
feral_config_from_options(app.options()).refine_max_steps,
pounce_feral::FeralConfig::default().refine_max_steps,
"unset feral_refine_steps must keep feral's own default"
);
app.initialize_with_options_str("feral_refine_steps 1\n")
.unwrap();
assert_eq!(feral_config_from_options(app.options()).refine_max_steps, 1);
app.initialize_with_options_str("feral_refine_steps 0\n")
.unwrap();
assert_eq!(feral_config_from_options(app.options()).refine_max_steps, 0);
}
#[test]
fn feral_static_pivoting_option_reaches_config() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
assert_eq!(
feral_config_from_options(app.options()).static_pivoting,
None,
"unset feral_static_pivoting must not force a numeric override"
);
app.initialize_with_options_str("feral_static_pivoting yes\n")
.unwrap();
assert_eq!(
feral_config_from_options(app.options()).static_pivoting,
Some(true)
);
app.initialize_with_options_str("feral_static_pivoting no\n")
.unwrap();
assert_eq!(
feral_config_from_options(app.options()).static_pivoting,
Some(false)
);
}
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));
}
}
struct DescribesItselfOnce {
refuse: std::rc::Rc<std::cell::Cell<bool>>,
inner: ExactQuadratic,
}
impl TNLP for DescribesItselfOnce {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
self.inner.get_nlp_info()
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
if self.refuse.get() {
return false;
}
self.inner.get_bounds_info(b)
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
self.inner.get_starting_point(sp)
}
fn eval_f(&mut self, x: &[Number], n: bool) -> Option<Number> {
self.inner.eval_f(x, n)
}
fn eval_grad_f(&mut self, x: &[Number], n: bool, g: &mut [Number]) -> bool {
self.inner.eval_grad_f(x, n, g)
}
fn eval_g(&mut self, x: &[Number], n: bool, g: &mut [Number]) -> bool {
self.inner.eval_g(x, n, g)
}
fn eval_jac_g(&mut self, x: Option<&[Number]>, n: bool, mode: SparsityRequest<'_>) -> bool {
self.inner.eval_jac_g(x, n, mode)
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
n: bool,
o: Number,
l: Option<&[Number]>,
nl: bool,
mode: SparsityRequest<'_>,
) -> bool {
self.inner.eval_h(x, n, o, l, nl, mode)
}
fn finalize_solution(&mut self, s: Solution<'_>, d: &IpoptData, c: &IpoptCq) {
self.inner.finalize_solution(s, d, c)
}
}
#[test]
fn row_scaling_active_is_cleared_when_a_later_solve_bails_early() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
let refuse = std::rc::Rc::new(std::cell::Cell::new(false));
let first = std::rc::Rc::new(std::cell::RefCell::new(DescribesItselfOnce {
refuse: std::rc::Rc::clone(&refuse),
inner: ExactQuadratic,
}));
let _ = app
.optimize_tnlp(std::rc::Rc::clone(&first) as std::rc::Rc<std::cell::RefCell<dyn TNLP>>);
assert!(
app.row_scaling_active.get().is_some(),
"the first solve did not record a row-scaling verdict, so this \
test cannot show the second one clearing it",
);
refuse.set(true);
let _ = app.optimize_tnlp(first as std::rc::Rc<std::cell::RefCell<dyn TNLP>>);
assert_eq!(
app.row_scaling_active.get(),
None,
"a solve that bailed before recording row scaling left the \
previous solve's verdict in place; the ℓ₁ outer loop would \
read it as fact and mirror an original-units violation into \
the scaled family (gh#794 review round 2)",
);
}
#[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_qp_subproblem_options_are_registered_and_propagate() {
use pounce_qp::AntiCyclingChoice;
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"algorithm active-set-sqp\n\
sqp_qp_max_iter 37\n\
sqp_qp_feas_tol 1e-7\n\
sqp_qp_opt_tol 2e-7\n\
sqp_qp_elastic_gamma 1e4\n\
sqp_qp_anti_cycling bland\n\
sqp_qp_use_schur_updates yes\n\
sqp_qp_max_schur_updates_before_refactor 12\n\
sqp_qp_use_homotopy yes\n\
sqp_qp_certify_second_order yes\n",
)
.expect("every sqp_qp_* option must be registered (gh #360)");
let qp = &app.algorithm_builder_snapshot().sqp_qp;
assert_eq!(qp.max_iter, 37);
assert!((qp.feas_tol - 1e-7).abs() < 1e-20);
assert!((qp.opt_tol - 2e-7).abs() < 1e-20);
assert!((qp.elastic_gamma - 1e4).abs() < 1e-9);
assert_eq!(qp.anti_cycling, AntiCyclingChoice::Bland);
assert!(qp.use_schur_updates);
assert_eq!(qp.max_schur_updates_before_refactor, 12);
assert!(qp.use_homotopy);
assert!(qp.certify_second_order);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("algorithm active-set-sqp\n")
.unwrap();
let defaults = pounce_qp::QpOptions::default();
let qp = &app.algorithm_builder_snapshot().sqp_qp;
assert_eq!(qp.max_iter, defaults.max_iter);
assert!((qp.feas_tol - defaults.feas_tol).abs() < 1e-20);
assert!((qp.opt_tol - defaults.opt_tol).abs() < 1e-20);
assert_eq!(qp.anti_cycling, defaults.anti_cycling);
assert!(!qp.use_schur_updates);
assert_eq!(
qp.max_schur_updates_before_refactor,
defaults.max_schur_updates_before_refactor
);
assert!(!qp.certify_second_order);
assert!(pounce_qp::QpOptions::default().certify_second_order);
}
#[test]
fn application_every_registered_sqp_qp_option_is_read_by_the_subproblem_reader() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
let mut registered: Vec<String> = app
.registered_options()
.registered_options_in_order()
.iter()
.map(|o| o.name.clone())
.filter(|n| n.starts_with("sqp_qp_"))
.collect();
registered.sort();
let mut read_by_the_reader = vec![
"sqp_qp_anti_cycling".to_string(),
"sqp_qp_certify_second_order".to_string(),
"sqp_qp_elastic_gamma".to_string(),
"sqp_qp_feas_tol".to_string(),
"sqp_qp_max_iter".to_string(),
"sqp_qp_max_schur_updates_before_refactor".to_string(),
"sqp_qp_opt_tol".to_string(),
"sqp_qp_use_homotopy".to_string(),
"sqp_qp_use_schur_updates".to_string(),
];
read_by_the_reader.sort();
assert_eq!(
registered, read_by_the_reader,
"registered sqp_qp_* options and the ones \
`apply_qp_subproblem_options` reads have diverged. A key that is \
registered but unread is a no-op knob with working documentation \
(that is how `sqp_qp_use_homotopy` shipped); a key read but not \
registered is gh #360. Wire it up in both places, assert it in \
`application_sqp_qp_subproblem_options_are_registered_and_propagate`, \
then add it here."
);
}
#[test]
fn application_sqp_hessian_approximation_maps_to_damped_bfgs() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"algorithm active-set-sqp\n\
hessian_approximation limited-memory\n",
)
.unwrap();
assert_eq!(
app.algorithm_builder_snapshot().sqp.hessian,
crate::sqp::SqpHessianSource::DampedBfgs
);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"algorithm active-set-sqp\n\
hessian_approximation limited-memory\n\
sqp_hessian lbfgs\n",
)
.unwrap();
assert_eq!(
app.algorithm_builder_snapshot().sqp.hessian,
crate::sqp::SqpHessianSource::Lbfgs
);
}
#[test]
fn application_linear_solver_records_the_effective_backend() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
assert_eq!(
app.algorithm_builder_from_options().linear_solver,
LinearSolverChoice::Feral,
"the registered default is `feral`, in an ma57 build too"
);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("linear_solver ma57\n")
.unwrap();
let got = app.algorithm_builder_from_options().linear_solver;
if cfg!(feature = "ma57") {
assert_eq!(got, LinearSolverChoice::Ma57);
} else {
assert_eq!(got, LinearSolverChoice::Feral);
}
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("linear_solver feral\n")
.unwrap();
assert_eq!(
app.algorithm_builder_from_options().linear_solver,
LinearSolverChoice::Feral
);
}
#[test]
fn limited_memory_defaults_mu_strategy_to_adaptive() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
assert_eq!(
app.algorithm_builder_from_options().mu_strategy,
MuStrategyChoice::Monotone,
"the exact arm must keep the registered default"
);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("hessian_approximation limited-memory\n")
.unwrap();
assert_eq!(
app.algorithm_builder_from_options().mu_strategy,
MuStrategyChoice::Adaptive,
"limited-memory must take upstream's quasi-Newton default"
);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"hessian_approximation limited-memory\n\
mu_strategy monotone\n",
)
.unwrap();
assert_eq!(
app.algorithm_builder_from_options().mu_strategy,
MuStrategyChoice::Monotone,
"an explicit mu_strategy must not be overridden"
);
}
#[test]
fn fallback_flip_follows_the_resolved_mu_strategy() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
assert!(
!app.effective_mu_strategy_is_adaptive(),
"unset + exact resolves to monotone"
);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("hessian_approximation limited-memory\n")
.unwrap();
assert!(
app.effective_mu_strategy_is_adaptive(),
"unset + limited-memory resolves to adaptive"
);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"hessian_approximation limited-memory\n\
mu_strategy monotone\n",
)
.unwrap();
assert!(
!app.effective_mu_strategy_is_adaptive(),
"an explicit monotone under limited-memory resolves to monotone"
);
}
#[test]
fn application_limited_memory_options_propagate_to_builder() {
use crate::hess::lim_mem_quasi_newton::UpdateType;
let mut app = IpoptApplication::new();
app.initialize().unwrap();
let def = app.algorithm_builder_from_options();
assert_eq!(def.limited_memory_update_type, UpdateType::Bfgs);
assert_eq!(def.limited_memory_max_history, 6);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"hessian_approximation limited-memory\n\
limited_memory_update_type sr1\n\
limited_memory_max_history 9\n",
)
.unwrap();
let snap = app.algorithm_builder_from_options();
assert_eq!(snap.limited_memory_update_type, UpdateType::Sr1);
assert_eq!(snap.limited_memory_max_history, 9);
}
#[test]
fn application_recalc_y_is_wired_and_defaults_off() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
let b = app.algorithm_builder_from_options();
assert!(!b.recalc_y, "exact-Hessian default must stay off");
assert_eq!(b.recalc_y_feas_tol, 1e-6, "default changed");
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("hessian_approximation limited-memory\n")
.unwrap();
assert!(
!app.algorithm_builder_from_options().recalc_y,
"limited-memory must not silently enable recalc_y"
);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str("recalc_y yes\nrecalc_y_feas_tol 1e-3\n")
.unwrap();
let b = app.algorithm_builder_from_options();
assert!(b.recalc_y);
assert_eq!(b.recalc_y_feas_tol, 1e-3);
}
#[test]
fn application_limited_memory_initialization_propagates_to_builder() {
use crate::hess::lim_mem_quasi_newton::InitialApprox;
for (kw, want) in [
("scalar1", InitialApprox::Scalar1),
("scalar2", InitialApprox::Scalar2),
("scalar3", InitialApprox::Scalar3),
("scalar4", InitialApprox::Scalar4),
("constant", InitialApprox::Constant),
("history-max", InitialApprox::HistoryMax),
] {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(&format!(
"hessian_approximation limited-memory\n\
limited_memory_initialization {kw}\n"
))
.unwrap();
assert_eq!(
app.algorithm_builder_from_options()
.limited_memory_initialization,
want,
"limited_memory_initialization={kw} did not reach the builder"
);
}
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"hessian_approximation limited-memory\n\
limited_memory_init_val 4.5\n",
)
.unwrap();
assert_eq!(
app.algorithm_builder_from_options().limited_memory_init_val,
4.5
);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
let def = app.algorithm_builder_from_options();
assert_eq!(def.limited_memory_initialization, InitialApprox::Scalar1);
assert_eq!(def.limited_memory_init_val, 1.0);
}
#[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);
}
#[test]
fn each_constant_derivative_hint_lights_up_its_own_slot() {
use pounce_nlp::constant_derivatives::HINT_OPTIONS;
let app = IpoptApplication::new();
assert_eq!(
app.asserted_constant_derivative_hints(),
[false; 4],
"no hint is asserted on a fresh options list",
);
for (k, name) in HINT_OPTIONS.iter().enumerate() {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(&format!("{name} yes\n"))
.unwrap();
let mut expected = [false; 4];
expected[k] = true;
assert_eq!(
app.asserted_constant_derivative_hints(),
expected,
"`{name}=yes` must set slot {k} and nothing else",
);
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(&format!("{name} no\n"))
.unwrap();
assert_eq!(
app.asserted_constant_derivative_hints(),
[false; 4],
"`{name}=no` is the registered default and asserts nothing",
);
}
}
struct ExactQuadratic;
impl TNLP for ExactQuadratic {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 1,
m: 0,
nnz_jac_g: 0,
nnz_h_lag: 0,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
b.x_l.copy_from_slice(&[-10.0]);
b.x_u.copy_from_slice(&[10.0]);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
sp.x.copy_from_slice(&[1.0]);
true
}
fn eval_f(&mut self, x: &[Number], _new_x: bool) -> Option<Number> {
Some(x[0] * x[0])
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
grad[0] = 2.0 * x[0];
true
}
fn eval_g(&mut self, _x: &[Number], _new_x: bool, _g: &mut [Number]) -> bool {
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_mode: SparsityRequest<'_>,
) -> bool {
true
}
fn eval_h(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_obj_factor: Number,
_lambda: Option<&[Number]>,
_new_lambda: bool,
_mode: SparsityRequest<'_>,
) -> bool {
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
struct RecordingQuadratic {
gradient_points: Rc<RefCell<Vec<Number>>>,
objective_points: Rc<RefCell<Vec<Number>>>,
x_scaling: Number,
}
impl TNLP for RecordingQuadratic {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
ExactQuadratic.get_nlp_info()
}
fn get_bounds_info(&mut self, bounds: BoundsInfo<'_>) -> bool {
ExactQuadratic.get_bounds_info(bounds)
}
fn get_starting_point(&mut self, start: StartingPoint<'_>) -> bool {
ExactQuadratic.get_starting_point(start)
}
fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
self.objective_points.borrow_mut().push(x[0]);
ExactQuadratic.eval_f(x, new_x)
}
fn eval_grad_f(&mut self, x: &[Number], _new_x: bool, grad: &mut [Number]) -> bool {
self.gradient_points.borrow_mut().push(x[0]);
grad[0] = 2.0 * x[0];
true
}
fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
ExactQuadratic.eval_g(x, new_x, g)
}
fn eval_jac_g(
&mut self,
x: Option<&[Number]>,
new_x: bool,
mode: SparsityRequest<'_>,
) -> bool {
ExactQuadratic.eval_jac_g(x, new_x, mode)
}
fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
*req.obj_scaling = 1.0;
*req.use_x_scaling = true;
req.x_scaling[0] = self.x_scaling;
*req.use_g_scaling = false;
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
fn derivative_test_verdict(extra: &str) -> pounce_nlp::derivative_test::DerivativeTestReport {
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(&format!("derivative_test first-order\n{extra}"))
.unwrap();
let opts = app.derivative_test_options();
pounce_nlp::derivative_test::run(&mut ExactQuadratic, &opts).expect("a report")
}
#[test]
fn the_derivative_checker_knobs_change_the_verdict() {
let clean = derivative_test_verdict("");
assert_eq!(clean.checked, 1);
assert_eq!(clean.suspicious, 0, "{:#?}", clean.lines);
let coarse = derivative_test_verdict("derivative_test_perturbation 0.5\n");
assert_eq!(coarse.checked, 1);
assert_eq!(
coarse.suspicious, 1,
"derivative_test_perturbation never reached the checker: {:#?}",
coarse.lines,
);
let tolerant =
derivative_test_verdict("derivative_test_perturbation 0.5\nderivative_test_tol 0.5\n");
assert_eq!(tolerant.checked, 1);
assert_eq!(
tolerant.suspicious, 0,
"derivative_test_tol never reached the checker: {:#?}",
tolerant.lines,
);
assert!(
tolerant.lines[0].contains("5.0e-1"),
"{:#?}",
tolerant.lines,
);
}
#[test]
fn ordinary_derivative_test_keeps_the_conditioned_start() {
let gradient_points = Rc::new(RefCell::new(Vec::new()));
let tnlp = Rc::new(RefCell::new(RecordingQuadratic {
gradient_points: Rc::clone(&gradient_points),
objective_points: Rc::new(RefCell::new(Vec::new())),
x_scaling: 1.0,
})) as Rc<RefCell<dyn TNLP>>;
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"derivative_test first-order\n\
start_point_perturbation 0.5\n\
hessian_approximation limited-memory\n\
max_iter 0\n\
print_level 0\n",
)
.unwrap();
let _ = app.optimize_tnlp(tnlp);
let first = gradient_points.borrow()[0];
assert!((first - 1.7666216164272852).abs() < 1e-12, "{first}");
}
#[test]
fn ordinary_derivative_test_keeps_variable_scaling() {
let objective_points = Rc::new(RefCell::new(Vec::new()));
let tnlp = Rc::new(RefCell::new(RecordingQuadratic {
gradient_points: Rc::new(RefCell::new(Vec::new())),
objective_points: Rc::clone(&objective_points),
x_scaling: 0.5,
})) as Rc<RefCell<dyn TNLP>>;
let mut app = IpoptApplication::new();
app.initialize().unwrap();
app.initialize_with_options_str(
"derivative_test first-order\n\
derivative_test_perturbation 0.5\n\
nlp_scaling_method user-scaling\n\
hessian_approximation limited-memory\n\
max_iter 0\n\
print_level 0\n",
)
.unwrap();
let _ = app.optimize_tnlp(tnlp);
assert_eq!(&objective_points.borrow()[..2], &[1.0, 2.0]);
}
fn ratio_only(dual_inf: Number, viol: Number, compl: Number) -> bool {
runaway_is_the_whole_residual(dual_inf, viol, compl, 0.0)
}
fn runaway(dual_inf: Number, viol: Number, compl: Number) -> bool {
runaway_is_the_whole_residual(dual_inf, viol, compl, DUAL_DIV_RETRY_DU_FLOOR)
}
#[test]
fn a_converged_point_with_a_runaway_multiplier_opens_the_retry() {
assert!(runaway(7.90e4, 1.1e-16, 1.1e-9));
assert!(runaway(3.25e11, 2.5e-16, 2.8e-3));
assert!(runaway(5.5743e3, 5.6e-14, 2.08e-5));
}
#[test]
fn an_unconverged_point_does_not_open_the_retry() {
assert!(!ratio_only(9.90e1, 8.0e-13, 4.65e0));
assert!(!ratio_only(1.0e6, 1.0e1, 1.0e-16));
assert!(!ratio_only(1.0e6, 1.0e-16, 1.0e1));
}
#[test]
fn a_small_dual_residual_is_not_a_runaway_however_dominant() {
assert!(ratio_only(4.4e-1, 1.0e-30, 1.0e-30));
assert!(!runaway(4.4e-1, 1.0e-30, 1.0e-30));
assert!(!runaway(4.397e-1, 1.0e-16, 1.0e-16));
assert!(!runaway(2.026e1, 1.0e-16, 1.0e-16));
assert!(runaway(DUAL_DIV_RETRY_DU_FLOOR, 0.0, 0.0));
assert!(!runaway(DUAL_DIV_RETRY_DU_FLOOR * 0.999, 0.0, 0.0));
}
#[test]
fn the_threshold_is_where_the_constant_says_it_is() {
assert!(ratio_only(1.0, DUAL_DIV_RETRY_DOMINANCE, 0.0));
assert!(!ratio_only(1.0, DUAL_DIV_RETRY_DOMINANCE * 1.001, 0.0));
}
#[test]
fn what_we_cannot_measure_does_not_buy_a_retry() {
assert!(!ratio_only(Number::NAN, 0.0, 0.0));
assert!(!ratio_only(1.0e6, Number::NAN, 0.0));
assert!(!ratio_only(1.0e6, 0.0, Number::NAN));
assert!(!ratio_only(Number::INFINITY, 0.0, 0.0));
assert!(!ratio_only(0.0, 0.0, 0.0));
assert!(!ratio_only(-1.0, 0.0, 0.0));
}
const ACCEPT: Number = 1e-6;
const MIN: Number = 1.0;
const MAX: Number = -1.0;
fn admissible(bo: Number, bv: Number, ro: Number, rv: Number) -> bool {
retry_answer_is_admissible(bo, bv, ro, rv, ACCEPT, MIN)
}
#[test]
fn the_reproducers_promotion_is_still_admissible() {
assert!(admissible(3.586e-28, 1.11e-16, 5.835e-11, 5.47e-12));
}
#[test]
fn a_worse_feasible_objective_is_refused_however_clean_the_certificate() {
assert!(!admissible(
-1.3005680756e1,
2.22e-16,
-1.2072337962e0,
4.55e-13
));
assert!(!admissible(
-4.7919265770e0,
1.07e-14,
-9.8562977711e-1,
7.99e-14
));
assert!(!admissible(
-2.9558632401e-1,
6.25e-17,
-9.7321185691e-2,
6.21e-13
));
}
#[test]
fn an_improvement_bought_with_primal_slack_is_refused() {
assert!(!admissible(
1.8175997416e-9,
2.07e-25,
-6.6088333055e-5,
1.09e-9
));
assert!(admissible(
1.8175997416e-9,
2.07e-25,
-6.6088333055e-5,
2.07e-25
));
}
#[test]
fn the_objective_tolerance_is_not_fitted_to_one_model() {
let admit = 5.835e-11;
let refuse = 0.198;
assert!(admit < ACCEPT / 1.0e4, "{admit} is not well inside");
assert!(refuse > ACCEPT * 1.0e4, "{refuse} is not well outside");
assert!(admissible(1.0e6, 0.0, 1.0e6 + 0.5, 0.0));
assert!(!admissible(1.0e6, 0.0, 1.0e6 + 5.0, 0.0));
}
#[test]
fn the_rules_follow_the_objective_sense() {
assert!(retry_answer_is_admissible(
-3.586e-28, 1.11e-16, -5.835e-11, 5.47e-12, ACCEPT, MAX
));
assert!(!retry_answer_is_admissible(
1.3005680756e1,
2.22e-16,
1.2072337962e0,
4.55e-13,
ACCEPT,
MAX
));
assert!(!retry_answer_is_admissible(
-1.8175997416e-9,
2.07e-25,
6.6088333055e-5,
1.09e-9,
ACCEPT,
MAX
));
assert!(retry_answer_is_admissible(
-1.8175997416e-9,
2.07e-25,
6.6088333055e-5,
2.07e-25,
ACCEPT,
MAX
));
}
#[test]
fn negating_the_objective_and_the_sense_is_inert() {
for &(bo, bv, ro, rv) in &[
(3.586e-28, 1.11e-16, 5.835e-11, 5.47e-12),
(-1.3005680756e1, 2.22e-16, -1.2072337962e0, 4.55e-13),
(1.8175997416e-9, 2.07e-25, -6.6088333055e-5, 1.09e-9),
(1.8175997416e-9, 2.07e-25, -6.6088333055e-5, 2.07e-25),
(-1.0e3, 1.0e-2, 0.0, 1.0e-12),
] {
assert_eq!(
retry_answer_is_admissible(bo, bv, ro, rv, ACCEPT, MIN),
retry_answer_is_admissible(-bo, bv, -ro, rv, ACCEPT, MAX),
"verdict moved under (obj, sense) -> (-obj, -sense) at {bo:e}/{ro:e}"
);
}
}
#[test]
fn an_infeasible_base_attempt_protects_nothing() {
assert!(admissible(-1.0e3, 1.0e-2, 0.0, 1.0e-12));
assert!(admissible(Number::NAN, 0.0, 0.0, 0.0));
assert!(!admissible(0.0, 0.0, Number::NAN, 0.0));
assert!(!admissible(0.0, 0.0, -1.0, Number::NAN));
}
}