1use std::path::Path;
7
8use hifitime::Epoch;
9
10use crate::error::{FitsCubeError, Result};
11use crate::fits_io::{HeaderGeom, find_target_axis, read_key_f64, read_key_string};
12
13#[derive(Debug, Clone)]
15pub struct SpecInfo {
16 pub file_specs: Vec<f64>,
18 pub specs: Vec<f64>,
21 pub missing: Vec<bool>,
23}
24
25fn freq_unit_to_hz(cunit: Option<&str>) -> f64 {
28 match cunit.map(|s| s.trim().to_ascii_uppercase()) {
29 Some(ref u) if u == "HZ" => 1.0,
30 Some(ref u) if u == "KHZ" => 1e3,
31 Some(ref u) if u == "MHZ" => 1e6,
32 Some(ref u) if u == "GHZ" => 1e9,
33 _ => 1.0,
34 }
35}
36
37pub fn utc_to_mjdsec(utc_time: &str) -> Result<f64> {
41 const SEC_PER_DAY: f64 = 86400.0;
42 let trimmed = utc_time.trim().trim_matches('\'').trim();
43 let epoch = Epoch::from_gregorian_str(trimmed)
44 .or_else(|_| trimmed.parse::<Epoch>())
45 .map_err(|e| FitsCubeError::TimeParse {
46 value: utc_time.to_string(),
47 msg: e.to_string(),
48 })?;
49 Ok(epoch.to_mjd_utc_days() * SEC_PER_DAY)
50}
51
52pub fn read_spec_from_header(path: &Path, time_domain_mode: bool) -> Result<f64> {
59 if time_domain_mode {
60 let date_obs = read_key_string(path, "DATE-OBS")?.ok_or_else(|| {
61 FitsCubeError::MissingKeyword(format!(
62 "DATE-OBS not in header of {} (needed for time-domain mode)",
63 path.display()
64 ))
65 })?;
66 return utc_to_mjdsec(&date_obs);
67 }
68
69 let geom = HeaderGeom::read(path)?;
70 if geom.is_2d() {
71 let reffreq = read_key_f64(path, "REFFREQ")?.ok_or_else(|| {
72 FitsCubeError::MissingKeyword(format!(
73 "REFFREQ not in header of {}. Cannot combine 2D images without \
74 frequency information.",
75 path.display()
76 ))
77 })?;
78 return Ok(reffreq);
79 }
80
81 let axis = find_target_axis(path, "FREQ").map_err(|_| {
83 FitsCubeError::TargetAxisMissing(format!(
84 "No FREQ axis found in WCS of {}. Cannot combine N-D images without \
85 frequency information.",
86 path.display()
87 ))
88 })?;
89 let world = axis.crval + (1.0 - axis.crpix) * axis.cdelt;
90 Ok(world * freq_unit_to_hz(axis.cunit.as_deref()))
91}
92
93pub fn isin_close(elements: &[f64], test: &[f64], time_domain_mode: bool) -> Vec<bool> {
104 let (atol, rtol) = if time_domain_mode {
105 (1e-10, 1e-9)
106 } else {
107 (1e-5, 1e-9)
108 };
109 elements
110 .iter()
111 .map(|&e| test.iter().any(|&b| (e - b).abs() <= atol + rtol * b.abs()))
112 .collect()
113}
114
115fn np_arange_fix(start: f64, stop: f64, step: f64) -> Vec<f64> {
118 let n = (stop - start) / step + 1.0;
119 let x = n - n.trunc();
120 let stop = if x < 0.5 {
121 stop + step * f64::max(0.1, x)
122 } else {
123 stop
124 };
125 let mut out = Vec::new();
126 let mut v = start;
127 while v < stop {
129 out.push(v);
130 v += step;
131 }
132 out
133}
134
135pub fn even_spacing(specs: &[f64], time_domain_mode: bool) -> (Vec<f64>, Vec<bool>) {
138 assert!(specs.len() >= 2, "need at least two values to space evenly");
139 let diffs: Vec<f64> = specs.windows(2).map(|w| w[1] - w[0]).collect();
140 let min_diff = diffs.iter().cloned().fold(f64::INFINITY, f64::min);
141 let new_specs = np_arange_fix(specs[0], specs[specs.len() - 1], min_diff);
142 let present = isin_close(&new_specs, specs, time_domain_mode);
143 let missing: Vec<bool> = present.iter().map(|&p| !p).collect();
144 (new_specs, missing)
145}
146
147pub fn parse_specs(
153 file_list: &[std::path::PathBuf],
154 spec_file: Option<&Path>,
155 spec_list: Option<&[f64]>,
156 ignore_spec: bool,
157 create_blanks: bool,
158 time_domain_mode: bool,
159) -> Result<SpecInfo> {
160 let n = file_list.len();
161
162 if ignore_spec {
163 tracing::info!("Ignoring frequency/time information");
164 let seq: Vec<f64> = (0..n).map(|i| i as f64).collect();
165 return Ok(SpecInfo {
166 file_specs: seq.clone(),
167 specs: seq,
168 missing: vec![false; n],
169 });
170 }
171
172 if spec_file.is_some() && spec_list.is_some() {
173 return Err(FitsCubeError::InvalidSpec(
174 "Must specify either spec_file or spec_list, not both".to_string(),
175 ));
176 }
177
178 let mut file_specs: Vec<f64> = if let Some(path) = spec_file {
179 tracing::info!("Reading spec values from {}", path.display());
180 let text = std::fs::read_to_string(path)?;
181 let vals: Vec<f64> = text
182 .split_whitespace()
183 .map(|t| t.parse::<f64>())
184 .collect::<std::result::Result<_, _>>()
185 .map_err(|e| FitsCubeError::InvalidSpec(format!("parsing {}: {e}", path.display())))?;
186 if vals.len() != n {
187 return Err(FitsCubeError::InvalidSpec(format!(
188 "Number of values in {} ({}) does not match number of images ({n})",
189 path.display(),
190 vals.len()
191 )));
192 }
193 vals
194 } else if let Some(list) = spec_list {
195 if list.len() != n {
196 return Err(FitsCubeError::InvalidSpec(format!(
197 "Number of --specs values ({}) does not match number of images ({n})",
198 list.len()
199 )));
200 }
201 list.to_vec()
202 } else {
203 file_list
204 .iter()
205 .map(|p| read_spec_from_header(p, time_domain_mode))
206 .collect::<Result<Vec<f64>>>()?
207 };
208
209 let (specs, missing) = if create_blanks {
210 tracing::info!("Creating an evenly-spaced axis with interpolated blanks");
211 even_spacing(&file_specs, time_domain_mode)
212 } else {
213 (file_specs.clone(), vec![false; file_specs.len()])
214 };
215
216 file_specs.truncate(n);
218
219 Ok(SpecInfo {
220 file_specs,
221 specs,
222 missing,
223 })
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use approx::assert_relative_eq;
230
231 #[test]
232 fn even_axis_has_no_missing() {
233 let specs = vec![1.0e9, 2.0e9, 3.0e9, 4.0e9];
234 let (new, missing) = even_spacing(&specs, false);
235 assert_eq!(new.len(), 4);
236 assert!(missing.iter().all(|&m| !m));
237 }
238
239 #[test]
240 fn gap_is_flagged_missing() {
241 let specs = vec![1.0e9, 2.0e9, 4.0e9];
243 let (new, missing) = even_spacing(&specs, false);
244 assert_eq!(new.len(), 4);
245 assert_eq!(missing.iter().filter(|&&m| m).count(), 1);
247 assert!(missing[2]);
248 }
249
250 #[test]
251 fn mjdsec_round_number() {
252 let s = utc_to_mjdsec("1858-11-17T00:00:00").unwrap();
254 assert_relative_eq!(s, 0.0, epsilon = 1e-3);
255 }
256
257 #[test]
258 fn mjdsec_one_day() {
259 let s = utc_to_mjdsec("1858-11-18T00:00:00").unwrap();
260 assert_relative_eq!(s, 86400.0, epsilon = 1e-3);
261 }
262}