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 Interrupted,
26 NumericError,
28 NotSolved,
30 Other(String),
32}
33
34impl TerminationStatus {
35 pub fn admits_primal(&self) -> bool {
40 matches!(
41 self,
42 Self::Optimal
43 | Self::LocallyOptimal
44 | Self::Feasible
45 | Self::IterationLimit
46 | Self::TimeLimit
47 | Self::NodeLimit
48 | Self::Interrupted
49 )
50 }
51
52 #[must_use]
58 pub fn is_infeasible(&self) -> bool {
59 matches!(self, Self::Infeasible | Self::InfeasibleOrUnbounded)
60 }
61}
62
63#[derive(Copy, Clone, Debug, PartialEq, Eq)]
68pub enum PrimalStatus {
69 NoSolution,
71 FeasiblePoint,
73 OptimalPoint,
75}
76
77impl PrimalStatus {
78 pub fn infer(termination: &TerminationStatus, has_point: bool) -> Self {
84 if !has_point {
85 Self::NoSolution
86 } else if matches!(termination, TerminationStatus::Optimal) {
87 Self::OptimalPoint
88 } else {
89 Self::FeasiblePoint
90 }
91 }
92
93 pub fn has_solution(self) -> bool {
95 !matches!(self, Self::NoSolution)
96 }
97}
98
99#[derive(Error)]
100pub enum SolverError {
101 #[error("solver does not support model kind {0:?}")]
102 UnsupportedKind(oximo_core::ModelKind),
103 #[error("model is missing an objective")]
104 NoObjective,
105 #[error("{location} contains a nonlinear term unsupported by this backend: {term}")]
106 Nonlinear { location: String, term: String },
107 #[error("backend error: {0}")]
108 Backend(String),
109 #[error(transparent)]
110 Core(#[from] oximo_core::Error),
111}
112
113impl std::fmt::Debug for SolverError {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 std::fmt::Display::fmt(self, f)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 fn contract(t: &TerminationStatus) -> (bool, PrimalStatus) {
131 use TerminationStatus as T;
132 match t {
133 T::Optimal => (true, PrimalStatus::OptimalPoint),
134 T::LocallyOptimal
135 | T::Feasible
136 | T::IterationLimit
137 | T::TimeLimit
138 | T::NodeLimit
139 | T::Interrupted => (true, PrimalStatus::FeasiblePoint),
140 T::Infeasible
141 | T::Unbounded
142 | T::InfeasibleOrUnbounded
143 | T::NumericError
144 | T::NotSolved
145 | T::Other(_) => (false, PrimalStatus::FeasiblePoint),
146 }
147 }
148
149 fn all_terminations() -> Vec<TerminationStatus> {
150 use TerminationStatus as T;
151 vec![
152 T::Optimal,
153 T::LocallyOptimal,
154 T::Feasible,
155 T::Infeasible,
156 T::Unbounded,
157 T::InfeasibleOrUnbounded,
158 T::IterationLimit,
159 T::TimeLimit,
160 T::NodeLimit,
161 T::Interrupted,
162 T::NumericError,
163 T::NotSolved,
164 T::Other("backend_specific".into()),
165 ]
166 }
167
168 #[test]
169 fn admits_primal_and_infer_match_contract() {
170 for t in all_terminations() {
171 let (admits, with_point) = contract(&t);
172 assert_eq!(t.admits_primal(), admits, "admits_primal for {t:?}");
173 assert_eq!(PrimalStatus::infer(&t, true), with_point, "infer(.., true) for {t:?}");
174 assert_eq!(
175 PrimalStatus::infer(&t, false),
176 PrimalStatus::NoSolution,
177 "infer(.., false) for {t:?}"
178 );
179 }
180 }
181
182 #[test]
183 fn admits_primal_drives_inference_for_status_driven_backends() {
184 for t in all_terminations() {
185 let has_point = t.admits_primal();
186 let primal = PrimalStatus::infer(&t, has_point);
187 assert_eq!(
188 primal.has_solution(),
189 has_point,
190 "has_solution mirrors admits_primal for {t:?}"
191 );
192 let expected = match (has_point, &t) {
193 (false, _) => PrimalStatus::NoSolution,
194 (true, TerminationStatus::Optimal) => PrimalStatus::OptimalPoint,
195 (true, _) => PrimalStatus::FeasiblePoint,
196 };
197 assert_eq!(primal, expected, "inferred primal for {t:?}");
198 }
199 }
200
201 #[test]
202 fn is_infeasible_covers_infeasible_and_ambiguous() {
203 use TerminationStatus as T;
204 for t in all_terminations() {
205 let expected = matches!(t, T::Infeasible | T::InfeasibleOrUnbounded);
206 assert_eq!(t.is_infeasible(), expected, "is_infeasible for {t:?}");
207 }
208 }
209
210 #[test]
211 fn primal_status_has_solution() {
212 assert!(!PrimalStatus::NoSolution.has_solution());
213 assert!(PrimalStatus::FeasiblePoint.has_solution());
214 assert!(PrimalStatus::OptimalPoint.has_solution());
215 }
216}