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}