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