Skip to main content

atfits_rs/
header.rs

1//! Header geometry and WCS axis lookup.
2use std::path::Path;
3
4use fitsio::FitsFile;
5
6use crate::error::{AtfitsError, Result};
7
8/// Shape and pixel metadata of a FITS primary image, in FITS axis order
9/// (`dims[0]` = NAXIS1, the fastest-varying axis).
10#[derive(Debug, Clone)]
11pub struct HeaderGeom {
12    pub naxis: usize,
13    /// `dims[i]` = NAXIS(i+1).
14    pub dims: Vec<usize>,
15    pub bitpix: i64,
16}
17
18impl HeaderGeom {
19    /// Read NAXIS, every NAXISn, and BITPIX from the primary HDU.
20    pub fn read(path: &Path) -> Result<Self> {
21        let mut fptr = FitsFile::open(path.to_string_lossy().as_ref())?;
22        let hdu = fptr.primary_hdu()?;
23        let naxis: i64 = hdu.read_key(&mut fptr, "NAXIS")?;
24        let bitpix: i64 = hdu.read_key(&mut fptr, "BITPIX")?;
25        let mut dims = Vec::with_capacity(naxis as usize);
26        for i in 1..=naxis {
27            let n: i64 = hdu.read_key(&mut fptr, &format!("NAXIS{i}"))?;
28            dims.push(n as usize);
29        }
30        Ok(Self {
31            naxis: naxis as usize,
32            dims,
33            bitpix,
34        })
35    }
36
37    /// Whether the image is two-dimensional (a single plane).
38    pub fn is_2d(&self) -> bool {
39        self.naxis == 2
40    }
41
42    /// Number of pixels per plane = NAXIS1 × NAXIS2.
43    pub fn plane_len(&self) -> usize {
44        self.dims.first().copied().unwrap_or(1) * self.dims.get(1).copied().unwrap_or(1)
45    }
46}
47
48/// Location of a target (FREQ/TIME) axis within a header's WCS.
49#[derive(Debug, Clone)]
50pub struct TargetAxis {
51    /// FITS axis number (1-based), e.g. 3 for NAXIS3.
52    pub fits_idx: usize,
53    /// numpy/array index (0-based, axis order reversed), as used by `np.take`.
54    pub array_idx: usize,
55    pub ctype: String,
56    pub crpix: f64,
57    pub crval: f64,
58    pub cdelt: f64,
59    pub cunit: Option<String>,
60}
61
62/// Search a header for the axis whose CTYPE contains `name` ("FREQ" or "TIME").
63///
64/// Returns [`AtfitsError::TargetAxisMissing`] when absent.
65pub fn find_target_axis(path: &Path, name: &str) -> Result<TargetAxis> {
66    let mut fptr = FitsFile::open(path.to_string_lossy().as_ref())?;
67    let hdu = fptr.primary_hdu()?;
68    let naxis: i64 = hdu.read_key(&mut fptr, "NAXIS")?;
69
70    for axis in 1..=naxis {
71        let ctype: String = match hdu.read_key(&mut fptr, &format!("CTYPE{axis}")) {
72            Ok(c) => c,
73            Err(_) => continue,
74        };
75        if ctype.contains(name) {
76            let crpix: f64 = hdu
77                .read_key(&mut fptr, &format!("CRPIX{axis}"))
78                .unwrap_or(1.0);
79            let crval: f64 = hdu
80                .read_key(&mut fptr, &format!("CRVAL{axis}"))
81                .unwrap_or(0.0);
82            let cdelt: f64 = hdu
83                .read_key(&mut fptr, &format!("CDELT{axis}"))
84                .unwrap_or(1.0);
85            let cunit: Option<String> = hdu.read_key(&mut fptr, &format!("CUNIT{axis}")).ok();
86            return Ok(TargetAxis {
87                fits_idx: axis as usize,
88                array_idx: (naxis - axis) as usize,
89                ctype,
90                crpix,
91                crval,
92                cdelt,
93                cunit,
94            });
95        }
96    }
97    Err(AtfitsError::TargetAxisMissing(format!(
98        "No {name} axis found in WCS of {}",
99        path.display()
100    )))
101}