1use clarabel::solver::{SolverError as ClarabelSolverError, SolverStatus as ClarabelSolverStatus};
2use thiserror::Error;
3
4#[derive(Error)]
6pub enum CoppError {
7 #[error("I/O error: {0}")]
9 IoError(#[from] std::io::Error),
10 #[error("Some error occurred in the Constraint struct: {0}")]
12 ConstraintError(#[from] ConstraintError),
13 #[error("Some error occurred in the Path struct: {0}")]
15 PathError(#[from] PathError),
16 #[error("{0} reported an infeasibility: {1}")]
18 Infeasible(String, String),
19 #[error("{0} reported an unboundedness: {1}")]
21 Unbounded(String, String),
22 #[error("{0} reported an invalid input: {1}")]
24 InvalidInput(String, String),
25 #[error("{0} reported an invalid options: {1}")]
27 InvalidOptions(String, String),
28 #[error("{0} reported an error in Clarabel solver: {1}")]
30 ClarabelSolverError(String, #[source] ClarabelSolverError),
31 #[error("{0} reported a failure in Clarabel solver with status {1}")]
33 ClarabelSolverStatus(String, ClarabelSolverStatus),
34 #[error("{0} reported an error: {1}")]
36 Other(String, String),
37}
38
39#[derive(Error, Debug)]
49pub enum ConstraintError {
50 #[error("`s` must be strictly increasing; first violation at local index {index}.")]
52 NonIncreasingS { index: usize },
53
54 #[error("Input dimensions do not match the expected shape.")]
56 NoMatchDimensions,
57
58 #[error("Input order does not satisfy the expected contract.")]
60 NoMatchOrder,
61
62 #[error(
64 "`{bound_name}` requires strict signed limits at every station: upper bound > 0 and lower bound < 0."
65 )]
66 InvalidSignedBounds { bound_name: &'static str },
67
68 #[error("Requested station interval is out of bounds: idx_s={idx_s}, len={len}.")]
70 OutOfSBounds { idx_s: usize, len: usize },
71
72 #[error(
74 "Input `a` violates positivity requirements (must be nonnegative, and strictly positive where required)."
75 )]
76 NonPositiveA,
77
78 #[error("Linearization floor must be strictly positive.")]
80 NonPositiveLinearizationFloor,
81
82 #[error(
84 "Required derivative data (`q`, `dq`, `ddq`, `dddq`) is not fully available in the requested interval."
85 )]
86 NoGivenQInfo,
87
88 #[error(
90 "Linearized jerk constraints are unavailable at idx_s={idx_s}; valid range is [{}, {}).",
91 valid_range.0,
92 valid_range.1
93 )]
94 LinearJerkNotAvailable {
95 idx_s: usize,
96 valid_range: (usize, usize),
97 },
98
99 #[error("Required dynamic-model information is not available.")]
101 NoDynamic,
102
103 #[error("Reference profile is infeasible under current constraints.")]
105 InfeasibleReference,
106
107 #[error("Requested interval is empty: {start} <= idx_s < {end}.")]
109 EmptyInterval { start: usize, end: usize },
110}
111
112#[derive(Error, Debug)]
118pub enum PathError {
119 #[error("invalid dimension: {dim}")]
121 InvalidDimension { dim: usize },
122 #[error("invalid s range: [{s_min}, {s_max}]")]
124 InvalidRange { s_min: f64, s_max: f64 },
125 #[error("invalid spline order: {order}, expected >= 3")]
127 InvalidOrder { order: usize },
128 #[error("dimension mismatch")]
130 DimensionMismatch,
131 #[error("invalid shape for parameter s: ({rows}, {cols}), expected 1xN or Nx1")]
133 InvalidSShape { rows: usize, cols: usize },
134 #[error("not enough waypoints: {n}, expected >= 2")]
136 NotEnoughWaypoints { n: usize },
137 #[error("s out of range [{s_min}, {s_max}] at index {index}: {value}")]
139 OutOfRangeS {
140 s_min: f64,
141 s_max: f64,
142 index: usize,
143 value: f64,
144 },
145 #[error("unsupported boundary for order={order}")]
147 UnsupportedBoundary { order: usize },
148 #[error("singular linear system")]
150 SingularSystem,
151}
152
153impl std::fmt::Debug for CoppError {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 write!(f, "{self}")
157 }
158}
159
160#[inline(always)]
162pub(crate) fn check_s_interval_valid(
163 function_name: &str,
164 idx_s_start: usize,
165 idx_s_final: usize,
166) -> Result<(), CoppError> {
167 if idx_s_final < idx_s_start + 2 {
168 Err(CoppError::InvalidInput(
169 function_name.into(),
170 format!(
171 "The final index {idx_s_final} must be at least two positions after the start index {idx_s_start} in Topp2Problem."
172 ),
173 ))
174 } else {
175 Ok(())
176 }
177}
178
179#[inline(always)]
181pub(crate) fn check_abs_rel_tol(
182 function_name: &str,
183 abs_tol_name: &str,
184 abs_tol: f64,
185 rel_tol_name: &str,
186 rel_tol: f64,
187) -> Result<(), CoppError> {
188 check_not_nan_infinite(function_name, abs_tol_name, abs_tol)?;
189 check_not_nan_infinite(function_name, rel_tol_name, rel_tol)?;
190 if abs_tol >= 0.0 && rel_tol >= 0.0 && (abs_tol > f64::EPSILON || rel_tol > f64::EPSILON) {
191 Ok(())
192 } else {
193 Err(CoppError::InvalidOptions(
194 function_name.into(),
195 format!(
196 "At least one of {abs_tol_name} = {abs_tol} and {rel_tol_name} = {rel_tol} must be strictly positive."
197 ),
198 ))
199 }
200}
201
202#[inline(always)]
203pub(crate) fn check_not_nan_infinite(
204 function_name: &str,
205 var_name: &str,
206 var_value: f64,
207) -> Result<(), CoppError> {
208 if var_value.is_nan() {
209 Err(CoppError::InvalidOptions(
210 function_name.into(),
211 format!("{var_name} = {var_value} must not be NaN",),
212 ))
213 } else if var_value.is_infinite() {
214 Err(CoppError::InvalidOptions(
215 function_name.into(),
216 format!("{var_name} = {var_value} must not be infinite",),
217 ))
218 } else {
219 Ok(())
220 }
221}
222
223#[inline(always)]
225pub(crate) fn check_non_negative(
226 function_name: &str,
227 var_name: &str,
228 var_value: f64,
229) -> Result<(), CoppError> {
230 check_not_nan_infinite(function_name, var_name, var_value)?;
231 if var_value < 0.0 {
232 Err(CoppError::InvalidOptions(
233 function_name.into(),
234 format!("{var_name} = {var_value} must be strictly non-negative"),
235 ))
236 } else {
237 Ok(())
238 }
239}
240
241#[inline(always)]
243pub(crate) fn check_strictly_positive(
244 function_name: &str,
245 var_name: &str,
246 var_value: f64,
247) -> Result<(), CoppError> {
248 check_not_nan_infinite(function_name, var_name, var_value)?;
249 if var_value < f64::EPSILON {
250 Err(CoppError::InvalidOptions(
251 function_name.into(),
252 format!("{var_name} = {var_value} must be strictly positive"),
253 ))
254 } else {
255 Ok(())
256 }
257}
258
259#[inline(always)]
260pub(crate) fn check_boundary_state_copp3_valid(
261 a_boundary: (f64, f64),
262 b_boundary: (f64, f64),
263) -> Result<(), CoppError> {
264 if a_boundary.0 < 0.0 {
265 return Err(CoppError::InvalidInput(
266 "copp3_socp".into(),
267 format!("The initial a = {} must be non-negative.", a_boundary.0),
268 ));
269 }
270 if a_boundary.1 < 0.0 {
271 return Err(CoppError::InvalidInput(
272 "copp3_socp".into(),
273 format!("The terminal a = {} must be non-negative.", a_boundary.1),
274 ));
275 }
276 if a_boundary.0.abs() < f64::EPSILON {
277 if b_boundary.0.abs() >= f64::EPSILON {
279 return Err(CoppError::InvalidInput(
280 "copp3_socp".into(),
281 format!(
282 "The initial a = {} is zero, so the initial b = {} must also be zero.",
283 a_boundary.0, b_boundary.0
284 ),
285 ));
286 }
287 }
288 if a_boundary.1.abs() < f64::EPSILON {
289 if b_boundary.1.abs() >= f64::EPSILON {
291 return Err(CoppError::InvalidInput(
292 "copp3_socp".into(),
293 format!(
294 "The terminal a = {} is zero, so the terminal b = {} must also be zero.",
295 a_boundary.1, b_boundary.1
296 ),
297 ));
298 }
299 }
300 Ok(())
301}