1use core::fmt;
8
9use crate::astro::math::linear::invert_3x3_adjugate;
10use crate::astro::math::mat3::{mul_vec3, Mat3};
11use crate::astro::math::vec3::{add3, scale3, sub3};
12use crate::frame::ItrfPositionM;
13
14const MM_TO_M: f64 = 1.0e-3;
15const PPB_TO_SCALE: f64 = 1.0e-9;
16const MAS_TO_RAD: f64 = core::f64::consts::PI / (180.0 * 3600.0 * 1000.0);
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum TerrestrialFrame {
21 Itrf2020,
23 Itrf2014,
25 Itrf2008,
27 Etrf2020,
29}
30
31impl TerrestrialFrame {
32 fn index(self) -> usize {
33 match self {
34 Self::Itrf2020 => 0,
35 Self::Itrf2014 => 1,
36 Self::Itrf2008 => 2,
37 Self::Etrf2020 => 3,
38 }
39 }
40}
41
42impl fmt::Display for TerrestrialFrame {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::Itrf2020 => f.write_str("ITRF2020"),
46 Self::Itrf2014 => f.write_str("ITRF2014"),
47 Self::Itrf2008 => f.write_str("ITRF2008"),
48 Self::Etrf2020 => f.write_str("ETRF2020"),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq)]
55pub struct TerrestrialPositionM {
56 pub x_m: f64,
58 pub y_m: f64,
60 pub z_m: f64,
62}
63
64impl TerrestrialPositionM {
65 pub const fn new(x_m: f64, y_m: f64, z_m: f64) -> Result<Self, FrameCatalogError> {
67 if !x_m.is_finite() {
68 return Err(invalid_input("x_m", "must be finite"));
69 }
70 if !y_m.is_finite() {
71 return Err(invalid_input("y_m", "must be finite"));
72 }
73 if !z_m.is_finite() {
74 return Err(invalid_input("z_m", "must be finite"));
75 }
76 Ok(Self { x_m, y_m, z_m })
77 }
78
79 pub const fn from_array(position_m: [f64; 3]) -> Result<Self, FrameCatalogError> {
81 Self::new(position_m[0], position_m[1], position_m[2])
82 }
83
84 pub const fn as_array(self) -> [f64; 3] {
86 [self.x_m, self.y_m, self.z_m]
87 }
88
89 pub const fn from_itrf(position: ItrfPositionM) -> Self {
91 Self {
92 x_m: position.x_m,
93 y_m: position.y_m,
94 z_m: position.z_m,
95 }
96 }
97}
98
99impl From<ItrfPositionM> for TerrestrialPositionM {
100 fn from(position: ItrfPositionM) -> Self {
101 Self::from_itrf(position)
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq)]
107pub struct TerrestrialVelocityMPerYear {
108 pub vx_m_per_year: f64,
110 pub vy_m_per_year: f64,
112 pub vz_m_per_year: f64,
114}
115
116impl TerrestrialVelocityMPerYear {
117 pub const fn new(
119 vx_m_per_year: f64,
120 vy_m_per_year: f64,
121 vz_m_per_year: f64,
122 ) -> Result<Self, FrameCatalogError> {
123 if !vx_m_per_year.is_finite() {
124 return Err(invalid_input("vx_m_per_year", "must be finite"));
125 }
126 if !vy_m_per_year.is_finite() {
127 return Err(invalid_input("vy_m_per_year", "must be finite"));
128 }
129 if !vz_m_per_year.is_finite() {
130 return Err(invalid_input("vz_m_per_year", "must be finite"));
131 }
132 Ok(Self {
133 vx_m_per_year,
134 vy_m_per_year,
135 vz_m_per_year,
136 })
137 }
138
139 pub const fn from_array(velocity_m_per_year: [f64; 3]) -> Result<Self, FrameCatalogError> {
141 Self::new(
142 velocity_m_per_year[0],
143 velocity_m_per_year[1],
144 velocity_m_per_year[2],
145 )
146 }
147
148 pub const fn as_array(self) -> [f64; 3] {
150 [self.vx_m_per_year, self.vy_m_per_year, self.vz_m_per_year]
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq)]
156pub struct TerrestrialState {
157 pub position: TerrestrialPositionM,
159 pub velocity: Option<TerrestrialVelocityMPerYear>,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq)]
165pub struct HelmertParameters {
166 pub translation_mm: [f64; 3],
168 pub scale_ppb: f64,
170 pub rotation_mas: [f64; 3],
172}
173
174#[derive(Debug, Clone, Copy, PartialEq)]
176pub struct HelmertRates {
177 pub translation_mm_per_year: [f64; 3],
179 pub scale_ppb_per_year: f64,
181 pub rotation_mas_per_year: [f64; 3],
183}
184
185#[derive(Debug, Clone, Copy, PartialEq)]
187pub struct HelmertTransform {
188 pub from: TerrestrialFrame,
190 pub to: TerrestrialFrame,
192 pub reference_epoch_year: f64,
194 pub parameters: HelmertParameters,
196 pub rates: HelmertRates,
198 pub provenance: &'static str,
200}
201
202impl HelmertTransform {
203 pub fn parameters_at(&self, epoch_year: f64) -> Result<HelmertParameters, FrameCatalogError> {
205 validate_epoch(epoch_year)?;
206 validate_finite("reference_epoch_year", self.reference_epoch_year)?;
207 validate_helmert_parameters(&self.parameters)?;
208 validate_helmert_rates(&self.rates)?;
209 let dt_years = epoch_year - self.reference_epoch_year;
210 validate_finite("epoch_delta_years", dt_years)?;
211 let parameters = HelmertParameters {
212 translation_mm: [
213 self.parameters.translation_mm[0]
214 + self.rates.translation_mm_per_year[0] * dt_years,
215 self.parameters.translation_mm[1]
216 + self.rates.translation_mm_per_year[1] * dt_years,
217 self.parameters.translation_mm[2]
218 + self.rates.translation_mm_per_year[2] * dt_years,
219 ],
220 scale_ppb: self.parameters.scale_ppb + self.rates.scale_ppb_per_year * dt_years,
221 rotation_mas: [
222 self.parameters.rotation_mas[0] + self.rates.rotation_mas_per_year[0] * dt_years,
223 self.parameters.rotation_mas[1] + self.rates.rotation_mas_per_year[1] * dt_years,
224 self.parameters.rotation_mas[2] + self.rates.rotation_mas_per_year[2] * dt_years,
225 ],
226 };
227 validate_helmert_parameters(¶meters)?;
228 Ok(parameters)
229 }
230
231 fn evaluated(&self, epoch_year: f64) -> Result<EvaluatedHelmert, FrameCatalogError> {
232 let parameters = self.parameters_at(epoch_year)?;
233 Ok(EvaluatedHelmert {
234 translation_m: scale3(parameters.translation_mm, MM_TO_M),
235 scale: parameters.scale_ppb * PPB_TO_SCALE,
236 rotation_rad: scale3(parameters.rotation_mas, MAS_TO_RAD),
237 translation_rate_m_per_year: scale3(self.rates.translation_mm_per_year, MM_TO_M),
238 scale_rate_per_year: self.rates.scale_ppb_per_year * PPB_TO_SCALE,
239 rotation_rate_rad_per_year: scale3(self.rates.rotation_mas_per_year, MAS_TO_RAD),
240 })
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
246pub enum FrameCatalogError {
247 #[error("invalid frame catalog {field}: {reason}")]
249 InvalidInput {
250 field: &'static str,
252 reason: &'static str,
254 },
255 #[error("no terrestrial frame catalog path from {from} to {to}")]
257 NoCatalogPath {
258 from: TerrestrialFrame,
260 to: TerrestrialFrame,
262 },
263 #[error("singular terrestrial frame transform from {from} to {to} at epoch {epoch_year}")]
265 SingularTransform {
266 from: TerrestrialFrame,
268 to: TerrestrialFrame,
270 epoch_year: f64,
272 },
273}
274
275pub const TERRESTRIAL_FRAME_CATALOG: &[HelmertTransform] = &[
277 HelmertTransform {
278 from: TerrestrialFrame::Itrf2020,
279 to: TerrestrialFrame::Itrf2014,
280 reference_epoch_year: 2015.0,
281 parameters: HelmertParameters {
282 translation_mm: [-1.4, -0.9, 1.4],
283 scale_ppb: -0.42,
284 rotation_mas: [0.0, 0.0, 0.0],
285 },
286 rates: HelmertRates {
287 translation_mm_per_year: [0.0, -0.1, 0.2],
288 scale_ppb_per_year: 0.0,
289 rotation_mas_per_year: [0.0, 0.0, 0.0],
290 },
291 provenance: "ITRF/IGN Transfo-ITRF2020_TRFs.txt, ITRF2020 to past ITRFs, epoch 2015.0",
292 },
293 HelmertTransform {
294 from: TerrestrialFrame::Itrf2020,
295 to: TerrestrialFrame::Itrf2008,
296 reference_epoch_year: 2015.0,
297 parameters: HelmertParameters {
298 translation_mm: [0.2, 1.0, 3.3],
299 scale_ppb: -0.29,
300 rotation_mas: [0.0, 0.0, 0.0],
301 },
302 rates: HelmertRates {
303 translation_mm_per_year: [0.0, -0.1, 0.1],
304 scale_ppb_per_year: 0.03,
305 rotation_mas_per_year: [0.0, 0.0, 0.0],
306 },
307 provenance: "ITRF/IGN Transfo-ITRF2020_TRFs.txt, ITRF2020 to past ITRFs, epoch 2015.0",
308 },
309 HelmertTransform {
310 from: TerrestrialFrame::Itrf2014,
311 to: TerrestrialFrame::Itrf2008,
312 reference_epoch_year: 2010.0,
313 parameters: HelmertParameters {
314 translation_mm: [1.6, 1.9, 2.4],
315 scale_ppb: -0.02,
316 rotation_mas: [0.0, 0.0, 0.0],
317 },
318 rates: HelmertRates {
319 translation_mm_per_year: [0.0, 0.0, -0.1],
320 scale_ppb_per_year: 0.03,
321 rotation_mas_per_year: [0.0, 0.0, 0.0],
322 },
323 provenance: "IERS Technical Note 38, Table 2, ITRF2014 to ITRF2008, epoch 2010.0",
324 },
325 HelmertTransform {
326 from: TerrestrialFrame::Itrf2020,
327 to: TerrestrialFrame::Etrf2020,
328 reference_epoch_year: 2015.0,
329 parameters: HelmertParameters {
330 translation_mm: [0.0, 0.0, 0.0],
331 scale_ppb: 0.0,
332 rotation_mas: [2.236, 13.494, -19.578],
333 },
334 rates: HelmertRates {
335 translation_mm_per_year: [0.0, 0.0, 0.0],
336 scale_ppb_per_year: 0.0,
337 rotation_mas_per_year: [0.086, 0.519, -0.753],
338 },
339 provenance: "EUREF Technical Note 1, Table 2, ITRF2020 to ETRF2020, epoch 2015.0",
340 },
341];
342
343pub fn catalog() -> &'static [HelmertTransform] {
345 TERRESTRIAL_FRAME_CATALOG
346}
347
348pub fn catalog_entry(
350 from: TerrestrialFrame,
351 to: TerrestrialFrame,
352) -> Option<&'static HelmertTransform> {
353 TERRESTRIAL_FRAME_CATALOG
354 .iter()
355 .find(|entry| entry.from == from && entry.to == to)
356}
357
358pub fn propagate_position(
360 position: TerrestrialPositionM,
361 velocity: TerrestrialVelocityMPerYear,
362 from_epoch_year: f64,
363 to_epoch_year: f64,
364) -> Result<TerrestrialPositionM, FrameCatalogError> {
365 validate_epoch(from_epoch_year)?;
366 validate_epoch(to_epoch_year)?;
367 let position = TerrestrialPositionM::from_array(position.as_array())?;
368 let velocity = TerrestrialVelocityMPerYear::from_array(velocity.as_array())?;
369 let dt_years = to_epoch_year - from_epoch_year;
370 let propagated = add3(position.as_array(), scale3(velocity.as_array(), dt_years));
371 TerrestrialPositionM::from_array(propagated)
372}
373
374pub fn transform_from_epoch(
376 position: TerrestrialPositionM,
377 velocity: TerrestrialVelocityMPerYear,
378 position_epoch_year: f64,
379 from: TerrestrialFrame,
380 to: TerrestrialFrame,
381 transform_epoch_year: f64,
382) -> Result<TerrestrialState, FrameCatalogError> {
383 let position_at_transform_epoch = propagate_position(
384 position,
385 velocity,
386 position_epoch_year,
387 transform_epoch_year,
388 )?;
389 transform(
390 position_at_transform_epoch,
391 Some(velocity),
392 from,
393 to,
394 transform_epoch_year,
395 )
396}
397
398pub fn transform(
400 position: TerrestrialPositionM,
401 velocity: Option<TerrestrialVelocityMPerYear>,
402 from: TerrestrialFrame,
403 to: TerrestrialFrame,
404 epoch_year: f64,
405) -> Result<TerrestrialState, FrameCatalogError> {
406 validate_epoch(epoch_year)?;
407 let position = TerrestrialPositionM::from_array(position.as_array())?;
408 let velocity = velocity
409 .map(|value| TerrestrialVelocityMPerYear::from_array(value.as_array()))
410 .transpose()?;
411 if from == to {
412 return Ok(TerrestrialState { position, velocity });
413 }
414
415 let path = resolve_path(from, to)?;
416 let mut state = WorkingState {
417 position_m: position.as_array(),
418 velocity_m_per_year: velocity.map(TerrestrialVelocityMPerYear::as_array),
419 };
420 for step in path.iter().take_while(|step| step.entry.is_some()) {
421 let step = step.entry.expect("filtered step");
422 state = apply_step(state, step, epoch_year)?;
423 }
424
425 Ok(TerrestrialState {
426 position: TerrestrialPositionM::from_array(state.position_m)?,
427 velocity: state
428 .velocity_m_per_year
429 .map(TerrestrialVelocityMPerYear::from_array)
430 .transpose()?,
431 })
432}
433
434const fn invalid_input(field: &'static str, reason: &'static str) -> FrameCatalogError {
435 FrameCatalogError::InvalidInput { field, reason }
436}
437
438fn validate_epoch(epoch_year: f64) -> Result<(), FrameCatalogError> {
439 validate_finite("epoch_year", epoch_year)
440}
441
442fn validate_finite(field: &'static str, value: f64) -> Result<(), FrameCatalogError> {
443 if value.is_finite() {
444 Ok(())
445 } else {
446 Err(invalid_input(field, "must be finite"))
447 }
448}
449
450fn validate_array3(fields: [&'static str; 3], values: [f64; 3]) -> Result<(), FrameCatalogError> {
451 validate_finite(fields[0], values[0])?;
452 validate_finite(fields[1], values[1])?;
453 validate_finite(fields[2], values[2])
454}
455
456fn validate_helmert_parameters(parameters: &HelmertParameters) -> Result<(), FrameCatalogError> {
457 validate_array3(
458 [
459 "translation_mm[0]",
460 "translation_mm[1]",
461 "translation_mm[2]",
462 ],
463 parameters.translation_mm,
464 )?;
465 validate_finite("scale_ppb", parameters.scale_ppb)?;
466 validate_array3(
467 ["rotation_mas[0]", "rotation_mas[1]", "rotation_mas[2]"],
468 parameters.rotation_mas,
469 )
470}
471
472fn validate_helmert_rates(rates: &HelmertRates) -> Result<(), FrameCatalogError> {
473 validate_array3(
474 [
475 "translation_mm_per_year[0]",
476 "translation_mm_per_year[1]",
477 "translation_mm_per_year[2]",
478 ],
479 rates.translation_mm_per_year,
480 )?;
481 validate_finite("scale_ppb_per_year", rates.scale_ppb_per_year)?;
482 validate_array3(
483 [
484 "rotation_mas_per_year[0]",
485 "rotation_mas_per_year[1]",
486 "rotation_mas_per_year[2]",
487 ],
488 rates.rotation_mas_per_year,
489 )
490}
491
492#[derive(Debug, Clone, Copy)]
493struct EvaluatedHelmert {
494 translation_m: [f64; 3],
495 scale: f64,
496 rotation_rad: [f64; 3],
497 translation_rate_m_per_year: [f64; 3],
498 scale_rate_per_year: f64,
499 rotation_rate_rad_per_year: [f64; 3],
500}
501
502impl EvaluatedHelmert {
503 fn matrix(self) -> Mat3 {
504 let [rx, ry, rz] = self.rotation_rad;
505 [
506 [1.0 + self.scale, -rz, ry],
507 [rz, 1.0 + self.scale, -rx],
508 [-ry, rx, 1.0 + self.scale],
509 ]
510 }
511
512 fn rate_term(self, position_m: [f64; 3]) -> [f64; 3] {
513 let [rx, ry, rz] = self.rotation_rate_rad_per_year;
514 let scale = self.scale_rate_per_year;
515 [
516 self.translation_rate_m_per_year[0] + scale * position_m[0] - rz * position_m[1]
517 + ry * position_m[2],
518 self.translation_rate_m_per_year[1] + rz * position_m[0] + scale * position_m[1]
519 - rx * position_m[2],
520 self.translation_rate_m_per_year[2] - ry * position_m[0]
521 + rx * position_m[1]
522 + scale * position_m[2],
523 ]
524 }
525}
526
527#[derive(Debug, Clone, Copy)]
528struct WorkingState {
529 position_m: [f64; 3],
530 velocity_m_per_year: Option<[f64; 3]>,
531}
532
533#[derive(Debug, Clone, Copy)]
534struct PathStep {
535 entry: Option<DirectedEntry>,
536}
537
538#[derive(Debug, Clone, Copy)]
539struct DirectedEntry {
540 transform: &'static HelmertTransform,
541 reverse: bool,
542}
543
544fn apply_step(
545 state: WorkingState,
546 step: DirectedEntry,
547 epoch_year: f64,
548) -> Result<WorkingState, FrameCatalogError> {
549 let evaluated = step.transform.evaluated(epoch_year)?;
550 if step.reverse {
551 apply_reverse(state, step.transform, evaluated, epoch_year)
552 } else {
553 Ok(apply_forward(state, evaluated))
554 }
555}
556
557fn apply_forward(state: WorkingState, evaluated: EvaluatedHelmert) -> WorkingState {
558 let matrix = evaluated.matrix();
559 let position_m = add3(evaluated.translation_m, mul_vec3(&matrix, state.position_m));
560 let velocity_m_per_year = state
561 .velocity_m_per_year
562 .map(|velocity| add3(velocity, evaluated.rate_term(state.position_m)));
563 WorkingState {
564 position_m,
565 velocity_m_per_year,
566 }
567}
568
569fn apply_reverse(
570 state: WorkingState,
571 transform: &HelmertTransform,
572 evaluated: EvaluatedHelmert,
573 epoch_year: f64,
574) -> Result<WorkingState, FrameCatalogError> {
575 let matrix = evaluated.matrix();
576 let inverse = invert_3x3_adjugate(&matrix).ok_or(FrameCatalogError::SingularTransform {
577 from: transform.from,
578 to: transform.to,
579 epoch_year,
580 })?;
581 let position_m = mul_vec3(&inverse, sub3(state.position_m, evaluated.translation_m));
582 let velocity_m_per_year = state
583 .velocity_m_per_year
584 .map(|velocity| sub3(velocity, evaluated.rate_term(position_m)));
585 Ok(WorkingState {
586 position_m,
587 velocity_m_per_year,
588 })
589}
590
591fn resolve_path(
592 from: TerrestrialFrame,
593 to: TerrestrialFrame,
594) -> Result<[PathStep; 3], FrameCatalogError> {
595 let frames = [
596 TerrestrialFrame::Itrf2020,
597 TerrestrialFrame::Itrf2014,
598 TerrestrialFrame::Itrf2008,
599 TerrestrialFrame::Etrf2020,
600 ];
601 let mut queue = [TerrestrialFrame::Itrf2020; 4];
602 let mut visited = [false; 4];
603 let mut predecessor: [Option<(TerrestrialFrame, DirectedEntry)>; 4] = [None; 4];
604 let mut head = 0_usize;
605 let mut tail = 0_usize;
606
607 visited[from.index()] = true;
608 queue[tail] = from;
609 tail += 1;
610
611 while head < tail {
612 let current = queue[head];
613 head += 1;
614 if current == to {
615 break;
616 }
617
618 for neighbor in frames {
619 if visited[neighbor.index()] {
620 continue;
621 }
622 if let Some(step) = directed_entry(current, neighbor) {
623 visited[neighbor.index()] = true;
624 predecessor[neighbor.index()] = Some((current, step));
625 queue[tail] = neighbor;
626 tail += 1;
627 }
628 }
629 }
630
631 if !visited[to.index()] {
632 return Err(FrameCatalogError::NoCatalogPath { from, to });
633 }
634
635 let mut reversed = [PathStep { entry: None }; 3];
636 let mut count = 0_usize;
637 let mut current = to;
638 while current != from {
639 let Some((previous, step)) = predecessor[current.index()] else {
640 return Err(FrameCatalogError::NoCatalogPath { from, to });
641 };
642 if count == reversed.len() {
643 return Err(FrameCatalogError::NoCatalogPath { from, to });
644 }
645 reversed[count] = PathStep { entry: Some(step) };
646 count += 1;
647 current = previous;
648 }
649
650 let mut path = [PathStep { entry: None }; 3];
651 for i in 0..count {
652 path[i] = reversed[count - 1 - i];
653 }
654 Ok(path)
655}
656
657fn directed_entry(from: TerrestrialFrame, to: TerrestrialFrame) -> Option<DirectedEntry> {
658 TERRESTRIAL_FRAME_CATALOG.iter().find_map(|entry| {
659 if entry.from == from && entry.to == to {
660 Some(DirectedEntry {
661 transform: entry,
662 reverse: false,
663 })
664 } else if entry.from == to && entry.to == from {
665 Some(DirectedEntry {
666 transform: entry,
667 reverse: true,
668 })
669 } else {
670 None
671 }
672 })
673}