use pounce_algorithm::alg_builder::{LinearBackendFactory, LinearSolverChoice};
use pounce_algorithm::application::IpoptApplication;
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::{
status_to_solve_result_num, write_report_file, InputDescriptor, ReportBuilder, ReportDetail,
SolutionSuffix,
};
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::TNLP;
use pounce_nlp::SolveStatistics;
use pounce_restoration::resto_alg_builder::RestoAlgorithmBuilder;
use pounce_restoration::resto_inner_solver::{
make_default_restoration_factory_provider, InnerBackendFactoryFactory,
};
use std::cell::RefCell;
use std::path::PathBuf;
use std::process::ExitCode;
use std::rc::Rc;
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..]);
}
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();
if let Err(e) = app.registered_options().add_string_option(
"solver_selection",
"Which solver to route the problem to.",
"auto",
&[
(
"auto",
"Most specialized solver matching the detected problem class.",
),
(
"nlp",
"Always the filter-IPM NLP solver (current default behavior).",
),
(
"lp-ipm",
"Force IPM-LP; errors if the problem is not an LP.",
),
(
"qp-ipm",
"Force IPM-QP; errors if the problem is not LP/convex-QP.",
),
(
"qp-active-set",
"Force active-set QP; errors if not LP/convex-QP.",
),
(
"socp",
"Force the SOCP conic IPM; errors if not a convex LP/QP/QCQP.",
),
],
"Selects the solver by problem class. `auto` routes LP and convex \
QP to the specialized convex interior-point solver (pounce-convex), \
a convex QCQP to the SOCP conic IPM, and all other classes to the \
NLP filter-IPM. `qp-active-set` routes an LP / convex QP through the \
active-set SQP engine (pounce-qp QP subproblems) instead of the IPM; \
on these classes it converges in essentially one QP solve. `socp` \
forces the conic IPM (a convex QCQP routes there under `auto`).",
) {
eprintln!("pounce: failed to register solver_selection option: {e}");
return ExitCode::from(2);
}
if let Err(e) = app.registered_options().add_string_option(
"qp_presolve",
"Run presolve before the convex LP/QP interior-point solve.",
"yes",
&[
("yes", "Reduce the problem (and detect trivial infeasibility / unboundedness) before solving."),
("no", "Solve the extracted problem directly, without presolve."),
],
"Only affects the convex LP/QP path (`solver_selection` routing to \
pounce-convex). When on, presolve removes empty / duplicate / \
redundant rows, fixes and substitutes structural columns, and may \
report infeasible / unbounded without invoking the solver.",
) {
eprintln!("pounce: failed to register qp_presolve option: {e}");
return ExitCode::from(2);
}
let convex_knobs: Result<(), pounce_common::SolverException> = (|| {
let r = app.registered_options();
r.add_bounded_number_option(
"qp_tau",
"Convex IPM fraction-to-boundary parameter τ ∈ (0,1).",
0.0,
true,
1.0,
true,
0.95,
"Convex LP/QP interior-point only. Caps each Newton step at a \
fraction τ of the distance to the cone boundary; nearer 1 is more \
aggressive. Default 0.95.",
)?;
r.add_lower_bounded_number_option(
"qp_reg",
"Convex IPM static KKT regularization δ ≥ 0.",
0.0,
false,
1e-10,
"Convex LP/QP interior-point only. Added on the (block) diagonal to \
keep the reduced KKT quasi-definite for a stable LDLᵀ inertia. Too \
large freezes the primal residual on badly-scaled LPs; the default \
1e-10 is centered in the band that converges the LP/QP suites.",
)?;
r.add_lower_bounded_number_option(
"qp_infeas_tol",
"Convex IPM infeasibility-certificate value tolerance > 0.",
0.0,
true,
1e-7,
"Convex LP/QP interior-point only. Relative tolerance on the value \
and cone-membership parts of an infeasibility / unboundedness \
certificate. The certificate's defining-equation residual is held \
to a far tighter internal tolerance; this only governs when a \
status is backed by a verified proof. Default 1e-7.",
)?;
r.add_string_option(
"qp_hsde",
"Use the homogeneous self-dual embedding for the convex IPM.",
"yes",
&[
("yes", "Self-dual embedding: self-starting, native certificates, robust on ill-conditioned data."),
("no", "Infeasible-start primal–dual method (the warm-start / build-once substrate)."),
],
"Convex LP/QP interior-point only. HSDE (default) self-starts and \
produces infeasibility / unboundedness certificates natively; it \
is also the substrate for non-symmetric cones. Default yes.",
)?;
r.add_string_option(
"qp_equilibrate",
"Ruiz-equilibrate the data before the direct convex IPM solve.",
"yes",
&[
(
"yes",
"Apply Ruiz row/column scaling before solving (direct, non-HSDE path).",
),
("no", "Solve the raw data without equilibration."),
],
"Convex LP/QP interior-point only, and only when `qp_hsde=no` (the \
direct infeasible-start path): a conditioning aid for the raw KKT \
factorization. HSDE conditions internally and ignores this. \
Default yes.",
)?;
r.add_string_option(
"qp_crossover",
"Run LP crossover to purify the IPM iterate to an exact vertex.",
"no",
&[
("yes", "After the IPM, pivot the interior iterate to an exact optimal vertex (active-set purification)."),
("no", "Return the interior-point iterate directly (default)."),
],
"Convex LP path only (pure LP, P=0); a no-op for genuine QPs. \
Correct (never-regress) but currently slow on the degenerate / \
large NETLIB LPs it targets and does not yet reach an exact \
`Optimal` vertex on the GEN family (issue #133), so it is off by \
default and offered as an opt-in for small, well-behaved LPs that \
want exact-vertex refinement. Default no.",
)?;
Ok(())
})();
if let Err(e) = convex_knobs {
eprintln!("pounce: failed to register convex LP/QP options: {e}");
return ExitCode::from(2);
}
let sqp_qp_knobs: Result<(), pounce_common::SolverException> = (|| {
let r = app.registered_options();
r.add_lower_bounded_number_option(
"sqp_qp_feas_tol",
"Active-set QP-subproblem feasibility tolerance > 0.",
0.0,
true,
1e-9,
"Active-set SQP only (solver_selection=qp-active-set). Constraint \
feasibility tolerance for the pounce-qp subproblem solve. \
Default 1e-9.",
)?;
r.add_lower_bounded_number_option(
"sqp_qp_opt_tol",
"Active-set QP-subproblem optimality (KKT) tolerance > 0.",
0.0,
true,
1e-9,
"Active-set SQP only. Optimality / KKT tolerance for the pounce-qp \
subproblem solve. Default 1e-9.",
)?;
r.add_lower_bounded_integer_option(
"sqp_qp_max_iter",
"Active-set QP-subproblem iteration cap.",
1,
200,
"Active-set SQP only. Maximum active-set pivots per QP subproblem \
solve. Default 200.",
)?;
r.add_lower_bounded_number_option(
"sqp_qp_elastic_gamma",
"Active-set QP-subproblem elastic-mode penalty γ > 0.",
0.0,
true,
1e6,
"Active-set SQP only. Penalty on the elastic (phase-1) slacks used \
to recover from an infeasible QP subproblem. Large enough that the \
slacks vanish at the solution of a feasible QP, small enough not to \
dominate the Hessian conditioning. Default 1e6.",
)?;
r.add_string_option(
"sqp_qp_anti_cycling",
"Active-set QP-subproblem anti-cycling rule.",
"expand",
&[
("expand", "EXPAND tolerance-growth + Harris two-pass (Gill-Murray-Saunders-Wright 1989). Default."),
("bland", "Bland's rule: slower but guaranteed finite; mainly for tests."),
("none", "No anti-cycling — benchmarking only; may cycle on degenerate QPs."),
],
"Active-set SQP only. Anti-cycling strategy for the pounce-qp \
subproblem ratio test. Default expand.",
)?;
Ok(())
})();
if let Err(e) = sqp_qp_knobs {
eprintln!("pounce: failed to register active-set QP options: {e}");
return ExitCode::from(2);
}
if args.json_output.is_some() && matches!(args.json_detail, ReportDetail::Full) {
app.enable_iter_history();
}
if let Some(path) = &args.options_file {
if let Err(e) = app.initialize_with_options_file(path) {
eprintln!("pounce: failed to load options file: {e}");
return ExitCode::from(2);
}
} else if let Err(e) = app.initialize() {
eprintln!("pounce: initialize failed: {e}");
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 bff_mint = move || -> InnerBackendFactoryFactory {
let feral_cfg = feral_cfg.clone();
Box::new(move || default_backend_factory(feral_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);
let backend_tag = {
let (v, explicit) = app
.options()
.get_string_value("linear_solver", "")
.unwrap_or_else(|_| ("feral".to_string(), false));
match (v.as_str(), explicit) {
("ma57", true) => {
#[cfg(feature = "ma57")]
{
"MA57 (HSL)"
}
#[cfg(not(feature = "ma57"))]
{
"FERAL (ma57 requested but not compiled)"
}
}
("ma57", false) => "FERAL",
_ => "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);
}
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_class: Option<pounce_cli::dispatch::ProblemClass> = None;
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));
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)));
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);
}
}
}
};
if let Some(mcfg) = &args.minima {
return pounce_cli::minima::run(&mut app, &inner_tnlp, mcfg, &args, sol_path.as_deref());
}
{
use pounce_cli::dispatch::{resolve_solver, ProblemClass, SolverChoice, SolverSelection};
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);
}
};
if !suppress_banner && !json_dbg {
println!(
"Problem class: {}. Selected solver: {} [solver_selection={}].",
class.name(),
choice.describe(),
sel_str
);
println!();
}
if matches!(
choice,
SolverChoice::LpIpm | SolverChoice::QpIpm | SolverChoice::SocpIpm
) {
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 = convex_cli_opts(&app);
if matches!(choice, SolverChoice::SocpIpm) {
return run_convex_socp(
&prob,
class,
sol_path.as_deref(),
json_cfg,
debug_hook.as_ref(),
args.ampl,
convex_opts,
);
}
let opts = app.options();
let presolve_on = resolve_convex_presolve(
opts.get_string_value("qp_presolve", "").ok(),
opts.get_string_value("presolve", "").ok(),
);
return run_convex_qp(
&prob,
class,
sol_path.as_deref(),
presolve_on,
json_cfg,
debug_hook.as_ref(),
args.ampl,
convex_opts,
);
}
}
if matches!(choice, SolverChoice::QpActiveSet) {
if let Err(e) = app
.options_mut()
.read_from_str("algorithm active-set-sqp\n", true)
{
eprintln!("pounce: failed to select the active-set-sqp algorithm: {e}");
return ExitCode::from(2);
}
if 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."
);
}
}
let _ = choice;
}
let sens_active = nl_suffixes
.as_ref()
.map(sens::is_sensitivity_input)
.unwrap_or(false);
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 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 || args.compute_red_hessian {
let cap = Rc::clone(&nominal_capture);
let sens_cap = Rc::clone(&sens_capture);
let rh_cap = Rc::clone(&red_hessian_capture);
let suffixes_cb = nl_suffixes.clone();
let dims_cb = nl_dims;
let compute_rh = args.compute_red_hessian;
let rh_eigen = args.rh_eigendecomp;
let boundcheck_eps = args.sens_boundcheck.then_some(args.sens_bound_eps);
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 = nlp.borrow().lift_x_to_full(&*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));
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,
boundcheck_eps,
) {
*sens_cap.borrow_mut() = Some(xp);
}
}
if compute_rh {
match sens::try_compute_red_hessian(
data,
cq,
nlp,
Rc::clone(&pd),
suffixes,
rh_eigen,
) {
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 || args.compute_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 post_presolve: Rc<RefCell<dyn TNLP>> = match &presolve_handle {
Some(p) => Rc::clone(p) as Rc<RefCell<dyn TNLP>>,
None => Rc::clone(&inner_tnlp),
};
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>>;
if !json_dbg {
use pounce_nlp::tnlp_adapter::{
FixedVarTreatment, DEFAULT_NLP_LOWER_BOUND_INF, DEFAULT_NLP_UPPER_BOUND_INF,
};
let opt = app.options();
let lo_inf = opt
.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 = opt
.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 opt
.get_string_value("fixed_variable_treatment", "")
.ok()
.and_then(|(v, f)| f.then_some(v))
.as_deref()
{
Some("relax_bounds") => FixedVarTreatment::RelaxBounds,
_ => FixedVarTreatment::MakeParameter,
};
if let Some(stats) = print::collect_stats(&tnlp, lo_inf, up_inf, fixed_treatment) {
print::print_problem_stats(&stats);
}
}
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 scaling_retry_enabled = app
.options()
.get_bool_value("feral_infeasibility_scaling_retry", "")
.map(|(v, _found)| v)
.unwrap_or(true);
let already_mc64 = matches!(
pounce_algorithm::application::feral_config_from_options(app.options()).scaling,
pounce_feral::ScalingStrategy::Mc64Symmetric
);
if scaling_retry_enabled
&& debug_hook.is_none()
&& !already_mc64
&& status == ApplicationReturnStatus::InfeasibleProblemDetected
{
eprintln!(
"pounce: local infeasibility under the current FERAL scaling — re-solving once with \
MC64 before believing it (discs-class hypersensitivity guard; \
feral_infeasibility_scaling_retry)…"
);
let _ = app
.options_mut()
.read_from_str("feral_scaling mc64\n", true);
let feral_cfg = pounce_algorithm::application::feral_config_from_options(app.options());
let bff_mint = move || -> InnerBackendFactoryFactory {
let feral_cfg = feral_cfg.clone();
Box::new(move || default_backend_factory(feral_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);
let retry_status = app.optimize_tnlp(Rc::clone(&tnlp));
let retry_stats = app.statistics();
if scaling_retry_promoted(retry_status) {
eprintln!(
"pounce: MC64 re-solve recovered the problem — promoting ({retry_status:?})."
);
} else {
eprintln!(
"pounce: MC64 re-solve did not recover ({retry_status:?}); keeping the original \
local-infeasibility verdict (now corroborated by a second scaling)."
);
}
(status, solve_stats) =
resolve_scaling_retry_outcome(retry_status, solve_stats, retry_stats);
}
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}");
} else {
print::print_summary(status, &solve_stats, &counters);
}
drop(counters);
if nominal_capture.borrow().is_none() {
if let Some(xl) = counting.borrow().captured_solution() {
*nominal_capture.borrow_mut() = Some(xl);
}
}
if let Some(p) = &presolve_handle {
let lifted = if p.borrow().n_dropped_rows() > 0 {
p.borrow().finalized_full_solution().map(|(_x, lam)| lam)
} else {
None
};
if let Some(lam_full) = lifted {
if let Some((_x, lambda)) = nominal_capture.borrow_mut().as_mut() {
*lambda = lam_full;
}
}
}
if let Some(rh) = red_hessian_capture.borrow().as_ref() {
sens::print_red_hessian_to_stderr(rh);
} else if args.compute_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(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.status = status;
builder.solution.solve_result_num = status_to_solve_result_num(status);
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 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 = format!("POUNCE {}: {status:?}", env!("CARGO_PKG_VERSION"));
let payload = nl_writer::SolutionFile {
message: &message,
x: &x,
lambda: &lambda,
solve_result_num: status_to_solve_result_num(status),
suffixes: &sol_suffixes,
};
match nl_writer::write_sol_file(sol_path, &payload) {
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
)
}
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 scaling_retry_promoted(retry_status: ApplicationReturnStatus) -> bool {
matches!(
retry_status,
ApplicationReturnStatus::SolveSucceeded | ApplicationReturnStatus::SolvedToAcceptableLevel
)
}
fn resolve_scaling_retry_outcome(
retry_status: ApplicationReturnStatus,
original_stats: SolveStatistics,
retry_stats: SolveStatistics,
) -> (ApplicationReturnStatus, SolveStatistics) {
if scaling_retry_promoted(retry_status) {
(retry_status, retry_stats)
} else {
(
ApplicationReturnStatus::InfeasibleProblemDetected,
original_stats,
)
}
}
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::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, 100)
}
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::NumericalFailure => ("Numerical failure in KKT factorization.", false, 500),
}
}
fn convex_cli_opts(app: &IpoptApplication) -> pounce_convex::QpOptions {
let mut o = pounce_convex::QpOptions::default();
let opt = app.options();
if let Ok((v, true)) = opt.get_integer_value("max_iter", "") {
if v > 0 {
o.max_iter = v as usize;
}
}
if let Ok((v, true)) = opt.get_numeric_value("tol", "") {
o.tol = v;
}
if let Ok((v, true)) = opt.get_numeric_value("qp_tau", "") {
o.tau = v;
}
if let Ok((v, true)) = opt.get_numeric_value("qp_reg", "") {
o.reg = v;
}
if let Ok((v, true)) = opt.get_numeric_value("qp_infeas_tol", "") {
o.infeas_tol = v;
}
if let Ok((v, true)) = opt.get_string_value("qp_hsde", "") {
o.use_hsde = v != "no";
}
if let Ok((v, true)) = opt.get_string_value("qp_equilibrate", "") {
o.equilibrate = v != "no";
}
if let Ok((v, true)) = opt.get_string_value("qp_crossover", "") {
o.crossover = v != "no";
}
o
}
fn resolve_convex_presolve(
qp_presolve: Option<(String, bool)>,
presolve: Option<(String, bool)>,
) -> bool {
match (qp_presolve, presolve) {
(Some((v, true)), _) => v != "no",
(_, Some((v, true))) => v != "no",
_ => true,
}
}
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,
) -> ExitCode {
use pounce_convex::presolve::{presolve, PresolveOutcome};
use pounce_convex::{solve_qp_ipm, solve_qp_ipm_debug, QpOptions, QpStatus};
let (qp, con_map, obj_nl_const) = match pounce_cli::qp_extract::extract_qp_with_map(prob) {
Some(q) => q,
None => {
eprintln!(
"pounce: internal error: {} not extractable as QP",
class.name()
);
return 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 t0 = std::time::Instant::now();
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,
..convex_opts
};
let sol = if let Some(hook) = debug_hook {
let mut h = hook.borrow_mut();
solve_qp_ipm_debug(&qp, &qp_opts, &mut *h, backend)
} else if presolve_on {
match presolve(&qp) {
PresolveOutcome::Reduced(ps) => {
let st = ps.stats();
if st.reduced_anything() {
println!(
"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 = solve_qp_ipm(&ps.reduced, &qp_opts, backend);
ps.postsolve(&red)
}
PresolveOutcome::Infeasible => trivial(QpStatus::PrimalInfeasible),
PresolveOutcome::Unbounded => trivial(QpStatus::DualInfeasible),
}
} else {
solve_qp_ipm(&qp, &qp_opts, backend)
};
let elapsed = t0.elapsed().as_secs_f64();
let reported_obj = sign * sol.obj + obj_const;
let (msg, ok, srn) = convex_status_report(sol.status);
println!(
"POUNCE ({} IPM, pounce-convex): {msg} obj={reported_obj:.8} iters={} ({elapsed:.3}s)",
class.name(),
sol.iters,
);
let res = sol.kkt_residuals(&qp);
print::print_convex_summary(
sol.iters,
reported_obj,
res.primal_infeasibility,
res.dual_infeasibility,
res.complementarity,
res.kkt_error(),
);
let lambda = pounce_cli::qp_extract::recover_duals(prob, &con_map, &sol.y, &sol.z);
if let Some(path) = sol_path {
let payload = nl_writer::SolutionFile {
message: &format!("POUNCE {} IPM (pounce-convex): {msg}", class.name()),
x: &sol.x,
lambda: &lambda,
solve_result_num: srn,
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.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();
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());
}
}
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,
) -> ExitCode {
use pounce_convex::{solve_socp_ipm, solve_socp_ipm_debug, QpOptions};
let (qp, con_map, obj_nl_const, cones) =
match pounce_cli::qp_extract::extract_socp_with_map(prob) {
Some(q) => q,
None => {
eprintln!(
"pounce: internal error: {} not extractable as SOCP",
class.name()
);
return 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,
..convex_opts
};
let t0 = std::time::Instant::now();
let sol = if let Some(hook) = debug_hook {
let mut h = hook.borrow_mut();
solve_socp_ipm_debug(&qp, &cones, &qp_opts, &mut *h, backend)
} else {
solve_socp_ipm(&qp, &cones, &qp_opts, backend)
};
let elapsed = t0.elapsed().as_secs_f64();
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(&qp);
print::print_convex_summary(
sol.iters,
reported_obj,
res.primal_infeasibility,
res.dual_infeasibility,
res.complementarity,
res.kkt_error(),
);
let lambda = pounce_cli::qp_extract::recover_socp_duals(prob, &con_map, &sol.y, &sol.z);
if let Some(path) = sol_path {
let payload = nl_writer::SolutionFile {
message: &format!("POUNCE {} conic IPM (pounce-convex): {msg}", class.name()),
x: &sol.x,
lambda: &lambda,
solve_result_num: srn,
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.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();
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());
}
}
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) -> 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::new())
}
#[cfg(not(feature = "ma57"))]
{
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, 100, "reduced-accuracy solve must use the 100 band");
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
);
}
}
#[cfg(test)]
mod scaling_retry_tests {
use super::{resolve_scaling_retry_outcome, scaling_retry_promoted};
use pounce_nlp::return_codes::ApplicationReturnStatus;
use pounce_nlp::SolveStatistics;
fn stats_with_iters(n: i32) -> SolveStatistics {
SolveStatistics {
iteration_count: n,
final_objective: n as f64,
..SolveStatistics::default()
}
}
#[test]
fn failed_retry_keeps_original_status_and_stats() {
let original = stats_with_iters(7);
let retry = stats_with_iters(42);
for retry_status in [
ApplicationReturnStatus::InfeasibleProblemDetected,
ApplicationReturnStatus::MaximumIterationsExceeded,
ApplicationReturnStatus::RestorationFailed,
] {
assert!(!scaling_retry_promoted(retry_status));
let (status, stats) =
resolve_scaling_retry_outcome(retry_status, original.clone(), retry.clone());
assert_eq!(
status,
ApplicationReturnStatus::InfeasibleProblemDetected,
"a non-promoting retry ({retry_status:?}) keeps the original verdict"
);
assert_eq!(
stats.iteration_count, 7,
"stats must stay the original solve's, not the failed retry's"
);
assert_eq!(stats.final_objective, 7.0);
}
}
#[test]
fn promoted_retry_adopts_retry_status_and_stats() {
let original = stats_with_iters(7);
let retry = stats_with_iters(42);
for retry_status in [
ApplicationReturnStatus::SolveSucceeded,
ApplicationReturnStatus::SolvedToAcceptableLevel,
] {
assert!(scaling_retry_promoted(retry_status));
let (status, stats) =
resolve_scaling_retry_outcome(retry_status, original.clone(), retry.clone());
assert_eq!(status, retry_status, "a promoting retry adopts its verdict");
assert_eq!(
stats.iteration_count, 42,
"promoted: stats must be the retry solve's"
);
assert_eq!(stats.final_objective, 42.0);
}
}
}
#[cfg(test)]
mod nlp_exit_code_tests {
use super::nlp_solve_succeeded;
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 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"
);
}
}
}
#[cfg(test)]
mod convex_presolve_tests {
use super::resolve_convex_presolve;
fn set(v: &str) -> Option<(String, bool)> {
Some((v.to_string(), true))
}
fn unset(v: &str) -> Option<(String, bool)> {
Some((v.to_string(), false))
}
#[test]
fn defaults_on_when_nothing_set() {
assert!(resolve_convex_presolve(None, None));
assert!(resolve_convex_presolve(unset("yes"), unset("yes")));
}
#[test]
fn explicit_presolve_is_honored() {
assert!(!resolve_convex_presolve(unset("yes"), set("no")));
assert!(resolve_convex_presolve(unset("yes"), set("yes")));
}
#[test]
fn explicit_qp_presolve_is_honored() {
assert!(!resolve_convex_presolve(set("no"), None));
assert!(resolve_convex_presolve(set("yes"), None));
}
#[test]
fn qp_presolve_wins_when_both_explicit() {
assert!(!resolve_convex_presolve(set("no"), set("yes")));
assert!(resolve_convex_presolve(set("yes"), set("no")));
}
#[test]
fn explicit_presolve_overrides_unset_qp_presolve() {
assert!(!resolve_convex_presolve(unset("yes"), set("no")));
}
}