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}