1use 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
13const 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
36pub 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 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 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 (false, false) => {}
90 }
91 }
92 }
93
94 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
142const LOGO: [&str; 5] = [
144 "#### ### # # # # #### #####",
145 "# # # # # # ## # # # ",
146 "#### # # # # # # # # #### ",
147 "# # # # # # ## # # ",
148 "# ### ### # # #### #####",
149];
150
151const BANNER_WIDTH: usize = 80;
155
156pub fn print_logo() {
167 use std::io::Write as _;
168 let width = LOGO
169 .iter()
170 .map(|l| l.chars().count())
171 .max()
172 .unwrap_or(1)
173 .max(2);
174 let mut out = anstream::stdout();
175 let _ = writeln!(out, "{}", "*".repeat(BANNER_WIDTH));
180 let _ = writeln!(out);
181 let pad = " ".repeat(BANNER_WIDTH.saturating_sub(width) / 2);
182 for row in logo_rows(true) {
183 let _ = writeln!(out, "{pad}{row}");
184 }
185 let _ = writeln!(out);
186}
187
188pub fn logo_rows(color: bool) -> Vec<String> {
194 use pounce_common::style::{downgrade, truecolor_enabled, ALPHA_HOT, BRIGHT_YEL, TIGER_ORANGE};
195
196 fn lerp(a: u8, b: u8, t: f64) -> u8 {
197 (a as f64 + (b as f64 - a as f64) * t)
198 .round()
199 .clamp(0.0, 255.0) as u8
200 }
201 fn mix(a: anstyle::RgbColor, b: anstyle::RgbColor, t: f64) -> anstyle::RgbColor {
202 anstyle::RgbColor(lerp(a.0, b.0, t), lerp(a.1, b.1, t), lerp(a.2, b.2, t))
203 }
204 const STEEL_HI: anstyle::RgbColor = anstyle::RgbColor(0xd2, 0xd6, 0xdc);
207 const STEEL_LO: anstyle::RgbColor = anstyle::RgbColor(0x5c, 0x60, 0x68);
208
209 let rows = LOGO.len();
210 let width = LOGO
211 .iter()
212 .map(|l| l.chars().count())
213 .max()
214 .unwrap_or(1)
215 .max(2);
216 let vfrac = |r: usize| {
217 if rows <= 1 {
218 0.0
219 } else {
220 r as f64 / (rows - 1) as f64
221 }
222 };
223 let molten = |r: usize| {
224 let t = vfrac(r);
225 if t < 0.5 {
226 mix(BRIGHT_YEL, TIGER_ORANGE, t / 0.5)
227 } else {
228 mix(TIGER_ORANGE, ALPHA_HOT, (t - 0.5) / 0.5)
229 }
230 };
231
232 let mut grid: Vec<Vec<Option<(char, anstyle::RgbColor)>>> = vec![vec![None; width]; rows];
233 for (r, line) in LOGO.iter().enumerate() {
234 let steel = mix(STEEL_HI, STEEL_LO, vfrac(r));
235 for (c, ch) in line.chars().enumerate() {
236 if ch != ' ' {
237 grid[r][c] = Some((ch, steel));
238 }
239 }
240 }
241 for &start in &[width / 4, width / 4 + 6, width / 4 + 12] {
243 for r in 0..rows {
244 let c = start + (rows - 1 - r);
245 if c < width {
246 grid[r][c] = Some(('/', molten(r)));
247 }
248 }
249 }
250
251 let truecolor = truecolor_enabled();
252 grid.iter()
253 .map(|row| {
254 let mut rendered = String::new();
255 for cell in row {
256 match cell {
257 Some((ch, rgb)) if color => {
258 let style = anstyle::Style::new()
259 .bold()
260 .fg_color(Some(downgrade(*rgb, truecolor)));
261 rendered.push_str(&format!(
262 "{}{}{}",
263 style.render(),
264 ch,
265 style.render_reset()
266 ));
267 }
268 Some((ch, _)) => rendered.push(*ch),
269 None => rendered.push(' '),
270 }
271 }
272 rendered.trim_end().to_string()
273 })
274 .collect()
275}
276
277pub fn print_banner(linear_solver: &str) {
278 use std::io::IsTerminal as _;
279
280 const URL: &str = "https://github.com/jkitchin/pounce";
283 let link = if std::io::stdout().is_terminal() {
284 format!("\x1b]8;;{URL}\x1b\\{URL}\x1b]8;;\x1b\\")
285 } else {
286 URL.to_string()
287 };
288
289 let rule = "*".repeat(BANNER_WIDTH);
290 println!("{rule}");
291 println!("This program contains POUNCE, a Rust port of Ipopt for nonlinear optimization.");
292 println!("Released under the Eclipse Public License (EPL) — drop-in compatible with Ipopt.");
293 println!(" For more information visit {link}");
294 println!("{rule}");
295 println!();
296 println!(
297 "This is POUNCE version {}, running with linear solver {}.",
298 env!("CARGO_PKG_VERSION"),
299 linear_solver
300 );
301 println!();
302}
303
304pub fn print_problem_stats(s: &ProblemStats) {
305 println!(
306 "Number of nonzeros in equality constraint Jacobian...: {:>8}",
307 s.nnz_jac_eq
308 );
309 println!(
310 "Number of nonzeros in inequality constraint Jacobian.: {:>8}",
311 s.nnz_jac_ineq
312 );
313 println!(
314 "Number of nonzeros in Lagrangian Hessian.............: {:>8}",
315 s.nnz_h_lag
316 );
317 println!();
318 println!(
319 "Total number of variables............................: {:>8}",
320 s.n
321 );
322 println!(
323 " variables with only lower bounds: {:>8}",
324 s.var_lower_only
325 );
326 println!(
327 " variables with lower and upper bounds: {:>8}",
328 s.var_both
329 );
330 println!(
331 " variables with only upper bounds: {:>8}",
332 s.var_upper_only
333 );
334 println!(
335 "Total number of equality constraints.................: {:>8}",
336 s.n_eq
337 );
338 println!(
339 "Total number of inequality constraints...............: {:>8}",
340 s.n_ineq
341 );
342 println!(
343 " inequality constraints with only lower bounds: {:>8}",
344 s.ineq_lower_only
345 );
346 println!(
347 " inequality constraints with lower and upper bounds: {:>8}",
348 s.ineq_both
349 );
350 println!(
351 " inequality constraints with only upper bounds: {:>8}",
352 s.ineq_upper_only
353 );
354 println!();
355}
356
357pub fn print_summary(
358 status: ApplicationReturnStatus,
359 stats: &SolveStatistics,
360 counters: &CountingTnlp,
361) {
362 println!();
363 println!();
364 println!("Number of Iterations....: {}", stats.iteration_count);
365 println!();
366 println!(" (scaled) (unscaled)");
367 let row = |label: &str, scaled: f64, unscaled: f64| {
368 println!(
369 "{label}: {} {}",
370 fmt_ipopt(scaled),
371 fmt_ipopt(unscaled)
372 );
373 };
374 row(
375 "Objective...............",
376 stats.final_scaled_objective,
377 stats.final_objective,
378 );
379 row(
380 "Dual infeasibility......",
381 stats.final_dual_inf,
382 stats.final_dual_inf,
383 );
384 row(
385 "Constraint violation....",
386 stats.final_constr_viol,
387 stats.final_constr_viol,
388 );
389 row("Variable bound violation", 0.0, 0.0);
390 row(
391 "Complementarity.........",
392 stats.final_compl,
393 stats.final_compl,
394 );
395 row(
396 "Overall NLP error.......",
397 stats.final_kkt_error,
398 stats.final_kkt_error,
399 );
400 println!();
401 println!();
402 println!(
403 "Number of objective function evaluations = {}",
404 counters.n_obj.get()
405 );
406 println!(
407 "Number of objective gradient evaluations = {}",
408 counters.n_grad_f.get()
409 );
410 println!(
411 "Number of equality constraint evaluations = {}",
412 counters.n_g.get()
413 );
414 println!(
415 "Number of inequality constraint evaluations = {}",
416 counters.n_g.get()
417 );
418 println!(
419 "Number of equality constraint Jacobian evaluations = {}",
420 counters.n_jac_g.get()
421 );
422 println!(
423 "Number of inequality constraint Jacobian evaluations = {}",
424 counters.n_jac_g.get()
425 );
426 println!(
427 "Number of Lagrangian Hessian evaluations = {}",
428 counters.n_h.get()
429 );
430 println!(
431 "Total seconds in POUNCE = {:.3}",
432 stats.total_wallclock_time_secs
433 );
434 println!();
435 println!("EXIT: {}", status_message(status));
436 println!();
437 println!(
438 "POUNCE {}: {}",
439 env!("CARGO_PKG_VERSION"),
440 status_message(status)
441 );
442}
443
444pub fn fmt_ipopt(v: f64) -> String {
449 if v.is_nan() {
450 return "nan".to_string();
451 }
452 if v.is_infinite() {
453 return if v > 0.0 { "inf".into() } else { "-inf".into() };
454 }
455 let s = format!("{:.16e}", v);
456 let Some(e_pos) = s.rfind('e') else {
457 return s;
458 };
459 let (mantissa, exp_part) = s.split_at(e_pos);
460 let exp_str = &exp_part[1..];
461 let (sign, digits) = if let Some(rest) = exp_str.strip_prefix('-') {
462 ('-', rest)
463 } else if let Some(rest) = exp_str.strip_prefix('+') {
464 ('+', rest)
465 } else {
466 ('+', exp_str)
467 };
468 let padded = if digits.len() < 2 {
469 format!("0{digits}")
470 } else {
471 digits.to_string()
472 };
473 format!("{mantissa}e{sign}{padded}")
474}
475
476pub fn status_message(s: ApplicationReturnStatus) -> &'static str {
477 match s {
478 ApplicationReturnStatus::SolveSucceeded => "Optimal Solution Found.",
479 ApplicationReturnStatus::SolvedToAcceptableLevel => "Solved To Acceptable Level.",
480 ApplicationReturnStatus::InfeasibleProblemDetected => {
481 "Converged to a point of local infeasibility. Problem may be infeasible."
482 }
483 ApplicationReturnStatus::SearchDirectionBecomesTooSmall => {
484 "Search Direction is becoming Too Small."
485 }
486 ApplicationReturnStatus::DivergingIterates => {
487 "Iterates diverging; problem might be unbounded."
488 }
489 ApplicationReturnStatus::UserRequestedStop => "Stopping optimization at user request.",
490 ApplicationReturnStatus::FeasiblePointFound => "Feasible Point Found.",
491 ApplicationReturnStatus::MaximumIterationsExceeded => {
492 "Maximum Number of Iterations Exceeded."
493 }
494 ApplicationReturnStatus::RestorationFailed => "Restoration Failed!",
495 ApplicationReturnStatus::ErrorInStepComputation => "Error in step computation.",
496 ApplicationReturnStatus::MaximumCpuTimeExceeded => "Maximum CPU time exceeded.",
497 ApplicationReturnStatus::MaximumWallTimeExceeded => "Maximum wallclock time exceeded.",
498 ApplicationReturnStatus::NotEnoughDegreesOfFreedom => "Not Enough Degrees of Freedom.",
499 ApplicationReturnStatus::InvalidProblemDefinition => "Invalid Problem Definition.",
500 ApplicationReturnStatus::InvalidOption => "Invalid Option.",
501 ApplicationReturnStatus::InvalidNumberDetected => {
502 "Invalid number in NLP function or derivative detected."
503 }
504 ApplicationReturnStatus::UnrecoverableException => "Unrecoverable Exception.",
505 ApplicationReturnStatus::NonIpoptExceptionThrown => "Exception of type generic.",
506 ApplicationReturnStatus::InsufficientMemory => "Insufficient memory.",
507 ApplicationReturnStatus::InternalError => "INTERNAL ERROR: Unknown SolverReturn value.",
508 }
509}