@neilberkman/sidereon 2.1.1 → 3.0.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.
@@ -12,11 +12,33 @@
12
12
  //! displacement, the out-of-phase corrections (`ST1IDIU`, `ST1ISEM`), the
13
13
  //! latitude-dependence correction (`ST1L1`), and the frequency-dependent
14
14
  //! step-2 diurnal/long-period band corrections (`STEP2DIU`, `STEP2LON`),
15
- //! evaluating the identical Love/Shida numbers, Doodson/argument tables, and
15
+ //! evaluating the identical Love/Shida numbers, long-period table and
16
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.
17
+ //! * Its diurnal (Step 2) table has two variants, [`StationTideConstants`].
18
+ //! [`StationTideConstants::IersRoutine`] is the routine's `DATDI` array as
19
+ //! distributed. [`StationTideConstants::Conventions`], the default, corrects
20
+ //! three rows of it to the Conventions text and the physics it states:
21
+ //! - K1 out-of-phase radial amplitude -0.78 mm, as Table 7.3a prints it in
22
+ //! the 2010 edition and in the chapter update of 1 February 2018, and as
23
+ //! Equation (7.12c) gives it from the K1 row of Table 7.2 (h(0)I = 0.0030
24
+ //! against the nominal hI = -0.0025, with the Cartwright-Tayler-Edden
25
+ //! amplitude Hf = 0.368645 m of the IERS routine `ADMINT.F`: -0.783 mm).
26
+ //! The routine (revision of 19 December 2016) and RTKLIB use -0.80 mm.
27
+ //! - P1 out-of-phase radial amplitude +0.07 mm. Table 7.3a and the routine
28
+ //! both have -0.07 mm, but Equation (7.12c) with Table 7.2 (P1
29
+ //! h(0)I = -0.0011, Hf = -0.121995 m) gives +0.066 mm; the sign error was
30
+ //! identified by H. Krásná and is corrected in Orekit's copy of Table 7.3a.
31
+ //! - The 25th row is tide 166,564 (s multiplier 1), the tide Tables 6.5a and
32
+ //! 7.2 list between psi1 and 167,355; the routine's s multiplier 0 names
33
+ //! 156,564, out of the table's frequency order and in no Conventions table.
34
+ //!
35
+ //! The Conventions' version notes (v1.0.0 to v1.3.0) list no correction to
36
+ //! either source. The two variants differ by at most the sum of the three
37
+ //! changes, 0.02 + 0.14 + 0.02 mm, in the displacement.
38
+ //! * It applies the permanent part of the displacement: the routine's
39
+ //! commented-out "Step 3" permanent-tide removal stays out, so coordinates
40
+ //! corrected with it are "conventional tide free", the system of the ITRF
41
+ //! (IERS Conventions (2010), Section 7.1.1.2).
20
42
  //! * The routine names are changed from the IERS originals (per the IERS
21
43
  //! Conventions Software License), and the Fortran subroutine structure is
22
44
  //! inlined into private helpers.
@@ -43,7 +65,8 @@ mod ocean;
43
65
  mod pole;
44
66
  pub use ocean::{
45
67
  ocean_tide_loading, parse_ocean_loading_blq_block, parse_ocean_loading_blq_blocks,
46
- OceanLoadingBlq, OceanLoadingBlqBlock, OceanTideConstituent, NUM_OCEAN_CONSTITUENTS,
68
+ write_ocean_loading_blq_blocks, OceanLoadingBlq, OceanLoadingBlqBlock, OceanLoadingBlqComment,
69
+ OceanLoadingBlqCommentPlacement, OceanTideConstituent, NUM_OCEAN_CONSTITUENTS,
47
70
  OCEAN_LOADING_CONSTITUENTS,
48
71
  };
49
72
  pub use pole::solid_earth_pole_tide;
@@ -54,58 +77,214 @@ use crate::astro::constants::time::{
54
77
  DAYS_PER_JULIAN_CENTURY, J2000_JD, SECONDS_PER_DAY, TT_MINUS_TAI_S,
55
78
  };
56
79
  use crate::astro::constants::units::{ARCSEC_TO_RAD, DEG_TO_RAD, KM_TO_M};
57
- use crate::astro::frames::transforms::{FrameTransformError, PolarMotion};
80
+ use crate::astro::frames::transforms::{FrameTransformError, PolarMotion, Ut1Gate};
58
81
  use crate::astro::math::vec3::{dot3_ref as dot, norm3_ref as norm8};
59
- use crate::astro::time::{CoverageError, TimeScaleInputErrorKind, TimeScales};
82
+ use crate::astro::time::{
83
+ CoverageError, TimeScaleInputErrorKind, TimeScales, Validated, ValidityMode,
84
+ };
60
85
  use crate::frame::{geodetic_to_itrf, ItrfPositionM, Wgs84Geodetic};
61
86
  use crate::validate::{self, FieldError};
62
87
 
63
88
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
89
+ /// Validation categories carried by [`TideError::InvalidInput`].
90
+ ///
91
+ /// The categories are converted from shared field and time-scale validation
92
+ /// errors before they are exposed by the tide APIs.
64
93
  pub enum TideInputErrorKind {
94
+ /// A required input was not supplied.
65
95
  Missing,
96
+ /// An input value or component was not finite.
66
97
  NonFinite,
98
+ /// A value required to be positive was zero or negative.
67
99
  NotPositive,
100
+ /// A value failed a negative-value check.
68
101
  Negative,
102
+ /// A finite input was outside its permitted domain.
69
103
  OutOfRange,
104
+ /// Text could not be parsed as a floating-point value.
70
105
  FloatParse,
106
+ /// Text could not be parsed as an integer value.
71
107
  IntParse,
108
+ /// A calendar date failed civil-date validation.
72
109
  InvalidCivilDate,
110
+ /// A clock value failed civil-time validation.
73
111
  InvalidCivilTime,
74
112
  }
75
113
 
76
114
  #[derive(Debug, Clone, PartialEq, Eq)]
