@neilberkman/sidereon 0.31.2 → 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.
- package/CHANGELOG.md +44 -0
- package/README.md +52 -8
- package/THIRD-PARTY-NOTICES.md +99 -0
- package/licenses/Apache-2.0.txt +202 -0
- package/licenses/ERFA-BSD-3-Clause.txt +53 -0
- package/licenses/IERS-Conventions-Software-License.txt +78 -0
- package/licenses/SciPy-BSD-3-Clause.txt +30 -0
- package/licenses/libloading-ISC.txt +13 -0
- package/package.json +32 -5
- package/pkg/sidereon.d.ts +1758 -1651
- package/pkg/sidereon.js +373 -1
- package/pkg/sidereon_bg.wasm +0 -0
- package/pkg/sidereon_bg.wasm.d.ts +1604 -1581
- package/pkg-node/sidereon.d.ts +154 -70
- package/pkg-node/sidereon.js +382 -1
- package/pkg-node/sidereon_bg.wasm +0 -0
- package/pkg-node/sidereon_bg.wasm.d.ts +1604 -1581
- package/third_party_source/sidereon-core-0.33.1/tides/mod.rs +1082 -0
- package/third_party_source/sidereon-core-0.33.1/tides/ocean.rs +682 -0
- package/third_party_source/sidereon-core-0.33.1/tides/pole.rs +203 -0
- package/types/sidereon-extra.cjs +2 -0
- package/types/sidereon-extra.js +2 -0
|
@@ -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
|
+
}
|