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