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    /// A user-provided path evaluator returned or threw an error.
219    #[error("path evaluator error: {message}")]
220    EvaluatorError {
221        /// Display-ready evaluator error message.
222        message: String,
223    },
224    /// Waypoint sequence is too short to build a valid path.
225    #[error("not enough waypoints: {n}, expected >= 2")]
226    NotEnoughWaypoints {
227        /// Number of supplied waypoints.
228        n: usize,
229    },
230    /// Query parameter `s` is outside the configured valid interval.
231    #[error("s out of range [{s_min}, {s_max}] at index {index}: {value}")]
232    OutOfRangeS {
233        /// Lower endpoint of the valid parameter range.
234        s_min: f64,
235        /// Upper endpoint of the valid parameter range.
236        s_max: f64,
237        /// Index of the out-of-range query value.
238        index: usize,
239        /// Out-of-range query value.
240        value: f64,
241    },
242    /// Boundary conditions are not supported for the requested spline order.
243    #[error("unsupported boundary for order={order}")]
244    UnsupportedBoundary {
245        /// Requested spline order.
246        order: usize,
247    },
248    /// Internal linear system is singular and cannot be solved robustly.
249    #[error("singular linear system")]
250    SingularSystem,
251}
252
253/// Force the debug format of ToppError to be the same as the display format, which is more concise and user-friendly.
254impl 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/// Check whether a given s_interval is valid.
261#[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/// Check the positivity of a given tolerance.
280#[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/// Check that a scalar input value is neither NaN nor infinite.
324///
325/// This is the input-data counterpart of [`check_not_nan_infinite`], which is
326/// reserved for option validation and therefore returns [`InvalidOptions`](CoppError::InvalidOptions).
327#[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/// Check that every value in an input slice is neither NaN nor infinite.
349///
350/// The reported variable name includes the first offending local index, which
351/// keeps interpolation and solver diagnostics precise without duplicating this
352/// scan logic in each module.
353#[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/// Check that an input slice is strictly increasing.
379///
380/// This check assumes finiteness has already been verified when NaN-specific
381/// diagnostics are needed; otherwise comparisons involving NaN simply fail the
382/// ordering contract at the first affected pair.
383#[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/// Check that an input scalar is nonnegative.
402///
403/// This helper first rejects NaN and infinity so downstream numerical code can
404/// safely use ordinary comparisons and square roots.
405#[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/// Check that every value in an input slice is nonnegative.
423///
424/// The function reports the first offending entry and is intended for data
425/// profiles such as sampled `a(s)` that must be valid before interpolation.
426#[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/// Check that an input length is at least the required minimum.
444///
445/// Callers pass display-ready length names such as `` `s.len()` `` so error
446/// messages can mirror the notation used in each API contract.
447#[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/// Check that two input lengths are equal.
465///
466/// This is used for shape contracts where both the actual and reference lengths
467/// are useful to report to the caller.
468#[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/// Check that an input slice is not empty.
487///
488/// This helper is for APIs where an empty user-provided sample grid is
489/// ambiguous and should be rejected before interpolation starts.
490#[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/// Check the non-negativity.
507#[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/// Check the positivity of a given tolerance.
525#[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 a[0]==0 but b[0]!=0, then a<0 will occur near s_start
561        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 a[n]==0 but b[n]!=0, then a<0 will occur near s_final
573        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}