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("not enough waypoints: {n}, expected >= 2")]
220 NotEnoughWaypoints {
221 n: usize,
223 },
224 #[error("s out of range [{s_min}, {s_max}] at index {index}: {value}")]
226 OutOfRangeS {
227 s_min: f64,
229 s_max: f64,
231 index: usize,
233 value: f64,
235 },
236 #[error("unsupported boundary for order={order}")]
238 UnsupportedBoundary {
239 order: usize,
241 },
242 #[error("singular linear system")]
244 SingularSystem,
245}
246
247impl std::fmt::Debug for CoppError {
249 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250 write!(f, "{self}")
251 }
252}
253
254#[inline(always)]
256pub(crate) fn check_s_interval_valid(
257 function_name: &str,
258 idx_s_start: usize,
259 idx_s_final: usize,
260) -> Result<(), CoppError> {
261 if idx_s_final < idx_s_start + 2 {
262 Err(CoppError::InvalidInput(
263 function_name.into(),
264 format!(
265 "The final index {idx_s_final} must be at least two positions after the start index {idx_s_start} in Topp2Problem."
266 ),
267 ))
268 } else {
269 Ok(())
270 }
271}
272
273#[inline(always)]
275pub(crate) fn check_abs_rel_tol(
276 function_name: &str,
277 abs_tol_name: &str,
278 abs_tol: f64,
279 rel_tol_name: &str,
280 rel_tol: f64,
281) -> Result<(), CoppError> {
282 check_options_not_nan_infinite(function_name, abs_tol_name, abs_tol)?;
283 check_options_not_nan_infinite(function_name, rel_tol_name, rel_tol)?;
284 if abs_tol >= 0.0 && rel_tol >= 0.0 && (abs_tol > f64::EPSILON || rel_tol > f64::EPSILON) {
285 Ok(())
286 } else {
287 Err(CoppError::InvalidOptions(
288 function_name.into(),
289 format!(
290 "At least one of {abs_tol_name} = {abs_tol} and {rel_tol_name} = {rel_tol} must be strictly positive."
291 ),
292 ))
293 }
294}
295
296#[inline(always)]
297pub(crate) fn check_options_not_nan_infinite(
298 function_name: &str,
299 var_name: &str,
300 var_value: f64,
301) -> Result<(), CoppError> {
302 if var_value.is_nan() {
303 Err(CoppError::InvalidOptions(
304 function_name.into(),
305 format!("{var_name} = {var_value} must not be NaN",),
306 ))
307 } else if var_value.is_infinite() {
308 Err(CoppError::InvalidOptions(
309 function_name.into(),
310 format!("{var_name} = {var_value} must not be infinite",),
311 ))
312 } else {
313 Ok(())
314 }
315}
316
317#[inline(always)]
322pub(crate) fn check_input_not_nan_infinite(
323 function_name: &str,
324 var_name: &str,
325 var_value: f64,
326) -> Result<(), CoppError> {
327 if var_value.is_nan() {
328 Err(CoppError::InvalidInput(
329 function_name.into(),
330 format!("{var_name} = {var_value} must not be NaN"),
331 ))
332 } else if var_value.is_infinite() {
333 Err(CoppError::InvalidInput(
334 function_name.into(),
335 format!("{var_name} = {var_value} must not be infinite"),
336 ))
337 } else {
338 Ok(())
339 }
340}
341
342#[inline(always)]
348pub(crate) fn check_input_slice_not_nan_infinite(
349 function_name: &str,
350 slice_name: &str,
351 values: &[f64],
352) -> Result<(), CoppError> {
353 if let Some((index, value)) = values.iter().enumerate().find(|(_, value)| value.is_nan()) {
354 Err(CoppError::InvalidInput(
355 function_name.into(),
356 format!("`{slice_name}[{index}]` = {value} must not be NaN"),
357 ))
358 } else if let Some((index, value)) = values
359 .iter()
360 .enumerate()
361 .find(|(_, value)| value.is_infinite())
362 {
363 Err(CoppError::InvalidInput(
364 function_name.into(),
365 format!("`{slice_name}[{index}]` = {value} must not be infinite"),
366 ))
367 } else {
368 Ok(())
369 }
370}
371
372#[inline(always)]
378pub(crate) fn check_input_strictly_increasing(
379 function_name: &str,
380 slice_name: &str,
381 values: &[f64],
382) -> Result<(), CoppError> {
383 if let Some(index) = values.windows(2).position(|pair| pair[0] >= pair[1]) {
384 Err(CoppError::InvalidInput(
385 function_name.into(),
386 format!(
387 "`{slice_name}` must be strictly increasing; first violation at local index {index}."
388 ),
389 ))
390 } else {
391 Ok(())
392 }
393}
394
395#[inline(always)]
400pub(crate) fn check_input_non_negative(
401 function_name: &str,
402 var_name: &str,
403 var_value: f64,
404) -> Result<(), CoppError> {
405 check_input_not_nan_infinite(function_name, var_name, var_value)?;
406 if var_value < 0.0 {
407 Err(CoppError::InvalidInput(
408 function_name.into(),
409 format!("{var_name} = {var_value} must be nonnegative"),
410 ))
411 } else {
412 Ok(())
413 }
414}
415
416#[inline(always)]
421pub(crate) fn check_input_slice_non_negative(
422 function_name: &str,
423 slice_name: &str,
424 values: &[f64],
425) -> Result<(), CoppError> {
426 check_input_slice_not_nan_infinite(function_name, slice_name, values)?;
427 if let Some((index, value)) = values.iter().enumerate().find(|(_, value)| **value < 0.0) {
428 Err(CoppError::InvalidInput(
429 function_name.into(),
430 format!("`{slice_name}[{index}]` = {value} must be nonnegative"),
431 ))
432 } else {
433 Ok(())
434 }
435}
436
437#[inline(always)]
442pub(crate) fn check_input_len_at_least(
443 function_name: &str,
444 len_name: &str,
445 len: usize,
446 min_len: usize,
447) -> Result<(), CoppError> {
448 if len < min_len {
449 Err(CoppError::InvalidInput(
450 function_name.into(),
451 format!("{len_name} = {len} must be at least {min_len}"),
452 ))
453 } else {
454 Ok(())
455 }
456}
457
458#[inline(always)]
463pub(crate) fn check_input_len_equal(
464 function_name: &str,
465 lhs_name: &str,
466 lhs_len: usize,
467 rhs_name: &str,
468 rhs_len: usize,
469) -> Result<(), CoppError> {
470 if lhs_len != rhs_len {
471 Err(CoppError::InvalidInput(
472 function_name.into(),
473 format!("{lhs_name} = {lhs_len} must equal {rhs_name} = {rhs_len}"),
474 ))
475 } else {
476 Ok(())
477 }
478}
479
480#[inline(always)]
485pub(crate) fn check_input_not_empty(
486 function_name: &str,
487 slice_name: &str,
488 len: usize,
489) -> Result<(), CoppError> {
490 if len == 0 {
491 Err(CoppError::InvalidInput(
492 function_name.into(),
493 format!("{slice_name} must not be empty"),
494 ))
495 } else {
496 Ok(())
497 }
498}
499
500#[inline(always)]
502pub(crate) fn check_non_negative(
503 function_name: &str,
504 var_name: &str,
505 var_value: f64,
506) -> Result<(), CoppError> {
507 check_options_not_nan_infinite(function_name, var_name, var_value)?;
508 if var_value < 0.0 {
509 Err(CoppError::InvalidOptions(
510 function_name.into(),
511 format!("{var_name} = {var_value} must be strictly non-negative"),
512 ))
513 } else {
514 Ok(())
515 }
516}
517
518#[inline(always)]
520pub(crate) fn check_strictly_positive(
521 function_name: &str,
522 var_name: &str,
523 var_value: f64,
524) -> Result<(), CoppError> {
525 check_options_not_nan_infinite(function_name, var_name, var_value)?;
526 if var_value < f64::EPSILON {
527 Err(CoppError::InvalidOptions(
528 function_name.into(),
529 format!("{var_name} = {var_value} must be strictly positive"),
530 ))
531 } else {
532 Ok(())
533 }
534}
535
536#[inline(always)]
537pub(crate) fn check_boundary_state_copp3_valid(
538 a_boundary: (f64, f64),
539 b_boundary: (f64, f64),
540) -> Result<(), CoppError> {
541 if a_boundary.0 < 0.0 {
542 return Err(CoppError::InvalidInput(
543 "copp3_socp".into(),
544 format!("The initial a = {} must be non-negative.", a_boundary.0),
545 ));
546 }
547 if a_boundary.1 < 0.0 {
548 return Err(CoppError::InvalidInput(
549 "copp3_socp".into(),
550 format!("The terminal a = {} must be non-negative.", a_boundary.1),
551 ));
552 }
553 if a_boundary.0.abs() < f64::EPSILON {
554 if b_boundary.0.abs() >= f64::EPSILON {
556 return Err(CoppError::InvalidInput(
557 "copp3_socp".into(),
558 format!(
559 "The initial a = {} is zero, so the initial b = {} must also be zero.",
560 a_boundary.0, b_boundary.0
561 ),
562 ));
563 }
564 }
565 if a_boundary.1.abs() < f64::EPSILON {
566 if b_boundary.1.abs() >= f64::EPSILON {
568 return Err(CoppError::InvalidInput(
569 "copp3_socp".into(),
570 format!(
571 "The terminal a = {} is zero, so the terminal b = {} must also be zero.",
572 a_boundary.1, b_boundary.1
573 ),
574 ));
575 }
576 }
577 Ok(())
578}