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            // The "original" mode wants the unscaled NLP constraint
85            // violation; until NLP-side scaling lands we feed the
86            // (already unscaled) internal violation.
87            InfPrTag::Original => c.curr_primal_infeasibility_max(),
88        };
89        let inf_du = c.curr_dual_infeasibility_max();
90        let mu = d.curr_mu;
91        let lg_mu = mu.log10();
92
93        // ||d||_∞ over the (x, s) blocks of the latest search step.
94        let dnrm = match &d.delta {
95            Some(delta) => delta.x.amax().max(delta.s.amax()),
96            None => 0.0,
97        };
98
99        let regu_x = d.info_regu_x;
100        let regu_str: String = if regu_x == 0.0 {
101            "     -".to_string()
102        } else {
103            format!("{:6.1}", regu_x.log10())
104        };
105
106        let alpha_dual = d.info_alpha_dual;
107        let alpha_primal = d.info_alpha_primal;
108        let alpha_char = d.info_alpha_primal_char;
109        let ls_count = d.info_ls_count;
110
111        let mut row = format!(
112            "{:>4} {:>14} {:>8} {:>8} {:6.1} {:>8} {:>6} {:>8} {:>8}{}{:>3}",
113            iter,
114            format_e(unscaled_f, 7),
115            format_e(inf_pr, 2),
116            format_e(inf_du, 2),
117            lg_mu,
118            format_e(dnrm, 2),
119            regu_str,
120            format_e(alpha_dual, 2),
121            format_e(alpha_primal, 2),
122            alpha_char,
123            ls_count,
124        );
125        // `print_info_string` (upstream
126        // `IpOrigIterationOutput.cpp:WriteOutputImpl`): append the
127        // per-iter diagnostic-tag string accumulated on `IpoptData`
128        // (e.g. soft-resto / watchdog / corrector markers). The string
129        // is cleared by the algorithm at the start of each outer
130        // iteration via `clear_info_string`.
131        if self.print_info_string == PrintInfoString::Yes && !d.info_string.is_empty() {
132            row.push(' ');
133            row.push_str(&d.info_string);
134        }
135        row
136    }
137}
138
139/// Format `x` in C printf `%.{precision}e` style — signed exponent,
140/// zero-padded to at least two digits. E.g. `0.178` with precision 2
141/// → `"1.78e-01"`, `1.0` → `"1.00e+00"`, `8.83e-13` → `"8.83e-13"`.
142///
143/// Rust's native `{:.Ne}` formatter emits the exponent with no sign
144/// and no zero-pad (so `1e0`, `1.78e-1`, `8.83e-13` are 6 / 7 / 8
145/// chars respectively), which causes the e-format columns in the
146/// iteration log to wiggle as the exponent magnitude changes. This
147/// helper normalises to the C `%e` form, which is always 8 chars for
148/// 1-precision e-fields with 1- or 2-digit exponents.
149pub(crate) fn format_e(x: f64, precision: usize) -> String {
150    if !x.is_finite() {
151        return format!("{}", x);
152    }
153    let s = format!("{:.*e}", precision, x);
154    let (mantissa, exp) = match s.split_once('e') {
155        Some(pair) => pair,
156        None => return s,
157    };
158    let (sign, digits) = match exp.strip_prefix('-') {
159        Some(rest) => ('-', rest),
160        None => ('+', exp),
161    };
162    if digits.len() == 1 {
163        format!("{}e{}0{}", mantissa, sign, digits)
164    } else {
165        format!("{}e{}{}", mantissa, sign, digits)
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn header_layout_right_aligns_each_label() {
175        // Width budget: iter(4) sp obj(14) sp inf_pr(8) sp inf_du(8)
176        // sp lg_mu(6) sp dnrm(8) sp regu(6) sp alpha_du(8) sp
177        // alpha_pr(8) alpha_char(1) ls(3) = 82 chars.
178        assert_eq!(OrigIterationOutput::HEADER.len(), 83); // 82 + \n
179                                                           // Spot-check the right-edges of a few labels.
180        let h = OrigIterationOutput::HEADER.trim_end_matches('\n');
181        assert!(h.ends_with("ls"), "h = {h:?}");
182        assert_eq!(&h[10..19], "objective");
183        assert_eq!(&h[22..28], "inf_pr");
184        assert_eq!(&h[61..69], "alpha_du");
185        assert_eq!(&h[70..78], "alpha_pr");
186    }
187
188    #[test]
189    fn format_e_pads_short_exponents() {
190        assert_eq!(format_e(0.0, 2), "0.00e+00");
191        assert_eq!(format_e(1.0, 2), "1.00e+00");
192        assert_eq!(format_e(0.178, 2), "1.78e-01");
193        assert_eq!(format_e(8.83e-13, 2), "8.83e-13");
194        assert_eq!(format_e(7.74, 2), "7.74e+00");
195        // 2-digit exponent: no padding needed.
196        assert_eq!(format_e(1.0e10, 2), "1.00e+10");
197    }
198
199    #[test]
200    fn format_e_passes_through_non_finite() {
201        assert_eq!(format_e(f64::NAN, 2), "NaN");
202        assert_eq!(format_e(f64::INFINITY, 2), "inf");
203    }
204}