use pounce_algorithm::alg_builder::{AlgorithmBuilder, 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::{
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::tnlp::TNLP;
use pounce_restoration::resto_alg_builder::RestoAlgorithmBuilder;
use pounce_restoration::resto_inner_solver::{
make_default_restoration_factory, InnerBackendFactoryFactory,
};
use std::cell::RefCell;
use std::path::PathBuf;
use std::process::ExitCode;
use std::rc::Rc;
pub fn main() -> ExitCode {
let 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 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;
}
let mut app = IpoptApplication::new();
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 feral_cfg = pounce_algorithm::application::feral_config_from_options(app.options());
let bff: InnerBackendFactoryFactory = Box::new(move || default_backend_factory(feral_cfg));
let resto_factory = make_default_restoration_factory(
RestoAlgorithmBuilder::new(),
AlgorithmBuilder::new(),
bff,
);
app.set_restoration_factory(resto_factory);
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 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) => {
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();
let t: Rc<RefCell<dyn TNLP>> =
Rc::new(RefCell::new(nl_reader::NlTnlp::new(prob)));
if let Some(info) = t.borrow_mut().get_nlp_info() {
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 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 n_c = curr.y_c.dim() as usize;
let n_d = curr.y_d.dim() as usize;
let mut 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(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());
println!(
"Presolve: tightened {} bounds ({} newly-finite), dropped {} redundant rows, LICQ={}",
tr.n_tightened, tr.n_new_finite, dropped, licq
);
}
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>>;
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 {
print::print_banner(backend_tag);
}
if let Some(stats) = print::collect_stats(&tnlp) {
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() {
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 status = app.optimize_tnlp(Rc::clone(&tnlp));
let solve_stats = app.statistics();
let counters = counting.borrow();
print::print_summary(status, &solve_stats, &counters);
drop(counters);
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;
builder.problem.n_constraints = info.m;
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) = nlp_info_snapshot
.as_ref()
.map(|i| (i.n as usize, i.m as usize))
.unwrap_or((0, 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);
}
match status {
ApplicationReturnStatus::SolveSucceeded
| ApplicationReturnStatus::SolvedToAcceptableLevel => ExitCode::SUCCESS,
_ if args.ampl => ExitCode::SUCCESS,
_ => 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, spec) in dump_specs {
let cat = DiagCategory::parse(cat)?;
let spec = IterSpec::parse(spec)?;
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 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 status_to_solve_result_num(status: ApplicationReturnStatus) -> i32 {
use ApplicationReturnStatus::*;
match status {
SolveSucceeded => 0,
SolvedToAcceptableLevel => 100,
FeasiblePointFound => 100,
InfeasibleProblemDetected => 200,
SearchDirectionBecomesTooSmall => 400,
DivergingIterates => 401,
MaximumIterationsExceeded => 400,
MaximumCpuTimeExceeded => 400,
MaximumWallTimeExceeded => 400,
UserRequestedStop => 502,
RestorationFailed => 500,
ErrorInStepComputation => 500,
InvalidNumberDetected => 500,
InternalError => 500,
UnrecoverableException => 500,
NonIpoptExceptionThrown => 500,
InsufficientMemory => 503,
InvalidProblemDefinition => 504,
InvalidOption => 504,
NotEnoughDegreesOfFreedom => 504,
}
}
fn default_backend_factory(feral_cfg: pounce_feral::FeralConfig) -> LinearBackendFactory {
Box::new(
move |choice: LinearSolverChoice| -> Box<dyn SparseSymLinearSolverInterface> {
match choice {
LinearSolverChoice::Feral => {
Box::new(pounce_feral::FeralSolverInterface::with_config(feral_cfg))
}
LinearSolverChoice::Ma57 => {
#[cfg(feature = "ma57")]
{
Box::new(pounce_hsl::Ma57SolverInterface::new())
}
#[cfg(not(feature = "ma57"))]
{
Box::new(pounce_feral::FeralSolverInterface::with_config(feral_cfg))
}
}
}
},
)
}