target-match 0.1.0

Pure-Rust, dependency-light target identification for astrophotography: given a telescope pointing and field of view, rank which catalogued sky objects fall on the frame — by sky position, never by name.
Documentation

target-match

A pure-Rust library that identifies which catalogued sky object a telescope frame captured — from where the scope pointed and how much sky the frame covers.

  • Coordinates, never names — matching is done purely by sky position. A frame's OBJECT string (written inconsistently by capture software) is never a search key; a designation may ride along on a result for display only.
  • Catalog-agnostic — the crate owns no catalogue data and does no I/O. You bring your own objects (a database, a file, a SIMBAD resolver, a hand-built list) by implementing one small trait; target-match does the geometry.
  • Pure Rust, dependency-light — one small runtime dependency (thiserror), MSVC-safe. Optional off-by-default serde.
  • Flexible inputs — pointing and distances in decimal degrees or sexagesimal (HH:MM:SS / ±DD:MM:SS); field of view from optics (focal length, pixel size x/y, binning x/y, sensor pixels), from a pixel scale, or given directly.

Status

Implemented and tested, extracted from the nightwatch-astro/alm targeting pipeline. The angle, optics, and matcher modules are complete; the specification lives under specs/001-target-match-core/ (SpecKit). See examples/identify.rs for a runnable demo.

Usage

use target_match::{rank, Angle, Constraint, Equatorial, Field, Optics, RadiusPolicy, SkyObject};

// Your catalogue type — target-match owns no catalogue data, and never reads the name.
struct Target { name: &'static str, ra_deg: f64, dec_deg: f64 }
impl SkyObject for Target {
    fn position(&self) -> Equatorial {
        Equatorial::j2000(Angle::from_degrees(self.ra_deg), Angle::from_degrees(self.dec_deg)).unwrap()
    }
}

let catalog = [
    Target { name: "M 31", ra_deg: 10.6847, dec_deg: 41.2688 },
    Target { name: "M 33", ra_deg: 23.4621, dec_deg: 30.6599 },
];

// Where the scope pointed (decimal degrees or sexagesimal)...
let pointing = Equatorial::parse_j2000("00:42:44.3", "+41:16:09").unwrap();

// ...and how much sky the frame covers (from optics, a pixel scale, or a direct FOV).
let field = Field::from_optics(Optics {
    focal_mm: 800.0, pixel_um: (3.76, 3.76), binning: (1, 1), pixels: (6248, 4176),
}).unwrap();

// Which catalogued object the frame captured, nearest first.
let hits = rank(pointing, &catalog, Constraint::within(&field, RadiusPolicy::Circumscribed).nearest_one());
assert_eq!(hits[0].object.name, "M 31");

For a batch of frames against one catalogue, build the index once with Matcher::from_objects(..) and call .query(pointing, constraint) repeatedly. Rectangular in-frame membership (with optional camera rotation) and JNow→J2000 precession are supported; see the module docs.

Features

  • serde (off by default) — derive Serialize/Deserialize on the public coordinate and match types. Enable with target-match = { version = "…", features = ["serde"] }.

Development

Requires a stable Rust toolchain (pinned via rust-toolchain.toml) and, optionally, just.

just verify   # fmt-check + clippy (-D warnings) + tests
just test
just doc

License

Licensed under the Apache License, Version 2.0.