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    /// The solve was stopped through a [`crate::cancel::CancelToken`].
47    Cancelled,
48    /// No blind index file was found at the given path.
49    IndexNotFound(std::path::PathBuf),
50    /// A blind index placed the field outside the search radius, where the
51    /// radius-limited search cannot reach it. The field is not reported, as the
52    /// radius requires; this is a "no solution" with a reason.
53    OutsideSearchRadius {
54        /// Distance from the hint to the field the index found, degrees.
55        separation_deg: f64,
56    },
57}
58
59impl fmt::Display for ArcsecError {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::Singular => write!(f, "singular matrix in LSQ solver"),
63            Self::InsufficientStars { found, required } => {
64                write!(f, "insufficient stars: found {found}, required {required}")
65            }
66            Self::InsufficientQuads { found, required } => {
67                write!(f, "insufficient quads: found {found}, required {required}")
68            }
69            Self::BadSolution { ratio } => {
70                write!(
71                    f,
72                    "bad solution: plate singular-value ratio {ratio:.4} exceeds {}",
73                    crate::math::lsq::MAX_PLATE_ANISOTROPY
74                )
75            }
76            Self::CatalogNotFound(p) => write!(f, "catalog not found: {}", p.display()),
77            Self::CatalogIo(e) => write!(f, "catalog I/O error: {e}"),
78            Self::InvalidParameter(msg) => write!(f, "invalid parameter: {msg}"),
79            Self::Cancelled => write!(f, "cancelled"),
80            Self::IndexNotFound(p) => write!(f, "no index files found at {}", p.display()),
81            Self::OutsideSearchRadius { separation_deg } => write!(
82                f,
83                "the blind index places this field {separation_deg:.1}° from the start \
84                 position, outside the search radius"
85            ),
86        }
87    }
88}
89
90impl core::error::Error for ArcsecError {
91    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
92        match self {
93            Self::CatalogIo(e) => Some(e),
94            _ => None,
95        }
96    }
97}
98
99impl From<std::io::Error> for ArcsecError {
100    fn from(e: std::io::Error) -> Self {
101        Self::CatalogIo(e)
102    }
103}
104
105/// `Result` specialised to [`ArcsecError`].
106pub type Result<T> = core::result::Result<T, ArcsecError>;
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use core::error::Error as _;
112
113    /// The messages are what the CLI prints, so pin them.
114    #[test]
115    fn display_messages() {
116        let cases = [
117            (ArcsecError::Singular, "singular matrix in LSQ solver"),
118            (
119                ArcsecError::InsufficientStars {
120                    found: 3,
121                    required: 5,
122                },
123                "insufficient stars: found 3, required 5",
124            ),
125            (
126                ArcsecError::InsufficientQuads {
127                    found: 0,
128                    required: 4,
129                },
130                "insufficient quads: found 0, required 4",
131            ),
132            (
133                ArcsecError::BadSolution { ratio: 1.23456 },
134                "bad solution: plate singular-value ratio 1.2346 exceeds 1.08",
135            ),
136            (
137                ArcsecError::CatalogNotFound(std::path::PathBuf::from("/db")),
138                "catalog not found: /db",
139            ),
140            (
141                ArcsecError::InvalidParameter("fov".into()),
142                "invalid parameter: fov",
143            ),
144            (ArcsecError::Cancelled, "cancelled"),
145            (
146                ArcsecError::IndexNotFound(std::path::PathBuf::from("/ix")),
147                "no index files found at /ix",
148            ),
149            (
150                ArcsecError::OutsideSearchRadius {
151                    separation_deg: 42.04,
152                },
153                "the blind index places this field 42.0° from the start position, \
154                 outside the search radius",
155            ),
156        ];
157        for (err, want) in cases {
158            assert_eq!(err.to_string(), want);
159            assert!(err.source().is_none());
160        }
161    }
162
163    #[test]
164    fn io_errors_convert_and_keep_their_source() {
165        let err: ArcsecError = std::io::Error::new(std::io::ErrorKind::NotFound, "gone").into();
166        assert_eq!(err.to_string(), "catalog I/O error: gone");
167        let source = err.source().expect("source");
168        assert_eq!(source.to_string(), "gone");
169        assert!(
170            matches!(err, ArcsecError::CatalogIo(ref e) if e.kind() == std::io::ErrorKind::NotFound)
171        );
172    }
173}