Expand description
§Guide
A task-by-task walkthrough of skymath, planning a night on M31 (RA
00:42:44.3, Dec +41:16:09) from a site at 52°N, 4°E. Each snippet below is
copy-paste runnable on its own. The full sequence — assembled into one
program — is examples/plan_night.rs; run
cargo run --example plan_night to see it end to end.
Add the crate first:
cargo add skymath time§Parse a site and a target
Location::parse
accepts the FITS SITELAT/SITELONG sexagesimal shape directly.
Equatorial::parse_j2000
does the same for a catalogue RA/Dec, with
ParseMode::Strict
requiring all three sexagesimal fields.
use skymath::{Equatorial, Location, ParseMode, SexaStyle};
fn main() -> skymath::Result<()> {
let site = Location::parse("+52 05 32", "+004 18 27", 6.0)?;
let m31 = Equatorial::parse_j2000("00:42:44.3", "+41:16:09", ParseMode::Strict)?;
println!(
"Site {:.4}°N {:.4}°E, {:.0} m",
site.latitude().degrees(),
site.longitude().degrees(),
site.elevation_m()
);
println!(
"M31 {} {}",
m31.ra_sexagesimal(SexaStyle::default()),
m31.dec_sexagesimal(SexaStyle::default())
);
Ok(())
}§Which constellation is it in?
constellation
walks the IAU boundary table and returns a typed
Constellation.
use skymath::{constellation, Equatorial, ParseMode};
fn main() -> skymath::Result<()> {
let m31 = Equatorial::parse_j2000("00:42:44.3", "+41:16:09", ParseMode::Strict)?;
let con = constellation(m31);
println!("M31 is in {con} ({})", con.abbreviation());
Ok(())
}§Precess to tonight
Catalogue coordinates are J2000; observer-local quantities need the position
at the epoch of the observation. julian_epoch_of
turns an instant into that epoch, and precess
moves the coordinate there.
use skymath::{julian_epoch_of, precess, Equatorial, ParseMode, SexaStyle};
use time::OffsetDateTime;
fn main() -> skymath::Result<()> {
let m31 = Equatorial::parse_j2000("00:42:44.3", "+41:16:09", ParseMode::Strict)?;
let tonight = julian_epoch_of(OffsetDateTime::now_utc());
let of_date = precess(m31, tonight);
println!(
"M31 {tonight:?}: {} {}",
of_date.ra_sexagesimal(SexaStyle::default()),
of_date.dec_sexagesimal(SexaStyle::default())
);
Ok(())
}§Sidereal time at the site
gmst is
Greenwich Mean Sidereal Time; lst
adds the site’s east-positive longitude.
use skymath::{gmst, lst, Location};
use time::OffsetDateTime;
fn main() -> skymath::Result<()> {
let site = Location::parse("+52 05 32", "+004 18 27", 6.0)?;
let now = OffsetDateTime::now_utc();
println!("GMST {:.5} h", gmst(now).hours());
println!("LST {:.5} h", lst(now, site.longitude()).hours());
Ok(())
}§Alt/az, airmass, and parallactic angle
alt_az gives the
target’s horizontal position; airmass
errors once the target is well below the horizon (< −1°) rather than returning
a nonsensical number.
parallactic_angle
is the position angle of the zenith, useful for planning field rotation.
use skymath::{airmass, alt_az, julian_epoch_of, parallactic_angle, precess, Equatorial, Location, ParseMode};
use time::OffsetDateTime;
fn main() -> skymath::Result<()> {
let site = Location::parse("+52 05 32", "+004 18 27", 6.0)?;
let m31 = Equatorial::parse_j2000("00:42:44.3", "+41:16:09", ParseMode::Strict)?;
let now = OffsetDateTime::now_utc();
let of_date = precess(m31, julian_epoch_of(now));
let h = alt_az(of_date, now, &site);
println!("Alt/Az {:.2}° / {:.2}°", h.altitude.degrees(), h.azimuth.degrees());
match airmass(h.altitude) {
Ok(x) => println!("Airmass {x:.3}"),
Err(_) => println!("Airmass n/a (below the horizon)"),
}
println!(
"Parallactic {:.1}°",
parallactic_angle(of_date, now, &site).degrees()
);
Ok(())
}§Transit and an altitude window
transit finds the
meridian crossing nearest a given instant.
altitude_crossings
returns a typed CrossingOutcome
so circumpolar and never-visible targets can’t be confused with a normal
rise/set pair.
use skymath::{altitude_crossings, julian_epoch_of, precess, transit, Angle, CrossingOutcome, Equatorial, Location, ParseMode};
use time::OffsetDateTime;
fn main() -> skymath::Result<()> {
let site = Location::parse("+52 05 32", "+004 18 27", 6.0)?;
let m31 = Equatorial::parse_j2000("00:42:44.3", "+41:16:09", ParseMode::Strict)?;
let now = OffsetDateTime::now_utc();
let of_date = precess(m31, julian_epoch_of(now));
println!("Transit {} (UTC)", transit(of_date, now, &site));
match altitude_crossings(of_date, Angle::from_degrees(30.0), now, &site) {
CrossingOutcome::AlwaysAbove => println!("30° window: always above"),
CrossingOutcome::NeverAbove => println!("30° window: never above"),
CrossingOutcome::Crosses { rise, set } => {
println!("30° window: {rise} → {set} (UTC)");
}
}
Ok(())
}§Is it dark enough?
twilight solves
for dusk and dawn at a chosen Twilight
level, returning a typed TwilightOutcome
that distinguishes a normal dark window from a summer night that never gets
dark or a polar night that never brightens.
use skymath::{twilight, Location, Twilight, TwilightOutcome};
use time::OffsetDateTime;
fn main() -> skymath::Result<()> {
let site = Location::parse("+52 05 32", "+004 18 27", 6.0)?;
let now = OffsetDateTime::now_utc();
match twilight(Twilight::Astronomical, now, &site) {
TwilightOutcome::Night { dusk, dawn } => println!("Dark sky {dusk} → {dawn} (UTC)"),
TwilightOutcome::NeverDark => println!("Never astronomically dark tonight"),
TwilightOutcome::AlwaysDark => println!("Dark around the clock"),
}
Ok(())
}§How close is the Moon?
lunar_separation
corrects for topocentric parallax internally, so the separation is accurate
for the observing site, not just the Earth’s centre.
moon_illumination
gives the illuminated fraction of the disk.
use skymath::{lunar_separation, moon_illumination, Equatorial, Location, ParseMode};
use time::OffsetDateTime;
fn main() -> skymath::Result<()> {
let site = Location::parse("+52 05 32", "+004 18 27", 6.0)?;
let m31 = Equatorial::parse_j2000("00:42:44.3", "+41:16:09", ParseMode::Strict)?;
let now = OffsetDateTime::now_utc();
println!(
"Moon {:.1}° from M31, {:.0}% illuminated",
lunar_separation(m31, now, &site).degrees(),
moon_illumination(now) * 100.0
);
Ok(())
}