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) => ineq_both += 1,
95 }
96 }
97 }
98
99 let nnz_total = info.nnz_jac_g as usize;
101 let (mut nnz_jac_eq, mut nnz_jac_ineq) = (0, 0);
102 if nnz_total > 0 && m > 0 {
103 let mut irow = vec![0_i32; nnz_total];
104 let mut jcol = vec![0_i32; nnz_total];
105 if t.eval_jac_g(
106 None,
107 true,
108 SparsityRequest::Structure {
109 irow: &mut irow,
110 jcol: &mut jcol,
111 },
112 ) {
113 let one_based = matches!(info.index_style, pounce_nlp::tnlp::IndexStyle::Fortran);
114 for &r in &irow {
115 let row = if one_based {
116 (r - 1) as usize
117 } else {
118 r as usize
119 };
120 if row < m && row_is_eq[row] {
121 nnz_jac_eq += 1;
122 } else {
123 nnz_jac_ineq += 1;
124 }
125 }
126 }
127 }
128
129 Some(ProblemStats {
130 n: info.n,
131 m: info.m,
132 nnz_jac_eq,
133 nnz_jac_ineq,
134 nnz_h_lag: info.nnz_h_lag,
135 var_lower_only,
136 var_upper_only,
137 var_both,
138 var_free,
139 n_eq,
140 n_ineq,
141 ineq_lower_only,
142 ineq_upper_only,
143 ineq_both,
144 })
145}
146
147const LOGO: [&str; 5] = [
149 "#### ### # # # # #### #####",
150 "# # # # # # ## # # # ",
151 "#### # # # # # # # # #### ",
152 "# # # # # # ## # # ",
153 "# ### ### # # #### #####",
154];
155
156const BANNER_WIDTH: usize = 80;
160
161pub fn print_logo() {
172 use std::io::Write as _;
173 let width = LOGO
174 .iter()
175 .map(|l| l.chars().count())
176 .max()
177 .unwrap_or(1)
178 .max(2);
179 let mut out = anstream::stdout();
180 let _ = writeln!(out, "{}", "*".repeat(BANNER_WIDTH));
185 let _ = writeln!(out);
186 let pad = " ".repeat(BANNER_WIDTH.saturating_sub(width) / 2);
187 for row in logo_rows(true) {
188 let _ = writeln!(out, "{pad}{row}");
189 }
190 let _ = writeln!(out);
191}
192
193pub fn logo_rows(color: bool) -> Vec<String> {
199 use pounce_common::style::{downgrade, truecolor_enabled, ALPHA_HOT, BRIGHT_YEL, TIGER_ORANGE};
200
201 fn lerp(a: u8, b: u8, t: f64) -> u8 {
202 (a as f64 + (b as f64 - a as f64) * t)
203 .round()
204 .clamp(0.0, 255.0) as u8
205 }
206 fn mix(a: anstyle::RgbColor, b: anstyle::RgbColor, t: f64) -> anstyle::RgbColor {
207 anstyle::RgbColor(lerp(a.0, b.0, t), lerp(a.1, b.1, t), lerp(a.2, b.2, t))
208 }
209 const STEEL_HI: anstyle::RgbColor = anstyle::RgbColor(0xd2, 0xd6, 0xdc);
212 const STEEL_LO: anstyle::RgbColor = anstyle::RgbColor(0x5c, 0x60, 0x68);
213
214 let rows = LOGO.len();
215 let width = LOGO
216 .iter()
217 .map(|l| l.chars().count())
218 .max()
219 .unwrap_or(1)
220 .max(2);
221 let vfrac = |r: usize| {
222 if rows <= 1 {
223 0.0
224 } else {
225 r as f64 / (rows - 1) as f64
226 }
227 };
228 let molten = |r: usize| {
230 let t = vfrac(r);
231 if t < 0.5 {
232 mix(BRIGHT_YEL, TIGER_ORANGE, t / 0.5)
233 } else {
234 mix(TIGER_ORANGE, ALPHA_HOT, (t - 0.5) / 0.5)
235 }
236 };
237
238 let mut grid: Vec<Vec<Option<(char, anstyle::RgbColor)>>> = vec![vec![None; width]; rows];
239 for (r, line) in LOGO.iter().enumerate() {
240 let steel = mix(STEEL_HI, STEEL_LO, vfrac(r));
241 for (c, ch) in line.chars().enumerate() {
242 if ch != ' ' {
243 grid[r][c] = Some((ch, steel));
244 }
245 }
246 }
247 for &start in &[width / 4, width / 4 + 6, width / 4 + 12] {
249 for r in 0..rows {
250 let c = start + (rows - 1 - r);
251 if c < width {
252 grid[r][c] = Some(('/', molten(r)));
253 }
254 }
255 }
256
257 let truecolor = truecolor_enabled();
258 grid.iter()
259 .map(|row| {
260 let mut rendered = String::new();
261 for cell in row {
262 match cell {
263 Some((ch, rgb)) if color => {
264 let style = anstyle::Style::new()
265 .bold()
266 .fg_color(Some(downgrade(*rgb, truecolor)));
267 rendered.push_str(&format!(
268 "{}{}{}",
269 style.render(),
270 ch,
271 style.render_reset()
272 ));
273 }
274 Some((ch, _)) => rendered.push(*ch),
275 None => rendered.push(' '),
276 }
277 }
278 rendered.trim_end().to_string()
279 })
280 .collect()
281}
282
283pub fn print_banner(linear_solver: &str) {
284 use std::io::IsTerminal as _;
285
286 const URL: &str = "https://github.com/jkitchin/pounce";
289 let link = if std::io::stdout().is_terminal() {
290 format!("\x1b]8;;{URL}\x1b\\{URL}\x1b]8;;\x1b\\")
291 } else {
292 URL.to_string()
293 };
294
295 let rule = "*".repeat(BANNER_WIDTH);
296 println!("{rule}");
297 println!("This program contains POUNCE, a pure-Rust interior-point optimization solver");
298 println!("for nonlinear, conic, and global problems (its NLP core is ported from Ipopt).");
299 println!("Released under the Eclipse Public License (EPL) — drop-in compatible with Ipopt.");
300 println!(" For more information visit {link}");
301 println!("{rule}");
302 println!();
303 println!(
304 "This is POUNCE version {}, running with linear solver {}.",
305 env!("CARGO_PKG_VERSION"),
306 linear_solver
307 );
308 println!();
309}
310
311pub fn print_problem_stats(s: &ProblemStats) {
312 println!(
313 "Number of nonzeros in equality constraint Jacobian...: {:>8}",
314 s.nnz_jac_eq
315 );
316 println!(
317 "Number of nonzeros in inequality constraint Jacobian.: {:>8}",
318 s.nnz_jac_ineq
319 );
320 println!(
321 "Number of nonzeros in Lagrangian Hessian.............: {:>8}",
322 s.nnz_h_lag
323 );
324 println!();
325 println!(
326 "Total number of variables............................: {:>8}",
327 s.n
328 );
329 println!(
330 " variables with only lower bounds: {:>8}",
331 s.var_lower_only
332 );
333 println!(
334 " variables with lower and upper bounds: {:>8}",
335 s.var_both
336 );
337 println!(
338 " variables with only upper bounds: {:>8}",
339 s.var_upper_only
340 );
341 println!(
342 "Total number of equality constraints.................: {:>8}",
343 s.n_eq
344 );
345 println!(
346 "Total number of inequality constraints...............: {:>8}",
347 s.n_ineq
348 );
349 println!(
350 " inequality constraints with only lower bounds: {:>8}",
351 s.ineq_lower_only
352 );
353 println!(
354 " inequality constraints with lower and upper bounds: {:>8}",
355 s.ineq_both
356 );
357 println!(
358 " inequality constraints with only upper bounds: {:>8}",
359 s.ineq_upper_only
360 );
361 println!();
362}
363
364pub fn print_summary(
365 status: ApplicationReturnStatus,
366 stats: &SolveStatistics,
367 counters: &CountingTnlp,
368) {
369 println!();
370 println!();
371 println!("Number of Iterations....: {}", stats.iteration_count);
372 println!();
373 println!(" (scaled) (unscaled)");
374 let row = |label: &str, scaled: f64, unscaled: f64| {
375 println!(
376 "{label}: {} {}",
377 fmt_ipopt(scaled),
378 fmt_ipopt(unscaled)
379 );
380 };
381 row(
382 "Objective...............",
383 stats.final_scaled_objective,
384 stats.final_objective,
385 );
386 row(
387 "Dual infeasibility......",
388 stats.final_dual_inf,
389 stats.final_dual_inf,
390 );
391 row(
392 "Constraint violation....",
393 stats.final_constr_viol,
394 stats.final_constr_viol,
395 );
396 row("Variable bound violation", 0.0, 0.0);
397 row(
398 "Complementarity.........",
399 stats.final_compl,
400 stats.final_compl,
401 );
402 row(
403 "Overall NLP error.......",
404 stats.final_kkt_error,
405 stats.final_kkt_error,
406 );
407 println!();
408 println!();
409 println!(
410 "Number of objective function evaluations = {}",
411 counters.n_obj.get()
412 );
413 println!(
414 "Number of objective gradient evaluations = {}",
415 counters.n_grad_f.get()
416 );
417 println!(
418 "Number of equality constraint evaluations = {}",
419 counters.n_g.get()
420 );
421 println!(
422 "Number of inequality constraint evaluations = {}",
423 counters.n_g.get()
424 );
425 println!(
426 "Number of equality constraint Jacobian evaluations = {}",
427 counters.n_jac_g.get()
428 );
429 println!(
430 "Number of inequality constraint Jacobian evaluations = {}",
431 counters.n_jac_g.get()
432 );
433 println!(
434 "Number of Lagrangian Hessian evaluations = {}",
435 counters.n_h.get()
436 );
437 println!(
438 "Total seconds in POUNCE = {:.3}",
439 stats.total_wallclock_time_secs
440 );
441 println!();
442 println!("EXIT: {}", status_message(status));
443 println!();
444 println!(
445 "POUNCE {}: {}",
446 env!("CARGO_PKG_VERSION"),
447 status_message(status)
448 );
449}
450
451pub fn print_convex_summary(
463 iterations: usize,
464 objective: f64,
465 primal_inf: f64,
466 dual_inf: f64,
467 complementarity: f64,
468 kkt_error: f64,
469) {
470 println!();
471 println!();
472 println!("Number of Iterations....: {iterations}");
473 println!();
474 println!(" (scaled) (unscaled)");
475 let row = |label: &str, v: f64| {
476 println!("{label}: {} {}", fmt_ipopt(v), fmt_ipopt(v));
477 };
478 row("Objective...............", objective);
479 row("Dual infeasibility......", dual_inf);
480 row("Constraint violation....", primal_inf);
481 row("Variable bound violation", 0.0);
482 row("Complementarity.........", complementarity);
483 row("Overall NLP error.......", kkt_error);
484 println!();
485}
486
487pub fn fmt_ipopt(v: f64) -> String {
492 if v.is_nan() {
493 return "nan".to_string();
494 }
495 if v.is_infinite() {
496 return if v > 0.0 { "inf".into() } else { "-inf".into() };
497 }
498 let s = format!("{:.16e}", v);
499 let Some(e_pos) = s.rfind('e') else {
500 return s;
501 };
502 let (mantissa, exp_part) = s.split_at(e_pos);
503 let exp_str = &exp_part[1..];
504 let (sign, digits) = if let Some(rest) = exp_str.strip_prefix('-') {
505 ('-', rest)
506 } else if let Some(rest) = exp_str.strip_prefix('+') {
507 ('+', rest)
508 } else {
509 ('+', exp_str)
510 };
511 let padded = if digits.len() < 2 {
512 format!("0{digits}")
513 } else {
514 digits.to_string()
515 };
516 format!("{mantissa}e{sign}{padded}")
517}
518
519pub fn status_message(s: ApplicationReturnStatus) -> &'static str {
520 match s {
521 ApplicationReturnStatus::SolveSucceeded => "Optimal Solution Found.",
522 ApplicationReturnStatus::SolvedToAcceptableLevel => "Solved To Acceptable Level.",
523 ApplicationReturnStatus::InfeasibleProblemDetected => {
524 "Converged to a point of local infeasibility. Problem may be infeasible."
525 }
526 ApplicationReturnStatus::SearchDirectionBecomesTooSmall => {
527 "Search Direction is becoming Too Small."
528 }
529 ApplicationReturnStatus::DivergingIterates => {
530 "Iterates diverging; problem might be unbounded."
531 }
532 ApplicationReturnStatus::UserRequestedStop => "Stopping optimization at user request.",
533 ApplicationReturnStatus::FeasiblePointFound => "Feasible Point Found.",
534 ApplicationReturnStatus::MaximumIterationsExceeded => {
535 "Maximum Number of Iterations Exceeded."
536 }
537 ApplicationReturnStatus::RestorationFailed => "Restoration Failed!",
538 ApplicationReturnStatus::ErrorInStepComputation => "Error in step computation.",
539 ApplicationReturnStatus::MaximumCpuTimeExceeded => "Maximum CPU time exceeded.",
540 ApplicationReturnStatus::MaximumWallTimeExceeded => "Maximum wallclock time exceeded.",
541 ApplicationReturnStatus::NotEnoughDegreesOfFreedom => "Not Enough Degrees of Freedom.",
542 ApplicationReturnStatus::InvalidProblemDefinition => "Invalid Problem Definition.",
543 ApplicationReturnStatus::InvalidOption => "Invalid Option.",
544 ApplicationReturnStatus::InvalidNumberDetected => {
545 "Invalid number in NLP function or derivative detected."
546 }
547 ApplicationReturnStatus::UnrecoverableException => "Unrecoverable Exception.",
548 ApplicationReturnStatus::NonIpoptExceptionThrown => "Exception of type generic.",
549 ApplicationReturnStatus::InsufficientMemory => "Insufficient memory.",
550 ApplicationReturnStatus::InternalError => "INTERNAL ERROR: Unknown SolverReturn value.",
551 }
552}
553
554#[cfg(test)]
555mod inequality_tally_tests {
556 use super::*;
562 use pounce_common::types::{Index, Number};
563 use pounce_nlp::tnlp::{IndexStyle, IpoptCq, IpoptData, Solution, StartingPoint};
564
565 struct FreeIneqRow;
569 impl TNLP for FreeIneqRow {
570 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
571 Some(NlpInfo {
572 n: 2,
573 m: 3,
574 nnz_jac_g: 3,
575 nnz_h_lag: 0,
576 index_style: IndexStyle::C,
577 })
578 }
579 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
580 b.x_l.iter_mut().for_each(|v| *v = -BOUND_INF);
581 b.x_u.iter_mut().for_each(|v| *v = BOUND_INF);
582 b.g_l[0] = 0.0;
584 b.g_u[0] = BOUND_INF;
585 b.g_l[1] = 0.0;
587 b.g_u[1] = 1.0;
588 b.g_l[2] = -BOUND_INF;
590 b.g_u[2] = BOUND_INF;
591 true
592 }
593 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
594 sp.x.iter_mut().for_each(|v| *v = 0.0);
595 true
596 }
597 fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
598 Some(0.0)
599 }
600 fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, grad_f: &mut [Number]) -> bool {
601 grad_f.iter_mut().for_each(|v| *v = 0.0);
602 true
603 }
604 fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
605 g.iter_mut().for_each(|v| *v = 0.0);
606 true
607 }
608 fn eval_jac_g(
609 &mut self,
610 _x: Option<&[Number]>,
611 _new_x: bool,
612 mode: SparsityRequest<'_>,
613 ) -> bool {
614 match mode {
615 SparsityRequest::Structure { irow, jcol } => {
616 irow.copy_from_slice(&[0, 1, 2]);
619 jcol.copy_from_slice(&[0, 0, 0]);
620 }
621 SparsityRequest::Values { values } => {
622 values.copy_from_slice(&[1.0, 1.0, 1.0]);
623 }
624 }
625 true
626 }
627 fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
628 }
629
630 #[test]
631 fn free_inequality_row_keeps_breakdown_summing_to_total() {
632 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(FreeIneqRow));
633 let s = collect_stats(&tnlp).expect("collect_stats succeeds");
634
635 assert_eq!(s.n_eq, 0, "no equality rows");
636 assert_eq!(s.n_ineq, 3, "all three rows are inequalities");
637 let bucket_sum: Index = s.ineq_lower_only + s.ineq_both + s.ineq_upper_only;
640 assert_eq!(
641 bucket_sum, s.n_ineq,
642 "ineq bound-type breakdown ({} lower + {} both + {} upper) must sum to n_ineq={}",
643 s.ineq_lower_only, s.ineq_both, s.ineq_upper_only, s.n_ineq
644 );
645 assert_eq!(s.ineq_lower_only, 1);
648 assert_eq!(s.ineq_upper_only, 0);
649 assert_eq!(s.ineq_both, 2);
650 }
651}