mod analyze_linkages;
mod analyze_observations;
mod common;
use std::process::ExitCode;
use anyhow::Result;
use clap::{Parser, Subcommand};
use common::RunContext;
#[derive(Parser)]
#[command(
name = "difi",
version,
about = "Did I Find It? — linkage completeness and purity for astronomical surveys",
long_about = "difi evaluates which objects in a survey are findable (CIFI) and \
classifies linkages as pure, contaminated, or mixed (DIFI)."
)]
struct Cli {
#[arg(long, global = true)]
threads: Option<usize>,
#[arg(long, global = true)]
progress_json: bool,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
#[command(alias = "cifi")]
AnalyzeObservations(analyze_observations::Args),
#[command(alias = "analyze")]
AnalyzeLinkages(analyze_linkages::Args),
}
fn main() -> ExitCode {
let cli = Cli::parse();
if cli.progress_json {
install_progress_json_panic_hook();
}
if let Some(n) = cli.threads {
if let Err(e) = rayon::ThreadPoolBuilder::new()
.num_threads(n)
.build_global()
{
eprintln!("difi: error: failed to configure rayon thread pool: {e}");
return ExitCode::from(1);
}
}
let ctx = RunContext::new(cli.progress_json, common::argv());
let result = match cli.command {
Command::AnalyzeObservations(args) => analyze_observations::run(args, &ctx),
Command::AnalyzeLinkages(args) => analyze_linkages::run(args, &ctx),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
ctx.emit_error(&e);
ExitCode::from(exit_code_for(&e))
}
}
}
fn exit_code_for(err: &anyhow::Error) -> u8 {
for cause in err.chain() {
if let Some(e) = cause.downcast_ref::<difi::error::Error>() {
return match e {
difi::error::Error::InvalidInput(_)
| difi::error::Error::Parquet(_)
| difi::error::Error::Arrow(_) => 3,
difi::error::Error::Io(_) => 2,
};
}
}
for cause in err.chain() {
if cause.downcast_ref::<std::io::Error>().is_some() {
return 2;
}
}
1
}
fn install_progress_json_panic_hook() {
use std::io::Write;
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let payload = if let Some(s) = info.payload().downcast_ref::<&str>() {
(*s).to_string()
} else if let Some(s) = info.payload().downcast_ref::<String>() {
s.clone()
} else {
"panic with non-string payload".to_string()
};
let location = info
.location()
.map(|l| format!(" at {}:{}", l.file(), l.line()))
.unwrap_or_default();
let event = serde_json::json!({
"event": "error",
"message": format!("panicked: {payload}{location}"),
"ts_unix_s": common::now_unix_s(),
});
let _ = writeln!(std::io::stdout(), "{event}");
default_hook(info);
}));
}
const _: fn() -> Result<()> = || Ok(());