use std::path::{Path, PathBuf};
use arcsec_core::detection::{Analysis, MeasuredStar, analyse_image};
use arcsec_core::types::ImageBuffer;
use arcsec_core::wcs::TanWcs;
pub const NO_HFD: f64 = 21.47;
pub fn snr_min(value: f64) -> f64 {
if value == 0.0 { 30.0 } else { value }
}
pub fn csv_path(image: &Path) -> PathBuf {
let mut s = image.with_extension("").into_os_string();
s.push(".csv");
PathBuf::from(s)
}
pub fn analyse_and_report(img: &ImageBuffer, snr_min: f64, max_stars: usize) -> (Analysis, f64) {
let analysis = analyse_image(img, snr_min, max_stars);
let hfd = analysis.hfd_median().unwrap_or(NO_HFD);
println!("HFD_MEDIAN={hfd:.1}");
println!("STARS={}", analysis.stars.len());
(analysis, hfd)
}
pub fn analyse_exit_code(hfd_median: f64, stars: usize) -> i32 {
let hfd = (hfd_median * 100.0).round_ties_even() as i64;
(hfd * 1_000_000 + stars as i64) as i32
}
#[cfg(windows)]
const LINE_END: &str = "\r\n";
#[cfg(not(windows))]
const LINE_END: &str = "\n";
pub fn csv_text(stars: &[MeasuredStar], wcs: Option<&TanWcs>) -> String {
use core::fmt::Write as _;
let mut out = String::with_capacity(64 * (stars.len() + 1));
out.push_str("x,y,hfd,snr,flux,ra[0..360],dec[0..360]");
out.push_str(LINE_END);
for s in stars {
let (x, y) = (s.x + 1.0, s.y + 1.0);
let _ = write!(
out,
"{x:.4},{y:.4},{:.4},{},{}",
s.hfd,
s.snr.round_ties_even() as i64,
s.flux.round_ties_even() as i64
);
if let Some(w) = wcs {
let (ra, dec) = w.pixel_to_sky(x, y);
let _ = write!(out, ",{:.8},{:.8}", ra.to_degrees(), dec.to_degrees());
}
out.push_str(LINE_END);
}
out.push_str(LINE_END);
out
}
pub fn write_csv(path: &Path, stars: &[MeasuredStar], wcs: Option<&TanWcs>) -> std::io::Result<()> {
std::fs::write(path, csv_text(stars, wcs))
}
pub struct Extract2 {
pub snr_min: f64,
pub max_stars: usize,
pub csv: PathBuf,
pub img: ImageBuffer,
pub header_wcs: Option<TanWcs>,
}
impl Extract2 {
pub fn run(&self, solution: Option<&TanWcs>) {
let analysis = analyse_image(&self.img, self.snr_min, self.max_stars);
let wcs = solution.or(self.header_wcs.as_ref());
if let Err(e) = write_csv(&self.csv, &analysis.stars, wcs) {
eprintln!("Warning: could not write {}: {e}", self.csv.display());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn star(x: f64, y: f64) -> MeasuredStar {
MeasuredStar {
x,
y,
hfd: 4.66784,
snr: 274.5,
flux: 377_475.6,
}
}
#[test]
fn csv_matches_astap_layout() {
let text = csv_text(&[star(1588.67292, 14.80514)], None);
let expected = format!(
"x,y,hfd,snr,flux,ra[0..360],dec[0..360]{LINE_END}\
1589.6729,15.8051,4.6678,274,377476{LINE_END}{LINE_END}"
);
assert_eq!(
text, expected,
"SNR 274.5 rounds half to even, as Pascal does"
);
}
#[test]
fn csv_adds_ra_and_dec_with_a_wcs() {
let wcs = TanWcs {
ra0: 210.8f64.to_radians(),
dec0: 54.35f64.to_radians(),
crpix1: 1589.6729,
crpix2: 15.8051,
cd: [[-3.45e-4, 0.0], [0.0, 3.45e-4]],
sip: None,
};
let text = csv_text(&[star(1588.6729, 14.8051)], Some(&wcs));
let row = text.lines().nth(1).unwrap();
assert_eq!(
row,
"1589.6729,15.8051,4.6678,274,377476,210.80000000,54.35000000"
);
assert_eq!(
csv_text(&[], Some(&wcs)),
format!("x,y,hfd,snr,flux,ra[0..360],dec[0..360]{LINE_END}{LINE_END}")
);
}
#[test]
fn csv_goes_next_to_the_image() {
assert_eq!(
csv_path(Path::new("dir/M31.fits")),
Path::new("dir/M31.csv")
);
assert_eq!(
csv_path(Path::new("dir/30.00s_0018.fit")),
Path::new("dir/30.00s_0018.csv")
);
assert_eq!(csv_path(Path::new("image")), Path::new("image.csv"));
}
#[test]
fn windows_exit_code_packs_hfd_and_count() {
assert_eq!(analyse_exit_code(4.5, 733), 450_000_733);
assert_eq!(analyse_exit_code(NO_HFD, 0), 2_147_000_000);
assert_eq!(analyse_exit_code(25.0, 7), (2_500_000_007_i64 as i32));
}
#[test]
fn zero_snr_means_thirty() {
assert_eq!(snr_min(0.0), 30.0);
assert_eq!(snr_min(12.0), 12.0);
}
}