use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::{Args, Parser, Subcommand};
use tracing::info;
use tracing_subscriber::EnvFilter;
use fitscube_rs::combine::{CombineOptions, combine_fits};
use fitscube_rs::extract::{ExtractOptions, extract_plane_from_cube};
#[derive(Parser)]
#[command(
name = "fitscubers",
about = "Combine single-frequency/single-time FITS images into a cube, or extract a plane",
version
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Combine(CombineArgs),
Extract(ExtractArgs),
}
#[derive(Args)]
struct CombineArgs {
#[arg(required = true, num_args = 1..)]
file_list: Vec<PathBuf>,
out_cube: PathBuf,
#[arg(short, long)]
overwrite: bool,
#[arg(long)]
create_blanks: bool,
#[arg(long)]
time_domain: bool,
#[arg(long, group = "spec")]
spec_file: Option<PathBuf>,
#[arg(long, num_args = 1.., group = "spec")]
specs: Option<Vec<f64>>,
#[arg(long, group = "spec")]
ignore_spec: bool,
#[arg(short, long, action = clap::ArgAction::Count)]
verbosity: u8,
#[arg(long)]
max_workers: Option<usize>,
#[arg(long)]
bounding_box: bool,
#[arg(long)]
invalidate_zeros: bool,
#[arg(long, value_parser = ["32", "64"])]
floating: Option<String>,
}
#[derive(Args)]
struct ExtractArgs {
fits_cube: PathBuf,
#[arg(long, group = "idx")]
channel_index: Option<usize>,
#[arg(long, group = "idx")]
time_index: Option<usize>,
#[arg(long, default_value_t = 0)]
hdu_index: usize,
#[arg(short, long, action = clap::ArgAction::Count)]
verbosity: u8,
#[arg(long)]
overwrite: bool,
#[arg(long)]
output_path: Option<PathBuf>,
}
fn init_logging(verbosity: u8) {
let level = match verbosity {
0 => "warn",
1 => "info",
_ => "debug",
};
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(format!("fitscube={level},fitscube_rs={level}")));
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.try_init();
}
fn run_combine(args: CombineArgs) -> Result<()> {
init_logging(args.verbosity);
if !args.overwrite && args.out_cube.exists() {
anyhow::bail!(
"Output file {} already exists. Use --overwrite to overwrite.",
args.out_cube.display()
);
}
let options = CombineOptions {
spec_file: args.spec_file,
spec_list: args.specs,
ignore_spec: args.ignore_spec,
create_blanks: args.create_blanks,
overwrite: args.overwrite,
max_workers: args.max_workers,
time_domain_mode: args.time_domain,
bounding_box: args.bounding_box,
invalidate_zeros: args.invalidate_zeros,
float_length: args.floating.as_deref().map(|s| s.parse().unwrap()),
progress: true,
};
let specs = combine_fits(&args.file_list, &args.out_cube, &options)
.context("combining FITS files into a cube")?;
info!("Written cube to {}", args.out_cube.display());
let unit = if args.time_domain { "s" } else { "Hz" };
let specs_file = args.out_cube.with_extension(format!("specs_{unit}.txt"));
let body: String = specs.iter().map(|v| format!("{v}\n")).collect::<String>();
std::fs::write(&specs_file, body)
.with_context(|| format!("writing spec values to {}", specs_file.display()))?;
info!("Written axis values to {}", specs_file.display());
Ok(())
}
fn run_extract(args: ExtractArgs) -> Result<()> {
init_logging(args.verbosity);
let options = ExtractOptions {
hdu_index: args.hdu_index,
channel_index: args.channel_index,
time_index: args.time_index,
overwrite: args.overwrite,
output_path: args.output_path,
};
let spin = fitscube_rs::progress::spinner("extracting plane from cube");
let out =
extract_plane_from_cube(&args.fits_cube, &options).context("extracting plane from cube")?;
spin.finish_and_clear();
info!("Written plane to {}", out.display());
Ok(())
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Combine(args) => run_combine(args),
Command::Extract(args) => run_extract(args),
}
}