use std::path::Path;
use hifitime::Epoch;
use crate::error::{FitsCubeError, Result};
use crate::fits_io::{HeaderGeom, find_target_axis, read_key_f64, read_key_string};
#[derive(Debug, Clone)]
pub struct SpecInfo {
pub file_specs: Vec<f64>,
pub specs: Vec<f64>,
pub missing: Vec<bool>,
}
fn freq_unit_to_hz(cunit: Option<&str>) -> f64 {
match cunit.map(|s| s.trim().to_ascii_uppercase()) {
Some(ref u) if u == "HZ" => 1.0,
Some(ref u) if u == "KHZ" => 1e3,
Some(ref u) if u == "MHZ" => 1e6,
Some(ref u) if u == "GHZ" => 1e9,
_ => 1.0,
}
}
pub fn utc_to_mjdsec(utc_time: &str) -> Result<f64> {
const SEC_PER_DAY: f64 = 86400.0;
let trimmed = utc_time.trim().trim_matches('\'').trim();
let epoch = Epoch::from_gregorian_str(trimmed)
.or_else(|_| trimmed.parse::<Epoch>())
.map_err(|e| FitsCubeError::TimeParse {
value: utc_time.to_string(),
msg: e.to_string(),
})?;
Ok(epoch.to_mjd_utc_days() * SEC_PER_DAY)
}
pub fn read_spec_from_header(path: &Path, time_domain_mode: bool) -> Result<f64> {
if time_domain_mode {
let date_obs = read_key_string(path, "DATE-OBS")?.ok_or_else(|| {
FitsCubeError::MissingKeyword(format!(
"DATE-OBS not in header of {} (needed for time-domain mode)",
path.display()
))
})?;
return utc_to_mjdsec(&date_obs);
}
let geom = HeaderGeom::read(path)?;
if geom.is_2d() {
let reffreq = read_key_f64(path, "REFFREQ")?.ok_or_else(|| {
FitsCubeError::MissingKeyword(format!(
"REFFREQ not in header of {}. Cannot combine 2D images without \
frequency information.",
path.display()
))
})?;
return Ok(reffreq);
}
let axis = find_target_axis(path, "FREQ").map_err(|_| {
FitsCubeError::TargetAxisMissing(format!(
"No FREQ axis found in WCS of {}. Cannot combine N-D images without \
frequency information.",
path.display()
))
})?;
let world = axis.crval + (1.0 - axis.crpix) * axis.cdelt;
Ok(world * freq_unit_to_hz(axis.cunit.as_deref()))
}
pub fn isin_close(elements: &[f64], test: &[f64], time_domain_mode: bool) -> Vec<bool> {
let (atol, rtol) = if time_domain_mode {
(1e-10, 1e-9)
} else {
(1e-5, 1e-9)
};
elements
.iter()
.map(|&e| test.iter().any(|&b| (e - b).abs() <= atol + rtol * b.abs()))
.collect()
}
fn np_arange_fix(start: f64, stop: f64, step: f64) -> Vec<f64> {
let n = (stop - start) / step + 1.0;
let x = n - n.trunc();
let stop = if x < 0.5 {
stop + step * f64::max(0.1, x)
} else {
stop
};
let mut out = Vec::new();
let mut v = start;
while v < stop {
out.push(v);
v += step;
}
out
}
fn grid_step(diffs: &[f64]) -> f64 {
let min_diff = diffs.iter().cloned().fold(f64::INFINITY, f64::min);
let max_diff = diffs.iter().map(|d| d.abs()).fold(0.0_f64, f64::max);
let tol = max_diff * 1e-6;
let mut g = diffs[0].abs();
for &d in &diffs[1..] {
let (mut a, mut b) = if g >= d.abs() {
(g, d.abs())
} else {
(d.abs(), g)
};
while b > tol {
let r = a % b;
a = b;
b = r;
}
g = a;
}
let divides_all = diffs.iter().all(|&d| {
let q = d.abs() / g;
(q - q.round()).abs() <= 1e-3
});
if g <= tol || !divides_all {
min_diff
} else {
g
}
}
pub fn even_spacing(specs: &[f64], time_domain_mode: bool) -> (Vec<f64>, Vec<bool>) {
assert!(specs.len() >= 2, "need at least two values to space evenly");
let diffs: Vec<f64> = specs.windows(2).map(|w| w[1] - w[0]).collect();
let step = grid_step(&diffs);
let new_specs = np_arange_fix(specs[0], specs[specs.len() - 1], step);
let present = isin_close(&new_specs, specs, time_domain_mode);
let missing: Vec<bool> = present.iter().map(|&p| !p).collect();
(new_specs, missing)
}
pub fn parse_specs(
file_list: &[std::path::PathBuf],
spec_file: Option<&Path>,
spec_list: Option<&[f64]>,
ignore_spec: bool,
create_blanks: bool,
time_domain_mode: bool,
) -> Result<SpecInfo> {
let n = file_list.len();
if ignore_spec {
tracing::info!("Ignoring frequency/time information");
let seq: Vec<f64> = (0..n).map(|i| i as f64).collect();
return Ok(SpecInfo {
file_specs: seq.clone(),
specs: seq,
missing: vec![false; n],
});
}
if spec_file.is_some() && spec_list.is_some() {
return Err(FitsCubeError::InvalidSpec(
"Must specify either spec_file or spec_list, not both".to_string(),
));
}
let mut file_specs: Vec<f64> = if let Some(path) = spec_file {
tracing::info!("Reading spec values from {}", path.display());
let text = std::fs::read_to_string(path)?;
let vals: Vec<f64> = text
.split_whitespace()
.map(|t| t.parse::<f64>())
.collect::<std::result::Result<_, _>>()
.map_err(|e| FitsCubeError::InvalidSpec(format!("parsing {}: {e}", path.display())))?;
if vals.len() != n {
return Err(FitsCubeError::InvalidSpec(format!(
"Number of values in {} ({}) does not match number of images ({n})",
path.display(),
vals.len()
)));
}
vals
} else if let Some(list) = spec_list {
if list.len() != n {
return Err(FitsCubeError::InvalidSpec(format!(
"Number of --specs values ({}) does not match number of images ({n})",
list.len()
)));
}
list.to_vec()
} else {
file_list
.iter()
.map(|p| read_spec_from_header(p, time_domain_mode))
.collect::<Result<Vec<f64>>>()?
};
let (specs, missing) = if create_blanks {
tracing::info!("Creating an evenly-spaced axis with interpolated blanks");
even_spacing(&file_specs, time_domain_mode)
} else {
(file_specs.clone(), vec![false; file_specs.len()])
};
file_specs.truncate(n);
Ok(SpecInfo {
file_specs,
specs,
missing,
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn even_axis_has_no_missing() {
let specs = vec![1.0e9, 2.0e9, 3.0e9, 4.0e9];
let (new, missing) = even_spacing(&specs, false);
assert_eq!(new.len(), 4);
assert!(missing.iter().all(|&m| !m));
}
#[test]
fn gap_is_flagged_missing() {
let specs = vec![1.0e9, 2.0e9, 4.0e9];
let (new, missing) = even_spacing(&specs, false);
assert_eq!(new.len(), 4);
assert_eq!(missing.iter().filter(|&&m| m).count(), 1);
assert!(missing[2]);
}
#[test]
fn non_adjacent_gaps_recover_true_step() {
let specs = vec![0.0, 3.0, 5.0];
let (new, missing) = even_spacing(&specs, false);
assert_eq!(new.len(), 6, "should rebuild the full 0..=5 grid");
assert_eq!(missing.iter().filter(|&&m| m).count(), 3);
let expected = [false, true, true, false, true, false];
assert_eq!(missing, expected);
}
#[test]
fn mjdsec_round_number() {
let s = utc_to_mjdsec("1858-11-17T00:00:00").unwrap();
assert_relative_eq!(s, 0.0, epsilon = 1e-3);
}
#[test]
fn mjdsec_one_day() {
let s = utc_to_mjdsec("1858-11-18T00:00:00").unwrap();
assert_relative_eq!(s, 86400.0, epsilon = 1e-3);
}
}