use clarabel::solver::{SolverError as ClarabelSolverError, SolverStatus as ClarabelSolverStatus};
use thiserror::Error;
#[derive(Error)]
pub enum CoppError {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Some error occurred in the Constraint struct: {0}")]
ConstraintError(#[from] ConstraintError),
#[error("Some error occurred in the Path struct: {0}")]
PathError(#[from] PathError),
#[error("Robot dynamics error: {0}")]
RobotDynamicsError(#[from] RobotDynamicsError),
#[error("{0} reported an infeasibility: {1}")]
Infeasible(String, String),
#[error("{0} reported an unboundedness: {1}")]
Unbounded(String, String),
#[error("{0} reported an invalid input: {1}")]
InvalidInput(String, String),
#[error("{0} reported an invalid options: {1}")]
InvalidOptions(String, String),
#[error("{0} reported an error in Clarabel solver: {1}")]
ClarabelSolverError(String, #[source] ClarabelSolverError),
#[error("{0} reported a failure in Clarabel solver with status {1}")]
ClarabelSolverStatus(String, ClarabelSolverStatus),
#[error("{0} reported an error: {1}")]
Other(String, String),
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
#[error("{message}")]
pub struct RobotDynamicsError {
message: String,
}
impl RobotDynamicsError {
#[inline]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
#[inline]
pub fn message(&self) -> &str {
&self.message
}
#[inline]
pub fn into_message(self) -> String {
self.message
}
}
impl From<String> for RobotDynamicsError {
#[inline]
fn from(message: String) -> Self {
Self::new(message)
}
}
impl From<&str> for RobotDynamicsError {
#[inline]
fn from(message: &str) -> Self {
Self::new(message)
}
}
#[derive(Error, Debug)]
pub enum ConstraintError {
#[error("`s` must be strictly increasing; first violation at local index {index}.")]
NonIncreasingS {
index: usize,
},
#[error("Input dimensions do not match the expected shape.")]
NoMatchDimensions,
#[error("Input order does not satisfy the expected contract.")]
NoMatchOrder,
#[error(
"`{bound_name}` requires strict signed limits at every station: upper bound > 0 and lower bound < 0."
)]
InvalidSignedBounds {
bound_name: &'static str,
},
#[error("Requested station interval is out of bounds: idx_s={idx_s}, len={len}.")]
OutOfSBounds {
idx_s: usize,
len: usize,
},
#[error(
"Input `a` violates positivity requirements (must be nonnegative, and strictly positive where required)."
)]
NonPositiveA,
#[error("Linearization floor must be strictly positive.")]
NonPositiveLinearizationFloor,
#[error(
"Required derivative data (`q`, `dq`, `ddq`, `dddq`) is not fully available in the requested interval."
)]
NoGivenQInfo,
#[error(
"Linearized jerk constraints are unavailable at idx_s={idx_s}; valid range is [{}, {}).",
valid_range.0,
valid_range.1
)]
LinearJerkNotAvailable {
idx_s: usize,
valid_range: (usize, usize),
},
#[error("Required dynamic-model information is not available.")]
NoDynamic,
#[error("Reference profile is infeasible under current constraints.")]
InfeasibleReference,
#[error("Requested interval is empty: {start} <= idx_s < {end}.")]
EmptyInterval {
start: usize,
end: usize,
},
}
#[derive(Error, Debug)]
pub enum PathError {
#[error("invalid dimension: {dim}")]
InvalidDimension {
dim: usize,
},
#[error("invalid s range: [{s_min}, {s_max}]")]
InvalidRange {
s_min: f64,
s_max: f64,
},
#[error("invalid spline order: {order}, expected >= 3")]
InvalidOrder {
order: usize,
},
#[error("dimension mismatch")]
DimensionMismatch,
#[error("unsupported derivative order: requested {requested}, available {available}")]
UnsupportedDerivativeOrder {
requested: usize,
available: usize,
},
#[error("not enough waypoints: {n}, expected >= 2")]
NotEnoughWaypoints {
n: usize,
},
#[error("s out of range [{s_min}, {s_max}] at index {index}: {value}")]
OutOfRangeS {
s_min: f64,
s_max: f64,
index: usize,
value: f64,
},
#[error("unsupported boundary for order={order}")]
UnsupportedBoundary {
order: usize,
},
#[error("singular linear system")]
SingularSystem,
}
impl std::fmt::Debug for CoppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self}")
}
}
#[inline(always)]
pub(crate) fn check_s_interval_valid(
function_name: &str,
idx_s_start: usize,
idx_s_final: usize,
) -> Result<(), CoppError> {
if idx_s_final < idx_s_start + 2 {
Err(CoppError::InvalidInput(
function_name.into(),
format!(
"The final index {idx_s_final} must be at least two positions after the start index {idx_s_start} in Topp2Problem."
),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_abs_rel_tol(
function_name: &str,
abs_tol_name: &str,
abs_tol: f64,
rel_tol_name: &str,
rel_tol: f64,
) -> Result<(), CoppError> {
check_options_not_nan_infinite(function_name, abs_tol_name, abs_tol)?;
check_options_not_nan_infinite(function_name, rel_tol_name, rel_tol)?;
if abs_tol >= 0.0 && rel_tol >= 0.0 && (abs_tol > f64::EPSILON || rel_tol > f64::EPSILON) {
Ok(())
} else {
Err(CoppError::InvalidOptions(
function_name.into(),
format!(
"At least one of {abs_tol_name} = {abs_tol} and {rel_tol_name} = {rel_tol} must be strictly positive."
),
))
}
}
#[inline(always)]
pub(crate) fn check_options_not_nan_infinite(
function_name: &str,
var_name: &str,
var_value: f64,
) -> Result<(), CoppError> {
if var_value.is_nan() {
Err(CoppError::InvalidOptions(
function_name.into(),
format!("{var_name} = {var_value} must not be NaN",),
))
} else if var_value.is_infinite() {
Err(CoppError::InvalidOptions(
function_name.into(),
format!("{var_name} = {var_value} must not be infinite",),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_input_not_nan_infinite(
function_name: &str,
var_name: &str,
var_value: f64,
) -> Result<(), CoppError> {
if var_value.is_nan() {
Err(CoppError::InvalidInput(
function_name.into(),
format!("{var_name} = {var_value} must not be NaN"),
))
} else if var_value.is_infinite() {
Err(CoppError::InvalidInput(
function_name.into(),
format!("{var_name} = {var_value} must not be infinite"),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_input_slice_not_nan_infinite(
function_name: &str,
slice_name: &str,
values: &[f64],
) -> Result<(), CoppError> {
if let Some((index, value)) = values.iter().enumerate().find(|(_, value)| value.is_nan()) {
Err(CoppError::InvalidInput(
function_name.into(),
format!("`{slice_name}[{index}]` = {value} must not be NaN"),
))
} else if let Some((index, value)) = values
.iter()
.enumerate()
.find(|(_, value)| value.is_infinite())
{
Err(CoppError::InvalidInput(
function_name.into(),
format!("`{slice_name}[{index}]` = {value} must not be infinite"),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_input_strictly_increasing(
function_name: &str,
slice_name: &str,
values: &[f64],
) -> Result<(), CoppError> {
if let Some(index) = values.windows(2).position(|pair| pair[0] >= pair[1]) {
Err(CoppError::InvalidInput(
function_name.into(),
format!(
"`{slice_name}` must be strictly increasing; first violation at local index {index}."
),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_input_non_negative(
function_name: &str,
var_name: &str,
var_value: f64,
) -> Result<(), CoppError> {
check_input_not_nan_infinite(function_name, var_name, var_value)?;
if var_value < 0.0 {
Err(CoppError::InvalidInput(
function_name.into(),
format!("{var_name} = {var_value} must be nonnegative"),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_input_slice_non_negative(
function_name: &str,
slice_name: &str,
values: &[f64],
) -> Result<(), CoppError> {
check_input_slice_not_nan_infinite(function_name, slice_name, values)?;
if let Some((index, value)) = values.iter().enumerate().find(|(_, value)| **value < 0.0) {
Err(CoppError::InvalidInput(
function_name.into(),
format!("`{slice_name}[{index}]` = {value} must be nonnegative"),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_input_len_at_least(
function_name: &str,
len_name: &str,
len: usize,
min_len: usize,
) -> Result<(), CoppError> {
if len < min_len {
Err(CoppError::InvalidInput(
function_name.into(),
format!("{len_name} = {len} must be at least {min_len}"),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_input_len_equal(
function_name: &str,
lhs_name: &str,
lhs_len: usize,
rhs_name: &str,
rhs_len: usize,
) -> Result<(), CoppError> {
if lhs_len != rhs_len {
Err(CoppError::InvalidInput(
function_name.into(),
format!("{lhs_name} = {lhs_len} must equal {rhs_name} = {rhs_len}"),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_input_not_empty(
function_name: &str,
slice_name: &str,
len: usize,
) -> Result<(), CoppError> {
if len == 0 {
Err(CoppError::InvalidInput(
function_name.into(),
format!("{slice_name} must not be empty"),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_non_negative(
function_name: &str,
var_name: &str,
var_value: f64,
) -> Result<(), CoppError> {
check_options_not_nan_infinite(function_name, var_name, var_value)?;
if var_value < 0.0 {
Err(CoppError::InvalidOptions(
function_name.into(),
format!("{var_name} = {var_value} must be strictly non-negative"),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_strictly_positive(
function_name: &str,
var_name: &str,
var_value: f64,
) -> Result<(), CoppError> {
check_options_not_nan_infinite(function_name, var_name, var_value)?;
if var_value < f64::EPSILON {
Err(CoppError::InvalidOptions(
function_name.into(),
format!("{var_name} = {var_value} must be strictly positive"),
))
} else {
Ok(())
}
}
#[inline(always)]
pub(crate) fn check_boundary_state_copp3_valid(
a_boundary: (f64, f64),
b_boundary: (f64, f64),
) -> Result<(), CoppError> {
if a_boundary.0 < 0.0 {
return Err(CoppError::InvalidInput(
"copp3_socp".into(),
format!("The initial a = {} must be non-negative.", a_boundary.0),
));
}
if a_boundary.1 < 0.0 {
return Err(CoppError::InvalidInput(
"copp3_socp".into(),
format!("The terminal a = {} must be non-negative.", a_boundary.1),
));
}
if a_boundary.0.abs() < f64::EPSILON {
if b_boundary.0.abs() >= f64::EPSILON {
return Err(CoppError::InvalidInput(
"copp3_socp".into(),
format!(
"The initial a = {} is zero, so the initial b = {} must also be zero.",
a_boundary.0, b_boundary.0
),
));
}
}
if a_boundary.1.abs() < f64::EPSILON {
if b_boundary.1.abs() >= f64::EPSILON {
return Err(CoppError::InvalidInput(
"copp3_socp".into(),
format!(
"The terminal a = {} is zero, so the terminal b = {} must also be zero.",
a_boundary.1, b_boundary.1
),
));
}
}
Ok(())
}