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