Skip to main content

pounce_nlp/
return_codes.rs

1//! Application-level return codes.
2//!
3//! Mirrors `Interfaces/IpReturnCodes.{h,hpp}` and `IpReturnCodes_inc.h`.
4//! The integer values **must** match upstream — `pounce-cinterface`
5//! uses `#[repr(i32)]` so the C ABI emits identical numeric codes for
6//! drop-in compatibility with PyIpopt / cyipopt / JuMP.
7
8use pounce_common::types::Index;
9
10/// Mirrors `enum ApplicationReturnStatus`.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[repr(i32)]
14pub enum ApplicationReturnStatus {
15    SolveSucceeded = 0,
16    SolvedToAcceptableLevel = 1,
17    InfeasibleProblemDetected = 2,
18    SearchDirectionBecomesTooSmall = 3,
19    DivergingIterates = 4,
20    UserRequestedStop = 5,
21    FeasiblePointFound = 6,
22
23    MaximumIterationsExceeded = -1,
24    RestorationFailed = -2,
25    ErrorInStepComputation = -3,
26    MaximumCpuTimeExceeded = -4,
27    MaximumWallTimeExceeded = -5,
28
29    NotEnoughDegreesOfFreedom = -10,
30    InvalidProblemDefinition = -11,
31    InvalidOption = -12,
32    InvalidNumberDetected = -13,
33
34    UnrecoverableException = -100,
35    NonIpoptExceptionThrown = -101,
36    InsufficientMemory = -102,
37    InternalError = -199,
38}
39
40impl ApplicationReturnStatus {
41    pub fn as_int(self) -> Index {
42        self as Index
43    }
44
45    /// The upstream C enumerator spelling (`Solve_Succeeded`,
46    /// `Infeasible_Problem_Detected`, …) from `IpReturnCodes_inc.h`.
47    ///
48    /// This is the name every consumer of Ipopt already keys off — CUTEst
49    /// status tables, `benchmarks/scripts/run_nl_bench.sh`, the reference
50    /// JSONs under `benchmarks/*/ipopt_ma57.json` — so it is what the CLI
51    /// prints on its machine-readable `Status:` line. The Rust `Debug` name
52    /// is *not* interchangeable with it: `Debug` gives `SolveSucceeded`, and
53    /// anything comparing against upstream's tables would silently never
54    /// match.
55    pub fn upstream_name(self) -> &'static str {
56        match self {
57            Self::SolveSucceeded => "Solve_Succeeded",
58            Self::SolvedToAcceptableLevel => "Solved_To_Acceptable_Level",
59            Self::InfeasibleProblemDetected => "Infeasible_Problem_Detected",
60            Self::SearchDirectionBecomesTooSmall => "Search_Direction_Becomes_Too_Small",
61            Self::DivergingIterates => "Diverging_Iterates",
62            Self::UserRequestedStop => "User_Requested_Stop",
63            Self::FeasiblePointFound => "Feasible_Point_Found",
64            Self::MaximumIterationsExceeded => "Maximum_Iterations_Exceeded",
65            Self::RestorationFailed => "Restoration_Failed",
66            Self::ErrorInStepComputation => "Error_In_Step_Computation",
67            Self::MaximumCpuTimeExceeded => "Maximum_CpuTime_Exceeded",
68            Self::MaximumWallTimeExceeded => "Maximum_WallTime_Exceeded",
69            Self::NotEnoughDegreesOfFreedom => "Not_Enough_Degrees_Of_Freedom",
70            Self::InvalidProblemDefinition => "Invalid_Problem_Definition",
71            Self::InvalidOption => "Invalid_Option",
72            Self::InvalidNumberDetected => "Invalid_Number_Detected",
73            Self::UnrecoverableException => "Unrecoverable_Exception",
74            Self::NonIpoptExceptionThrown => "NonIpopt_Exception_Thrown",
75            Self::InsufficientMemory => "Insufficient_Memory",
76            Self::InternalError => "Internal_Error",
77        }
78    }
79}
80
81/// Mirrors `enum AlgorithmMode`. Exposed in `intermediate_callback`.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83#[repr(i32)]
84pub enum AlgorithmMode {
85    RegularMode = 0,
86    RestorationPhaseMode = 1,
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    /// From `IpReturnCodes_inc.h` — these values are the C ABI
94    /// contract for `pounce-cinterface`. Never change them.
95    #[test]
96    fn integer_values_match_upstream() {
97        assert_eq!(ApplicationReturnStatus::SolveSucceeded.as_int(), 0);
98        assert_eq!(ApplicationReturnStatus::SolvedToAcceptableLevel.as_int(), 1);
99        assert_eq!(
100            ApplicationReturnStatus::InfeasibleProblemDetected.as_int(),
101            2
102        );
103        assert_eq!(
104            ApplicationReturnStatus::SearchDirectionBecomesTooSmall.as_int(),
105            3
106        );
107        assert_eq!(ApplicationReturnStatus::DivergingIterates.as_int(), 4);
108        assert_eq!(ApplicationReturnStatus::UserRequestedStop.as_int(), 5);
109        assert_eq!(ApplicationReturnStatus::FeasiblePointFound.as_int(), 6);
110
111        assert_eq!(
112            ApplicationReturnStatus::MaximumIterationsExceeded.as_int(),
113            -1
114        );
115        assert_eq!(ApplicationReturnStatus::RestorationFailed.as_int(), -2);
116        assert_eq!(ApplicationReturnStatus::ErrorInStepComputation.as_int(), -3);
117        assert_eq!(ApplicationReturnStatus::MaximumCpuTimeExceeded.as_int(), -4);
118        assert_eq!(
119            ApplicationReturnStatus::MaximumWallTimeExceeded.as_int(),
120            -5
121        );
122
123        assert_eq!(
124            ApplicationReturnStatus::NotEnoughDegreesOfFreedom.as_int(),
125            -10
126        );
127        assert_eq!(
128            ApplicationReturnStatus::InvalidProblemDefinition.as_int(),
129            -11
130        );
131        assert_eq!(ApplicationReturnStatus::InvalidOption.as_int(), -12);
132        assert_eq!(ApplicationReturnStatus::InvalidNumberDetected.as_int(), -13);
133
134        assert_eq!(
135            ApplicationReturnStatus::UnrecoverableException.as_int(),
136            -100
137        );
138        assert_eq!(
139            ApplicationReturnStatus::NonIpoptExceptionThrown.as_int(),
140            -101
141        );
142        assert_eq!(ApplicationReturnStatus::InsufficientMemory.as_int(), -102);
143        assert_eq!(ApplicationReturnStatus::InternalError.as_int(), -199);
144
145        assert_eq!(AlgorithmMode::RegularMode as i32, 0);
146        assert_eq!(AlgorithmMode::RestorationPhaseMode as i32, 1);
147    }
148
149    const ALL_STATUSES: [ApplicationReturnStatus; 20] = [
150        ApplicationReturnStatus::SolveSucceeded,
151        ApplicationReturnStatus::SolvedToAcceptableLevel,
152        ApplicationReturnStatus::InfeasibleProblemDetected,
153        ApplicationReturnStatus::SearchDirectionBecomesTooSmall,
154        ApplicationReturnStatus::DivergingIterates,
155        ApplicationReturnStatus::UserRequestedStop,
156        ApplicationReturnStatus::FeasiblePointFound,
157        ApplicationReturnStatus::MaximumIterationsExceeded,
158        ApplicationReturnStatus::RestorationFailed,
159        ApplicationReturnStatus::ErrorInStepComputation,
160        ApplicationReturnStatus::MaximumCpuTimeExceeded,
161        ApplicationReturnStatus::MaximumWallTimeExceeded,
162        ApplicationReturnStatus::NotEnoughDegreesOfFreedom,
163        ApplicationReturnStatus::InvalidProblemDefinition,
164        ApplicationReturnStatus::InvalidOption,
165        ApplicationReturnStatus::InvalidNumberDetected,
166        ApplicationReturnStatus::UnrecoverableException,
167        ApplicationReturnStatus::NonIpoptExceptionThrown,
168        ApplicationReturnStatus::InsufficientMemory,
169        ApplicationReturnStatus::InternalError,
170    ];
171
172    /// The names consumers actually key off — CUTEst status tables, the
173    /// benchmark driver's `Status:` scrape, `benchmarks/*/ipopt_ma57.json`.
174    /// A typo here is silent: the label just never matches and the run is
175    /// scored as something it was not.
176    #[test]
177    fn upstream_names_match_upstream_spelling() {
178        assert_eq!(
179            ApplicationReturnStatus::SolveSucceeded.upstream_name(),
180            "Solve_Succeeded"
181        );
182        assert_eq!(
183            ApplicationReturnStatus::InfeasibleProblemDetected.upstream_name(),
184            "Infeasible_Problem_Detected"
185        );
186        assert_eq!(
187            ApplicationReturnStatus::MaximumIterationsExceeded.upstream_name(),
188            "Maximum_Iterations_Exceeded"
189        );
190        // Upstream's own inconsistent casing, preserved verbatim: `CpuTime`
191        // and `WallTime` are one word each, `NonIpopt` likewise.
192        assert_eq!(
193            ApplicationReturnStatus::MaximumCpuTimeExceeded.upstream_name(),
194            "Maximum_CpuTime_Exceeded"
195        );
196        assert_eq!(
197            ApplicationReturnStatus::NonIpoptExceptionThrown.upstream_name(),
198            "NonIpopt_Exception_Thrown"
199        );
200    }
201
202    /// Every upstream name is its `Debug` name with underscores inserted —
203    /// true of all twenty, including the odd-cased ones above. Checking the
204    /// invariant over the whole enum catches a typo or a missed variant in
205    /// the arm that the spot checks above do not cover.
206    #[test]
207    fn upstream_names_are_debug_names_with_separators() {
208        for status in ALL_STATUSES {
209            assert_eq!(
210                status.upstream_name().replace('_', ""),
211                format!("{status:?}"),
212                "upstream name for {status:?} is not its Debug name with separators",
213            );
214        }
215    }
216
217    /// Distinct statuses must not collapse onto one label — that would make
218    /// a scrape read a different outcome than the one that shipped.
219    #[test]
220    fn upstream_names_are_unique() {
221        let mut names: Vec<&str> = ALL_STATUSES.iter().map(|s| s.upstream_name()).collect();
222        names.sort_unstable();
223        let before = names.len();
224        names.dedup();
225        assert_eq!(before, names.len(), "duplicate upstream status name");
226    }
227}