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("{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)]
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("invalid shape for parameter s: ({rows}, {cols}), expected 1xN or Nx1")]
InvalidSShape { rows: usize, cols: 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_not_nan_infinite(function_name, abs_tol_name, abs_tol)?;
check_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_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_non_negative(
function_name: &str,
var_name: &str,
var_value: f64,
) -> Result<(), CoppError> {
check_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_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(())
}