Skip to main content

grib/grid/
gaussian.rs

1use super::{
2    GridPointIndexIterator, Unrotate,
3    helpers::{RegularGridIterator, evenly_spaced_longitudes},
4};
5use crate::{
6    GridPointIndex, LatLons,
7    def::grib2::template::{Template3_41, param_set},
8    error::GribError,
9    grid::AngleUnit,
10};
11
12const MAX_ITER: usize = 10;
13
14impl crate::GridShortName for param_set::GaussianGrid {
15    fn short_name(&self) -> &'static str {
16        "regular_gg"
17    }
18}
19
20impl GridPointIndex for param_set::GaussianGrid {
21    fn grid_shape(&self) -> (usize, usize) {
22        (self.grid.ni as usize, self.grid.nj as usize)
23    }
24
25    fn scanning_mode(&self) -> &super::ScanningMode {
26        &self.scanning_mode
27    }
28}
29
30impl LatLons for param_set::GaussianGrid {
31    type Iter<'a> = RegularGridIterator;
32
33    fn latlons_unchecked<'a>(&'a self) -> Result<Self::Iter<'a>, GribError> {
34        if !self.is_consistent_for_j() {
35            return Err(GribError::InvalidValueError(
36                "Latitudes for first/last grid points are not consistent with scanning mode"
37                    .to_owned(),
38            ));
39        }
40
41        let ij = self.ij()?;
42        let mut lat = compute_gaussian_latitudes_in_degrees(self.grid.nj as usize)?;
43        if self.scanning_mode.scans_positively_for_j() {
44            lat.reverse()
45        };
46        let lat = lat.into_iter().map(|v| v as f32).collect();
47        let lon = evenly_spaced_longitudes(
48            self.grid.first_point_lon,
49            self.grid.last_point_lon,
50            (self.grid.ni - 1) as usize,
51            self.angle_unit() as f32,
52            self.scanning_mode,
53        );
54
55        let iter = RegularGridIterator::new(lat, lon, ij);
56        Ok(iter)
57    }
58}
59
60impl AngleUnit for param_set::GaussianGrid {
61    fn angle_unit(&self) -> f64 {
62        self.grid.angle_unit()
63    }
64}
65
66impl param_set::GaussianGrid {
67    pub(crate) fn is_consistent_for_j(&self) -> bool {
68        let lat_diff = self.grid.last_point_lat - self.grid.first_point_lat;
69        !((lat_diff > 0) ^ self.scanning_mode.scans_positively_for_j())
70    }
71}
72
73impl crate::GridShortName for Template3_41 {
74    fn short_name(&self) -> &'static str {
75        "rotated_gg"
76    }
77}
78
79impl GridPointIndex for Template3_41 {
80    fn grid_shape(&self) -> (usize, usize) {
81        self.gaussian.grid_shape()
82    }
83
84    fn scanning_mode(&self) -> &crate::def::grib2::template::param_set::ScanningMode {
85        self.gaussian.scanning_mode()
86    }
87
88    fn ij(&self) -> Result<GridPointIndexIterator, GribError> {
89        self.gaussian.ij()
90    }
91}
92
93impl LatLons for Template3_41 {
94    type Iter<'a>
95        = Unrotate<RegularGridIterator>
96    where
97        Self: 'a;
98
99    fn latlons_unchecked<'a>(&'a self) -> Result<Self::Iter<'a>, GribError> {
100        let iter = Unrotate::new(
101            self.gaussian.latlons_unchecked()?,
102            &self.rotation,
103            self.angle_unit() as f32,
104        );
105        Ok(iter)
106    }
107}
108
109impl AngleUnit for Template3_41 {
110    fn angle_unit(&self) -> f64 {
111        self.gaussian.grid.angle_unit()
112    }
113}
114
115fn compute_gaussian_latitudes_in_degrees(div: usize) -> Result<Vec<f64>, &'static str> {
116    let lat: Option<Vec<_>> = compute_gaussian_latitudes(div)
117        .map(|x| x.map(|i| i.to_degrees()))
118        .collect();
119    lat.ok_or("finding root for Legendre polynomial failed")
120}
121
122/// Computes Gaussian latitudes in radians.
123///
124/// The Newton-Raphson method is used for the computation.
125/// If the computation does not converge and no solution is obtained after the
126/// predefined number of iterations (10), the solution will have the value
127/// `None`.
128///
129/// # Examples
130///
131/// ```
132/// let mut iter = grib::utils::compute_gaussian_latitudes(0);
133/// assert_eq!(iter.next(), None);
134///
135/// let mut iter = grib::utils::compute_gaussian_latitudes(1);
136/// assert_eq!(iter.next(), Some(Some(0.0)));
137/// assert_eq!(iter.next(), None);
138///
139/// let mut iter = grib::utils::compute_gaussian_latitudes(2);
140/// assert!((iter.next().unwrap().unwrap() - (1.0 / 3.0_f64.sqrt()).asin()).abs() < 1e-15);
141/// assert!((iter.next().unwrap().unwrap() - (-1.0 / 3.0_f64.sqrt()).asin()).abs() < 1e-15);
142/// assert_eq!(iter.next(), None);
143/// ```
144pub fn compute_gaussian_latitudes(div: usize) -> impl Iterator<Item = Option<f64>> {
145    legendre_roots_iterator(div).map(|x| x.map(|i| i.asin()))
146}
147
148// Finds roots (zero points) of the Legendre polynomial using Newton–Raphson
149// method.
150//
151// The implementation uses initial guess based on following papers:
152//
153// - Francesco G. Tricomi, Sugli zeri dei polinomi sferici ed ultrasferici,
154//   Annali di Matematica Pura ed Applicata, 31 (1950), pp. 93–97.
155// - F.G. Lether, P.R. Wenston, Minimax approximations to the zeros of Pn(x) and
156//   Gauss-Legendre quadrature, Journal of Computational and Applied Mathematics,
157//   Volume 59, Issue 2, 1995, Pages 245-252, ISSN 0377-0427, https://doi.org/10.1016/0377-0427(94)00030-5.
158fn legendre_roots_iterator(n: usize) -> impl Iterator<Item = Option<f64>> {
159    let coeff = 1.0_f64 - 1.0 / (8 * n * n) as f64 + 1.0 / (8 * n * n * n) as f64;
160    (0..n).map(move |i| {
161        let guess = coeff * ((4 * i + 3) as f64 * std::f64::consts::PI / (4 * n + 2) as f64).cos();
162        find_root(guess, |x| {
163            let (p_prev, p) = legendre_polynomial(n, x);
164            let fpx = legendre_polynomial_derivative(n, x, p_prev, p);
165            p / fpx
166        })
167    })
168}
169
170// `n` is assumed to be greater than or equal to 2.
171fn legendre_polynomial(n: usize, x: f64) -> (f64, f64) {
172    let mut p0 = 1.0;
173    let mut p1 = x;
174    for k in 2..=n {
175        let pk = ((2 * k - 1) as f64 * x * p1 - (k - 1) as f64 * p0) / k as f64;
176        p0 = p1;
177        p1 = pk;
178    }
179    (p0, p1)
180}
181
182fn legendre_polynomial_derivative(n: usize, x: f64, p_prev: f64, p: f64) -> f64 {
183    (n as f64 * (p_prev - x * p)) / (1.0 - x * x)
184}
185
186// Finds a root (zero point) of the given function using Newton–Raphson method.
187fn find_root<F>(initial_guess: f64, f: F) -> Option<f64>
188where
189    F: Fn(f64) -> f64,
190{
191    let mut count = MAX_ITER;
192    let mut x = initial_guess;
193    while count > 0 {
194        let dx = f(x);
195        x -= dx;
196        if dx.abs() < f64::EPSILON {
197            return Some(x);
198        }
199
200        count -= 1;
201    }
202    None
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::{LatLons, grid::helpers::test_helpers::assert_almost_eq};
209
210    #[test]
211    fn latlon_computation_for_real_world_gaussian_grid_compared_with_results_from_eccodes()
212    -> Result<(), Box<dyn std::error::Error>> {
213        let buf =
214            crate::test_utils::decompress_to_vec(crate::test_utils::data::grib2::NOAA_GDAS_SFLUX)?;
215
216        let f = std::io::Cursor::new(buf);
217        let grib2 = crate::from_reader(f)?;
218
219        let ((_, _), first_submessage) = grib2
220            .submessages()
221            .next()
222            .ok_or_else(|| Box::<dyn std::error::Error>::from("first submessage not found"))?;
223        let grid_shape = first_submessage.grid_shape()?;
224        assert_eq!(grid_shape, (3072, 1536));
225
226        // Results from the following command line using ecCodes:
227        //
228        // ```
229        // xzcat testdata/gdas.t00z.sfluxgrbf000.grib2.0.xz \
230        //     | grib_get_data -m foo -L "%11.6f%11.6f" - \
231        //     | grep -v '^Latitude' | awk '{print $1;}' | uniq | head -160
232        // ```
233        let first_160_lats_expected = "
234                89.910325 89.794157 89.677304 89.560296 89.443229 89.326134 89.209022 89.091901
235                88.974774 88.857642 88.740506 88.623369 88.506229 88.389088 88.271946 88.154803
236                88.037660 87.920515 87.803370 87.686225 87.569079 87.451933 87.334787 87.217640
237                87.100493 86.983346 86.866199 86.749052 86.631904 86.514757 86.397609 86.280461
238                86.163313 86.046165 85.929017 85.811869 85.694721 85.577572 85.460424 85.343275
239                85.226127 85.108979 84.991830 84.874681 84.757533 84.640384 84.523236 84.406087
240                84.288938 84.171789 84.054641 83.937492 83.820343 83.703194 83.586045 83.468896
241                83.351747 83.234599 83.117450 83.000301 82.883152 82.766003 82.648854 82.531705
242                82.414556 82.297407 82.180258 82.063109 81.945960 81.828811 81.711662 81.594512
243                81.477363 81.360214 81.243065 81.125916 81.008767 80.891618 80.774469 80.657320
244                80.540171 80.423021 80.305872 80.188723 80.071574 79.954425 79.837276 79.720126
245                79.602977 79.485828 79.368679 79.251530 79.134381 79.017231 78.900082 78.782933
246                78.665784 78.548635 78.431485 78.314336 78.197187 78.080038 77.962888 77.845739
247                77.728590 77.611441 77.494292 77.377142 77.259993 77.142844 77.025695 76.908545
248                76.791396 76.674247 76.557098 76.439948 76.322799 76.205650 76.088501 75.971351
249                75.854202 75.737053 75.619904 75.502754 75.385605 75.268456 75.151306 75.034157
250                74.917008 74.799859 74.682709 74.565560 74.448411 74.331262 74.214112 74.096963
251                73.979814 73.862664 73.745515 73.628366 73.511217 73.394067 73.276918 73.159769
252                73.042619 72.925470 72.808321 72.691172 72.574022 72.456873 72.339724 72.222574
253                72.105425 71.988276 71.871126 71.753977 71.636828 71.519679 71.402529 71.285380
254            ";
255        let first_160_lats_expected = first_160_lats_expected
256            .split_whitespace()
257            .filter_map(|s| s.parse::<f32>().ok());
258
259        let delta = 1.0e-6;
260        let first_160_lats = first_submessage
261            .latlons()?
262            .map(|(lat, _lon)| lat)
263            .step_by(3072)
264            .take(160);
265        for (actual, expected) in first_160_lats.zip(first_160_lats_expected) {
266            assert_almost_eq!(actual, expected, delta);
267        }
268
269        // Results from the following command line using ecCodes:
270        //
271        // ```
272        // xzcat testdata/gdas.t00z.sfluxgrbf000.grib2.0.xz \
273        //     | grib_get_data -m foo -L "%11.6f%11.6f" - \
274        //     | grep -v '^Latitude' | awk '{print $2;}' | head -160
275        // ```
276        let first_160_lons_expected = "
277                0.000000  0.117188  0.234375  0.351563  0.468750  0.585938  0.703125  0.820313
278                0.937500  1.054688  1.171875  1.289063  1.406250  1.523438  1.640625  1.757813
279                1.875000  1.992188  2.109375  2.226563  2.343750  2.460938  2.578125  2.695313
280                2.812500  2.929688  3.046875  3.164063  3.281250  3.398438  3.515625  3.632813
281                3.750000  3.867188  3.984375  4.101563  4.218750  4.335938  4.453125  4.570313
282                4.687500  4.804688  4.921875  5.039063  5.156250  5.273438  5.390625  5.507813
283                5.625000  5.742188  5.859375  5.976563  6.093750  6.210938  6.328125  6.445313
284                6.562500  6.679688  6.796875  6.914063  7.031250  7.148438  7.265625  7.382813
285                7.500000  7.617188  7.734375  7.851563  7.968750  8.085938  8.203125  8.320313
286                8.437500  8.554688  8.671875  8.789063  8.906250  9.023438  9.140625  9.257813
287                9.375000  9.492188  9.609375  9.726563  9.843750  9.960938  10.078125 10.195313
288                10.312500 10.429688 10.546875 10.664063 10.781250 10.898438 11.015625 11.132813
289                11.250000 11.367188 11.484375 11.601563 11.718750 11.835938 11.953125 12.070313
290                12.187500 12.304688 12.421875 12.539063 12.656250 12.773438 12.890625 13.007813
291                13.125000 13.242188 13.359375 13.476563 13.593750 13.710938 13.828125 13.945313
292                14.062500 14.179688 14.296875 14.414063 14.531250 14.648438 14.765625 14.882813
293                15.000000 15.117188 15.234375 15.351563 15.468750 15.585938 15.703125 15.820313
294                15.937500 16.054688 16.171875 16.289063 16.406250 16.523438 16.640625 16.757813
295                16.875000 16.992188 17.109375 17.226563 17.343750 17.460938 17.578125 17.695313
296                17.812500 17.929688 18.046875 18.164063 18.281250 18.398438 18.515625 18.632813
297                ";
298        let first_160_lons_expected = first_160_lons_expected
299            .split_whitespace()
300            .filter_map(|s| s.parse::<f32>().ok());
301
302        let delta = 2.0e-6;
303        let first_160_lons = first_submessage.latlons()?.map(|(_lat, lon)| lon).take(160);
304        for (actual, expected) in first_160_lons.zip(first_160_lons_expected) {
305            assert_almost_eq!(actual, expected, delta);
306        }
307
308        Ok(())
309    }
310
311    macro_rules! test_legendre_roots_iterator_with_analytical_solutions {
312        ($((
313            $name:ident,
314            $n:expr,
315            $expected:expr,
316        ),)*) => ($(
317            #[test]
318            fn $name() {
319                let actual = legendre_roots_iterator($n);
320                let expected = $expected.into_iter();
321                for (actual_val, expected_val) in actual.zip(expected) {
322                    assert!(actual_val.is_some());
323                    let actual_val = actual_val.unwrap();
324                    assert_almost_eq!(actual_val, expected_val, f64::EPSILON);
325                }
326            }
327        )*);
328    }
329
330    test_legendre_roots_iterator_with_analytical_solutions! {
331        (
332            legendre_roots_iterator_for_n_being_2_compared_with_analytical_solutions,
333            2,
334            vec![1.0 / 3.0_f64.sqrt(), -1.0 / 3.0_f64.sqrt()],
335        ),
336        (
337            legendre_roots_iterator_for_n_being_5_compared_with_analytical_solutions,
338            5,
339            vec![
340                (5.0_f64 + 2.0 * (10.0_f64 / 7.0).sqrt()).sqrt() / 3.0,
341                (5.0_f64 - 2.0 * (10.0_f64 / 7.0).sqrt()).sqrt() / 3.0,
342                0.0,
343                - (5.0_f64 - 2.0 * (10.0_f64 / 7.0).sqrt()).sqrt() / 3.0,
344                - (5.0_f64 + 2.0 * (10.0_f64 / 7.0).sqrt()).sqrt() / 3.0,
345            ],
346        ),
347    }
348
349    // Values are copied and pasted from ["Features for ERA-40 grids"](https://web.archive.org/web/20160925045844/http://rda.ucar.edu/datasets/common/ecmwf/ERA40/docs/std-transformations/dss_code_glwp.html).
350    #[test]
351    fn gaussian_latitudes_computation_compared_with_numerical_solutions() {
352        let n = 160;
353        let result = compute_gaussian_latitudes_in_degrees(n);
354        assert!(result.is_ok());
355
356        let actual = result.unwrap().into_iter().take(n / 2);
357        let expected = "
358                    +   89.1416,  88.0294,  86.9108,  85.7906,  84.6699,  83.5489,
359                    +   82.4278,  81.3066,  80.1853,  79.0640,  77.9426,  76.8212,
360                    +   75.6998,  74.5784,  73.4570,  72.3356,  71.2141,  70.0927,
361                    +   68.9712,  67.8498,  66.7283,  65.6069,  64.4854,  63.3639,
362                    +   62.2425,  61.1210,  59.9995,  58.8780,  57.7566,  56.6351,
363                    +   55.5136,  54.3921,  53.2707,  52.1492,  51.0277,  49.9062,
364                    +   48.7847,  47.6632,  46.5418,  45.4203,  44.2988,  43.1773,
365                    +   42.0558,  40.9343,  39.8129,  38.6914,  37.5699,  36.4484,
366                    +   35.3269,  34.2054,  33.0839,  31.9624,  30.8410,  29.7195,
367                    +   28.5980,  27.4765,  26.3550,  25.2335,  24.1120,  22.9905,
368                    +   21.8690,  20.7476,  19.6261,  18.5046,  17.3831,  16.2616,
369                    +   15.1401,  14.0186,  12.8971,  11.7756,  10.6542,   9.5327,
370                    +    8.4112,   7.2897,   6.1682,   5.0467,   3.9252,   2.8037,
371                    +    1.6822,   0.5607 /
372                    ";
373        let expected = expected
374            .split(&['+', ' ', ',', '\n', '/'])
375            .filter_map(|s| s.parse::<f64>().ok());
376
377        let delta = 1.0e-4;
378        for (actual_val, expected_val) in actual.zip(expected) {
379            assert_almost_eq!(actual_val, expected_val, delta);
380        }
381    }
382
383    #[test]
384    fn finding_root() {
385        let actual = find_root(1.0, |x| {
386            let fx = x * x - 2.0;
387            let fpx = x * 2.0;
388            fx / fpx
389        });
390        assert!(actual.is_some());
391        let expected = 1.41421356;
392        assert_almost_eq!(actual.unwrap(), expected, 1.0e-8)
393    }
394
395    #[test]
396    fn failure_to_find_root() {
397        let actual = find_root(1000.0, |x| {
398            let fx = x * x - 2.0;
399            let fpx = x * 2.0;
400            fx / fpx
401        });
402        assert!(actual.is_none());
403    }
404}