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