Skip to main content

pounce_algorithm/output/
orig.rs

1//! Original iteration output — port of
2//! `Algorithm/IpOrigIterationOutput.{hpp,cpp}`.
3//!
4//! Column layout follows upstream's literal `Snprintf` schema but
5//! widens the `lg(mu)` / `lg(rg)` / e-format fields by one or two
6//! characters so that:
7//!
8//! * the e-format columns no longer wiggle by one character when a
9//!   value transitions between 1-digit and 2-digit exponent
10//!   magnitudes (`1.44e-7` vs `8.83e-13`), since [`format_e`] always
11//!   emits the C `%.Ne` form (signed, zero-padded 2-digit exponent);
12//! * each header label right-aligns exactly to the right edge of its
13//!   data column, instead of inheriting upstream's hand-rolled spacing
14//!   that left several labels off by 1–2 characters.
15
16use crate::ipopt_cq::IpoptCqHandle;
17use crate::ipopt_data::IpoptDataHandle;
18use crate::output::r#trait::IterationOutput;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PrintInfoString {
22    Yes,
23    No,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum InfPrTag {
28    Internal,
29    Original,
30}
31
32pub struct OrigIterationOutput {
33    pub print_info_string: PrintInfoString,
34    pub inf_pr_output: InfPrTag,
35    pub print_frequency_iter: i32,
36    pub print_frequency_time: f64,
37    /// Iteration index of the last header print; the upstream code
38    /// re-prints the header every 10 lines.
39    last_header_iter: i32,
40}
41
42impl Default for OrigIterationOutput {
43    fn default() -> Self {
44        Self {
45            print_info_string: PrintInfoString::No,
46            inf_pr_output: InfPrTag::Original,
47            print_frequency_iter: 1,
48            print_frequency_time: 0.0,
49            last_header_iter: -1,
50        }
51    }
52}
53
54impl OrigIterationOutput {
55    pub fn new() -> Self {
56        Self::default()
57    }
58
59    /// Header line printed every ten iterations. Each label is
60    /// right-aligned to the right edge of its data column under the
61    /// new widths (see module docs).
62    pub const HEADER: &'static str =
63        "iter      objective   inf_pr   inf_du lg(mu)    ||d|| lg(rg) alpha_du alpha_pr  ls\n";
64}
65
66impl IterationOutput for OrigIterationOutput {
67    fn write_output(&mut self) {
68        // Header-print bookkeeping; the actual emission is handled by
69        // `format_row`, which the caller wires to its journalist.
70        self.last_header_iter = 0;
71    }
72
73    /// Build the single-line iteration row. Field-for-field port of
74    /// the `Snprintf` block at `IpOrigIterationOutput.cpp:152`:
75    /// `"%4d %14.7e %7.2e %7.2e %5.1f %7.2e %5s %7.2e %7.2e%c%3d"`.
76    fn format_row(&mut self, data: &IpoptDataHandle, cq: &IpoptCqHandle) -> String {
77        let d = data.borrow();
78        let c = cq.borrow();
79
80        let iter = d.iter_count;
81        let unscaled_f = c.unscaled_curr_f();
82        let inf_pr = match self.inf_pr_output {
83            InfPrTag::Internal => c.curr_primal_infeasibility_max(),
84            InfPrTag::Original => c.curr_unscaled_nlp_constraint_violation_max(),
85        };
86        let inf_du = c.curr_dual_infeasibility_max();
87        let mu = d.curr_mu;
88        let lg_mu = mu.log10();
89
90        // ||d||_∞ over the (x, s) blocks of the latest search step.
91        let dnrm = match &d.delta {
92            Some(delta) => delta.x.amax().max(delta.s.amax()),
93            None => 0.0,
94        };
95
96        let regu_x = d.info_regu_x;
97        let regu_str: String = if regu_x == 0.0 {
98            "     -".to_string()
99        } else {
100            format!("{:6.1}", regu_x.log10())
101        };
102
103        let alpha_dual = d.info_alpha_dual;
104        let alpha_primal = d.info_alpha_primal;
105        let alpha_char = d.info_alpha_primal_char;
106        let ls_count = d.info_ls_count;
107
108        let mut row = format!(
109            "{:>4} {:>14} {:>8} {:>8} {:6.1} {:>8} {:>6} {:>8} {:>8}{}{:>3}",
110            iter,
111            format_e(unscaled_f, 7),
112            format_e(inf_pr, 2),
113            format_e(inf_du, 2),
114            lg_mu,
115            format_e(dnrm, 2),
116            regu_str,
117            format_e(alpha_dual, 2),
118            format_e(alpha_primal, 2),
119            alpha_char,
120            ls_count,
121        );
122        // `print_info_string` (upstream
123        // `IpOrigIterationOutput.cpp:WriteOutputImpl`): append the
124        // per-iter diagnostic-tag string accumulated on `IpoptData`
125        // (e.g. soft-resto / watchdog / corrector markers). The string
126        // is cleared by the algorithm at the start of each outer
127        // iteration via `clear_info_string`.
128        if self.print_info_string == PrintInfoString::Yes && !d.info_string.is_empty() {
129            row.push(' ');
130            row.push_str(&d.info_string);
131        }
132        row
133    }
134}
135
136/// Format `x` in C printf `%.{precision}e` style — signed exponent,
137/// zero-padded to at least two digits. E.g. `0.178` with precision 2
138/// → `"1.78e-01"`, `1.0` → `"1.00e+00"`, `8.83e-13` → `"8.83e-13"`.
139///
140/// Rust's native `{:.Ne}` formatter emits the exponent with no sign
141/// and no zero-pad (so `1e0`, `1.78e-1`, `8.83e-13` are 6 / 7 / 8
142/// chars respectively), which causes the e-format columns in the
143/// iteration log to wiggle as the exponent magnitude changes. This
144/// helper normalises to the C `%e` form, which is always 8 chars for
145/// 1-precision e-fields with 1- or 2-digit exponents.
146pub(crate) fn format_e(x: f64, precision: usize) -> String {
147    if !x.is_finite() {
148        return format!("{}", x);
149    }
150    let s = format!("{:.*e}", precision, x);
151    let (mantissa, exp) = match s.split_once('e') {
152        Some(pair) => pair,
153        None => return s,
154    };
155    let (sign, digits) = match exp.strip_prefix('-') {
156        Some(rest) => ('-', rest),
157        None => ('+', exp),
158    };
159    if digits.len() == 1 {
160        format!("{}e{}0{}", mantissa, sign, digits)
161    } else {
162        format!("{}e{}{}", mantissa, sign, digits)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn header_layout_right_aligns_each_label() {
172        // Width budget: iter(4) sp obj(14) sp inf_pr(8) sp inf_du(8)
173        // sp lg_mu(6) sp dnrm(8) sp regu(6) sp alpha_du(8) sp
174        // alpha_pr(8) alpha_char(1) ls(3) = 82 chars.
175        assert_eq!(OrigIterationOutput::HEADER.len(), 83); // 82 + \n
176        // Spot-check the right-edges of a few labels.
177        let h = OrigIterationOutput::HEADER.trim_end_matches('\n');
178        assert!(h.ends_with("ls"), "h = {h:?}");
179        assert_eq!(&h[10..19], "objective");
180        assert_eq!(&h[22..28], "inf_pr");
181        assert_eq!(&h[61..69], "alpha_du");
182        assert_eq!(&h[70..78], "alpha_pr");
183    }
184
185    #[test]
186    fn format_e_pads_short_exponents() {
187        assert_eq!(format_e(0.0, 2), "0.00e+00");
188        assert_eq!(format_e(1.0, 2), "1.00e+00");
189        assert_eq!(format_e(0.178, 2), "1.78e-01");
190        assert_eq!(format_e(8.83e-13, 2), "8.83e-13");
191        assert_eq!(format_e(7.74, 2), "7.74e+00");
192        // 2-digit exponent: no padding needed.
193        assert_eq!(format_e(1.0e10, 2), "1.00e+10");
194    }
195
196    #[test]
197    fn format_e_passes_through_non_finite() {
198        assert_eq!(format_e(f64::NAN, 2), "NaN");
199        assert_eq!(format_e(f64::INFINITY, 2), "inf");
200    }
201}