115
+ /// Failure details produced while parsing a BLQ station block.
116
+ ///
117
+ /// Numeric rows and constituent headers use these variants to retain the
118
+ /// offending count, token, station, or constituent label.
77
119
  pub enum BlqParseErrorKind {
120
+ /// The input contained no non-whitespace content.
78
121
  Empty,
122
+ /// A numeric coefficient row appeared before a station identifier.
79
123
  MissingStation,
124
+ /// A station ended before its six coefficient rows were complete.
80
125
  MissingCoefficientRows {
126
+ /// Trimmed station identifier for the incomplete block.
81
127
  station: String,
128
+ /// Required number of coefficient rows, which is six.
82
129
  expected: usize,
130
+ /// Number of coefficient rows collected before end of input.
83
131
  found: usize,
84
132
  },
133
+ /// The active station accumulated more than six coefficient rows.
85
134
  TooManyCoefficientRows {
135
+ /// Active station identifier when the extra row was read.
86
136
  station: String,
87
137
  },
138
+ /// A numeric row or recognized header did not contain eleven columns.
88
139
  WrongColumnCount {
140
+ /// Required ARG2 column count, equal to `NUM_OCEAN_CONSTITUENTS`.
89
141
  expected: usize,
142
+ /// Number of tokens or constituent labels found on the line.
90
143
  found: usize,
91
144
  },
145
+ /// A coefficient token could not be parsed as an `f64`.
92
146
  InvalidNumber {
147
+ /// Original token, or the trimmed line reported as a numeric candidate.
93
148
  token: String,
94
149
  },
150
+ /// A token parsed as an `f64` but produced a non-finite value.
95
151
  NonFiniteNumber {
152
+ /// Original token that produced the non-finite value.
96
153
  token: String,
97
154
  },
155
+ /// A constituent-like header label is absent from the supported table.
98
156
  UnsupportedConstituent {
157
+ /// Normalized uppercase label rejected by the constituent lookup.
99
158
  constituent: String,
100
159
  },
160
+ /// A supported constituent occurred more than once in a header.
101
161
  DuplicateConstituent {
162
+ /// Canonical label of the repeated constituent.
102
163
  constituent: String,
103
164
  },
165
+ /// The single-block parser found more than one complete station block.
104
166
  MultipleBlocks {
167
+ /// Number of complete station blocks found.
105
168
  found: usize,
106
169
  },
107
170
  }
108
171
 
