Skip to main content

apex_solver/
error.rs

1//! Error types for the apex-solver library
2//!
3//! This module provides the main error and result types used throughout the library.
4//! All errors use the `thiserror` crate for automatic trait implementations.
5//!
6//! # Error Hierarchy
7//!
8//! The library uses a strict three-layer error hierarchy with bubble-up propagation
9//! using the `?` operator:
10//!
11//! - **Layer A (Top/API)**: `ApexSolverError` — exposed to end-users, wraps all module errors
12//! - **Layer B (Logic)**: `OptimizerError`, `ObserverError` — wrap Layer C errors with context
13//! - **Layer C (Deep/Math)**: `CoreError`, `LinAlgError`, `ManifoldError`, `FactorError` —
14//!   module-specific errors that must **never** return `ApexSolverError` directly
15//!
16//! # Error Propagation Convention
17//!
18//! Deep modules (Layer C) must return their own module-specific error type
19//! (e.g., `Result<T, CoreError>`, not `Result<T, ApexSolverError>`). The `?` operator
20//! and `#[from]` attributes handle automatic conversions at each layer boundary.
21//!
22//! Example error chain (Layer C → B → A):
23//!
24//! ```text
25//! LinAlgError::SingularMatrix
26//!   → OptimizerError::LinAlg (via #[from] at Layer B)
27//!     → ApexSolverError::Optimizer (via #[from] at Layer A)
28//! ```
29//!
30//! # Dual-Path Convention for LinAlgError
31//!
32//! `LinAlgError` can convert to `ApexSolverError` via two paths:
33//! 1. Direct: `LinAlgError → ApexSolverError::LinearAlgebra(...)` — for standalone linalg usage
34//! 2. Through optimizer: `LinAlgError → OptimizerError::LinAlg → ApexSolverError::Optimizer(...)` — during optimization
35//!
36//! When a `LinAlgError` occurs inside the optimizer, it should propagate through the
37//! optimizer layer (path 2) to preserve optimization context. Standalone linalg usage
38//! should use path 1 directly.
39
40use 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
50/// Main result type used throughout the apex-solver library
51pub type ApexSolverResult<T> = Result<T, ApexSolverError>;
52
53/// Main error type for the apex-solver library
54///
55/// This is the top-level error type (Layer A) exposed by public APIs. It wraps
56/// module-specific errors while preserving the full error chain for debugging.
57///
58/// # Error Chain Access
59///
60/// You can access the full error chain using the `chain()` method:
61///
62/// ```no_run
63/// # use apex_solver::error::ApexSolverError;
64/// # use tracing::warn;
65/// # fn solver_optimize() -> Result<(), ApexSolverError> { Ok(()) }
66/// # fn example() {
67/// if let Err(e) = solver_optimize() {
68///     warn!("Error: {}", e);
69///     warn!("Full chain: {}", e.chain());
70/// }
71/// # }
72/// ```
73#[derive(Debug, Error)]
74pub enum ApexSolverError {
75    /// Core module errors (problem construction, factors, variables, loss functions)
76    #[error(transparent)]
77    Core(#[from] CoreError),
78
79    /// Optimization algorithm errors
80    #[error(transparent)]
81    Optimizer(#[from] OptimizerError),
82
83    /// Linear algebra errors
84    #[error(transparent)]
85    LinearAlgebra(#[from] LinAlgError),
86
87    /// Manifold operation errors
88    #[error(transparent)]
89    Manifold(#[from] ManifoldError),
90
91    /// I/O and file parsing errors
92    #[error(transparent)]
93    Io(#[from] IoError),
94
95    /// Observer/visualization errors
96    #[error(transparent)]
97    Observer(#[from] ObserverError),
98
99    /// Factor computation errors (projection, between factors, etc.)
100    #[error(transparent)]
101    Factor(#[from] FactorError),
102
103    /// Linearizer errors (Jacobian assembly, symbolic structure)
104    #[error(transparent)]
105    Linearizer(#[from] LinearizerError),
106
107    /// Camera model errors (projection, parameter validation, etc.)
108    #[error(transparent)]
109    Camera(#[from] CameraModelError),
110}
111
112// Module-specific errors are automatically converted via #[from] attributes above
113// No manual From implementations needed - thiserror handles it!
114
115/// Trait for error logging with chaining support.
116///
117/// Provides `log()` and `log_with_source()` methods for all error types in the
118/// apex-solver library. Implemented as a blanket trait for any type that implements
119/// `Display`, so all error enums (`CoreError`, `LinAlgError`, `OptimizerError`,
120/// `FactorError`, `LinearizerError`, `ObserverError`, `ApexSolverError`) get these
121/// methods automatically without per-type boilerplate.
122///
123/// # Example
124///
125/// ```no_run
126/// use apex_solver::error::ErrorLogging;
127/// use apex_solver::core::CoreError;
128///
129/// fn operation() -> Result<(), CoreError> { Ok(()) }
130///
131/// fn example() -> Result<(), CoreError> {
132///     // Log and propagate — .log() returns self for chaining with ?
133///     operation()
134///         .map_err(|e| e.log())?;
135///     Ok(())
136/// }
137/// ```
138///
139/// # Logging with source context
140///
141/// ```no_run
142/// use apex_solver::error::ErrorLogging;
143/// use apex_solver::linalg::LinAlgError;
144///
145/// fn matrix_op() -> Result<(), std::io::Error> { Ok(()) }
146///
147/// fn example() -> Result<(), LinAlgError> {
148///     matrix_op()
149///         .map_err(|e| {
150///             LinAlgError::SingularMatrix("matrix is singular".to_string())
151///                 .log_with_source(e)
152///         })?;
153///     Ok(())
154/// }
155/// ```
156pub trait ErrorLogging: Sized + std::fmt::Display {
157    /// Log the error with `tracing::error` and return self for chaining.
158    ///
159    /// This is equivalent to `tracing::error!("{}", self); self` but allows
160    /// method chaining with `?` via `.map_err(|e| e.log())`.
161    ///
162    /// # Example
163    ///
164    /// ```
165    /// use apex_solver::core::CoreError;
166    /// use apex_solver::error::ErrorLogging;
167    ///
168    /// let e = CoreError::Variable("missing key".to_string());
169    /// let returned = e.log();
170    /// assert_eq!(returned.to_string(), "Variable error: missing key");
171    /// ```
172    fn log(self) -> Self {
173        tracing::error!("{}", self);
174        self
175    }
176
177    /// Log the error with an additional source error for debugging context.
178    ///
179    /// Logs both the error and the underlying source error (from a third-party
180    /// library or internal operation), providing full debugging context.
181    ///
182    /// # Arguments
183    ///
184    /// * `source_error` — The original error (must implement `Debug`)
185    ///
186    /// # Example
187    ///
188    /// ```
189    /// use apex_solver::linalg::LinAlgError;
190    /// use apex_solver::error::ErrorLogging;
191    ///
192    /// let source = std::io::Error::other("disk full");
193    /// let e = LinAlgError::FactorizationFailed("LU decomposition failed".to_string());
194    /// let returned = e.log_with_source(source);
195    /// assert_eq!(returned.to_string(), "Matrix factorization failed: LU decomposition failed");
196    /// ```
197    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
203// Blanket implementation: any type that implements Display gets ErrorLogging for free.
204// This eliminates the need for per-error-type log()/log_with_source() methods.
205impl<T: std::fmt::Display> ErrorLogging for T {}
206
207impl ApexSolverError {
208    /// Get the full error chain as a string for logging and debugging.
209    ///
210    /// This method traverses the error source chain and returns a formatted string
211    /// showing the hierarchy of errors from the top-level ApexSolverError down to the
212    /// root cause.
213    ///
214    /// # Example
215    ///
216    /// ```no_run
217    /// # use apex_solver::error::ApexSolverError;
218    /// # use tracing::warn;
219    /// # fn solver_optimize() -> Result<(), ApexSolverError> { Ok(()) }
220    /// # fn example() {
221    /// match solver_optimize() {
222    ///     Ok(result) => { /* ... */ }
223    ///     Err(e) => {
224    ///         warn!("Optimization failed!");
225    ///         warn!("Error chain: {}", e.chain());
226    ///         // Output: "Optimizer error: Linear system solve failed →
227    ///         //          Linear algebra error: Singular matrix detected"
228    ///     }
229    /// }
230    /// # }
231    /// ```
232    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    /// Get a compact single-line error chain for logging
245    ///
246    /// Similar to `chain()` but formats as a single line with arrow separators.
247    ///
248    /// # Example
249    ///
250    /// ```no_run
251    /// # use apex_solver::error::ApexSolverError;
252    /// # use apex_solver::core::CoreError;
253    /// # use tracing::error;
254    /// # fn example() {
255    /// # let apex_err = ApexSolverError::Core(CoreError::InvalidInput("test".to_string()));
256    /// error!("Operation failed: {}", apex_err.chain_compact());
257    /// // Output: "Optimizer error → Linear algebra error → Singular matrix"
258    /// # }
259    /// ```
260    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    // ========================================================================
282    // Layer C (Deep/Math): Simulates failures at the deepest layer
283    // ========================================================================
284
285    /// Layer C: Simulates a singular matrix failure at the deepest module layer.
286    /// Returns `Result<T, LinAlgError>` — the module-specific error type.
287    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    /// Layer C: Simulates a symbolic structure failure in the core module.
294    /// Returns `Result<T, CoreError>` — the module-specific error type.
295    fn build_structure() -> Result<(), CoreError> {
296        Err(CoreError::SymbolicStructure(
297            "Simulated duplicate variable index".to_string(),
298        ))
299    }
300
301    /// Layer C: Simulates a factor dimension mismatch.
302    /// Returns `Result<T, FactorError>` — the module-specific error type.
303    fn compute_projection() -> Result<(), FactorError> {
304        Err(FactorError::InvalidDimension {
305            expected: 3,
306            actual: 2,
307        })
308    }
309
310    // ========================================================================
311    // Layer B (Logic/Optimization): Calls Layer C using `?` operator
312    // ========================================================================
313
314    /// Layer B: Calls the linear solver using `?`.
315    /// The `?` operator auto-converts `LinAlgError` → `OptimizerError::LinAlg`
316    /// because `OptimizerError` implements `#[from] LinAlgError`.
317    fn run_optimization_step() -> Result<Mat<f64>, OptimizerError> {
318        let result = solve_linear_system()?;
319        Ok(result)
320    }
321
322    /// Layer B: Calls the core structure builder using `?`.
323    /// The `?` operator auto-converts `CoreError` → `OptimizerError::Core`
324    /// because `OptimizerError` implements `#[from] CoreError`.
325    fn initialize_optimization() -> Result<(), OptimizerError> {
326        build_structure()?;
327        Ok(())
328    }
329
330    // ========================================================================
331    // Layer A (Top/API): The public API function returning `ApexSolverResult`.
332    // The `?` operator auto-converts module errors → `ApexSolverError`.
333    // ========================================================================
334
335    /// Layer A: Public API function. The `?` operator auto-converts
336    /// `OptimizerError` → `ApexSolverError::Optimizer(...)` via `#[from]`.
337    fn solver_optimize() -> ApexSolverResult<()> {
338        let _ = run_optimization_step()?;
339        Ok(())
340    }
341
342    /// Layer A: Public API function with core error propagation.
343    fn solver_optimize_with_core_error() -> ApexSolverResult<()> {
344        initialize_optimization()?;
345        Ok(())
346    }
347
348    /// Layer A: Public API function with factor error propagation.
349    fn solver_optimize_with_factor_error() -> ApexSolverResult<()> {
350        compute_projection()?;
351        Ok(())
352    }
353
354    // ========================================================================
355    // Existing tests
356    // ========================================================================
357
358    #[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    // ========================================================================
416    // New tests: Bubble-up error propagation (A → B → C)
417    // ========================================================================
418
419    #[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    // ========================================================================
706    // CameraModelError → ApexSolverError::Camera (transparent wrap)
707    // ========================================================================
708
709    #[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}