target-match 0.5.0

Given a telescope pointing and field of view, rank which catalogued sky objects fall on the frame. Matches by sky position; holds no catalogue data and does no I/O.
Documentation
# target-match

[![CI](https://github.com/nightwatch-astro/target-match/actions/workflows/ci.yml/badge.svg)](https://github.com/nightwatch-astro/target-match/actions/workflows/ci.yml)
[![crates.io](https://img.shields.io/crates/v/target-match.svg)](https://crates.io/crates/target-match)
[![docs.rs](https://img.shields.io/docsrs/target-match)](https://docs.rs/target-match)

Rust library that identifies which catalogued sky objects a telescope frame
covers, given a pointing (right ascension / declination) and a field of view.

Matching is by angular position; object names are never used as search keys (a
designation can be carried on a result for display). The crate holds no
catalogue data and performs no I/O: callers supply objects by implementing the
one-method [`SkyObject`](https://docs.rs/target-match/latest/target_match/trait.SkyObject.html)
trait, and the library computes the geometry — angular separation, in-frame
[`Membership`](https://docs.rs/target-match/latest/target_match/enum.Membership.html)
(a fast circular radius that approximates the frame, or the exact rectangle,
with optional camera rotation), tangent-plane
[`Offset`](https://docs.rs/target-match/latest/target_match/struct.Offset.html)s,
and deterministic ranking.

The field of view can be computed from
[`Optics`](https://docs.rs/target-match/latest/target_match/struct.Optics.html)
(focal length, pixel size, binning, sensor dimensions), from a pixel scale, or
supplied directly via
[`Field`](https://docs.rs/target-match/latest/target_match/struct.Field.html).
Each `Field` constructor is fallible: non-positive or non-finite inputs return
[`Error::InvalidOptics`](https://docs.rs/target-match/latest/target_match/enum.Error.html)
(the snippets below `.unwrap()` on known-good values). Matching itself is
infallible once inputs are valid. Pointings at any epoch are precessed to J2000
before matching.

Coordinate and angle types come from the
[`skymath`](https://github.com/nightwatch-astro/skymath) crate and appear
directly in this crate's API (`skymath::Equatorial`, `skymath::Angle` — with
decimal and strict/lenient sexagesimal parsing); `skymath` is re-exported as
`target_match::skymath` so a version-matched copy is always available.

Captured-image geometry uses `SkyFootprint` values built from a solved centre,
ordered sky boundary, solved sky position angle, image parity, and evidence
identity. Pair comparison reports intersection over the smaller footprint,
centre separation relative to the smaller diagonal, transported sky-axis
rotation, and parity. `FootprintUnion` preserves disconnected captured regions
and interior gaps for point, footprint, and sampled-ellipse coverage queries.
The crate reports measurements and does not apply session or mosaic policy.

API documentation, generated from the source on every release:
[docs.rs/target-match](https://docs.rs/target-match). For a task-oriented
walkthrough, see the
[`guide`](https://docs.rs/target-match/latest/target_match/guide/index.html) module.

## Usage

```toml
[dependencies]
target-match = "0.4"
```

```rust
use skymath::{Angle, Equatorial, ParseMode};
use target_match::{rank, Constraint, 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", ParseMode::Strict).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();

// Nearest catalogued object within the frame's search radius. `within` uses a
// circular approximation (here the circumscribed circle, half the diagonal); for
// the exact sensor rectangle use `Constraint::frame(&field)`.
let hits = rank(pointing, &catalog, Constraint::within(&field, RadiusPolicy::Circumscribed).nearest_one());
assert_eq!(hits[0].object.name, "M 31");
```

Each hit is a
[`Match`](https://docs.rs/target-match/latest/target_match/struct.Match.html)
carrying the borrowed object, its separation, in-frame flag, offset, and
position angle. The
[`Query`](https://docs.rs/target-match/latest/target_match/enum.Query.html)
mode on the constraint selects all-within-field, nearest-one, or nearest-N.
To test a single object without ranking a catalogue, call
[`is_framed`](https://docs.rs/target-match/latest/target_match/fn.is_framed.html).

For a batch of frames against one catalogue, build the index once with
[`Matcher::from_objects`](https://docs.rs/target-match/latest/target_match/struct.Matcher.html#method.from_objects)`(..)`
and call
[`.query(pointing, constraint)`](https://docs.rs/target-match/latest/target_match/struct.Matcher.html#method.query)
repeatedly. See
[`examples/identify.rs`](https://github.com/nightwatch-astro/target-match/blob/main/examples/identify.rs)
for a runnable end-to-end demo, and the
[`guide`](https://docs.rs/target-match/latest/target_match/guide/index.html)
module for a task-oriented walkthrough.

## Features

- `serde` *(off by default)* — derives `Serialize`/`Deserialize` on the public
  match types, and forwards to `skymath/serde` for the coordinate types they
  embed.

## Development

Requires a stable Rust toolchain (pinned via `rust-toolchain.toml`) and, optionally,
[`just`](https://github.com/casey/just).

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

## License

[![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)

This project is licensed under the Mozilla Public License 2.0 — see [LICENSE](./LICENSE) for details.

You can use this library in closed-source projects. If you modify any of the source files in this library, the modified files must be made available under the MPL-2.0 when distributed.