use siderust::bodies::solar_system::{Moon, Sun};
use siderust::coordinates::centers::Geodetic;
use siderust::coordinates::frames::ECEF;
use siderust::coordinates::spherical::direction;
use siderust::event::altitude::{
above_threshold, altitude_ranges, below_threshold, crossings, culminations,
AltitudePeriodsProvider, CrossingDirection, CulminationKind, SearchOpts,
};
use siderust::time::{Interval, ModifiedJulianDate};
use siderust::qtty::*;
fn roque() -> Geodetic<ECEF> {
Geodetic::<ECEF>::new(
Degrees::new(-17.892),
Degrees::new(28.762),
Quantity::<Meter>::new(2396.0),
)
}
fn greenwich() -> Geodetic<ECEF> {
Geodetic::<ECEF>::new(
Degrees::new(0.0),
Degrees::new(51.4769),
Quantity::<Meter>::new(0.0),
)
}
#[test]
fn altitude_at_sun_j2000_greenwich() {
let alt = Sun.altitude_at(
&greenwich(),
siderust::time::ModifiedJulianDate::new(51544.5),
);
assert!(
alt.abs().value() < std::f64::consts::FRAC_PI_2,
"Sun altitude out of range: {} rad",
alt.value()
);
}
#[test]
fn altitude_at_moon_j2000_greenwich() {
let alt = Moon.altitude_at(
&greenwich(),
siderust::time::ModifiedJulianDate::new(51544.5),
);
assert!(alt.abs().value() < std::f64::consts::FRAC_PI_2);
}
#[test]
fn altitude_at_sirius_reasonable() {
let sirius = direction::ICRS::new(Degrees::new(101.287), Degrees::new(-16.716));
let alt = sirius.altitude_at(
&greenwich(),
siderust::time::ModifiedJulianDate::new(51544.5),
);
assert!(alt.abs().value() < std::f64::consts::FRAC_PI_2);
}
#[test]
fn crossings_sun_one_day_greenwich() {
let site = greenwich();
let window = Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60001.0)).unwrap(),
);
let events = crossings(
&Sun,
&site,
window,
Degrees::new(0.0),
SearchOpts::default(),
);
let rises: Vec<_> = events
.iter()
.filter(|e| e.direction == CrossingDirection::Rising)
.collect();
let sets: Vec<_> = events
.iter()
.filter(|e| e.direction == CrossingDirection::Setting)
.collect();
assert_eq!(rises.len(), 1, "expect 1 sunrise in 24h at 51°N");
assert_eq!(sets.len(), 1, "expect 1 sunset in 24h at 51°N");
assert!(rises[0].mjd < sets[0].mjd);
}
#[test]
fn crossings_sun_astronomical_twilight() {
let site = roque();
let window = Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60001.0)).unwrap(),
);
let events = crossings(
&Sun,
&site,
window,
Degrees::new(-18.0),
SearchOpts::default(),
);
assert!(
!events.is_empty(),
"should find astronomical twilight crossings"
);
}
#[test]
fn culminations_sun_one_day() {
let site = greenwich();
let window = Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60001.0)).unwrap(),
);
let culms = culminations(&Sun, &site, window, SearchOpts::default());
let upper: Vec<_> = culms
.iter()
.filter(|c| c.kind == CulminationKind::Max)
.collect();
let lower: Vec<_> = culms
.iter()
.filter(|c| c.kind == CulminationKind::Min)
.collect();
assert!(!upper.is_empty(), "should find upper culmination");
assert!(!lower.is_empty(), "should find lower culmination");
let max_alt = upper[0].altitude.value();
assert!(
max_alt > 10.0 && max_alt < 50.0,
"upper culmination altitude {} should be between 10-50° at 51°N in winter",
max_alt
);
}
#[test]
fn culminations_moon_one_day() {
let site = greenwich();
let window = Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60001.0)).unwrap(),
);
let culms = culminations(&Moon, &site, window, SearchOpts::default());
assert!(!culms.is_empty(), "should find Moon culminations in 24h");
}
#[test]
fn above_threshold_sun_week() {
let site = roque();
let window = Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60007.0)).unwrap(),
);
let days = above_threshold(
&Sun,
&site,
window,
Degrees::new(0.0),
SearchOpts::default(),
);
assert_eq!(
days.len(),
7,
"should find 7 daytime periods in a week, found {}",
days.len()
);
for (i, p) in days.iter().enumerate() {
let hours = ((p).end.raw() - (p).start.raw()) * 24.0;
assert!(
hours.value() > 8.0 && hours.value() < 16.0,
"day {} duration {} hours is unreasonable",
i,
hours
);
}
}
#[test]
fn below_threshold_astronomical_night_week() {
let site = roque();
let window = Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60007.0)).unwrap(),
);
let nights = below_threshold(
&Sun,
&site,
window,
Degrees::new(-18.0),
SearchOpts::default(),
);
assert!(!nights.is_empty(), "should find astronomical night periods");
for p in &nights {
let hours = ((p).end.raw() - (p).start.raw()) * 24.0;
assert!(
hours.value() > 3.0,
"night period is too short: {} hours",
hours
);
}
}
#[test]
fn altitude_ranges_nautical_to_astro_twilight() {
let site = greenwich();
let window = Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60002.0)).unwrap(),
);
let bands = altitude_ranges(
&Sun,
&site,
window,
Degrees::new(-18.0),
Degrees::new(-12.0),
SearchOpts::default(),
);
assert!(
bands.len() >= 2,
"should find at least 2 nautical-astronomical twilight bands, found {}",
bands.len()
);
for p in &bands {
let minutes = ((p).end.raw() - (p).start.raw()) * 1440.0;
assert!(
minutes.value() > 10.0 && minutes.value() < 120.0,
"twilight band duration {} min is unreasonable",
minutes
);
}
}
#[test]
fn moon_above_horizon_week() {
let site = roque();
let window = Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60007.0)).unwrap(),
);
let periods = above_threshold(
&Moon,
&site,
window,
Degrees::new(0.0),
SearchOpts::default(),
);
assert!(!periods.is_empty(), "should find moon-up periods in 7 days");
}
#[test]
fn polaris_always_above_horizon_at_greenwich() {
let polaris = direction::ICRS::new(Degrees::new(37.95), Degrees::new(89.26));
let site = greenwich();
let window = Interval::new(
ModifiedJulianDate::try_new(Days::new(60000.0)).unwrap(),
ModifiedJulianDate::try_new(Days::new(60001.0)).unwrap(),
);
let events = crossings(
&polaris,
&site,
window,
Degrees::new(0.0),
SearchOpts::default(),
);
assert!(
events.is_empty(),
"Polaris should be circumpolar at 51.5°N, found {} crossings",
events.len()
);
let up = above_threshold(
&polaris,
&site,
window,
Degrees::new(0.0),
SearchOpts::default(),
);
assert_eq!(
up.len(),
1,
"Polaris should be continuously above horizon, found {} periods",
up.len()
);
let duration = (up[0]).end.raw() - (up[0]).start.raw();
assert!(
(duration - Days::new(1.0)).abs().value() < 0.01,
"Polaris up-period should span the full day, got {} days",
duration
);
}