Skip to main content

deep_time/sidereal/
mod.rs

1//! Sidereal rotation and time calculations for celestial bodies.
2//!
3//! [`Sidereal`] struct with ready-to-use `EARTH`, `MARS`, `MOON` constants.
4//! Computes rotation angle, LMST/LAST, GMST/GAST.
5//!
6//! With the `"sidereal-earth"` feature enabled a rust implementation of the
7//! ERFA Earth Equation of the Origins / Equinoxes are both available as well.
8
9#[cfg(feature = "sidereal-earth")]
10pub mod earth_eo_ee;
11
12use crate::Real;
13use core::f64::consts::TAU;
14
15#[cfg(feature = "sidereal-earth")]
16use earth_eo_ee::*;
17
18/// Represents the rotational state of a celestial body and provides
19/// methods to compute the orientation of its prime meridian at any
20/// given time.
21///
22/// The rotation angle of the prime meridian is the basis for
23/// calculating local sidereal time. Local sidereal time is required
24/// to compute the hour angle of a celestial object (HA = LST − RA),
25/// to determine when an object will cross the local meridian,
26/// to convert between horizon coordinates (altitude/azimuth) and
27/// equatorial coordinates, and to calculate accurate pointing
28/// directions for telescopes and spacecraft antennas.
29///
30/// The struct implements the modern CIO-based rotation model and
31/// works for any rotating body (Earth, Mars, the Moon, etc.) by
32/// supplying the appropriate rotation rate and reference values.
33///
34/// ## Fields
35///
36/// * `rate_rad_per_sec` — Mean sidereal rotation rate in radians per SI second.
37/// * `ref_epoch` — Reference epoch (MJD) at which `ref_angle_rad` is defined.
38/// * `ref_angle_rad` — Rotation angle of the prime meridian at `ref_epoch`.
39/// * `longitude_rad` — Observer longitude on the body (radians, east positive).
40///   `0.0` corresponds to the body's prime meridian.
41/// * `correction_rad` — General-purpose additive correction in radians.
42///
43/// ## Examples
44///
45/// Basic usage with Earth constants:
46///
47/// ```rust
48/// use deep_time::Sidereal;
49///
50/// let mut earth = Sidereal::EARTH;
51/// earth.longitude_rad = 0.0; // Greenwich
52///
53/// let mjd = 60000.0;
54/// let era = earth.rotation_angle(mjd);
55///
56/// // Local Mean Sidereal Time using the mean Equation of the Origins
57/// let eo_mean = earth.earth_eo_mean(mjd + 32.184 / 86400.0);
58/// let lmst = earth.local_sidereal_time_mean(mjd, eo_mean);
59/// ```
60///
61/// Realistic usage with DUT1 correction (UT1 time scale):
62///
63/// ```rust
64/// use deep_time::{Dt, Sidereal};
65/// use deep_time::eop::{EopData, EopFormat, Separator};
66///
67/// let eop = EopData::from_text_file(
68///     "finals.all.iau2000.txt",
69///     EopFormat::Finals2000A,
70///     Separator::Whitespace,
71/// ).unwrap();
72///
73/// let mjd_utc = 56879.0;
74/// let dut1 = Dt::mjd_to_eop_offset_f(mjd_utc, &eop).unwrap();
75/// let mjd_ut1 = mjd_utc + dut1 / 86400.0;
76///
77/// let earth = Sidereal::EARTH;
78///
79/// let era = earth.rotation_angle(mjd_ut1);
80///
81/// // Greenwich Mean Sidereal Time
82/// let eo_mean = earth.earth_eo_mean(mjd_ut1 + 32.184 / 86400.0);
83/// let gmst = earth.sidereal_angle_mean(mjd_ut1, eo_mean);
84///
85/// // Local Mean Sidereal Time
86/// let lmst = earth.local_sidereal_time_mean(mjd_ut1, eo_mean);
87/// ```
88#[derive(Debug, Clone, Copy)]
89pub struct Sidereal {
90    /// Mean sidereal rotation rate in **radians per SI second**.
91    pub rate_rad_per_sec: Real,
92    /// Reference epoch.
93    pub ref_epoch: Real,
94    /// Rotation angle of the prime meridian (radians) at `ref_epoch`.
95    pub ref_angle_rad: Real,
96    /// Longitude of the observer on the body (radians, east positive).
97    /// `0.0` = body's prime meridian.
98    pub longitude_rad: Real,
99    /// General scalar correction in radians.
100    pub correction_rad: Real,
101}
102
103impl Sidereal {
104    /// Pre-configured `Sidereal` for Earth using IAU 2000/2006 conventions.
105    ///
106    /// This uses:
107    /// - The conventional mean sidereal rotation rate of Earth.
108    /// - J2000.0 as the reference epoch (`ref_epoch = 51544.5`).
109    /// - The Earth Rotation Angle (ERA) at J2000.0 as `ref_angle_rad`.
110    ///
111    /// You can still customize fields after construction (e.g. `longitude_rad`
112    /// or `correction_rad`).
113    pub const EARTH: Self = Self {
114        rate_rad_per_sec: (1.00273781191135448 * core::f64::consts::TAU) / 86400.0,
115        ref_epoch: 51544.5,
116        ref_angle_rad: 0.7790572732640 * core::f64::consts::TAU,
117        longitude_rad: 0.0,
118        correction_rad: 0.0,
119    };
120
121    /// Pre-configured `Sidereal` for Mars.
122    ///
123    /// Uses a simplified mean sidereal rotation rate and J2000.0 as the
124    /// reference epoch. `ref_angle_rad` is set to zero (no specific
125    /// reference angle is defined).
126    ///
127    /// You can customize fields (especially `longitude_rad`) after construction.
128    pub const MARS: Self = Self {
129        rate_rad_per_sec: core::f64::consts::TAU / 88642.663,
130        ref_epoch: 51544.5,
131        ref_angle_rad: 0.0,
132        longitude_rad: 0.0,
133        correction_rad: 0.0,
134    };
135
136    /// Pre-configured `Sidereal` for the Moon.
137    ///
138    /// Uses a simplified mean sidereal rotation rate and J2000.0 as the
139    /// reference epoch. `ref_angle_rad` is set to zero (no specific
140    /// reference angle is defined).
141    ///
142    /// You can customize fields (especially `longitude_rad`) after construction.
143    pub const MOON: Self = Self {
144        rate_rad_per_sec: core::f64::consts::TAU / 2_360_591.424,
145        ref_epoch: 51544.5,
146        ref_angle_rad: 0.0,
147        longitude_rad: 0.0,
148        correction_rad: 0.0,
149    };
150
151    // Normalize to [0, 2π)
152    #[inline]
153    const fn normalize_angle(angle: Real) -> Real {
154        ((angle % TAU) + TAU) % TAU
155    }
156
157    /// Returns the instantaneous rotation angle of the body's prime meridian
158    /// (in radians) at the given instant, normalized to `[0, 2π)`.
159    ///
160    /// For Earth this is the pure Earth Rotation Angle (ERA) in the
161    /// Celestial Intermediate Origin (CIO) frame. It does **not** include
162    /// observer longitude or the Equation of the Origins.
163    ///
164    /// Matches Astropy's `Time.earth_rotation_angle(longitude=None)`
165    /// (or with `longitude=0`).
166    ///
167    /// ## Example
168    ///
169    /// ```rust
170    /// use deep_time::Sidereal;
171    ///
172    /// let era = Sidereal::EARTH.rotation_angle(57753.5);
173    /// ```
174    pub const fn rotation_angle(&self, mjd: Real) -> Real {
175        // elapsed time in seconds between ref_epoch (MJD) and the given mjd
176        let elapsed_days = mjd - self.ref_epoch;
177        let elapsed_sec = elapsed_days * 86400.0;
178
179        let angle = self.ref_angle_rad + self.rate_rad_per_sec * elapsed_sec + self.correction_rad;
180
181        Self::normalize_angle(angle)
182    }
183
184    /// Returns the rotation angle of the prime meridian at the observer's
185    /// longitude, normalized to `[0, 2π)`.
186    ///
187    /// This is equivalent to `rotation_angle(mjd) + self.longitude_rad`.
188    /// It gives the angle between the Celestial Intermediate Origin (CIO)
189    /// and the observer’s local meridian.
190    ///
191    /// This value is commonly used when computing the local hour angle
192    /// of a celestial object:
193    ///
194    /// ```text
195    /// HA = local_rotation_angle(mjd) - RA
196    /// ```
197    ///
198    /// ## Example
199    ///
200    /// ```rust
201    /// use deep_time::Sidereal;
202    ///
203    /// let mut earth = Sidereal::EARTH;
204    /// earth.longitude_rad = 0.0; // Greenwich
205    ///
206    /// let mjd = 60000.0;
207    /// let local_era = earth.local_rotation_angle(mjd);
208    /// ```
209    #[inline]
210    pub const fn local_rotation_angle(&self, mjd: Real) -> Real {
211        Self::normalize_angle(self.rotation_angle(mjd) + self.longitude_rad)
212    }
213
214    /// Returns the sidereal angle of the body's prime meridian in radians,
215    /// normalized to `[0, 2π)`.
216    ///
217    /// This computes Greenwich Mean Sidereal Time (GMST) when an appropriate
218    /// Equation of the Origins value is supplied.
219    ///
220    /// ## Parameters
221    ///
222    /// - `eo_rad`: The Equation of the Origins value to subtract from the
223    ///   Earth Rotation Angle (ERA).  
224    ///   - Pass `0.0` to get the pure CIO-based rotation angle (ERA).
225    ///   - Pass the **mean** Equation of the Origins (e.g. from
226    ///     [`earth_eo_mean`](Self::earth_eo_mean)) to obtain GMST.
227    ///
228    /// ## Details
229    ///
230    /// - When `eo_rad = 0.0`, the result is the modern Earth Rotation Angle (ERA)
231    ///   relative to the Celestial Intermediate Origin (CIO).
232    ///
233    /// - When `eo_rad` is the mean Equation of the Origins (i.e. the value that
234    ///   satisfies `GMST = ERA − eo_rad`), the result is Greenwich Mean Sidereal
235    ///   Time (GMST) referred to the mean equinox. This is the traditional
236    ///   equinox-based mean sidereal time.
237    ///
238    /// ## Example
239    ///
240    /// ```rust
241    /// use deep_time::Sidereal;
242    ///
243    /// let earth = Sidereal::EARTH;
244    /// let mjd = 60000.0;
245    ///
246    /// // Pure CIO-based rotation angle (Earth Rotation Angle)
247    /// let era = earth.sidereal_angle_mean(mjd, 0.0);
248    ///
249    /// // Traditional mean sidereal time using the mean Equation of the Origins
250    /// // convert to the mjd to tt if necessary
251    /// let eo_mean = earth.earth_eo_mean(mjd + 32.184 / 86400.0);
252    /// let gmst = earth.sidereal_angle_mean(mjd, eo_mean);
253    /// ```
254    #[inline]
255    pub const fn sidereal_angle_mean(&self, mjd: Real, eo_rad: Real) -> Real {
256        let angle = self.rotation_angle(mjd) - eo_rad;
257        Self::normalize_angle(angle)
258    }
259
260    /// Returns the local sidereal angle at the observer's longitude in radians,
261    /// normalized to `[0, 2π)`.
262    ///
263    /// This computes **Local Mean Sidereal Time (LMST)** when an appropriate
264    /// Equation of the Origins value is supplied.
265    ///
266    /// ## Parameters
267    ///
268    /// - `eo_rad`: The Equation of the Origins value to subtract from the
269    ///   Earth Rotation Angle (ERA).  
270    ///   - Pass `0.0` to get the pure local Earth Rotation Angle (CIO-based).
271    ///   - Pass the **mean** Equation of the Origins (e.g. from
272    ///     [`earth_eo_mean`](Self::earth_eo_mean)) to obtain Local Mean
273    ///     Sidereal Time (LMST).
274    ///
275    /// ## Details
276    ///
277    /// - When `eo_rad = 0.0`, the result is the local Earth Rotation Angle
278    ///   relative to the Celestial Intermediate Origin (CIO) at the observer’s
279    ///   longitude.
280    ///
281    /// - When `eo_rad` is the mean Equation of the Origins, the result is
282    ///   **Local Mean Sidereal Time (LMST)** referred to the mean equinox.
283    ///
284    /// This value is commonly used when calculating the local hour angle of a
285    /// celestial object:
286    ///
287    /// ```text
288    /// HA = local_sidereal_angle_mean(mjd, eo) − RA
289    /// ```
290    ///
291    /// ## Example
292    ///
293    /// ```rust
294    /// use deep_time::Sidereal;
295    ///
296    /// let mut earth = Sidereal::EARTH;
297    /// earth.longitude_rad = 0.0; // Greenwich
298    ///
299    /// let mjd = 60000.0;
300    ///
301    /// // Pure local Earth Rotation Angle (CIO-based)
302    /// let local_era = earth.local_sidereal_angle_mean(mjd, 0.0);
303    ///
304    /// // Local Mean Sidereal Time using the mean Equation of the Origins
305    /// let eo_mean = earth.earth_eo_mean(mjd + 32.184 / 86400.0);
306    /// let lmst = earth.local_sidereal_angle_mean(mjd, eo_mean);
307    /// ```
308    #[inline]
309    pub const fn local_sidereal_angle_mean(&self, mjd: Real, eo_rad: Real) -> Real {
310        let angle = self.rotation_angle(mjd) + self.longitude_rad - eo_rad;
311        Self::normalize_angle(angle)
312    }
313
314    /// Returns sidereal time at the body's prime meridian as seconds since
315    /// sidereal midnight, wrapped to the range `[0, 86400)`.
316    ///
317    /// This is the time equivalent of [`sidereal_angle_mean`].
318    ///
319    /// ## Parameters
320    ///
321    /// - `eo_rad`: The Equation of the Origins value to use.  
322    ///   - Pass `0.0` to get the time equivalent of the pure Earth Rotation Angle (ERA).  
323    ///   - Pass the **mean** Equation of the Origins (e.g. from
324    ///     [`earth_eo_mean`](Self::earth_eo_mean)) to obtain Greenwich Mean
325    ///     Sidereal Time (GMST).
326    ///
327    /// ## Details
328    ///
329    /// - When `eo_rad = 0.0`, the result is the time equivalent of the modern
330    ///   Earth Rotation Angle (ERA).
331    ///
332    /// - When `eo_rad` is the mean Equation of the Origins, the result is
333    ///   **Greenwich Mean Sidereal Time (GMST)** referred to the mean equinox.
334    ///
335    /// As of Astropy 7.x, this is consistent with
336    /// `Time.sidereal_time("mean").to_value("sec")` (when no longitude is
337    /// specified) when using matching UT1 time and the mean Equation of the Origins.
338    ///
339    /// ## Example
340    ///
341    /// ```rust
342    /// use deep_time::Sidereal;
343    ///
344    /// let earth = Sidereal::EARTH;
345    /// let mjd = 60000.0;
346    ///
347    /// // Time equivalent of pure Earth Rotation Angle
348    /// let era_seconds = earth.sidereal_time_mean(mjd, 0.0);
349    ///
350    /// // Greenwich Mean Sidereal Time in seconds
351    /// let eo_mean = earth.earth_eo_mean(mjd + 32.184 / 86400.0);
352    /// let gmst_seconds = earth.sidereal_time_mean(mjd, eo_mean);
353    /// ```
354    pub const fn sidereal_time_mean(&self, mjd: Real, eo_rad: Real) -> Real {
355        let angle = self.sidereal_angle_mean(mjd, eo_rad);
356        let fraction = ((angle / TAU) % 1.0 + 1.0) % 1.0;
357        fraction * 86400.0
358    }
359
360    /// Returns local sidereal time at the observer's longitude as seconds since
361    /// sidereal midnight, wrapped to the range `[0, 86400)`.
362    ///
363    /// This is the time equivalent of [`local_sidereal_angle_mean`].
364    ///
365    /// ## Parameters
366    ///
367    /// - `eo_rad`: The Equation of the Origins value to use.  
368    ///   - Pass `0.0` to get the time equivalent of the local Earth Rotation Angle (CIO-based).  
369    ///   - Pass the **mean** Equation of the Origins (e.g. from
370    ///     [`earth_eo_mean`](Self::earth_eo_mean)) to obtain **Local Mean Sidereal Time (LMST)**.
371    ///
372    /// ## Details
373    ///
374    /// - When `eo_rad = 0.0`, the result is the time equivalent of the local
375    ///   Earth Rotation Angle relative to the Celestial Intermediate Origin (CIO)
376    ///   at the observer’s longitude.
377    ///
378    /// - When `eo_rad` is the mean Equation of the Origins, the result is
379    ///   **Local Mean Sidereal Time (LMST)** referred to the mean equinox.
380    ///
381    /// As of Astropy 7.x, this is consistent with
382    /// `Time.sidereal_time("mean", longitude=...).to_value("sec")` when using
383    /// matching UT1 time and the mean Equation of the Origins.
384    ///
385    /// ## Example
386    ///
387    /// ```rust
388    /// use deep_time::Sidereal;
389    ///
390    /// let mut earth = Sidereal::EARTH;
391    /// earth.longitude_rad = 0.0; // Greenwich
392    ///
393    /// let mjd = 60000.0;
394    ///
395    /// // Time equivalent of local Earth Rotation Angle
396    /// let local_era_seconds = earth.local_sidereal_time_mean(mjd, 0.0);
397    ///
398    /// // Local Mean Sidereal Time in seconds
399    /// let eo_mean = earth.earth_eo_mean(mjd + 32.184 / 86400.0);
400    /// let lmst_seconds = earth.local_sidereal_time_mean(mjd, eo_mean);
401    /// ```
402    pub const fn local_sidereal_time_mean(&self, mjd: Real, eo_rad: Real) -> Real {
403        let angle = self.local_sidereal_angle_mean(mjd, eo_rad);
404        let fraction = ((angle / TAU) % 1.0 + 1.0) % 1.0;
405        fraction * 86400.0
406    }
407
408    /// Returns the apparent sidereal angle of the body's prime meridian
409    /// in radians, normalized to `[0, 2π)`.
410    ///
411    /// This computes **Greenwich Apparent Sidereal Time (GAST)** when the
412    /// apparent Equation of the Origins is supplied.
413    ///
414    /// ## Parameters
415    ///
416    /// - `eo_rad`: The **apparent** Equation of the Origins
417    ///   (e.g. from [`earth_eo_apparent`](Self::earth_eo_apparent)).
418    ///   When supplied, the result is Greenwich Apparent Sidereal Time (GAST)
419    ///   referred to the true equinox.
420    ///
421    /// ## Details
422    ///
423    /// This function implements the direct relationship:
424    ///
425    /// ```text
426    /// GAST = ERA − EO_apparent
427    /// ```
428    ///
429    /// As of Astropy 7.x, this is consistent with
430    /// `Time.sidereal_time("apparent").rad` (when no longitude is specified)
431    /// when using matching UT1 time and the apparent Equation of the Origins.
432    ///
433    /// ## Example
434    ///
435    /// ```rust
436    /// use deep_time::Sidereal;
437    ///
438    /// let earth = Sidereal::EARTH;
439    /// let mjd = 60000.0;
440    ///
441    /// // Greenwich Apparent Sidereal Time
442    /// let eo_app = earth.earth_eo_apparent(mjd + 32.184 / 86400.0);
443    /// let gast = earth.sidereal_angle_apparent(mjd, eo_app);
444    /// ```
445    pub const fn sidereal_angle_apparent(&self, mjd: Real, eo_rad: Real) -> Real {
446        let angle = self.rotation_angle(mjd) - eo_rad;
447        Self::normalize_angle(angle)
448    }
449
450    /// Returns the local apparent sidereal angle at the observer's longitude
451    /// in radians, normalized to `[0, 2π)`.
452    ///
453    /// This computes **Local Apparent Sidereal Time (LAST)** when the
454    /// apparent Equation of the Origins is supplied.
455    ///
456    /// ## Parameters
457    ///
458    /// - `eo_rad`: The **apparent** Equation of the Origins
459    ///   (e.g. from [`earth_eo_apparent`](Self::earth_eo_apparent)).
460    ///   When supplied, the result is Local Apparent Sidereal Time (LAST)
461    ///   at the observer’s longitude, referred to the true equinox.
462    ///
463    /// ## Details
464    ///
465    /// This function implements the direct relationship:
466    ///
467    /// ```text
468    /// LAST = ERA + longitude − EO_apparent
469    /// ```
470    ///
471    /// As of Astropy 7.x, this is consistent with
472    /// `Time.sidereal_time("apparent", longitude=...).rad` when using
473    /// matching UT1 time and the apparent Equation of the Origins.
474    ///
475    /// ## Example
476    ///
477    /// ```rust
478    /// use deep_time::Sidereal;
479    ///
480    /// let mut earth = Sidereal::EARTH;
481    /// earth.longitude_rad = 0.0; // Greenwich
482    ///
483    /// let mjd = 60000.0;
484    ///
485    /// // Local Apparent Sidereal Time
486    /// let eo_app = earth.earth_eo_apparent(mjd + 32.184 / 86400.0);
487    /// let last = earth.local_sidereal_angle_apparent(mjd, eo_app);
488    /// ```
489    pub const fn local_sidereal_angle_apparent(&self, mjd: Real, eo_rad: Real) -> Real {
490        let angle = self.rotation_angle(mjd) + self.longitude_rad - eo_rad;
491        Self::normalize_angle(angle)
492    }
493
494    /// Returns apparent sidereal time at the body's prime meridian as seconds
495    /// since sidereal midnight, wrapped to the range `[0, 86400)`.
496    ///
497    /// This is the time equivalent of [`sidereal_angle_apparent`].
498    ///
499    /// When the **apparent** Equation of the Origins is supplied, this function
500    /// returns **Greenwich Apparent Sidereal Time (GAST)**.
501    ///
502    /// ## Parameters
503    ///
504    /// - `eo_rad`: The **apparent** Equation of the Origins
505    ///   (e.g. from [`earth_eo_apparent`](Self::earth_eo_apparent)).
506    ///   When supplied, the result is Greenwich Apparent Sidereal Time (GAST)
507    ///   in seconds since sidereal midnight.
508    ///
509    /// ## Details
510    ///
511    /// This function computes:
512    ///
513    /// ```text
514    /// GAST (seconds) = (ERA − EO_apparent) in fractional days × 86400
515    /// ```
516    ///
517    /// As of Astropy 7.x, this is consistent with
518    /// `Time.sidereal_time("apparent").to_value("sec")` (Greenwich) when using
519    /// matching UT1 time and the apparent Equation of the Origins.
520    ///
521    /// ## Example
522    ///
523    /// ```rust
524    /// use deep_time::Sidereal;
525    ///
526    /// let earth = Sidereal::EARTH;
527    /// let mjd = 60000.0;
528    ///
529    /// // Greenwich Apparent Sidereal Time in seconds
530    /// let eo_app = earth.earth_eo_apparent(mjd + 32.184 / 86400.0);
531    /// let gast_seconds = earth.sidereal_time_apparent(mjd, eo_app);
532    /// ```
533    pub const fn sidereal_time_apparent(&self, mjd: Real, eo_rad: Real) -> Real {
534        let angle = self.sidereal_angle_apparent(mjd, eo_rad);
535        let fraction = ((angle / TAU) % 1.0 + 1.0) % 1.0;
536        fraction * 86400.0
537    }
538
539    /// Returns local apparent sidereal time at the observer's longitude as
540    /// seconds since sidereal midnight, wrapped to the range `[0, 86400)`.
541    ///
542    /// This is the time equivalent of [`local_sidereal_angle_apparent`].
543    ///
544    /// When the **apparent** Equation of the Origins is supplied, this function
545    /// returns **Local Apparent Sidereal Time (LAST)**.
546    ///
547    /// ## Parameters
548    ///
549    /// - `eo_rad`: The **apparent** Equation of the Origins
550    ///   (e.g. from [`earth_eo_apparent`](Self::earth_eo_apparent)).
551    ///   When supplied, the result is Local Apparent Sidereal Time (LAST)
552    ///   at the observer’s longitude, in seconds since sidereal midnight.
553    ///
554    /// ## Details
555    ///
556    /// This function computes:
557    ///
558    /// ```text
559    /// LAST (seconds) = (ERA + longitude − EO_apparent) in fractional days × 86400
560    /// ```
561    ///
562    /// As of Astropy 7.x, this is consistent with
563    /// `Time.sidereal_time("apparent", longitude=...).to_value("sec")` when using
564    /// matching UT1 time and the apparent Equation of the Origins.
565    ///
566    /// ## Example
567    ///
568    /// ```rust
569    /// use deep_time::Sidereal;
570    ///
571    /// let mut earth = Sidereal::EARTH;
572    /// earth.longitude_rad = 0.0; // Greenwich
573    ///
574    /// let mjd = 60000.0;
575    ///
576    /// // Local Apparent Sidereal Time in seconds
577    /// let eo_app = earth.earth_eo_apparent(mjd + 32.184 / 86400.0);
578    /// let last_seconds = earth.local_sidereal_time_apparent(mjd, eo_app);
579    /// ```
580    pub const fn local_sidereal_time_apparent(&self, mjd: Real, eo_rad: Real) -> Real {
581        let angle = self.local_sidereal_angle_apparent(mjd, eo_rad);
582        let fraction = ((angle / TAU) % 1.0 + 1.0) % 1.0;
583        fraction * 86400.0
584    }
585
586    /// Returns the apparent Equation of the Origins (radians) at the given MJD.
587    ///
588    /// This returns the value computed by ERFA’s `eo06a`. It is the modern
589    /// CIO-based quantity used to derive **Greenwich Apparent Sidereal Time (GAST)**
590    /// from the Earth Rotation Angle (ERA).
591    ///
592    /// When you subtract this value from the ERA, you get GAST:
593    ///
594    /// ```text
595    /// GAST = ERA − earth_eo_apparent(...)
596    /// ```
597    ///
598    /// This method is equivalent to calling `erfa.eo06a(tt.jd1, tt.jd2)` in Astropy.
599    ///
600    /// You should pass the value returned by this function to the apparent
601    /// sidereal time functions (`sidereal_angle_apparent`, `local_sidereal_angle_apparent`,
602    /// `sidereal_time_apparent`, and `local_sidereal_time_apparent`).
603    ///
604    /// ## Example
605    ///
606    /// ```rust
607    /// use deep_time::Sidereal;
608    ///
609    /// let earth = Sidereal::EARTH;
610    /// let mjd_tt = 60000.0 + 32.184 / 86400.0;
611    ///
612    /// let eo_app = earth.earth_eo_apparent(mjd_tt);
613    /// let gast = earth.sidereal_angle_apparent(mjd_tt, eo_app);
614    /// ```
615    #[cfg(feature = "sidereal-earth")]
616    #[inline]
617    pub const fn earth_eo_apparent(&self, tt_mjd: Real) -> Real {
618        // Convert MJD → two-part Julian Date
619        let date1 = 2400000.5 + tt_mjd;
620        earth_eo(date1, 0.0)
621    }
622
623    /// Returns the mean Equation of the Origins (radians) at the given MJD.
624    ///
625    /// This returns the value that should be subtracted from the Earth Rotation
626    /// Angle (ERA) to obtain **Greenwich Mean Sidereal Time (GMST)**:
627    ///
628    /// ```text
629    /// GMST = ERA − earth_eo_mean(...)
630    /// ```
631    ///
632    /// Internally, this is computed as:
633    ///
634    /// ```text
635    /// earth_eo_mean = earth_eo_apparent() + earth_ee()
636    /// ```
637    ///
638    /// This is equivalent to computing `era - gmst` in Astropy:
639    ///
640    /// ```python
641    /// era = ut1.earth_rotation_angle(...).rad
642    /// gmst = ut1.sidereal_time("mean", ...).rad
643    /// eo_mean = era - gmst
644    /// ```
645    ///
646    /// You should pass the value returned by this function to the mean
647    /// sidereal time functions (`sidereal_angle_mean`, `local_sidereal_angle_mean`,
648    /// `sidereal_time_mean`, and `local_sidereal_time_mean`).
649    ///
650    /// ## Example
651    ///
652    /// ```rust
653    /// use deep_time::Sidereal;
654    ///
655    /// let earth = Sidereal::EARTH;
656    /// let mjd_tt = 60000.0 + 32.184 / 86400.0;
657    ///
658    /// let eo_mean = earth.earth_eo_mean(mjd_tt);
659    /// let gmst = earth.sidereal_angle_mean(mjd_tt, eo_mean);
660    /// ```
661    #[cfg(feature = "sidereal-earth")]
662    #[inline]
663    pub const fn earth_eo_mean(&self, tt_mjd: Real) -> Real {
664        // Convert MJD → two-part Julian Date
665        let date1 = 2400000.5 + tt_mjd;
666        earth_eo(date1, 0.0) + earth_ee(date1, 0.0)
667    }
668
669    /// Returns the Equation of the Equinoxes (radians) at the given MJD.
670    ///
671    /// This returns the value computed by ERFA’s `ee06a`. The Equation of the
672    /// Equinoxes represents the nutation contribution to sidereal time and is
673    /// defined as:
674    ///
675    /// ```text
676    /// EE = GAST − GMST
677    /// ```
678    ///
679    /// It is equivalent to computing `gast - gmst` in Astropy:
680    ///
681    /// ```python
682    /// gast = ut1.sidereal_time("apparent", ...).rad
683    /// gmst = ut1.sidereal_time("mean", ...).rad
684    /// ee = gast - gmst
685    /// ```
686    ///
687    /// This value is used internally when converting between mean and apparent
688    /// sidereal time (for example, when the mean functions are given the apparent
689    /// EO + EE).
690    ///
691    /// ## Example
692    ///
693    /// ```rust
694    /// use deep_time::Sidereal;
695    ///
696    /// let earth = Sidereal::EARTH;
697    /// let mjd_tt = 60000.0 + 32.184 / 86400.0;
698    ///
699    /// let ee = earth.earth_ee(mjd_tt);
700    /// ```
701    #[cfg(feature = "sidereal-earth")]
702    #[inline]
703    pub const fn earth_ee(&self, tt_mjd: Real) -> Real {
704        // Convert MJD → two-part Julian Date
705        let date1 = 2400000.5 + tt_mjd;
706        earth_ee(date1, 0.0)
707    }
708}