1use thiserror::Error;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
5pub enum TerminationStatus {
6 Optimal,
8 LocallyOptimal,
10 Feasible,
12 Infeasible,
14 Unbounded,
16 InfeasibleOrUnbounded,
18 IterationLimit,
20 TimeLimit,
22 NodeLimit,
24 ObjectiveLimit,
26 SolutionLimit,
28 WorkLimit,
30 MemoryLimit,
32 LocallyInfeasible,
34 LicenseError,
36 Interrupted,
38 NumericError,
40 NotSolved,
42 Other(String),
44}
45
46impl TerminationStatus {
47 pub fn admits_primal(&self) -> bool {
52 matches!(
53 self,
54 Self::Optimal
55 | Self::LocallyOptimal
56 | Self::Feasible
57 | Self::IterationLimit
58 | Self::TimeLimit
59 | Self::NodeLimit
60 | Self::ObjectiveLimit
61 | Self::SolutionLimit
62 | Self::WorkLimit
63 | Self::MemoryLimit
64 | Self::Interrupted
65 )
66 }
67
68 #[must_use]
74 pub fn is_infeasible(&self) -> bool {
75 matches!(self, Self::Infeasible | Self::InfeasibleOrUnbounded)
76 }
77}
78
79#[derive(Copy, Clone, Debug, PartialEq, Eq)]
84pub enum PrimalStatus {
85 NoSolution,
87 FeasiblePoint,
89 OptimalPoint,
91}
92
93impl PrimalStatus {
94 pub fn infer(termination: &TerminationStatus, has_point: bool) -> Self {
100 if !has_point {
101 Self::NoSolution
102 } else if matches!(termination, TerminationStatus::Optimal) {
103 Self::OptimalPoint
104 } else {
105 Self::FeasiblePoint
106 }
107 }
108
109 pub fn has_solution(self) -> bool {
111 !matches!(self, Self::NoSolution)
112 }
113}
114
115#[derive(Error)]
116pub enum SolverError {
117 #[error("solver does not support model kind {0:?}")]
118 UnsupportedKind(oximo_core::ModelKind),
119 #[error("model is missing an objective")]
120 NoObjective,
121 #[error("{location} contains a nonlinear term unsupported by this backend: {term}")]
122 Nonlinear { location: String, term: String },
123 #[error("backend error: {0}")]
124 Backend(String),
125 #[error(transparent)]
126 Core(#[from] oximo_core::Error),
127}
128
129impl std::fmt::Debug for SolverError {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 std::fmt::Display::fmt(self, f)
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 fn contract(t: &TerminationStatus) -> (bool, PrimalStatus) {
147 use TerminationStatus as T;
148 match t {
149 T::Optimal => (true, PrimalStatus::OptimalPoint),
150 T::LocallyOptimal
151 | T::Feasible
152 | T::IterationLimit
153 | T::TimeLimit
154 | T::NodeLimit
155 | T::ObjectiveLimit
156 | T::SolutionLimit
157 | T::WorkLimit
158 | T::MemoryLimit
159 | T::Interrupted => (true, PrimalStatus::FeasiblePoint),
160 T::Infeasible
161 | T::Unbounded
162 | T::InfeasibleOrUnbounded
163 | T::LocallyInfeasible
164 | T::LicenseError
165 | T::NumericError
166 | T::NotSolved
167 | T::Other(_) => (false, PrimalStatus::FeasiblePoint),
168 }
169 }
170
171 fn all_terminations() -> Vec<TerminationStatus> {
172 use TerminationStatus as T;
173 vec![
174 T::Optimal,
175 T::LocallyOptimal,
176 T::Feasible,
177 T::Infeasible,
178 T::Unbounded,
179 T::InfeasibleOrUnbounded,
180 T::IterationLimit,
181 T::TimeLimit,
182 T::NodeLimit,
183 T::ObjectiveLimit,
184 T::SolutionLimit,
185 T::WorkLimit,
186 T::MemoryLimit,
187 T::LocallyInfeasible,
188 T::LicenseError,
189 T::Interrupted,
190 T::NumericError,
191 T::NotSolved,
192 T::Other("backend_specific".into()),
193 ]
194 }
195
196 #[test]
197 fn admits_primal_and_infer_match_contract() {
198 for t in all_terminations() {
199 let (admits, with_point) = contract(&t);
200 assert_eq!(t.admits_primal(), admits, "admits_primal for {t:?}");
201 assert_eq!(PrimalStatus::infer(&t, true), with_point, "infer(.., true) for {t:?}");
202 assert_eq!(
203 PrimalStatus::infer(&t, false),
204 PrimalStatus::NoSolution,
205 "infer(.., false) for {t:?}"
206 );
207 }
208 }
209
210 #[test]
211 fn admits_primal_drives_inference_for_status_driven_backends() {
212 for t in all_terminations() {
213 let has_point = t.admits_primal();
214 let primal = PrimalStatus::infer(&t, has_point);
215 assert_eq!(
216 primal.has_solution(),
217 has_point,
218 "has_solution mirrors admits_primal for {t:?}"
219 );
220 let expected = match (has_point, &t) {
221 (false, _) => PrimalStatus::NoSolution,
222 (true, TerminationStatus::Optimal) => PrimalStatus::OptimalPoint,
223 (true, _) => PrimalStatus::FeasiblePoint,
224 };
225 assert_eq!(primal, expected, "inferred primal for {t:?}");
226 }
227 }
228
229 #[test]
230 fn is_infeasible_covers_infeasible_and_ambiguous() {
231 use TerminationStatus as T;
232 for t in all_terminations() {
233 let expected = matches!(t, T::Infeasible | T::InfeasibleOrUnbounded);
234 assert_eq!(t.is_infeasible(), expected, "is_infeasible for {t:?}");
235 }
236 }
237
238 #[test]
239 fn primal_status_has_solution() {
240 assert!(!PrimalStatus::NoSolution.has_solution());
241 assert!(PrimalStatus::FeasiblePoint.has_solution());
242 assert!(PrimalStatus::OptimalPoint.has_solution());
243 }
244}