Skip to content

Commit 05a2da2

Browse files
committed
Support conversion between permanent tide systems
1 parent 867d70f commit 05a2da2

File tree

5 files changed

+244
-2
lines changed

5 files changed

+244
-2
lines changed

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Rust Geodesy provides a number of **features** to support a number of **objectiv
88

99
The most important **features** are
1010

11-
- a set of more than 20 geodetic **transformation primitives**
11+
- a set of more than 30 geodetic **transformation primitives**
1212
- a set of more than 40 primitives for **operations on the ellipsoid**
1313
- a means for **composing** these primitives into more complex operations.
1414

Diff for: ruminations/002-rumination.md

+61
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ $ echo 553036. -124509 | kp "dms:in | geo:out"
4242
- [`molodensky`](#operator-molodensky): The full and abridged Molodensky transformations
4343
- [`noop`](#operator-noop): The no-operation
4444
- [`omerc`](#operator-omerc): The oblique Mercator projection
45+
- [`permtide`](#operator-permtide):
46+
Convert geoid undulations between different permanent tide systems
4547
- [`pop`](#operator-pop): Pop a dimension from the stack into the operands
4648
- [`push`](#operator-push): Push a dimension from the operands onto the stack
4749
- [`stack`](#operator-stack): Push/pop/swap dimensions from the operands onto the stack
@@ -861,6 +863,65 @@ and RG does not support PROJ's "indirectly given azimuth" case.
861863

862864
---
863865

866+
### Operator `permtide`
867+
868+
**Purpose:** Convert geoid undulations between different permanent tide systems
869+
870+
**Description:**
871+
872+
Since the orbits of the sun and the moon (as observed from the earth)
873+
are concentrated at lower latitudes, the mean tidal effect of these
874+
celestial bodies do not vanish, but results in a non-zero mean potential.
875+
876+
Hence, if we compute the long term mean of a series of repeated
877+
levellings between two fixed points at different latitudes, then
878+
we will eliminate the time-varying parts of the lunar and solar tidal
879+
potentials, but the non-vanishing long term mean will still blend into
880+
our attempt to measure the geopotential difference between the two
881+
points. This is known as *the mean tide* case.
882+
883+
If correcting for the mean as well, we formally obtain a more pure
884+
*geo*-potential. This is known as the *zero-tide* case, and is
885+
the equivalent to formally moving all external gravitating masses to
886+
infinity.
887+
888+
But since the permanent tide not only influences the potential, but
889+
also the shape of the earth's crust, there is a secondary effect from
890+
the external gravitating bodies due to the deformation. When we
891+
formally remove that as well, we are left with what is known as the
892+
*non-tidal* or *tide free* case.
893+
894+
In height systems, we must discern between *mean tide*,
895+
*zero tide*, and *tide free* conventions, and adapt the corresponding
896+
geoid model to fit with the convention. Hence, this operator uses
897+
geoid-centric terminology and sign conventions.
898+
899+
900+
901+
902+
| Argument | Description |
903+
|----------|-------------|
904+
| `inv` | swap forward and inverse operations |
905+
| `ellps=name` | Use ellipsoid `name` for the conversion |
906+
| `k` | zero frequency Love number. Defaults to $0.3$ |
907+
| `from=system` | Convert from either `mean`, `zero` or `free` tide system |
908+
| `to=system` | Convert to either `mean`, `zero` or `free` tide system |
909+
910+
**Example**: Convert a geoid model using the zero-tide convention to a
911+
corresponding model using the mean-tide convention
912+
913+
```geodesy
914+
permtide from=zero to=mean ellps=GRS80
915+
```
916+
917+
**See also:**
918+
[Martin Losch and Verena Seufer, 2003:](https://mitgcm.org/~mlosch/geoidcookbook.pdf)
919+
*How to Compute Geoid Undulations (Geoid Height Relative to a Given
920+
Reference Ellipsoid) from Spherical Harmonic Coefficients for
921+
Satellite Altimetry Applications*
922+
923+
---
924+
864925
### Operator `pop`
865926

866927
**DEPRECATED!** Use [`stack`](#operator-stack)

Diff for: src/bibliography/mod.rs

+20
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,20 @@ pub enum Bibliography {
103103
/// [pdf](https://gfzpublic.gfz-potsdam.de/rest/items/item_8827_5/component/file_130038/content).
104104
Kru12,
105105

106+
/// Martin Losch and Verena Seufer, 2003:
107+
/// *How to Compute Geoid Undulations (Geoid Height Relative to a Given Reference Ellipsoid)
108+
/// from Spherical Harmonic Coefficients for Satellite Altimetry Applications*
109+
/// Unpublished manuscript, 10 pp.
110+
/// [html](https://mitgcm.org/~mlosch/geoidcookbook/geoidcookbook.html)
111+
/// [pdf](https://mitgcm.org/~mlosch/geoidcookbook.pdf).
112+
Los03,
113+
114+
/// Gérard Petit and Brian Luzum (eds), 2010:
115+
/// *IERS conventions (2010)*.
116+
/// IERS technical note 36.
117+
/// Verlag des Bundesamts für Kartographie und Geodäsie, Frankfurt am Main, 179 pp.
118+
/// [pdf](https://iers-conventions.obspm.fr/content/tn36.pdf)
119+
106120
/// Knud Poder and Karsten Engsager, 1998.
107121
/// *Some Conformal Mappings and Transformations for Geodesy and Topographic Cartography*.
108122
/// Copenhagen, Denmark: Geodetic Division, KMS,
@@ -116,6 +130,12 @@ pub enum Bibliography {
116130
/// [DOI](https://doi.org/10.1080/00396265.2016.1191748).
117131
Ruf16,
118132

133+
/// Dru A. Smith, 1998:
134+
/// *There is no such thing as "The" EGM96 geoid: Subtle points on the use of a global geopotential model*,
135+
/// IGeS Bulletin No. 8, International Geoid Service, Milan, Italy, pp. 17-28,
136+
/// [URL](https://www.ngs.noaa.gov/PUBS_LIB/EGM96_GEOID_PAPER/egm96_geoid_paper.html).
137+
Dru98,
138+
119139
/// T. Vincenty (1975) *Direct and Inverse Solutions of Geodesics on the Ellipsoid
120140
/// with application of nested equations*.
121141
/// Survey Review, 23(176): 88-93.

Diff for: src/inner_op/mod.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ mod merc;
2525
mod molodensky;
2626
mod noop;
2727
mod omerc;
28+
mod permtide;
2829
pub(crate) mod pipeline; // Needed by Op for instantiation
2930
mod pushpop;
3031
mod somerc;
@@ -35,7 +36,7 @@ mod units;
3536
mod webmerc;
3637

3738
#[rustfmt::skip]
38-
const BUILTIN_OPERATORS: [(&str, OpConstructor); 35] = [
39+
const BUILTIN_OPERATORS: [(&str, OpConstructor); 36] = [
3940
("adapt", OpConstructor(adapt::new)),
4041
("addone", OpConstructor(addone::new)),
4142
("axisswap", OpConstructor(axisswap::new)),
@@ -58,6 +59,7 @@ const BUILTIN_OPERATORS: [(&str, OpConstructor); 35] = [
5859
("webmerc", OpConstructor(webmerc::new)),
5960
("molodensky", OpConstructor(molodensky::new)),
6061
("omerc", OpConstructor(omerc::new)),
62+
("permtide", OpConstructor(permtide::new)),
6163
("somerc", OpConstructor(somerc::new)),
6264
("tmerc", OpConstructor(tmerc::new)),
6365
("unitconvert", OpConstructor(unitconvert::new)),

Diff for: src/inner_op/permtide.rs

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/// Permanent tide systems
2+
use crate::authoring::*;
3+
4+
// ----- F O R W A R D -----------------------------------------------------------------
5+
6+
fn fwd(op: &Op, _ctx: &dyn Context, operands: &mut dyn CoordinateSet) -> usize {
7+
let mut successes = 0_usize;
8+
let n = operands.len();
9+
let ellps = op.params.ellps(0);
10+
let Ok(coefficient) = op.params.real("coefficient") else {
11+
return successes;
12+
};
13+
14+
for i in 0..n {
15+
let mut coord = operands.get_coord(i);
16+
let phibar = ellps.latitude_geographic_to_geocentric(coord[1]);
17+
let s = phibar.sin();
18+
coord[2] += coefficient * (-0.198) * (1.5 * s * s - 0.5);
19+
operands.set_coord(i, &coord);
20+
successes += 1;
21+
}
22+
23+
successes
24+
}
25+
26+
// ----- I N V E R S E -----------------------------------------------------------------
27+
28+
fn inv(op: &Op, _ctx: &dyn Context, operands: &mut dyn CoordinateSet) -> usize {
29+
let mut successes = 0_usize;
30+
let n = operands.len();
31+
let ellps = op.params.ellps(0);
32+
let Ok(coefficient) = op.params.real("coefficient") else {
33+
return successes;
34+
};
35+
36+
for i in 0..n {
37+
let mut coord = operands.get_coord(i);
38+
let phibar = ellps.latitude_geographic_to_geocentric(coord[1]);
39+
let s = phibar.sin();
40+
coord[2] -= coefficient * (-0.198) * (1.5 * s * s - 0.5);
41+
operands.set_coord(i, &coord);
42+
successes += 1;
43+
}
44+
45+
successes
46+
}
47+
48+
// ----- C O N S T R U C T O R ---------------------------------------------------------
49+
50+
// Example...
51+
#[rustfmt::skip]
52+
pub const GAMUT: [OpParameter; 5] = [
53+
OpParameter::Flag { key: "inv" },
54+
OpParameter::Real { key: "k", default: Some(0.3) },
55+
OpParameter::Text { key: "ellps", default: Some("GRS80") },
56+
OpParameter::Text { key: "from", default: None },
57+
OpParameter::Text { key: "to", default: None }
58+
];
59+
60+
pub fn new(parameters: &RawParameters, ctx: &dyn Context) -> Result<Op, Error> {
61+
let mut op = Op::plain(parameters, InnerOp(fwd), Some(InnerOp(inv)), &GAMUT, ctx)?;
62+
let k = op.params.real("k")?;
63+
64+
let Ok(to) = op.params.text("to") else {
65+
return Err(Error::MissingParam(
66+
"permtide: must specify 'to=' as exactly one of {'mean', 'zero', 'free'}".to_string(),
67+
));
68+
};
69+
let Ok(from) = op.params.text("from") else {
70+
return Err(Error::MissingParam(
71+
"permtide: must specify 'from=' as exactly one of {'mean', 'zero', 'free'}".to_string(),
72+
));
73+
};
74+
75+
let coefficient = match (to.as_str(), from.as_str()) {
76+
("mean", "mean") => 0.0,
77+
("mean", "zero") => 1.0,
78+
("mean", "free") => 1.0 + k,
79+
("zero", "zero") => 0.0,
80+
("zero", "mean") => -1.0,
81+
("zero", "free") => k,
82+
("free", "free") => 0.0,
83+
("free", "mean") => -(1.0 + k),
84+
("free", "zero") => -k,
85+
_ => f64::NAN,
86+
};
87+
88+
if coefficient.is_nan() {
89+
return Err(Error::BadParam(
90+
"'to=' or 'from='".to_string(),
91+
"must be one of {'mean', 'zero', 'free'}".to_string(),
92+
));
93+
}
94+
95+
op.params.real.insert("coefficient", coefficient);
96+
Ok(op)
97+
}
98+
99+
// ----- T E S T S ---------------------------------------------------------------------
100+
101+
#[cfg(test)]
102+
mod tests {
103+
use super::*;
104+
use float_eq::assert_float_eq;
105+
106+
#[test]
107+
fn permtide() -> Result<(), Error> {
108+
let mut ctx = Minimal::default();
109+
110+
// A test point near Copenhagen
111+
let pnt = Coor4D::geo(55., 12., 0., 0.);
112+
113+
// Mean -> zero
114+
let op = ctx.op("permtide from=mean to=zero ellps=GRS80")?;
115+
let mut operands = [pnt];
116+
ctx.apply(op, Fwd, &mut operands)?;
117+
assert_float_eq!(operands[0][2], 0.099407199, abs_all <= 1e-8);
118+
ctx.apply(op, Inv, &mut operands)?;
119+
assert_float_eq!(operands[0][2], pnt[2], abs_all <= 1e-12);
120+
121+
// Mean -> free
122+
let op = ctx.op("permtide from=mean to=free ellps=GRS80")?;
123+
let mut operands = [pnt];
124+
ctx.apply(op, Fwd, &mut operands)?;
125+
assert_float_eq!(operands[0][2], 0.1292293587824579, abs_all <= 1e-8);
126+
ctx.apply(op, Inv, &mut operands)?;
127+
assert_float_eq!(operands[0][2], pnt[2], abs_all <= 1e-12);
128+
129+
// Inversion
130+
let fwd_op = ctx.op("permtide from=mean to=zero ellps=GRS80")?;
131+
let inv_op = ctx.op("permtide from=zero to=mean ellps=GRS80 inv")?;
132+
let mut operands = [pnt];
133+
ctx.apply(fwd_op, Fwd, &mut operands)?;
134+
let fwd_h = operands[0][2];
135+
136+
let mut operands = [pnt];
137+
ctx.apply(inv_op, Fwd, &mut operands)?;
138+
let inv_h = operands[0][2];
139+
assert_float_eq!(fwd_h, inv_h, abs_all <= 1e-20);
140+
141+
// Bad tide system names
142+
assert!(matches!(
143+
ctx.op("permtide from=cheese to=zero ellps=GRS80"),
144+
Err(Error::BadParam(_, _))
145+
));
146+
147+
// Missing tide system names
148+
assert!(matches!(ctx.op("permtide"), Err(Error::MissingParam(_))));
149+
assert!(matches!(
150+
ctx.op("permtide to=zero ellps=GRS80"),
151+
Err(Error::MissingParam(_))
152+
));
153+
assert!(matches!(
154+
ctx.op("permtide from=mean ellps=GRS80"),
155+
Err(Error::MissingParam(_))
156+
));
157+
Ok(())
158+
}
159+
}

0 commit comments

Comments
 (0)