172
+ #[derive(Debug, Clone, PartialEq, Eq)]
173
+ /// Reasons a BLQ block cannot be written so that the parser reads it back
174
+ /// unchanged.
175
+ pub enum BlqWriteErrorKind {
176
+ /// The station identifier is empty.
177
+ EmptyStation,
178
+ /// The station identifier contains a line break.
179
+ StationLineBreak,
180
+ /// The station identifier has leading or trailing whitespace, which the
181
+ /// parser trims.
182
+ StationSurroundingWhitespace,
183
+ /// The station identifier starts with a comment marker (`$`, `#`, `!`).
184
+ StationReadsAsComment,
185
+ /// The station identifier would be read as a column-order header.
186
+ StationReadsAsHeader,
187
+ /// The station identifier would be read as a coefficient row.
188
+ StationReadsAsCoefficientRow,
189
+ /// A coefficient is NaN or infinite.
190
+ NonFiniteCoefficient {
191
+ /// Zero-based BLQ row: amplitudes radial, EW, NS, then phases.
192
+ row: usize,
193
+ /// Constituent of the value.
194
+ constituent: crate::tides::OceanTideConstituent,
195
+ },
196
+ /// A retained comment line contains a line break or ends with a carriage
197
+ /// return.
198
+ CommentLineBreak {
199
+ /// Index in [`crate::tides::OceanLoadingBlqBlock::comments`].
200
+ index: usize,
201
+ },
202
+ /// A retained line is blank or has no comment marker and is not a
203
+ /// column-order header, so it would not be read as a comment.
204
+ NotACommentLine {
205
+ /// Index in [`crate::tides::OceanLoadingBlqBlock::comments`].
206
+ index: usize,
207
+ },
208
+ /// A retained comment names a coefficient row after the sixth.
209
+ CommentPlacementOutOfRange {
210
+ /// Index in [`crate::tides::OceanLoadingBlqBlock::comments`].
211
+ index: usize,
212
+ },
213
+ /// A retained line is a column-order header the parser refuses.
214
+ InvalidHeader {
215
+ /// Index in [`crate::tides::OceanLoadingBlqBlock::comments`].
216
+ index: usize,
217
+ /// The parser's refusal.
218
+ kind: BlqParseErrorKind,
219
+ },
220
+ /// A retained comment is placed after the coefficient rows of a block
221
+ /// that is not the last one written; the parser reads such a line as part
222
+ /// of the next block.
223
+ AfterRowsBeforeAnotherBlock {
224
+ /// Index in [`crate::tides::OceanLoadingBlqBlock::comments`].
225
+ index: usize,
226
+ },
227
+ /// Retained comments are not grouped by placement in file order (before
228
+ /// the station, before rows 0 to 5, after the rows); the parser would read
229
+ /// them back in that order.
230
+ CommentsOutOfPlacementOrder {
231
+ /// Index of the first comment placed before its predecessor.
232
+ index: usize,
233
+ },
234
+ }
235
+
236
+ impl core::fmt::Display for BlqWriteErrorKind {
237
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
238
+ match self {
239
+ Self::EmptyStation => f.write_str("empty station identifier"),
240
+ Self::StationLineBreak => f.write_str("station identifier contains a line break"),
241
+ Self::StationSurroundingWhitespace => {
242
+ f.write_str("station identifier has leading or trailing whitespace")
243
+ }
244
+ Self::StationReadsAsComment => {
245
+ f.write_str("station identifier starts with a comment marker")
246
+ }
247
+ Self::StationReadsAsHeader => {
248
+ f.write_str("station identifier reads as a column-order header")
249
+ }
250
+ Self::StationReadsAsCoefficientRow => {
251
+ f.write_str("station identifier reads as a coefficient row")
252
+ }
253
+ Self::NonFiniteCoefficient { row, constituent } => write!(
254
+ f,
255
+ "coefficient row {row} constituent {} is not finite",
256
+ constituent.label()
257
+ ),
258
+ Self::CommentLineBreak { index } => {
259
+ write!(f, "comment {index} contains a line break")
260
+ }
261
+ Self::NotACommentLine { index } => {
262
+ write!(f, "comment {index} would not be read as a comment")
263
+ }
264
+ Self::CommentPlacementOutOfRange { index } => {
265
+ write!(
266
+ f,
267
+ "comment {index} is placed after the sixth coefficient row"
268
+ )
269
+ }
270
+ Self::InvalidHeader { index, kind } => {
271
+ write!(
272
+ f,
273
+ "comment {index} is a column-order header the parser refuses: {kind}"
274
+ )
275
+ }
276
+ Self::AfterRowsBeforeAnotherBlock { index } => write!(
277
+ f,
278
+ "comment {index} follows the coefficient rows of a block that is not the last"
279
+ ),
280
+ Self::CommentsOutOfPlacementOrder { index } => write!(
281
+ f,
282
+ "comment {index} is placed before the comment preceding it"
283
+ ),
284
+ }
285
+ }
286
+ }
287
+
109
288
  impl core::fmt::Display for BlqParseErrorKind {
110
289
  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
111
290
  match self {
@@ -176,25 +355,50 @@ impl From<&FieldError> for TideInputErrorKind {
176
355
  }
177
356
 
178
357
  #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
358
+ /// Errors returned by station displacement evaluators and BLQ parsers.
359
+ ///
360
+ /// Validation failures carry normalized field and reason data; other variants
361
+ /// preserve the underlying time-scale, frame, ephemeris, or parser failure.
179
362
  pub enum TideError {
363
+ /// A tide input failed validation.
180
364
  #[error("invalid solid-earth tide input {field}: {kind}")]
181
365
  InvalidInput {
366
+ /// Field label supplied by the originating validator or constructor.
182
367
  field: &'static str,
368
+ /// Normalized validation category for the field.
183
369
  kind: TideInputErrorKind,
184
370
  },
371
+ /// Time-scale conversion failed for a non-input coverage or conversion reason.
185
372
  #[error("station displacement time-scale conversion failed: {0}")]
186
373
  TimeScale(#[from] CoverageError),
374
+ /// Geodetic, ECEF, or polar-motion frame conversion failed.
187
375
  #[error("station displacement frame transform failed: {0}")]
188
376
  FrameTransform(#[from] FrameTransformError),
377
+ /// Polar-motion-aware Sun/Moon evaluation failed.
189
378
  #[error("station displacement Sun/Moon evaluation failed: {0}")]
190
379
  SunMoon(#[from] SunMoonError),
380
+ /// A required high-level station-displacement input was not supplied.
191
381
  #[error("missing station displacement input {field}")]
192
- MissingInput { field: &'static str },
382
+ MissingInput {
383
+ /// Missing-input label; the dispatcher uses `"polar motion"` here.
384
+ field: &'static str,
385
+ },
386
+ /// A BLQ parser rejected an input line or whole-input condition.
193
387
  #[error("invalid BLQ block at line {line}: {kind}")]
194
388
  BlqParse {
389
+ /// One-based offending line number, or zero for whole-input failures.
195
390
  line: usize,
391
+ /// Detailed BLQ parsing failure and its source payload.
196
392
  kind: BlqParseErrorKind,
197
393
  },
394
+ /// A BLQ block could not be written so that it reads back unchanged.
395
+ #[error("cannot write BLQ block {block}: {kind}")]
396
+ BlqWrite {
397
+ /// Zero-based index of the block in the written sequence.
398
+ block: usize,
399
+ /// Reason the block was refused.
400
+ kind: BlqWriteErrorKind,
401
+ },
198
402
  }
199
403
 
200
404
  fn invalid_tide_input(error: FieldError) -> TideError {
@@ -278,11 +482,17 @@ impl StationDisplacementPosition {
278
482
  /// IERS polar-motion coordinates of the epoch, in arcseconds.
279
483
  #[derive(Debug, Clone, Copy, PartialEq)]
280
484
  pub struct StationPolarMotion {
485
+ /// IERS x-pole coordinate supplied in arcseconds.
281
486
  pub xp_arcsec: f64,
487
+ /// IERS y-pole coordinate supplied in arcseconds.
282
488
  pub yp_arcsec: f64,
283
489
  }
284
490
 
285
491
  impl StationPolarMotion {
492
+ /// Construct from the IERS x- and y-pole coordinates in arcseconds.
493
+ ///
494
+ /// The coordinates are stored as supplied; tide evaluation performs the
495
+ /// conversion and validation required by its downstream model.
286
496
  pub const fn from_arcseconds(xp_arcsec: f64, yp_arcsec: f64) -> Self {
287
497
  Self {
288
498
  xp_arcsec,
@@ -301,11 +511,17 @@ impl StationPolarMotion {
301
511
  /// UTC epoch for station displacement evaluation.
302
512
  #[derive(Debug, Clone, Copy, PartialEq)]
303
513
  pub struct StationDisplacementEpoch {
514
+ /// UTC calendar year passed to validation and tide models.
304
515
  pub year: i32,
516
+ /// UTC calendar month passed to validation and tide models.
305
517
  pub month: u8,
518
+ /// UTC calendar day passed to validation and tide models.
306
519
  pub day: u8,
520
+ /// UTC hour used by the `fractional_hour` calculation.
307
521
  pub hour: u8,
522
+ /// UTC minute used by the `fractional_hour` calculation.
308
523
  pub minute: u8,
524
+ /// UTC seconds, including a fractional part, used in time conversion and fractional-hour calculation.
309
525
  pub second: f64,
310
526
  /// Optional IERS polar motion for pole tide and polar-motion-aware Sun/Moon
311
527
  /// rotation.
@@ -313,6 +529,10 @@ pub struct StationDisplacementEpoch {
313
529
  }
314
530
 
315
531
  impl StationDisplacementEpoch {
532
+ /// Construct an epoch from UTC calendar and clock components.
533
+ ///
534
+ /// The returned epoch has no polar motion until a caller adds it with
535
+ /// [`StationDisplacementEpoch::with_polar_motion_arcsec`].
316
536
  pub const fn from_utc(
317
537
  year: i32,
318
538
  month: u8,
@@ -332,6 +552,7 @@ impl StationDisplacementEpoch {
332
552
  }
333
553
  }
334
554
 
555
+ /// Return this epoch with the supplied IERS polar motion in arcseconds.
335
556
  pub const fn with_polar_motion_arcsec(mut self, xp_arcsec: f64, yp_arcsec: f64) -> Self {
336
557
  self.polar_motion = Some(StationPolarMotion::from_arcseconds(xp_arcsec, yp_arcsec));
337
558
  self
@@ -370,6 +591,7 @@ impl StationDisplacementEpoch {
370
591
 
371
592
  /// Switches for the high-level station displacement entry.
372
593
  #[derive(Debug, Clone, Copy, PartialEq)]
594
+ #[non_exhaustive]
373
595
  pub struct StationDisplacementOptions<'a> {
374
596
  /// Apply the IERS solid Earth tide station displacement.
375
597
  pub solid_earth_tide: bool,
@@ -378,6 +600,9 @@ pub struct StationDisplacementOptions<'a> {
378
600
  pub pole_tide: bool,
379
601
  /// Optional ocean-loading BLQ coefficients supplied by the caller.
380
602
  pub ocean_loading: Option<&'a OceanLoadingBlq>,
603
+ /// Diurnal Step 2 constants of the solid Earth tide;
604
+ /// [`StationTideConstants::Conventions`] by default.
605
+ pub solid_earth_tide_constants: StationTideConstants,
381
606
  }
382
607
 
383
608
  impl Default for StationDisplacementOptions<'_> {
@@ -386,6 +611,7 @@ impl Default for StationDisplacementOptions<'_> {
386
611
  solid_earth_tide: true,
387
612
  pole_tide: false,
388
613
  ocean_loading: None,
614
+ solid_earth_tide_constants: StationTideConstants::Conventions,
389
615
  }
390
616
  }
391
617
  }
@@ -395,8 +621,11 @@ impl Default for StationDisplacementOptions<'_> {
395
621
  pub struct StationDisplacement {
396
622
  /// Sum of all enabled component displacements, in ITRF/ECEF metres.
397
623
  pub ecef_m: [f64; 3],
624
+ /// Solid-Earth tide component, or `None` when that correction is disabled.
398
625
  pub solid_earth_tide_ecef_m: Option<[f64; 3]>,
626
+ /// Pole-tide component, or `None` when that correction is disabled.
399
627
  pub pole_tide_ecef_m: Option<[f64; 3]>,
628
+ /// Ocean-loading component, or `None` when no BLQ coefficients are supplied.
400
629
  pub ocean_loading_ecef_m: Option<[f64; 3]>,
401
630
  }
402
631
 
@@ -421,33 +650,55 @@ impl StationDisplacement {
421
650
  /// metre components.
422
651
  ///
423
652
  /// The solid Earth tide path uses IERS Conventions (2010), Chapter 7 station
424
- /// displacement with the permanent tide retained. The low-level
653
+ /// displacement with its permanent part applied. The low-level
425
654
  /// [`solid_earth_tide`] routine ships the in-phase degree-2 and degree-3
426
655
  /// displacement, the step-1 out-of-phase and latitude-dependence corrections,
427
656
  /// 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
657
+ /// optional step-3 permanent-tide removal out, so corrected coordinates are
658
+ /// conventional tide free, as the ITRF is. Sun/Moon
429
659
  /// positions are generated through the same Earth-fixed analytic ephemeris path
430
660
  /// used by the tide-force lane, including caller-supplied polar motion when the
431
661
  /// epoch carries it.
662
+ ///
663
+ /// The solid Earth tide rotates the Sun and Moon into ITRF with UT1, so it
664
+ /// refuses an epoch outside the UT1 table; see
665
+ /// [`station_displacement_ecef_m_with_validity`].
432
666
  pub fn station_displacement_ecef_m(
433
667
  position: StationDisplacementPosition,
434
668
  epoch: StationDisplacementEpoch,
435
669
  options: StationDisplacementOptions<'_>,
436
670
  ) -> Result<StationDisplacement, TideError> {
671
+ station_displacement_ecef_m_with_validity(position, epoch, options, ValidityMode::Strict)
672
+ .map(|validated| validated.value)
673
+ }
674
+
675
+ /// [`station_displacement_ecef_m`] under an explicit UT1 [`ValidityMode`].
676
+ ///
677
+ /// Only the solid Earth tide reads UT1. [`ValidityMode::Strict`] refuses an
678
+ /// epoch outside the UT1 table when it is enabled;
679
+ /// [`ValidityMode::Permissive`] evaluates it with the long-term UT1 and
680
+ /// reports the departure in [`Validated::degraded`].
681
+ pub fn station_displacement_ecef_m_with_validity(
682
+ position: StationDisplacementPosition,
683
+ epoch: StationDisplacementEpoch,
684
+ options: StationDisplacementOptions<'_>,
685
+ mode: ValidityMode,
686
+ ) -> Result<Validated<StationDisplacement>, TideError> {
437
687
  let receiver_ecef_m = position.ecef_m()?;
438
688
  epoch.validate_utc()?;
439
689
  let fhr = epoch.fractional_hour();
440
690
  let mut displacement = StationDisplacement::zero();
691
+ let gate = Ut1Gate::new(mode);
441
692
 
442
693
  if options.solid_earth_tide {
443
- let ts = epoch.time_scales()?;
694
+ let ts = gate.admit(epoch.time_scales()?)?;
444
695
  let polar_motion = epoch
445
696
  .polar_motion
446
697
  .map(StationPolarMotion::polar_motion)
447
698
  .transpose()?
448
699
  .unwrap_or_default();
449
700
  let sun_moon = sun_moon_ecef_with_polar_motion(&ts, polar_motion)?;
450
- let solid = solid_earth_tide(
701
+ let solid = solid_earth_tide_with_constants(
451
702
  &receiver_ecef_m,
452
703
  epoch.year,
453
704
  i32::from(epoch.month),
@@ -455,6 +706,7 @@ pub fn station_displacement_ecef_m(
455
706
  fhr,
456
707
  &sun_moon.sun,
457
708
  &sun_moon.moon,
709
+ options.solid_earth_tide_constants,
458
710
  )?;
459
711
  StationDisplacement::add_component(&mut displacement.ecef_m, solid);
460
712
  displacement.solid_earth_tide_ecef_m = Some(solid);
@@ -490,7 +742,7 @@ pub fn station_displacement_ecef_m(
490
742
  displacement.ocean_loading_ecef_m = Some(ocean);
491
743
  }
492
744
 
493
- Ok(displacement)
745
+ Ok(gate.finish(displacement)?)
494
746
  }
495
747
 
496
748
  /// Evaluate station displacement for many epochs. Each element is equivalent to
@@ -507,6 +759,20 @@ pub fn station_displacement_ecef_m_batch(
507
759
  .collect()
508
760
  }
509
761
 
762
+ /// [`station_displacement_ecef_m_batch`] under an explicit UT1
763
+ /// [`ValidityMode`]; each row is [`station_displacement_ecef_m_with_validity`].
764
+ pub fn station_displacement_ecef_m_batch_with_validity(
765
+ position: StationDisplacementPosition,
766
+ epochs: &[StationDisplacementEpoch],
767
+ options: StationDisplacementOptions<'_>,
768
+ mode: ValidityMode,
769
+ ) -> Vec<Result<Validated<StationDisplacement>, TideError>> {
770
+ epochs
771
+ .iter()
772
+ .map(|&epoch| station_displacement_ecef_m_with_validity(position, epoch, options, mode))
773
+ .collect()
774
+ }
775
+
510
776
  /// Solid-earth tide displacement of an ITRF station, in metres (ECEF).
511
777
  ///
512
778
  /// Arguments mirror the IERS reference routine:
@@ -516,8 +782,12 @@ pub fn station_displacement_ecef_m_batch(
516
782
  /// * `xsun` - geocentric Sun position (m, ECEF).
517
783
  /// * `xmon` - geocentric Moon position (m, ECEF).
518
784
  ///
519
- /// Returns the displacement vector `dxtide` (m, geocentric ITRF). The permanent
520
- /// (mean) tide deformation is retained (ITRF/IGS convention).
785
+ /// Returns the displacement vector `dxtide` (m, geocentric ITRF), permanent
786
+ /// part included, so corrected coordinates are conventional tide free, as the
787
+ /// ITRF is.
788
+ ///
789
+ /// The diurnal Step 2 constants are [`StationTideConstants::Conventions`];
790
+ /// [`solid_earth_tide_with_constants`] chooses them.
521
791
  ///
522
792
  /// Returns [`TideError`] when inputs are non-finite or geometrically
523
793
  /// degenerate: the station vector must be non-zero and non-polar, and Sun/Moon
@@ -530,10 +800,36 @@ pub fn solid_earth_tide(
530
800
  fhr: f64,
531
801
  xsun: &[f64; 3],
532
802
  xmon: &[f64; 3],
803
+ ) -> Result<[f64; 3], TideError> {
804
+ solid_earth_tide_with_constants(
805
+ xsta,
806
+ year,
807
+ month,
808
+ day,
809
+ fhr,
810
+ xsun,
811
+ xmon,
812
+ StationTideConstants::Conventions,
813
+ )
814
+ }
815
+
816
+ /// [`solid_earth_tide`] with the diurnal Step 2 constants `constants`.
817
+ /// [`StationTideConstants::IersRoutine`] reproduces `DEHANTTIDEINEL.F` and
818
+ /// RTKLIB `tidedisp`.
819
+ #[allow(clippy::too_many_arguments)]
820
+ pub fn solid_earth_tide_with_constants(
821
+ xsta: &[f64; 3],
822
+ year: i32,
823
+ month: i32,
824
+ day: i32,
825
+ fhr: f64,
826
+ xsun: &[f64; 3],
827
+ xmon: &[f64; 3],
828
+ constants: StationTideConstants,
533
829
  ) -> Result<[f64; 3], TideError> {
534
830
  validate_tide_domain(xsta, year, month, day, fhr, xsun, xmon)?;
535
831
  Ok(solid_earth_tide_unchecked(
536
- xsta, year, month, day, fhr, xsun, xmon,
832
+ xsta, year, month, day, fhr, xsun, xmon, constants,
537
833
  ))
538
834
  }
539
835
 
@@ -572,6 +868,7 @@ fn validate_tide_domain(
572
868
  Ok(())
573
869
  }
574
870
 
871
+ #[allow(clippy::too_many_arguments)]
575
872
  fn solid_earth_tide_unchecked(
576
873
  xsta: &[f64; 3],
577
874
  year: i32,
@@ -580,6 +877,7 @@ fn solid_earth_tide_unchecked(
580
877
  fhr: f64,
581
878
  xsun: &[f64; 3],
582
879
  xmon: &[f64; 3],
880
+ constants: StationTideConstants,
583
881
  ) -> [f64; 3] {
584
882
  // Nominal second- and third-degree Love and Shida numbers.
585
883
  const H20: f64 = 0.6078;
@@ -606,8 +904,10 @@ fn solid_earth_tide_unchecked(
606
904
  let p2mon = 3.0 * (h2 / 2.0 - l2) * scmon * scmon - h2 / 2.0;
607
905
 
608
906
  // 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;
907
+ let scsun3 = scsun * scsun * scsun;
908
+ let scmon3 = scmon * scmon * scmon;
909
+ let p3sun = 5.0 / 2.0 * (H3 - 3.0 * L3) * scsun3 + 3.0 / 2.0 * (L3 - H3) * scsun;
910
+ let p3mon = 5.0 / 2.0 * (H3 - 3.0 * L3) * scmon3 + 3.0 / 2.0 * (L3 - H3) * scmon;
611
911
 
612
912
  // Term in direction of Sun/Moon vector.
613
913
  let x2sun = 3.0 * l2 * scsun;
@@ -619,8 +919,10 @@ fn solid_earth_tide_unchecked(
619
919
  const MASS_RATIO_SUN: f64 = 332946.0482;
620
920
  const MASS_RATIO_MOON: f64 = 0.0123000371;
621
921
  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);
922
+ let re_over_rsun = RE / rsun;
923
+ let re_over_rmon = RE / rmon;
924
+ let fac2sun = MASS_RATIO_SUN * RE * re_over_rsun * re_over_rsun * re_over_rsun;
925
+ let fac2mon = MASS_RATIO_MOON * RE * re_over_rmon * re_over_rmon * re_over_rmon;
624
926
  let fac3sun = fac2sun * (RE / rsun);
625
927
  let fac3mon = fac2mon * (RE / rmon);
626
928
 
@@ -651,10 +953,10 @@ fn solid_earth_tide_unchecked(
651
953
  let (jjm0, jjm1) = gregorian_to_two_part_julian_date(year, month, day);
652
954
  let fhrd = fhr / 24.0;
653
955
  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;
956
+ let dtt = tai_minus_utc_seconds(year, month, day, fhrd) + TT_MINUS_TAI_S;
655
957
  t += dtt / (SECONDS_PER_DAY * DAYS_PER_JULIAN_CENTURY);
656
958
 
657
- let c = frequency_dependent_diurnal_correction(xsta, fhr, t);
959
+ let c = frequency_dependent_diurnal_correction(xsta, fhr, t, constants.diurnal_table());
658
960
  for i in 0..3 {
659
961
  dxtide[i] += c[i];
660
962
  }
@@ -664,8 +966,8 @@ fn solid_earth_tide_unchecked(
664
966
  }
665
967
 
666
968
  // 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).
969
+ // stays out as it does in the routine, so the displacement includes its
970
+ // permanent part and corrected coordinates are conventional tide free.
669
971
  dxtide
670
972
  }
671
973
 
@@ -875,44 +1177,117 @@ fn latitude_dependence_correction(
875
1177
  xcorsta
876
1178
  }
877
1179
 
1180
+ /// Constants of the diurnal-band frequency-dependent (Step 2) station tide.
1181
+ ///
1182
+ /// See the module documentation for the three rows in which the variants
1183
+ /// differ and the sources for each.
1184
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1185
+ pub enum StationTideConstants {
1186
+ /// The IERS Conventions (2010) Chapter 7 text: Table 7.3a with the P1
1187
+ /// sign it misprints corrected, and tide 166,564 as Tables 6.5a and 7.2
1188
+ /// list it.
1189
+ #[default]
1190
+ Conventions,
1191
+ /// The `DATDI` array of the IERS routine `DEHANTTIDEINEL.F` (`STEP2DIU`)
1192
+ /// as distributed, which RTKLIB also uses.
1193
+ IersRoutine,
1194
+ }
1195
+
1196
+ impl StationTideConstants {
1197
+ fn diurnal_table(self) -> &'static [[f64; 9]; 31] {
1198
+ match self {
1199
+ Self::Conventions => &DIURNAL_BAND_CONVENTIONS,
1200
+ Self::IersRoutine => &DIURNAL_BAND_IERS_ROUTINE,
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ // Diurnal-band tables, 31 rows: multipliers of s, h, p, N', ps (tau's is 1),
1206
+ // then the radial in-phase, radial out-of-phase, transverse in-phase and
1207
+ // transverse out-of-phase amplitudes in mm, the columns of Table 7.3a.
1208
+
1209
+ /// `DATDI` of `STEP2DIU.F` as distributed.
1210
+ #[rustfmt::skip]
1211
+ const DIURNAL_BAND_IERS_ROUTINE: [[f64; 9]; 31] = [
1212
+ [-3.0, 0.0, 2.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
1213
+ [-3.0, 2.0, 0.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
1214
+ [-2.0, 0.0, 1.0, -1.0, 0.0, -0.02, 0.0, 0.0, 0.0],
1215
+ [-2.0, 0.0, 1.0, 0.0, 0.0, -0.08, 0.0, -0.01, 0.01],
1216
+ [-2.0, 2.0, -1.0, 0.0, 0.0, -0.02, 0.0, 0.0, 0.0],
1217
+ [-1.0, 0.0, 0.0, -1.0, 0.0, -0.10, 0.0, 0.0, 0.0],
1218
+ [-1.0, 0.0, 0.0, 0.0, 0.0, -0.51, 0.0, -0.02, 0.03],
1219
+ [-1.0, 2.0, 0.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0],
1220
+ [0.0, -2.0, 1.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0],
1221
+ [0.0, 0.0, -1.0, 0.0, 0.0, 0.02, 0.0, 0.0, 0.0],
1222
+ [0.0, 0.0, 1.0, 0.0, 0.0, 0.06, 0.0, 0.0, 0.0],
1223
+ [0.0, 0.0, 1.0, 1.0, 0.0, 0.01, 0.0, 0.0, 0.0],
1224
+ [0.0, 2.0, -1.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0],
1225
+ [1.0, -3.0, 0.0, 0.0, 1.0, -0.06, 0.0, 0.0, 0.0],
1226
+ [1.0, -2.0, 0.0, -1.0, 0.0, 0.01, 0.0, 0.0, 0.0],
1227
+ [1.0, -2.0, 0.0, 0.0, 0.0, -1.23, -0.07, 0.06, 0.01],
1228
+ [1.0, -1.0, 0.0, 0.0, -1.0, 0.02, 0.0, 0.0, 0.0],
1229
+ [1.0, -1.0, 0.0, 0.0, 1.0, 0.04, 0.0, 0.0, 0.0],
1230
+ [1.0, 0.0, 0.0, -1.0, 0.0, -0.22, 0.01, 0.01, 0.0],
1231
+ [1.0, 0.0, 0.0, 0.0, 0.0, 12.00, -0.80, -0.67, -0.03],
1232
+ [1.0, 0.0, 0.0, 1.0, 0.0, 1.73, -0.12, -0.10, 0.0],
1233
+ [1.0, 0.0, 0.0, 2.0, 0.0, -0.04, 0.0, 0.0, 0.0],
1234
+ [1.0, 1.0, 0.0, 0.0, -1.0, -0.50, -0.01, 0.03, 0.0],
1235
+ [1.0, 1.0, 0.0, 0.0, 1.0, 0.01, 0.0, 0.0, 0.0],
1236
+ [0.0, 1.0, 0.0, 1.0, -1.0, -0.01, 0.0, 0.0, 0.0],
1237
+ [1.0, 2.0, -2.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
1238
+ [1.0, 2.0, 0.0, 0.0, 0.0, -0.11, 0.01, 0.01, 0.0],
1239
+ [2.0, -2.0, 1.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
1240
+ [2.0, 0.0, -1.0, 0.0, 0.0, -0.02, 0.0, 0.0, 0.0],
1241
+ [3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
1242
+ [3.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
1243
+ ];
1244
+
1245
+ /// `DATDI` with three rows corrected to the Conventions text: row 16 (P1)
1246
+ /// out-of-phase radial +0.07, row 20 (K1) out-of-phase radial -0.78, and
1247
+ /// row 25 tide 166,564 (s multiplier 1).
1248
+ #[rustfmt::skip]
1249
+ const DIURNAL_BAND_CONVENTIONS: [[f64; 9]; 31] = [
1250
+ [-3.0, 0.0, 2.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
1251
+ [-3.0, 2.0, 0.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
1252
+ [-2.0, 0.0, 1.0, -1.0, 0.0, -0.02, 0.0, 0.0, 0.0],
1253
+ [-2.0, 0.0, 1.0, 0.0, 0.0, -0.08, 0.0, -0.01, 0.01],
1254
+ [-2.0, 2.0, -1.0, 0.0, 0.0, -0.02, 0.0, 0.0, 0.0],
1255
+ [-1.0, 0.0, 0.0, -1.0, 0.0, -0.10, 0.0, 0.0, 0.0],
1256
+ [-1.0, 0.0, 0.0, 0.0, 0.0, -0.51, 0.0, -0.02, 0.03],
1257
+ [-1.0, 2.0, 0.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0],
1258
+ [0.0, -2.0, 1.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0],
1259
+ [0.0, 0.0, -1.0, 0.0, 0.0, 0.02, 0.0, 0.0, 0.0],
1260
+ [0.0, 0.0, 1.0, 0.0, 0.0, 0.06, 0.0, 0.0, 0.0],
1261
+ [0.0, 0.0, 1.0, 1.0, 0.0, 0.01, 0.0, 0.0, 0.0],
1262
+ [0.0, 2.0, -1.0, 0.0, 0.0, 0.01, 0.0, 0.0, 0.0],
1263
+ [1.0, -3.0, 0.0, 0.0, 1.0, -0.06, 0.0, 0.0, 0.0],
1264
+ [1.0, -2.0, 0.0, -1.0, 0.0, 0.01, 0.0, 0.0, 0.0],
1265
+ [1.0, -2.0, 0.0, 0.0, 0.0, -1.23, 0.07, 0.06, 0.01],
1266
+ [1.0, -1.0, 0.0, 0.0, -1.0, 0.02, 0.0, 0.0, 0.0],
1267
+ [1.0, -1.0, 0.0, 0.0, 1.0, 0.04, 0.0, 0.0, 0.0],
1268
+ [1.0, 0.0, 0.0, -1.0, 0.0, -0.22, 0.01, 0.01, 0.0],
1269
+ [1.0, 0.0, 0.0, 0.0, 0.0, 12.00, -0.78, -0.67, -0.03],
1270
+ [1.0, 0.0, 0.0, 1.0, 0.0, 1.73, -0.12, -0.10, 0.0],
1271
+ [1.0, 0.0, 0.0, 2.0, 0.0, -0.04, 0.0, 0.0, 0.0],
1272
+ [1.0, 1.0, 0.0, 0.0, -1.0, -0.50, -0.01, 0.03, 0.0],
1273
+ [1.0, 1.0, 0.0, 0.0, 1.0, 0.01, 0.0, 0.0, 0.0],
1274
+ [1.0, 1.0, 0.0, 1.0, -1.0, -0.01, 0.0, 0.0, 0.0],
1275
+ [1.0, 2.0, -2.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
1276
+ [1.0, 2.0, 0.0, 0.0, 0.0, -0.11, 0.01, 0.01, 0.0],
1277
+ [2.0, -2.0, 1.0, 0.0, 0.0, -0.01, 0.0, 0.0, 0.0],
1278
+ [2.0, 0.0, -1.0, 0.0, 0.0, -0.02, 0.0, 0.0, 0.0],
1279
+ [3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
1280
+ [3.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
1281
+ ];
1282
+
878
1283
  /// In-phase / out-of-phase frequency-dependent corrections, diurnal band
879
1284
  /// (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
- ];
1285
+ fn frequency_dependent_diurnal_correction(
1286
+ xsta: &[f64; 3],
1287
+ fhr: f64,
1288
+ t: f64,
1289
+ datdi: &[[f64; 9]; 31],
1290
+ ) -> [f64; 3] {
916
1291
  let mut s = 218.31664563 + (481267.88194 + (-0.0014663889 + 0.00000185139 * t) * t) * t;
917
1292
  let mut tau = fhr * 15.0
918
1293
  + 280.4606184
@@ -929,28 +1304,31 @@ fn frequency_dependent_diurnal_correction(xsta: &[f64; 3], fhr: f64, t: f64) ->
929
1304
  let mut ps = 282.93734098
930
1305
  + (1.71945766667 + (0.00045688889 + (-0.00000001778 + -0.00000000334 * t) * t) * t) * t;
931
1306
 
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);
1307
+ s %= 360.0;
1308
+ tau %= 360.0;
1309
+ h %= 360.0;
1310
+ p %= 360.0;
1311
+ zns %= 360.0;
1312
+ ps %= 360.0;
938
1313
 
939
1314
  let rsta = (xsta[0] * xsta[0] + xsta[1] * xsta[1] + xsta[2] * xsta[2]).sqrt();
940
1315
  let sinphi = xsta[2] / rsta;
941
1316
  let cosphi = (xsta[0] * xsta[0] + xsta[1] * xsta[1]).sqrt() / rsta;
942
1317
  let cosla = xsta[0] / cosphi / rsta;
943
1318
  let sinla = xsta[1] / cosphi / rsta;
944
- let zla = xsta[1].atan2(xsta[0]);
1319
+ let zla = libm::atan2(xsta[1], xsta[0]);
945
1320
 
946
1321
  let mut xcorsta = [0.0_f64; 3];
947
- for w in &DATDI {
1322
+ for w in datdi {
948
1323
  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();
1324
+ let angle = thetaf + zla;
1325
+ let sin_angle = libm::sin(angle);
1326
+ let cos_angle = libm::cos(angle);
1327
+ let dr =
1328
+ w[5] * 2.0 * sinphi * cosphi * sin_angle + w[6] * 2.0 * sinphi * cosphi * cos_angle;
1329
+ let dn = w[7] * (cosphi * cosphi - sinphi * sinphi) * sin_angle
1330
+ + w[8] * (cosphi * cosphi - sinphi * sinphi) * cos_angle;
1331
+ let de = w[7] * sinphi * cos_angle - w[8] * sinphi * sin_angle;
954
1332
 
955
1333
  xcorsta[0] += dr * cosla * cosphi - de * sinla - dn * sinphi * cosla;
956
1334
  xcorsta[1] += dr * sinla * cosphi + de * cosla - dn * sinphi * sinla;
@@ -965,6 +1343,11 @@ fn frequency_dependent_diurnal_correction(xsta: &[f64; 3], fhr: f64, t: f64) ->
965
1343
  /// In-phase / out-of-phase frequency-dependent corrections, long-period band
966
1344
  /// (STEP2LON). `t` is Julian centuries (TT).
967
1345
  fn frequency_dependent_long_period_correction(xsta: &[f64; 3], t: f64) -> [f64; 3] {
1346
+ // DATDI(9,5): multipliers of s, h, p, N', ps, then the radial in-phase,
1347
+ // transverse in-phase, radial out-of-phase and transverse out-of-phase
1348
+ // amplitudes in mm (Table 7.3b lists the same values in the order radial
1349
+ // in-phase, radial out-of-phase, transverse in-phase, transverse
1350
+ // out-of-phase).
968
1351
  #[rustfmt::skip]
969
1352
  const DATDI: [[f64; 9]; 5] = [
970
1353
  [0.0, 0.0, 0.0, 1.0, 0.0, 0.47, 0.23, 0.16, 0.07],
@@ -991,19 +1374,21 @@ fn frequency_dependent_long_period_correction(xsta: &[f64; 3], t: f64) -> [f64;
991
1374
  let cosla = xsta[0] / cosphi / rsta;
992
1375
  let sinla = xsta[1] / cosphi / rsta;
993
1376
 
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);
1377
+ s %= 360.0;
1378
+ h %= 360.0;
1379
+ p %= 360.0;
1380
+ zns %= 360.0;
1381
+ ps %= 360.0;
999
1382
 
1000
1383
  let mut xcorsta = [0.0_f64; 3];
1001
1384
  for w in &DATDI {
1002
1385
  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();
1386
+ let sin_theta = libm::sin(thetaf);
1387
+ let cos_theta = libm::cos(thetaf);
1388
+ let dr = w[5] * (3.0 * sinphi * sinphi - 1.0) / 2.0 * cos_theta
1389
+ + w[7] * (3.0 * sinphi * sinphi - 1.0) / 2.0 * sin_theta;
1390
+ let dn =
1391
+ w[6] * (cosphi * sinphi * 2.0) * cos_theta + w[8] * (cosphi * sinphi * 2.0) * sin_theta;
1007
1392
  let de = 0.0;
1008
1393
 
1009
1394
  xcorsta[0] += dr * cosla * cosphi - de * sinla - dn * sinphi * cosla;
@@ -1036,10 +1421,30 @@ fn gregorian_to_two_part_julian_date(iy: i32, im: i32, id: i32) -> (f64, f64) {
1036
1421
  (djm0, djm)
1037
1422
  }
1038
1423
 
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 {
1424
+ /// TAI-UTC (Delta(AT)) in seconds for the given UTC date and fraction of day
1425
+ /// `fd`, as the SOFA `DAT` routine distributed with DEHANTTIDEINEL returns it:
1426
+ /// 0 before 1960 (the routine's "pre-UTC year" warning path, whose result
1427
+ /// DEHANTTIDEINEL uses as is), the 1960-1971 offsets with their drift terms,
1428
+ /// and the leap-second table from 1972. Dates after the last entry keep its
1429
+ /// value.
1430
+ fn tai_minus_utc_seconds(iy: i32, im: i32, id: i32, fd: f64) -> f64 {
1431
+ // 1960-1971: (year, month, Delta(AT) seconds, reference MJD, drift s/day).
1432
+ const DRIFT: [(i32, i32, f64, f64, f64); 14] = [
1433
+ (1960, 1, 1.4178180, 37300.0, 0.001296),
1434
+ (1961, 1, 1.4228180, 37300.0, 0.001296),
1435
+ (1961, 8, 1.3728180, 37300.0, 0.001296),
1436
+ (1962, 1, 1.8458580, 37665.0, 0.0011232),
1437
+ (1963, 11, 1.9458580, 37665.0, 0.0011232),
1438
+ (1964, 1, 3.2401300, 38761.0, 0.001296),
1439
+ (1964, 4, 3.3401300, 38761.0, 0.001296),
1440
+ (1964, 9, 3.4401300, 38761.0, 0.001296),
1441
+ (1965, 1, 3.5401300, 38761.0, 0.001296),
1442
+ (1965, 3, 3.6401300, 38761.0, 0.001296),
1443
+ (1965, 7, 3.7401300, 38761.0, 0.001296),
1444
+ (1965, 9, 3.8401300, 38761.0, 0.001296),
1445
+ (1966, 1, 4.3131700, 39126.0, 0.002592),
1446
+ (1968, 2, 4.2131700, 39126.0, 0.002592),
1447
+ ];
1043
1448
  // Post-1972 leap-second table: (year, month, Delta(AT) seconds).
1044
1449
  const IDAT: [(i32, i32, f64); 28] = [
1045
1450
  (1972, 1, 10.0),
@@ -1071,12 +1476,26 @@ fn tai_minus_utc_seconds(iy: i32, im: i32, _id: i32) -> f64 {
1071
1476
  (2015, 7, 36.0),
1072
1477
  (2017, 1, 37.0),
1073
1478
  ];
1479
+ if iy < DRIFT[0].0 {
1480
+ return 0.0;
1481
+ }
1074
1482
  let m = 12 * iy + im;
1075
- let mut da = IDAT[0].2;
1483
+ let mut leap = None;
1076
1484
  for &(y, mo, d) in &IDAT {
1077
1485
  if m >= 12 * y + mo {
1078
- da = d;
1486
+ leap = Some(d);
1487
+ }
1488
+ }
1489
+ if let Some(da) = leap {
1490
+ return da;
1491
+ }
1492
+ let mut era = DRIFT[0];
1493
+ for entry in DRIFT {
1494
+ if m >= 12 * entry.0 + entry.1 {
1495
+ era = entry;
1079
1496
  }
1080
1497
  }
1081
- da
1498
+ let (_, djm) = gregorian_to_two_part_julian_date(iy, im, id);
1499
+ let (_, _, da, reference_mjd, rate) = era;
1500
+ da + (djm + fd - reference_mjd) * rate
1082
1501
  }