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
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ScaleAudit {
Rescale,
Upscale(Number),
SmallObjective,
}
const SCALE_AUDIT_COMPL_REL: Number = 1e-7;
const SCALE_AUDIT_MAX_TOL: Number = 1e-6;
const SCALE_AUDIT_MAX_UPSCALE: Number = 1e10;
const SCALE_AUDIT_MATERIAL: Number = 10.0;
const SCALE_AUDIT_RETRY_ITER_FACTOR: Index = 3;
const SCALE_AUDIT_RETRY_MIN_ITER: Index = 50;
const SCALE_AUDIT_BARRIER_GRAD: Number = 10.0;
#[derive(Debug, Clone, Copy)]
struct ScaleAuditInput {
tol: Number,
obj_scale: Number,
rescaled_obj_scale: Number,
start_grad_max: Number,
final_grad_max: Number,
unscaled_kkt: Number,
unscaled_compl: Number,
acceptable_tol: Number,
masked_threshold: Number,
warm_start: bool,
}
impl ScaleAuditInput {
fn grad_scale(&self) -> Number {
let fin = |v: Number| if v.is_finite() && v > 0.0 { v } else { 0.0 };
if self.start_grad_max == 0.0 {
return 0.0;
}
if self.warm_start && self.unscaled_compl.is_finite() {
let floor = SCALE_AUDIT_BARRIER_GRAD * self.unscaled_compl;
let answer = |v: Number| if fin(v) > floor { v } else { 0.0 };
return answer(self.start_grad_max).max(answer(self.final_grad_max));
}
fin(self.start_grad_max).max(fin(self.final_grad_max))
}
}
fn scale_audit_trigger(inp: ScaleAuditInput) -> Option<ScaleAudit> {
use crate::conv_check::opt_error::certificate_masked;
if inp.obj_scale.is_finite()
&& inp.obj_scale > 0.0
&& inp.unscaled_kkt.is_finite()
&& certificate_masked(
inp.obj_scale,
inp.unscaled_kkt,
inp.masked_threshold,
inp.acceptable_tol,
)
&& inp.rescaled_obj_scale.is_finite()
&& inp.rescaled_obj_scale >= SCALE_AUDIT_MATERIAL * inp.obj_scale
{
return Some(ScaleAudit::Rescale);
}
let g = inp.grad_scale();
if inp.tol <= SCALE_AUDIT_MAX_TOL
&& g > 0.0
&& g < 1.0
&& inp.unscaled_compl.is_finite()
&& inp.unscaled_compl / g > SCALE_AUDIT_COMPL_REL
{
let k = (1.0 / g).min(SCALE_AUDIT_MAX_UPSCALE);
return Some(if k >= SCALE_AUDIT_MATERIAL {
ScaleAudit::Upscale(k)
} else {
ScaleAudit::SmallObjective
});
}
None
}
fn scale_audit_warning(
kind: ScaleAudit,
base: &SolveStatistics,
grad_scale: Number,
acceptable_tol: Number,
outcome: &str,
) -> String {
match kind {
ScaleAudit::Rescale => format!(
"unscaled_stationarity_above_tol: gradient-based scaling froze the objective \
factor at {:.2e} at the starting point and the reported point's unscaled KKT \
error is {:.2e} (acceptable_tol {:.0e}); the scaled test passed in a space \
where the objective gradient is shrunk by that factor. A re-solve from this \
point with the scaling re-evaluated {outcome}.",
base.final_obj_scaling_factor, base.final_unscaled_kkt_error, acceptable_tol
),
ScaleAudit::Upscale(k) => format!(
"objective_scale_small: the objective gradient is at most {:.2e} (start and \
returned point) and nothing scales it up, so the unscaled complementarity {:.2e} \
is {:.1e} of that scale -- the objective is only good to about that relative \
error. A re-solve with the objective multiplied by {:.1e} {outcome}. Rescale the \
objective (e.g. obj_scaling_factor) to silence this.",
grad_scale,
base.final_unscaled_compl,
base.final_unscaled_compl / grad_scale,
k
),
ScaleAudit::SmallObjective => format!(
"objective_scale_small: the objective gradient is at most {:.2e} (start and \
returned point), so the unscaled complementarity {:.2e} is {:.1e} of that scale \
-- the objective is only good to about that relative error. No re-solve: \
scaling the objective up by {:.1e} is below the factor of {SCALE_AUDIT_MATERIAL} \
that would change the solve. Rescale the objective (e.g. obj_scaling_factor) or \
tighten tol to silence this.",
grad_scale,
base.final_unscaled_compl,
base.final_unscaled_compl / grad_scale,
1.0 / grad_scale
),
}
}
fn quality_warnings(
status: ApplicationReturnStatus,
unscaled_dual_inf: Number,
dual_scale: Number,
dual_inf_tol: Number,
acceptable_tol: Number,
bound_relax_off: bool,
) -> Vec<String> {
let mut out = Vec::new();
if !matches!(
status,
ApplicationReturnStatus::SolveSucceeded | ApplicationReturnStatus::SolvedToAcceptableLevel
) {
return out;
}
if dual_scale.is_finite()
&& dual_scale >= 1e8
&& unscaled_dual_inf.is_finite()
&& unscaled_dual_inf > acceptable_tol
{
out.push(format!(
"large_dual_scale: the stationarity residual is assembled from terms of size {dual_scale:.2e} \
(multipliers or gradients this large usually mean a failed constraint qualification), \
so the unscaled dual infeasibility {unscaled_dual_inf:.2e} was judged relative to that scale{}",
if bound_relax_off {
". bound_relax_factor = 0 is set: an active bound that coincides with a \
dependent row has no interior there, and the default relaxation (1e-8) \
usually removes this"
} else {
""
}
));
}
if unscaled_dual_inf.is_finite()
&& unscaled_dual_inf > UNSCALED_DUAL_WARN
&& unscaled_dual_inf > acceptable_tol
{
out.push(format!(
"unscaled_dual_inf_above_acceptable: the unscaled dual infeasibility is {unscaled_dual_inf:.2e}, \
above acceptable_tol {acceptable_tol:.0e}; the strict gate tolerates up to dual_inf_tol \
= {dual_inf_tol:.0e}. Verify stationarity in your own units"
));
}
out
}
const UNSCALED_DUAL_WARN: Number = 1e-3;
const UNSCALED_DUAL_ROUNDOFF_ULPS: Number = 64.0;
fn unscaled_dual_inf_is_roundoff(du: Number, scale: Number) -> bool {
du.is_finite()
&& scale.is_finite()
&& scale > 0.0
&& du <= UNSCALED_DUAL_ROUNDOFF_ULPS * Number::EPSILON * scale
}
fn warning_code(w: &str) -> &str {
w.split(':').next().unwrap_or(w)
}
fn push_warning_once(warnings: &mut Vec<String>, w: String) {
if !warnings.iter().any(|x| warning_code(x) == warning_code(&w)) {
warnings.push(w);
}
}
const RETRY_VIOL_NOISE_FLOOR: Number = 1e-12;
const RETRY_DOMINANCE_ORDERS: Number = 1e-6;
fn retry_certificate_dominates(
base_unscaled_kkt: Number,
retry_unscaled_kkt: Number,
retry_viol: Number,
accept_tol: Number,
) -> bool {
base_unscaled_kkt.is_finite()
&& retry_unscaled_kkt.is_finite()
&& retry_viol.is_finite()
&& retry_unscaled_kkt <= RETRY_DOMINANCE_ORDERS * base_unscaled_kkt
&& retry_unscaled_kkt <= UNSCALED_DUAL_WARN
&& retry_viol <= accept_tol
}
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.max(RETRY_VIOL_NOISE_FLOOR);
}
true
}
fn declared_box_relative_violation(
x: &[Number],
x_l: &[Number],
x_u: &[Number],
lower_inf: Number,
upper_inf: Number,
) -> Number {
if x.len() != x_l.len() || x.len() != x_u.len() {
return Number::NAN;
}
let mut worst = 0.0_f64;
for i in 0..x.len() {
let (l, u) = (x_l[i], x_u[i]);
if l == u {
continue;
}
if l > lower_inf && x[i] < l {
worst = worst.max((l - x[i]) / l.abs().max(1.0));
}
if u < upper_inf && x[i] > u {
worst = worst.max((x[i] - u) / u.abs().max(1.0));
}
}
worst
}
const RETRY_BOX_REL_NOISE: Number = 4.0 * Number::EPSILON;
fn retry_keeps_declared_box(base_rel_viol: Number, retry_rel_viol: Number) -> bool {
if retry_rel_viol.is_nan() {
return base_rel_viol.is_nan();
}
let base = if base_rel_viol.is_nan() {
0.0
} else {
base_rel_viol.max(0.0)
};
retry_rel_viol <= base + RETRY_BOX_REL_NOISE
}
#[derive(Debug, Default, Clone)]
pub struct OptionFileLoad {
pub path: Option<PathBuf>,
pub explicit: bool,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct LinearSolverSummaryState {
main: LinearSolverSummary,
restoration: LinearSolverSummary,
}
pub type RestorationFactory = Box<dyn FnMut() -> Box<dyn RestorationPhase>>;
pub type RestorationFactoryProvider = Box<dyn FnMut() -> RestorationFactory>;
#[derive(Default)]
struct MultiPassTotals {
iterations: Index,
rows: Vec<pounce_nlp::solve_statistics::IterRecord>,
obj_evals: Index,
constr_evals: Index,
grad_evals: Index,
jac_evals: Index,
hess_evals: Index,
restoration_calls: Index,
restoration_inner_iters: Index,
restoration_outer_iters: Index,
passes: Vec<pounce_nlp::SolvePassRecord>,
}
impl MultiPassTotals {
fn push_pass(
&mut self,
st: &SolveStatistics,
rho: Option<Number>,
status: ApplicationReturnStatus,
) {
self.passes.push(pounce_nlp::SolvePassRecord {
rho,
iterations: st.iteration_count,
first_row: self.rows.len(),
slack_sum: None,
constraint_violation: None,
status: format!("{status:?}"),
});
self.iterations += st.iteration_count;
self.rows.extend(st.iterations.iter().cloned());
self.obj_evals += st.num_obj_evals;
self.constr_evals += st.num_constr_evals;
self.grad_evals += st.num_obj_grad_evals;
self.jac_evals += st.num_constr_jac_evals;
self.hess_evals += st.num_hess_evals;
self.restoration_calls += st.restoration_calls;
self.restoration_inner_iters += st.restoration_inner_iters;
self.restoration_outer_iters += st.restoration_outer_iters;
}
fn set_slack_sum(&mut self, v: Number) {
if let Some(p) = self.passes.last_mut() {
p.slack_sum = v.is_finite().then_some(v);
}
}
fn set_violation(&mut self, v: Number) {
if let Some(p) = self.passes.last_mut() {
p.constraint_violation = v.is_finite().then_some(v);
}
}
fn apply(&self, st: &mut SolveStatistics) {
if self.passes.is_empty() {
return;
}
st.iteration_count = self.iterations;
st.iterations = self.rows.clone();
st.num_obj_evals = self.obj_evals;
st.num_constr_evals = self.constr_evals;
st.num_obj_grad_evals = self.grad_evals;
st.num_constr_jac_evals = self.jac_evals;
st.num_hess_evals = self.hess_evals;
st.restoration_calls = self.restoration_calls;
st.restoration_inner_iters = self.restoration_inner_iters;
st.restoration_outer_iters = self.restoration_outer_iters;
st.passes = self.passes.clone();
}
}
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>>,
resto_linsol_summary_sink: Arc<Mutex<LinearSolverSummary>>,
kkt_blocks_published: crate::alg_builder::KktBlocksCell,
quality_escalations: Rc<std::cell::Cell<u64>>,
mu_strategy_pounce_chosen: bool,
dual_divergence_signature: std::cell::Cell<bool>,
derivative_check: RefCell<Option<pounce_nlp::derivative_test::DerivativeCheckSummary>>,
obj_scale_refused: std::cell::Cell<(bool, bool)>,
dual_divergence_retry_promoted: std::cell::Cell<bool>,
answer_restored_from_floor: std::cell::Cell<bool>,
retry_keeps_declared_box: 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>,
x_elimination: Option<(Vec<Index>, usize)>,
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>>,
kkt_blocks: Option<Vec<i32>>,
model_blocks: Option<(Vec<i32>, Vec<i32>)>,
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())),
resto_linsol_summary_sink: Arc::new(Mutex::new(LinearSolverSummary::default())),
kkt_blocks_published: Arc::new(Mutex::new(None)),
quality_escalations: Rc::new(std::cell::Cell::new(0)),
mu_strategy_pounce_chosen: false,
dual_divergence_signature: std::cell::Cell::new(false),
derivative_check: RefCell::new(None),
obj_scale_refused: std::cell::Cell::new((false, false)),
dual_divergence_retry_promoted: std::cell::Cell::new(false),
answer_restored_from_floor: std::cell::Cell::new(false),
retry_keeps_declared_box: 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,
x_elimination: None,
crossover_report: None,
warm_start_iterate: None,
warm_start_diag: RefCell::new(None),
external_ordering: None,
kkt_schur_block: None,
kkt_blocks: None,
model_blocks: 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 reports_fixed_var_duals(&self) -> bool {
!matches!(
self.options
.get_string_value("fixed_variable_treatment", "")
.ok()
.map(|(v, _)| v)
.as_deref(),
Some("make_parameter_nodual")
)
}
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 take_on_converged(&mut self) -> Option<ConvergedCallback> {
self.on_converged.take()
}
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 iter_history_enabled(&self) -> bool {
self.record_iter_history
}
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 mut main = self.linsol_summary_sink.lock().ok()?.clone();
let resto = self
.resto_linsol_summary_sink
.lock()
.ok()
.map(|g| g.clone())
.filter(|r| !r.is_empty());
if main.is_empty() && resto.is_none() {
return None;
}
main.restoration = resto.map(Box::new);
main.requested = self
.options
.get_string_value("linear_solver", "")
.ok()
.map(|(v, _)| v);
Some(main)
}
pub fn linear_solver_summary_state(&self) -> LinearSolverSummaryState {
let read =
|s: &Arc<Mutex<LinearSolverSummary>>| s.lock().map(|g| g.clone()).unwrap_or_default();
LinearSolverSummaryState {
main: read(&self.linsol_summary_sink),
restoration: read(&self.resto_linsol_summary_sink),
}
}
pub fn restore_linear_solver_summary_state(&self, state: LinearSolverSummaryState) {
if let Ok(mut g) = self.linsol_summary_sink.lock() {
*g = state.main;
}
if let Ok(mut g) = self.resto_linsol_summary_sink.lock() {
*g = state.restoration;
}
}
pub fn restoration_summary_sink(&self) -> Arc<Mutex<LinearSolverSummary>> {
Arc::clone(&self.resto_linsol_summary_sink)
}
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.statistics.borrow_mut().warnings.clear();
self.statistics.borrow_mut().sqp_warm_working_set_applied = false;
self.retry_keeps_declared_box.set(false);
let status = self.optimize_tnlp_dispatch(tnlp, derivative_test_tnlp);
let console_output = match self.options.get_integer_value("print_level", "") {
Ok((v, true)) => v >= 1,
_ => true,
};
if console_output {
for w in &self.statistics.borrow().warnings {
println!("WARNING: {w}");
}
}
status
}
fn optimize_tnlp_dispatch(
&mut self,
tnlp: Rc<RefCell<dyn TNLP>>,
derivative_test_tnlp: Option<Rc<RefCell<dyn TNLP>>>,
) -> ApplicationReturnStatus {
self.dual_divergence_signature.set(false);
self.obj_scale_refused.set((false, 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);
}
let status = if self.is_dual_divergence_retry_enabled() {
self.run_with_dual_divergence_retry(tnlp)
} else {
self.dispatch_standard_solve(tnlp)
};
self.finalize_quality_verdict(status)
}
fn dispatch_standard_solve(&mut self, tnlp: Rc<RefCell<dyn TNLP>>) -> ApplicationReturnStatus {
let status = self.dispatch_standard_solve_inner(Rc::clone(&tnlp));
self.audit_solve_quality(tnlp, status)
}
fn dispatch_standard_solve_inner(
&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() || self.mu_strategy_pounce_chosen,
}
}
pub fn mark_mu_strategy_pounce_chosen(&mut self) {
self.mu_strategy_pounce_chosen = true;
}
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_block_structure(&mut self, var_blocks: Vec<i32>, con_blocks: Vec<i32>) {
self.model_blocks = Some((var_blocks, con_blocks));
}
pub fn clear_block_structure(&mut self) {
self.model_blocks = None;
}
pub fn block_structure(&self) -> Option<(&[i32], &[i32])> {
self.model_blocks
.as_ref()
.map(|(v, c)| (v.as_slice(), c.as_slice()))
}
pub fn set_kkt_block_structure(&mut self, labels: Vec<i32>) {
self.kkt_blocks = Some(labels);
}
pub fn clear_kkt_block_structure(&mut self) {
self.kkt_blocks = None;
}
pub fn kkt_block_structure(&self) -> Option<&[i32]> {
self.kkt_blocks.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 working_set_to_user(
&self,
nlp: &dyn IpoptNlp,
ws: pounce_qp::WorkingSet,
) -> pounce_qp::WorkingSet {
let bounds = match &self.x_elimination {
Some((map, n_full)) if ws.bounds.len() == map.len() && map.len() != *n_full => {
let mut bounds = vec![pounce_qp::BoundStatus::Fixed; *n_full];
for (k, &j) in map.iter().enumerate() {
bounds[j as usize] = ws.bounds[k];
}
bounds
}
_ => ws.bounds,
};
pounce_qp::WorkingSet {
bounds,
constraints: working_set_rows_to_user(nlp, ws.constraints),
}
}
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); the `qp_*` options themselves are \
read only there and by `pounce.solve_qp`. From Python, \
`pounce.solve_qp` drives the engine directly and takes `qp_presolve`, \
`qp_reg`, `qp_hsde`, `qp_equilibrate` and `qp_crossover` as keyword \
arguments (presolve is off there unless asked for). \
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 derivative_check(&self) -> Option<pounce_nlp::derivative_test::DerivativeCheckSummary> {
self.derivative_check.borrow().clone()
}
pub fn run_derivative_test(&self, tnlp: &Rc<RefCell<dyn TNLP>>) {
*self.derivative_check.borrow_mut() = None;
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;
};
*self.derivative_check.borrow_mut() = Some(report.summary.clone());
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 mut warm = self.sqp_warm_start.take();
let (x_map, n_full) = {
let a = adapter.borrow();
let cls = a.classification();
(cls.x_not_fixed_map.clone(), cls.n_full_x as usize)
};
let reduced = x_map.len() != n_full;
let prev_elimination = self.x_elimination.replace((x_map.clone(), n_full));
if let Some(w) = warm.as_mut() {
sqp_warm_start_to_algorithm(&*nlp_rc.borrow(), w);
if reduced {
let mut drop_reason: Option<String> = None;
if let Some(ws) = w.working.as_mut() {
if ws.bounds.len() == n_full {
ws.bounds = x_map.iter().map(|&j| ws.bounds[j as usize]).collect();
} else if ws.bounds.len() == x_map.len()
&& prev_elimination.as_ref() != Some(&(x_map.clone(), n_full))
{
drop_reason = Some(
"its bound statuses are in the reduced space of a solve that \
fixed different variables"
.into(),
);
}
}
if let Some(why) = drop_reason {
tracing::warn!(
target: "pounce::sqp",
"SQP warm-start working set dropped: {why}; pass the full-space \
working set (last_sqp_working_set / info[\"working_set\"]) instead"
);
w.working = None;
}
}
}
if warm.is_none()
&& matches!(
self.options.get_bool_value("warm_start_init_point", ""),
Ok((true, _))
)
{
warm = sqp_tnlp_warm_start(&nlp_rc);
}
if let Some(w) = warm.as_ref() {
use crate::sqp::SqpProblemSpec;
let (n, m) = (sqp_adapter.n(), sqp_adapter.m());
let fits = w.x.len() == n
&& w.lambda_x.len() == n
&& w.lambda_g.len() == m
&& w.working
.as_ref()
.map(|ws| ws.validate_dims(n, m).is_ok())
.unwrap_or(true);
if !fits {
tracing::warn!(
target: "pounce::sqp",
"SQP warm start dropped: it is sized for a problem with n = {}, m = {} \
but this one has n = {n}, m = {m} (a fixed variable or removed row \
changes the reduced dimensions); solving cold",
w.x.len(),
w.lambda_g.len(),
);
warm = None;
}
}
let warm_working_set_applied = warm.as_ref().is_some_and(|w| w.working.is_some());
{
let hook_nlp = Rc::clone(&nlp_rc);
let hook_tnlp = Rc::clone(&tnlp);
let fixed_var_duals = self.reports_fixed_var_duals();
alg.set_iteration_hook(Box::new(move |info| {
let _guard = hook_tnlp.borrow().wants_current_iterate().then(|| {
crate::intermediate::UserIterateGuard::install(sqp_user_iterate(
&hook_nlp,
info.x,
info.lambda_g,
info.lambda_x,
info.c_vals,
info.grad_lag_x,
fixed_var_duals,
))
});
let stats = pounce_nlp::tnlp::IterStats {
mode: pounce_nlp::return_codes::AlgorithmMode::RegularMode,
iter: info.iter as Index,
obj_value: info.obj,
inf_pr: info.inf_pr,
inf_du: info.inf_du,
mu: 0.0,
d_norm: info.p_inf,
regularization_size: 0.0,
alpha_du: 0.0,
alpha_pr: info.alpha,
ls_trials: 0,
};
hook_tnlp.borrow_mut().intermediate_callback(
stats,
&pounce_nlp::tnlp::IpoptData::default(),
&pounce_nlp::tnlp::IpoptCq::default(),
)
}));
}
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()
.map(|ws| self.working_set_to_user(&*nlp_rc.borrow(), ws));
{
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.sqp_warm_working_set_applied = warm_working_set_applied;
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.derivative_check = self.derivative_check.borrow().clone();
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,
),
crate::sqp::SqpStatus::UserRequestedStop => (
ApplicationReturnStatus::UserRequestedStop,
pounce_nlp::SolverReturn::UserRequestedStop,
),
};
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,
self.reports_fixed_var_duals(),
);
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,
};
let widening_applied = self
.options
.get_numeric_value("bound_relax_factor", "")
.map(|(v, _)| v > 0.0)
.unwrap_or(true);
pounce_solve_report::console::print_summary_with_widening(
app_status,
&stats,
&counts,
widening_applied,
);
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()
.map(|ws| self.working_set_to_user(&*nlp_handle.borrow(), ws));
}
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 first_attempt = self.statistics.borrow().clone();
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,
);
{
let mut st = self.statistics.borrow_mut();
if !st.passes.is_empty() {
let mut head = MultiPassTotals::default();
head.push_pass(&first_attempt, None, first_status);
let shift = head.rows.len();
let mut passes = head.passes;
passes.extend(st.passes.iter().cloned().map(|mut p| {
p.first_row += shift;
p
}));
let mut rows = head.rows;
rows.append(&mut st.iterations);
st.iterations = rows;
st.iteration_count += first_attempt.iteration_count;
st.num_obj_evals += first_attempt.num_obj_evals;
st.num_constr_evals += first_attempt.num_constr_evals;
st.num_obj_grad_evals += first_attempt.num_obj_grad_evals;
st.num_constr_jac_evals += first_attempt.num_constr_jac_evals;
st.num_hess_evals += first_attempt.num_hess_evals;
st.restoration_calls += first_attempt.restoration_calls;
st.restoration_inner_iters += first_attempt.restoration_inner_iters;
st.restoration_outer_iters += first_attempt.restoration_outer_iters;
st.passes = passes;
}
}
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 mut scope = RetryOptionScope::enter(self, &["mu_strategy"], true);
let _ = scope
.options
.set_string_value("mu_strategy", flipped, true, false);
let solution_floor = scope.last_finalize.borrow().clone();
let certificate_floor = SolutionCertificate::of(&scope.statistics.borrow());
let trace_floor = *scope.last_iter_stats.borrow();
let retry_status = scope.optimize_constrained(Rc::clone(&tnlp));
let _ = scope.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,
);
drop(scope);
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 mut scope = RetryOptionScope::enter(self, &["perturb_always_cd"], true);
let _ = scope
.options
.set_string_value("perturb_always_cd", "yes", true, false);
let retry_status = scope.dispatch_standard_solve(Rc::clone(&tnlp));
let _ = scope.options.set_string_value(
"perturb_always_cd",
prev.as_ref()
.filter(|(_, found)| *found)
.map(|(v, _)| v.as_str())
.unwrap_or("no"),
true,
false,
);
drop(scope);
let retry_unscaled_kkt = self.statistics.borrow().final_unscaled_kkt_error;
let retry_viol = self.statistics.borrow().final_unscaled_constr_viol;
let accept_tol = self.dual_divergence_retry_accept_tol();
let claimed_success_is_real = (retry_unscaled_kkt <= accept_tol
&& retry_viol <= accept_tol)
|| retry_certificate_dominates(
base_unscaled_kkt,
retry_unscaled_kkt,
retry_viol,
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 is_solve_quality_audit_enabled(&self) -> bool {
self.options
.get_bool_value("solve_quality_audit", "")
.map(|(v, _found)| v)
.unwrap_or(true)
}
fn audit_solve_quality(
&mut self,
tnlp: Rc<RefCell<dyn TNLP>>,
first_status: ApplicationReturnStatus,
) -> ApplicationReturnStatus {
if !self.is_solve_quality_audit_enabled()
|| !matches!(
first_status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
)
{
return first_status;
}
let num = |name: &str, default: Number| -> Number {
self.options
.get_numeric_value(name, "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(default)
};
let acceptable_tol = num("acceptable_tol", 1e-6);
let masked_threshold = num("obj_scale_certificate_threshold", 1e-4);
let gradient_based = self
.options
.get_string_value("nlp_scaling_method", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.map(|v| v == "gradient-based" || v == "equilibration-based")
.unwrap_or(true);
let base = self.statistics.borrow().clone();
let final_grad_max = self
.last_finalize
.borrow()
.as_ref()
.and_then(|f| {
let mut g = vec![0.0; f.x.len()];
tnlp.borrow_mut()
.eval_grad_f(&f.x, true, &mut g)
.then(|| g.iter().fold(0.0_f64, |m, v| m.max(v.abs())))
})
.unwrap_or(Number::NAN);
let rescaled_obj_scale = if final_grad_max.is_finite() {
pounce_nlp::orig_ipopt_nlp::gradient_obj_scale(
final_grad_max,
num("nlp_scaling_max_gradient", 100.0),
num("nlp_scaling_min_value", 1e-8),
num("nlp_scaling_obj_target_gradient", 0.0),
)
} else {
Number::NAN
};
let input = ScaleAuditInput {
tol: num("tol", 1e-8),
obj_scale: base.final_obj_scaling_factor,
rescaled_obj_scale,
start_grad_max: base.start_obj_grad_max,
final_grad_max,
unscaled_kkt: base.final_unscaled_kkt_error,
unscaled_compl: base.final_unscaled_compl,
acceptable_tol,
masked_threshold,
warm_start: self
.options
.get_bool_value("warm_start_init_point", "")
.map(|(v, _)| v)
.unwrap_or(false),
};
let grad_scale = input.grad_scale();
let trigger = scale_audit_trigger(input);
let mut status = first_status;
match trigger {
Some(ScaleAudit::SmallObjective) => {
self.statistics
.borrow_mut()
.warnings
.push(scale_audit_warning(
ScaleAudit::SmallObjective,
&base,
grad_scale,
acceptable_tol,
"",
));
}
Some(kind) if gradient_based => {
status = self.run_scale_retry(tnlp, first_status, kind, grad_scale);
}
Some(kind) => {
self.statistics
.borrow_mut()
.warnings
.push(scale_audit_warning(
kind,
&base,
grad_scale,
acceptable_tol,
"was not attempted: nlp_scaling_method is not gradient-based",
));
}
None => {}
}
status
}
fn finalize_quality_verdict(
&mut self,
status: ApplicationReturnStatus,
) -> ApplicationReturnStatus {
if !self.is_solve_quality_audit_enabled() {
return status;
}
let acceptable_tol = self.dual_divergence_retry_accept_tol();
let mut status = status;
if matches!(status, ApplicationReturnStatus::SolveSucceeded) {
let (sig, du, du_scale) = {
let st = self.statistics.borrow();
(
st.returned_run_dual_divergence_signature,
st.final_unscaled_dual_inf,
st.final_unscaled_dual_scale,
)
};
if sig
&& du.is_finite()
&& du > acceptable_tol.max(UNSCALED_DUAL_WARN)
&& !unscaled_dual_inf_is_roundoff(du, du_scale)
{
status = ApplicationReturnStatus::SolvedToAcceptableLevel;
push_warning_once(
&mut self.statistics.borrow_mut().warnings,
format!(
"unscaled_dual_inf_above_acceptable: the run saw a settled primal with a \
runaway multiplier (gh#884) and the returned point's unscaled dual \
infeasibility is {du:.2e}, above {:.0e}; the status is downgraded from \
Solve_Succeeded to Solved_To_Acceptable_Level",
acceptable_tol.max(UNSCALED_DUAL_WARN)
),
);
}
}
let st = self.statistics.borrow().clone();
let extra = quality_warnings(
status,
st.final_unscaled_dual_inf,
st.final_unscaled_dual_scale,
self.options
.get_numeric_value("dual_inf_tol", "")
.map(|(v, _)| v)
.unwrap_or(1.0),
acceptable_tol,
self.options
.get_numeric_value("bound_relax_factor", "")
.is_ok_and(|(v, _)| v == 0.0),
);
let mut sm = self.statistics.borrow_mut();
for w in extra {
push_warning_once(&mut sm.warnings, w);
}
status
}
fn declared_x_box(&self, tnlp: &Rc<RefCell<dyn TNLP>>) -> Option<(Vec<Number>, Vec<Number>)> {
let mut t = tnlp.borrow_mut();
let info = t.get_nlp_info()?;
let (n, m) = (info.n.max(0) as usize, info.m.max(0) as usize);
let (mut x_l, mut x_u) = (vec![0.0; n], vec![0.0; n]);
let (mut g_l, mut g_u) = (vec![0.0; m], vec![0.0; m]);
t.get_bounds_info(BoundsInfo {
x_l: &mut x_l,
x_u: &mut x_u,
g_l: &mut g_l,
g_u: &mut g_u,
})
.then_some((x_l, x_u))
}
fn run_scale_retry(
&mut self,
tnlp: Rc<RefCell<dyn TNLP>>,
first_status: ApplicationReturnStatus,
kind: ScaleAudit,
grad_scale: Number,
) -> ApplicationReturnStatus {
let acceptable_tol = self.dual_divergence_retry_accept_tol();
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 base = self.statistics.borrow().clone();
let Some(seed) = solution_floor.as_ref().map(|f| f.x.clone()) else {
return first_status;
};
let prev_iter = self.options.get_integer_value("max_iter", "").ok();
let prev_cpu = self.options.get_numeric_value("max_cpu_time", "").ok();
let prev_wall = self.options.get_numeric_value("max_wall_time", "").ok();
let cpu_left = prev_cpu.map(|(v, _)| v - base.total_cpu_time_secs);
let wall_left = prev_wall.map(|(v, _)| v - base.total_wallclock_time_secs);
if cpu_left.is_some_and(|t| t <= 0.0) || wall_left.is_some_and(|t| t <= 0.0) {
return first_status;
}
let retry_iter = (SCALE_AUDIT_RETRY_ITER_FACTOR * base.iteration_count)
.max(SCALE_AUDIT_RETRY_MIN_ITER)
.min(prev_iter.map_or(Index::MAX, |(v, _)| v));
let seeded: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(
pounce_nlp::seeded_tnlp::SeededTnlp::new(Rc::clone(&tnlp), seed),
));
let prev_user = self
.options
.get_numeric_value("obj_scaling_factor", "")
.ok()
.and_then(|(v, f)| f.then_some(v));
let prev_user_factor = prev_user.unwrap_or(1.0);
let caller_named_relax = self
.options
.get_numeric_value("bound_relax_factor", "")
.is_ok_and(|(_, found)| found);
let mut scope = RetryOptionScope::enter(
self,
&[
"obj_scaling_factor",
"max_iter",
"max_cpu_time",
"max_wall_time",
],
false,
);
if let ScaleAudit::Upscale(k) = kind {
let _ = scope.options.set_numeric_value(
"obj_scaling_factor",
prev_user_factor * k,
true,
false,
);
}
let _ = scope
.options
.set_integer_value("max_iter", retry_iter, true, false);
scope.retry_keeps_declared_box.set(!caller_named_relax);
if let Some(t) = cpu_left {
let _ = scope
.options
.set_numeric_value("max_cpu_time", t, true, false);
}
if let Some(t) = wall_left {
let _ = scope
.options
.set_numeric_value("max_wall_time", t, true, false);
}
tracing::debug!(target: "pounce::algorithm",
"[POUNCE] gh#983: {:?} verdict audited ({:?}); re-solving from the \
returned point (max_iter {retry_iter}).", first_status, kind);
if match scope.options.get_integer_value("print_level", "") {
Ok((v, true)) => v >= 1,
_ => true,
} {
println!();
println!(
"gh#983 scale audit: re-solving from the returned point ({kind:?}); the \
summary below is a candidate's, and the line after it says whether it \
replaced the answer above."
);
}
let retry_status = scope.optimize_constrained(Rc::clone(&seeded));
drop(scope);
let retry = self.statistics.borrow().clone();
let sense = if prev_user_factor < 0.0 { -1.0 } else { 1.0 };
let clean = match first_status {
ApplicationReturnStatus::SolveSucceeded => {
matches!(retry_status, ApplicationReturnStatus::SolveSucceeded)
}
_ => matches!(
retry_status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
),
};
let better = match kind {
ScaleAudit::Rescale => retry.final_unscaled_kkt_error < base.final_unscaled_kkt_error,
ScaleAudit::Upscale(_) | ScaleAudit::SmallObjective => {
retry.final_unscaled_compl < 0.1 * base.final_unscaled_compl
&& retry.final_unscaled_kkt_error <= base.final_unscaled_kkt_error
}
};
let admissible = retry_answer_is_admissible(
certificate_floor.objective,
certificate_floor.unscaled_constr_viol,
retry.final_objective,
retry.final_unscaled_constr_viol,
acceptable_tol,
sense,
);
let (base_box_rel, retry_box_rel) = match self.declared_x_box(&tnlp) {
Some((x_l, x_u)) => {
let (lo, up) = (self.nlp_lower_bound_inf(), self.nlp_upper_bound_inf());
let rel = |x: &[Number]| declared_box_relative_violation(x, &x_l, &x_u, lo, up);
(
solution_floor.as_ref().map_or(Number::NAN, |f| rel(&f.x)),
self.last_finalize
.borrow()
.as_ref()
.map_or(Number::NAN, |f| rel(&f.x)),
)
}
None => (Number::NAN, Number::NAN),
};
let keeps_box = retry_keeps_declared_box(base_box_rel, retry_box_rel);
let promote = clean && better && admissible && keeps_box;
let console_output = match self.options.get_integer_value("print_level", "") {
Ok((v, true)) => v >= 1,
_ => true,
};
if console_output {
println!();
if clean && better && admissible && !keeps_box {
println!(
"gh#983 scale audit: declined (the re-solve's point is {:.4e} outside the \
declared variable bounds, the base attempt's {:.4e}); the base attempt's \
answer is the one reported.",
retry.final_declared_box_viol, certificate_floor.declared_box_viol
);
} else if promote {
println!(
"gh#983 scale audit: promoted -- unscaled KKT error {:.4e} -> {:.4e}, \
unscaled complementarity {:.4e} -> {:.4e}.",
base.final_unscaled_kkt_error,
retry.final_unscaled_kkt_error,
base.final_unscaled_compl,
retry.final_unscaled_compl
);
} else {
println!(
"gh#983 scale audit: declined ({retry_status:?}, unscaled KKT error \
{:.4e} against the base attempt's {:.4e}); the base attempt's answer \
is the one reported.",
retry.final_unscaled_kkt_error, base.final_unscaled_kkt_error
);
}
if !promote {
println!(
"gh#983 scale audit: the summary block above is the discarded re-solve's; \
the reported answer is the base attempt's (objective {:.16e}, variable \
bound violation {:.16e}).",
certificate_floor.objective, certificate_floor.declared_box_viol
);
}
}
if promote {
return retry_status;
}
if let Some(floor) = solution_floor {
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 |=
retry.dual_divergence_signature;
if let Some(stats) = trace_floor {
let _ = tnlp.borrow_mut().intermediate_callback(
stats,
&TnlpIpoptData::default(),
&TnlpIpoptCq::default(),
);
}
}
let detail = if keeps_box {
format!(
"was declined: the re-solve returned {retry_status:?} with unscaled KKT error {:.2e}",
retry.final_unscaled_kkt_error
)
} else {
format!(
"was declined: the re-solve returned {retry_status:?} at a point {:.2e} outside \
the declared variable bounds",
retry.final_declared_box_viol
)
};
self.statistics
.borrow_mut()
.warnings
.push(scale_audit_warning(
kind,
&base,
grad_scale,
acceptable_tol,
&detail,
));
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;
let mut totals = MultiPassTotals::default();
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);
totals.push_pass(&self.statistics.borrow(), Some(rho), last_status);
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);
totals.set_slack_sum(slack_sum);
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| {
totals.set_violation(f.max_violation);
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);
}
totals.apply(&mut self.statistics.borrow_mut());
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();
self.statistics.borrow_mut().final_declared_constr_viol =
feas.as_ref().map_or(Number::NAN, |f| f.max_violation);
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);
self.statistics.borrow_mut().warnings.clear();
self.statistics
.borrow_mut()
.returned_run_dual_divergence_signature = false;
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();
for sink in [&self.linsol_summary_sink, &self.resto_linsol_summary_sink] {
match sink.lock() {
Ok(mut guard) => {
*guard = LinearSolverSummary::default();
}
_ => {
debug_assert!(false, "linsol summary sink mutex poisoned");
}
}
}
self.quality_escalations.set(0);
if let Ok(mut g) = self.kkt_blocks_published.lock() {
*g = None;
}
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);
if self.retry_keeps_declared_box.get() {
orig_nlp.relax_bounds_keeping_box(bound_relax_factor, constr_viol_tol);
} else {
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::Partitioned
) {
let declared = !matches!(
self.options.get_string_value("partitioned_structure", ""),
Ok((ref v, true)) if v == "jacobian"
);
if declared {
builder.hessian_declared_pattern = adapter.borrow().lagrangian_hessian_pattern();
}
}
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());
builder.set_kkt_schur_summary_sink(Arc::clone(&self.linsol_summary_sink));
}
let detect = matches!(
self.options.get_string_value("kkt_block_detect", ""),
Ok((ref v, true)) if v.eq_ignore_ascii_case("yes")
);
{
let a = adapter.borrow();
let cls = a.classification();
self.x_elimination = Some((cls.x_not_fixed_map.clone(), cls.n_full_x as usize));
}
let mapped = self.model_blocks.as_ref().and_then(|(v, c)| {
let cls = adapter.borrow().classification().clone();
match map_block_structure_to_kkt(&cls, v, c) {
Ok(l) => Some(l),
Err(e) => {
tracing::warn!(
target: "pounce::kkt",
error = %e,
"declared block structure does not fit this problem; using the standard solver"
);
None
}
}
});
if let Some(labels) = mapped
.or_else(|| self.kkt_blocks.clone())
.or(detect.then(Vec::new))
{
builder.set_kkt_blocks(labels.clone(), feral_cfg.clone());
builder.set_kkt_schur_summary_sink(Arc::clone(&self.linsol_summary_sink));
let resto_blocks = !matches!(
self.options.get_string_value("kkt_block_restoration", ""),
Ok((ref v, _)) if v.eq_ignore_ascii_case("no")
);
if resto_blocks {
if let Ok(mut g) = self.kkt_blocks_published.lock() {
*g = Some((labels, 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, "");
builder.inertia_sink = Some(Arc::clone(&self.linsol_summary_sink));
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));
alg.live_iterate_fixed_var_duals = self.reports_fixed_var_duals();
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.returned_run_dual_divergence_signature = alg.dual_divergence_signature();
stats.dual_divergence_retry_promoted = self.dual_divergence_retry_promoted.get();
stats.derivative_check = self.derivative_check.borrow().clone();
let (rs, ra) = self.obj_scale_refused.get();
self.obj_scale_refused.set((
rs || alg.obj_scale_certificate_refused(),
ra || alg.obj_scale_acceptable_refused(),
));
stats.obj_scale_certificate_refused = self.obj_scale_refused.get().0;
stats.obj_scale_acceptable_refused = self.obj_scale_refused.get().1;
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 = cq.curr_declared_primal_violation_max();
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_dual_scale = cq.curr_unscaled_dual_infeasibility_scale_max();
stats.final_obj_scaling_factor = cq.computed_obj_scaling_factor();
stats.final_total_obj_scaling_factor = cq.obj_scaling_factor().abs();
stats.start_obj_grad_max = cq.start_obj_gradient_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 app_status == ApplicationReturnStatus::RestorationFailed
&& matches!(
self.options.get_string_value("mehrotra_algorithm", ""),
Ok((ref v, true)) if v == "yes"
)
{
tracing::warn!(
target: "pounce::algorithm",
"pounce: restoration failed under mehrotra_algorithm=yes. That option \
disables the line search and is intended for LPs and convex QPs; on a \
general NLP every trial step is accepted, so the iterate can leave the \
region restoration can repair, and a successful run may stop at a \
different local solution than the default algorithm finds. Retry with \
the default (mehrotra_algorithm=no)."
);
}
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,
self.reports_fixed_var_duals(),
) {
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, _)) = self.options.get_integer_value("kkt_block_min_size", "") {
builder.kkt_block_min_size = v.max(0) as usize;
}
builder.set_kkt_blocks_cell(Arc::clone(&self.kkt_blocks_published));
builder.set_kkt_schur_summary_sink(Arc::clone(&self.resto_linsol_summary_sink));
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;
let default_tol = crate::alg_builder::ConvCheckOptions::default().dual_inf_tol;
if read_num("dual_inf_scale_kappa").is_none() && v != default_tol {
builder.conv_check.dual_inf_scale_kappa = 0.0;
}
}
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 Ok((v, true)) = self.options.get_bool_value("objective_noise_wolfe", "") {
builder.line_search.objective_noise_wolfe = 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 map_block_structure_to_kkt(
cls: &pounce_nlp::tnlp_adapter::BoundClassification,
var_blocks: &[i32],
con_blocks: &[i32],
) -> Result<Vec<i32>, String> {
if var_blocks.len() != cls.n_full_x as usize {
return Err(format!(
"block structure has {} variable labels, the problem has {} variables",
var_blocks.len(),
cls.n_full_x
));
}
if con_blocks.len() != cls.n_full_g as usize {
return Err(format!(
"block structure has {} constraint labels, the problem has {} constraints",
con_blocks.len(),
cls.n_full_g
));
}
let n_x = cls.x_not_fixed_map.len();
let n_c = cls.c_map.len();
let n_d = cls.d_map.len();
let mut labels = Vec::with_capacity(n_x + 2 * n_d + n_c);
for &full in &cls.x_not_fixed_map {
labels.push(var_blocks[full as usize]);
}
for &full in &cls.d_map {
labels.push(con_blocks[full as usize]); }
for &full in &cls.c_map {
labels.push(con_blocks[full as usize]); }
for &full in &cls.d_map {
labels.push(con_blocks[full as usize]); }
Ok(labels)
}
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_ordering_preprocess", "") {
if let Some(p) = pounce_feral::parse_ordering_preprocess(&v) {
cfg.ordering_preprocess = p;
}
}
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,
}
struct RetryOptionScope<'a> {
app: &'a mut IpoptApplication,
saved: Vec<(
&'static str,
pounce_common::options_list::OptionEntrySnapshot,
)>,
on_unwind_only: bool,
}
impl<'a> RetryOptionScope<'a> {
fn enter(app: &'a mut IpoptApplication, tags: &[&'static str], on_unwind_only: bool) -> Self {
let saved = tags
.iter()
.map(|&t| (t, app.options.snapshot_entry(t)))
.collect();
Self {
app,
saved,
on_unwind_only,
}
}
}
impl std::ops::Deref for RetryOptionScope<'_> {
type Target = IpoptApplication;
fn deref(&self) -> &IpoptApplication {
self.app
}
}
impl std::ops::DerefMut for RetryOptionScope<'_> {
fn deref_mut(&mut self) -> &mut IpoptApplication {
self.app
}
}
impl Drop for RetryOptionScope<'_> {
fn drop(&mut self) {
if self.on_unwind_only && !std::thread::panicking() {
return;
}
for (tag, snap) in self.saved.drain(..) {
self.app.options.restore_entry(tag, snap);
}
self.app.retry_keeps_declared_box.set(false);
}
}
#[derive(Debug, Clone)]
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,
declared_constr_viol: Number,
declared_box_viol: Number,
iteration_count: Index,
iterations: Vec<pounce_nlp::solve_statistics::IterRecord>,
evals: [Index; 5],
restoration: (Index, Index, Index, Number),
quality_escalations: Index,
unscaled_dual_scale: Number,
obj_scaling_factor: Number,
total_obj_scaling_factor: Number,
start_obj_grad_max: Number,
warnings: Vec<String>,
returned_run_signature: bool,
}
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,
declared_constr_viol: s.final_declared_constr_viol,
declared_box_viol: s.final_declared_box_viol,
iteration_count: s.iteration_count,
iterations: s.iterations.clone(),
evals: [
s.num_obj_evals,
s.num_constr_evals,
s.num_obj_grad_evals,
s.num_constr_jac_evals,
s.num_hess_evals,
],
restoration: (
s.restoration_calls,
s.restoration_inner_iters,
s.restoration_outer_iters,
s.restoration_wall_secs,
),
quality_escalations: s.quality_escalations,
unscaled_dual_scale: s.final_unscaled_dual_scale,
obj_scaling_factor: s.final_obj_scaling_factor,
total_obj_scaling_factor: s.final_total_obj_scaling_factor,
start_obj_grad_max: s.start_obj_grad_max,
warnings: s.warnings.clone(),
returned_run_signature: s.returned_run_dual_divergence_signature,
}
}
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;
s.final_declared_constr_viol = self.declared_constr_viol;
s.final_declared_box_viol = self.declared_box_viol;
s.iteration_count = self.iteration_count;
s.iterations = self.iterations.clone();
s.num_obj_evals = self.evals[0];
s.num_constr_evals = self.evals[1];
s.num_obj_grad_evals = self.evals[2];
s.num_constr_jac_evals = self.evals[3];
s.num_hess_evals = self.evals[4];
s.restoration_calls = self.restoration.0;
s.restoration_inner_iters = self.restoration.1;
s.restoration_outer_iters = self.restoration.2;
s.restoration_wall_secs = self.restoration.3;
s.quality_escalations = self.quality_escalations;
s.final_unscaled_dual_scale = self.unscaled_dual_scale;
s.final_obj_scaling_factor = self.obj_scaling_factor;
s.final_total_obj_scaling_factor = self.total_obj_scaling_factor;
s.start_obj_grad_max = self.start_obj_grad_max;
s.warnings = self.warnings.clone();
s.returned_run_dual_divergence_signature = self.returned_run_signature;
}
}
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>>,
fixed_var_duals: bool,
) -> 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];
}
if fixed_var_duals {
nlp_borrow.complete_fixed_var_bound_multipliers(&x_vec, &lambda, &mut z_l, &mut z_u);
}
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 sqp_tnlp_warm_start(nlp_rc: &Rc<RefCell<dyn IpoptNlp>>) -> Option<crate::sqp::SqpIterates> {
use pounce_linalg::dense_vector::DenseVectorSpace;
let dense = |v: &dyn pounce_linalg::Vector| -> Vec<Number> {
v.as_any()
.downcast_ref::<pounce_linalg::dense_vector::DenseVector>()
.map(|d| d.expanded_values().to_vec())
.unwrap_or_default()
};
let (z_l, z_u, x, lambda_g) = {
let mut nlp = nlp_rc.borrow_mut();
if !nlp.prepare_warm_start() {
return None;
}
let (n, m_eq, m_ineq) = (nlp.n(), nlp.m_eq(), nlp.m_ineq());
let (n_xl, n_xu) = (nlp.x_l().dim(), nlp.x_u().dim());
let (n_dl, n_du) = (nlp.d_l().dim(), nlp.d_u().dim());
let mut x = DenseVectorSpace::new(n).make_new_dense();
let mut y_c = DenseVectorSpace::new(m_eq).make_new_dense();
let mut y_d = DenseVectorSpace::new(m_ineq).make_new_dense();
let mut z_l = DenseVectorSpace::new(n_xl).make_new_dense();
let mut z_u = DenseVectorSpace::new(n_xu).make_new_dense();
let mut v_l = DenseVectorSpace::new(n_dl).make_new_dense();
let mut v_u = DenseVectorSpace::new(n_du).make_new_dense();
let ok = nlp.get_starting_x(&mut x)
&& nlp.get_starting_y(&mut y_c, &mut y_d)
&& nlp.get_starting_z(&mut z_l, &mut z_u, &mut v_l, &mut v_u);
nlp.finish_warm_start();
if !ok {
return None;
}
let finite = |v: Vec<Number>| -> Vec<Number> {
v.into_iter()
.map(|a| if a.is_finite() { a } else { 0.0 })
.collect()
};
let mut lambda_g = finite(dense(&y_c));
lambda_g.extend(finite(dense(&y_d)));
(
finite(dense(&z_l)),
finite(dense(&z_u)),
dense(&x),
lambda_g,
)
};
let lambda_x = crate::sqp::ipopt_adapter::pack_bound_multipliers(nlp_rc, &z_l, &z_u);
Some(crate::sqp::SqpIterates {
x,
lambda_g,
lambda_x,
working: None,
})
}
fn sqp_warm_start_to_algorithm(nlp: &dyn IpoptNlp, w: &mut crate::sqp::SqpIterates) {
let n_alg = nlp.n() as usize;
let n_full = nlp.n_full_x() as usize;
let m_full = nlp.n_full_g() as usize;
let m_eq = nlp.m_eq() as usize;
let obj_scal = nlp.obj_scaling_factor();
let d = nlp.variable_scaling();
let cols: Vec<(usize, Number)> = (0..n_alg)
.map(|k| {
let j = nlp.var_x_to_full_x(k as Index) as usize;
let dj = d.as_ref().and_then(|d| d.get(j).copied()).unwrap_or(1.0);
(j, dj)
})
.collect();
let to_alg = |v: &[Number], f: &dyn Fn(Number, Number) -> Number| -> Option<Vec<Number>> {
if v.len() == n_full {
Some(cols.iter().map(|&(j, dj)| f(v[j], dj)).collect())
} else if v.len() == n_alg {
Some(cols.iter().zip(v).map(|(&(_, dj), &x)| f(x, dj)).collect())
} else {
None
}
};
if let Some(x) = to_alg(&w.x, &|x, dj| x * dj) {
w.x = x;
}
if let Some(lx) = to_alg(&w.lambda_x, &|z, dj| z * obj_scal / dj) {
w.lambda_x = lx;
}
if w.lambda_g.len() == m_full {
let cs = nlp.c_scale_vec();
let ds = nlp.d_scale_vec();
let mut y = vec![0.0; m_full];
for (i, &lam) in w.lambda_g.iter().enumerate() {
let i = i as Index;
if let Some(k) = nlp.full_g_to_c_block(i) {
let k = k as usize;
let s = cs.as_ref().map_or(1.0, |v| v[k]);
y[k] = lam / s * obj_scal;
} else if let Some(k) = nlp.full_g_to_d_block(i) {
let k = k as usize;
let s = ds.as_ref().map_or(1.0, |v| v[k]);
y[m_eq + k] = lam / s * obj_scal;
}
}
w.lambda_g = y;
}
if let Some(ws) = w.working.as_mut() {
ws.constraints = working_set_rows_to_algorithm(nlp, std::mem::take(&mut ws.constraints));
}
}
fn algorithm_rows_to_user(nlp: &dyn IpoptNlp) -> Option<Vec<usize>> {
let m_full = nlp.n_full_g() as usize;
let m_eq = nlp.m_eq() as usize;
let m_alg = m_eq + nlp.m_ineq() as usize;
if m_full != m_alg {
return None;
}
let mut map = vec![usize::MAX; m_alg];
for i in 0..m_full {
let k = if let Some(k) = nlp.full_g_to_c_block(i as Index) {
k as usize
} else if let Some(k) = nlp.full_g_to_d_block(i as Index) {
m_eq + k as usize
} else {
return None;
};
if k >= m_alg || map[k] != usize::MAX {
return None;
}
map[k] = i;
}
Some(map)
}
fn working_set_rows_to_user(
nlp: &dyn IpoptNlp,
rows: Vec<pounce_qp::ConsStatus>,
) -> Vec<pounce_qp::ConsStatus> {
match algorithm_rows_to_user(nlp) {
Some(map) if map.len() == rows.len() => {
let mut user = vec![pounce_qp::ConsStatus::Inactive; rows.len()];
for (k, &i) in map.iter().enumerate() {
user[i] = rows[k];
}
user
}
_ => rows,
}
}
fn working_set_rows_to_algorithm(
nlp: &dyn IpoptNlp,
rows: Vec<pounce_qp::ConsStatus>,
) -> Vec<pounce_qp::ConsStatus> {
match algorithm_rows_to_user(nlp) {
Some(map) if map.len() == rows.len() => map.iter().map(|&i| rows[i]).collect(),
_ => rows,
}
}
fn sqp_user_iterate(
nlp: &Rc<RefCell<dyn IpoptNlp>>,
x: &[Number],
lambda_g: &[Number],
lambda_x: &[Number],
c_vals: &[Number],
grad_lag_x: &[Number],
fixed_var_duals: bool,
) -> crate::intermediate::UserIterate {
use pounce_linalg::dense_vector::DenseVectorSpace;
let (zl, zu) = crate::sqp::ipopt_adapter::split_bound_multipliers(nlp, lambda_x);
let nlp = nlp.borrow();
let n_alg = nlp.n() as usize;
let m_eq = nlp.m_eq() as usize;
let m_ineq = nlp.m_ineq() as usize;
let n_user = nlp.n_full_x() as usize;
let m_user = nlp.n_full_g() as usize;
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 dv = |space: &Rc<DenseVectorSpace>, v: &[Number]| {
let mut d = space.make_new_dense();
if !v.is_empty() {
d.set_values(v);
}
d
};
let x_full = nlp.finalize_solution_x(&dv(&x_space, x));
let mut z_l = nlp.finalize_solution_z_l(&dv(&DenseVectorSpace::new(zl.len() as Index), &zl));
if z_l.is_empty() {
z_l = vec![0.0; n_user];
}
let mut z_u = nlp.finalize_solution_z_u(&dv(&DenseVectorSpace::new(zu.len() as Index), &zu));
if z_u.is_empty() {
z_u = vec![0.0; n_user];
}
let (y_c, y_d) = lambda_g.split_at(m_eq.min(lambda_g.len()));
let mut lambda = nlp.finalize_solution_lambda(&dv(&c_space, y_c), &dv(&d_space, y_d));
if lambda.is_empty() {
lambda = vec![0.0; m_user];
}
if fixed_var_duals {
nlp.complete_fixed_var_bound_multipliers(&x_full, &lambda, &mut z_l, &mut z_u);
}
let (c, d) = c_vals.split_at(m_eq.min(c_vals.len()));
let g = nlp.pack_g_for_user(&dv(&c_space, c), &dv(&d_space, d));
let (x_l_alg, x_u_alg) = crate::intermediate::algorithm_x_bounds(&*nlp, &x_full);
crate::intermediate::UserIterate {
x: x_full,
z_l,
z_u,
g,
lambda,
scaling: Some(crate::intermediate::iterate_scaling(&*nlp)),
x_l_alg,
x_u_alg,
grad_lag_x: crate::intermediate::user_grad_lag(&*nlp, grad_lag_x),
}
}
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>>,
fixed_var_duals: bool,
) -> 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 (zl_vals, zu_vals) = crate::sqp::ipopt_adapter::split_bound_multipliers(nlp, &res.lambda_x);
let mut z_l_compressed = DenseVectorSpace::new(zl_vals.len() as Index).make_new_dense();
let mut z_u_compressed = DenseVectorSpace::new(zu_vals.len() as Index).make_new_dense();
if !zl_vals.is_empty() {
z_l_compressed.set_values(&zl_vals);
}
if !zu_vals.is_empty() {
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];
}
if fixed_var_duals {
nlp_borrow.complete_fixed_var_bound_multipliers(&x_vec, &lambda, &mut z_l, &mut z_u);
}
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 block_structure_tests {
use super::map_block_structure_to_kkt;
use pounce_nlp::tnlp_adapter::BoundClassification;
fn classification() -> BoundClassification {
BoundClassification {
n_full_x: 3,
n_full_g: 4,
n_x_fixed: 1,
x_not_fixed_map: vec![0, 2],
x_fixed_map: vec![1],
x_fixed_vals: vec![0.0],
full_to_var: vec![0, -1, 1],
x_l_map: vec![],
x_u_map: vec![],
n_c: 2,
c_map: vec![0, 3],
n_d: 2,
d_map: vec![1, 2],
d_l_map: vec![],
d_u_map: vec![],
full_to_c: vec![0, -1, -1, 1],
full_to_d: vec![-1, 0, 1, -1],
}
}
#[test]
fn model_labels_map_onto_the_kkt_layout() {
let cls = classification();
let var_blocks = vec![7, 99, -1]; let con_blocks = vec![5, 6, -1, 8]; let labels = map_block_structure_to_kkt(&cls, &var_blocks, &con_blocks).unwrap();
assert_eq!(
labels,
vec![
7, -1, 6, -1, 5, 8, 6, -1, ]
);
}
#[test]
fn a_mismatched_declaration_is_refused() {
let cls = classification();
assert!(map_block_structure_to_kkt(&cls, &[1, 2], &[1, 2, 3, 4]).is_err());
assert!(map_block_structure_to_kkt(&cls, &[1, 2, 3], &[1, 2, 3]).is_err());
}
}
#[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());
}
#[test]
fn a_pounce_chosen_mu_strategy_keeps_the_fallback_on() {
let mut app = IpoptApplication::new();
app.options_mut()
.set_string_value("hessian_approximation", "limited-memory", true, false)
.unwrap();
app.options_mut()
.set_string_value("mu_strategy", "monotone", true, false)
.unwrap();
assert!(!app.is_mu_strategy_fallback_enabled());
app.mark_mu_strategy_pounce_chosen();
assert!(app.is_mu_strategy_fallback_enabled());
assert!(!app.effective_mu_strategy_is_adaptive());
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 the_declared_box_conjunct_is_relative_to_each_bounds_magnitude() {
let (lo, up) = (-1e19, 1e19);
let rel = |x: &[Number], l: &[Number], u: &[Number]| {
declared_box_relative_violation(x, l, u, lo, up)
};
let ub: Number = 1e6;
let pinned: Number = ub + 1e-4; let one_ulp_out = f64::from_bits(pinned.to_bits() + 1);
assert!(
one_ulp_out - pinned > 1e-12,
"premise: an ulp of 1e6 exceeds 1e-12"
);
let (l, u) = (vec![0.0], vec![ub]);
let base = rel(&[pinned], &l, &u);
assert!(retry_keeps_declared_box(base, rel(&[one_ulp_out], &l, &u)));
assert!(one_ulp_out - ub > (pinned - ub) + 1e-12);
let genuine = rel(&[ub + 9.9995e-5], &l, &u);
let base_genuine = rel(&[ub + 9.7494e-5], &l, &u);
assert!(!retry_keeps_declared_box(base_genuine, genuine));
let (l1, u1) = (vec![0.0], vec![1.0]);
assert!(!retry_keeps_declared_box(0.0, rel(&[1.0 + 1e-8], &l1, &u1)));
assert!(retry_keeps_declared_box(
0.0,
rel(&[f64::from_bits(1.0f64.to_bits() + 1)], &l1, &u1)
));
assert_eq!(rel(&[-2e-4], &[-1e4], &[1e19]), 0.0);
assert!(rel(&[-1e4 - 1e-4], &[-1e4], &[1e19]) > 0.0);
assert_eq!(rel(&[5.0], &[-1e19], &[1e19]), 0.0);
assert_eq!(rel(&[0.5 + 1e-8], &[0.5], &[0.5]), 0.0);
assert!(rel(&[1.0], &[0.0, 0.0], &[1.0, 1.0]).is_nan());
assert!(retry_keeps_declared_box(Number::NAN, Number::NAN));
assert!(!retry_keeps_declared_box(0.0, Number::NAN));
}
#[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 a_retry_violation_at_the_noise_floor_does_not_block_an_improvement() {
assert!(retry_answer_is_admissible(
2.0, 3.037e-38, 1.0, 1.9584e-13, ACCEPT, MIN
));
assert!(retry_answer_is_admissible(
-2.0, 3.037e-38, -1.0, 1.9584e-13, ACCEPT, MAX
));
assert!(!retry_answer_is_admissible(
2.0,
3.037e-38,
1.0,
RETRY_VIOL_NOISE_FLOOR * 10.0,
ACCEPT,
MIN
));
assert!(!retry_answer_is_admissible(
1.0, 3.037e-38, 2.0, 1.0e-13, ACCEPT, MIN
));
}
#[allow(clippy::too_many_arguments)]
fn audit_in(
tol: Number,
obj_scale: Number,
rescaled: Number,
start_grad: Number,
final_grad: Number,
kkt: Number,
compl: Number,
th: Number,
) -> ScaleAuditInput {
ScaleAuditInput {
tol,
obj_scale,
rescaled_obj_scale: rescaled,
start_grad_max: start_grad,
final_grad_max: final_grad,
unscaled_kkt: kkt,
unscaled_compl: compl,
acceptable_tol: 1e-6,
masked_threshold: th,
warm_start: false,
}
}
#[allow(clippy::too_many_arguments)]
fn audit_in_warm(
tol: Number,
obj_scale: Number,
rescaled: Number,
start_grad: Number,
final_grad: Number,
kkt: Number,
compl: Number,
th: Number,
) -> ScaleAuditInput {
ScaleAuditInput {
warm_start: true,
..audit_in(
tol, obj_scale, rescaled, start_grad, final_grad, kkt, compl, th,
)
}
}
#[test]
fn a_warm_start_at_an_answer_is_not_read_as_a_tiny_objective() {
let th = 1e-4;
let t = |inp| scale_audit_trigger(inp);
assert_eq!(
t(audit_in_warm(
1e-8, 1.0, 1.0, 1.0e-11, 3.726e-9, 3.72e-9, 3.72e-9, th
)),
None
);
assert!(matches!(
t(audit_in(
1e-8, 1.0, 1.0, 1.0e-11, 3.726e-9, 3.72e-9, 3.72e-9, th
)),
Some(ScaleAudit::Upscale(_))
));
match t(audit_in_warm(
1e-8, 1.0, 1.0, 1e-5, 1e-5, 2.5e-9, 2.5e-9, th,
)) {
Some(ScaleAudit::Upscale(k)) => assert!((k - 1e5).abs() < 1.0),
other => panic!("{other:?}"),
}
assert!(matches!(
t(audit_in_warm(
1e-8, 1.0, 1.0, 1e-11, 1e-5, 2.5e-9, 2.5e-9, th
)),
Some(ScaleAudit::Upscale(_))
));
assert_eq!(
t(audit_in_warm(
1e-8, 3e-7, 1.0, 3e8, 2.9e-2, 2.9e-2, 1e-9, th
)),
Some(ScaleAudit::Rescale)
);
}
#[test]
fn the_scale_audit_trigger_takes_each_branch_and_neither_by_default() {
let th = 1e-4;
let t = |inp| scale_audit_trigger(inp);
assert_eq!(
t(audit_in(1e-8, 3e-7, 1.0, 3e8, 2.9e-2, 2.9e-2, 1e-9, th)),
Some(ScaleAudit::Rescale)
);
assert_eq!(
t(audit_in(1e-8, 2.2e-7, 2.4e-7, 4.6e8, 4.2e8, 3e-6, 1e-9, th)),
None
);
match t(audit_in(1e-8, 1.0, 1.0, 1e-5, 1e-5, 2.5e-9, 2.5e-9, th)) {
Some(ScaleAudit::Upscale(k)) => assert!((k - 1e5).abs() < 1.0),
other => panic!("{other:?}"),
}
assert!(matches!(
t(audit_in(1e-8, 1.0, 1.0, 3e-4, 3e-4, 2.5e-9, 2.5e-9, th)),
Some(ScaleAudit::Upscale(_))
));
assert!(matches!(
t(audit_in(1e-8, 1.0, 1.0, 1e-2, 1e-2, 2.5e-9, 2.5e-9, th)),
Some(ScaleAudit::Upscale(_))
));
assert_eq!(
t(audit_in(1e-8, 1.0, 1.0, 10.0, 10.0, 2.5e-9, 2.5e-9, th)),
None
);
assert_eq!(
t(audit_in(1e-8, 1.0, 1.0, 0.5, 0.5, 1e-6, 1e-6, th)),
Some(ScaleAudit::SmallObjective)
);
assert_eq!(
t(audit_in(1e-8, 1.0, 1.0, 2e-7, 1.0, 2.5e-9, 2.5e-9, th)),
None
);
assert_eq!(
t(audit_in(1e-8, 1.0, 1.0, 0.0, 2e-4, 6e-3, 2.5e-9, th)),
None
);
assert!(matches!(
t(audit_in(
1e-8,
f64::NAN,
f64::NAN,
f64::NAN,
1e-5,
2.5e-9,
2.5e-9,
th
)),
Some(ScaleAudit::Upscale(_))
));
assert_eq!(
t(audit_in(1e-8, 1.0, 1.0, 50.0, 50.0, 1e-3, 1e-3, th)),
None
);
assert_eq!(
t(audit_in(1e-8, 1e-8, 1e-8, 1e9, 1e9, 1e-9, 1e-12, th)),
None
);
assert_eq!(
t(audit_in(1e-8, 3e-7, 1.0, 3e8, 2.9e-2, 2.9e-2, 1e-9, 0.0)),
None
);
assert_eq!(
t(audit_in(5e-2, 1.0, 1.0, 1e-5, 1e-5, 1e-2, 1e-2, th)),
None
);
let nan = f64::NAN;
assert_eq!(t(audit_in(1e-8, nan, nan, nan, nan, nan, nan, th)), None);
match t(audit_in(1e-8, 1.0, 1.0, 1e-14, 1e-14, 1.0, 1.0, th)) {
Some(ScaleAudit::Upscale(k)) => assert_eq!(k, SCALE_AUDIT_MAX_UPSCALE),
other => panic!("{other:?}"),
}
}
#[test]
fn a_residual_at_the_roundoff_floor_of_its_terms_is_not_a_miss() {
assert!(unscaled_dual_inf_is_roundoff(5.54e-3, 1.57e13));
assert!(unscaled_dual_inf_is_roundoff(7.39e-3, 2.66e13));
assert!(!unscaled_dual_inf_is_roundoff(1.33e-2, 6.6e10));
assert!(!unscaled_dual_inf_is_roundoff(0.5, 7.55e11));
assert!(!unscaled_dual_inf_is_roundoff(6.25e-2, 3.21e11));
assert!(!unscaled_dual_inf_is_roundoff(3.92e-3, 5.03e9));
assert!(!unscaled_dual_inf_is_roundoff(0.144, 3.55e9));
assert!(!unscaled_dual_inf_is_roundoff(5.54e-3, f64::NAN));
assert!(!unscaled_dual_inf_is_roundoff(5.54e-3, 0.0));
assert!(!unscaled_dual_inf_is_roundoff(f64::NAN, 1e13));
}
#[test]
fn a_warning_code_is_attached_at_most_once() {
let mut w = vec!["unscaled_dual_inf_above_acceptable: downgraded".to_string()];
push_warning_once(
&mut w,
"unscaled_dual_inf_above_acceptable: verify in your units".to_string(),
);
push_warning_once(&mut w, "large_dual_scale: terms of 1e9".to_string());
assert_eq!(w.len(), 2, "{w:?}");
assert!(w[0].ends_with("downgraded"));
}
#[test]
fn quality_warnings_carry_a_code_and_only_on_success_verdicts() {
use ApplicationReturnStatus::*;
let w = quality_warnings(SolveSucceeded, 0.144, 3.55e9, 1.0, 1e-6, false);
assert!(
w.iter().any(|m| m.starts_with("large_dual_scale:")),
"{w:?}"
);
assert!(
w.iter()
.any(|m| m.starts_with("unscaled_dual_inf_above_acceptable:")),
"{w:?}"
);
let w = quality_warnings(SolveSucceeded, 89.7, 1.6e10, 1.0, 1e-6, false);
assert!(w.iter().any(|m| m.starts_with("large_dual_scale:")));
assert!(!w.iter().any(|m| m.contains("bound_relax_factor")), "{w:?}");
let w = quality_warnings(SolveSucceeded, 89.7, 1.6e10, 1.0, 1e-6, true);
assert!(
w.iter()
.any(|m| m.starts_with("large_dual_scale:") && m.contains("bound_relax_factor = 0")),
"{w:?}"
);
assert!(quality_warnings(SolveSucceeded, 1e-9, 5.0, 1.0, 1e-6, false).is_empty());
assert!(quality_warnings(SolveSucceeded, 2.9e-7, 1.46e9, 1.0, 1e-6, false).is_empty());
assert!(quality_warnings(SolveSucceeded, 5e-4, 5.0, 1.0, 1e-6, false).is_empty());
assert!(quality_warnings(RestorationFailed, 0.144, 3.55e9, 1.0, 1e-6, false).is_empty());
assert!(quality_warnings(SolveSucceeded, f64::NAN, f64::NAN, 1.0, 1e-6, false).is_empty());
}
#[test]
fn the_certificate_floor_carries_the_iteration_trace() {
use pounce_nlp::solve_statistics::{IterRecord, SolveStatistics};
let mut base = SolveStatistics::new();
base.iteration_count = 64;
base.iterations = vec![IterRecord::default(); 3];
base.num_obj_evals = 159;
base.restoration_calls = 2;
let floor = SolutionCertificate::of(&base);
let mut retry = SolveStatistics::new();
retry.iteration_count = 15;
retry.iterations = vec![IterRecord::default(); 15];
retry.num_obj_evals = 86;
floor.restore_into(&mut retry);
assert_eq!(retry.num_obj_evals, 159);
assert_eq!(retry.restoration_calls, 2);
assert_eq!(retry.iteration_count, 64);
assert_eq!(retry.iterations.len(), 3);
}
#[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));
}
}