Skip to main content

pounce_common/
exception.rs

1//! POUNCE exceptions.
2//!
3//! Mirrors `Common/IpException.hpp`. Ipopt uses inheritance to
4//! distinguish exception types and a `DECLARE_STD_EXCEPTION` macro
5//! to name them; we use a single `SolverException` struct carrying a
6//! `kind: ExceptionKind` enum.
7//!
8//! The variant names are byte-identical to the upstream class names
9//! (`TINY_STEP_DETECTED`, `RESTORATION_FAILED`, ...) so that
10//! `ReportException` formatting matches upstream when we eventually
11//! Display them.
12
13use crate::types::Index;
14use std::fmt;
15
16/// All exception kinds Ipopt declares via `DECLARE_STD_EXCEPTION` in
17/// `src/{Common,Algorithm,LinAlg,Interfaces,Apps}/`. Names match upstream.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19#[allow(non_camel_case_types)]
20pub enum ExceptionKind {
21    // Common
22    OPTION_INVALID,
23    OPTION_ALREADY_REGISTERED,
24    DYNAMIC_LIBRARY_FAILURE,
25    // LinAlg
26    UNIMPLEMENTED_LINALG_METHOD_CALLED,
27    UNKNOWN_MATRIX_TYPE,
28    UNKNOWN_VECTOR_TYPE,
29    LAPACK_NOT_INCLUDED,
30    METADATA_ERROR,
31    // Algorithm — line search / step
32    TINY_STEP_DETECTED,
33    ACCEPTABLE_POINT_REACHED,
34    STEP_COMPUTATION_FAILED,
35    LOCALLY_INFEASIBLE,
36    FEASIBILITY_PROBLEM_SOLVED,
37    // Algorithm — restoration
38    RESTORATION_FAILED,
39    RESTORATION_CONVERGED_TO_FEASIBLE_POINT,
40    RESTORATION_MAXITER_EXCEEDED,
41    RESTORATION_CPUTIME_EXCEEDED,
42    RESTORATION_WALLTIME_EXCEEDED,
43    RESTORATION_USER_STOP,
44    // Linear solvers / scaling
45    FATAL_ERROR_IN_LINEAR_SOLVER,
46    ERROR_IN_LINEAR_SCALING_METHOD,
47    NONPOSITIVE_SCALING_FACTOR,
48    USER_SCALING_NOT_IMPLEMENTED,
49    // NLP / TNLP
50    INVALID_NLP,
51    INVALID_TNLP,
52    INVALID_STDINTERFACE_NLP,
53    INVALID_WARMSTART,
54    INCONSISTENT_BOUNDS,
55    TOO_FEW_DOF,
56    ERROR_IN_TNLP_DERIVATIVE_TEST,
57    // Application
58    IPOPT_APPLICATION_ERROR,
59    FAILED_INITIALIZATION,
60    INTERNAL_ABORT,
61    // Misc
62    ERROR_CONVERTING_STRING_TO_ENUM,
63}
64
65impl ExceptionKind {
66    /// Class name as Ipopt prints it in `ReportException` (`type_` field).
67    pub fn name(self) -> &'static str {
68        use ExceptionKind::*;
69        match self {
70            OPTION_INVALID => "OPTION_INVALID",
71            OPTION_ALREADY_REGISTERED => "OPTION_ALREADY_REGISTERED",
72            DYNAMIC_LIBRARY_FAILURE => "DYNAMIC_LIBRARY_FAILURE",
73            UNIMPLEMENTED_LINALG_METHOD_CALLED => "UNIMPLEMENTED_LINALG_METHOD_CALLED",
74            UNKNOWN_MATRIX_TYPE => "UNKNOWN_MATRIX_TYPE",
75            UNKNOWN_VECTOR_TYPE => "UNKNOWN_VECTOR_TYPE",
76            LAPACK_NOT_INCLUDED => "LAPACK_NOT_INCLUDED",
77            METADATA_ERROR => "METADATA_ERROR",
78            TINY_STEP_DETECTED => "TINY_STEP_DETECTED",
79            ACCEPTABLE_POINT_REACHED => "ACCEPTABLE_POINT_REACHED",
80            STEP_COMPUTATION_FAILED => "STEP_COMPUTATION_FAILED",
81            LOCALLY_INFEASIBLE => "LOCALLY_INFEASIBLE",
82            FEASIBILITY_PROBLEM_SOLVED => "FEASIBILITY_PROBLEM_SOLVED",
83            RESTORATION_FAILED => "RESTORATION_FAILED",
84            RESTORATION_CONVERGED_TO_FEASIBLE_POINT => "RESTORATION_CONVERGED_TO_FEASIBLE_POINT",
85            RESTORATION_MAXITER_EXCEEDED => "RESTORATION_MAXITER_EXCEEDED",
86            RESTORATION_CPUTIME_EXCEEDED => "RESTORATION_CPUTIME_EXCEEDED",
87            RESTORATION_WALLTIME_EXCEEDED => "RESTORATION_WALLTIME_EXCEEDED",
88            RESTORATION_USER_STOP => "RESTORATION_USER_STOP",
89            FATAL_ERROR_IN_LINEAR_SOLVER => "FATAL_ERROR_IN_LINEAR_SOLVER",
90            ERROR_IN_LINEAR_SCALING_METHOD => "ERROR_IN_LINEAR_SCALING_METHOD",
91            NONPOSITIVE_SCALING_FACTOR => "NONPOSITIVE_SCALING_FACTOR",
92            USER_SCALING_NOT_IMPLEMENTED => "USER_SCALING_NOT_IMPLEMENTED",
93            INVALID_NLP => "INVALID_NLP",
94            INVALID_TNLP => "INVALID_TNLP",
95            INVALID_STDINTERFACE_NLP => "INVALID_STDINTERFACE_NLP",
96            INVALID_WARMSTART => "INVALID_WARMSTART",
97            INCONSISTENT_BOUNDS => "INCONSISTENT_BOUNDS",
98            TOO_FEW_DOF => "TOO_FEW_DOF",
99            ERROR_IN_TNLP_DERIVATIVE_TEST => "ERROR_IN_TNLP_DERIVATIVE_TEST",
100            IPOPT_APPLICATION_ERROR => "IPOPT_APPLICATION_ERROR",
101            FAILED_INITIALIZATION => "FAILED_INITIALIZATION",
102            INTERNAL_ABORT => "INTERNAL_ABORT",
103            ERROR_CONVERTING_STRING_TO_ENUM => "ERROR_CONVERTING_STRING_TO_ENUM",
104        }
105    }
106}
107
108impl fmt::Display for ExceptionKind {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        f.write_str(self.name())
111    }
112}
113
114/// A single exception value carrying kind + message + source location.
115///
116/// Equivalent to `IpoptException`; raise via [`throw`] or
117/// [`assert_or_throw`] to capture file/line.
118#[derive(Debug, Clone)]
119pub struct SolverException {
120    pub kind: ExceptionKind,
121    pub message: String,
122    pub file: &'static str,
123    pub line: Index,
124}
125
126impl SolverException {
127    pub fn new(
128        kind: ExceptionKind,
129        message: impl Into<String>,
130        file: &'static str,
131        line: Index,
132    ) -> Self {
133        Self {
134            kind,
135            message: message.into(),
136            file,
137            line,
138        }
139    }
140}
141
142impl fmt::Display for SolverException {
143    /// Matches the format used in `IpoptException::ReportException`.
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        write!(
146            f,
147            "Exception of type: {} in file \"{}\" at line {}:\n Exception message: {}",
148            self.kind, self.file, self.line, self.message
149        )
150    }
151}
152
153impl std::error::Error for SolverException {}
154
155/// Macro replacement for Ipopt's `THROW_EXCEPTION(kind, msg)`. Produces
156/// a `Result::Err(SolverException)` using `file!()`/`line!()`.
157#[macro_export]
158macro_rules! throw {
159    ($kind:expr, $msg:expr) => {
160        return ::core::result::Result::Err($crate::exception::SolverException::new(
161            $kind,
162            $msg,
163            file!(),
164            line!() as $crate::types::Index,
165        ))
166    };
167}
168
169/// Macro replacement for `ASSERT_EXCEPTION(cond, kind, msg)`.
170#[macro_export]
171macro_rules! assert_exc {
172    ($cond:expr, $kind:expr, $msg:expr) => {
173        if !($cond) {
174            let mut newmsg = ::std::string::String::from(stringify!($cond));
175            newmsg.push_str(" evaluated false: ");
176            newmsg.push_str($msg);
177            return ::core::result::Result::Err($crate::exception::SolverException::new(
178                $kind,
179                newmsg,
180                file!(),
181                line!() as $crate::types::Index,
182            ));
183        }
184    };
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn display_matches_upstream_format() {
193        let e = SolverException::new(
194            ExceptionKind::OPTION_INVALID,
195            "bad option",
196            "src/foo.rs",
197            42,
198        );
199        let s = format!("{}", e);
200        assert!(s.contains("Exception of type: OPTION_INVALID"));
201        assert!(s.contains("src/foo.rs"));
202        assert!(s.contains("line 42"));
203        assert!(s.contains("Exception message: bad option"));
204    }
205
206    #[test]
207    fn names_all_round_trip() {
208        for k in [
209            ExceptionKind::OPTION_INVALID,
210            ExceptionKind::TINY_STEP_DETECTED,
211            ExceptionKind::RESTORATION_FAILED,
212            ExceptionKind::LOCALLY_INFEASIBLE,
213        ] {
214            assert!(!k.name().is_empty());
215            assert_eq!(format!("{k}"), k.name());
216        }
217    }
218}