Skip to main content

arcsec_core/
error.rs

1//! The crate's error type.
2
3use core::fmt;
4
5/// Everything that can stop a solve.
6///
7/// The variants map onto the ASTAP-compatible exit codes the `arcsec` CLI returns
8/// (2 for too few stars, 1 for no solution, 32/33 for database problems), which is
9/// why "no match" is reported through [`ArcsecError::InsufficientQuads`] rather than
10/// a dedicated variant. New variants may be added, so match with a wildcard arm.
11#[derive(Debug)]
12#[non_exhaustive]
13pub enum ArcsecError {
14    /// The least-squares system was singular (degenerate or too few points).
15    Singular,
16    /// Fewer stars were detected than the solver needs.
17    InsufficientStars {
18        /// Stars actually detected.
19        found: usize,
20        /// Minimum the solver requires.
21        required: usize,
22    },
23    /// Pattern matching did not produce a verified solution.
24    ///
25    /// Returned both when too few patterns could be built and when the search
26    /// finished without any position verifying.
27    InsufficientQuads {
28        /// Patterns (or, for the blind solver, verification score) achieved.
29        found: usize,
30        /// Minimum required.
31        required: usize,
32    },
33    /// The plate fit is not a similarity transform: it stretches or shears the
34    /// image by more than [`crate::math::lsq::MAX_PLATE_ANISOTROPY`].
35    BadSolution {
36        /// Ratio of the larger to the smaller singular value of the plate's linear
37        /// part ([`crate::math::lsq::plate_anisotropy`]); 1 for a similarity.
38        ratio: f64,
39    },
40    /// The star database directory does not contain the named database.
41    CatalogNotFound(std::path::PathBuf),
42    /// A catalogue or index file could not be read or parsed.
43    CatalogIo(std::io::Error),
44    /// A caller-supplied parameter is out of range (e.g. a non-positive field of view).
45    InvalidParameter(String),
46}
47
48impl fmt::Display for ArcsecError {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Self::Singular => write!(f, "singular matrix in LSQ solver"),
52            Self::InsufficientStars { found, required } => {
53                write!(f, "insufficient stars: found {found}, required {required}")
54            }
55            Self::InsufficientQuads { found, required } => {
56                write!(f, "insufficient quads: found {found}, required {required}")
57            }
58            Self::BadSolution { ratio } => {
59                write!(
60                    f,
61                    "bad solution: plate singular-value ratio {ratio:.4} exceeds {}",
62                    crate::math::lsq::MAX_PLATE_ANISOTROPY
63                )
64            }
65            Self::CatalogNotFound(p) => write!(f, "catalog not found: {}", p.display()),
66            Self::CatalogIo(e) => write!(f, "catalog I/O error: {e}"),
67            Self::InvalidParameter(msg) => write!(f, "invalid parameter: {msg}"),
68        }
69    }
70}
71
72impl core::error::Error for ArcsecError {
73    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
74        match self {
75            Self::CatalogIo(e) => Some(e),
76            _ => None,
77        }
78    }
79}
80
81impl From<std::io::Error> for ArcsecError {
82    fn from(e: std::io::Error) -> Self {
83        Self::CatalogIo(e)
84    }
85}
86
87/// `Result` specialised to [`ArcsecError`].
88pub type Result<T> = core::result::Result<T, ArcsecError>;
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use core::error::Error as _;
94
95    /// The messages are what the CLI prints, so pin them.
96    #[test]
97    fn display_messages() {
98        let cases = [
99            (ArcsecError::Singular, "singular matrix in LSQ solver"),
100            (
101                ArcsecError::InsufficientStars {
102                    found: 3,
103                    required: 5,
104                },
105                "insufficient stars: found 3, required 5",
106            ),
107            (
108                ArcsecError::InsufficientQuads {
109                    found: 0,
110                    required: 4,
111                },
112                "insufficient quads: found 0, required 4",
113            ),
114            (
115                ArcsecError::BadSolution { ratio: 1.23456 },
116                "bad solution: plate singular-value ratio 1.2346 exceeds 1.08",
117            ),
118            (
119                ArcsecError::CatalogNotFound(std::path::PathBuf::from("/db")),
120                "catalog not found: /db",
121            ),
122            (
123                ArcsecError::InvalidParameter("fov".into()),
124                "invalid parameter: fov",
125            ),
126        ];
127        for (err, want) in cases {
128            assert_eq!(err.to_string(), want);
129            assert!(err.source().is_none());
130        }
131    }
132
133    #[test]
134    fn io_errors_convert_and_keep_their_source() {
135        let err: ArcsecError = std::io::Error::new(std::io::ErrorKind::NotFound, "gone").into();
136        assert_eq!(err.to_string(), "catalog I/O error: gone");
137        let source = err.source().expect("source");
138        assert_eq!(source.to_string(), "gone");
139        assert!(
140            matches!(err, ArcsecError::CatalogIo(ref e) if e.kind() == std::io::ErrorKind::NotFound)
141        );
142    }
143}