1use pounce_common::types::Number;
10use pounce_nlp::return_codes::ApplicationReturnStatus;
11use pounce_nlp::solve_statistics::SolveStatistics;
12use pounce_nlp::tnlp::{IndexStyle, NlpInfo, SparsityRequest, TNLP};
13use pounce_nlp::tnlp_adapter::{FixedVarTreatment, TNLPAdapter};
14use std::cell::RefCell;
15use std::rc::Rc;
16
17#[cfg(test)]
21const BOUND_INF: f64 = 1.0e19;
22
23#[derive(Debug, Clone, Copy)]
24pub struct ProblemStats {
25 pub n: i32,
26 pub m: i32,
27 pub nnz_jac_eq: i32,
28 pub nnz_jac_ineq: i32,
29 pub nnz_h_lag: i32,
30 pub var_lower_only: i32,
31 pub var_upper_only: i32,
32 pub var_both: i32,
33 pub var_free: i32,
34 pub n_eq: i32,
35 pub n_ineq: i32,
36 pub ineq_lower_only: i32,
37 pub ineq_upper_only: i32,
38 pub ineq_both: i32,
39}
40
41pub fn collect_stats(
56 tnlp: &Rc<RefCell<dyn TNLP>>,
57 lo_inf: Number,
58 up_inf: Number,
59 fixed_treatment: FixedVarTreatment,
60) -> Option<ProblemStats> {
61 let adapter =
62 TNLPAdapter::new_with_options(Rc::clone(tnlp), lo_inf, up_inf, fixed_treatment).ok()?;
63 let cls = adapter.classification();
64 let info: NlpInfo = *adapter.nlp_info();
65 let n_full_x = cls.n_full_x as usize;
66 let m = info.m as usize;
67 let one_based = matches!(info.index_style, IndexStyle::Fortran);
68
69 let nv = cls.n_x_var() as usize;
75 let mut has_l = vec![false; nv];
76 let mut has_u = vec![false; nv];
77 for &p in &cls.x_l_map {
78 has_l[p as usize] = true;
79 }
80 for &p in &cls.x_u_map {
81 has_u[p as usize] = true;
82 }
83 let (mut var_lower_only, mut var_upper_only, mut var_both, mut var_free) = (0, 0, 0, 0);
84 for k in 0..nv {
85 match (has_l[k], has_u[k]) {
86 (true, true) => var_both += 1,
87 (true, false) => var_lower_only += 1,
88 (false, true) => var_upper_only += 1,
89 (false, false) => var_free += 1,
90 }
91 }
92
93 let n_eq = cls.n_c;
96 let n_ineq = cls.n_d;
97 let nd = cls.n_d as usize;
98 let mut has_dl = vec![false; nd];
99 let mut has_du = vec![false; nd];
100 for &p in &cls.d_l_map {
101 has_dl[p as usize] = true;
102 }
103 for &p in &cls.d_u_map {
104 has_du[p as usize] = true;
105 }
106 let (mut ineq_lower_only, mut ineq_upper_only, mut ineq_both) = (0, 0, 0);
107 for k in 0..nd {
108 match (has_dl[k], has_du[k]) {
109 (true, true) => ineq_both += 1,
110 (true, false) => ineq_lower_only += 1,
111 (false, true) => ineq_upper_only += 1,
112 (false, false) => ineq_both += 1,
117 }
118 }
119
120 let mut row_is_eq = vec![false; m];
122 for &r in &cls.c_map {
123 row_is_eq[r as usize] = true;
124 }
125
126 let nnz_total = info.nnz_jac_g as usize;
129 let (mut nnz_jac_eq, mut nnz_jac_ineq) = (0, 0);
130 if nnz_total > 0 && m > 0 {
131 let mut irow = vec![0_i32; nnz_total];
132 let mut jcol = vec![0_i32; nnz_total];
133 let mut t = tnlp.borrow_mut();
134 if t.eval_jac_g(
135 None,
136 true,
137 SparsityRequest::Structure {
138 irow: &mut irow,
139 jcol: &mut jcol,
140 },
141 ) {
142 for k in 0..nnz_total {
143 let col = if one_based {
144 (jcol[k] - 1) as usize
145 } else {
146 jcol[k] as usize
147 };
148 if col >= n_full_x || cls.full_to_var[col] < 0 {
151 continue;
152 }
153 let row = if one_based {
154 (irow[k] - 1) as usize
155 } else {
156 irow[k] as usize
157 };
158 if row < m && row_is_eq[row] {
159 nnz_jac_eq += 1;
160 } else {
161 nnz_jac_ineq += 1;
162 }
163 }
164 }
165 }
166
167 let mut nnz_h_lag = info.nnz_h_lag;
171 let nnz_h = info.nnz_h_lag as usize;
172 if nnz_h > 0 {
173 let mut irow = vec![0_i32; nnz_h];
174 let mut jcol = vec![0_i32; nnz_h];
175 let mut t = tnlp.borrow_mut();
176 if t.eval_h(
177 None,
178 true,
179 1.0,
180 None,
181 true,
182 SparsityRequest::Structure {
183 irow: &mut irow,
184 jcol: &mut jcol,
185 },
186 ) {
187 let mut kept = 0_i32;
188 for k in 0..nnz_h {
189 let r = if one_based {
190 (irow[k] - 1) as usize
191 } else {
192 irow[k] as usize
193 };
194 let c = if one_based {
195 (jcol[k] - 1) as usize
196 } else {
197 jcol[k] as usize
198 };
199 if r < n_full_x
200 && c < n_full_x
201 && cls.full_to_var[r] >= 0
202 && cls.full_to_var[c] >= 0
203 {
204 kept += 1;
205 }
206 }
207 nnz_h_lag = kept;
208 }
209 }
210
211 Some(ProblemStats {
212 n: cls.n_x_var(),
213 m: info.m,
214 nnz_jac_eq,
215 nnz_jac_ineq,
216 nnz_h_lag,
217 var_lower_only,
218 var_upper_only,
219 var_both,
220 var_free,
221 n_eq,
222 n_ineq,
223 ineq_lower_only,
224 ineq_upper_only,
225 ineq_both,
226 })
227}
228
229const LOGO: [&str; 5] = [
231 "#### ### # # # # #### #####",
232 "# # # # # # ## # # # ",
233 "#### # # # # # # # # #### ",
234 "# # # # # # ## # # ",
235 "# ### ### # # #### #####",
236];
237
238const BANNER_WIDTH: usize = 80;
242
243pub fn print_logo() {
254 use std::io::Write as _;
255 let width = LOGO
256 .iter()
257 .map(|l| l.chars().count())
258 .max()
259 .unwrap_or(1)
260 .max(2);
261 let mut out = anstream::stdout();
262 let _ = writeln!(out, "{}", "*".repeat(BANNER_WIDTH));
267 let _ = writeln!(out);
268 let pad = " ".repeat(BANNER_WIDTH.saturating_sub(width) / 2);
269 for row in logo_rows(true) {
270 let _ = writeln!(out, "{pad}{row}");
271 }
272 let _ = writeln!(out);
273}
274
275pub fn logo_rows(color: bool) -> Vec<String> {
281 use pounce_common::style::{ALPHA_HOT, BRIGHT_YEL, TIGER_ORANGE, downgrade, truecolor_enabled};
282
283 fn lerp(a: u8, b: u8, t: f64) -> u8 {
284 (a as f64 + (b as f64 - a as f64) * t)
285 .round()
286 .clamp(0.0, 255.0) as u8
287 }
288 fn mix(a: anstyle::RgbColor, b: anstyle::RgbColor, t: f64) -> anstyle::RgbColor {
289 anstyle::RgbColor(lerp(a.0, b.0, t), lerp(a.1, b.1, t), lerp(a.2, b.2, t))
290 }
291 const STEEL_HI: anstyle::RgbColor = anstyle::RgbColor(0xd2, 0xd6, 0xdc);
294 const STEEL_LO: anstyle::RgbColor = anstyle::RgbColor(0x5c, 0x60, 0x68);
295
296 let rows = LOGO.len();
297 let width = LOGO
298 .iter()
299 .map(|l| l.chars().count())
300 .max()
301 .unwrap_or(1)
302 .max(2);
303 let vfrac = |r: usize| {
304 if rows <= 1 {
305 0.0
306 } else {
307 r as f64 / (rows - 1) as f64
308 }
309 };
310 let molten = |r: usize| {
312 let t = vfrac(r);
313 if t < 0.5 {
314 mix(BRIGHT_YEL, TIGER_ORANGE, t / 0.5)
315 } else {
316 mix(TIGER_ORANGE, ALPHA_HOT, (t - 0.5) / 0.5)
317 }
318 };
319
320 let mut grid: Vec<Vec<Option<(char, anstyle::RgbColor)>>> = vec![vec![None; width]; rows];
321 for (r, line) in LOGO.iter().enumerate() {
322 let steel = mix(STEEL_HI, STEEL_LO, vfrac(r));
323 for (c, ch) in line.chars().enumerate() {
324 if ch != ' ' {
325 grid[r][c] = Some((ch, steel));
326 }
327 }
328 }
329 for &start in &[width / 4, width / 4 + 6, width / 4 + 12] {
331 for r in 0..rows {
332 let c = start + (rows - 1 - r);
333 if c < width {
334 grid[r][c] = Some(('/', molten(r)));
335 }
336 }
337 }
338
339 let truecolor = truecolor_enabled();
340 grid.iter()
341 .map(|row| {
342 let mut rendered = String::new();
343 for cell in row {
344 match cell {
345 Some((ch, rgb)) if color => {
346 let style = anstyle::Style::new()
347 .bold()
348 .fg_color(Some(downgrade(*rgb, truecolor)));
349 rendered.push_str(&format!(
350 "{}{}{}",
351 style.render(),
352 ch,
353 style.render_reset()
354 ));
355 }
356 Some((ch, _)) => rendered.push(*ch),
357 None => rendered.push(' '),
358 }
359 }
360 rendered.trim_end().to_string()
361 })
362 .collect()
363}
364
365pub fn print_banner(linear_solver: &str) {
366 use std::io::IsTerminal as _;
367
368 const URL: &str = "https://github.com/jkitchin/pounce";
371 let link = if std::io::stdout().is_terminal() {
372 format!("\x1b]8;;{URL}\x1b\\{URL}\x1b]8;;\x1b\\")
373 } else {
374 URL.to_string()
375 };
376
377 let rule = "*".repeat(BANNER_WIDTH);
378 println!("{rule}");
379 println!("This program contains POUNCE, a pure-Rust interior-point optimization solver");
380 println!("for nonlinear, conic, and global problems (its NLP core is ported from Ipopt).");
381 println!("Released under the Eclipse Public License (EPL) — drop-in compatible with Ipopt.");
382 println!(" For more information visit {link}");
383 println!("{rule}");
384 println!();
385 println!(
386 "This is POUNCE version {}, running with linear solver {}.",
387 env!("CARGO_PKG_VERSION"),
388 linear_solver
389 );
390 println!();
391}
392
393pub fn print_problem_stats(s: &ProblemStats) {
394 println!(
395 "Number of nonzeros in equality constraint Jacobian...: {:>8}",
396 s.nnz_jac_eq
397 );
398 println!(
399 "Number of nonzeros in inequality constraint Jacobian.: {:>8}",
400 s.nnz_jac_ineq
401 );
402 println!(
403 "Number of nonzeros in Lagrangian Hessian.............: {:>8}",
404 s.nnz_h_lag
405 );
406 println!();
407 println!(
408 "Total number of variables............................: {:>8}",
409 s.n
410 );
411 println!(
412 " variables with only lower bounds: {:>8}",
413 s.var_lower_only
414 );
415 println!(
416 " variables with lower and upper bounds: {:>8}",
417 s.var_both
418 );
419 println!(
420 " variables with only upper bounds: {:>8}",
421 s.var_upper_only
422 );
423 println!(
424 "Total number of equality constraints.................: {:>8}",
425 s.n_eq
426 );
427 println!(
428 "Total number of inequality constraints...............: {:>8}",
429 s.n_ineq
430 );
431 println!(
432 " inequality constraints with only lower bounds: {:>8}",
433 s.ineq_lower_only
434 );
435 println!(
436 " inequality constraints with lower and upper bounds: {:>8}",
437 s.ineq_both
438 );
439 println!(
440 " inequality constraints with only upper bounds: {:>8}",
441 s.ineq_upper_only
442 );
443 println!();
444}
445
446#[derive(Debug, Clone, Copy, Default)]
450pub struct EvalCounts {
451 pub n_obj: u64,
452 pub n_grad_f: u64,
453 pub n_g: u64,
454 pub n_jac_g: u64,
455 pub n_h: u64,
456}
457
458pub fn print_summary(
459 status: ApplicationReturnStatus,
460 stats: &SolveStatistics,
461 counters: &EvalCounts,
462) {
463 println!();
464 println!();
465 println!("Number of Iterations....: {}", stats.iteration_count);
466 println!();
467 println!(" (scaled) (unscaled)");
468 let row = |label: &str, scaled: f64, unscaled: f64| {
469 println!(
470 "{label}: {} {}",
471 fmt_ipopt(scaled),
472 fmt_ipopt(unscaled)
473 );
474 };
475 row(
476 "Objective...............",
477 stats.final_scaled_objective,
478 stats.final_objective,
479 );
480 row(
490 "Dual infeasibility......",
491 stats.final_dual_inf,
492 stats.final_unscaled_dual_inf,
493 );
494 row(
495 "Constraint violation....",
496 stats.final_constr_viol,
497 stats.final_unscaled_constr_viol,
498 );
499 row("Variable bound violation", 0.0, 0.0);
500 row(
501 "Complementarity.........",
502 stats.final_compl,
503 stats.final_unscaled_compl,
504 );
505 row(
506 "Overall NLP error.......",
507 stats.final_kkt_error,
508 stats.final_unscaled_kkt_error,
509 );
510 if stats.final_kkt_error_above_noise.is_finite()
517 && stats.final_kkt_error.is_finite()
518 && stats.final_kkt_error_above_noise != stats.final_kkt_error
519 {
520 println!(
521 " ...above the per-row floating-point noise floor: {}",
522 fmt_ipopt(stats.final_kkt_error_above_noise),
523 );
524 println!(
525 " (the strict convergence test judges this value; the residual \
526 below it is finer than the row's own arithmetic can resolve. \
527 Set primal_noise_floor_kappa = 0 to disable.)"
528 );
529 }
530 println!();
531 println!();
532 println!(
533 "Number of objective function evaluations = {}",
534 counters.n_obj
535 );
536 println!(
537 "Number of objective gradient evaluations = {}",
538 counters.n_grad_f
539 );
540 println!(
541 "Number of equality constraint evaluations = {}",
542 counters.n_g
543 );
544 println!(
545 "Number of inequality constraint evaluations = {}",
546 counters.n_g
547 );
548 println!(
549 "Number of equality constraint Jacobian evaluations = {}",
550 counters.n_jac_g
551 );
552 println!(
553 "Number of inequality constraint Jacobian evaluations = {}",
554 counters.n_jac_g
555 );
556 println!(
557 "Number of Lagrangian Hessian evaluations = {}",
558 counters.n_h
559 );
560 println!(
561 "Total seconds in POUNCE = {:.3}",
562 stats.total_wallclock_time_secs
563 );
564 println!();
565 println!("EXIT: {}", status_message(status));
566 println!();
567 println!(
568 "POUNCE {}: {}",
569 env!("CARGO_PKG_VERSION"),
570 status_message(status)
571 );
572}
573
574pub fn print_convex_summary(
586 iterations: usize,
587 objective: f64,
588 primal_inf: f64,
589 dual_inf: f64,
590 complementarity: f64,
591 kkt_error: f64,
592) {
593 println!();
594 println!();
595 println!("Number of Iterations....: {iterations}");
596 println!();
597 println!(" (scaled) (unscaled)");
598 let row = |label: &str, v: f64| {
599 println!("{label}: {} {}", fmt_ipopt(v), fmt_ipopt(v));
600 };
601 row("Objective...............", objective);
602 row("Dual infeasibility......", dual_inf);
603 row("Constraint violation....", primal_inf);
604 row("Variable bound violation", 0.0);
605 row("Complementarity.........", complementarity);
606 row("Overall NLP error.......", kkt_error);
607 println!();
608}
609
610pub fn fmt_ipopt(v: f64) -> String {
615 if v.is_nan() {
616 return "nan".to_string();
617 }
618 if v.is_infinite() {
619 return if v > 0.0 { "inf".into() } else { "-inf".into() };
620 }
621 let s = format!("{:.16e}", v);
622 let Some(e_pos) = s.rfind('e') else {
623 return s;
624 };
625 let (mantissa, exp_part) = s.split_at(e_pos);
626 let exp_str = &exp_part[1..];
627 let (sign, digits) = if let Some(rest) = exp_str.strip_prefix('-') {
628 ('-', rest)
629 } else if let Some(rest) = exp_str.strip_prefix('+') {
630 ('+', rest)
631 } else {
632 ('+', exp_str)
633 };
634 let padded = if digits.len() < 2 {
635 format!("0{digits}")
636 } else {
637 digits.to_string()
638 };
639 format!("{mantissa}e{sign}{padded}")
640}
641
642pub fn status_message(s: ApplicationReturnStatus) -> &'static str {
643 match s {
644 ApplicationReturnStatus::SolveSucceeded => "Optimal Solution Found.",
645 ApplicationReturnStatus::SolvedToAcceptableLevel => "Solved To Acceptable Level.",
646 ApplicationReturnStatus::InfeasibleProblemDetected => {
647 "Converged to a point of local infeasibility. Problem may be infeasible."
648 }
649 ApplicationReturnStatus::SearchDirectionBecomesTooSmall => {
650 "Search Direction is becoming Too Small."
651 }
652 ApplicationReturnStatus::DivergingIterates => {
653 "Iterates diverging; problem might be unbounded."
654 }
655 ApplicationReturnStatus::UserRequestedStop => "Stopping optimization at user request.",
656 ApplicationReturnStatus::FeasiblePointFound => "Feasible Point Found.",
657 ApplicationReturnStatus::MaximumIterationsExceeded => {
658 "Maximum Number of Iterations Exceeded."
659 }
660 ApplicationReturnStatus::RestorationFailed => "Restoration Failed!",
661 ApplicationReturnStatus::ErrorInStepComputation => "Error in step computation.",
662 ApplicationReturnStatus::MaximumCpuTimeExceeded => "Maximum CPU time exceeded.",
663 ApplicationReturnStatus::MaximumWallTimeExceeded => "Maximum wallclock time exceeded.",
664 ApplicationReturnStatus::NotEnoughDegreesOfFreedom => "Not Enough Degrees of Freedom.",
665 ApplicationReturnStatus::InvalidProblemDefinition => "Invalid Problem Definition.",
666 ApplicationReturnStatus::InvalidOption => "Invalid Option.",
667 ApplicationReturnStatus::InvalidNumberDetected => {
668 "Invalid number in NLP function or derivative detected."
669 }
670 ApplicationReturnStatus::UnrecoverableException => "Unrecoverable Exception.",
671 ApplicationReturnStatus::NonIpoptExceptionThrown => "Exception of type generic.",
672 ApplicationReturnStatus::InsufficientMemory => "Insufficient memory.",
673 ApplicationReturnStatus::InternalError => "INTERNAL ERROR: Unknown SolverReturn value.",
674 }
675}
676
677#[cfg(test)]
678mod inequality_tally_tests {
679 use super::*;
685 use pounce_common::types::{Index, Number};
686 use pounce_nlp::tnlp::{BoundsInfo, IndexStyle, IpoptCq, IpoptData, Solution, StartingPoint};
687
688 struct FreeIneqRow;
692 impl TNLP for FreeIneqRow {
693 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
694 Some(NlpInfo {
695 n: 2,
696 m: 3,
697 nnz_jac_g: 3,
698 nnz_h_lag: 0,
699 index_style: IndexStyle::C,
700 })
701 }
702 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
703 b.x_l.iter_mut().for_each(|v| *v = -BOUND_INF);
704 b.x_u.iter_mut().for_each(|v| *v = BOUND_INF);
705 b.g_l[0] = 0.0;
707 b.g_u[0] = BOUND_INF;
708 b.g_l[1] = 0.0;
710 b.g_u[1] = 1.0;
711 b.g_l[2] = -BOUND_INF;
713 b.g_u[2] = BOUND_INF;
714 true
715 }
716 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
717 sp.x.iter_mut().for_each(|v| *v = 0.0);
718 true
719 }
720 fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
721 Some(0.0)
722 }
723 fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, grad_f: &mut [Number]) -> bool {
724 grad_f.iter_mut().for_each(|v| *v = 0.0);
725 true
726 }
727 fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
728 g.iter_mut().for_each(|v| *v = 0.0);
729 true
730 }
731 fn eval_jac_g(
732 &mut self,
733 _x: Option<&[Number]>,
734 _new_x: bool,
735 mode: SparsityRequest<'_>,
736 ) -> bool {
737 match mode {
738 SparsityRequest::Structure { irow, jcol } => {
739 irow.copy_from_slice(&[0, 1, 2]);
742 jcol.copy_from_slice(&[0, 0, 0]);
743 }
744 SparsityRequest::Values { values } => {
745 values.copy_from_slice(&[1.0, 1.0, 1.0]);
746 }
747 }
748 true
749 }
750 fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
751 }
752
753 #[test]
754 fn free_inequality_row_keeps_breakdown_summing_to_total() {
755 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(FreeIneqRow));
756 let s = collect_stats(
757 &tnlp,
758 -BOUND_INF,
759 BOUND_INF,
760 FixedVarTreatment::MakeParameter,
761 )
762 .expect("collect_stats succeeds");
763
764 assert_eq!(s.n_eq, 0, "no equality rows");
765 assert_eq!(s.n_ineq, 3, "all three rows are inequalities");
766 let bucket_sum: Index = s.ineq_lower_only + s.ineq_both + s.ineq_upper_only;
769 assert_eq!(
770 bucket_sum, s.n_ineq,
771 "ineq bound-type breakdown ({} lower + {} both + {} upper) must sum to n_ineq={}",
772 s.ineq_lower_only, s.ineq_both, s.ineq_upper_only, s.n_ineq
773 );
774 assert_eq!(s.ineq_lower_only, 1);
777 assert_eq!(s.ineq_upper_only, 0);
778 assert_eq!(s.ineq_both, 2);
779 }
780
781 struct OneFixedVar;
789 impl TNLP for OneFixedVar {
790 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
791 Some(NlpInfo {
792 n: 3,
793 m: 1,
794 nnz_jac_g: 3,
795 nnz_h_lag: 0,
796 index_style: IndexStyle::C,
797 })
798 }
799 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
800 b.x_l[0] = 0.0;
802 b.x_u[0] = BOUND_INF;
803 b.x_l[1] = 2.0;
804 b.x_u[1] = 2.0;
805 b.x_l[2] = -BOUND_INF;
806 b.x_u[2] = BOUND_INF;
807 b.g_l[0] = 0.0;
809 b.g_u[0] = 0.0;
810 true
811 }
812 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
813 sp.x.iter_mut().for_each(|v| *v = 0.0);
814 true
815 }
816 fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
817 Some(0.0)
818 }
819 fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, grad_f: &mut [Number]) -> bool {
820 grad_f.iter_mut().for_each(|v| *v = 0.0);
821 true
822 }
823 fn eval_g(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
824 g.iter_mut().for_each(|v| *v = 0.0);
825 true
826 }
827 fn eval_jac_g(
828 &mut self,
829 _x: Option<&[Number]>,
830 _new_x: bool,
831 mode: SparsityRequest<'_>,
832 ) -> bool {
833 match mode {
834 SparsityRequest::Structure { irow, jcol } => {
837 irow.copy_from_slice(&[0, 0, 0]);
838 jcol.copy_from_slice(&[0, 1, 2]);
839 }
840 SparsityRequest::Values { values } => {
841 values.copy_from_slice(&[1.0, 1.0, 1.0]);
842 }
843 }
844 true
845 }
846 fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
847 }
848
849 #[test]
850 fn make_parameter_banner_reports_reduced_problem() {
851 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneFixedVar));
852 let s = collect_stats(
853 &tnlp,
854 -BOUND_INF,
855 BOUND_INF,
856 FixedVarTreatment::MakeParameter,
857 )
858 .expect("collect_stats succeeds");
859
860 assert_eq!(s.n, 2, "fixed variable must be dropped from the total");
862 assert_eq!(s.var_both, 0, "fixed var must NOT count as lower-and-upper");
863 assert_eq!(s.var_lower_only, 1, "var 0 is lower-only");
864 assert_eq!(s.var_free, 1, "var 2 is free");
865 assert_eq!(s.var_upper_only, 0);
866 assert_eq!(s.n_eq, 1);
868 assert_eq!(
869 s.nnz_jac_eq, 2,
870 "fixed-var column dropped from the Jacobian"
871 );
872 assert_eq!(s.nnz_jac_ineq, 0);
873 }
874
875 #[test]
876 fn relax_bounds_banner_keeps_fixed_variable() {
877 let tnlp: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(OneFixedVar));
880 let s = collect_stats(&tnlp, -BOUND_INF, BOUND_INF, FixedVarTreatment::RelaxBounds)
881 .expect("collect_stats succeeds");
882
883 assert_eq!(s.n, 3, "relax_bounds keeps the fixed variable");
884 assert_eq!(
885 s.var_both, 1,
886 "fixed var reported as lower-and-upper bounded"
887 );
888 assert_eq!(s.var_lower_only, 1);
889 assert_eq!(s.var_free, 1);
890 assert_eq!(s.nnz_jac_eq, 3, "all columns retained under relax_bounds");
891 }
892}