Skip to main content

pounce_algorithm/output/
orig.rs

1//! Original iteration output — port of
2//! `Algorithm/IpOrigIterationOutput.{hpp,cpp}`.
3//!
4//! Reproduces upstream's column layout byte-exactly so iteration
5//! logs can be text-diffed against an Ipopt 3.14.x run.
6
7use crate::ipopt_cq::IpoptCqHandle;
8use crate::ipopt_data::IpoptDataHandle;
9use crate::output::r#trait::IterationOutput;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum PrintInfoString {
13    Yes,
14    No,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum InfPrTag {
19    Internal,
20    Original,
21}
22
23pub struct OrigIterationOutput {
24    pub print_info_string: PrintInfoString,
25    pub inf_pr_output: InfPrTag,
26    pub print_frequency_iter: i32,
27    pub print_frequency_time: f64,
28    /// Iteration index of the last header print; the upstream code
29    /// re-prints the header every 10 lines.
30    last_header_iter: i32,
31}
32
33impl Default for OrigIterationOutput {
34    fn default() -> Self {
35        Self {
36            print_info_string: PrintInfoString::No,
37            inf_pr_output: InfPrTag::Original,
38            print_frequency_iter: 1,
39            print_frequency_time: 0.0,
40            last_header_iter: -1,
41        }
42    }
43}
44
45impl OrigIterationOutput {
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Header line printed every ten iterations; matches the literal
51    /// in `IpOrigIterationOutput.cpp:75`.
52    pub const HEADER: &'static str =
53        "iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls\n";
54}
55
56impl IterationOutput for OrigIterationOutput {
57    fn write_output(&mut self) {
58        // Header-print bookkeeping; the actual emission is handled by
59        // `format_row`, which the caller wires to its journalist.
60        self.last_header_iter = 0;
61    }
62
63    /// Build the single-line iteration row. Field-for-field port of
64    /// the `Snprintf` block at `IpOrigIterationOutput.cpp:152`:
65    /// `"%4d %14.7e %7.2e %7.2e %5.1f %7.2e %5s %7.2e %7.2e%c%3d"`.
66    fn format_row(&mut self, data: &IpoptDataHandle, cq: &IpoptCqHandle) -> String {
67        let d = data.borrow();
68        let c = cq.borrow();
69
70        let iter = d.iter_count;
71        let unscaled_f = c.unscaled_curr_f();
72        let inf_pr = match self.inf_pr_output {
73            InfPrTag::Internal => c.curr_primal_infeasibility_max(),
74            // The "original" mode wants the unscaled NLP constraint
75            // violation; until NLP-side scaling lands we feed the
76            // (already unscaled) internal violation.
77            InfPrTag::Original => c.curr_primal_infeasibility_max(),
78        };
79        let inf_du = c.curr_dual_infeasibility_max();
80        let mu = d.curr_mu;
81        let lg_mu = mu.log10();
82
83        // ||d||_∞ over the (x, s) blocks of the latest search step.
84        let dnrm = match &d.delta {
85            Some(delta) => delta.x.amax().max(delta.s.amax()),
86            None => 0.0,
87        };
88
89        let regu_x = d.info_regu_x;
90        let regu_str: String = if regu_x == 0.0 {
91            "   - ".to_string()
92        } else {
93            format_field_5_1(regu_x.log10())
94        };
95
96        let alpha_dual = d.info_alpha_dual;
97        let alpha_primal = d.info_alpha_primal;
98        let alpha_char = d.info_alpha_primal_char;
99        let ls_count = d.info_ls_count;
100
101        let mut row = format!(
102            "{:>4} {:14.7e} {:7.2e} {:7.2e} {:5.1} {:7.2e} {:>5} {:7.2e} {:7.2e}{}{:>3}",
103            iter,
104            unscaled_f,
105            inf_pr,
106            inf_du,
107            lg_mu,
108            dnrm,
109            regu_str,
110            alpha_dual,
111            alpha_primal,
112            alpha_char,
113            ls_count,
114        );
115        // `print_info_string` (upstream
116        // `IpOrigIterationOutput.cpp:WriteOutputImpl`): append the
117        // per-iter diagnostic-tag string accumulated on `IpoptData`
118        // (e.g. soft-resto / watchdog / corrector markers). The string
119        // is cleared by the algorithm at the start of each outer
120        // iteration via `clear_info_string`.
121        if self.print_info_string == PrintInfoString::Yes && !d.info_string.is_empty() {
122            row.push(' ');
123            row.push_str(&d.info_string);
124        }
125        row
126    }
127}
128
129/// Width-5, precision-1 fixed-point — emulates upstream's
130/// `Snprintf(buf, 7, "%5.1f", x)`. The format!() spec
131/// `"{:5.1}"` already matches `%5.1f` for `f64` in Rust.
132fn format_field_5_1(x: f64) -> String {
133    format!("{:5.1}", x)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn header_matches_upstream_literal() {
142        assert_eq!(
143            OrigIterationOutput::HEADER,
144            "iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls\n"
145        );
146    }
147
148    #[test]
149    fn regu_field_dashes_when_zero() {
150        // `regu_x == 0` → "   - " (5 chars including trailing space).
151        assert_eq!(format_field_5_1(-1.0), " -1.0");
152    }
153}