Skip to main content

arcsec_core/
lib.rs

1//! Core astrometry for the [arcsec] plate solver.
2//!
3//! Given the pixels of an astronomical image and an approximate pointing, this crate
4//! works out exactly where the image lies on the sky and returns a FITS-style WCS
5//! solution. It implements the star-pattern matching approach introduced by ASTAP and
6//! reads ASTAP's star databases (`.1476`, `.290` and `.001`). For hint-free ("blind")
7//! solving it builds its own pattern index from those databases ([`mod@index`],
8//! [`pipeline::index_solve()`]), and also reads Astrometry.net index files.
9//!
10//! The pipeline, driven by [`pipeline::solve_image`]:
11//!
12//! 1. **Detection** ([`detection`]) — background and noise estimation, then a
13//!    multi-pass star finder measuring centroid, HFD and SNR.
14//! 2. **Patterns** ([`quads`]) — 4-star quads described by five distance ratios.
15//! 3. **Search** ([`pipeline`]) — a square spiral around the hint; at each position
16//!    catalogue stars ([`catalog`]) are projected onto the tangent plane
17//!    ([`math::coords`]), turned into quads and matched against the image.
18//! 4. **Fit and verify** ([`math::lsq`], [`wcs`]) — a least-squares plate fit,
19//!    checked star by star before it is accepted.
20//!
21//! Angles are radians throughout the API unless a name says otherwise.
22//!
23//! # Example
24//!
25//! ```no_run
26//! use std::path::PathBuf;
27//! use arcsec_core::ImageBuffer;
28//! use arcsec_core::pipeline::{SearchSpeed, SolveMethod, SolveParams, solve_image};
29//!
30//! # fn load_pixels() -> ImageBuffer { ImageBuffer::new(4096, 4096) }
31//! let img: ImageBuffer = load_pixels(); // row-major f32 pixels from your FITS reader
32//! let params = SolveParams {
33//!     ra_hint: 83.82_f64.to_radians(),
34//!     dec_hint: (-5.39_f64).to_radians(),
35//!     fov: 1.2_f64.to_radians(),
36//!     search_radius: 10.0_f64.to_radians(),
37//!     quad_tolerance: 0.007,
38//!     hfd_min: 1.5,
39//!     max_stars: 500,
40//!     db_path: PathBuf::from("/usr/share/astap/data"),
41//!     db_name: "d50".into(),
42//!     binning: 1,
43//!     method: SolveMethod::Quads,
44//!     threads: 0,
45//!     speed: SearchSpeed::Auto,
46//! };
47//! let wcs = solve_image(&img, &params)?;
48//! println!(
49//!     "centre RA {:.4}°, Dec {:.4}°, scale {:.2}\"/px",
50//!     wcs.ra0.to_degrees(),
51//!     wcs.dec0.to_degrees(),
52//!     wcs.cdelt2 * 3600.0
53//! );
54//! # Ok::<(), arcsec_core::ArcsecError>(())
55//! ```
56//!
57//! Progress is reported through the [`log`] crate at `info` level; install any
58//! logger to see it.
59//!
60//! [arcsec]: https://github.com/cruzzil/arcsec
61//! [`log`]: https://docs.rs/log
62
63#![warn(missing_docs)]
64// Library-only API hygiene, on top of the workspace lints (which the CLI shares).
65#![warn(
66    clippy::must_use_candidate,
67    clippy::return_self_not_must_use,
68    clippy::missing_errors_doc,
69    clippy::missing_panics_doc,
70    clippy::doc_markdown
71)]
72
73extern crate alloc;
74
75use core::sync::atomic::{AtomicUsize, Ordering};
76
77/// Process-wide worker-thread limit. 0 = one per available core.
78static MAX_THREADS: AtomicUsize = AtomicUsize::new(0);
79
80/// Set the maximum number of worker threads any stage may use.
81///
82/// Detection bands, the background histogram, the pixel-range scan and the spiral
83/// search each spawn their own workers, so a single knob has to reach all of them;
84/// threading a parameter through every signature would be worse. `1` makes the whole
85/// solve single-threaded, which is what you want when running many solves in
86/// parallel yourself, or when profiling.
87pub fn set_max_threads(n: usize) {
88    MAX_THREADS.store(n, Ordering::Relaxed);
89}
90
91std::thread_local! {
92    /// Per-thread override of [`MAX_THREADS`]; 0 = none. See [`with_max_threads`].
93    static LOCAL_MAX_THREADS: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
94}
95
96/// Resolve the thread limit: this thread's override from [`with_max_threads`], else
97/// the process-wide value from [`set_max_threads`], else one per core.
98#[must_use]
99pub fn max_threads() -> usize {
100    match LOCAL_MAX_THREADS.with(core::cell::Cell::get) {
101        0 => match MAX_THREADS.load(Ordering::Relaxed) {
102            0 => std::thread::available_parallelism().map_or(1, core::num::NonZero::get),
103            n => n,
104        },
105        n => n,
106    }
107}
108
109/// Run `f` with the thread limit set to `n` on this thread only (0 = no override).
110///
111/// [`set_max_threads`] is process-wide, which is right for a command-line tool and
112/// wrong for a library host running several solves at once with different
113/// budgets. Every stage reads the limit on the thread that called the solver, and
114/// the solvers that hand work to their own threads pass the override on, so a
115/// solve inside `f` keeps to `n` threads whatever the rest of the process does.
116/// The previous value is restored when `f` returns or unwinds.
117pub fn with_max_threads<R>(n: usize, f: impl FnOnce() -> R) -> R {
118    struct Restore(usize);
119    impl Drop for Restore {
120        fn drop(&mut self) {
121            LOCAL_MAX_THREADS.with(|c| c.set(self.0));
122        }
123    }
124    let _restore = Restore(LOCAL_MAX_THREADS.with(|c| c.replace(n)));
125    f()
126}
127
128/// This thread's override from [`with_max_threads`], 0 if none: what a solver
129/// passes on to a thread it spawns.
130#[must_use]
131pub fn local_max_threads() -> usize {
132    LOCAL_MAX_THREADS.with(core::cell::Cell::get)
133}
134
135pub mod auto;
136pub mod cancel;
137pub mod catalog;
138pub mod detection;
139pub mod error;
140pub mod index;
141pub mod math;
142pub mod pipeline;
143pub mod quads;
144pub mod types;
145pub mod wcs;
146
147/// Synthetic skies, images and star databases for tests: compiled for this crate's
148/// own tests, and with the `test-support` feature for the tests of the crates built
149/// on it (the C library's end-to-end test). Not a stable API.
150#[cfg(any(test, feature = "test-support"))]
151#[doc(hidden)]
152pub mod test_support;
153
154pub use catalog::{AnetIndex, AnetIndexEntry, AnetStar, load_anet_index, peek_anet_scale};
155pub use error::{ArcsecError, Result};
156pub use pipeline::{BlindSolveParams, blind_solve};
157pub use types::{
158    ImageBuffer, MatchedStar, PairedPositions, PlateConstants, Quad, QuadList, Star, StarList,
159    WcsSolution,
160};