Skip to main content

deep_time/sidereal/
mod.rs

1//! Prime-meridian / spin-angle clocks, plus Earth equinox sidereal time.
2//!
3//! [`Sidereal`] is a planet-agnostic prime-meridian clock with presets
4//! [`Sidereal::EARTH`], [`Sidereal::MARS`], and [`Sidereal::MOON`]. It
5//! evaluates rotation angle and local meridian angle from a linear spin model.
6//!
7//! On Earth that angle is the Earth Rotation Angle (ERA, CIO origin). Hour
8//! angle uses the same frame as the angle (`HA = local meridian − RA`).
9//!
10//! **Earth equinox sidereal time** (GMST, GAST, LMST, LAST) is not a generic
11//! mode of this clock. It is an Earth-only readout of ERA via the IAU 2000/2006
12//! Equation of the Origins / Equinoxes (`sidereal-earth`):
13//! [`Sidereal::gmst`](struct.Sidereal.html#method.gmst),
14//! [`Sidereal::gast`](struct.Sidereal.html#method.gast),
15//! [`Sidereal::lmst`](struct.Sidereal.html#method.lmst),
16//! [`Sidereal::last`](struct.Sidereal.html#method.last) (see [`earth`]).
17
18/// ERFA Earth equation of the origins / equinoxes (`sidereal-earth` feature).
19#[cfg(feature = "sidereal-earth")]
20pub mod earth_eo_ee;
21
22/// Earth equinox sidereal time: GMST, GAST, LMST, LAST (`sidereal-earth`).
23#[cfg(feature = "sidereal-earth")]
24pub mod earth;
25
26use crate::Real;
27use core::f64::consts::TAU;
28
29/// Wrap an angle into `[0, 2π)`.
30#[inline]
31const fn wrap_angle(angle: Real) -> Real {
32    ((angle % TAU) + TAU) % TAU
33}
34
35/// Prime-meridian / spin-angle clock for a rotating body.
36///
37/// The model is linear in time:
38///
39/// ```text
40/// angle(t) = ref_angle + rate × (t − ref_epoch) + correction
41/// ```
42///
43/// plus optional observer longitude for local meridian angle. For Earth that
44/// is the Earth Rotation Angle (ERA). For other bodies it is only a simple
45/// mean spin / meridian angle if you supply a rate — not a full orientation
46/// ephemeris (the Moon’s librations, for example, are not included).
47///
48/// **Earth.** [`Sidereal::EARTH`] uses the IAU 2000 Earth Rotation Angle
49/// relative to the Celestial Intermediate Origin (CIO). Equinox sidereal times
50/// (`sidereal-earth`) are on this type:
51/// [`Sidereal::gmst`](struct.Sidereal.html#method.gmst),
52/// [`Sidereal::gast`](struct.Sidereal.html#method.gast),
53/// [`Sidereal::lmst`](struct.Sidereal.html#method.lmst),
54/// [`Sidereal::last`](struct.Sidereal.html#method.last).
55///
56/// **Other bodies.** Supply a published spin rate and reference angle (for
57/// example IAU WGCCRE `Ẇ` / `W0`), or start from the simplified
58/// [`Sidereal::MARS`] / [`Sidereal::MOON`] presets. Use
59/// [`rotation_angle`](Self::rotation_angle) /
60/// [`local_rotation_angle`](Self::local_rotation_angle).
61///
62/// Local meridian angle is the usual input to hour angle
63/// (`HA = local meridian − RA`), meridian transit, and horizon ↔ equatorial
64/// conversions. Meridian angle and `RA` must share the same equatorial frame
65/// (CIO/CIRS with local ERA; mean or true equinox with LMST/LAST).
66///
67/// ## Fields
68///
69/// * `rate_rad_per_sec` — Sidereal rotation rate in radians per SI second.
70/// * `ref_epoch` — Reference epoch as an MJD at which `ref_angle_rad` is defined.
71///   For Earth ERA this is a **UT1** MJD.
72/// * `ref_angle_rad` — Rotation angle of the prime meridian at `ref_epoch`.
73/// * `longitude_rad` — Observer longitude on the body (radians, east positive).
74///   `0.0` corresponds to the body's prime meridian.
75/// * `correction_rad` — Optional additive angle (radians) folded into
76///   [`rotation_angle`](Self::rotation_angle). Do **not** use this for DUT1;
77///   put UT1 in the time argument instead.
78///
79/// ## Examples
80///
81/// Earth ERA from UTC via IERS C04 (needs `eop` and `std`). Equinox sidereal
82/// time needs `sidereal-earth` as well — see
83/// [`Sidereal::gmst`](struct.Sidereal.html#method.gmst).
84///
85/// ```rust
86/// # #[cfg(all(feature = "eop", feature = "std"))] {
87/// use deep_time::{Dt, Scale, Sidereal};
88/// use deep_time::eop::{EopData, EopFormat, Separator};
89///
90/// let eop = EopData::from_text_file(
91///     "tests/assets/EOP_20u24_C04_one_file_1962-now.txt",
92///     EopFormat::C04,
93///     Separator::Whitespace,
94/// ).unwrap();
95///
96/// let utc = Dt::from_mjd_f(56879.0, Scale::UTC);
97/// let mjd_ut1 = utc.to_ut1(&eop).unwrap().to_mjd_f_raw();
98///
99/// let mut earth = Sidereal::EARTH;
100/// earth.longitude_rad = 0.0; // Greenwich
101///
102/// let era = earth.rotation_angle(mjd_ut1);
103/// let local_era = earth.local_rotation_angle(mjd_ut1);
104/// let _ = (era, local_era);
105/// # }
106/// ```
107#[derive(Clone, Debug, PartialEq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
110#[cfg_attr(feature = "defmt", derive(defmt::Format))]
111pub struct Sidereal {
112    /// Sidereal rotation rate in **radians per SI second**.
113    pub rate_rad_per_sec: Real,
114    /// Reference epoch as an MJD (UT1 for Earth ERA).
115    pub ref_epoch: Real,
116    /// Rotation angle of the prime meridian (radians) at `ref_epoch`.
117    pub ref_angle_rad: Real,
118    /// Longitude of the observer on the body (radians, east positive).
119    /// `0.0` = body's prime meridian.
120    pub longitude_rad: Real,
121    /// Optional additive angle (radians) applied inside [`Self::rotation_angle`].
122    /// Not a substitute for DUT1 — pass UT1 via the time argument instead.
123    pub correction_rad: Real,
124}
125
126impl Sidereal {
127    /// Pre-configured `Sidereal` for Earth using the IAU 2000 ERA.
128    ///
129    /// This uses:
130    /// - The IAU 2000 Earth Rotation Angle rate
131    ///   (`1.00273781191135448` turns per UT1 day).
132    /// - J2000.0 as the reference epoch (`ref_epoch = 51544.5` UT1 MJD).
133    /// - The Earth Rotation Angle (ERA) at J2000.0 as `ref_angle_rad`.
134    ///
135    /// You can still customize fields after construction (e.g. `longitude_rad`
136    /// or `correction_rad`). For GMST/GAST/LMST/LAST see
137    /// [`Sidereal::gmst`](struct.Sidereal.html#method.gmst)
138    /// (`sidereal-earth`).
139    pub const EARTH: Self = Self {
140        rate_rad_per_sec: (1.00273781191135448 * core::f64::consts::TAU) / 86400.0,
141        ref_epoch: 51544.5,
142        ref_angle_rad: 0.7790572732640 * core::f64::consts::TAU,
143        longitude_rad: 0.0,
144        correction_rad: 0.0,
145    };
146
147    /// Simplified `Sidereal` preset for Mars (mean spin rate only).
148    ///
149    /// Uses an approximate mean sidereal rotation period and J2000.0 as the
150    /// reference epoch. `ref_angle_rad` is `0.0` (not a published prime-meridian
151    /// offset). Suitable for demos and coarse meridian/spin geometry — not a
152    /// full IAU WGCCRE orientation series. For higher fidelity, replace the
153    /// rate and reference angle with published values.
154    ///
155    /// You can customize fields (especially `longitude_rad`) after construction.
156    pub const MARS: Self = Self {
157        rate_rad_per_sec: core::f64::consts::TAU / 88642.663,
158        ref_epoch: 51544.5,
159        ref_angle_rad: 0.0,
160        longitude_rad: 0.0,
161        correction_rad: 0.0,
162    };
163
164    /// Simplified `Sidereal` preset for the Moon (mean spin rate only).
165    ///
166    /// Uses an approximate mean sidereal rotation period and J2000.0 as the
167    /// reference epoch. `ref_angle_rad` is `0.0` (not a published prime-meridian
168    /// offset). Useful for coarse work; precise selenographic orientation needs
169    /// lunar librations, which this preset does not include.
170    ///
171    /// You can customize fields (especially `longitude_rad`) after construction.
172    pub const MOON: Self = Self {
173        rate_rad_per_sec: core::f64::consts::TAU / 2_360_591.424,
174        ref_epoch: 51544.5,
175        ref_angle_rad: 0.0,
176        longitude_rad: 0.0,
177        correction_rad: 0.0,
178    };
179
180    /// Convert a meridian / sidereal angle in radians to seconds on a 24-hour
181    /// sidereal clock, wrapped to `[0, 86400)`.
182    ///
183    /// This is `(angle / 2π) × 86400` — an hour-angle clock, not SI seconds of a
184    /// sidereal day.
185    ///
186    /// ## Examples
187    ///
188    /// ```
189    /// use core::f64::consts::PI;
190    /// use deep_time::Sidereal;
191    ///
192    /// assert!((Sidereal::to_sec(PI) - 43_200.0).abs() < 1e-9);
193    /// ```
194    #[inline]
195    pub const fn to_sec(angle_rad: Real) -> Real {
196        let fraction = ((angle_rad / TAU) % 1.0 + 1.0) % 1.0;
197        fraction * 86400.0
198    }
199
200    /// Returns the instantaneous rotation angle of the body's prime meridian
201    /// (in radians) at the given instant, normalized to `[0, 2π)`.
202    ///
203    /// For Earth this is the IAU 2000 Earth Rotation Angle (ERA) relative to the
204    /// Celestial Intermediate Origin (CIO) — the same definition as ERFA `era00`.
205    /// `mjd` is **UT1** MJD for Earth ERA.
206    /// It does **not** include observer longitude or the Equation of the Origins.
207    ///
208    /// ## Examples
209    ///
210    /// ```rust
211    /// # #[cfg(all(feature = "eop", feature = "std"))] {
212    /// use deep_time::{Dt, Scale, Sidereal};
213    /// use deep_time::eop::{EopData, EopFormat, Separator};
214    ///
215    /// let eop = EopData::from_text_file(
216    ///     "tests/assets/EOP_20u24_C04_one_file_1962-now.txt",
217    ///     EopFormat::C04,
218    ///     Separator::Whitespace,
219    /// ).unwrap();
220    /// let utc = Dt::from_mjd_f(57753.5, Scale::UTC);
221    /// let mjd_ut1 = utc.to_ut1(&eop).unwrap().to_mjd_f_raw();
222    ///
223    /// let era = Sidereal::EARTH.rotation_angle(mjd_ut1);
224    /// let _ = era;
225    /// # }
226    /// ```
227    pub const fn rotation_angle(&self, mjd: Real) -> Real {
228        // elapsed time in seconds between ref_epoch (MJD) and the given mjd
229        let elapsed_days = mjd - self.ref_epoch;
230        let elapsed_sec = elapsed_days * 86400.0;
231
232        let angle = self.ref_angle_rad + self.rate_rad_per_sec * elapsed_sec + self.correction_rad;
233
234        wrap_angle(angle)
235    }
236
237    /// Returns the rotation angle of the prime meridian at the observer's
238    /// longitude, normalized to `[0, 2π)`.
239    ///
240    /// This is equivalent to `rotation_angle(mjd) + self.longitude_rad`.
241    /// For Earth with [`Sidereal::EARTH`], that is the local ERA: the angle
242    /// between the Celestial Intermediate Origin (CIO) and the observer’s
243    /// local meridian.
244    ///
245    /// Hour angle of a source:
246    ///
247    /// ```text
248    /// HA = local_rotation_angle(mjd) − RA
249    /// ```
250    ///
251    /// Use a right ascension in the **same** frame as this angle (CIO/CIRS RA
252    /// with local ERA; equinox RA with LMST/LAST).
253    ///
254    /// ## Examples
255    ///
256    /// ```rust
257    /// # #[cfg(all(feature = "eop", feature = "std"))] {
258    /// use deep_time::{Dt, Scale, Sidereal};
259    /// use deep_time::eop::{EopData, EopFormat, Separator};
260    ///
261    /// let eop = EopData::from_text_file(
262    ///     "tests/assets/EOP_20u24_C04_one_file_1962-now.txt",
263    ///     EopFormat::C04,
264    ///     Separator::Whitespace,
265    /// ).unwrap();
266    /// let utc = Dt::from_mjd_f(56879.0, Scale::UTC);
267    /// let mjd_ut1 = utc.to_ut1(&eop).unwrap().to_mjd_f_raw();
268    ///
269    /// let mut earth = Sidereal::EARTH;
270    /// earth.longitude_rad = 0.0; // Greenwich
271    /// let local_era = earth.local_rotation_angle(mjd_ut1);
272    /// let _ = local_era;
273    /// # }
274    /// ```
275    #[inline]
276    pub const fn local_rotation_angle(&self, mjd: Real) -> Real {
277        wrap_angle(self.rotation_angle(mjd) + self.longitude_rad)
278    }
279}