@neilberkman/sidereon 0.32.0 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1082 @@
1
+ //! Solid-earth tide station displacement (IERS Conventions, Chapter 7).
2
+ //!
3
+ //! [`solid_earth_tide`] computes the tidal displacement of an Earth-fixed (ITRF)
4
+ //! GNSS station caused by the lunar and solar gravitational attraction. It is a
5
+ //! derived work of the IERS Conventions (2010) reference routine
6
+ //! `DEHANTTIDEINEL.F` (and its companion routines `ST1IDIU`, `ST1ISEM`,
7
+ //! `ST1L1`, `STEP2DIU`, `STEP2LON`, `CAL2JD`, `DAT`), reproduced here in Rust.
8
+ //!
9
+ //! How this derived work is based upon and differs from the original Software:
10
+ //!
11
+ //! * It is a line-for-line Rust translation of the in-phase degree-2/degree-3
12
+ //! displacement, the out-of-phase corrections (`ST1IDIU`, `ST1ISEM`), the
13
+ //! latitude-dependence correction (`ST1L1`), and the frequency-dependent
14
+ //! step-2 diurnal/long-period band corrections (`STEP2DIU`, `STEP2LON`),
15
+ //! evaluating the identical Love/Shida numbers, Doodson/argument tables, and
16
+ //! leap-second table.
17
+ //! * It keeps the permanent (mean) tide deformation: the original routine's
18
+ //! commented-out "Step 3" permanent-tide removal is left disabled, matching
19
+ //! the ITRF/IGS conform-to-mean-tide convention.
20
+ //! * The routine names are changed from the IERS originals (per the IERS
21
+ //! Conventions Software License), and the Fortran subroutine structure is
22
+ //! inlined into private helpers.
23
+ //! * The bundled IERS routine incorporates the SOFA `CAL2JD` and `DAT`
24
+ //! companion routines. Their Rust translations are likewise private and
25
+ //! renamed, while retaining SOFA-compatible numerical behavior.
26
+ //!
27
+ //! The Sun and Moon geocentric positions are inputs (metres, ECEF/ITRF); the
28
+ //! caller supplies them, e.g. from [`crate::astro::bodies::sun_moon_ecef`].
29
+ //!
30
+ //! This Sidereon derived work is neither distributed by nor endorsed by the
31
+ //! IERS Conventions Center.
32
+ //!
33
+ //! IERS Conventions Software License: permission is granted to use this software
34
+ //! for any purpose, including commercial applications, free of charge, and to
35
+ //! distribute derived works subject to its conditions. The intact notice ships
36
+ //! as `IERS-CONVENTIONS-SOFTWARE-LICENSE.txt`. Results obtained with this
37
+ //! software acknowledge use of the IERS Conventions software.
38
+
39
+ #[cfg(all(test, sidereon_repo_tests))]
40
+ mod tests;
41
+
42
+ mod ocean;
43
+ mod pole;
44
+ pub use ocean::{
45
+ ocean_tide_loading, parse_ocean_loading_blq_block, parse_ocean_loading_blq_blocks,
46
+ OceanLoadingBlq, OceanLoadingBlqBlock, OceanTideConstituent, NUM_OCEAN_CONSTITUENTS,
47
+ OCEAN_LOADING_CONSTITUENTS,
48
+ };
49
+ pub use pole::solid_earth_pole_tide;
50
+
51
+ use crate::astro::bodies::{sun_moon_ecef_with_polar_motion, SunMoonError};
52
+ use crate::astro::constants::models::iers::SOLID_TIDE_EARTH_RADIUS_M;
53
+ use crate::astro::constants::time::{
54
+ DAYS_PER_JULIAN_CENTURY, J2000_JD, SECONDS_PER_DAY, TT_MINUS_TAI_S,
55
+ };
56
+ use crate::astro::constants::units::{ARCSEC_TO_RAD, DEG_TO_RAD, KM_TO_M};
57
+ use crate::astro::frames::transforms::{FrameTransformError, PolarMotion};
58
+ use crate::astro::math::vec3::{dot3_ref as dot, norm3_ref as norm8};
59
+ use crate::astro::time::{CoverageError, TimeScaleInputErrorKind, TimeScales};
60
+ use crate::frame::{geodetic_to_itrf, ItrfPositionM, Wgs84Geodetic};
61
+ use crate::validate::{self, FieldError};
62
+
63
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
64
+ pub enum TideInputErrorKind {
65
+ Missing,
66
+ NonFinite,
67
+ NotPositive,
68
+ Negative,
69
+ OutOfRange,
70
+ FloatParse,
71
+ IntParse,
72
+ InvalidCivilDate,
73
+ InvalidCivilTime,
74
+ }
75
+
76
+ #[derive(Debug, Clone, PartialEq, Eq)]
77
+ pub enum BlqParseErrorKind {
78
+ Empty,
79
+ MissingStation,
80
+ MissingCoefficientRows {
81
+ station: String,
82
+ expected: usize,
83
+ found: usize,
84
+ },
85
+ TooManyCoefficientRows {
86
+ station: String,
87
+ },
88
+ WrongColumnCount {
89
+ expected: usize,
90
+ found: usize,
91
+ },
92
+ InvalidNumber {
93
+ token: String,
94
+ },
95
+ NonFiniteNumber {
96
+ token: String,
97
+ },
98
+ UnsupportedConstituent {
99
+ constituent: String,
100
+ },
101
+ DuplicateConstituent {
102
+ constituent: String,
103
+ },
104
+ MultipleBlocks {
105
+ found: usize,
106
+ },
107
+ }
108
+
109
+ impl core::fmt::Display for BlqParseErrorKind {
110
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
111
+ match self {
112
+ Self::Empty => f.write_str("empty BLQ block"),
113
+ Self::MissingStation => f.write_str("missing station identifier"),
114
+ Self::MissingCoefficientRows {
115
+ station,
116
+ expected,
117
+ found,
118
+ } => write!(
119
+ f,
120
+ "station {station} has {found} coefficient rows, expected {expected}"
121
+ ),
122
+ Self::TooManyCoefficientRows { station } => {
123
+ write!(f, "station {station} has more than 6 coefficient rows")
124
+ }
125
+ Self::WrongColumnCount { expected, found } => {
126
+ write!(
127
+ f,
128
+ "coefficient row has {found} columns, expected {expected}"
129
+ )
130
+ }
131
+ Self::InvalidNumber { token } => write!(f, "invalid number {token:?}"),
132
+ Self::NonFiniteNumber { token } => write!(f, "non-finite number {token:?}"),
133
+ Self::UnsupportedConstituent { constituent } => {
134
+ write!(f, "unsupported constituent {constituent}")
135
+ }
136
+ Self::DuplicateConstituent { constituent } => {
137
+ write!(f, "duplicate constituent {constituent}")
138
+ }
139
+ Self::MultipleBlocks { found } => {
140
+ write!(f, "expected one BLQ station block, found {found}")
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ impl core::fmt::Display for TideInputErrorKind {
147
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
148
+ f.write_str(match self {
149
+ Self::Missing => "missing",
150
+ Self::NonFinite => "not finite",
151
+ Self::NotPositive => "not positive",
152
+ Self::Negative => "negative",
153
+ Self::OutOfRange => "out of range",
154
+ Self::FloatParse => "invalid float",
155
+ Self::IntParse => "invalid integer",
156
+ Self::InvalidCivilDate => "invalid civil date",
157
+ Self::InvalidCivilTime => "invalid civil time",
158
+ })
159
+ }
160
+ }
161
+
162
+ impl From<&FieldError> for TideInputErrorKind {
163
+ fn from(error: &FieldError) -> Self {
164
+ match error {
165
+ FieldError::Missing { .. } => Self::Missing,
166
+ FieldError::NonFinite { .. } => Self::NonFinite,
167
+ FieldError::NotPositive { .. } => Self::NotPositive,
168
+ FieldError::Negative { .. } => Self::Negative,
169
+ FieldError::OutOfRange { .. } => Self::OutOfRange,
170
+ FieldError::FloatParse { .. } => Self::FloatParse,
171
+ FieldError::IntParse { .. } => Self::IntParse,
172
+ FieldError::InvalidCivilDate { .. } => Self::InvalidCivilDate,
173
+ FieldError::InvalidCivilTime { .. } => Self::InvalidCivilTime,
174
+ }
175
+ }
176
+ }
177
+
178
+ #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
179
+ pub enum TideError {
180
+ #[error("invalid solid-earth tide input {field}: {kind}")]
181
+ InvalidInput {
182
+ field: &'static str,
183
+ kind: TideInputErrorKind,
184
+ },
185
+ #[error("station displacement time-scale conversion failed: {0}")]
186
+ TimeScale(#[from] CoverageError),
187
+ #[error("station displacement frame transform failed: {0}")]
188
+ FrameTransform(#[from] FrameTransformError),
189
+ #[error("station displacement Sun/Moon evaluation failed: {0}")]
190
+ SunMoon(#[from] SunMoonError),
191
+ #[error("missing station displacement input {field}")]
192
+ MissingInput { field: &'static str },
193
+ #[error("invalid BLQ block at line {line}: {kind}")]
194
+ BlqParse {
195
+ line: usize,
196
+ kind: BlqParseErrorKind,
197
+ },
198
+ }
199
+
200
+ fn invalid_tide_input(error: FieldError) -> TideError {
201
+ TideError::InvalidInput {
202
+ field: error.field(),
203
+ kind: (&error).into(),
204
+ }
205
+ }
206
+
207
+ fn map_time_input(error: CoverageError) -> TideError {
208
+ match error {
209
+ CoverageError::InvalidInput { field, kind } => TideError::InvalidInput {
210
+ field,
211
+ kind: tide_kind_from_time_kind(kind),
212
+ },
213
+ other => TideError::TimeScale(other),
214
+ }
215
+ }
216
+
217
+ fn tide_kind_from_time_kind(kind: TimeScaleInputErrorKind) -> TideInputErrorKind {
218
+ match kind {
219
+ TimeScaleInputErrorKind::Missing => TideInputErrorKind::Missing,
220
+ TimeScaleInputErrorKind::NonFinite => TideInputErrorKind::NonFinite,
221
+ TimeScaleInputErrorKind::NotPositive => TideInputErrorKind::NotPositive,
222
+ TimeScaleInputErrorKind::Negative => TideInputErrorKind::Negative,
223
+ TimeScaleInputErrorKind::OutOfRange => TideInputErrorKind::OutOfRange,
224
+ TimeScaleInputErrorKind::FloatParse => TideInputErrorKind::FloatParse,
225
+ TimeScaleInputErrorKind::IntParse => TideInputErrorKind::IntParse,
226
+ TimeScaleInputErrorKind::InvalidCivilDate => TideInputErrorKind::InvalidCivilDate,
227
+ TimeScaleInputErrorKind::InvalidCivilTime => TideInputErrorKind::InvalidCivilTime,
228
+ }
229
+ }
230
+
231
+ /// Station position accepted by the high-level displacement API.
232
+ #[derive(Debug, Clone, Copy, PartialEq)]
233
+ pub enum StationDisplacementPosition {
234
+ /// ITRF/ECEF metres.
235
+ Ecef(ItrfPositionM),
236
+ /// WGS84 geodetic radians/metres. Converted to ITRF through the public
237
+ /// frame conversion path before any tide model is evaluated.
238
+ Geodetic(Wgs84Geodetic),
239
+ }
240
+
241
+ impl From<ItrfPositionM> for StationDisplacementPosition {
242
+ fn from(value: ItrfPositionM) -> Self {
243
+ Self::Ecef(value)
244
+ }
245
+ }
246
+
247
+ impl From<Wgs84Geodetic> for StationDisplacementPosition {
248
+ fn from(value: Wgs84Geodetic) -> Self {
249
+ Self::Geodetic(value)
250
+ }
251
+ }
252
+
253
+ impl StationDisplacementPosition {
254
+ /// Construct from raw ITRF/ECEF metre components.
255
+ pub fn from_ecef_m(position_m: [f64; 3]) -> Result<Self, TideError> {
256
+ let position =
257
+ ItrfPositionM::new(position_m[0], position_m[1], position_m[2]).map_err(|error| {
258
+ match error {
259
+ crate::frame::FrameValueError::InvalidInput { field, reason: _ } => {
260
+ TideError::InvalidInput {
261
+ field,
262
+ kind: TideInputErrorKind::NonFinite,
263
+ }
264
+ }
265
+ }
266
+ })?;
267
+ Ok(Self::Ecef(position))
268
+ }
269
+
270
+ fn ecef_m(self) -> Result<[f64; 3], TideError> {
271
+ match self {
272
+ Self::Ecef(position) => Ok(position.as_array()),
273
+ Self::Geodetic(position) => Ok(geodetic_to_itrf(position)?.as_array()),
274
+ }
275
+ }
276
+ }
277
+
278
+ /// IERS polar-motion coordinates of the epoch, in arcseconds.
279
+ #[derive(Debug, Clone, Copy, PartialEq)]
280
+ pub struct StationPolarMotion {
281
+ pub xp_arcsec: f64,
282
+ pub yp_arcsec: f64,
283
+ }
284
+
285
+ impl StationPolarMotion {
286
+ pub const fn from_arcseconds(xp_arcsec: f64, yp_arcsec: f64) -> Self {
287
+ Self {
288
+ xp_arcsec,
289
+ yp_arcsec,
290
+ }
291
+ }
292
+
293
+ fn polar_motion(self) -> Result<PolarMotion, TideError> {
294
+ Ok(PolarMotion::from_radians(
295
+ self.xp_arcsec * ARCSEC_TO_RAD,
296
+ self.yp_arcsec * ARCSEC_TO_RAD,
297
+ )?)
298
+ }
299
+ }
300
+
301
+ /// UTC epoch for station displacement evaluation.
302
+ #[derive(Debug, Clone, Copy, PartialEq)]
303
+ pub struct StationDisplacementEpoch {
304
+ pub year: i32,
305
+ pub month: u8,
306
+ pub day: u8,
307
+ pub hour: u8,
308
+ pub minute: u8,
309
+ pub second: f64,
310
+ /// Optional IERS polar motion for pole tide and polar-motion-aware Sun/Moon
311
+ /// rotation.
312
+ pub polar_motion: Option<StationPolarMotion>,
313
+ }
314
+
315
+ impl StationDisplacementEpoch {
316
+ pub const fn from_utc(
317
+ year: i32,
318
+ month: u8,
319
+ day: u8,
320
+ hour: u8,
321
+ minute: u8,
322
+ second: f64,
323
+ ) -> Self {
324
+ Self {
325
+ year,
326
+ month,
327
+ day,
328
+ hour,
329
+ minute,
330
+ second,
331
+ polar_motion: None,
332
+ }
333
+ }
334
+
335
+ pub const fn with_polar_motion_arcsec(mut self, xp_arcsec: f64, yp_arcsec: f64) -> Self {
336
+ self.polar_motion = Some(StationPolarMotion::from_arcseconds(xp_arcsec, yp_arcsec));
337
+ self
338
+ }
339
+
340
+ fn time_scales(self) -> Result<TimeScales, TideError> {
341
+ TimeScales::from_utc(
342
+ self.year,
343
+ i32::from(self.month),
344
+ i32::from(self.day),
345
+ i32::from(self.hour),
346
+ i32::from(self.minute),
347
+ self.second,
348
+ )
349
+ .map_err(map_time_input)
350
+ }
351
+
352
+ fn validate_utc(self) -> Result<(), TideError> {
353
+ validate::civil_datetime_with_second_policy(
354
+ i64::from(self.year),
355
+ i64::from(self.month),
356
+ i64::from(self.day),
357
+ i64::from(self.hour),
358
+ i64::from(self.minute),
359
+ self.second,
360
+ validate::CivilSecondPolicy::Continuous,
361
+ )
362
+ .map(|_| ())
363
+ .map_err(invalid_tide_input)
364
+ }
365
+
366
+ fn fractional_hour(self) -> f64 {
367
+ f64::from(self.hour) + f64::from(self.minute) / 60.0 + self.second / 3600.0
368
+ }
369
+ }
370
+
371
+ /// Switches for the high-level station displacement entry.
372
+ #[derive(Debug, Clone, Copy, PartialEq)]
373
+ pub struct StationDisplacementOptions<'a> {
374
+ /// Apply the IERS solid Earth tide station displacement.
375
+ pub solid_earth_tide: bool,
376
+ /// Apply the IERS pole tide station displacement. Each epoch must carry
377
+ /// polar motion when this is true.
378
+ pub pole_tide: bool,
379
+ /// Optional ocean-loading BLQ coefficients supplied by the caller.
380
+ pub ocean_loading: Option<&'a OceanLoadingBlq>,
381
+ }
382
+
383
+ impl Default for StationDisplacementOptions<'_> {
384
+ fn default() -> Self {
385
+ Self {
386
+ solid_earth_tide: true,
387
+ pole_tide: false,
388
+ ocean_loading: None,
389
+ }
390
+ }
391
+ }
392
+
393
+ /// Component-resolved station displacement in ITRF/ECEF metres.
394
+ #[derive(Debug, Clone, Copy, PartialEq)]
395
+ pub struct StationDisplacement {
396
+ /// Sum of all enabled component displacements, in ITRF/ECEF metres.
397
+ pub ecef_m: [f64; 3],
398
+ pub solid_earth_tide_ecef_m: Option<[f64; 3]>,
399
+ pub pole_tide_ecef_m: Option<[f64; 3]>,
400
+ pub ocean_loading_ecef_m: Option<[f64; 3]>,
401
+ }
402
+
403
+ impl StationDisplacement {
404
+ fn zero() -> Self {
405
+ Self {
406
+ ecef_m: [0.0; 3],
407
+ solid_earth_tide_ecef_m: None,
408
+ pole_tide_ecef_m: None,
409
+ ocean_loading_ecef_m: None,
410
+ }
411
+ }
412
+
413
+ fn add_component(total: &mut [f64; 3], component: [f64; 3]) {
414
+ for i in 0..3 {
415
+ total[i] += component[i];
416
+ }
417
+ }
418
+ }
419
+
420
+ /// Evaluate the enabled station displacement corrections, returning ITRF/ECEF
421
+ /// metre components.
422
+ ///
423
+ /// The solid Earth tide path uses IERS Conventions (2010), Chapter 7 station
424
+ /// displacement with the permanent tide retained. The low-level
425
+ /// [`solid_earth_tide`] routine ships the in-phase degree-2 and degree-3
426
+ /// displacement, the step-1 out-of-phase and latitude-dependence corrections,
427
+ /// and the step-2 diurnal/long-period frequency corrections; it leaves the
428
+ /// optional step-3 permanent-tide removal disabled for ITRF/IGS use. Sun/Moon
429
+ /// positions are generated through the same Earth-fixed analytic ephemeris path
430
+ /// used by the tide-force lane, including caller-supplied polar motion when the
431
+ /// epoch carries it.
432
+ pub fn station_displacement_ecef_m(
433
+ position: StationDisplacementPosition,
434
+ epoch: StationDisplacementEpoch,
435
+ options: StationDisplacementOptions<'_>,
436
+ ) -> Result<StationDisplacement, TideError> {
437
+ let receiver_ecef_m = position.ecef_m()?;
438
+ epoch.validate_utc()?;
439
+ let fhr = epoch.fractional_hour();
440
+ let mut displacement = StationDisplacement::zero();
441
+
442
+ if options.solid_earth_tide {
443
+ let ts = epoch.time_scales()?;
444
+ let polar_motion = epoch
445
+ .polar_motion
446
+ .map(StationPolarMotion::polar_motion)
447
+ .transpose()?
448
+ .unwrap_or_default();
449
+ let sun_moon = sun_moon_ecef_with_polar_motion(&ts, polar_motion)?;
450
+ let solid = solid_earth_tide(
451
+ &receiver_ecef_m,
452
+ epoch.year,
453
+ i32::from(epoch.month),
454
+ i32::from(epoch.day),
455
+ fhr,
456
+ &sun_moon.sun,
457
+ &sun_moon.moon,
458
+ )?;
459
+ StationDisplacement::add_component(&mut displacement.ecef_m, solid);
460
+ displacement.solid_earth_tide_ecef_m = Some(solid);
461
+ }
462
+
463
+ if options.pole_tide {
464
+ let polar = epoch.polar_motion.ok_or(TideError::MissingInput {
465
+ field: "polar motion",
466
+ })?;
467
+ let pole = solid_earth_pole_tide(
468
+ &receiver_ecef_m,
469
+ epoch.year,
470
+ i32::from(epoch.month),
471
+ i32::from(epoch.day),
472
+ fhr,
473
+ polar.xp_arcsec,
474
+ polar.yp_arcsec,
475
+ )?;
476
+ StationDisplacement::add_component(&mut displacement.ecef_m, pole);
477
+ displacement.pole_tide_ecef_m = Some(pole);
478
+ }
479
+
480
+ if let Some(blq) = options.ocean_loading {
481
+ let ocean = ocean_tide_loading(
482
+ &receiver_ecef_m,
483
+ epoch.year,
484
+ i32::from(epoch.month),
485
+ i32::from(epoch.day),
486
+ fhr,
487
+ blq,
488
+ )?;
489
+ StationDisplacement::add_component(&mut displacement.ecef_m, ocean);
490
+ displacement.ocean_loading_ecef_m = Some(ocean);
491
+ }
492
+
493
+ Ok(displacement)
494
+ }
495
+
496
+ /// Evaluate station displacement for many epochs. Each element is equivalent to
497
+ /// a scalar [`station_displacement_ecef_m`] call for the same position, epoch,
498
+ /// and options, so per-epoch failures stay local to their output row.
499
+ pub fn station_displacement_ecef_m_batch(
500
+ position: StationDisplacementPosition,
501
+ epochs: &[StationDisplacementEpoch],
502
+ options: StationDisplacementOptions<'_>,
503
+ ) -> Vec<Result<StationDisplacement, TideError>> {
504
+ epochs
505
+ .iter()
506
+ .map(|&epoch| station_displacement_ecef_m(position, epoch, options))
507
+ .collect()
508
+ }
509
+
510
+ /// Solid-earth tide displacement of an ITRF station, in metres (ECEF).
511
+ ///
512
+ /// Arguments mirror the IERS reference routine:
513
+ /// * `xsta` - geocentric station position (m, ITRF).
514
+ /// * `year`, `month`, `day` - UTC calendar date.
515
+ /// * `fhr` - UTC fractional hour of the day (hour + min/60 + sec/3600).
516
+ /// * `xsun` - geocentric Sun position (m, ECEF).
517
+ /// * `xmon` - geocentric Moon position (m, ECEF).
518
+ ///
519
+ /// Returns the displacement vector `dxtide` (m, geocentric ITRF). The permanent
520
+ /// (mean) tide deformation is retained (ITRF/IGS convention).
521
+ ///
522
+ /// Returns [`TideError`] when inputs are non-finite or geometrically
523
+ /// degenerate: the station vector must be non-zero and non-polar, and Sun/Moon
524
+ /// vectors must be non-zero.
525
+ pub fn solid_earth_tide(
526
+ xsta: &[f64; 3],
527
+ year: i32,
528
+ month: i32,
529
+ day: i32,
530
+ fhr: f64,
531
+ xsun: &[f64; 3],
532
+ xmon: &[f64; 3],
533
+ ) -> Result<[f64; 3], TideError> {
534
+ validate_tide_domain(xsta, year, month, day, fhr, xsun, xmon)?;
535
+ Ok(solid_earth_tide_unchecked(
536
+ xsta, year, month, day, fhr, xsun, xmon,
537
+ ))
538
+ }
539
+
540
+ fn validate_tide_domain(
541
+ xsta: &[f64; 3],
542
+ year: i32,
543
+ month: i32,
544
+ day: i32,
545
+ fhr: f64,
546
+ xsun: &[f64; 3],
547
+ xmon: &[f64; 3],
548
+ ) -> Result<(), TideError> {
549
+ validate::finite_vec3(*xsta, "station position").map_err(invalid_tide_input)?;
550
+ validate::civil_datetime_with_second_policy(
551
+ i64::from(year),
552
+ i64::from(month),
553
+ i64::from(day),
554
+ 0,
555
+ 0,
556
+ 0.0,
557
+ validate::CivilSecondPolicy::Continuous,
558
+ )
559
+ .map_err(invalid_tide_input)?;
560
+ validate::finite_in_range_exclusive_upper(fhr, 0.0, 24.0, "fractional hour")
561
+ .map_err(invalid_tide_input)?;
562
+ validate::finite_vec3(*xsun, "sun position").map_err(invalid_tide_input)?;
563
+ validate::finite_vec3(*xmon, "moon position").map_err(invalid_tide_input)?;
564
+
565
+ validate::finite_positive(norm8(xsta), "station radius").map_err(invalid_tide_input)?;
566
+ let station_horizontal_radius = (xsta[0] * xsta[0] + xsta[1] * xsta[1]).sqrt();
567
+ validate::finite_positive(station_horizontal_radius, "station horizontal radius")
568
+ .map_err(invalid_tide_input)?;
569
+ validate::finite_positive(norm8(xsun), "sun radius").map_err(invalid_tide_input)?;
570
+ validate::finite_positive(norm8(xmon), "moon radius").map_err(invalid_tide_input)?;
571
+
572
+ Ok(())
573
+ }
574
+
575
+ fn solid_earth_tide_unchecked(
576
+ xsta: &[f64; 3],
577
+ year: i32,
578
+ month: i32,
579
+ day: i32,
580
+ fhr: f64,
581
+ xsun: &[f64; 3],
582
+ xmon: &[f64; 3],
583
+ ) -> [f64; 3] {
584
+ // Nominal second- and third-degree Love and Shida numbers.
585
+ const H20: f64 = 0.6078;
586
+ const L20: f64 = 0.0847;
587
+ const H3: f64 = 0.292;
588
+ const L3: f64 = 0.015;
589
+
590
+ // Scalar product of station vector with Sun/Moon vector (SPROD).
591
+ let rsta = norm8(xsta);
592
+ let rsun = norm8(xsun);
593
+ let rmon = norm8(xmon);
594
+ let scs = dot(xsta, xsun);
595
+ let scm = dot(xsta, xmon);
596
+ let scsun = scs / rsta / rsun;
597
+ let scmon = scm / rsta / rmon;
598
+
599
+ // Latitude-corrected H2 and L2.
600
+ let cosphi = (xsta[0] * xsta[0] + xsta[1] * xsta[1]).sqrt() / rsta;
601
+ let h2 = H20 - 0.0006 * (1.0 - 3.0 / 2.0 * cosphi * cosphi);
602
+ let l2 = L20 + 0.0002 * (1.0 - 3.0 / 2.0 * cosphi * cosphi);
603
+
604
+ // P2 term.
605
+ let p2sun = 3.0 * (h2 / 2.0 - l2) * scsun * scsun - h2 / 2.0;
606
+ let p2mon = 3.0 * (h2 / 2.0 - l2) * scmon * scmon - h2 / 2.0;
607
+
608
+ // P3 term.
609
+ let p3sun = 5.0 / 2.0 * (H3 - 3.0 * L3) * scsun.powi(3) + 3.0 / 2.0 * (L3 - H3) * scsun;
610
+ let p3mon = 5.0 / 2.0 * (H3 - 3.0 * L3) * scmon.powi(3) + 3.0 / 2.0 * (L3 - H3) * scmon;
611
+
612
+ // Term in direction of Sun/Moon vector.
613
+ let x2sun = 3.0 * l2 * scsun;
614
+ let x2mon = 3.0 * l2 * scmon;
615
+ let x3sun = 3.0 * L3 / 2.0 * (5.0 * scsun * scsun - 1.0);
616
+ let x3mon = 3.0 * L3 / 2.0 * (5.0 * scmon * scmon - 1.0);
617
+
618
+ // Factors for Sun/Moon (IAU current best estimates).
619
+ const MASS_RATIO_SUN: f64 = 332946.0482;
620
+ const MASS_RATIO_MOON: f64 = 0.0123000371;
621
+ const RE: f64 = SOLID_TIDE_EARTH_RADIUS_M;
622
+ let fac2sun = MASS_RATIO_SUN * RE * (RE / rsun).powi(3);
623
+ let fac2mon = MASS_RATIO_MOON * RE * (RE / rmon).powi(3);
624
+ let fac3sun = fac2sun * (RE / rsun);
625
+ let fac3mon = fac2mon * (RE / rmon);
626
+
627
+ // Total in-phase degree-2/degree-3 displacement.
628
+ let mut dxtide = [0.0_f64; 3];
629
+ for i in 0..3 {
630
+ dxtide[i] = fac2sun * (x2sun * xsun[i] / rsun + p2sun * xsta[i] / rsta)
631
+ + fac2mon * (x2mon * xmon[i] / rmon + p2mon * xsta[i] / rsta)
632
+ + fac3sun * (x3sun * xsun[i] / rsun + p3sun * xsta[i] / rsta)
633
+ + fac3mon * (x3mon * xmon[i] / rmon + p3mon * xsta[i] / rsta);
634
+ }
635
+
636
+ // Out-of-phase corrections (diurnal, semi-diurnal) and latitude dependence.
637
+ let c = out_of_phase_diurnal_correction(xsta, xsun, xmon, fac2sun, fac2mon);
638
+ for i in 0..3 {
639
+ dxtide[i] += c[i];
640
+ }
641
+ let c = out_of_phase_semidiurnal_correction(xsta, xsun, xmon, fac2sun, fac2mon);
642
+ for i in 0..3 {
643
+ dxtide[i] += c[i];
644
+ }
645
+ let c = latitude_dependence_correction(xsta, xsun, xmon, fac2sun, fac2mon);
646
+ for i in 0..3 {
647
+ dxtide[i] += c[i];
648
+ }
649
+
650
+ // Step 2 corrections need the date in Julian centuries (TT).
651
+ let (jjm0, jjm1) = gregorian_to_two_part_julian_date(year, month, day);
652
+ let fhrd = fhr / 24.0;
653
+ let mut t = ((jjm0 - J2000_JD) + jjm1 + fhrd) / DAYS_PER_JULIAN_CENTURY;
654
+ let dtt = tai_minus_utc_seconds(year, month, day) + TT_MINUS_TAI_S;
655
+ t += dtt / (SECONDS_PER_DAY * DAYS_PER_JULIAN_CENTURY);
656
+
657
+ let c = frequency_dependent_diurnal_correction(xsta, fhr, t);
658
+ for i in 0..3 {
659
+ dxtide[i] += c[i];
660
+ }
661
+ let c = frequency_dependent_long_period_correction(xsta, t);
662
+ for i in 0..3 {
663
+ dxtide[i] += c[i];
664
+ }
665
+
666
+ // Step 3 of the IERS routine, the permanent (zero-frequency) tide removal,
667
+ // is intentionally not applied, so the permanent (mean) tide deformation is
668
+ // retained (the ITRF/IGS conform-to-mean-tide convention; see module docs).
669
+ dxtide
670
+ }
671
+
672
+ /// Out-of-phase part of the Love numbers, diurnal band (ST1IDIU).
673
+ fn out_of_phase_diurnal_correction(
674
+ xsta: &[f64; 3],
675
+ xsun: &[f64; 3],
676
+ xmon: &[f64; 3],
677
+ fac2sun: f64,
678
+ fac2mon: f64,
679
+ ) -> [f64; 3] {
680
+ const DHI: f64 = -0.0025;
681
+ const DLI: f64 = -0.0007;
682
+ let rsta = norm8(xsta);
683
+ let sinphi = xsta[2] / rsta;
684
+ let cosphi = (xsta[0] * xsta[0] + xsta[1] * xsta[1]).sqrt() / rsta;
685
+ let cos2phi = cosphi * cosphi - sinphi * sinphi;
686
+ let sinla = xsta[1] / cosphi / rsta;
687
+ let cosla = xsta[0] / cosphi / rsta;
688
+ let rmon = norm8(xmon);
689
+ let rsun = norm8(xsun);
690
+
691
+ let drsun =
692
+ -3.0 * DHI * sinphi * cosphi * fac2sun * xsun[2] * (xsun[0] * sinla - xsun[1] * cosla)
693
+ / (rsun * rsun);
694
+ let drmon =
695
+ -3.0 * DHI * sinphi * cosphi * fac2mon * xmon[2] * (xmon[0] * sinla - xmon[1] * cosla)
696
+ / (rmon * rmon);
697
+ let dnsun = -3.0 * DLI * cos2phi * fac2sun * xsun[2] * (xsun[0] * sinla - xsun[1] * cosla)
698
+ / (rsun * rsun);
699
+ let dnmon = -3.0 * DLI * cos2phi * fac2mon * xmon[2] * (xmon[0] * sinla - xmon[1] * cosla)
700
+ / (rmon * rmon);
701
+ let desun = -3.0 * DLI * sinphi * fac2sun * xsun[2] * (xsun[0] * cosla + xsun[1] * sinla)
702
+ / (rsun * rsun);
703
+ let demon = -3.0 * DLI * sinphi * fac2mon * xmon[2] * (xmon[0] * cosla + xmon[1] * sinla)
704
+ / (rmon * rmon);
705
+
706
+ let dr = drsun + drmon;
707
+ let dn = dnsun + dnmon;
708
+ let de = desun + demon;
709
+
710
+ [
711
+ dr * cosla * cosphi - de * sinla - dn * sinphi * cosla,
712
+ dr * sinla * cosphi + de * cosla - dn * sinphi * sinla,
713
+ dr * sinphi + dn * cosphi,
714
+ ]
715
+ }
716
+
717
+ /// Out-of-phase part of the Love numbers, semi-diurnal band (ST1ISEM).
718
+ fn out_of_phase_semidiurnal_correction(
719
+ xsta: &[f64; 3],
720
+ xsun: &[f64; 3],
721
+ xmon: &[f64; 3],
722
+ fac2sun: f64,
723
+ fac2mon: f64,
724
+ ) -> [f64; 3] {
725
+ const DHI: f64 = -0.0022;
726
+ const DLI: f64 = -0.0007;
727
+ let rsta = norm8(xsta);
728
+ let sinphi = xsta[2] / rsta;
729
+ let cosphi = (xsta[0] * xsta[0] + xsta[1] * xsta[1]).sqrt() / rsta;
730
+ let sinla = xsta[1] / cosphi / rsta;
731
+ let cosla = xsta[0] / cosphi / rsta;
732
+ let costwola = cosla * cosla - sinla * sinla;
733
+ let sintwola = 2.0 * cosla * sinla;
734
+ let rmon = norm8(xmon);
735
+ let rsun = norm8(xsun);
736
+
737
+ let drsun = -3.0 / 4.0
738
+ * DHI
739
+ * cosphi
740
+ * cosphi
741
+ * fac2sun
742
+ * ((xsun[0] * xsun[0] - xsun[1] * xsun[1]) * sintwola - 2.0 * xsun[0] * xsun[1] * costwola)
743
+ / (rsun * rsun);
744
+ let drmon = -3.0 / 4.0
745
+ * DHI
746
+ * cosphi
747
+ * cosphi
748
+ * fac2mon
749
+ * ((xmon[0] * xmon[0] - xmon[1] * xmon[1]) * sintwola - 2.0 * xmon[0] * xmon[1] * costwola)
750
+ / (rmon * rmon);
751
+ let dnsun = 3.0 / 2.0
752
+ * DLI
753
+ * sinphi
754
+ * cosphi
755
+ * fac2sun
756
+ * ((xsun[0] * xsun[0] - xsun[1] * xsun[1]) * sintwola - 2.0 * xsun[0] * xsun[1] * costwola)
757
+ / (rsun * rsun);
758
+ let dnmon = 3.0 / 2.0
759
+ * DLI
760
+ * sinphi
761
+ * cosphi
762
+ * fac2mon
763
+ * ((xmon[0] * xmon[0] - xmon[1] * xmon[1]) * sintwola - 2.0 * xmon[0] * xmon[1] * costwola)
764
+ / (rmon * rmon);
765
+ let desun = -3.0 / 2.0
766
+ * DLI
767
+ * cosphi
768
+ * fac2sun
769
+ * ((xsun[0] * xsun[0] - xsun[1] * xsun[1]) * costwola + 2.0 * xsun[0] * xsun[1] * sintwola)
770
+ / (rsun * rsun);
771
+ let demon = -3.0 / 2.0
772
+ * DLI
773
+ * cosphi
774
+ * fac2mon
775
+ * ((xmon[0] * xmon[0] - xmon[1] * xmon[1]) * costwola + 2.0 * xmon[0] * xmon[1] * sintwola)
776
+ / (rmon * rmon);
777
+
778
+ let dr = drsun + drmon;
779
+ let dn = dnsun + dnmon;
780
+ let de = desun + demon;
781
+
782
+ [
783
+ dr * cosla * cosphi - de * sinla - dn * sinphi * cosla,
784
+ dr * sinla * cosphi + de * cosla - dn * sinphi * sinla,
785
+ dr * sinphi + dn * cosphi,
786
+ ]
787
+ }
788
+
789
+ /// Latitude dependence of the Love numbers, part L^(1) (ST1L1).
790
+ fn latitude_dependence_correction(
791
+ xsta: &[f64; 3],
792
+ xsun: &[f64; 3],
793
+ xmon: &[f64; 3],
794
+ fac2sun: f64,
795
+ fac2mon: f64,
796
+ ) -> [f64; 3] {
797
+ const L1D: f64 = 0.0012;
798
+ const L1SD: f64 = 0.0024;
799
+ let rsta = norm8(xsta);
800
+ let sinphi = xsta[2] / rsta;
801
+ let cosphi = (xsta[0] * xsta[0] + xsta[1] * xsta[1]).sqrt() / rsta;
802
+ let sinla = xsta[1] / cosphi / rsta;
803
+ let cosla = xsta[0] / cosphi / rsta;
804
+ let rmon = norm8(xmon);
805
+ let rsun = norm8(xsun);
806
+
807
+ // Diurnal band.
808
+ let mut l1 = L1D;
809
+ let dnsun = -l1 * sinphi * sinphi * fac2sun * xsun[2] * (xsun[0] * cosla + xsun[1] * sinla)
810
+ / (rsun * rsun);
811
+ let dnmon = -l1 * sinphi * sinphi * fac2mon * xmon[2] * (xmon[0] * cosla + xmon[1] * sinla)
812
+ / (rmon * rmon);
813
+ let desun = l1
814
+ * sinphi
815
+ * (cosphi * cosphi - sinphi * sinphi)
816
+ * fac2sun
817
+ * xsun[2]
818
+ * (xsun[0] * sinla - xsun[1] * cosla)
819
+ / (rsun * rsun);
820
+ let demon = l1
821
+ * sinphi
822
+ * (cosphi * cosphi - sinphi * sinphi)
823
+ * fac2mon
824
+ * xmon[2]
825
+ * (xmon[0] * sinla - xmon[1] * cosla)
826
+ / (rmon * rmon);
827
+
828
+ let de = 3.0 * (desun + demon);
829
+ let dn = 3.0 * (dnsun + dnmon);
830
+
831
+ let mut xcorsta = [
832
+ -de * sinla - dn * sinphi * cosla,
833
+ de * cosla - dn * sinphi * sinla,
834
+ dn * cosphi,
835
+ ];
836
+
837
+ // Semi-diurnal band.
838
+ l1 = L1SD;
839
+ let costwola = cosla * cosla - sinla * sinla;
840
+ let sintwola = 2.0 * cosla * sinla;
841
+
842
+ let dnsun = -l1 / 2.0
843
+ * sinphi
844
+ * cosphi
845
+ * fac2sun
846
+ * ((xsun[0] * xsun[0] - xsun[1] * xsun[1]) * costwola + 2.0 * xsun[0] * xsun[1] * sintwola)
847
+ / (rsun * rsun);
848
+ let dnmon = -l1 / 2.0
849
+ * sinphi
850
+ * cosphi
851
+ * fac2mon
852
+ * ((xmon[0] * xmon[0] - xmon[1] * xmon[1]) * costwola + 2.0 * xmon[0] * xmon[1] * sintwola)
853
+ / (rmon * rmon);
854
+ let desun = -l1 / 2.0
855
+ * sinphi
856
+ * sinphi
857
+ * cosphi
858
+ * fac2sun
859
+ * ((xsun[0] * xsun[0] - xsun[1] * xsun[1]) * sintwola - 2.0 * xsun[0] * xsun[1] * costwola)
860
+ / (rsun * rsun);
861
+ let demon = -l1 / 2.0
862
+ * sinphi
863
+ * sinphi
864
+ * cosphi
865
+ * fac2mon
866
+ * ((xmon[0] * xmon[0] - xmon[1] * xmon[1]) * sintwola - 2.0 * xmon[0] * xmon[1] * costwola)
867
+ / (rmon * rmon);
868
+
869
+ let de = 3.0 * (desun + demon);
870
+ let dn = 3.0 * (dnsun + dnmon);
871
+
872
+ xcorsta[0] += -de * sinla - dn * sinphi * cosla;
873
+ xcorsta[1] += de * cosla - dn * sinphi * sinla;
874
+ xcorsta[2] += dn * cosphi;
875
+ xcorsta
876
+ }
877
+
878
+ /// In-phase / out-of-phase frequency-dependent corrections, diurnal band
879
+ /// (STEP2DIU). `fhr` is UTC fractional hour, `t` is Julian centuries (TT).
880
+ fn frequency_dependent_diurnal_correction(xsta: &[f64; 3], fhr: f64, t: f64) -> [f64; 3] {
881
+ // DATDI(9,31): {l, l', F, D, Omega(Ps), Adr, Adi, Anr, Ani} per wave.
882
+ #[rustfmt::skip]
883
+ const DATDI: [[f64; 9]; 31] = [
884
+ [-3.0, 0.0, 2.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
885
+ [-3.0, 2.0, 0.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
886
+ [-2.0, 0.0, 1.0, -1.0, 0.0, -0.02, 0.0, 0.0, 0.0],
887
+ [-2.0, 0.0, 1.0, 0.0, 0.0, -0.08, 0.0, -0.01, 0.01],
888
+ [-2.0, 2.0, -1.0, 0.0, 0.0, -0.02, 0.0, 0.0, 0.0],
889
+ [-1.0, 0.0, 0.0, -1.0, 0.0, -0.10, 0.0, 0.0, 0.0],
890
+ [-1.0, 0.0, 0.0, 0.0, 0.0, -0.51, 0.0, -0.02, 0.03],
891
+ [-1.0, 2.0, 0.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0],
892
+ [0.0, -2.0, 1.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0],
893
+ [0.0, 0.0, -1.0, 0.0, 0.0, 0.02, 0.0, 0.0, 0.0],
894
+ [0.0, 0.0, 1.0, 0.0, 0.0, 0.06, 0.0, 0.0, 0.0],
895
+ [0.0, 0.0, 1.0, 1.0, 0.0, 0.01, 0.0, 0.0, 0.0],
896
+ [0.0, 2.0, -1.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0],
897
+ [1.0, -3.0, 0.0, 0.0, 1.0, -0.06, 0.0, 0.0, 0.0],
898
+ [1.0, -2.0, 0.0, -1.0, 0.0, 0.01, 0.0, 0.0, 0.0],
899
+ [1.0, -2.0, 0.0, 0.0, 0.0, -1.23, -0.07, 0.06, 0.01],
900
+ [1.0, -1.0, 0.0, 0.0, -1.0, 0.02, 0.0, 0.0, 0.0],
901
+ [1.0, -1.0, 0.0, 0.0, 1.0, 0.04, 0.0, 0.0, 0.0],
902
+ [1.0, 0.0, 0.0, -1.0, 0.0, -0.22, 0.01, 0.01, 0.0],
903
+ [1.0, 0.0, 0.0, 0.0, 0.0, 12.00, -0.80, -0.67, -0.03],
904
+ [1.0, 0.0, 0.0, 1.0, 0.0, 1.73, -0.12, -0.10, 0.0],
905
+ [1.0, 0.0, 0.0, 2.0, 0.0, -0.04, 0.0, 0.0, 0.0],
906
+ [1.0, 1.0, 0.0, 0.0, -1.0, -0.50, -0.01, 0.03, 0.0],
907
+ [1.0, 1.0, 0.0, 0.0, 1.0, 0.01, 0.0, 0.0, 0.0],
908
+ [0.0, 1.0, 0.0, 1.0, -1.0, -0.01, 0.0, 0.0, 0.0],
909
+ [1.0, 2.0, -2.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
910
+ [1.0, 2.0, 0.0, 0.0, 0.0, -0.11, 0.01, 0.01, 0.0],
911
+ [2.0, -2.0, 1.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
912
+ [2.0, 0.0, -1.0, 0.0, 0.0, -0.02, 0.0, 0.0, 0.0],
913
+ [3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
914
+ [3.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
915
+ ];
916
+ let mut s = 218.31664563 + (481267.88194 + (-0.0014663889 + 0.00000185139 * t) * t) * t;
917
+ let mut tau = fhr * 15.0
918
+ + 280.4606184
919
+ + (36000.7700536 + (0.00038793 + -0.0000000258 * t) * t) * t
920
+ + (-s);
921
+ let pr = (1.396971278 + (0.000308889 + (0.000000021 + 0.000000007 * t) * t) * t) * t;
922
+ s += pr;
923
+ let mut h = 280.46645
924
+ + (36000.7697489 + (0.00030322222 + (0.000000020 + -0.00000000654 * t) * t) * t) * t;
925
+ let mut p = 83.35324312
926
+ + (4069.01363525 + (-0.01032172222 + (-0.0000124991 + 0.00000005263 * t) * t) * t) * t;
927
+ let mut zns = 234.95544499
928
+ + (1934.13626197 + (-0.00207561111 + (-0.00000213944 + 0.00000001650 * t) * t) * t) * t;
929
+ let mut ps = 282.93734098
930
+ + (1.71945766667 + (0.00045688889 + (-0.00000001778 + -0.00000000334 * t) * t) * t) * t;
931
+
932
+ s = s.rem_euclid(360.0);
933
+ tau = tau.rem_euclid(360.0);
934
+ h = h.rem_euclid(360.0);
935
+ p = p.rem_euclid(360.0);
936
+ zns = zns.rem_euclid(360.0);
937
+ ps = ps.rem_euclid(360.0);
938
+
939
+ let rsta = (xsta[0] * xsta[0] + xsta[1] * xsta[1] + xsta[2] * xsta[2]).sqrt();
940
+ let sinphi = xsta[2] / rsta;
941
+ let cosphi = (xsta[0] * xsta[0] + xsta[1] * xsta[1]).sqrt() / rsta;
942
+ let cosla = xsta[0] / cosphi / rsta;
943
+ let sinla = xsta[1] / cosphi / rsta;
944
+ let zla = xsta[1].atan2(xsta[0]);
945
+
946
+ let mut xcorsta = [0.0_f64; 3];
947
+ for w in &DATDI {
948
+ let thetaf = (tau + w[0] * s + w[1] * h + w[2] * p + w[3] * zns + w[4] * ps) * DEG_TO_RAD;
949
+ let dr = w[5] * 2.0 * sinphi * cosphi * (thetaf + zla).sin()
950
+ + w[6] * 2.0 * sinphi * cosphi * (thetaf + zla).cos();
951
+ let dn = w[7] * (cosphi * cosphi - sinphi * sinphi) * (thetaf + zla).sin()
952
+ + w[8] * (cosphi * cosphi - sinphi * sinphi) * (thetaf + zla).cos();
953
+ let de = w[7] * sinphi * (thetaf + zla).cos() - w[8] * sinphi * (thetaf + zla).sin();
954
+
955
+ xcorsta[0] += dr * cosla * cosphi - de * sinla - dn * sinphi * cosla;
956
+ xcorsta[1] += dr * sinla * cosphi + de * cosla - dn * sinphi * sinla;
957
+ xcorsta[2] += dr * sinphi + dn * cosphi;
958
+ }
959
+ for v in &mut xcorsta {
960
+ *v /= KM_TO_M;
961
+ }
962
+ xcorsta
963
+ }
964
+
965
+ /// In-phase / out-of-phase frequency-dependent corrections, long-period band
966
+ /// (STEP2LON). `t` is Julian centuries (TT).
967
+ fn frequency_dependent_long_period_correction(xsta: &[f64; 3], t: f64) -> [f64; 3] {
968
+ #[rustfmt::skip]
969
+ const DATDI: [[f64; 9]; 5] = [
970
+ [0.0, 0.0, 0.0, 1.0, 0.0, 0.47, 0.23, 0.16, 0.07],
971
+ [0.0, 2.0, 0.0, 0.0, 0.0, -0.20, -0.12, -0.11, -0.05],
972
+ [1.0, 0.0, -1.0, 0.0, 0.0, -0.11, -0.08, -0.09, -0.04],
973
+ [2.0, 0.0, 0.0, 0.0, 0.0, -0.13, -0.11, -0.15, -0.07],
974
+ [2.0, 0.0, 0.0, 1.0, 0.0, -0.05, -0.05, -0.06, -0.03],
975
+ ];
976
+ let mut s = 218.31664563 + (481267.88194 + (-0.0014663889 + 0.00000185139 * t) * t) * t;
977
+ let pr = (1.396971278 + (0.000308889 + (0.000000021 + 0.000000007 * t) * t) * t) * t;
978
+ s += pr;
979
+ let mut h = 280.46645
980
+ + (36000.7697489 + (0.00030322222 + (0.000000020 + -0.00000000654 * t) * t) * t) * t;
981
+ let mut p = 83.35324312
982
+ + (4069.01363525 + (-0.01032172222 + (-0.0000124991 + 0.00000005263 * t) * t) * t) * t;
983
+ let mut zns = 234.95544499
984
+ + (1934.13626197 + (-0.00207561111 + (-0.00000213944 + 0.00000001650 * t) * t) * t) * t;
985
+ let mut ps = 282.93734098
986
+ + (1.71945766667 + (0.00045688889 + (-0.00000001778 + -0.00000000334 * t) * t) * t) * t;
987
+
988
+ let rsta = (xsta[0] * xsta[0] + xsta[1] * xsta[1] + xsta[2] * xsta[2]).sqrt();
989
+ let sinphi = xsta[2] / rsta;
990
+ let cosphi = (xsta[0] * xsta[0] + xsta[1] * xsta[1]).sqrt() / rsta;
991
+ let cosla = xsta[0] / cosphi / rsta;
992
+ let sinla = xsta[1] / cosphi / rsta;
993
+
994
+ s = s.rem_euclid(360.0);
995
+ h = h.rem_euclid(360.0);
996
+ p = p.rem_euclid(360.0);
997
+ zns = zns.rem_euclid(360.0);
998
+ ps = ps.rem_euclid(360.0);
999
+
1000
+ let mut xcorsta = [0.0_f64; 3];
1001
+ for w in &DATDI {
1002
+ let thetaf = (w[0] * s + w[1] * h + w[2] * p + w[3] * zns + w[4] * ps) * DEG_TO_RAD;
1003
+ let dr = w[5] * (3.0 * sinphi * sinphi - 1.0) / 2.0 * thetaf.cos()
1004
+ + w[7] * (3.0 * sinphi * sinphi - 1.0) / 2.0 * thetaf.sin();
1005
+ let dn = w[6] * (cosphi * sinphi * 2.0) * thetaf.cos()
1006
+ + w[8] * (cosphi * sinphi * 2.0) * thetaf.sin();
1007
+ let de = 0.0;
1008
+
1009
+ xcorsta[0] += dr * cosla * cosphi - de * sinla - dn * sinphi * cosla;
1010
+ xcorsta[1] += dr * sinla * cosphi + de * cosla - dn * sinphi * sinla;
1011
+ xcorsta[2] += dr * sinphi + dn * cosphi;
1012
+ }
1013
+ for v in &mut xcorsta {
1014
+ *v /= KM_TO_M;
1015
+ }
1016
+ xcorsta
1017
+ }
1018
+
1019
+ /// Gregorian calendar date -> (MJD epoch 2400000.5, MJD) (SOFA CAL2JD).
1020
+ ///
1021
+ /// This is a SOFA parity adapter, deliberately NOT routed through
1022
+ /// [`crate::astro::time::civil`]: the solid-Earth/ocean/pole tide models are
1023
+ /// validated bit-for-bit against the IERS/SOFA reference (the
1024
+ /// `ocean_loading_oracle` test), so the calendar-to-MJD step must reproduce
1025
+ /// SOFA's `iauCal2jd` exactly. It is kept local under this tides-specific name
1026
+ /// so it is not mistaken for a duplicate of the canonical civil conversions and
1027
+ /// is never consolidated into them.
1028
+ fn gregorian_to_two_part_julian_date(iy: i32, im: i32, id: i32) -> (f64, f64) {
1029
+ let my = (im - 14) / 12;
1030
+ let iypmy = iy + my;
1031
+ let djm0 = 2400000.5;
1032
+ let djm = ((1461 * (iypmy + 4800)) / 4 + (367 * (im - 2 - 12 * my)) / 12
1033
+ - (3 * ((iypmy + 4900) / 100)) / 4
1034
+ + id
1035
+ - 2432076) as f64;
1036
+ (djm0, djm)
1037
+ }
1038
+
1039
+ /// TAI-UTC (Delta(AT)) in seconds for the given date (SOFA DAT, post-1972
1040
+ /// leap-second table only). The four golden dates are all post-1972; SOFA's
1041
+ /// pre-1972 drift terms are not implemented here.
1042
+ fn tai_minus_utc_seconds(iy: i32, im: i32, _id: i32) -> f64 {
1043
+ // Post-1972 leap-second table: (year, month, Delta(AT) seconds).
1044
+ const IDAT: [(i32, i32, f64); 28] = [
1045
+ (1972, 1, 10.0),
1046
+ (1972, 7, 11.0),
1047
+ (1973, 1, 12.0),
1048
+ (1974, 1, 13.0),
1049
+ (1975, 1, 14.0),
1050
+ (1976, 1, 15.0),
1051
+ (1977, 1, 16.0),
1052
+ (1978, 1, 17.0),
1053
+ (1979, 1, 18.0),
1054
+ (1980, 1, 19.0),
1055
+ (1981, 7, 20.0),
1056
+ (1982, 7, 21.0),
1057
+ (1983, 7, 22.0),
1058
+ (1985, 7, 23.0),
1059
+ (1988, 1, 24.0),
1060
+ (1990, 1, 25.0),
1061
+ (1991, 1, 26.0),
1062
+ (1992, 7, 27.0),
1063
+ (1993, 7, 28.0),
1064
+ (1994, 7, 29.0),
1065
+ (1996, 1, 30.0),
1066
+ (1997, 7, 31.0),
1067
+ (1999, 1, 32.0),
1068
+ (2006, 1, 33.0),
1069
+ (2009, 1, 34.0),
1070
+ (2012, 7, 35.0),
1071
+ (2015, 7, 36.0),
1072
+ (2017, 1, 37.0),
1073
+ ];
1074
+ let m = 12 * iy + im;
1075
+ let mut da = IDAT[0].2;
1076
+ for &(y, mo, d) in &IDAT {
1077
+ if m >= 12 * y + mo {
1078
+ da = d;
1079
+ }
1080
+ }
1081
+ da
1082
+ }