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