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