Skip to main content

pounce_cli/
print.rs

1//! Ipopt-style banner / problem-stats / final-summary printing for
2//! the `pounce` CLI. Output is structured to match upstream Ipopt's
3//! console layout closely enough that anyone familiar with `ipopt`
4//! can spot at a glance whether POUNCE is converging similarly.
5
6use crate::counting_tnlp::CountingTnlp;
7use pounce_nlp::return_codes::ApplicationReturnStatus;
8use pounce_nlp::solve_statistics::SolveStatistics;
9use pounce_nlp::tnlp::{BoundsInfo, NlpInfo, SparsityRequest, TNLP};
10use std::cell::RefCell;
11use std::rc::Rc;
12
13/// Same sentinel Ipopt uses for "no bound": ±1e19. Matched exactly so
14/// the per-bound-type tallies agree with `ipopt`'s own output on
15/// problems whose bounds were authored against that convention.
16const BOUND_INF: f64 = 1.0e19;
17
18#[derive(Debug, Clone, Copy)]
19pub struct ProblemStats {
20    pub n: i32,
21    pub m: i32,
22    pub nnz_jac_eq: i32,
23    pub nnz_jac_ineq: i32,
24    pub nnz_h_lag: i32,
25    pub var_lower_only: i32,
26    pub var_upper_only: i32,
27    pub var_both: i32,
28    pub var_free: i32,
29    pub n_eq: i32,
30    pub n_ineq: i32,
31    pub ineq_lower_only: i32,
32    pub ineq_upper_only: i32,
33    pub ineq_both: i32,
34}
35
36/// Walk the TNLP once to gather everything the banner block needs:
37/// `NlpInfo`, the four bound vectors, and the Jacobian row indices.
38/// Returns `None` if any of the required TNLP calls fails.
39pub fn collect_stats(tnlp: &Rc<RefCell<dyn TNLP>>) -> Option<ProblemStats> {
40    let mut t = tnlp.borrow_mut();
41    let info: NlpInfo = t.get_nlp_info()?;
42    let n = info.n as usize;
43    let m = info.m as usize;
44    let mut x_l = vec![0.0; n];
45    let mut x_u = vec![0.0; n];
46    let mut g_l = vec![0.0; m];
47    let mut g_u = vec![0.0; m];
48    if !t.get_bounds_info(BoundsInfo {
49        x_l: &mut x_l,
50        x_u: &mut x_u,
51        g_l: &mut g_l,
52        g_u: &mut g_u,
53    }) {
54        return None;
55    }
56
57    // Variable bound classification.
58    let (mut var_lower_only, mut var_upper_only, mut var_both, mut var_free) = (0, 0, 0, 0);
59    for i in 0..n {
60        let has_l = x_l[i] > -BOUND_INF;
61        let has_u = x_u[i] < BOUND_INF;
62        match (has_l, has_u) {
63            (true, true) => var_both += 1,
64            (true, false) => var_lower_only += 1,
65            (false, true) => var_upper_only += 1,
66            (false, false) => var_free += 1,
67        }
68    }
69
70    // Constraint classification (equality vs inequality, and the
71    // inequality bound type).
72    let (mut n_eq, mut n_ineq) = (0, 0);
73    let (mut ineq_lower_only, mut ineq_upper_only, mut ineq_both) = (0, 0, 0);
74    let mut row_is_eq = vec![false; m];
75    for i in 0..m {
76        if (g_l[i] - g_u[i]).abs() < 1e-12 && g_l[i].abs() < BOUND_INF {
77            n_eq += 1;
78            row_is_eq[i] = true;
79        } else {
80            n_ineq += 1;
81            let has_l = g_l[i] > -BOUND_INF;
82            let has_u = g_u[i] < BOUND_INF;
83            match (has_l, has_u) {
84                (true, true) => ineq_both += 1,
85                (true, false) => ineq_lower_only += 1,
86                (false, true) => ineq_upper_only += 1,
87                // A "free" inequality has no bounds at all — count it
88                // anyway under "both" to keep the totals consistent.
89                (false, false) => {}
90            }
91        }
92    }
93
94    // Jacobian split: read the structure once and tally per-row.
95    let nnz_total = info.nnz_jac_g as usize;
96    let (mut nnz_jac_eq, mut nnz_jac_ineq) = (0, 0);
97    if nnz_total > 0 && m > 0 {
98        let mut irow = vec![0_i32; nnz_total];
99        let mut jcol = vec![0_i32; nnz_total];
100        if t.eval_jac_g(
101            None,
102            true,
103            SparsityRequest::Structure {
104                irow: &mut irow,
105                jcol: &mut jcol,
106            },
107        ) {
108            let one_based = matches!(info.index_style, pounce_nlp::tnlp::IndexStyle::Fortran);
109            for &r in &irow {
110                let row = if one_based {
111                    (r - 1) as usize
112                } else {
113                    r as usize
114                };
115                if row < m && row_is_eq[row] {
116                    nnz_jac_eq += 1;
117                } else {
118                    nnz_jac_ineq += 1;
119                }
120            }
121        }
122    }
123
124    Some(ProblemStats {
125        n: info.n,
126        m: info.m,
127        nnz_jac_eq,
128        nnz_jac_ineq,
129        nnz_h_lag: info.nnz_h_lag,
130        var_lower_only,
131        var_upper_only,
132        var_both,
133        var_free,
134        n_eq,
135        n_ineq,
136        ineq_lower_only,
137        ineq_upper_only,
138        ineq_both,
139    })
140}
141
142pub fn print_banner(linear_solver: &str) {
143    println!("******************************************************************************");
144    println!("This program contains POUNCE, a Rust port of Ipopt for nonlinear optimization.");
145    println!(" Released under the Eclipse Public License (EPL) — drop-in compatible with Ipopt.");
146    println!("         For more information visit https://github.com/jkitchin/pounce");
147    println!("******************************************************************************");
148    println!();
149    println!(
150        "This is POUNCE version {}, running with linear solver {}.",
151        env!("CARGO_PKG_VERSION"),
152        linear_solver
153    );
154    println!();
155}
156
157pub fn print_problem_stats(s: &ProblemStats) {
158    println!(
159        "Number of nonzeros in equality constraint Jacobian...: {:>8}",
160        s.nnz_jac_eq
161    );
162    println!(
163        "Number of nonzeros in inequality constraint Jacobian.: {:>8}",
164        s.nnz_jac_ineq
165    );
166    println!(
167        "Number of nonzeros in Lagrangian Hessian.............: {:>8}",
168        s.nnz_h_lag
169    );
170    println!();
171    println!(
172        "Total number of variables............................: {:>8}",
173        s.n
174    );
175    println!(
176        "                     variables with only lower bounds: {:>8}",
177        s.var_lower_only
178    );
179    println!(
180        "                variables with lower and upper bounds: {:>8}",
181        s.var_both
182    );
183    println!(
184        "                     variables with only upper bounds: {:>8}",
185        s.var_upper_only
186    );
187    println!(
188        "Total number of equality constraints.................: {:>8}",
189        s.n_eq
190    );
191    println!(
192        "Total number of inequality constraints...............: {:>8}",
193        s.n_ineq
194    );
195    println!(
196        "        inequality constraints with only lower bounds: {:>8}",
197        s.ineq_lower_only
198    );
199    println!(
200        "   inequality constraints with lower and upper bounds: {:>8}",
201        s.ineq_both
202    );
203    println!(
204        "        inequality constraints with only upper bounds: {:>8}",
205        s.ineq_upper_only
206    );
207    println!();
208}
209
210pub fn print_summary(
211    status: ApplicationReturnStatus,
212    stats: &SolveStatistics,
213    counters: &CountingTnlp,
214) {
215    println!();
216    println!();
217    println!("Number of Iterations....: {}", stats.iteration_count);
218    println!();
219    println!("                                   (scaled)                 (unscaled)");
220    let row = |label: &str, scaled: f64, unscaled: f64| {
221        println!(
222            "{label}:   {}    {}",
223            fmt_ipopt(scaled),
224            fmt_ipopt(unscaled)
225        );
226    };
227    row(
228        "Objective...............",
229        stats.final_scaled_objective,
230        stats.final_objective,
231    );
232    row(
233        "Dual infeasibility......",
234        stats.final_dual_inf,
235        stats.final_dual_inf,
236    );
237    row(
238        "Constraint violation....",
239        stats.final_constr_viol,
240        stats.final_constr_viol,
241    );
242    row("Variable bound violation", 0.0, 0.0);
243    row(
244        "Complementarity.........",
245        stats.final_compl,
246        stats.final_compl,
247    );
248    row(
249        "Overall NLP error.......",
250        stats.final_kkt_error,
251        stats.final_kkt_error,
252    );
253    println!();
254    println!();
255    println!(
256        "Number of objective function evaluations             = {}",
257        counters.n_obj.get()
258    );
259    println!(
260        "Number of objective gradient evaluations             = {}",
261        counters.n_grad_f.get()
262    );
263    println!(
264        "Number of equality constraint evaluations            = {}",
265        counters.n_g.get()
266    );
267    println!(
268        "Number of inequality constraint evaluations          = {}",
269        counters.n_g.get()
270    );
271    println!(
272        "Number of equality constraint Jacobian evaluations   = {}",
273        counters.n_jac_g.get()
274    );
275    println!(
276        "Number of inequality constraint Jacobian evaluations = {}",
277        counters.n_jac_g.get()
278    );
279    println!(
280        "Number of Lagrangian Hessian evaluations             = {}",
281        counters.n_h.get()
282    );
283    println!(
284        "Total seconds in POUNCE                              = {:.3}",
285        stats.total_wallclock_time_secs
286    );
287    println!();
288    println!("EXIT: {}", status_message(status));
289    println!();
290    println!(
291        "POUNCE {}: {}",
292        env!("CARGO_PKG_VERSION"),
293        status_message(status)
294    );
295}
296
297/// Format a number in Ipopt's scientific notation: 16-digit mantissa,
298/// signed 2-digit exponent (e.g. `3.7952009505566139e+03`). Rust's
299/// `{:.16e}` is close but emits a 1-digit exponent without leading
300/// sign, which makes side-by-side diffs against `ipopt` output messy.
301pub fn fmt_ipopt(v: f64) -> String {
302    if v.is_nan() {
303        return "nan".to_string();
304    }
305    if v.is_infinite() {
306        return if v > 0.0 { "inf".into() } else { "-inf".into() };
307    }
308    let s = format!("{:.16e}", v);
309    let Some(e_pos) = s.rfind('e') else {
310        return s;
311    };
312    let (mantissa, exp_part) = s.split_at(e_pos);
313    let exp_str = &exp_part[1..];
314    let (sign, digits) = if let Some(rest) = exp_str.strip_prefix('-') {
315        ('-', rest)
316    } else if let Some(rest) = exp_str.strip_prefix('+') {
317        ('+', rest)
318    } else {
319        ('+', exp_str)
320    };
321    let padded = if digits.len() < 2 {
322        format!("0{digits}")
323    } else {
324        digits.to_string()
325    };
326    format!("{mantissa}e{sign}{padded}")
327}
328
329pub fn status_message(s: ApplicationReturnStatus) -> &'static str {
330    match s {
331        ApplicationReturnStatus::SolveSucceeded => "Optimal Solution Found.",
332        ApplicationReturnStatus::SolvedToAcceptableLevel => "Solved To Acceptable Level.",
333        ApplicationReturnStatus::InfeasibleProblemDetected => {
334            "Converged to a point of local infeasibility. Problem may be infeasible."
335        }
336        ApplicationReturnStatus::SearchDirectionBecomesTooSmall => {
337            "Search Direction is becoming Too Small."
338        }
339        ApplicationReturnStatus::DivergingIterates => {
340            "Iterates diverging; problem might be unbounded."
341        }
342        ApplicationReturnStatus::UserRequestedStop => "Stopping optimization at user request.",
343        ApplicationReturnStatus::FeasiblePointFound => "Feasible Point Found.",
344        ApplicationReturnStatus::MaximumIterationsExceeded => {
345            "Maximum Number of Iterations Exceeded."
346        }
347        ApplicationReturnStatus::RestorationFailed => "Restoration Failed!",
348        ApplicationReturnStatus::ErrorInStepComputation => "Error in step computation.",
349        ApplicationReturnStatus::MaximumCpuTimeExceeded => "Maximum CPU time exceeded.",
350        ApplicationReturnStatus::MaximumWallTimeExceeded => "Maximum wallclock time exceeded.",
351        ApplicationReturnStatus::NotEnoughDegreesOfFreedom => "Not Enough Degrees of Freedom.",
352        ApplicationReturnStatus::InvalidProblemDefinition => "Invalid Problem Definition.",
353        ApplicationReturnStatus::InvalidOption => "Invalid Option.",
354        ApplicationReturnStatus::InvalidNumberDetected => {
355            "Invalid number in NLP function or derivative detected."
356        }
357        ApplicationReturnStatus::UnrecoverableException => "Unrecoverable Exception.",
358        ApplicationReturnStatus::NonIpoptExceptionThrown => "Exception of type generic.",
359        ApplicationReturnStatus::InsufficientMemory => "Insufficient memory.",
360        ApplicationReturnStatus::InternalError => "INTERNAL ERROR: Unknown SolverReturn value.",
361    }
362}