use crate::{
core::CoreError, factors::FactorError, linalg::LinAlgError, linearizer::LinearizerError,
observers::ObserverError, optimizer::OptimizerError,
};
use apex_camera_models::CameraModelError;
use apex_io::IoError;
use apex_manifolds::ManifoldError;
use std::error::Error as StdError;
use thiserror::Error;
pub type ApexSolverResult<T> = Result<T, ApexSolverError>;
#[derive(Debug, Error)]
pub enum ApexSolverError {
#[error(transparent)]
Core(#[from] CoreError),
#[error(transparent)]
Optimizer(#[from] OptimizerError),
#[error(transparent)]
LinearAlgebra(#[from] LinAlgError),
#[error(transparent)]
Manifold(#[from] ManifoldError),
#[error(transparent)]
Io(#[from] IoError),
#[error(transparent)]
Observer(#[from] ObserverError),
#[error(transparent)]
Factor(#[from] FactorError),
#[error(transparent)]
Linearizer(#[from] LinearizerError),
#[error(transparent)]
Camera(#[from] CameraModelError),
}
pub trait ErrorLogging: Sized + std::fmt::Display {
fn log(self) -> Self {
tracing::error!("{}", self);
self
}
fn log_with_source<E: std::fmt::Debug>(self, source_error: E) -> Self {
tracing::error!("{} | Source: {:?}", self, source_error);
self
}
}
impl<T: std::fmt::Display> ErrorLogging for T {}
impl ApexSolverError {
pub fn chain(&self) -> String {
let mut chain = vec![self.to_string()];
let mut source = self.source();
while let Some(err) = source {
chain.push(format!(" → {}", err));
source = err.source();
}
chain.join("\n")
}
pub fn chain_compact(&self) -> String {
let mut chain = vec![self.to_string()];
let mut source = self.source();
while let Some(err) = source {
chain.push(err.to_string());
source = err.source();
}
chain.join(" → ")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::factors::FactorError;
use faer::Mat;
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn solve_linear_system() -> Result<Mat<f64>, LinAlgError> {
Err(LinAlgError::SingularMatrix(
"Simulated singular matrix in solve_linear_system".to_string(),
))
}
fn build_structure() -> Result<(), CoreError> {
Err(CoreError::SymbolicStructure(
"Simulated duplicate variable index".to_string(),
))
}
fn compute_projection() -> Result<(), FactorError> {
Err(FactorError::InvalidDimension {
expected: 3,
actual: 2,
})
}
fn run_optimization_step() -> Result<Mat<f64>, OptimizerError> {
let result = solve_linear_system()?;
Ok(result)
}
fn initialize_optimization() -> Result<(), OptimizerError> {
build_structure()?;
Ok(())
}
fn solver_optimize() -> ApexSolverResult<()> {
let _ = run_optimization_step()?;
Ok(())
}
fn solver_optimize_with_core_error() -> ApexSolverResult<()> {
initialize_optimization()?;
Ok(())
}
fn solver_optimize_with_factor_error() -> ApexSolverResult<()> {
compute_projection()?;
Ok(())
}
#[test]
fn test_apex_solver_error_display() {
let linalg_error = LinAlgError::SingularMatrix("test singular matrix".to_string());
let error = ApexSolverError::from(linalg_error);
assert!(error.to_string().contains("Singular matrix"));
}
#[test]
fn test_apex_solver_error_chain() {
let linalg_error =
LinAlgError::FactorizationFailed("Cholesky factorization failed".to_string());
let error = ApexSolverError::from(linalg_error);
let chain = error.chain();
assert!(chain.contains("factorization"));
assert!(chain.contains("Cholesky"));
}
#[test]
fn test_apex_solver_error_chain_compact() {
let core_error = CoreError::Variable("Invalid variable index".to_string());
let error = ApexSolverError::from(core_error);
let chain_compact = error.chain_compact();
assert!(chain_compact.contains("Invalid variable index"));
}
#[test]
fn test_apex_solver_result_ok() {
let result: ApexSolverResult<i32> = Ok(42);
assert!(result.is_ok());
if let Ok(value) = result {
assert_eq!(value, 42);
}
}
#[test]
fn test_apex_solver_result_err() {
let core_error = CoreError::ResidualBlock("Test error".to_string());
let result: ApexSolverResult<i32> = Err(ApexSolverError::from(core_error));
assert!(result.is_err());
}
#[test]
fn test_transparent_error_conversion() {
let manifold_error = ManifoldError::DimensionMismatch {
expected: 3,
actual: 2,
};
let apex_error: ApexSolverError = manifold_error.into();
assert!(
matches!(apex_error, ApexSolverError::Manifold(_)),
"Expected Manifold variant"
);
}
#[test]
fn test_error_chain_linalg_through_optimizer() -> TestResult {
let result = solver_optimize();
let Err(err) = result else {
return Err("solver_optimize should fail with LinAlgError".into());
};
assert!(
matches!(err, ApexSolverError::Optimizer(OptimizerError::LinAlg(_))),
"Expected Optimizer::LinAlg, got {:?}",
err
);
let chain = err.chain();
assert!(
chain.contains("Linear algebra error") || chain.contains("Singular matrix"),
"chain should contain error details: {}",
chain
);
let compact = err.chain_compact();
assert!(
compact.contains("→"),
"compact chain should contain →: {}",
compact
);
Ok(())
}
#[test]
fn test_error_chain_core_through_optimizer() -> TestResult {
let result = solver_optimize_with_core_error();
let Err(err) = result else {
return Err("should fail with CoreError".into());
};
assert!(
matches!(err, ApexSolverError::Optimizer(OptimizerError::Core(_))),
"Expected Optimizer::Core, got {:?}",
err
);
let chain = err.chain();
assert!(
chain.contains("Symbolic structure") || chain.contains("duplicate"),
"chain should contain error details: {}",
chain
);
Ok(())
}
#[test]
fn test_error_chain_factor_direct() -> TestResult {
let result = solver_optimize_with_factor_error();
let Err(err) = result else {
return Err("should fail with FactorError".into());
};
assert!(
matches!(
err,
ApexSolverError::Factor(FactorError::InvalidDimension { .. })
),
"Expected Factor::InvalidDimension, got {:?}",
err
);
let compact = err.chain_compact();
assert!(compact.contains("expected 3"), "compact: {}", compact);
assert!(compact.contains("got 2"), "compact: {}", compact);
Ok(())
}
#[test]
fn test_linalg_error_direct_to_apex() -> TestResult {
let linalg_err = LinAlgError::SingularMatrix("test_direct".to_string());
let apex_err: ApexSolverError = linalg_err.into();
assert!(
matches!(apex_err, ApexSolverError::LinearAlgebra(_)),
"Expected LinearAlgebra variant for direct LinAlgError conversion"
);
Ok(())
}
#[test]
fn test_core_error_direct_to_apex() -> TestResult {
let core_err = CoreError::SymbolicStructure("test_direct".to_string());
let apex_err: ApexSolverError = core_err.into();
assert!(
matches!(apex_err, ApexSolverError::Core(_)),
"Expected Core variant for direct CoreError conversion"
);
Ok(())
}
#[test]
fn test_core_error_through_optimizer_to_apex() -> TestResult {
let core_err = CoreError::InvalidInput("bad input".to_string());
let opt_err: OptimizerError = core_err.into();
let apex_err: ApexSolverError = opt_err.into();
assert!(
matches!(
apex_err,
ApexSolverError::Optimizer(OptimizerError::Core(_))
),
"Expected Optimizer::Core variant for CoreError through OptimizerError"
);
Ok(())
}
#[test]
fn test_linalg_error_through_optimizer_preserves_context() -> TestResult {
let linalg_err = LinAlgError::FactorizationFailed("LU decomposition failed".to_string());
let opt_err: OptimizerError = linalg_err.into();
let apex_err: ApexSolverError = opt_err.into();
let chain = apex_err.chain();
assert!(chain.contains("Linear algebra error"), "chain: {}", chain);
assert!(chain.contains("LU decomposition"), "chain: {}", chain);
Ok(())
}
#[test]
fn test_observer_error_to_apex() -> TestResult {
let obs_err = ObserverError::RerunInitialization("connect failed".to_string());
let apex_err: ApexSolverError = obs_err.into();
assert!(
matches!(apex_err, ApexSolverError::Observer(_)),
"Expected Observer variant"
);
let compact = apex_err.chain_compact();
assert!(
compact.contains("Rerun") || compact.contains("connect failed"),
"compact: {}",
compact
);
Ok(())
}
#[test]
fn test_factor_error_to_apex() -> TestResult {
let factor_err = FactorError::InvalidProjection("point behind camera".to_string());
let apex_err: ApexSolverError = factor_err.into();
assert!(
matches!(apex_err, ApexSolverError::Factor(_)),
"Expected Factor variant"
);
let compact = apex_err.chain_compact();
assert!(compact.contains("behind camera"), "compact: {}", compact);
Ok(())
}
#[test]
fn test_all_error_variants_are_accessible() -> TestResult {
let errors: Vec<ApexSolverError> = vec![
CoreError::Variable("var".into()).into(),
OptimizerError::EmptyProblem.into(),
LinAlgError::SingularMatrix("sing".into()).into(),
ManifoldError::DimensionMismatch {
expected: 1,
actual: 2,
}
.into(),
ObserverError::InvalidState("bad".into()).into(),
FactorError::InvalidDimension {
expected: 3,
actual: 2,
}
.into(),
LinearizerError::SymbolicStructure("sym_err".into()).into(),
CameraModelError::PointBehindCamera {
z: -0.5,
min_z: 1e-6,
}
.into(),
];
for err in &errors {
assert!(
!err.to_string().is_empty(),
"Error Display should not be empty"
);
assert!(
!err.chain_compact().is_empty(),
"chain_compact should not be empty"
);
}
Ok(())
}
#[test]
fn test_linearizer_error_direct_to_apex() -> TestResult {
let lin_err = LinearizerError::SymbolicStructure("sparse build failed".to_string());
let apex_err: ApexSolverError = lin_err.into();
assert!(
matches!(apex_err, ApexSolverError::Linearizer(_)),
"Expected Linearizer variant for direct LinearizerError conversion"
);
let compact = apex_err.chain_compact();
assert!(
compact.contains("sparse build failed"),
"compact: {}",
compact
);
Ok(())
}
#[test]
fn test_linearizer_error_through_core_to_apex() -> TestResult {
let lin_err = LinearizerError::ParallelComputation("lock failure".to_string());
let core_err: CoreError = lin_err.into();
let apex_err: ApexSolverError = core_err.into();
assert!(
matches!(
apex_err,
ApexSolverError::Core(CoreError::ParallelComputation(_))
),
"Expected Core::ParallelComputation variant for LinearizerError through CoreError, got {:?}",
apex_err
);
Ok(())
}
#[test]
fn test_linearizer_error_through_optimizer_to_apex() -> TestResult {
let lin_err = LinearizerError::Variable("missing key".to_string());
let opt_err: OptimizerError = lin_err.into();
let apex_err: ApexSolverError = opt_err.into();
assert!(
matches!(
apex_err,
ApexSolverError::Optimizer(OptimizerError::Linearizer(_))
),
"Expected Optimizer::Linearizer variant for LinearizerError through OptimizerError, got {:?}",
apex_err
);
Ok(())
}
#[test]
fn test_bubble_up_from_linalg_to_optimizer_to_api() -> TestResult {
let result = solver_optimize();
let Err(err) = result else {
return Err("should propagate LinAlgError through OptimizerError".into());
};
assert!(
matches!(err, ApexSolverError::Optimizer(OptimizerError::LinAlg(_))),
"Expected LinAlgError wrapped in OptimizerError, got {:?}",
err
);
let source_chain = err.chain();
assert!(
source_chain.contains("Singular matrix"),
"Chain should contain root cause: {}",
source_chain
);
Ok(())
}
#[test]
fn test_bubble_up_from_core_to_optimizer_to_api() -> TestResult {
let result = solver_optimize_with_core_error();
let Err(err) = result else {
return Err("should propagate CoreError through OptimizerError".into());
};
assert!(
matches!(err, ApexSolverError::Optimizer(OptimizerError::Core(_))),
"Expected CoreError wrapped in OptimizerError, got {:?}",
err
);
let source_chain = err.chain();
assert!(
source_chain.contains("Symbolic structure"),
"Chain should contain root cause: {}",
source_chain
);
Ok(())
}
#[test]
fn test_camera_error_point_behind_camera_direct() -> TestResult {
let cam_err = CameraModelError::PointBehindCamera {
z: -0.5,
min_z: 1e-6,
};
let apex_err: ApexSolverError = cam_err.into();
assert!(
matches!(apex_err, ApexSolverError::Camera(_)),
"Expected Camera variant, got {:?}",
apex_err
);
let compact = apex_err.chain_compact();
assert!(compact.contains("behind camera"), "compact: {}", compact);
assert!(
compact.contains("z=-0.5"),
"compact should preserve structured field z: {}",
compact
);
Ok(())
}
#[test]
fn test_camera_error_focal_length_preserves_fields() -> TestResult {
let cam_err = CameraModelError::FocalLengthNotPositive {
fx: -1.0,
fy: 500.0,
};
let apex_err: ApexSolverError = cam_err.into();
assert!(
matches!(apex_err, ApexSolverError::Camera(_)),
"Expected Camera variant, got {:?}",
apex_err
);
let msg = apex_err.to_string();
assert!(msg.contains("fx=-1"), "msg should contain fx: {}", msg);
assert!(msg.contains("fy=500"), "msg should contain fy: {}", msg);
Ok(())
}
#[test]
fn test_camera_error_numerical_preserves_fields() -> TestResult {
let cam_err = CameraModelError::DenominatorTooSmall {
denom: 1e-15,
threshold: 1e-6,
};
let apex_err: ApexSolverError = cam_err.into();
assert!(
matches!(apex_err, ApexSolverError::Camera(_)),
"Expected Camera variant, got {:?}",
apex_err
);
let msg = apex_err.to_string();
assert!(msg.contains("denom"), "msg: {}", msg);
assert!(
msg.contains("threshold"),
"msg should contain threshold: {}",
msg
);
Ok(())
}
#[test]
fn test_camera_error_parameter_out_of_range() -> TestResult {
let cam_err = CameraModelError::ParameterOutOfRange {
param: "alpha".to_string(),
value: 1.5,
min: 0.0,
max: 1.0,
};
let apex_err: ApexSolverError = cam_err.into();
assert!(
matches!(apex_err, ApexSolverError::Camera(_)),
"Expected Camera variant, got {:?}",
apex_err
);
let msg = apex_err.to_string();
assert!(msg.contains("alpha"), "msg: {}", msg);
assert!(msg.contains("1.5"), "msg should preserve value: {}", msg);
Ok(())
}
#[test]
fn test_camera_error_all_variants_accessible() -> TestResult {
let errors: Vec<ApexSolverError> = vec![
CameraModelError::PointBehindCamera {
z: -0.5,
min_z: 1e-6,
}
.into(),
CameraModelError::PointAtCameraCenter.into(),
CameraModelError::ProjectionOutOfBounds.into(),
CameraModelError::PointOutsideImage { x: 100.0, y: 200.0 }.into(),
CameraModelError::DenominatorTooSmall {
denom: 1e-15,
threshold: 1e-6,
}
.into(),
CameraModelError::NumericalError {
operation: "unproject".to_string(),
details: "convergence failed".to_string(),
}
.into(),
CameraModelError::FocalLengthNotPositive {
fx: -1.0,
fy: 500.0,
}
.into(),
CameraModelError::FocalLengthNotFinite {
fx: f64::INFINITY,
fy: 500.0,
}
.into(),
CameraModelError::PrincipalPointNotFinite {
cx: f64::NAN,
cy: 240.0,
}
.into(),
CameraModelError::DistortionNotFinite {
name: "k1".to_string(),
value: f64::NAN,
}
.into(),
CameraModelError::ParameterOutOfRange {
param: "alpha".to_string(),
value: 1.5,
min: 0.0,
max: 1.0,
}
.into(),
CameraModelError::InvalidParams("bad".to_string()).into(),
];
for err in &errors {
assert!(matches!(err, ApexSolverError::Camera(_)));
assert!(
!err.to_string().is_empty(),
"Error Display should not be empty"
);
assert!(
!err.chain_compact().is_empty(),
"chain_compact should not be empty"
);
}
Ok(())
}
}