use pounce_algorithm::alg_builder::{LinearBackendFactory, LinearSolverChoice};
use pounce_algorithm::application::{IpoptApplication, Ma57Config};
use pounce_cli::builtin;
use pounce_cli::cli::{Args, ProblemSource};
use pounce_cli::counting_tnlp::CountingTnlp;
use pounce_cli::nl_reader;
use pounce_cli::nl_writer;
use pounce_cli::print;
use pounce_cli::sens;
use pounce_cli::solve_report::{
InputDescriptor, ReportBuilder, ReportDetail, SolutionSuffix, status_to_solve_result_num,
write_report_file,
};
use pounce_common::diagnostics::{
DiagCategory, DiagnosticsConfig, DiagnosticsState, DumpFormat, IterSpec,
};
use pounce_linsol::sparse_sym_iface::SparseSymLinearSolverInterface;
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::solve_statistics::IterRecord;
use pounce_nlp::tnlp::{InfeasibilityProof, TNLP};
use pounce_restoration::resto_alg_builder::RestoAlgorithmBuilder;
use pounce_restoration::resto_inner_solver::{
InnerBackendFactoryFactory, make_default_restoration_factory_provider,
};
use pounce_restoration::second_opinion_driver::{SecondOpinionOutcome, run_second_opinion_ladder};
use std::cell::RefCell;
use std::path::PathBuf;
use std::process::ExitCode;
use std::rc::Rc;
fn presolve_verdict(
certified: Option<InfeasibilityProof>,
status: ApplicationReturnStatus,
) -> (String, i32) {
match certified.filter(|_| status == ApplicationReturnStatus::InfeasibleProblemDetected) {
Some(proof) => {
let detail = match proof {
InfeasibilityProof::BoundPropagation => "bound propagation".to_string(),
InfeasibilityProof::IntervalArithmetic { witness } => {
format!("interval arithmetic, constraint {witness}")
}
};
(
format!(
"POUNCE {}: InfeasibleProblemDetected (detected by presolve: {detail})",
env!("CARGO_PKG_VERSION")
),
201,
)
}
None => (
format!("POUNCE {}: {status:?}", env!("CARGO_PKG_VERSION")),
status_to_solve_result_num(status),
),
}
}
fn curvature_scaling_requested(app: &pounce_algorithm::application::IpoptApplication) -> bool {
app.options()
.get_string_value("nlp_scaling_method", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.is_some_and(|v| v == "curvature-based")
}
pub fn main() -> ExitCode {
pounce_observability::init_subscriber();
let raw_argv: Vec<String> = std::env::args().collect();
if raw_argv.get(1).map(|s| s == "verify").unwrap_or(false) {
return pounce_cli::verify::run_from_argv(&raw_argv[2..]);
}
if raw_argv.get(1).map(|s| s == "check-x0").unwrap_or(false) {
return pounce_cli::check_x0::run_from_argv(&raw_argv[2..]);
}
let mut args = match Args::parse_argv(std::env::args().collect()) {
Ok(a) => a,
Err(msg) => {
eprintln!("pounce: {msg}");
eprintln!("{}", Args::usage());
return ExitCode::from(2);
}
};
if let Ok(env_opts) = std::env::var("pounce_options") {
let mut merged = pounce_cli::cli::options_from_env(&env_opts);
if !merged.is_empty() {
merged.append(&mut args.set_options);
args.set_options = merged;
}
}
if args.help {
println!("{}", Args::usage());
return ExitCode::SUCCESS;
}
if args.version {
println!("pounce {}", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS;
}
if args.about {
print_about();
return ExitCode::SUCCESS;
}
if args.cite {
return run_cite(&args);
}
let mut app = IpoptApplication::new();
app.set_convex_routing_available(true);
if args.json_output.is_some() && matches!(args.json_detail, ReportDetail::Full) {
app.enable_iter_history();
}
let option_file_choice = match args.option_file_choice() {
Ok(c) => c,
Err(msg) => {
eprintln!("pounce: {msg}");
return ExitCode::from(2);
}
};
let mut option_file_read: Option<PathBuf> = None;
match &option_file_choice {
pounce_cli::cli::OptionFileChoice::Suppressed => {
if let Err(e) = app.initialize() {
eprintln!("pounce: initialize failed: {e}");
return ExitCode::from(2);
}
}
choice => {
let explicit = match choice {
pounce_cli::cli::OptionFileChoice::Named(p) => Some(p.as_path()),
_ => None,
};
match app.initialize_with_option_file(explicit) {
Ok(load) => {
for warning in &load.warnings {
eprintln!("pounce: warning: {warning}");
}
option_file_read = load.path;
}
Err(e) => {
eprintln!("pounce: failed to load options file: {}", e.message);
return ExitCode::from(2);
}
}
}
}
for (k, v) in &args.set_options {
let line = format!("{k} {v}\n");
if let Err(e) = app.options_mut().read_from_str(&line, true) {
eprintln!("pounce: failed to set {k}={v}: {e}");
return ExitCode::from(2);
}
}
let json_dbg = matches!(args.debug, Some(pounce_cli::cli::DebugMode::Json));
let restart_cell: pounce_cli::debug_repl::RestartCell = Rc::new(RefCell::new(None));
let mut debug_hook: Option<Rc<RefCell<pounce_cli::debug_repl::SolverDebugger>>> = None;
if let Some(mode) = args.debug {
if json_dbg {
let _ = app.options_mut().read_from_str("print_level 0\n", true);
}
let reg = Some(std::rc::Rc::clone(app.registered_options()));
let hook = Rc::new(RefCell::new(build_debugger(
mode,
args.debug_on_error,
args.debug_on_interrupt,
args.debug_script.as_deref(),
reg,
restart_cell.clone(),
)));
app.set_debug_hook(hook.clone());
debug_hook = Some(hook);
pounce_cli::debug_repl::interrupt::install();
pounce_cli::debug_repl::print_open_banner(mode);
let extra = if args.debug_on_error {
", on-error"
} else if args.debug_on_interrupt {
", on-interrupt"
} else {
""
};
eprintln!(
"pounce: interactive debugger enabled ({}{}). Type `help` at the prompt; Ctrl-C breaks in.",
match mode {
pounce_cli::cli::DebugMode::Repl => "repl",
pounce_cli::cli::DebugMode::Json => "json",
},
extra
);
}
let feral_cfg = pounce_algorithm::application::feral_config_from_options(app.options());
let ma57_cfg = pounce_algorithm::application::ma57_config_from_options(app.options(), "resto.");
let bff_mint = move || -> InnerBackendFactoryFactory {
let feral_cfg = feral_cfg.clone();
let ma57_cfg = ma57_cfg.clone();
Box::new(move || default_backend_factory(feral_cfg.clone(), ma57_cfg.clone()))
};
let resto_provider = make_default_restoration_factory_provider(
RestoAlgorithmBuilder::new(),
app.algorithm_builder_from_options(),
bff_mint,
);
app.set_restoration_factory_provider(resto_provider);
if let Some(value) = app.unimplemented_linear_solver() {
eprintln!(
"{}",
IpoptApplication::unimplemented_linear_solver_message(&value)
);
return ExitCode::from(2);
}
if let Some(msg) = app
.unimplemented_option_refusal()
.or_else(|| app.unimplemented_option_value_refusal())
{
eprintln!("{msg}");
return ExitCode::from(2);
}
let backend_warnings = app.take_unimplemented_backend_warnings();
for warning in app
.unexploited_hint_warnings()
.into_iter()
.chain(backend_warnings)
{
eprintln!("{warning}");
}
let sens_options = pounce_sensitivity::SensOptionOverrides::from_options_list(app.options());
let backend_tag = {
let (v, _) = app
.options()
.get_string_value("linear_solver", "")
.unwrap_or_else(|_| ("feral".to_string(), false));
if v.eq_ignore_ascii_case("ma57") {
#[cfg(feature = "ma57")]
{
"MA57 (HSL)"
}
#[cfg(not(feature = "ma57"))]
{
"FERAL (ma57 requested but not compiled)"
}
} else {
"FERAL"
}
};
let suppress_banner = app
.options()
.get_bool_value("sb", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.unwrap_or(false);
if !suppress_banner && !json_dbg {
print::print_logo();
print::print_banner(backend_tag);
}
if let Some(path) = &option_file_read {
if !suppress_banner && !json_dbg {
println!("Using option file \"{}\".\n", path.display());
}
}
let problem_desc: String = match &args.problem {
ProblemSource::Builtin(s) => format!("builtin:{s}"),
ProblemSource::NlFile(p) => format!("nl:{}", p.display()),
};
let sol_path: Option<PathBuf> = if args.no_sol {
None
} else if let Some(p) = &args.sol_output {
Some(p.clone())
} else {
match &args.problem {
ProblemSource::NlFile(p) => {
let mut s = p.clone();
s.set_extension("sol");
Some(s)
}
ProblemSource::Builtin(_) => None,
}
};
let mut nl_suffixes: Option<nl_reader::NlSuffixes> = None;
let mut nl_dims: Option<(usize, usize)> = None;
let mut nl_ampl_options: Vec<i64> = Vec::new();
let mut nl_class: Option<pounce_cli::dispatch::ProblemClass> = None;
let mut nl_curvature_read_curvature = false;
let mut nl_expr_provider: Option<
Rc<RefCell<dyn pounce_nlp::expression_provider::ExpressionProvider>>,
> = None;
let inner_tnlp: Rc<RefCell<dyn TNLP>> = match &args.problem {
ProblemSource::Builtin(name) => match builtin::lookup(name) {
Some(t) => t,
None => {
eprintln!("pounce: unknown builtin problem '{name}'");
eprintln!("available: {}", builtin::list().join(", "));
return ExitCode::from(2);
}
},
ProblemSource::NlFile(path) => {
if !json_dbg {
println!("Reading {}...", path.display());
}
let t0 = std::time::Instant::now();
match nl_reader::read_nl_file(path) {
Ok(prob) => {
nl_suffixes = Some(prob.suffixes.clone());
nl_dims = Some((prob.n, prob.m));
nl_ampl_options = prob.ampl_options.clone();
let elapsed = t0.elapsed().as_secs_f64();
if let Some(hook) = debug_hook.as_ref() {
let book = pounce_cli::debug_repl::EquationBook::new(
prob.con_names.clone(),
nl_reader::render_all_constraint_equations(&prob),
);
let (jac_irow, jac_jcol) = nl_reader::constraint_jacobian_sparsity(&prob);
let probe = pounce_presolve::incidence::ProbeView {
n_vars: prob.n,
m_rows: prob.m,
jac_irow: &jac_irow,
jac_jcol: &jac_jcol,
jac_values: None,
g_l: &prob.g_l,
g_u: &prob.g_u,
linearity: None,
one_based: false,
eq_tol: 1e-12,
excluded_vars: None,
excluded_rows: None,
};
let inc = pounce_presolve::incidence::EqualityIncidence::from_probe(&probe);
let structure = pounce_cli::debug_repl::StructureBook::new(
inc,
prob.con_names.clone(),
prob.var_names.clone(),
);
let mut h = hook.borrow_mut();
h.set_equation_book(book);
h.set_structure_book(structure);
}
nl_class = Some(pounce_cli::dispatch::classify_problem(&prob));
let nl_rc = Rc::new(RefCell::new(nl_reader::NlTnlp::new(prob)));
if curvature_scaling_requested(&app)
&& !nl_rc.borrow_mut().enable_curvature_scaling()
{
eprintln!(
"pounce: nlp_scaling_method=curvature-based needs \
every row and the objective to be degree <= 2 (it \
scales a model by its quadratic coefficients, and \
a genuine nonlinearity has none). This model has \
at least one row it cannot read that way. Use \
gradient-based, or user-scaling with your own \
scaling_factor suffixes."
);
return ExitCode::from(2);
}
nl_curvature_read_curvature = nl_rc.borrow().curvature_scaling_read_curvature();
nl_expr_provider = Some(Rc::clone(&nl_rc)
as Rc<RefCell<dyn pounce_nlp::expression_provider::ExpressionProvider>>);
let t: Rc<RefCell<dyn TNLP>> = nl_rc;
if let Some(info) = t.borrow_mut().get_nlp_info() {
if !json_dbg {
println!(
"Parsed {} vars, {} cons, jac_nnz={}, h_nnz={} in {:.2}s",
info.n, info.m, info.nnz_jac_g, info.nnz_h_lag, elapsed
);
}
}
t
}
Err(e) => {
eprintln!("pounce: failed to read {}: {e}", path.display());
return ExitCode::from(2);
}
}
}
};
let declares_sens_suffixes = nl_suffixes
.as_ref()
.map(sens::is_sensitivity_input)
.unwrap_or(false);
let wants_sens = declares_sens_suffixes && !sens_options.suppresses_sens_step();
let wants_red_hessian = args.compute_red_hessian || sens_options.wants_reduced_hessian();
let wants_nlp_postopt = wants_sens || wants_red_hessian;
let wants_user_scaling = app
.options()
.get_string_value("nlp_scaling_method", "")
.ok()
.and_then(|(v, set)| set.then_some(v))
.is_some_and(|v| v == "user-scaling")
&& nl_suffixes.as_ref().is_some_and(|s| {
s.obj_real.contains_key("scaling_factor")
|| s.con_real.contains_key("scaling_factor")
|| s.var_real.contains_key("scaling_factor")
});
let wants_curvature_scaling = curvature_scaling_requested(&app);
let negative_obj_scaling_option = app
.options()
.get_numeric_value("obj_scaling_factor", "")
.ok()
.and_then(|(v, set)| set.then_some(v))
.is_some_and(|v| v < 0.0);
let negative_obj_scaling_suffix = wants_user_scaling
&& nl_suffixes.as_ref().is_some_and(|s| {
s.obj_real
.get("scaling_factor")
.and_then(|v| v.first())
.is_some_and(|&f| f < 0.0)
});
let maximize_via_obj_scaling = negative_obj_scaling_option || negative_obj_scaling_suffix;
let postopt_what = match (wants_sens, wants_red_hessian) {
(true, true) => {
"a parametric sensitivity step (sIPOPT sens_* suffixes) and a \
reduced-Hessian computation"
}
(true, false) => "a parametric sensitivity step (sIPOPT sens_* suffixes)",
_ => "a reduced-Hessian computation",
};
if let Some(mcfg) = &args.minima {
if wants_nlp_postopt {
eprintln!(
"pounce: warning: the .nl requests {postopt_what}, but --minima \
runs a multistart search that does not compute it; the request \
will be skipped. Run without --minima to obtain it."
);
}
app.set_presolve_already_applied(true);
return pounce_cli::minima::run(&mut app, &inner_tnlp, mcfg, &args, sol_path.as_deref());
}
{
use pounce_cli::dispatch::{ProblemClass, SolverChoice, SolverSelection, resolve_solver};
let sel_str = app
.options()
.get_string_value("solver_selection", "")
.map(|(v, _)| v)
.unwrap_or_else(|_| "auto".to_string());
let selection = match SolverSelection::parse(&sel_str) {
Some(s) => s,
None => {
eprintln!(
"pounce: invalid solver_selection '{sel_str}'; valid values: {}",
SolverSelection::VALUES.join(", ")
);
return ExitCode::from(2);
}
};
let class = match &args.problem {
ProblemSource::NlFile(_) => nl_class.unwrap_or(ProblemClass::Nlp),
ProblemSource::Builtin(_) => ProblemClass::Nlp,
};
let choice = match resolve_solver(class, selection) {
Ok(c) => c,
Err(msg) => {
eprintln!("pounce: {msg}");
return ExitCode::from(2);
}
};
let convex_can_serve_sens = wants_sens
&& !wants_red_hessian
&& matches!(choice, SolverChoice::LpIpm | SolverChoice::QpIpm);
let decline_convex_for_postopt = wants_nlp_postopt
&& matches!(selection, SolverSelection::Auto)
&& !convex_can_serve_sens;
let decline_convex_for_user_scaling =
wants_user_scaling && matches!(selection, SolverSelection::Auto);
let decline_convex_for_curvature_scaling = wants_curvature_scaling
&& nl_curvature_read_curvature
&& matches!(selection, SolverSelection::Auto);
let decline_convex_for_obj_scaling =
maximize_via_obj_scaling && matches!(selection, SolverSelection::Auto);
let decline_convex = decline_convex_for_postopt
|| decline_convex_for_user_scaling
|| decline_convex_for_curvature_scaling
|| decline_convex_for_obj_scaling;
let socp_nlp_fallback = matches!(selection, SolverSelection::Auto);
let lp_nlp_fallback = matches!(selection, SolverSelection::Auto)
&& debug_hook.is_none()
&& !max_iter_explicitly_set(&app);
if !suppress_banner && !json_dbg {
let described = if decline_convex {
SolverChoice::Nlp.describe()
} else {
choice.describe()
};
println!(
"Problem class: {}. Selected solver: {} [solver_selection={}].",
class.name(),
described,
sel_str
);
println!();
}
if matches!(
choice,
SolverChoice::LpIpm
| SolverChoice::QpIpm
| SolverChoice::SocpIpm
| SolverChoice::QpActiveSet
) {
app.run_derivative_test(&inner_tnlp);
for warning in app.convex_unexploited_hint_warnings() {
eprintln!("{warning}");
}
if maximize_via_obj_scaling && !decline_convex_for_obj_scaling {
eprintln!(
"pounce: the objective scaling is negative (maximize) — via \
obj_scaling_factor or the .nl's `scaling_factor` suffix — \
but solver_selection={sel_str} forces the convex solver \
(pounce-convex), which minimizes and does not read that \
option — it would report the minimizer of the objective \
you asked to maximize. Use solver_selection=nlp or auto \
(which routes here automatically), or negate the \
objective in the model and drop obj_scaling_factor."
);
return ExitCode::from(2);
}
if wants_nlp_postopt {
if decline_convex_for_postopt {
eprintln!(
"pounce: note: this problem classifies as {} but the .nl \
requests {postopt_what}, which the convex solver \
(pounce-convex) does not provide; routing to the general \
NLP interior-point path so the request is honored.",
class.name()
);
} else if convex_can_serve_sens {
eprintln!(
"pounce: note: the .nl requests {postopt_what}; the convex \
solver (pounce-convex) computes it directly on this {} — \
no reroute to the general NLP path is needed.",
class.name()
);
} else {
eprintln!(
"pounce: warning: the .nl requests {postopt_what}, but \
solver_selection={sel_str} forces the convex solver \
(pounce-convex), which does not compute it; the request \
will be skipped. Use solver_selection=nlp or auto to \
obtain it."
);
}
}
if wants_user_scaling {
if decline_convex_for_user_scaling {
eprintln!(
"pounce: note: this problem classifies as {} but \
nlp_scaling_method=user-scaling asks for the .nl's \
`scaling_factor` suffixes to be applied, which the \
convex solver (pounce-convex) does not do; routing to \
the general NLP interior-point path so the scaling is \
honored.",
class.name()
);
} else {
eprintln!(
"pounce: warning: nlp_scaling_method=user-scaling asks \
for the .nl's `scaling_factor` suffixes to be applied, \
but solver_selection={sel_str} forces the convex solver \
(pounce-convex), which equilibrates internally and does \
not read them; the requested scaling will be skipped. \
Use solver_selection=nlp or auto to apply it."
);
}
}
if wants_curvature_scaling {
if decline_convex_for_curvature_scaling {
eprintln!(
"pounce: note: this problem classifies as {} but \
nlp_scaling_method=curvature-based asks for factors \
derived from the model's quadratic coefficients, which \
the convex solver (pounce-convex) does not read; \
routing to the general NLP interior-point path so the \
scaling is honored.",
class.name()
);
} else if !nl_curvature_read_curvature {
eprintln!(
"pounce: note: nlp_scaling_method=curvature-based was \
accepted, but every quadratic coefficient in this \
model is zero, so the scheme reduces to Ruiz \
equilibration of the linear rows; the convex solver \
(pounce-convex) equilibrates internally and keeps the \
fast path. Use solver_selection=nlp to run the scheme \
on the general path anyway."
);
} else {
eprintln!(
"pounce: warning: nlp_scaling_method=curvature-based \
asks for factors derived from the model's quadratic \
coefficients, but solver_selection={sel_str} forces \
the convex solver (pounce-convex), which equilibrates \
internally and does not read them; the requested \
scaling will be skipped. Use solver_selection=nlp or \
auto to apply it."
);
}
}
if decline_convex_for_obj_scaling {
eprintln!(
"pounce: note: this problem classifies as {} but \
obj_scaling_factor is negative (maximize), which the \
convex solver (pounce-convex) cannot express; routing to \
the general NLP interior-point path so the objective \
sense is honored.",
class.name()
);
}
if decline_convex {
} else if let ProblemSource::NlFile(path) = &args.problem {
let prob = match nl_reader::read_nl_file(path) {
Ok(p) => p,
Err(e) => {
eprintln!(
"pounce: failed to re-read {} for the convex solver: {e}",
path.display()
);
return ExitCode::from(2);
}
};
let json_cfg = args.json_output.as_deref().map(|p| {
let input = InputDescriptor::NlFile {
path: path.clone(),
size_bytes: std::fs::metadata(path).ok().map(|m| m.len()),
};
(p, args.json_detail, input)
});
let convex_opts =
match pounce_convex::QpOptions::try_from_options_list(app.options()) {
Ok(options) => options,
Err(error) => {
eprintln!("pounce: convex option setup failed: {error}");
return ExitCode::from(2);
}
};
let bound_relax = convex_bound_relax(&app);
let convex_t0 = std::time::Instant::now();
let presolve_on = match pounce_convex::ConvexPresolveOptions::try_from_options_list(
app.options(),
) {
Ok(options) => options.enabled,
Err(error) => {
eprintln!("pounce: convex presolve setup failed: {error}");
return ExitCode::from(2);
}
};
if matches!(choice, SolverChoice::SocpIpm) {
if let Some(code) = run_convex_socp(
&prob,
class,
sol_path.as_deref(),
json_cfg,
debug_hook.as_ref(),
args.ampl,
convex_opts,
bound_relax,
presolve_on,
socp_nlp_fallback,
convex_console(&app, json_dbg),
) {
return code;
}
} else {
let engine_overrides =
match pounce_convex::ActiveSetOverrides::try_from_options_list(
app.options(),
) {
Ok(options) => options,
Err(error) => {
eprintln!("pounce: active-set option setup failed: {error}");
return ExitCode::from(2);
}
};
if matches!(choice, SolverChoice::QpActiveSet) && debug_hook.is_some() {
eprintln!(
"pounce: note: the interactive debugger is IPM-only and does \
not engage on the active-set QP engine (solver_selection=\
qp-active-set); the solve runs without pausing. Use \
solver_selection=qp-ipm to debug a convex QP interactively."
);
}
if let Some(code) = run_convex_qp(
&prob,
class,
sol_path.as_deref(),
presolve_on && !convex_can_serve_sens,
json_cfg,
debug_hook.as_ref(),
args.ampl,
convex_opts,
bound_relax,
matches!(choice, SolverChoice::QpActiveSet),
engine_overrides,
lp_nlp_fallback,
convex_console(&app, json_dbg),
convex_can_serve_sens
.then(|| nl_suffixes.as_ref())
.flatten(),
matches!(selection, SolverSelection::Auto),
) {
return code;
}
}
charge_wall_budget(app.options_mut(), convex_t0.elapsed());
if !matches!(
app.options().get_numeric_value("bound_relax_factor", ""),
Ok((_, true))
) {
let _ =
app.options_mut()
.set_numeric_value("bound_relax_factor", 0.0, true, true);
}
}
}
let _ = choice;
}
let sens_active = wants_sens;
if sens_options.run_sens == Some(true) && !declares_sens_suffixes {
eprintln!(
"pounce: warning: `run_sens=yes` asks for a parametric sensitivity \
step, but the input declares none of the sIPOPT suffixes \
(sens_state_1, sens_state_value_1, sens_init_constr) that say \
which parameter to perturb; no step will be computed."
);
}
if sens_options.suppresses_sens_step() && declares_sens_suffixes {
eprintln!(
"pounce: `run_sens=no` — solving without the parametric \
sensitivity step the input's sIPOPT suffixes ask for."
);
}
let nominal_capture: Rc<
RefCell<
Option<(
Vec<pounce_common::types::Number>,
Vec<pounce_common::types::Number>,
)>,
>,
> = Rc::new(RefCell::new(None));
let sens_capture: Rc<RefCell<Option<Vec<pounce_common::types::Number>>>> =
Rc::new(RefCell::new(None));
let bound_mult_capture: Rc<
RefCell<
Option<(
Vec<pounce_common::types::Number>,
Vec<pounce_common::types::Number>,
)>,
>,
> = Rc::new(RefCell::new(None));
let red_hessian_capture: Rc<RefCell<Option<sens::RedHessianResult>>> =
Rc::new(RefCell::new(None));
if args.json_output.is_some() || sol_path.is_some() || sens_active || wants_red_hessian {
let cap = Rc::clone(&nominal_capture);
let sens_cap = Rc::clone(&sens_capture);
let bmult_cap = Rc::clone(&bound_mult_capture);
let rh_cap = Rc::clone(&red_hessian_capture);
let suffixes_cb = nl_suffixes.clone();
let dims_cb = nl_dims;
let compute_rh = wants_red_hessian;
let rh_eigen = args.rh_eigendecomp || sens_options.wants_eigendecomp();
let boundcheck_eps = {
let on = args.sens_boundcheck || sens_options.sens_boundcheck == Some(true);
let eps = if args.sens_bound_eps_explicit {
args.sens_bound_eps
} else {
sens_options
.sens_bound_eps
.unwrap_or(pounce_sensitivity::DEFAULT_SENS_BOUND_EPS)
};
on.then_some(eps)
};
let release_eps = pounce_sensitivity::release_floor_from_options(app.options());
let sens_opts_cb = sens_options;
app.set_on_converged(Box::new(move |data, cq, nlp, pd| {
let curr = match data.borrow().curr.clone() {
Some(c) => c,
None => return,
};
let x_iterate = nlp.borrow().lift_x_to_full(&*curr.x);
let x = nlp.borrow().finalize_solution_x(&*curr.x);
let mut lambda = nlp
.borrow()
.finalize_solution_lambda(&*curr.y_c, &*curr.y_d);
if lambda.is_empty() {
let n_c = curr.y_c.dim() as usize;
let n_d = curr.y_d.dim() as usize;
lambda = Vec::with_capacity(n_c + n_d);
if let Some(dv) = curr
.y_c
.as_any()
.downcast_ref::<pounce_linalg::dense_vector::DenseVector>()
{
lambda.extend_from_slice(&dv.expanded_values());
} else {
lambda.extend(std::iter::repeat(0.0).take(n_c));
}
if let Some(dv) = curr
.y_d
.as_any()
.downcast_ref::<pounce_linalg::dense_vector::DenseVector>()
{
lambda.extend_from_slice(&dv.expanded_values());
} else {
lambda.extend(std::iter::repeat(0.0).take(n_d));
}
}
*cap.borrow_mut() = Some((x.clone(), lambda));
let z_l_full = nlp.borrow().finalize_solution_z_l(&*curr.z_l);
let z_u_full = nlp.borrow().finalize_solution_z_u(&*curr.z_u);
if !z_l_full.is_empty() || !z_u_full.is_empty() {
*bmult_cap.borrow_mut() = Some((z_l_full, z_u_full));
}
if let Some(suffixes) = &suffixes_cb {
let (n_full, m_full) = dims_cb.unwrap_or((x.len(), 0));
if sens_active {
if let Some(xp) = sens::compute_sens_perturbed_x(
data,
cq,
nlp,
Rc::clone(&pd),
suffixes,
n_full,
m_full,
&x_iterate,
boundcheck_eps,
release_eps,
&sens_opts_cb,
) {
*sens_cap.borrow_mut() = Some(xp);
}
}
if compute_rh {
match sens::try_compute_red_hessian(
data,
cq,
nlp,
Rc::clone(&pd),
suffixes,
rh_eigen,
&sens_opts_cb,
) {
Some(r) => *rh_cap.borrow_mut() = Some(r),
None => eprintln!(
"pounce: --compute-red-hessian requested but the `red_hessian` \
suffix is missing or empty in the input .nl"
),
}
}
}
}));
}
let mut presolve_opts = match pounce_presolve::PresolveOptions::from_options_list(app.options())
{
Ok(o) => o,
Err(e) => {
eprintln!("pounce: presolve setup failed: {e}");
return ExitCode::from(2);
}
};
if (sens_active || wants_red_hessian) && presolve_opts.enabled {
eprintln!(
"pounce: disabling presolve — sensitivity / reduced-Hessian post-processing \
operates on the original (un-presolved) KKT system"
);
presolve_opts.enabled = false;
}
let presolve_handle = if presolve_opts.enabled {
let p = Rc::new(RefCell::new(match &nl_expr_provider {
Some(ep) => pounce_presolve::PresolveTnlp::with_expression_provider(
Rc::clone(&inner_tnlp),
Rc::clone(ep),
presolve_opts,
),
None => pounce_presolve::PresolveTnlp::new(Rc::clone(&inner_tnlp), presolve_opts),
}));
let _ = p.borrow_mut().get_nlp_info();
{
let h = p.borrow();
let tr = h.tighten_report();
let dropped = h.n_dropped_rows();
let licq = h
.licq_verdict()
.map(|v| format!("{v:?}"))
.unwrap_or_else(|| "off".into());
if !json_dbg {
println!(
"Presolve: tightened {} bounds ({} newly-finite), dropped {} redundant rows, LICQ={}",
tr.n_tightened, tr.n_new_finite, dropped, licq
);
}
if let Some(fr) = h.fbbt_report() {
if !json_dbg {
println!(
"Presolve FBBT: {} sweeps, {} variable tightenings (Σ|Δ|={:.3e})",
fr.iterations, fr.bound_updates, fr.total_tightening
);
}
if let Some(witness) = fr.infeasibility_witness {
eprintln!("pounce: FBBT detected infeasibility (witness constraint {witness})");
}
}
}
Some(p)
} else {
None
};
let elim_handle = match (&presolve_handle, presolve_opts.linear_eq_reduction) {
(Some(p), true) => {
let e = Rc::new(RefCell::new(pounce_presolve::LinearEqElimTnlp::new(
Rc::clone(p) as Rc<RefCell<dyn TNLP>>,
presolve_opts,
)));
let _ = e.borrow_mut().get_nlp_info();
if !json_dbg {
let h = e.borrow();
let r = h.report();
println!(
"Presolve linear-equality reduction: eliminated {} columns \
({} pinned, {} aggregated), dropped {} rows ({} redundant)",
h.n_eliminated_vars(),
r.n_constant_vars,
r.n_aggregated_vars,
h.n_eliminated_rows(),
r.n_redundant_rows,
);
}
Some(e)
}
_ => None,
};
let post_presolve: Rc<RefCell<dyn TNLP>> = match (&elim_handle, &presolve_handle) {
(Some(e), _) => Rc::clone(e) as Rc<RefCell<dyn TNLP>>,
(None, Some(p)) => Rc::clone(p) as Rc<RefCell<dyn TNLP>>,
(None, None) => Rc::clone(&inner_tnlp),
};
app.set_presolve_already_applied(true);
let post_presolve: Rc<RefCell<dyn TNLP>> =
if pounce_cli::no_hessian_tnlp::NoHessianTnlp::requested() {
Rc::new(RefCell::new(
pounce_cli::no_hessian_tnlp::NoHessianTnlp::new(Rc::clone(&post_presolve)),
))
} else {
Rc::clone(&post_presolve)
};
let counting = Rc::new(RefCell::new(CountingTnlp::new(Rc::clone(&post_presolve))));
let tnlp: Rc<RefCell<dyn TNLP>> = Rc::clone(&counting) as Rc<RefCell<dyn TNLP>>;
let diagnostics_handle = match build_diagnostics(
&args.dump_specs,
args.dump_dir.as_ref(),
args.dump_format.as_deref(),
) {
Ok(d) => d,
Err(msg) => {
eprintln!("pounce: {msg}");
return ExitCode::from(2);
}
};
if let Some(diag) = diagnostics_handle.as_ref() {
if !json_dbg {
println!(
"Diagnostics: dumping to {} ({} categor{} configured)",
diag.dump_dir().display(),
diag.config.categories.len(),
if diag.config.categories.len() == 1 {
"y"
} else {
"ies"
},
);
}
app.set_diagnostics(Rc::clone(diag));
}
let nlp_info_snapshot = tnlp.borrow_mut().get_nlp_info();
let mut solve_tnlp: Rc<RefCell<dyn TNLP>> = Rc::clone(&tnlp);
let mut status = loop {
let st = app.optimize_tnlp(Rc::clone(&solve_tnlp));
let req = restart_cell.borrow_mut().take();
let Some(req) = req else { break st };
for (k, v) in &req.options {
if let Err(e) = app.options_mut().read_from_str(&format!("{k} {v}\n"), true) {
eprintln!("pounce: re-solve could not set {k}={v}: {e}");
}
}
if let Some(snap) = req.warm {
let mu = snap.mu();
app.set_warm_start_iterate(snap);
let _ = app
.options_mut()
.read_from_str("warm_start_init_point yes\n", true);
if mu.is_finite() && mu > 0.0 {
let _ = app
.options_mut()
.read_from_str(&format!("warm_start_target_mu {mu}\n"), true);
}
}
solve_tnlp = Rc::new(RefCell::new(pounce_cli::seeded_tnlp::SeededTnlp::new(
Rc::clone(&tnlp),
req.seed_x,
)));
if let Some(hook) = debug_hook.as_ref() {
app.set_debug_hook(hook.clone());
}
eprintln!(
"pounce: re-solving from saved point with {} option override(s)…",
req.options.len()
);
};
let mut solve_stats = app.statistics();
let presolve_certified = presolve_handle
.as_ref()
.and_then(|p| p.borrow().certified_infeasible());
let second_opinion = if debug_hook.is_none() && presolve_certified.is_none() {
let narrate = pounce_algorithm::second_opinion::narration_is_wanted(app.options());
let outcome = run_second_opinion_ladder(
&mut app,
Rc::clone(&tnlp),
status,
solve_stats.clone(),
&mut |line| {
if narrate {
eprintln!("{line}");
}
},
);
status = outcome.status;
solve_stats = outcome.statistics.clone();
outcome
} else {
SecondOpinionOutcome::unchanged(status, solve_stats.clone())
};
if second_opinion.ran()
&& second_opinion.promoted_by.is_none()
&& app
.options()
.get_integer_value("print_level", "")
.map(|(v, _found)| v >= 1)
.unwrap_or(true)
{
println!();
println!("EXIT: {}", print::status_message(status));
println!();
println!(
"POUNCE {}: {}",
env!("CARGO_PKG_VERSION"),
print::status_message(status)
);
}
if matches!(
status,
ApplicationReturnStatus::InvalidNumberDetected
| ApplicationReturnStatus::InfeasibleProblemDetected
) && app
.options()
.get_integer_value("print_level", "")
.map(|(v, _found)| v >= 1)
.unwrap_or(true)
&& let Some(diagnosis) = pounce_nlp::degeneracy::diagnose_start_point(&inner_tnlp, 6)
{
if let Some(what) = diagnosis.audit.describe() {
eprintln!("pounce: the model is not finite at its own starting point: {what}.");
}
if presolve_certified.is_none()
&& status == ApplicationReturnStatus::InfeasibleProblemDetected
&& let Some(jac) = diagnosis.jacobian.as_ref()
&& let Some(what) = jac.describe(6)
{
eprintln!("pounce: the constraint Jacobian is rank-deficient there: {what}.");
eprintln!(
"pounce: LICQ fails at a point like that, so a local-infeasibility verdict \
reached from it is as much a statement about the starting point as about the \
problem. Try a different starting point, or `start_point_perturbation 1e-2`."
);
}
}
if !json_dbg
&& app
.options()
.get_integer_value("print_level", "")
.map(|(v, _found)| v >= 1)
.unwrap_or(true)
{
println!("Status: {}", status.upstream_name());
}
let counters = counting.borrow();
if json_dbg {
let ev = serde_json::json!({
"event": "terminated",
"status": format!("{status:?}"),
"status_message": print::status_message(status),
"iterations": solve_stats.iteration_count,
"objective": solve_stats.final_objective,
"evals": {
"obj": counters.n_obj.get(),
"obj_grad": counters.n_grad_f.get(),
"constr": counters.n_g.get(),
"constr_jac": counters.n_jac_g.get(),
"hess": counters.n_h.get(),
},
});
println!("{ev}");
}
drop(counters);
if nominal_capture.borrow().is_none() {
if let Some(xl) = counting.borrow().captured_solution() {
*nominal_capture.borrow_mut() = Some(xl);
}
}
let capture_is_already_in_model_units = app.answer_restored_from_floor();
if capture_is_already_in_model_units {
if let Some(xl) = counting.borrow().captured_solution() {
*nominal_capture.borrow_mut() = Some(xl);
}
if let Some((z_l, z_u)) = counting.borrow().captured_bound_mults() {
if !z_l.is_empty() || !z_u.is_empty() {
*bound_mult_capture.borrow_mut() = Some((z_l, z_u));
}
}
}
let elim_reduced = elim_handle
.as_ref()
.map(|e| {
let h = e.borrow();
h.n_eliminated_vars() > 0 || h.n_eliminated_rows() > 0
})
.unwrap_or(false);
if let Some(p) = &presolve_handle {
let lifted = if p.borrow().n_dropped_rows() > 0 || elim_reduced {
p.borrow().finalized_full_solution()
} else {
None
};
if let Some((x_full, lam_full)) = lifted {
if let Some((x, lambda)) = nominal_capture.borrow_mut().as_mut() {
*lambda = lam_full;
if elim_reduced {
*x = x_full;
}
}
}
}
if let Some(d) = app
.variable_scaling()
.filter(|_| !capture_is_already_in_model_units)
{
if let Some((x, _lambda)) = nominal_capture.borrow_mut().as_mut() {
assert_eq!(
x.len(),
d.len(),
"scaling: captured {} variables but {} factors",
x.len(),
d.len()
);
for (xi, s) in x.iter_mut().zip(d.iter()) {
*xi /= s;
}
}
if let Some((z_l, z_u)) = bound_mult_capture.borrow_mut().as_mut() {
assert_eq!(
z_l.len(),
d.len(),
"scaling: captured {} bound multipliers but {} factors",
z_l.len(),
d.len()
);
assert_eq!(z_l.len(), z_u.len(), "z_L and z_U must be the same length");
for ((l, u), s) in z_l.iter_mut().zip(z_u.iter_mut()).zip(d.iter()) {
*l *= s;
*u *= s;
}
}
}
if elim_reduced {
if let Some(full) = elim_handle
.as_ref()
.and_then(|e| e.borrow().finalized_full_solution().cloned())
{
if let Some((z_l, z_u)) = bound_mult_capture.borrow_mut().as_mut() {
if z_l.len() != full.z_l.len() {
*z_l = full.z_l;
*z_u = full.z_u;
}
}
}
}
if let Some(rh) = red_hessian_capture.borrow().as_ref() {
sens::print_red_hessian_to_stderr(rh);
} else if wants_red_hessian {
eprintln!(
"pounce: --compute-red-hessian requested but the reduced Hessian \
was not produced (see warnings above)."
);
}
let mut sol_suffixes: Vec<nl_writer::SolSuffix> = Vec::new();
if let Some(xp) = sens_capture.borrow().clone() {
sol_suffixes.push(nl_writer::SolSuffix {
name: "sens_sol_state_1".to_string(),
target: nl_writer::SolSuffixTarget::Var,
values: nl_writer::SolSuffixValues::Real(xp),
});
}
if let Some((z_l_full, z_u_full)) = bound_mult_capture.borrow().clone() {
let z_u_neg: Vec<pounce_common::types::Number> = z_u_full.iter().map(|&z| -z).collect();
sol_suffixes.push(nl_writer::SolSuffix {
name: "ipopt_zL_out".to_string(),
target: nl_writer::SolSuffixTarget::Var,
values: nl_writer::SolSuffixValues::Real(z_l_full),
});
sol_suffixes.push(nl_writer::SolSuffix {
name: "ipopt_zU_out".to_string(),
target: nl_writer::SolSuffixTarget::Var,
values: nl_writer::SolSuffixValues::Real(z_u_neg),
});
}
if let Some(json_path) = &args.json_output {
let input = match &args.problem {
ProblemSource::Builtin(name) => InputDescriptor::Builtin { name: name.clone() },
ProblemSource::NlFile(p) => InputDescriptor::NlFile {
path: p.clone(),
size_bytes: std::fs::metadata(p).ok().map(|m| m.len()),
},
};
let mut builder = ReportBuilder::new(args.json_detail, input);
if let Some(info) = nlp_info_snapshot {
builder.problem.n_variables = info.n;
let n_dropped = presolve_handle
.as_ref()
.map(|p| p.borrow().n_dropped_rows())
.unwrap_or(0);
builder.problem.n_constraints = info.m + n_dropped;
builder.problem.n_objectives = 1; builder.problem.nnz_jac_g = Some(info.nnz_jac_g);
builder.problem.nnz_h_lag = Some(info.nnz_h_lag);
}
builder.solution.engine = "nlp".to_string();
builder.solution.status = status;
builder.solution.solve_result_num = presolve_verdict(presolve_certified, status).1;
builder.solution.objective = solve_stats.final_objective;
if let Some((x, lambda)) = nominal_capture.borrow().clone() {
builder.solution.x = x;
builder.solution.lambda = lambda;
}
builder.ingest_stats(&solve_stats);
if let Some(linsol) = app.linear_solver_summary() {
builder.set_linear_solver_summary(linsol);
}
if second_opinion.ran() {
builder.set_second_opinion(pounce_solve_report::SecondOpinionInfo {
tried: second_opinion.tried.iter().map(|s| s.to_string()).collect(),
promoted_by: second_opinion.promoted_by.map(|s| s.to_string()),
base_status: second_opinion.base_status.upstream_name().to_string(),
base_iteration_count: second_opinion.base_iteration_count,
rung_iteration_counts: second_opinion.rung_iteration_counts.clone(),
total_iteration_count: second_opinion.total_iteration_count(),
});
}
if matches!(args.json_detail, ReportDetail::Full) {
for s in &sol_suffixes {
builder
.solution
.suffixes
.push(sens::sol_suffix_to_report(s));
}
if let Some(rh) = red_hessian_capture.borrow().as_ref() {
builder.solution.suffixes.push(SolutionSuffix {
name: "_red_hessian".to_string(),
target: "problem".to_string(),
kind: "real".to_string(),
values: rh.hr.clone(),
int_values: Vec::new(),
});
builder.solution.suffixes.push(SolutionSuffix {
name: "_red_hessian_vars".to_string(),
target: "problem".to_string(),
kind: "int".to_string(),
values: Vec::new(),
int_values: rh.var_indices.iter().map(|&v| v as i32).collect(),
});
if let Some(w) = &rh.eigenvalues {
builder.solution.suffixes.push(SolutionSuffix {
name: "_red_hessian_eigenvalues".to_string(),
target: "problem".to_string(),
kind: "real".to_string(),
values: w.clone(),
int_values: Vec::new(),
});
}
if let Some(v) = &rh.eigenvectors {
builder.solution.suffixes.push(SolutionSuffix {
name: "_red_hessian_eigenvectors".to_string(),
target: "problem".to_string(),
kind: "real".to_string(),
values: v.clone(),
int_values: Vec::new(),
});
}
}
}
let report = builder.finish();
if let Err(e) = write_report_file(json_path, &report) {
eprintln!(
"pounce: failed to write JSON report to {}: {e}",
json_path.display()
);
} else {
eprintln!("pounce: wrote {}", json_path.display());
}
}
if let Some(sol_path) = &sol_path {
let (n, m_out) = nlp_info_snapshot
.as_ref()
.map(|i| (i.n as usize, i.m as usize))
.unwrap_or((0, 0));
let m = m_out
+ presolve_handle
.as_ref()
.map(|p| p.borrow().n_dropped_rows() as usize)
.unwrap_or(0);
let (x, lambda) = nominal_capture
.borrow()
.clone()
.unwrap_or_else(|| (vec![0.0; n], vec![0.0; m]));
let (message, srn) = presolve_verdict(presolve_certified, status);
let payload = nl_writer::SolutionFile {
message: &message,
x: &x,
mult_g: &lambda,
solve_result_num: srn,
suffixes: &sol_suffixes,
};
match nl_writer::write_sol_file_with_options(sol_path, &payload, &nl_ampl_options) {
Ok(_) => eprintln!("pounce: wrote {}", sol_path.display()),
Err(e) => eprintln!("pounce: failed to write {}: {e}", sol_path.display()),
}
}
if let Some(diag) = diagnostics_handle.as_ref() {
write_diagnostics_manifest(diag, &problem_desc, status);
write_diagnostics_timing(diag, &app);
}
nlp_exit_code(status, args.ampl)
}
fn nlp_exit_code(status: ApplicationReturnStatus, ampl: bool) -> ExitCode {
if nlp_solve_succeeded(status) || ampl {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
fn nlp_solve_succeeded(status: ApplicationReturnStatus) -> bool {
matches!(
status,
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel
| ApplicationReturnStatus::FeasiblePointFound
)
}
fn build_debugger(
mode: pounce_cli::cli::DebugMode,
on_error: bool,
on_interrupt: bool,
script: Option<&std::path::Path>,
reg: Option<Rc<pounce_common::reg_options::RegisteredOptions>>,
cell: pounce_cli::debug_repl::RestartCell,
) -> pounce_cli::debug_repl::SolverDebugger {
use pounce_cli::debug_repl::SolverDebugger;
let dbg = if on_error {
SolverDebugger::on_error(mode, reg)
} else if on_interrupt {
SolverDebugger::on_interrupt(mode, reg)
} else {
SolverDebugger::new(mode, reg)
}
.with_restart(cell);
match script {
Some(p) => dbg.with_script(p.to_string_lossy().into_owned()),
None => dbg,
}
}
fn lp_declines_to_nlp(
class: pounce_cli::dispatch::ProblemClass,
status: pounce_convex::QpStatus,
allow_nlp_fallback: bool,
) -> bool {
use pounce_convex::QpStatus;
allow_nlp_fallback
&& matches!(
class,
pounce_cli::dispatch::ProblemClass::Lp
| pounce_cli::dispatch::ProblemClass::ConvexQp
)
&& matches!(
status,
QpStatus::OptimalInaccurate | QpStatus::IterationLimit | QpStatus::NumericalFailure
)
}
fn max_iter_explicitly_set(app: &IpoptApplication) -> bool {
matches!(
app.options().get_integer_value("max_iter", ""),
Ok((_, true))
)
}
fn qp_status_to_ars(s: pounce_convex::QpStatus) -> ApplicationReturnStatus {
use pounce_convex::QpStatus;
match s {
QpStatus::Optimal => ApplicationReturnStatus::SolveSucceeded,
QpStatus::OptimalInaccurate => ApplicationReturnStatus::SolvedToAcceptableLevel,
QpStatus::PrimalInfeasible => ApplicationReturnStatus::InfeasibleProblemDetected,
QpStatus::DualInfeasible => ApplicationReturnStatus::DivergingIterates, QpStatus::IterationLimit => ApplicationReturnStatus::MaximumIterationsExceeded,
QpStatus::TimeLimit => ApplicationReturnStatus::MaximumWallTimeExceeded,
QpStatus::NumericalFailure => ApplicationReturnStatus::InternalError,
}
}
fn convex_status_report(s: pounce_convex::QpStatus) -> (&'static str, bool, i32) {
use pounce_convex::QpStatus;
match s {
QpStatus::Optimal => ("Optimal Solution Found.", true, 0),
QpStatus::OptimalInaccurate => ("Solved to acceptable level (reduced accuracy).", true, 1),
QpStatus::PrimalInfeasible => ("Problem is primal infeasible.", false, 200),
QpStatus::DualInfeasible => ("Problem is unbounded (dual infeasible).", false, 300),
QpStatus::IterationLimit => ("Maximum iterations exceeded.", false, 400),
QpStatus::TimeLimit => ("Maximum wallclock time exceeded.", false, 400),
QpStatus::NumericalFailure => ("Numerical failure (no verified KKT point).", false, 500),
}
}
fn convex_bound_relax(app: &IpoptApplication) -> pounce_cli::qp_extract::BoundRelax {
let opt = app.options();
let set_value = |name: &str| {
opt.get_numeric_value(name, "")
.ok()
.and_then(|(v, set)| set.then_some(v))
};
match set_value("bound_relax_factor") {
None => pounce_cli::qp_extract::BoundRelax::NONE,
Some(factor) => pounce_cli::qp_extract::BoundRelax {
factor,
cap: set_value("constr_viol_tol").unwrap_or(1e-4),
},
}
}
#[derive(Debug, Clone, Copy, Default)]
struct ConvexConsole {
verdict: bool,
json_debug: bool,
collect_timing: bool,
print_timing: bool,
}
fn convex_console(app: &IpoptApplication, json_dbg: bool) -> ConvexConsole {
let opt = app.options();
let yes = |name: &str| {
opt.get_bool_value(name, "")
.ok()
.and_then(|(v, found)| found.then_some(v))
.unwrap_or(false)
};
let print_timing = yes("print_timing_statistics");
ConvexConsole {
verdict: opt
.get_integer_value("print_level", "")
.map(|(v, _found)| v >= 1)
.unwrap_or(true),
json_debug: json_dbg,
collect_timing: print_timing || yes("timing_statistics"),
print_timing,
}
}
fn print_convex_verdict(
console: ConvexConsole,
status: pounce_convex::QpStatus,
timing: &pounce_common::timing::ConvexTimingStatistics,
) {
if !console.verdict {
return;
}
if console.print_timing {
print!("{}", timing.report());
}
let ars = qp_status_to_ars(status);
print::print_convex_end(ars, timing.overall_alg.total_wallclock_time());
if !console.json_debug {
println!("Status: {}", ars.upstream_name());
}
}
fn convex_opts_with_remaining(
mut opts: pounce_convex::QpOptions,
started: std::time::Instant,
) -> pounce_convex::QpOptions {
if let Some(limit) = opts.time_limit {
opts.time_limit = Some(limit.saturating_sub(started.elapsed()));
}
opts
}
fn charge_wall_budget(
opts: &mut pounce_common::options_list::OptionsList,
spent: std::time::Duration,
) {
const WALL_BUDGET_FLOOR: f64 = 1e-9;
if let Ok((limit, true)) = opts.get_numeric_value("max_wall_time", "") {
let left = (limit - spent.as_secs_f64()).max(WALL_BUDGET_FLOOR);
let _ = opts.set_numeric_value("max_wall_time", left, true, false);
}
}
fn run_convex_qp(
prob: &nl_reader::NlProblem,
class: pounce_cli::dispatch::ProblemClass,
sol_path: Option<&std::path::Path>,
presolve_on: bool,
json_cfg: Option<(&std::path::Path, ReportDetail, InputDescriptor)>,
debug_hook: Option<&Rc<RefCell<pounce_cli::debug_repl::SolverDebugger>>>,
ampl: bool,
convex_opts: pounce_convex::QpOptions,
bound_relax: pounce_cli::qp_extract::BoundRelax,
use_active_set: bool,
engine_overrides: pounce_convex::ActiveSetOverrides,
allow_nlp_fallback: bool,
console: ConvexConsole,
sens_suffixes: Option<&nl_reader::NlSuffixes>,
sens_may_decline: bool,
) -> Option<ExitCode> {
let t0 = std::time::Instant::now();
use pounce_convex::HessianInertia;
use pounce_convex::active_set::solve_qp_active_set_inertia;
use pounce_convex::presolve::{FixpointExit, PresolveOutcome, presolve};
use pounce_convex::{QpOptions, QpStatus, solve_qp_ipm, solve_qp_ipm_debug};
let inertia = if class == pounce_cli::dispatch::ProblemClass::NonconvexQp {
HessianInertia::Indefinite
} else {
HessianInertia::Psd
};
let timing = Rc::new(pounce_common::timing::ConvexTimingStatistics::new());
timing.set_detailed_enabled(console.collect_timing);
timing.overall_alg.start();
let _timing_scope = pounce_common::timing::ConvexTimingScope::open(&timing);
let (qp, con_map, obj_nl_const) = {
let _t = timing.extraction.guard();
match pounce_cli::qp_extract::extract_qp_with_map(prob, bound_relax) {
Some(q) => q,
None => {
eprintln!(
"pounce: internal error: {} not extractable as QP",
class.name()
);
return Some(ExitCode::from(2));
}
}
};
let sens_pins = match sens_suffixes {
Some(suffixes) => {
match pounce_cli::convex_sens::resolve_pins(suffixes, &con_map, &qp, prob.n) {
Ok(pins) => Some(pins),
Err(why) if sens_may_decline => {
eprintln!(
"pounce: note: the .nl requests a parametric sensitivity step, but \
{} on the extracted convex model; routing to the general NLP \
interior-point path, which expresses it.",
why.describe()
);
return None;
}
Err(why) => {
eprintln!(
"pounce: warning: the .nl requests a parametric sensitivity step, \
but {} on the extracted convex model, and solver_selection forces \
the convex solver; the request will be skipped. Use \
solver_selection=nlp or auto to obtain it.",
why.describe()
);
None
}
}
}
None => None,
};
let obj_const = prob.obj_constant + obj_nl_const;
let sign = if prob.minimize { 1.0 } else { -1.0 };
let backend = || -> Box<dyn SparseSymLinearSolverInterface> {
Box::new(pounce_feral::FeralSolverInterface::new())
};
let trivial = |status| pounce_convex::QpSolution {
status,
x: vec![0.0; qp.n],
y: vec![0.0; qp.m_eq()],
z: vec![0.0; qp.m_ineq()],
z_lb: vec![0.0; qp.n],
z_ub: vec![0.0; qp.n],
obj: 0.0,
iters: 0,
iterates: Vec::new(),
};
let want_trace = matches!(&json_cfg, Some((_, ReportDetail::Full, _)));
let qp_opts = QpOptions {
collect_iterates: want_trace,
obj_constant: sign * obj_const,
..convex_opts
};
let solve_opts_offset = |offset: f64| {
convex_opts_with_remaining(
QpOptions {
obj_constant: qp_opts.obj_constant + offset,
..qp_opts
},
t0,
)
};
let solve_opts = || solve_opts_offset(0.0);
let ipm_solve = |p: &pounce_convex::QpProblem, o: &QpOptions| -> pounce_convex::QpSolution {
match debug_hook.filter(|_| !use_active_set) {
Some(hook) => {
let mut h = hook.borrow_mut();
solve_qp_ipm_debug(p, o, &mut *h, backend)
}
None => solve_qp_ipm(p, o, backend),
}
};
let mut presolve_log: Vec<String> = Vec::new();
let sol = if qp_opts.max_iter == 0 {
trivial(QpStatus::IterationLimit)
} else if presolve_on {
let outcome = {
let _t = timing.presolve.guard();
presolve(&qp)
};
match outcome {
PresolveOutcome::Reduced(ps) => {
if let Some(trigger) = ps.discarded_infeasibility() {
presolve_log.push(format!(
"Presolve: discarded an unconfirmed infeasibility claim — \
{trigger}; solving normally"
));
}
let st = ps.stats();
if st.reduced_anything() {
let exit = match st.exit {
FixpointExit::Fixpoint => String::new(),
FixpointExit::RoundCap => {
format!(", cap-truncated after {} layers", st.rounds)
}
};
presolve_log.push(format!(
"Presolve: {} → {} vars, {} → {} rows (fixed {}, \
free-fixed {}, substituted {}, aggregated {}, \
forcing {}, dominated {}, tightened {}{})",
st.orig_vars,
st.reduced_vars,
st.orig_rows,
st.reduced_rows,
st.fixed_vars,
st.free_cols_fixed,
st.free_col_singletons,
st.aggregated_vars,
st.forcing_rows,
st.dominated_cols,
st.tightened_bounds,
exit,
));
}
let red = {
let _t = timing.solve.guard();
if use_active_set {
let mut mk = backend;
solve_qp_active_set_inertia(
&ps.reduced,
&solve_opts_offset(ps.obj_offset()),
&engine_overrides,
inertia,
&mut mk,
)
} else {
ipm_solve(&ps.reduced, &solve_opts_offset(ps.obj_offset()))
}
};
let _t = timing.presolve.guard();
ps.postsolve(&red)
}
PresolveOutcome::Infeasible(trigger) => {
presolve_log.push(format!("Presolve: proved primal infeasible — {trigger}"));
trivial(QpStatus::PrimalInfeasible)
}
PresolveOutcome::Unbounded => {
presolve_log.push(
"Presolve: proved unbounded below — a free column with a \
nonzero objective coefficient"
.to_string(),
);
trivial(QpStatus::DualInfeasible)
}
}
} else if use_active_set {
let mut mk = backend;
let _t = timing.solve.guard();
solve_qp_active_set_inertia(&qp, &solve_opts(), &engine_overrides, inertia, &mut mk)
} else {
let _t = timing.solve.guard();
ipm_solve(&qp, &solve_opts())
};
let elapsed = t0.elapsed().as_secs_f64();
if lp_declines_to_nlp(class, sol.status, allow_nlp_fallback) {
let res = sol.kkt_residuals(&qp);
eprintln!(
"pounce: note: the convex ({}) solve did not certify a KKT point \
after {} iterations in {elapsed:.3}s (KKT error {:.2e} against \
tol {:.1e}); an LP or convex QP is also a valid NLP, so it is \
being re-solved on the general NLP interior-point path, which \
certifies the degenerate, rank-deficient and badly-scaled models \
the interior path stalls on (gh #133, gh #535). Use \
solver_selection=qp-ipm to see the convex result instead.",
class.name(),
sol.iters,
res.kkt_error(),
qp_opts.tol,
);
return None;
}
for line in &presolve_log {
println!("{line}");
}
let reported_obj = sign * sol.obj + obj_const;
let (msg, ok, srn) = convex_status_report(sol.status);
let engine = if use_active_set {
"active-set, pounce-qp"
} else {
"IPM, pounce-convex"
};
println!(
"POUNCE ({} {engine}): {msg} obj={reported_obj:.8} iters={} ({elapsed:.3}s)",
class.name(),
sol.iters,
);
if use_active_set && !ok && inertia == HessianInertia::Indefinite {
eprintln!(
"pounce: note: the active-set engine reached a point that is not a \
local minimum of this indefinite QP — a feasible direction of \
negative curvature leads to a strictly better point — so its \
first-order verdict was refused rather than reported as optimal \
(gh #848). Its `optimal` on an indefinite Hessian means first-order \
KKT plus no counterexample found, which is weaker than a local \
guarantee. Use solver_selection=nlp for one: the NLP filter \
line-search interior-point path is where `auto` sends this class, \
and it does give a local optimum."
);
}
if let Some(warn) = sol.scaling_diagnostic(&qp) {
eprintln!("pounce: {warn}");
}
let res = sol.kkt_residuals(&qp);
let reported_res = pounce_cli::qp_extract::declared_residuals_qp(prob, &sol, bound_relax);
print::print_convex_summary(
sol.iters,
reported_obj,
res.primal_infeasibility,
res.dual_infeasibility,
res.complementarity,
res.kkt_error(),
reported_res
.map(|d| d.bound_violation)
.unwrap_or(res.bound_violation),
reported_res.map(|d| d.primal_infeasibility),
);
let recovery = timing.solution_recovery.guard();
let lambda = pounce_cli::qp_extract::recover_duals(prob, &con_map, &sol.y, &sol.z);
let (z_lb_raw, z_ub_raw) = pounce_cli::qp_extract::recover_bound_mults(prob, &sol);
let z_l_suffix: Vec<f64> = z_lb_raw.iter().map(|&z| sign * z).collect();
let z_u_suffix: Vec<f64> = z_ub_raw.iter().map(|&z| -sign * z).collect();
let mut qp_bound_suffixes = vec![
nl_writer::SolSuffix {
name: "ipopt_zL_out".to_string(),
target: nl_writer::SolSuffixTarget::Var,
values: nl_writer::SolSuffixValues::Real(z_l_suffix),
},
nl_writer::SolSuffix {
name: "ipopt_zU_out".to_string(),
target: nl_writer::SolSuffixTarget::Var,
values: nl_writer::SolSuffixValues::Real(z_u_suffix),
},
];
if let Some(pins) = &sens_pins {
match pounce_cli::convex_sens::perturbed_x(&qp, &sol, &solve_opts(), pins, backend) {
Ok(x_pert) => {
qp_bound_suffixes.push(pounce_cli::convex_sens::sens_suffix(x_pert));
}
Err(why) => eprintln!(
"pounce: warning: the parametric sensitivity step was requested but not \
produced on the convex path: {why}. Use solver_selection=nlp for the \
general path's step."
),
}
}
recovery.stop();
timing.overall_alg.end();
print_convex_verdict(console, sol.status, &timing);
if let Some(path) = sol_path {
let payload = nl_writer::SolutionFile {
message: &format!("POUNCE {} IPM (pounce-convex): {msg}", class.name()),
x: &sol.x,
mult_g: &lambda,
solve_result_num: srn,
suffixes: &qp_bound_suffixes,
};
if let Err(e) = nl_writer::write_sol_file_with_options(path, &payload, &prob.ampl_options) {
eprintln!("pounce: failed to write {}: {e}", path.display());
}
}
if let Some((json_path, detail, input)) = json_cfg {
let mut builder = ReportBuilder::new(detail, input);
builder.problem.n_variables = qp.n as _;
builder.problem.n_constraints = lambda.len() as _;
builder.problem.n_objectives = 1;
builder.problem.minimize = prob.minimize;
builder.solution.engine = if use_active_set {
"qp-active-set"
} else {
"cvx-qp"
}
.to_string();
builder.solution.status = qp_status_to_ars(sol.status);
builder.solution.solve_result_num = srn;
builder.solution.objective = reported_obj;
builder.solution.x = sol.x.clone();
builder.solution.lambda = lambda.clone();
builder.stats.iteration_count = sol.iters as _;
builder.stats.final_objective = reported_obj;
builder.stats.total_wallclock_time_secs = elapsed;
builder.stats.final_constr_viol = res.primal_infeasibility;
builder.stats.final_dual_inf = res.dual_infeasibility;
builder.stats.final_compl = res.complementarity;
builder.stats.final_kkt_error = res.kkt_error();
builder.stats.final_declared_constr_viol = reported_res
.map(|d| d.primal_infeasibility)
.unwrap_or(f64::NAN);
builder.stats.final_declared_box_viol = reported_res
.map(|d| d.bound_violation)
.unwrap_or(res.bound_violation);
if matches!(detail, ReportDetail::Full) {
builder.iterations = sol
.iterates
.iter()
.map(|it| IterRecord {
iter: it.iter as _,
objective: it.objective,
inf_pr: it.primal_infeasibility,
inf_du: it.dual_infeasibility,
mu: it.mu,
alpha_primal: it.alpha_primal,
alpha_dual: it.alpha_dual,
..IterRecord::default()
})
.collect();
}
let report = builder.finish();
if let Err(e) = write_report_file(json_path, &report) {
eprintln!(
"pounce: failed to write JSON report to {}: {e}",
json_path.display()
);
} else {
eprintln!("pounce: wrote {}", json_path.display());
}
}
Some(convex_exit_code(ok, ampl))
}
fn run_convex_socp(
prob: &nl_reader::NlProblem,
class: pounce_cli::dispatch::ProblemClass,
sol_path: Option<&std::path::Path>,
json_cfg: Option<(&std::path::Path, ReportDetail, InputDescriptor)>,
debug_hook: Option<&Rc<RefCell<pounce_cli::debug_repl::SolverDebugger>>>,
ampl: bool,
convex_opts: pounce_convex::QpOptions,
bound_relax: pounce_cli::qp_extract::BoundRelax,
presolve_on: bool,
allow_nlp_fallback: bool,
console: ConvexConsole,
) -> Option<ExitCode> {
let t0 = std::time::Instant::now();
use pounce_convex::presolve::{PresolveOutcome, presolve_conic};
use pounce_convex::{QpOptions, solve_socp_ipm, solve_socp_ipm_debug};
let timing = Rc::new(pounce_common::timing::ConvexTimingStatistics::new());
timing.set_detailed_enabled(console.collect_timing);
timing.overall_alg.start();
let _timing_scope = pounce_common::timing::ConvexTimingScope::open(&timing);
let (qp, con_map, obj_nl_const, cones) = {
let _t = timing.extraction.guard();
match pounce_cli::qp_extract::extract_socp_with_map(prob, bound_relax) {
Some(q) => q,
None => {
eprintln!(
"pounce: internal error: {} not extractable as SOCP",
class.name()
);
return Some(ExitCode::from(2));
}
}
};
let obj_const = prob.obj_constant + obj_nl_const;
let sign = if prob.minimize { 1.0 } else { -1.0 };
let backend = || -> Box<dyn SparseSymLinearSolverInterface> {
Box::new(pounce_feral::FeralSolverInterface::new())
};
let want_trace = matches!(&json_cfg, Some((_, ReportDetail::Full, _)));
let qp_opts = QpOptions {
collect_iterates: want_trace,
obj_constant: sign * obj_const,
..convex_opts
};
let solve_opts = || convex_opts_with_remaining(qp_opts, t0);
let trivial = |status| pounce_convex::QpSolution {
status,
x: vec![0.0; qp.n],
y: vec![0.0; qp.m_eq()],
z: vec![0.0; qp.m_ineq()],
z_lb: vec![0.0; qp.n],
z_ub: vec![0.0; qp.n],
obj: 0.0,
iters: 0,
iterates: Vec::new(),
};
let mut presolve_log: Vec<String> = Vec::new();
let conic_solve = |p: &pounce_convex::QpProblem,
k: &[pounce_convex::ConeSpec],
o: &QpOptions|
-> pounce_convex::QpSolution {
match debug_hook {
Some(hook) => {
let mut h = hook.borrow_mut();
solve_socp_ipm_debug(p, k, o, &mut *h, backend)
}
None => solve_socp_ipm(p, k, o, backend),
}
};
let sol = if qp_opts.max_iter == 0 {
trivial(pounce_convex::QpStatus::IterationLimit)
} else if presolve_on {
let outcome = {
let _t = timing.presolve.guard();
presolve_conic(&qp, &cones)
};
match outcome {
PresolveOutcome::Reduced(ps) => {
if let Some(trigger) = ps.discarded_infeasibility() {
presolve_log.push(format!(
"Presolve: discarded an unconfirmed infeasibility claim — \
{trigger}; solving normally"
));
}
let st = ps.stats();
if st.reduced_anything() {
presolve_log.push(format!(
"Presolve: {} → {} vars, {} → {} rows (fixed {}, \
free-fixed {}, substituted {}, forcing {}, \
dominated {}, tightened {})",
st.orig_vars,
st.reduced_vars,
st.orig_rows,
st.reduced_rows,
st.fixed_vars,
st.free_cols_fixed,
st.free_col_singletons,
st.forcing_rows,
st.dominated_cols,
st.tightened_bounds,
));
}
let red_cones = ps.reduced_cones(&cones);
let red = {
let _t = timing.solve.guard();
conic_solve(&ps.reduced, &red_cones, &solve_opts())
};
let _t = timing.presolve.guard();
ps.postsolve(&red)
}
PresolveOutcome::Infeasible(trigger) => {
presolve_log.push(format!("Presolve: proved primal infeasible — {trigger}"));
trivial(pounce_convex::QpStatus::PrimalInfeasible)
}
PresolveOutcome::Unbounded => {
presolve_log.push(
"Presolve: proved unbounded below — a free column with a \
nonzero objective coefficient"
.to_string(),
);
trivial(pounce_convex::QpStatus::DualInfeasible)
}
}
} else {
let _t = timing.solve.guard();
conic_solve(&qp, &cones, &solve_opts())
};
let elapsed = t0.elapsed().as_secs_f64();
if allow_nlp_fallback && matches!(sol.status, pounce_convex::QpStatus::NumericalFailure) {
let res = sol.kkt_residuals_conic(&qp, &cones);
eprintln!(
"pounce: note: the conic ({}) solve returned no verified KKT point \
after {} iterations (KKT error {:.2e}); a convex QCQP is also a \
valid NLP, so it is being re-solved on the general NLP \
interior-point path. Use solver_selection=socp to see the conic \
result instead.",
class.name(),
sol.iters,
res.kkt_error(),
);
return None;
}
for line in &presolve_log {
println!("{line}");
}
let reported_obj = sign * sol.obj + obj_const;
let (msg, ok, srn) = convex_status_report(sol.status);
println!(
"POUNCE ({} conic IPM, pounce-convex): {msg} obj={reported_obj:.8} iters={} ({elapsed:.3}s)",
class.name(),
sol.iters,
);
let res = sol.kkt_residuals_conic(&qp, &cones);
let reported_res = pounce_cli::qp_extract::declared_residuals_socp(prob, &sol, bound_relax);
print::print_convex_summary(
sol.iters,
reported_obj,
res.primal_infeasibility,
res.dual_infeasibility,
res.complementarity,
res.kkt_error(),
reported_res
.map(|d| d.bound_violation)
.unwrap_or(res.bound_violation),
reported_res.map(|d| d.primal_infeasibility),
);
let recovery = timing.solution_recovery.guard();
let lambda = pounce_cli::qp_extract::recover_socp_duals(prob, &con_map, &sol.y, &sol.z);
let (z_lb_raw, z_ub_raw) = pounce_cli::qp_extract::recover_bound_mults(prob, &sol);
let z_l_suffix: Vec<f64> = z_lb_raw.iter().map(|&z| sign * z).collect();
let z_u_suffix: Vec<f64> = z_ub_raw.iter().map(|&z| -sign * z).collect();
let socp_bound_suffixes = [
nl_writer::SolSuffix {
name: "ipopt_zL_out".to_string(),
target: nl_writer::SolSuffixTarget::Var,
values: nl_writer::SolSuffixValues::Real(z_l_suffix),
},
nl_writer::SolSuffix {
name: "ipopt_zU_out".to_string(),
target: nl_writer::SolSuffixTarget::Var,
values: nl_writer::SolSuffixValues::Real(z_u_suffix),
},
];
recovery.stop();
timing.overall_alg.end();
print_convex_verdict(console, sol.status, &timing);
if let Some(path) = sol_path {
let payload = nl_writer::SolutionFile {
message: &format!("POUNCE {} conic IPM (pounce-convex): {msg}", class.name()),
x: &sol.x,
mult_g: &lambda,
solve_result_num: srn,
suffixes: &socp_bound_suffixes,
};
if let Err(e) = nl_writer::write_sol_file(path, &payload) {
eprintln!("pounce: failed to write {}: {e}", path.display());
}
}
if let Some((json_path, detail, input)) = json_cfg {
let mut builder = ReportBuilder::new(detail, input);
builder.problem.n_variables = qp.n as _;
builder.problem.n_constraints = lambda.len() as _;
builder.problem.n_objectives = 1;
builder.problem.minimize = prob.minimize;
builder.solution.engine = "cvx-qcqp".to_string();
builder.solution.status = qp_status_to_ars(sol.status);
builder.solution.solve_result_num = srn;
builder.solution.objective = reported_obj;
builder.solution.x = sol.x.clone();
builder.solution.lambda = lambda.clone();
builder.stats.iteration_count = sol.iters as _;
builder.stats.final_objective = reported_obj;
builder.stats.total_wallclock_time_secs = elapsed;
builder.stats.final_constr_viol = res.primal_infeasibility;
builder.stats.final_dual_inf = res.dual_infeasibility;
builder.stats.final_compl = res.complementarity;
builder.stats.final_kkt_error = res.kkt_error();
builder.stats.final_declared_constr_viol = reported_res
.map(|d| d.primal_infeasibility)
.unwrap_or(f64::NAN);
builder.stats.final_declared_box_viol = reported_res
.map(|d| d.bound_violation)
.unwrap_or(res.bound_violation);
if matches!(detail, ReportDetail::Full) {
builder.iterations = sol
.iterates
.iter()
.map(|it| IterRecord {
iter: it.iter as _,
objective: it.objective,
inf_pr: it.primal_infeasibility,
inf_du: it.dual_infeasibility,
mu: it.mu,
alpha_primal: it.alpha_primal,
alpha_dual: it.alpha_dual,
..IterRecord::default()
})
.collect();
}
let report = builder.finish();
if let Err(e) = write_report_file(json_path, &report) {
eprintln!(
"pounce: failed to write JSON report to {}: {e}",
json_path.display()
);
} else {
eprintln!("pounce: wrote {}", json_path.display());
}
}
Some(convex_exit_code(ok, ampl))
}
fn convex_exit_code(ok: bool, ampl: bool) -> ExitCode {
if ok || ampl {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
fn build_diagnostics(
dump_specs: &[(String, String)],
dump_dir: Option<&std::path::PathBuf>,
dump_format: Option<&str>,
) -> Result<Option<Rc<DiagnosticsState>>, String> {
if dump_specs.is_empty() {
if dump_dir.is_some() || dump_format.is_some() {
return Err(
"--dump-dir / --dump-format require at least one --dump <cat>[:spec]".to_string(),
);
}
return Ok(None);
}
let dump_dir = dump_dir.cloned().unwrap_or_else(|| {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
std::path::PathBuf::from(format!("pounce-dump-{secs}"))
});
let format = match dump_format {
Some(f) => DumpFormat::parse(f)?,
None => DumpFormat::Jsonl,
};
let mut config = DiagnosticsConfig::new(dump_dir);
config.format = format;
for (cat_str, spec_str) in dump_specs {
let cat = DiagCategory::parse(cat_str)?;
if cat == DiagCategory::Iterate {
let (filter, variant) = pounce_common::diagnostics::parse_iterate_spec(spec_str)?;
config = config
.with_category(cat, filter)
.with_iterate_variant(variant);
} else if cat == DiagCategory::Kkt {
let (filter, variant) = pounce_common::diagnostics::parse_kkt_spec(spec_str)?;
config = config.with_category(cat, filter).with_kkt_variant(variant);
} else {
let spec = IterSpec::parse(spec_str)?;
config = config.with_category(cat, spec);
}
}
let state = DiagnosticsState::new(config)
.map_err(|e| format!("could not create dump directory: {e}"))?;
Ok(Some(Rc::new(state)))
}
fn write_diagnostics_manifest(
diag: &DiagnosticsState,
problem_desc: &str,
status: ApplicationReturnStatus,
) {
let mut cats: Vec<String> = diag
.config
.categories
.iter()
.map(|(c, s)| format!("\"{}\":\"{:?}\"", c.as_str(), s))
.collect();
cats.sort();
let manifest = format!(
"{{\n \"pounce_version\": \"{ver}\",\n \"git\": \"{git}\",\n \"problem\": \"{problem}\",\n \"status\": \"{status:?}\",\n \"format\": \"{fmt:?}\",\n \"categories\": {{ {cats} }}\n}}\n",
ver = env!("CARGO_PKG_VERSION"),
git = env!("POUNCE_BUILD_GIT"),
problem = problem_desc,
fmt = diag.config.format,
cats = cats.join(", "),
);
let _ = diag.write_top_level("manifest.json", &manifest);
}
fn write_diagnostics_timing(diag: &DiagnosticsState, app: &IpoptApplication) {
let t = app.timing_stats();
let body = format!(
"{{\n \"overall_alg_secs\": {a:.6},\n \"linear_system_factorization_secs\": {f:.6},\n \"linear_system_back_solve_secs\": {b:.6}\n}}\n",
a = t.overall_alg.total_wallclock_time(),
f = t.linear_system_factorization.total_wallclock_time(),
b = t.linear_system_back_solve.total_wallclock_time(),
);
let _ = diag.write_top_level("timing.json", &body);
}
fn run_cite(args: &Args) -> ExitCode {
let report = match &args.cite_report {
Some(path) => {
let text = match std::fs::read_to_string(path) {
Ok(t) => t,
Err(e) => {
eprintln!("pounce: failed to read {}: {e}", path.display());
return ExitCode::from(2);
}
};
match serde_json::from_str::<pounce_cli::solve_report::SolveReport>(&text) {
Ok(r) => Some(r),
Err(e) => {
eprintln!(
"pounce: {} is not a valid solve report: {e}",
path.display()
);
if path.extension().and_then(|e| e.to_str()) == Some("nl") {
eprintln!(
"pounce: --cite expects a solve-report JSON, not a model file. \
Run `pounce {} --json-output report.json` first, then \
`pounce --cite report.json` — or use bare `pounce --cite` for the core citations.",
path.display()
);
}
return ExitCode::from(2);
}
}
}
None => None,
};
let selected = pounce_cli::citations::select(report.as_ref());
if args.cite_bibtex {
print!("{}", pounce_cli::citations::render_bibtex(&selected));
} else {
print!("{}", pounce_cli::citations::render_human(&selected));
}
ExitCode::SUCCESS
}
fn print_about() {
let pkg_ver = env!("CARGO_PKG_VERSION");
let git = env!("POUNCE_BUILD_GIT");
let when = env!("POUNCE_BUILD_TIME");
let profile = env!("POUNCE_BUILD_PROFILE");
let target = env!("POUNCE_BUILD_TARGET");
let host = env!("POUNCE_BUILD_HOST");
let rustc = env!("POUNCE_BUILD_RUSTC");
println!("pounce {pkg_ver} (commit {git}, built {when})");
println!();
println!("Build:");
println!(" profile: {profile}");
println!(" target: {target}");
if host != target {
println!(" host: {host}");
}
println!(" rustc: {rustc}");
println!();
println!("Features:");
#[cfg(feature = "ma57")]
println!(" ma57: enabled");
#[cfg(not(feature = "ma57"))]
println!(" ma57: disabled (rebuild with --features ma57 to enable HSL MA57)");
println!();
println!("Linear solvers:");
println!(" feral FERAL pure-Rust sparse LDL^T (always built-in)");
#[cfg(feature = "ma57")]
println!(" ma57 HSL MA57 via libcoinhsl (compiled in)");
#[cfg(not(feature = "ma57"))]
println!(
" ma57 HSL MA57 via libcoinhsl (not compiled; resolves to FERAL at runtime)"
);
println!();
println!("Runtime paths:");
match std::env::current_exe() {
Ok(p) => println!(" executable: {}", p.display()),
Err(e) => println!(" executable: <unknown: {e}>"),
}
match std::env::current_dir() {
Ok(p) => println!(" cwd: {}", p.display()),
Err(e) => println!(" cwd: <unknown: {e}>"),
}
println!();
println!("Report bugs at {}/issues", env!("CARGO_PKG_REPOSITORY"));
}
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 => {
#[cfg(feature = "ma57")]
{
Box::new(pounce_hsl::Ma57SolverInterface::with_options(
*ma57_cfg.options(),
))
}
#[cfg(not(feature = "ma57"))]
{
let _ = &ma57_cfg;
Box::new(pounce_feral::FeralSolverInterface::with_config(
feral_cfg.clone(),
))
}
}
}
},
)
}
#[cfg(test)]
mod convex_status_tests {
use super::{convex_status_report, qp_status_to_ars};
use pounce_convex::QpStatus;
use pounce_nlp::return_codes::ApplicationReturnStatus;
#[test]
fn optimal_inaccurate_is_distinct_from_optimal() {
let (msg, ok, srn) = convex_status_report(QpStatus::OptimalInaccurate);
assert_eq!(
srn,
pounce_cli::solve_report::status_to_solve_result_num(
ApplicationReturnStatus::SolvedToAcceptableLevel
),
"the convex and NLP paths must report one code for one status",
);
assert_eq!(
srn, 1,
"Ipopt's code for an accepted reduced-accuracy solve"
);
assert!(ok, "a reduced-accuracy solve is still a usable success");
assert!(
msg.contains("acceptable"),
"message should signal reduced accuracy, got {msg:?}"
);
let (opt_msg, _, opt_srn) = convex_status_report(QpStatus::Optimal);
assert_eq!(opt_srn, 0);
assert_ne!(
srn, opt_srn,
"OptimalInaccurate must not share Optimal's solve_result_num"
);
assert_ne!(msg, opt_msg, "the two must read differently to the user");
assert_eq!(
qp_status_to_ars(QpStatus::OptimalInaccurate),
ApplicationReturnStatus::SolvedToAcceptableLevel
);
assert_eq!(
qp_status_to_ars(QpStatus::Optimal),
ApplicationReturnStatus::SolveSucceeded
);
}
#[test]
fn a_declined_convex_attempt_is_charged_against_the_wall_budget() {
use std::time::Duration;
let mut app = super::IpoptApplication::new();
app.options_mut()
.set_numeric_value("max_wall_time", 60.0, true, false)
.unwrap();
super::charge_wall_budget(app.options_mut(), Duration::from_secs_f64(55.0));
let (left, set) = app
.options()
.get_numeric_value("max_wall_time", "")
.unwrap();
assert!(set, "the option must still read as explicitly set");
assert!(
(left - 5.0).abs() < 1e-9,
"60s budget minus a 55s attempt must leave 5s, got {left}"
);
super::charge_wall_budget(app.options_mut(), Duration::from_secs_f64(600.0));
let (gone, _) = app
.options()
.get_numeric_value("max_wall_time", "")
.unwrap();
assert!(
gone > 0.0 && gone < 1e-6,
"an exhausted budget must store as positive-but-spent, got {gone}"
);
}
#[test]
fn an_unset_wall_budget_is_not_rewritten() {
use std::time::Duration;
let mut app = super::IpoptApplication::new();
let (before, set_before) = app
.options()
.get_numeric_value("max_wall_time", "")
.unwrap();
assert!(!set_before, "precondition: the option starts unset");
super::charge_wall_budget(app.options_mut(), Duration::from_secs_f64(3.2));
let (after, set_after) = app
.options()
.get_numeric_value("max_wall_time", "")
.unwrap();
assert_eq!(after, before);
assert!(
!set_after,
"an untouched budget must not read as explicitly set"
);
}
#[test]
fn time_limit_maps_to_wall_clock_status() {
let (msg, ok, srn) = convex_status_report(QpStatus::TimeLimit);
assert_eq!(msg, "Maximum wallclock time exceeded.");
assert!(!ok);
assert_eq!(srn, 400);
assert_eq!(
qp_status_to_ars(QpStatus::TimeLimit),
ApplicationReturnStatus::MaximumWallTimeExceeded
);
}
}
#[cfg(test)]
mod lp_nlp_fallback_tests {
use super::lp_declines_to_nlp;
use pounce_cli::dispatch::ProblemClass;
use pounce_convex::QpStatus;
const ALL_STATUSES: [QpStatus; 7] = [
QpStatus::Optimal,
QpStatus::OptimalInaccurate,
QpStatus::PrimalInfeasible,
QpStatus::DualInfeasible,
QpStatus::IterationLimit,
QpStatus::TimeLimit,
QpStatus::NumericalFailure,
];
#[test]
fn an_uncertified_lp_is_handed_to_the_nlp_path() {
for status in [
QpStatus::OptimalInaccurate,
QpStatus::IterationLimit,
QpStatus::NumericalFailure,
] {
assert!(
lp_declines_to_nlp(ProblemClass::Lp, status, true),
"{status:?} on an LP must reroute"
);
}
}
#[test]
fn a_certified_lp_is_never_rerouted() {
assert!(!lp_declines_to_nlp(
ProblemClass::Lp,
QpStatus::Optimal,
true
));
}
#[test]
fn verified_verdicts_stand() {
for status in [QpStatus::PrimalInfeasible, QpStatus::DualInfeasible] {
assert!(
!lp_declines_to_nlp(ProblemClass::Lp, status, true),
"{status:?} must not reroute"
);
}
}
#[test]
fn an_unverified_convex_result_reroutes_on_the_lp_path_as_it_does_on_the_conic_one() {
assert!(
lp_declines_to_nlp(ProblemClass::Lp, QpStatus::NumericalFailure, true),
"NumericalFailure is what the conic path reroutes on; the LP path \
must not report it as the last word"
);
}
#[test]
fn a_spent_time_budget_is_not_a_reason_to_solve_again() {
assert!(!lp_declines_to_nlp(
ProblemClass::Lp,
QpStatus::TimeLimit,
true
));
}
#[test]
fn only_the_convex_qp_classes_reroute() {
assert!(lp_declines_to_nlp(
ProblemClass::ConvexQp,
QpStatus::IterationLimit,
true
));
for class in [
ProblemClass::ConvexQcqp,
ProblemClass::NonconvexQp,
ProblemClass::Nlp,
] {
for status in ALL_STATUSES {
assert!(
!lp_declines_to_nlp(class, status, true),
"{class:?}/{status:?} must not reroute"
);
}
}
}
#[test]
fn the_callers_gate_suppresses_every_case() {
for class in [ProblemClass::Lp, ProblemClass::ConvexQp] {
for status in ALL_STATUSES {
assert!(
!lp_declines_to_nlp(class, status, false),
"{class:?}/{status:?} must not reroute when the caller declines"
);
}
}
}
}
#[cfg(test)]
mod nlp_exit_code_tests {
use super::nlp_solve_succeeded;
use super::status_to_solve_result_num;
use pounce_nlp::return_codes::ApplicationReturnStatus as A;
#[test]
fn acceptable_level_counts_as_success() {
assert!(nlp_solve_succeeded(A::SolvedToAcceptableLevel));
assert!(nlp_solve_succeeded(A::SolveSucceeded));
}
#[test]
fn a_square_problem_feasible_point_counts_as_success() {
assert!(nlp_solve_succeeded(A::FeasiblePointFound));
}
#[test]
fn the_exit_code_and_the_sol_band_never_disagree() {
for s in ALL_STATUSES {
let code = status_to_solve_result_num(s);
let solved_band = (0..=99).contains(&code);
assert_eq!(
nlp_solve_succeeded(s),
solved_band,
"{s:?}: exit-code success is {} but solve_result_num {code} \
puts the `.sol` {} the solved band",
nlp_solve_succeeded(s),
if solved_band { "inside" } else { "outside" },
);
}
}
const ALL_STATUSES: [A; 20] = [
A::SolveSucceeded,
A::SolvedToAcceptableLevel,
A::InfeasibleProblemDetected,
A::SearchDirectionBecomesTooSmall,
A::DivergingIterates,
A::UserRequestedStop,
A::FeasiblePointFound,
A::MaximumIterationsExceeded,
A::RestorationFailed,
A::ErrorInStepComputation,
A::MaximumCpuTimeExceeded,
A::MaximumWallTimeExceeded,
A::NotEnoughDegreesOfFreedom,
A::InvalidProblemDefinition,
A::InvalidOption,
A::InvalidNumberDetected,
A::UnrecoverableException,
A::NonIpoptExceptionThrown,
A::InsufficientMemory,
A::InternalError,
];
#[test]
fn non_convergent_statuses_are_not_success() {
for s in [
A::InfeasibleProblemDetected,
A::MaximumIterationsExceeded,
A::RestorationFailed,
A::DivergingIterates,
A::MaximumCpuTimeExceeded,
A::InternalError,
] {
assert!(
!nlp_solve_succeeded(s),
"{s:?} must not count as a successful solve"
);
}
}
}