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("Robot dynamics error: {0}")]
18 RobotDynamicsError(#[from] RobotDynamicsError),
19 #[error("{0} reported an infeasibility: {1}")]
21 Infeasible(String, String),
22 #[error("{0} reported an unboundedness: {1}")]
24 Unbounded(String, String),
25 #[error("{0} reported an invalid input: {1}")]
27 InvalidInput(String, String),
28 #[error("{0} reported an invalid options: {1}")]
30 InvalidOptions(String, String),
31 #[error("{0} reported an error in Clarabel solver: {1}")]
33 ClarabelSolverError(String, #[source] ClarabelSolverError),
34 #[error("{0} reported a failure in Clarabel solver with status {1}")]
36 ClarabelSolverStatus(String, ClarabelSolverStatus),
37 #[error("{0} reported an error: {1}")]
39 Other(String, String),
40}
41
42#[derive(Error, Debug, Clone, PartialEq, Eq)]
48#[error("{message}")]
49pub struct RobotDynamicsError {
50 message: String,
51}
52
53impl RobotDynamicsError {
54 #[inline]
56 pub fn new(message: impl Into<String>) -> Self {
57 Self {
58 message: message.into(),
59 }
60 }
61
62 #[inline]
64 pub fn message(&self) -> &str {
65 &self.message
66 }
67
68 #[inline]
70 pub fn into_message(self) -> String {
71 self.message
72 }
73}
74
75impl From<String> for RobotDynamicsError {
76 #[inline]
77 fn from(message: String) -> Self {
78 Self::new(message)
79 }
80}
81
82impl From<&str> for RobotDynamicsError {
83 #[inline]
84 fn from(message: &str) -> Self {
85 Self::new(message)
86 }
87}
88
89#[derive(Error, Debug)]
99pub enum ConstraintError {
100 #[error("`s` must be strictly increasing; first violation at local index {index}.")]
102 NonIncreasingS {
103 index: usize,
105 },
106
107 #[error("Input dimensions do not match the expected shape.")]
109 NoMatchDimensions,
110
111 #[error("Input order does not satisfy the expected contract.")]
113 NoMatchOrder,
114
115 #[error(
117 "`{bound_name}` requires strict signed limits at every station: upper bound > 0 and lower bound < 0."
118 )]
119 InvalidSignedBounds {
120 bound_name: &'static str,
122 },
123
124 #[error("Requested station interval is out of bounds: idx_s={idx_s}, len={len}.")]
126 OutOfSBounds {
127 idx_s: usize,
129 len: usize,
131 },
132
133 #[error(
135 "Input `a` violates positivity requirements (must be nonnegative, and strictly positive where required)."
136 )]
137 NonPositiveA,
138
139 #[error("Linearization floor must be strictly positive.")]
141 NonPositiveLinearizationFloor,
142
143 #[error(
145 "Required derivative data (`q`, `dq`, `ddq`, `dddq`) is not fully available in the requested interval."
146 )]
147 NoGivenQInfo,
148
149 #[error(
151 "Linearized jerk constraints are unavailable at idx_s={idx_s}; valid range is [{}, {}).",
152 valid_range.0,
153 valid_range.1
154 )]
155 LinearJerkNotAvailable {
156 idx_s: usize,
158 valid_range: (usize, usize),
160 },
161
162 #[error("Required dynamic-model information is not available.")]
164 NoDynamic,
165
166 #[error("Reference profile is infeasible under current constraints.")]
168 InfeasibleReference,
169
170 #[error("Requested interval is empty: {start} <= idx_s < {end}.")]
172 EmptyInterval {
173 start: usize,
175 end: usize,
177 },
178}
179
180#[derive(Error, Debug)]
186pub enum PathError {
187 #[error("invalid dimension: {dim}")]
189 InvalidDimension {
190 dim: usize,
192 },
193 #[error("invalid s range: [{s_min}, {s_max}]")]
195 InvalidRange {
196 s_min: f64,
198 s_max: f64,
200 },
201 #[error("invalid spline order: {order}, expected >= 3")]
203 InvalidOrder {
204 order: usize,
206 },
207 #[error("dimension mismatch")]
209 DimensionMismatch,
210 #[error("unsupported derivative order: requested {requested}, available {available}")]
212 UnsupportedDerivativeOrder {
213 requested: usize,
215 available: usize,
217 },
218 #[error("path evaluator error: {message}")]
220 EvaluatorError {
221 message: String,
223 },
224 #[error("not enough waypoints: {n}, expected >= 2")]
226 NotEnoughWaypoints {
227 n: usize,
229 },
230 #[error("s out of range [{s_min}, {s_max}] at index {index}: {value}")]
232 OutOfRangeS {
233 s_min: f64,
235 s_max: f64,
237 index: usize,
239 value: f64,
241 },
242 #[error("unsupported boundary for order={order}")]
244 UnsupportedBoundary {
245 order: usize,
247 },
248 #[error("singular linear system")]
250 SingularSystem,
251}
252
253impl std::fmt::Debug for CoppError {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 write!(f, "{self}")
257 }
258}
259
260#[inline(always)]
262pub(crate) fn check_s_interval_valid(
263 function_name: &str,
264 idx_s_start: usize,
265 idx_s_final: usize,
266) -> Result<(), CoppError> {
267 if idx_s_final < idx_s_start + 2 {
268 Err(CoppError::InvalidInput(
269 function_name.into(),
270 format!(
271 "The final index {idx_s_final} must be at least two positions after the start index {idx_s_start} in Topp2Problem."
272 ),
273 ))
274 } else {
275 Ok(())
276 }
277}
278
279#[inline(always)]
281pub(crate) fn check_abs_rel_tol(
282 function_name: &str,
283 abs_tol_name: &str,
284 abs_tol: f64,
285 rel_tol_name: &str,
286 rel_tol: f64,
287) -> Result<(), CoppError> {
288 check_options_not_nan_infinite(function_name, abs_tol_name, abs_tol)?;
289 check_options_not_nan_infinite(function_name, rel_tol_name, rel_tol)?;
290 if abs_tol >= 0.0 && rel_tol >= 0.0 && (abs_tol > f64::EPSILON || rel_tol > f64::EPSILON) {
291 Ok(())
292 } else {
293 Err(CoppError::InvalidOptions(
294 function_name.into(),
295 format!(
296 "At least one of {abs_tol_name} = {abs_tol} and {rel_tol_name} = {rel_tol} must be strictly positive."
297 ),
298 ))
299 }
300}
301
302#[inline(always)]
303pub(crate) fn check_options_not_nan_infinite(
304 function_name: &str,
305 var_name: &str,
306 var_value: f64,
307) -> Result<(), CoppError> {
308 if var_value.is_nan() {
309 Err(CoppError::InvalidOptions(
310 function_name.into(),
311 format!("{var_name} = {var_value} must not be NaN",),
312 ))
313 } else if var_value.is_infinite() {
314 Err(CoppError::InvalidOptions(
315 function_name.into(),
316 format!("{var_name} = {var_value} must not be infinite",),
317 ))
318 } else {
319 Ok(())
320 }
321}
322
323#[inline(always)]
328pub(crate) fn check_input_not_nan_infinite(
329 function_name: &str,
330 var_name: &str,
331 var_value: f64,
332) -> Result<(), CoppError> {
333 if var_value.is_nan() {
334 Err(CoppError::InvalidInput(
335 function_name.into(),
336 format!("{var_name} = {var_value} must not be NaN"),
337 ))
338 } else if var_value.is_infinite() {
339 Err(CoppError::InvalidInput(
340 function_name.into(),
341 format!("{var_name} = {var_value} must not be infinite"),
342 ))
343 } else {
344 Ok(())
345 }
346}
347
348#[inline(always)]
354pub(crate) fn check_input_slice_not_nan_infinite(
355 function_name: &str,
356 slice_name: &str,
357 values: &[f64],
358) -> Result<(), CoppError> {
359 if let Some((index, value)) = values.iter().enumerate().find(|(_, value)| value.is_nan()) {
360 Err(CoppError::InvalidInput(
361 function_name.into(),
362 format!("`{slice_name}[{index}]` = {value} must not be NaN"),
363 ))
364 } else if let Some((index, value)) = values
365 .iter()
366 .enumerate()
367 .find(|(_, value)| value.is_infinite())
368 {
369 Err(CoppError::InvalidInput(
370 function_name.into(),
371 format!("`{slice_name}[{index}]` = {value} must not be infinite"),
372 ))
373 } else {
374 Ok(())
375 }
376}
377
378#[inline(always)]
384pub(crate) fn check_input_strictly_increasing(
385 function_name: &str,
386 slice_name: &str,
387 values: &[f64],
388) -> Result<(), CoppError> {
389 if let Some(index) = values.windows(2).position(|pair| pair[0] >= pair[1]) {
390 Err(CoppError::InvalidInput(
391 function_name.into(),
392 format!(
393 "`{slice_name}` must be strictly increasing; first violation at local index {index}."
394 ),
395 ))
396 } else {
397 Ok(())
398 }
399}
400
401#[inline(always)]
406pub(crate) fn check_input_non_negative(
407 function_name: &str,
408 var_name: &str,
409 var_value: f64,
410) -> Result<(), CoppError> {
411 check_input_not_nan_infinite(function_name, var_name, var_value)?;
412 if var_value < 0.0 {
413 Err(CoppError::InvalidInput(
414 function_name.into(),
415 format!("{var_name} = {var_value} must be nonnegative"),
416 ))
417 } else {
418 Ok(())
419 }
420}
421
422#[inline(always)]
427pub(crate) fn check_input_slice_non_negative(
428 function_name: &str,
429 slice_name: &str,
430 values: &[f64],
431) -> Result<(), CoppError> {
432 check_input_slice_not_nan_infinite(function_name, slice_name, values)?;
433 if let Some((index, value)) = values.iter().enumerate().find(|(_, value)| **value < 0.0) {
434 Err(CoppError::InvalidInput(
435 function_name.into(),
436 format!("`{slice_name}[{index}]` = {value} must be nonnegative"),
437 ))
438 } else {
439 Ok(())
440 }
441}
442
443#[inline(always)]
448pub(crate) fn check_input_len_at_least(
449 function_name: &str,
450 len_name: &str,
451 len: usize,
452 min_len: usize,
453) -> Result<(), CoppError> {
454 if len < min_len {
455 Err(CoppError::InvalidInput(
456 function_name.into(),
457 format!("{len_name} = {len} must be at least {min_len}"),
458 ))
459 } else {
460 Ok(())
461 }
462}
463
464#[inline(always)]
469pub(crate) fn check_input_len_equal(
470 function_name: &str,
471 lhs_name: &str,
472 lhs_len: usize,
473 rhs_name: &str,
474 rhs_len: usize,
475) -> Result<(), CoppError> {
476 if lhs_len != rhs_len {
477 Err(CoppError::InvalidInput(
478 function_name.into(),
479 format!("{lhs_name} = {lhs_len} must equal {rhs_name} = {rhs_len}"),
480 ))
481 } else {
482 Ok(())
483 }
484}
485
486#[inline(always)]
491pub(crate) fn check_input_not_empty(
492 function_name: &str,
493 slice_name: &str,
494 len: usize,
495) -> Result<(), CoppError> {
496 if len == 0 {
497 Err(CoppError::InvalidInput(
498 function_name.into(),
499 format!("{slice_name} must not be empty"),
500 ))
501 } else {
502 Ok(())
503 }
504}
505
506#[inline(always)]
508pub(crate) fn check_non_negative(
509 function_name: &str,
510 var_name: &str,
511 var_value: f64,
512) -> Result<(), CoppError> {
513 check_options_not_nan_infinite(function_name, var_name, var_value)?;
514 if var_value < 0.0 {
515 Err(CoppError::InvalidOptions(
516 function_name.into(),
517 format!("{var_name} = {var_value} must be strictly non-negative"),
518 ))
519 } else {
520 Ok(())
521 }
522}
523
524#[inline(always)]
526pub(crate) fn check_strictly_positive(
527 function_name: &str,
528 var_name: &str,
529 var_value: f64,
530) -> Result<(), CoppError> {
531 check_options_not_nan_infinite(function_name, var_name, var_value)?;
532 if var_value < f64::EPSILON {
533 Err(CoppError::InvalidOptions(
534 function_name.into(),
535 format!("{var_name} = {var_value} must be strictly positive"),
536 ))
537 } else {
538 Ok(())
539 }
540}
541
542#[inline(always)]
543pub(crate) fn check_boundary_state_copp3_valid(
544 a_boundary: (f64, f64),
545 b_boundary: (f64, f64),
546) -> Result<(), CoppError> {
547 if a_boundary.0 < 0.0 {
548 return Err(CoppError::InvalidInput(
549 "copp3_socp".into(),
550 format!("The initial a = {} must be non-negative.", a_boundary.0),
551 ));
552 }
553 if a_boundary.1 < 0.0 {
554 return Err(CoppError::InvalidInput(
555 "copp3_socp".into(),
556 format!("The terminal a = {} must be non-negative.", a_boundary.1),
557 ));
558 }
559 if a_boundary.0.abs() < f64::EPSILON {
560 if b_boundary.0.abs() >= f64::EPSILON {
562 return Err(CoppError::InvalidInput(
563 "copp3_socp".into(),
564 format!(
565 "The initial a = {} is zero, so the initial b = {} must also be zero.",
566 a_boundary.0, b_boundary.0
567 ),
568 ));
569 }
570 }
571 if a_boundary.1.abs() < f64::EPSILON {
572 if b_boundary.1.abs() >= f64::EPSILON {
574 return Err(CoppError::InvalidInput(
575 "copp3_socp".into(),
576 format!(
577 "The terminal a = {} is zero, so the terminal b = {} must also be zero.",
578 a_boundary.1, b_boundary.1
579 ),
580 ));
581 }
582 }
583 Ok(())
584}