1use thiserror::Error;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
5pub enum TerminationStatus {
6 Optimal,
8 LocallyOptimal,
10 Infeasible,
12 Unbounded,
14 InfeasibleOrUnbounded,
16 IterationLimit,
18 TimeLimit,
20 NodeLimit,
22 Interrupted,
24 NumericError,
26 NotSolved,
28 Other(String),
30}
31
32impl TerminationStatus {
33 pub fn admits_primal(&self) -> bool {
38 matches!(
39 self,
40 Self::Optimal
41 | Self::LocallyOptimal
42 | Self::IterationLimit
43 | Self::TimeLimit
44 | Self::NodeLimit
45 | Self::Interrupted
46 )
47 }
48}
49
50#[derive(Copy, Clone, Debug, PartialEq, Eq)]
55pub enum PrimalStatus {
56 NoSolution,
58 FeasiblePoint,
60 OptimalPoint,
62}
63
64impl PrimalStatus {
65 pub fn infer(termination: &TerminationStatus, has_point: bool) -> Self {
71 if !has_point {
72 Self::NoSolution
73 } else if matches!(termination, TerminationStatus::Optimal) {
74 Self::OptimalPoint
75 } else {
76 Self::FeasiblePoint
77 }
78 }
79
80 pub fn has_solution(self) -> bool {
82 !matches!(self, Self::NoSolution)
83 }
84}
85
86#[derive(Error)]
87pub enum SolverError {
88 #[error("solver does not support model kind {0:?}")]
89 UnsupportedKind(oximo_core::ModelKind),
90 #[error("model is missing an objective")]
91 NoObjective,
92 #[error("nonlinear constructs are not supported by this backend")]
93 Nonlinear,
94 #[error("backend error: {0}")]
95 Backend(String),
96 #[error(transparent)]
97 Core(#[from] oximo_core::Error),
98}
99
100impl std::fmt::Debug for SolverError {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 std::fmt::Display::fmt(self, f)
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 fn contract(t: &TerminationStatus) -> (bool, PrimalStatus) {
118 use TerminationStatus as T;
119 match t {
120 T::Optimal => (true, PrimalStatus::OptimalPoint),
121 T::LocallyOptimal
122 | T::IterationLimit
123 | T::TimeLimit
124 | T::NodeLimit
125 | T::Interrupted => (true, PrimalStatus::FeasiblePoint),
126 T::Infeasible
127 | T::Unbounded
128 | T::InfeasibleOrUnbounded
129 | T::NumericError
130 | T::NotSolved
131 | T::Other(_) => (false, PrimalStatus::FeasiblePoint),
132 }
133 }
134
135 fn all_terminations() -> Vec<TerminationStatus> {
136 use TerminationStatus as T;
137 vec![
138 T::Optimal,
139 T::LocallyOptimal,
140 T::Infeasible,
141 T::Unbounded,
142 T::InfeasibleOrUnbounded,
143 T::IterationLimit,
144 T::TimeLimit,
145 T::NodeLimit,
146 T::Interrupted,
147 T::NumericError,
148 T::NotSolved,
149 T::Other("backend_specific".into()),
150 ]
151 }
152
153 #[test]
154 fn admits_primal_and_infer_match_contract() {
155 for t in all_terminations() {
156 let (admits, with_point) = contract(&t);
157 assert_eq!(t.admits_primal(), admits, "admits_primal for {t:?}");
158 assert_eq!(PrimalStatus::infer(&t, true), with_point, "infer(.., true) for {t:?}");
159 assert_eq!(
160 PrimalStatus::infer(&t, false),
161 PrimalStatus::NoSolution,
162 "infer(.., false) for {t:?}"
163 );
164 }
165 }
166
167 #[test]
168 fn admits_primal_drives_inference_for_status_driven_backends() {
169 for t in all_terminations() {
170 let has_point = t.admits_primal();
171 let primal = PrimalStatus::infer(&t, has_point);
172 assert_eq!(
173 primal.has_solution(),
174 has_point,
175 "has_solution mirrors admits_primal for {t:?}"
176 );
177 let expected = match (has_point, &t) {
178 (false, _) => PrimalStatus::NoSolution,
179 (true, TerminationStatus::Optimal) => PrimalStatus::OptimalPoint,
180 (true, _) => PrimalStatus::FeasiblePoint,
181 };
182 assert_eq!(primal, expected, "inferred primal for {t:?}");
183 }
184 }
185
186 #[test]
187 fn primal_status_has_solution() {
188 assert!(!PrimalStatus::NoSolution.has_solution());
189 assert!(PrimalStatus::FeasiblePoint.has_solution());
190 assert!(PrimalStatus::OptimalPoint.has_solution());
191 }
192}