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)]
187pub enum PathError {
188 #[error("path smoothing failed: {message}")]
199 Smoothing {
200 message: String,
202 },
203 #[error("invalid dimension: {dim}")]
205 InvalidDimension {
206 dim: usize,
208 },
209 #[error("invalid s range: [{s_min}, {s_max}]")]
211 InvalidRange {
212 s_min: f64,
214 s_max: f64,
216 },
217 #[error("invalid spline order: {order}, expected >= 3")]
219 InvalidOrder {
220 order: usize,
222 },
223 #[error("dimension mismatch")]
225 DimensionMismatch,
226 #[error("unsupported derivative order: requested {requested}, available {available}")]
228 UnsupportedDerivativeOrder {
229 requested: usize,
231 available: usize,
233 },
234 #[error("path evaluator error: {message}")]
236 EvaluatorError {
237 message: String,
239 },
240 #[error("not enough waypoints: {n}, expected >= 2")]
242 NotEnoughWaypoints {
243 n: usize,
245 },
246 #[error("s out of range [{s_min}, {s_max}] at index {index}: {value}")]
248 OutOfRangeS {
249 s_min: f64,
251 s_max: f64,
253 index: usize,
255 value: f64,
257 },
258 #[error("unsupported boundary for order={order}")]
260 UnsupportedBoundary {
261 order: usize,
263 },
264 #[error("singular linear system")]
266 SingularSystem,
267}
268
269impl std::fmt::Debug for CoppError {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 write!(f, "{self}")
273 }
274}
275
276#[inline(always)]
278pub(crate) fn check_s_interval_valid(
279 function_name: &str,
280 idx_s_start: usize,
281 idx_s_final: usize,
282) -> Result<(), CoppError> {
283 if idx_s_final < idx_s_start + 2 {
284 Err(CoppError::InvalidInput(
285 function_name.into(),
286 format!(
287 "The final index {idx_s_final} must be at least two positions after the start index {idx_s_start} in Topp2Problem."
288 ),
289 ))
290 } else {
291 Ok(())
292 }
293}
294
295#[inline(always)]
297pub(crate) fn check_abs_rel_tol(
298 function_name: &str,
299 abs_tol_name: &str,
300 abs_tol: f64,
301 rel_tol_name: &str,
302 rel_tol: f64,
303) -> Result<(), CoppError> {
304 check_options_not_nan_infinite(function_name, abs_tol_name, abs_tol)?;
305 check_options_not_nan_infinite(function_name, rel_tol_name, rel_tol)?;
306 if abs_tol >= 0.0 && rel_tol >= 0.0 && (abs_tol > f64::EPSILON || rel_tol > f64::EPSILON) {
307 Ok(())
308 } else {
309 Err(CoppError::InvalidOptions(
310 function_name.into(),
311 format!(
312 "At least one of {abs_tol_name} = {abs_tol} and {rel_tol_name} = {rel_tol} must be strictly positive."
313 ),
314 ))
315 }
316}
317
318#[inline(always)]
319pub(crate) fn check_options_not_nan_infinite(
320 function_name: &str,
321 var_name: &str,
322 var_value: f64,
323) -> Result<(), CoppError> {
324 if var_value.is_nan() {
325 Err(CoppError::InvalidOptions(
326 function_name.into(),
327 format!("{var_name} = {var_value} must not be NaN",),
328 ))
329 } else if var_value.is_infinite() {
330 Err(CoppError::InvalidOptions(
331 function_name.into(),
332 format!("{var_name} = {var_value} must not be infinite",),
333 ))
334 } else {
335 Ok(())
336 }
337}
338
339#[inline(always)]
344pub(crate) fn check_input_not_nan_infinite(
345 function_name: &str,
346 var_name: &str,
347 var_value: f64,
348) -> Result<(), CoppError> {
349 if var_value.is_nan() {
350 Err(CoppError::InvalidInput(
351 function_name.into(),
352 format!("{var_name} = {var_value} must not be NaN"),
353 ))
354 } else if var_value.is_infinite() {
355 Err(CoppError::InvalidInput(
356 function_name.into(),
357 format!("{var_name} = {var_value} must not be infinite"),
358 ))
359 } else {
360 Ok(())
361 }
362}
363
364#[inline(always)]
370pub(crate) fn check_input_slice_not_nan_infinite(
371 function_name: &str,
372 slice_name: &str,
373 values: &[f64],
374) -> Result<(), CoppError> {
375 if let Some((index, value)) = values.iter().enumerate().find(|(_, value)| value.is_nan()) {
376 Err(CoppError::InvalidInput(
377 function_name.into(),
378 format!("`{slice_name}[{index}]` = {value} must not be NaN"),
379 ))
380 } else if let Some((index, value)) = values
381 .iter()
382 .enumerate()
383 .find(|(_, value)| value.is_infinite())
384 {
385 Err(CoppError::InvalidInput(
386 function_name.into(),
387 format!("`{slice_name}[{index}]` = {value} must not be infinite"),
388 ))
389 } else {
390 Ok(())
391 }
392}
393
394#[inline(always)]
400pub(crate) fn check_input_strictly_increasing(
401 function_name: &str,
402 slice_name: &str,
403 values: &[f64],
404) -> Result<(), CoppError> {
405 if let Some(index) = values.windows(2).position(|pair| pair[0] >= pair[1]) {
406 Err(CoppError::InvalidInput(
407 function_name.into(),
408 format!(
409 "`{slice_name}` must be strictly increasing; first violation at local index {index}."
410 ),
411 ))
412 } else {
413 Ok(())
414 }
415}
416
417#[inline(always)]
422pub(crate) fn check_input_non_negative(
423 function_name: &str,
424 var_name: &str,
425 var_value: f64,
426) -> Result<(), CoppError> {
427 check_input_not_nan_infinite(function_name, var_name, var_value)?;
428 if var_value < 0.0 {
429 Err(CoppError::InvalidInput(
430 function_name.into(),
431 format!("{var_name} = {var_value} must be nonnegative"),
432 ))
433 } else {
434 Ok(())
435 }
436}
437
438#[inline(always)]
443pub(crate) fn check_input_slice_non_negative(
444 function_name: &str,
445 slice_name: &str,
446 values: &[f64],
447) -> Result<(), CoppError> {
448 check_input_slice_not_nan_infinite(function_name, slice_name, values)?;
449 if let Some((index, value)) = values.iter().enumerate().find(|(_, value)| **value < 0.0) {
450 Err(CoppError::InvalidInput(
451 function_name.into(),
452 format!("`{slice_name}[{index}]` = {value} must be nonnegative"),
453 ))
454 } else {
455 Ok(())
456 }
457}
458
459#[inline(always)]
464pub(crate) fn check_input_len_at_least(
465 function_name: &str,
466 len_name: &str,
467 len: usize,
468 min_len: usize,
469) -> Result<(), CoppError> {
470 if len < min_len {
471 Err(CoppError::InvalidInput(
472 function_name.into(),
473 format!("{len_name} = {len} must be at least {min_len}"),
474 ))
475 } else {
476 Ok(())
477 }
478}
479
480#[inline(always)]
485pub(crate) fn check_input_len_equal(
486 function_name: &str,
487 lhs_name: &str,
488 lhs_len: usize,
489 rhs_name: &str,
490 rhs_len: usize,
491) -> Result<(), CoppError> {
492 if lhs_len != rhs_len {
493 Err(CoppError::InvalidInput(
494 function_name.into(),
495 format!("{lhs_name} = {lhs_len} must equal {rhs_name} = {rhs_len}"),
496 ))
497 } else {
498 Ok(())
499 }
500}
501
502#[inline(always)]
507pub(crate) fn check_input_not_empty(
508 function_name: &str,
509 slice_name: &str,
510 len: usize,
511) -> Result<(), CoppError> {
512 if len == 0 {
513 Err(CoppError::InvalidInput(
514 function_name.into(),
515 format!("{slice_name} must not be empty"),
516 ))
517 } else {
518 Ok(())
519 }
520}
521
522#[inline(always)]
524pub(crate) fn check_non_negative(
525 function_name: &str,
526 var_name: &str,
527 var_value: f64,
528) -> Result<(), CoppError> {
529 check_options_not_nan_infinite(function_name, var_name, var_value)?;
530 if var_value < 0.0 {
531 Err(CoppError::InvalidOptions(
532 function_name.into(),
533 format!("{var_name} = {var_value} must be strictly non-negative"),
534 ))
535 } else {
536 Ok(())
537 }
538}
539
540#[inline(always)]
542pub(crate) fn check_strictly_positive(
543 function_name: &str,
544 var_name: &str,
545 var_value: f64,
546) -> Result<(), CoppError> {
547 check_options_not_nan_infinite(function_name, var_name, var_value)?;
548 if var_value < f64::EPSILON {
549 Err(CoppError::InvalidOptions(
550 function_name.into(),
551 format!("{var_name} = {var_value} must be strictly positive"),
552 ))
553 } else {
554 Ok(())
555 }
556}
557
558#[inline(always)]
559pub(crate) fn check_boundary_state_copp3_valid(
573 a_boundary: (f64, f64),
574 b_boundary: (f64, f64),
575) -> Result<(), CoppError> {
576 for (name, value) in [
577 ("initial a", a_boundary.0),
578 ("terminal a", a_boundary.1),
579 ("initial b", b_boundary.0),
580 ("terminal b", b_boundary.1),
581 ] {
582 check_input_not_nan_infinite("copp3", name, value)?;
583 }
584 if a_boundary.0 < 0.0 {
585 return Err(CoppError::InvalidInput(
586 "copp3_socp".into(),
587 format!("The initial a = {} must be non-negative.", a_boundary.0),
588 ));
589 }
590 if a_boundary.1 < 0.0 {
591 return Err(CoppError::InvalidInput(
592 "copp3_socp".into(),
593 format!("The terminal a = {} must be non-negative.", a_boundary.1),
594 ));
595 }
596 if a_boundary.0 == 0.0 && b_boundary.0 < 0.0 {
603 return Err(CoppError::InvalidInput(
604 "copp3_socp".into(),
605 format!(
606 "The initial a = {} is zero, so the initial b = {} must be non-negative.",
607 a_boundary.0, b_boundary.0
608 ),
609 ));
610 }
611 if a_boundary.1 == 0.0 && b_boundary.1 > 0.0 {
612 return Err(CoppError::InvalidInput(
613 "copp3_socp".into(),
614 format!(
615 "The terminal a = {} is zero, so the terminal b = {} must be non-positive.",
616 a_boundary.1, b_boundary.1
617 ),
618 ));
619 }
620 Ok(())
621}