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