1use thiserror::Error;
8
9#[derive(Debug, Error)]
11pub enum OptimError {
12 #[error("line search failed at iteration {iteration}: {reason}")]
13 LineSearchFailed { iteration: usize, reason: String },
14 #[error("not a descent direction (dg = {directional_derivative:.2e})")]
15 NotDescentDirection { directional_derivative: f64 },
16 #[error("invalid function value: {value}")]
17 InvalidFunctionValue { value: f64 },
18 #[error("singular matrix in solver")]
19 SingularMatrix,
20 #[error("dimension mismatch: expected {expected}, got {actual}")]
21 DimensionMismatch { expected: usize, actual: usize },
22 #[error("no objective function specified")]
23 NoObjective,
24 #[error("no initial point specified")]
25 NoInitialPoint,
26 #[error("infeasible: constraint violation = {violation:.2e}")]
27 Infeasible { violation: f64 },
28 #[error("problem is unbounded")]
29 Unbounded,
30 #[error("linear program is infeasible")]
31 LPInfeasible,
32 #[error("QP Hessian is not positive semi-definite")]
33 QPNotPositiveSemiDefinite,
34 #[error("mixed-integer linear program is infeasible")]
35 MILPInfeasible,
36 #[error("{0}")]
37 Other(String),
38}
39
40impl From<String> for OptimError {
41 fn from(s: String) -> Self {
42 OptimError::Other(s)
43 }
44}
45
46impl From<&str> for OptimError {
47 fn from(s: &str) -> Self {
48 OptimError::Other(s.to_string())
49 }
50}
51
52impl From<numra_nonlinear::LineSearchError> for OptimError {
53 fn from(e: numra_nonlinear::LineSearchError) -> Self {
54 OptimError::Other(e.to_string())
55 }
56}
57
58impl From<numra_nonlinear::LinalgError> for OptimError {
59 fn from(e: numra_nonlinear::LinalgError) -> Self {
60 match e {
61 numra_nonlinear::LinalgError::Singular { .. }
62 | numra_nonlinear::LinalgError::NotSquare { .. }
63 | numra_nonlinear::LinalgError::NotPositiveDefinite => OptimError::SingularMatrix,
64 numra_nonlinear::LinalgError::DimensionMismatch { expected, actual } => {
65 OptimError::DimensionMismatch {
66 expected: expected.0,
67 actual: actual.0,
68 }
69 }
70 _ => OptimError::Other(e.to_string()),
71 }
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn test_display_formatting() {
81 let e = OptimError::LineSearchFailed {
82 iteration: 5,
83 reason: "step too small".into(),
84 };
85 assert_eq!(
86 e.to_string(),
87 "line search failed at iteration 5: step too small"
88 );
89
90 let e = OptimError::NotDescentDirection {
91 directional_derivative: 1.5,
92 };
93 assert!(e.to_string().contains("1.50e0"));
94
95 let e = OptimError::SingularMatrix;
96 assert_eq!(e.to_string(), "singular matrix in solver");
97
98 let e = OptimError::DimensionMismatch {
99 expected: 3,
100 actual: 5,
101 };
102 assert_eq!(e.to_string(), "dimension mismatch: expected 3, got 5");
103 }
104
105 #[test]
106 fn test_lp_qp_error_variants() {
107 let e = OptimError::Unbounded;
108 assert_eq!(e.to_string(), "problem is unbounded");
109
110 let e = OptimError::LPInfeasible;
111 assert_eq!(e.to_string(), "linear program is infeasible");
112
113 let e = OptimError::QPNotPositiveSemiDefinite;
114 assert_eq!(e.to_string(), "QP Hessian is not positive semi-definite");
115
116 let e = OptimError::MILPInfeasible;
117 assert_eq!(e.to_string(), "mixed-integer linear program is infeasible");
118 }
119
120 #[test]
121 fn test_from_string() {
122 let e: OptimError = "some error".into();
123 assert_eq!(e.to_string(), "some error");
124
125 let e: OptimError = String::from("another error").into();
126 assert_eq!(e.to_string(), "another error");
127 }
128}