1use crate::{
41 core::CoreError, factors::FactorError, linalg::LinAlgError, linearizer::LinearizerError,
42 observers::ObserverError, optimizer::OptimizerError,
43};
44use apex_camera_models::CameraModelError;
45use apex_io::IoError;
46use apex_manifolds::ManifoldError;
47use std::error::Error as StdError;
48use thiserror::Error;
49
50pub type ApexSolverResult<T> = Result<T, ApexSolverError>;
52
53#[derive(Debug, Error)]
74pub enum ApexSolverError {
75 #[error(transparent)]
77 Core(#[from] CoreError),
78
79 #[error(transparent)]
81 Optimizer(#[from] OptimizerError),
82
83 #[error(transparent)]
85 LinearAlgebra(#[from] LinAlgError),
86
87 #[error(transparent)]
89 Manifold(#[from] ManifoldError),
90
91 #[error(transparent)]
93 Io(#[from] IoError),
94
95 #[error(transparent)]
97 Observer(#[from] ObserverError),
98
99 #[error(transparent)]
101 Factor(#[from] FactorError),
102
103 #[error(transparent)]
105 Linearizer(#[from] LinearizerError),
106
107 #[error(transparent)]
109 Camera(#[from] CameraModelError),
110}
111
112pub trait ErrorLogging: Sized + std::fmt::Display {
157 fn log(self) -> Self {
173 tracing::error!("{}", self);
174 self
175 }
176
177 fn log_with_source<E: std::fmt::Debug>(self, source_error: E) -> Self {
198 tracing::error!("{} | Source: {:?}", self, source_error);
199 self
200 }
201}
202
203impl<T: std::fmt::Display> ErrorLogging for T {}
206
207impl ApexSolverError {
208 pub fn chain(&self) -> String {
233 let mut chain = vec![self.to_string()];
234 let mut source = self.source();
235
236 while let Some(err) = source {
237 chain.push(format!(" → {}", err));
238 source = err.source();
239 }
240
241 chain.join("\n")
242 }
243
244 pub fn chain_compact(&self) -> String {
261 let mut chain = vec![self.to_string()];
262 let mut source = self.source();
263
264 while let Some(err) = source {
265 chain.push(err.to_string());
266 source = err.source();
267 }
268
269 chain.join(" → ")
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::factors::FactorError;
277 use faer::Mat;
278
279 type TestResult = Result<(), Box<dyn std::error::Error>>;
280
281 fn solve_linear_system() -> Result<Mat<f64>, LinAlgError> {
288 Err(LinAlgError::SingularMatrix(
289 "Simulated singular matrix in solve_linear_system".to_string(),
290 ))
291 }
292
293 fn build_structure() -> Result<(), CoreError> {
296 Err(CoreError::SymbolicStructure(
297 "Simulated duplicate variable index".to_string(),
298 ))
299 }
300
301 fn compute_projection() -> Result<(), FactorError> {
304 Err(FactorError::InvalidDimension {
305 expected: 3,
306 actual: 2,
307 })
308 }
309
310 fn run_optimization_step() -> Result<Mat<f64>, OptimizerError> {
318 let result = solve_linear_system()?;
319 Ok(result)
320 }
321
322 fn initialize_optimization() -> Result<(), OptimizerError> {
326 build_structure()?;
327 Ok(())
328 }
329
330 fn solver_optimize() -> ApexSolverResult<()> {
338 let _ = run_optimization_step()?;
339 Ok(())
340 }
341
342 fn solver_optimize_with_core_error() -> ApexSolverResult<()> {
344 initialize_optimization()?;
345 Ok(())
346 }
347
348 fn solver_optimize_with_factor_error() -> ApexSolverResult<()> {
350 compute_projection()?;
351 Ok(())
352 }
353
354 #[test]
359 fn test_apex_solver_error_display() {
360 let linalg_error = LinAlgError::SingularMatrix("test singular matrix".to_string());
361 let error = ApexSolverError::from(linalg_error);
362 assert!(error.to_string().contains("Singular matrix"));
363 }
364
365 #[test]
366 fn test_apex_solver_error_chain() {
367 let linalg_error =
368 LinAlgError::FactorizationFailed("Cholesky factorization failed".to_string());
369 let error = ApexSolverError::from(linalg_error);
370
371 let chain = error.chain();
372 assert!(chain.contains("factorization"));
373 assert!(chain.contains("Cholesky"));
374 }
375
376 #[test]
377 fn test_apex_solver_error_chain_compact() {
378 let core_error = CoreError::Variable("Invalid variable index".to_string());
379 let error = ApexSolverError::from(core_error);
380
381 let chain_compact = error.chain_compact();
382 assert!(chain_compact.contains("Invalid variable index"));
383 }
384
385 #[test]
386 fn test_apex_solver_result_ok() {
387 let result: ApexSolverResult<i32> = Ok(42);
388 assert!(result.is_ok());
389 if let Ok(value) = result {
390 assert_eq!(value, 42);
391 }
392 }
393
394 #[test]
395 fn test_apex_solver_result_err() {
396 let core_error = CoreError::ResidualBlock("Test error".to_string());
397 let result: ApexSolverResult<i32> = Err(ApexSolverError::from(core_error));
398 assert!(result.is_err());
399 }
400
401 #[test]
402 fn test_transparent_error_conversion() {
403 let manifold_error = ManifoldError::DimensionMismatch {
404 expected: 3,
405 actual: 2,
406 };
407
408 let apex_error: ApexSolverError = manifold_error.into();
409 assert!(
410 matches!(apex_error, ApexSolverError::Manifold(_)),
411 "Expected Manifold variant"
412 );
413 }
414
415 #[test]
420 fn test_error_chain_linalg_through_optimizer() -> TestResult {
421 let result = solver_optimize();
422 let Err(err) = result else {
423 return Err("solver_optimize should fail with LinAlgError".into());
424 };
425
426 assert!(
427 matches!(err, ApexSolverError::Optimizer(OptimizerError::LinAlg(_))),
428 "Expected Optimizer::LinAlg, got {:?}",
429 err
430 );
431
432 let chain = err.chain();
433 assert!(
434 chain.contains("Linear algebra error") || chain.contains("Singular matrix"),
435 "chain should contain error details: {}",
436 chain
437 );
438
439 let compact = err.chain_compact();
440 assert!(
441 compact.contains("→"),
442 "compact chain should contain →: {}",
443 compact
444 );
445 Ok(())
446 }
447
448 #[test]
449 fn test_error_chain_core_through_optimizer() -> TestResult {
450 let result = solver_optimize_with_core_error();
451 let Err(err) = result else {
452 return Err("should fail with CoreError".into());
453 };
454
455 assert!(
456 matches!(err, ApexSolverError::Optimizer(OptimizerError::Core(_))),
457 "Expected Optimizer::Core, got {:?}",
458 err
459 );
460
461 let chain = err.chain();
462 assert!(
463 chain.contains("Symbolic structure") || chain.contains("duplicate"),
464 "chain should contain error details: {}",
465 chain
466 );
467 Ok(())
468 }
469
470 #[test]
471 fn test_error_chain_factor_direct() -> TestResult {
472 let result = solver_optimize_with_factor_error();
473 let Err(err) = result else {
474 return Err("should fail with FactorError".into());
475 };
476
477 assert!(
478 matches!(
479 err,
480 ApexSolverError::Factor(FactorError::InvalidDimension { .. })
481 ),
482 "Expected Factor::InvalidDimension, got {:?}",
483 err
484 );
485
486 let compact = err.chain_compact();
487 assert!(compact.contains("expected 3"), "compact: {}", compact);
488 assert!(compact.contains("got 2"), "compact: {}", compact);
489 Ok(())
490 }
491
492 #[test]
493 fn test_linalg_error_direct_to_apex() -> TestResult {
494 let linalg_err = LinAlgError::SingularMatrix("test_direct".to_string());
495 let apex_err: ApexSolverError = linalg_err.into();
496 assert!(
497 matches!(apex_err, ApexSolverError::LinearAlgebra(_)),
498 "Expected LinearAlgebra variant for direct LinAlgError conversion"
499 );
500 Ok(())
501 }
502
503 #[test]
504 fn test_core_error_direct_to_apex() -> TestResult {
505 let core_err = CoreError::SymbolicStructure("test_direct".to_string());
506 let apex_err: ApexSolverError = core_err.into();
507 assert!(
508 matches!(apex_err, ApexSolverError::Core(_)),
509 "Expected Core variant for direct CoreError conversion"
510 );
511 Ok(())
512 }
513
514 #[test]
515 fn test_core_error_through_optimizer_to_apex() -> TestResult {
516 let core_err = CoreError::InvalidInput("bad input".to_string());
517 let opt_err: OptimizerError = core_err.into();
518 let apex_err: ApexSolverError = opt_err.into();
519 assert!(
520 matches!(
521 apex_err,
522 ApexSolverError::Optimizer(OptimizerError::Core(_))
523 ),
524 "Expected Optimizer::Core variant for CoreError through OptimizerError"
525 );
526 Ok(())
527 }
528
529 #[test]
530 fn test_linalg_error_through_optimizer_preserves_context() -> TestResult {
531 let linalg_err = LinAlgError::FactorizationFailed("LU decomposition failed".to_string());
532 let opt_err: OptimizerError = linalg_err.into();
533 let apex_err: ApexSolverError = opt_err.into();
534
535 let chain = apex_err.chain();
536 assert!(chain.contains("Linear algebra error"), "chain: {}", chain);
537 assert!(chain.contains("LU decomposition"), "chain: {}", chain);
538 Ok(())
539 }
540
541 #[test]
542 fn test_observer_error_to_apex() -> TestResult {
543 let obs_err = ObserverError::RerunInitialization("connect failed".to_string());
544 let apex_err: ApexSolverError = obs_err.into();
545 assert!(
546 matches!(apex_err, ApexSolverError::Observer(_)),
547 "Expected Observer variant"
548 );
549
550 let compact = apex_err.chain_compact();
551 assert!(
552 compact.contains("Rerun") || compact.contains("connect failed"),
553 "compact: {}",
554 compact
555 );
556 Ok(())
557 }
558
559 #[test]
560 fn test_factor_error_to_apex() -> TestResult {
561 let factor_err = FactorError::InvalidProjection("point behind camera".to_string());
562 let apex_err: ApexSolverError = factor_err.into();
563 assert!(
564 matches!(apex_err, ApexSolverError::Factor(_)),
565 "Expected Factor variant"
566 );
567
568 let compact = apex_err.chain_compact();
569 assert!(compact.contains("behind camera"), "compact: {}", compact);
570 Ok(())
571 }
572
573 #[test]
574 fn test_all_error_variants_are_accessible() -> TestResult {
575 let errors: Vec<ApexSolverError> = vec![
576 CoreError::Variable("var".into()).into(),
577 OptimizerError::EmptyProblem.into(),
578 LinAlgError::SingularMatrix("sing".into()).into(),
579 ManifoldError::DimensionMismatch {
580 expected: 1,
581 actual: 2,
582 }
583 .into(),
584 ObserverError::InvalidState("bad".into()).into(),
585 FactorError::InvalidDimension {
586 expected: 3,
587 actual: 2,
588 }
589 .into(),
590 LinearizerError::SymbolicStructure("sym_err".into()).into(),
591 CameraModelError::PointBehindCamera {
592 z: -0.5,
593 min_z: 1e-6,
594 }
595 .into(),
596 ];
597
598 for err in &errors {
599 assert!(
600 !err.to_string().is_empty(),
601 "Error Display should not be empty"
602 );
603 assert!(
604 !err.chain_compact().is_empty(),
605 "chain_compact should not be empty"
606 );
607 }
608 Ok(())
609 }
610
611 #[test]
612 fn test_linearizer_error_direct_to_apex() -> TestResult {
613 let lin_err = LinearizerError::SymbolicStructure("sparse build failed".to_string());
614 let apex_err: ApexSolverError = lin_err.into();
615 assert!(
616 matches!(apex_err, ApexSolverError::Linearizer(_)),
617 "Expected Linearizer variant for direct LinearizerError conversion"
618 );
619
620 let compact = apex_err.chain_compact();
621 assert!(
622 compact.contains("sparse build failed"),
623 "compact: {}",
624 compact
625 );
626 Ok(())
627 }
628
629 #[test]
630 fn test_linearizer_error_through_core_to_apex() -> TestResult {
631 let lin_err = LinearizerError::ParallelComputation("lock failure".to_string());
632 let core_err: CoreError = lin_err.into();
633 let apex_err: ApexSolverError = core_err.into();
634 assert!(
635 matches!(
636 apex_err,
637 ApexSolverError::Core(CoreError::ParallelComputation(_))
638 ),
639 "Expected Core::ParallelComputation variant for LinearizerError through CoreError, got {:?}",
640 apex_err
641 );
642 Ok(())
643 }
644
645 #[test]
646 fn test_linearizer_error_through_optimizer_to_apex() -> TestResult {
647 let lin_err = LinearizerError::Variable("missing key".to_string());
648 let opt_err: OptimizerError = lin_err.into();
649 let apex_err: ApexSolverError = opt_err.into();
650 assert!(
651 matches!(
652 apex_err,
653 ApexSolverError::Optimizer(OptimizerError::Linearizer(_))
654 ),
655 "Expected Optimizer::Linearizer variant for LinearizerError through OptimizerError, got {:?}",
656 apex_err
657 );
658 Ok(())
659 }
660
661 #[test]
662 fn test_bubble_up_from_linalg_to_optimizer_to_api() -> TestResult {
663 let result = solver_optimize();
664 let Err(err) = result else {
665 return Err("should propagate LinAlgError through OptimizerError".into());
666 };
667
668 assert!(
669 matches!(err, ApexSolverError::Optimizer(OptimizerError::LinAlg(_))),
670 "Expected LinAlgError wrapped in OptimizerError, got {:?}",
671 err
672 );
673
674 let source_chain = err.chain();
675 assert!(
676 source_chain.contains("Singular matrix"),
677 "Chain should contain root cause: {}",
678 source_chain
679 );
680 Ok(())
681 }
682
683 #[test]
684 fn test_bubble_up_from_core_to_optimizer_to_api() -> TestResult {
685 let result = solver_optimize_with_core_error();
686 let Err(err) = result else {
687 return Err("should propagate CoreError through OptimizerError".into());
688 };
689
690 assert!(
691 matches!(err, ApexSolverError::Optimizer(OptimizerError::Core(_))),
692 "Expected CoreError wrapped in OptimizerError, got {:?}",
693 err
694 );
695
696 let source_chain = err.chain();
697 assert!(
698 source_chain.contains("Symbolic structure"),
699 "Chain should contain root cause: {}",
700 source_chain
701 );
702 Ok(())
703 }
704
705 #[test]
710 fn test_camera_error_point_behind_camera_direct() -> TestResult {
711 let cam_err = CameraModelError::PointBehindCamera {
712 z: -0.5,
713 min_z: 1e-6,
714 };
715 let apex_err: ApexSolverError = cam_err.into();
716 assert!(
717 matches!(apex_err, ApexSolverError::Camera(_)),
718 "Expected Camera variant, got {:?}",
719 apex_err
720 );
721 let compact = apex_err.chain_compact();
722 assert!(compact.contains("behind camera"), "compact: {}", compact);
723 assert!(
724 compact.contains("z=-0.5"),
725 "compact should preserve structured field z: {}",
726 compact
727 );
728 Ok(())
729 }
730
731 #[test]
732 fn test_camera_error_focal_length_preserves_fields() -> TestResult {
733 let cam_err = CameraModelError::FocalLengthNotPositive {
734 fx: -1.0,
735 fy: 500.0,
736 };
737 let apex_err: ApexSolverError = cam_err.into();
738 assert!(
739 matches!(apex_err, ApexSolverError::Camera(_)),
740 "Expected Camera variant, got {:?}",
741 apex_err
742 );
743 let msg = apex_err.to_string();
744 assert!(msg.contains("fx=-1"), "msg should contain fx: {}", msg);
745 assert!(msg.contains("fy=500"), "msg should contain fy: {}", msg);
746 Ok(())
747 }
748
749 #[test]
750 fn test_camera_error_numerical_preserves_fields() -> TestResult {
751 let cam_err = CameraModelError::DenominatorTooSmall {
752 denom: 1e-15,
753 threshold: 1e-6,
754 };
755 let apex_err: ApexSolverError = cam_err.into();
756 assert!(
757 matches!(apex_err, ApexSolverError::Camera(_)),
758 "Expected Camera variant, got {:?}",
759 apex_err
760 );
761 let msg = apex_err.to_string();
762 assert!(msg.contains("denom"), "msg: {}", msg);
763 assert!(
764 msg.contains("threshold"),
765 "msg should contain threshold: {}",
766 msg
767 );
768 Ok(())
769 }
770
771 #[test]
772 fn test_camera_error_parameter_out_of_range() -> TestResult {
773 let cam_err = CameraModelError::ParameterOutOfRange {
774 param: "alpha".to_string(),
775 value: 1.5,
776 min: 0.0,
777 max: 1.0,
778 };
779 let apex_err: ApexSolverError = cam_err.into();
780 assert!(
781 matches!(apex_err, ApexSolverError::Camera(_)),
782 "Expected Camera variant, got {:?}",
783 apex_err
784 );
785 let msg = apex_err.to_string();
786 assert!(msg.contains("alpha"), "msg: {}", msg);
787 assert!(msg.contains("1.5"), "msg should preserve value: {}", msg);
788 Ok(())
789 }
790
791 #[test]
792 fn test_camera_error_all_variants_accessible() -> TestResult {
793 let errors: Vec<ApexSolverError> = vec![
794 CameraModelError::PointBehindCamera {
795 z: -0.5,
796 min_z: 1e-6,
797 }
798 .into(),
799 CameraModelError::PointAtCameraCenter.into(),
800 CameraModelError::ProjectionOutOfBounds.into(),
801 CameraModelError::PointOutsideImage { x: 100.0, y: 200.0 }.into(),
802 CameraModelError::DenominatorTooSmall {
803 denom: 1e-15,
804 threshold: 1e-6,
805 }
806 .into(),
807 CameraModelError::NumericalError {
808 operation: "unproject".to_string(),
809 details: "convergence failed".to_string(),
810 }
811 .into(),
812 CameraModelError::FocalLengthNotPositive {
813 fx: -1.0,
814 fy: 500.0,
815 }
816 .into(),
817 CameraModelError::FocalLengthNotFinite {
818 fx: f64::INFINITY,
819 fy: 500.0,
820 }
821 .into(),
822 CameraModelError::PrincipalPointNotFinite {
823 cx: f64::NAN,
824 cy: 240.0,
825 }
826 .into(),
827 CameraModelError::DistortionNotFinite {
828 name: "k1".to_string(),
829 value: f64::NAN,
830 }
831 .into(),
832 CameraModelError::ParameterOutOfRange {
833 param: "alpha".to_string(),
834 value: 1.5,
835 min: 0.0,
836 max: 1.0,
837 }
838 .into(),
839 CameraModelError::InvalidParams("bad".to_string()).into(),
840 ];
841
842 for err in &errors {
843 assert!(matches!(err, ApexSolverError::Camera(_)));
844 assert!(
845 !err.to_string().is_empty(),
846 "Error Display should not be empty"
847 );
848 assert!(
849 !err.chain_compact().is_empty(),
850 "chain_compact should not be empty"
851 );
852 }
853 Ok(())
854 }
855}