seiza-cli 0.15.4

Command-line interface for seiza: star detection, plate solving, and dataset management
use seiza_fits::{F32ImageData, FitsImage, HeaderValue, WriteHeaderCard, write_f32_image};
use std::process::Command;

fn normalized(value: usize, extent: usize) -> f32 {
    2.0 * value as f32 / (extent - 1) as f32 - 1.0
}

#[test]
fn background_cli_writes_corrected_model_and_diagnostics() {
    let directory = tempfile::tempdir().unwrap();
    let input = directory.path().join("gradient.fits");
    let output = directory.path().join("corrected.fits");
    let model = directory.path().join("model.fits");
    let diagnostics = directory.path().join("fit.json");
    let (width, height) = (96, 72);
    let mut values = Vec::with_capacity(width * height);
    for y in 0..height {
        for x in 0..width {
            values.push(0.2 + 0.08 * normalized(x, width) - 0.04 * normalized(y, height));
        }
    }
    let wcs = [
        WriteHeaderCard::new("CRPIX1", HeaderValue::Float(48.0)),
        WriteHeaderCard::new("CRPIX2", HeaderValue::Float(36.0)),
        WriteHeaderCard::new("CRVAL1", HeaderValue::Float(180.0)),
        WriteHeaderCard::new("CRVAL2", HeaderValue::Float(20.0)),
        WriteHeaderCard::new("CTYPE1", HeaderValue::String("RA---TAN".into())),
        WriteHeaderCard::new("CTYPE2", HeaderValue::String("DEC--TAN".into())),
    ];
    write_f32_image(&input, width, height, F32ImageData::Mono(&values), &wcs).unwrap();

    let result = Command::new(env!("CARGO_BIN_EXE_seiza"))
        .args([
            "background",
            input.to_str().unwrap(),
            "--output",
            output.to_str().unwrap(),
            "--model-output",
            model.to_str().unwrap(),
            "--diagnostics",
            diagnostics.to_str().unwrap(),
            "--degree",
            "1",
            "--sample-radius",
            "2",
        ])
        .output()
        .unwrap();
    assert!(
        result.status.success(),
        "{}",
        String::from_utf8_lossy(&result.stderr)
    );

    let corrected = FitsImage::open(&output).unwrap();
    let corrected = match corrected.pixels {
        seiza_fits::Pixels::F32(values) => values,
        pixels => panic!("expected f32 output, got {pixels:?}"),
    };
    let left = corrected[height / 2 * width + 3];
    let right = corrected[height / 2 * width + width - 4];
    assert!((left - right).abs() < 0.003);
    let output_headers = FitsImage::open(&output).unwrap();
    assert_eq!(
        output_headers
            .header("SEIZABG")
            .and_then(HeaderValue::as_str),
        Some("SUBTRACT")
    );
    assert_eq!(
        output_headers
            .header("BGMODEL")
            .and_then(HeaderValue::as_str),
        Some("POLYNOMIAL")
    );
    assert_eq!(output_headers.header_f64("BGSTR"), Some(1.0));
    assert_eq!(output_headers.header_f64("CRVAL1"), Some(180.0));
    assert!(model.is_file());
    let report: serde_json::Value =
        serde_json::from_slice(&std::fs::read(diagnostics).unwrap()).unwrap();
    assert!(report["diagnostics"]["accepted_samples"].as_u64().unwrap() > 10);
    assert_eq!(
        report["diagnostics"]["model_selection"]["selected"],
        "polynomial_1"
    );
    assert_eq!(
        report["diagnostics"]["model_selection"]["candidates"]
            .as_array()
            .unwrap()
            .len(),
        2
    );
}

#[test]
fn background_cli_refuses_to_overwrite_its_input() {
    let directory = tempfile::tempdir().unwrap();
    let input = directory.path().join("gradient.fits");
    let values = vec![0.2_f32; 32 * 32];
    write_f32_image(&input, 32, 32, F32ImageData::Mono(&values), &[]).unwrap();
    let original = std::fs::read(&input).unwrap();
    let result = Command::new(env!("CARGO_BIN_EXE_seiza"))
        .args([
            "background",
            input.to_str().unwrap(),
            "--output",
            input.to_str().unwrap(),
        ])
        .output()
        .unwrap();
    assert!(!result.status.success());
    assert!(String::from_utf8_lossy(&result.stderr).contains("same file"));
    assert_eq!(std::fs::read(input).unwrap(), original);
}

#[test]
fn background_cli_rejects_an_undebayered_cfa() {
    let directory = tempfile::tempdir().unwrap();
    let input = directory.path().join("raw-cfa.fits");
    let output = directory.path().join("incorrect.fits");
    let values = vec![0.2_f32; 32 * 32];
    let headers = [WriteHeaderCard::new(
        "BAYERPAT",
        HeaderValue::String("RGGB".into()),
    )];
    write_f32_image(&input, 32, 32, F32ImageData::Mono(&values), &headers).unwrap();
    let result = Command::new(env!("CARGO_BIN_EXE_seiza"))
        .args([
            "background",
            input.to_str().unwrap(),
            "--output",
            output.to_str().unwrap(),
        ])
        .output()
        .unwrap();
    assert!(!result.status.success());
    assert!(String::from_utf8_lossy(&result.stderr).contains("raw Bayer subchannels"));
    assert!(!output.exists());
}