Skip to main content

fits_well/groups/
mod.rs

1//! Random-groups primary array (§6) — read only.
2//!
3//! A legacy structure (radio interferometry `uv` data): `GROUPS = T`, `NAXIS1 =
4//! 0`, and the data is `GCOUNT` groups, each `PCOUNT` parameters followed by an
5//! array of `NAXIS2 × … × NAXISm` elements. Per "once FITS, always FITS" this is
6//! decoded but never written.
7
8use crate::bitpix::Bitpix;
9use crate::data::ImageData;
10use crate::data::Scaling;
11use crate::error::FitsError;
12use crate::error::Result;
13use crate::header::Header;
14use crate::keyword::key;
15
16/// A decoded random-groups primary array.
17#[derive(Debug, Clone)]
18pub struct RandomGroups {
19    /// `PTYPEn` parameter names, in order (length `pcount`).
20    pub parameter_names: Vec<String>,
21    /// The per-group array shape (`NAXIS2..NAXISm`; the `NAXIS1` zero sentinel is
22    /// dropped).
23    pub group_shape: Vec<usize>,
24    pub gcount: usize,
25    pub pcount: usize,
26    pub bitpix: Bitpix,
27    array_scaling: Scaling,
28    /// `PSCALn`/`PZEROn` per parameter.
29    param_scaling: Vec<ParamScale>,
30    /// Flat host-endian samples: `gcount` groups of `pcount + array_len` elements.
31    samples: ImageData,
32}
33
34/// `PSCALn`/`PZEROn` linear scaling for one group parameter
35/// (`physical = pzero + pscal · raw`).
36#[derive(Debug, Clone, Copy)]
37struct ParamScale {
38    pscal: f64,
39    pzero: f64,
40}
41
42impl RandomGroups {
43    pub(crate) fn from_data(header: &Header, data: &[u8]) -> Result<RandomGroups> {
44        let bitpix = header.bitpix()?;
45        let axes = header.axes()?;
46        // NAXIS1 is the zero sentinel; the per-group array spans the rest.
47        let group_shape: Vec<usize> = axes.iter().skip(1).copied().collect();
48        let pcount = match header.get_integer("PCOUNT") {
49            Some(p) if p < 0 => return Err(FitsError::KeywordOutOfRange { name: "PCOUNT" }),
50            Some(p) => p as usize,
51            None => 0,
52        };
53        let gcount = match header.get_integer("GCOUNT") {
54            Some(g) if g < 1 => return Err(FitsError::KeywordOutOfRange { name: "GCOUNT" }),
55            Some(g) => g as usize,
56            None => 1,
57        };
58
59        let mut parameter_names = Vec::with_capacity(pcount);
60        let mut param_scaling = Vec::with_capacity(pcount);
61        for j in 1..=pcount {
62            parameter_names.push(
63                header
64                    .get_text(key!("PTYPE{j}").as_str())
65                    .unwrap_or("")
66                    .to_string(),
67            );
68            param_scaling.push(ParamScale {
69                pscal: header.get_real(key!("PSCAL{j}").as_str()).unwrap_or(1.0),
70                pzero: header.get_real(key!("PZERO{j}").as_str()).unwrap_or(0.0),
71            });
72        }
73
74        let samples = ImageData::decode(data, bitpix);
75        let groups = RandomGroups {
76            parameter_names,
77            group_shape,
78            gcount,
79            pcount,
80            bitpix,
81            array_scaling: Scaling::from_header(header),
82            param_scaling,
83            samples,
84        };
85        let expected = groups.gcount * groups.group_len();
86        if groups.samples.len() != expected {
87            return Err(FitsError::DataSizeMismatch {
88                expected,
89                got: groups.samples.len(),
90            });
91        }
92        Ok(groups)
93    }
94
95    /// Elements in one group's array — `Π NAXIS2..NAXISm`, the FITS Eq. 2 product.
96    /// With no array axis (`NAXIS = 1`) this is the empty product `1`, not `0`,
97    /// matching how [`crate::hdu`]'s `data_extent` sizes the group (one array element
98    /// per group); `shape_product`'s image-specific "empty ⇒ 0" rule must *not* be
99    /// used here, or the two disagree and a `NAXIS = 1` group fails the size check.
100    pub fn array_len(&self) -> usize {
101        self.group_shape.iter().product()
102    }
103
104    /// The physical parameter values of group `g`: `PZEROn + PSCALn × raw`.
105    pub fn parameters_physical(&self, group: usize) -> Vec<f64> {
106        let base = group * self.group_len();
107        (0..self.pcount)
108            .map(|j| {
109                let ParamScale { pscal, pzero } = self.param_scaling[j];
110                pzero + pscal * elem_f64(&self.samples, base + j)
111            })
112            .collect()
113    }
114
115    /// The physical value of the named group parameter (§6.3): when extra
116    /// precision splits one logical parameter into two or more group parameters
117    /// sharing a `PTYPEn` name, the value is the **sum** of those addends'
118    /// physical values. `None` if no parameter has the name. (For the raw
119    /// per-addend values, use [`RandomGroups::parameters_physical`].)
120    pub fn parameter_physical(&self, group: usize, name: &str) -> Option<f64> {
121        let base = group * self.group_len();
122        let mut sum = 0.0;
123        let mut found = false;
124        for j in 0..self.pcount {
125            if self.parameter_names[j] == name {
126                found = true;
127                let ParamScale { pscal, pzero } = self.param_scaling[j];
128                sum += pzero + pscal * elem_f64(&self.samples, base + j);
129            }
130        }
131        found.then_some(sum)
132    }
133
134    /// The physical array values of group `g`: `BZERO + BSCALE × raw`.
135    pub fn array_physical(&self, group: usize) -> Vec<f64> {
136        let base = group * self.group_len() + self.pcount;
137        (0..self.array_len())
138            .map(|k| {
139                self.array_scaling.bzero
140                    + self.array_scaling.bscale * elem_f64(&self.samples, base + k)
141            })
142            .collect()
143    }
144
145    fn group_len(&self) -> usize {
146        self.pcount + self.array_len()
147    }
148}
149
150/// Read sample `i` of a typed buffer as `f64` (widening).
151fn elem_f64(samples: &ImageData, i: usize) -> f64 {
152    match samples {
153        ImageData::U8(v) => v[i] as f64,
154        ImageData::I16(v) => v[i] as f64,
155        ImageData::I32(v) => v[i] as f64,
156        ImageData::I64(v) => v[i] as f64,
157        ImageData::F32(v) => v[i] as f64,
158        ImageData::F64(v) => v[i],
159    }
160}
161
162#[cfg(test)]
163mod tests;