Skip to main content

copp\diag/
error.rs

1use clarabel::solver::{SolverError as ClarabelSolverError, SolverStatus as ClarabelSolverStatus};
2use thiserror::Error;
3
4/// The error type for COPP.
5#[derive(Error)]
6pub enum CoppError {
7    /// Some error occurred in filesystem I/O operations, such as opening/creating log files.
8    #[error("I/O error: {0}")]
9    IoError(#[from] std::io::Error),
10    /// Some error occurred in the Constraint struct, such as invalid input or computation failure.
11    #[error("Some error occurred in the Constraint struct: {0}")]
12    ConstraintError(#[from] ConstraintError),
13    /// Some error occurred in the Path struct, such as invalid input or computation failure.
14    #[error("Some error occurred in the Path struct: {0}")]
15    PathError(#[from] PathError),
16    /// Some error occurred while evaluating user-provided robot inverse dynamics.
17    #[error("Robot dynamics error: {0}")]
18    RobotDynamicsError(#[from] RobotDynamicsError),
19    /// The specified optimization problem is reported infeasible by the backend solver.
20    #[error("{0} reported an infeasibility: {1}")]
21    Infeasible(String, String),
22    /// The specified optimization problem is reported unbounded by the backend solver.
23    #[error("{0} reported an unboundedness: {1}")]
24    Unbounded(String, String),
25    /// The solver/backend rejects the given model or data as invalid input.
26    #[error("{0} reported an invalid input: {1}")]
27    InvalidInput(String, String),
28    /// The solver/backend rejects the provided configuration options.
29    #[error("{0} reported an invalid options: {1}")]
30    InvalidOptions(String, String),
31    /// The Clarabel solver returned a concrete internal solver error.
32    #[error("{0} reported an error in Clarabel solver: {1}")]
33    ClarabelSolverError(String, #[source] ClarabelSolverError),
34    /// The Clarabel solver terminated with a non-success status.
35    #[error("{0} reported a failure in Clarabel solver with status {1}")]
36    ClarabelSolverStatus(String, ClarabelSolverStatus),
37    /// A backend-specific or uncategorized runtime error is reported.
38    #[error("{0} reported an error: {1}")]
39    Other(String, String),
40}
41
42/// Error type for user-provided robot inverse-dynamics evaluation.
43///
44/// This type intentionally stores a free-form message so robot integrations can
45/// report errors from external dynamics libraries without fitting them into a
46/// fixed COPP-specific taxonomy.
47#[derive(Error, Debug, Clone, PartialEq, Eq)]
48#[error("{message}")]
49pub struct RobotDynamicsError {
50    message: String,
51}
52
53impl RobotDynamicsError {
54    /// Construct a robot dynamics error from a display-ready message.
55    #[inline]
56    pub fn new(message: impl Into<String>) -> Self {
57        Self {
58            message: message.into(),
59        }
60    }
61
62    /// Borrow the underlying error message.
63    #[inline]
64    pub fn message(&self) -> &str {
65        &self.message
66    }
67
68    /// Consume the error and return its message.
69    #[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/// Error type for constraint storage/query operations.
90///
91/// # Usage recommendation
92/// For public-facing application code, prefer using [`CoppError`](crate::diag::CoppError)
93/// as the unified error type.
94///
95/// [`ConstraintError`](crate::diag::ConstraintError) is automatically converted into [`CoppError`](crate::diag::CoppError) via
96/// `From<ConstraintError> for CoppError`, so `?` can be used directly when
97/// your function returns `Result<_, CoppError>`.
98#[derive(Error, Debug)]
99pub enum ConstraintError {
100    /// Input station sequence is not strictly increasing.
101    #[error("`s` must be strictly increasing; first violation at local index {index}.")]
102    NonIncreasingS {
103        /// Local index of the first non-increasing station.
104        index: usize,
105    },
106
107    /// Input matrix/vector dimensions are incompatible with expected shape.
108    #[error("Input dimensions do not match the expected shape.")]
109    NoMatchDimensions,
110
111    /// Input ordering contract is violated.
112    #[error("Input order does not satisfy the expected contract.")]
113    NoMatchOrder,
114
115    /// Signed upper/lower bounds violate strict feasibility contract.
116    #[error(
117        "`{bound_name}` requires strict signed limits at every station: upper bound > 0 and lower bound < 0."
118    )]
119    InvalidSignedBounds {
120        /// Name of the bound array that violated the signed-bound contract.
121        bound_name: &'static str,
122    },
123
124    /// Requested station interval is outside currently stored constraints range.
125    #[error("Requested station interval is out of bounds: idx_s={idx_s}, len={len}.")]
126    OutOfSBounds {
127        /// Requested starting station index.
128        idx_s: usize,
129        /// Number of stations currently stored.
130        len: usize,
131    },
132
133    /// `a` violates positivity / non-negativity preconditions.
134    #[error(
135        "Input `a` violates positivity requirements (must be nonnegative, and strictly positive where required)."
136    )]
137    NonPositiveA,
138
139    /// Linearization floor must be strictly positive.
140    #[error("Linearization floor must be strictly positive.")]
141    NonPositiveLinearizationFloor,
142
143    /// Required path-derivative data is missing in the requested interval.
144    #[error(
145        "Required derivative data (`q`, `dq`, `ddq`, `dddq`) is not fully available in the requested interval."
146    )]
147    NoGivenQInfo,
148
149    /// Linearized jerk constraints are unavailable at the requested station.
150    #[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        /// Requested station index.
157        idx_s: usize,
158        /// Half-open station range for which linearized jerk data is available.
159        valid_range: (usize, usize),
160    },
161
162    /// Dynamic-model data has not been provided.
163    #[error("Required dynamic-model information is not available.")]
164    NoDynamic,
165
166    /// Reference profile is infeasible under current constraints.
167    #[error("Reference profile is infeasible under current constraints.")]
168    InfeasibleReference,
169
170    /// Requested interval is empty.
171    #[error("Requested interval is empty: {start} <= idx_s < {end}.")]
172    EmptyInterval {
173        /// Start index of the requested interval.
174        start: usize,
175        /// End index of the requested interval.
176        end: usize,
177    },
178}
179
180/// Error type for path construction and path evaluation APIs.
181///
182/// This error is returned by path-related modules such as [`Path`](crate::path::Path)
183/// and spline utilities when input data, parameter ranges, or numerical systems
184/// are invalid.
185#[derive(Error, Debug)]
186pub enum PathError {
187    /// Path dimension is invalid (typically zero).
188    #[error("invalid dimension: {dim}")]
189    InvalidDimension {
190        /// Requested path dimension.
191        dim: usize,
192    },
193    /// Path parameter range is invalid (must satisfy finite `s_min < s_max`).
194    #[error("invalid s range: [{s_min}, {s_max}]")]
195    InvalidRange {
196        /// Lower endpoint of the invalid parameter range.
197        s_min: f64,
198        /// Upper endpoint of the invalid parameter range.
199        s_max: f64,
200    },
201    /// Spline order is invalid (must satisfy required minimum/order constraints).
202    #[error("invalid spline order: {order}, expected >= 3")]
203    InvalidOrder {
204        /// Requested spline order.
205        order: usize,
206    },
207    /// Matrix/tensor shapes are incompatible for the requested operation.
208    #[error("dimension mismatch")]
209    DimensionMismatch,
210    /// The path representation cannot provide the requested derivative order.
211    #[error("unsupported derivative order: requested {requested}, available {available}")]
212    UnsupportedDerivativeOrder {
213        /// Requested derivative order.
214        requested: usize,
215        /// Highest derivative order available from the path representation.
216        available: usize,
217    },
218    /// Waypoint sequence is too short to build a valid path.
219    #[error("not enough waypoints: {n}, expected >= 2")]
220    NotEnoughWaypoints {
221        /// Number of supplied waypoints.
222        n: usize,
223    },
224    /// Query parameter `s` is outside the configured valid interval.
225    #[error("s out of range [{s_min}, {s_max}] at index {index}: {value}")]
226    OutOfRangeS {
227        /// Lower endpoint of the valid parameter range.
228        s_min: f64,
229        /// Upper endpoint of the valid parameter range.
230        s_max: f64,
231        /// Index of the out-of-range query value.
232        index: usize,
233        /// Out-of-range query value.
234        value: f64,
235    },
236    /// Boundary conditions are not supported for the requested spline order.
237    #[error("unsupported boundary for order={order}")]
238    UnsupportedBoundary {
239        /// Requested spline order.
240        order: usize,
241    },
242    /// Internal linear system is singular and cannot be solved robustly.
243    #[error("singular linear system")]
244    SingularSystem,
245}
246
247/// Force the debug format of ToppError to be the same as the display format, which is more concise and user-friendly.
248impl 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/// Check whether a given s_interval is valid.
255#[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/// Check the positivity of a given tolerance.
274#[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/// Check that a scalar input value is neither NaN nor infinite.
318///
319/// This is the input-data counterpart of [`check_not_nan_infinite`], which is
320/// reserved for option validation and therefore returns [`InvalidOptions`](CoppError::InvalidOptions).
321#[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/// Check that every value in an input slice is neither NaN nor infinite.
343///
344/// The reported variable name includes the first offending local index, which
345/// keeps interpolation and solver diagnostics precise without duplicating this
346/// scan logic in each module.
347#[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/// Check that an input slice is strictly increasing.
373///
374/// This check assumes finiteness has already been verified when NaN-specific
375/// diagnostics are needed; otherwise comparisons involving NaN simply fail the
376/// ordering contract at the first affected pair.
377#[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/// Check that an input scalar is nonnegative.
396///
397/// This helper first rejects NaN and infinity so downstream numerical code can
398/// safely use ordinary comparisons and square roots.
399#[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/// Check that every value in an input slice is nonnegative.
417///
418/// The function reports the first offending entry and is intended for data
419/// profiles such as sampled `a(s)` that must be valid before interpolation.
420#[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/// Check that an input length is at least the required minimum.
438///
439/// Callers pass display-ready length names such as `` `s.len()` `` so error
440/// messages can mirror the notation used in each API contract.
441#[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/// Check that two input lengths are equal.
459///
460/// This is used for shape contracts where both the actual and reference lengths
461/// are useful to report to the caller.
462#[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/// Check that an input slice is not empty.
481///
482/// This helper is for APIs where an empty user-provided sample grid is
483/// ambiguous and should be rejected before interpolation starts.
484#[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/// Check the non-negativity.
501#[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/// Check the positivity of a given tolerance.
519#[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 a[0]==0 but b[0]!=0, then a<0 will occur near s_start
555        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 a[n]==0 but b[n]!=0, then a<0 will occur near s_final
567        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}