Skip to content

Commit 9d5cceb

Browse files
Add general extract_coordinate_points preprocessor (#1581)
* Add extract preproc * Fix flake * Add documentation * Fix documentation * Add tests * Fix flake * Fix docu * Add missing space * Improve test coverage * Fix flake * Apply suggestions from code review Co-authored-by: Valeriu Predoi <[email protected]> * Add missing space * Update preproc name * Update esmvalcore/preprocessor/_regrid.py Co-authored-by: Valeriu Predoi <[email protected]> * Add underline * Update preproc name in tests * Improve documentation * Fix flake Co-authored-by: Valeriu Predoi <[email protected]>
1 parent ab205df commit 9d5cceb

File tree

5 files changed

+398
-29
lines changed

5 files changed

+398
-29
lines changed

doc/recipe/preprocessor.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,6 +1530,7 @@ Area manipulation
15301530
=================
15311531
The area manipulation module contains the following preprocessor functions:
15321532

1533+
* extract_coordinate_points_: Extract a point with arbitrary coordinates given an interpolation scheme.
15331534
* extract_region_: Extract a region from a cube based on ``lat/lon``
15341535
corners.
15351536
* extract_named_regions_: Extract a specific region from in the region
@@ -1542,6 +1543,33 @@ The area manipulation module contains the following preprocessor functions:
15421543
* area_statistics_: Compute area statistics.
15431544

15441545

1546+
``extract_coordinate_points``
1547+
-----------------------------
1548+
1549+
This function extracts points with given coordinates, following either a
1550+
``linear`` or a ``nearest`` interpolation scheme.
1551+
The resulting point cube will match the respective coordinates to
1552+
those of the input coordinates. If the input coordinate is a scalar,
1553+
the dimension will be a scalar in the output cube.
1554+
1555+
If the point to be extracted has at least one of the coordinate point
1556+
values outside the interval of the cube's same coordinate values, then
1557+
no extrapolation will be performed, and the resulting extracted cube
1558+
will have fully masked data.
1559+
1560+
Examples:
1561+
* Extract a point from coordinate `grid_latitude` with given coordinate value 26.0:
1562+
1563+
.. code-block:: yaml
1564+
1565+
extract_coordinate_points:
1566+
definition:
1567+
grid_latitude: 26.
1568+
scheme: nearest
1569+
1570+
See also :func:`esmvalcore.preprocessor.extract_coordinate_points`.
1571+
1572+
15451573
``extract_region``
15461574
------------------
15471575

esmvalcore/preprocessor/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@
4444
)
4545
from ._multimodel import ensemble_statistics, multi_model_statistics
4646
from ._other import clip
47-
from ._regrid import extract_levels, extract_location, extract_point, regrid
47+
from ._regrid import (
48+
extract_coordinate_points,
49+
extract_levels,
50+
extract_location,
51+
extract_point,
52+
regrid)
4853
from ._time import (
4954
annual_statistics,
5055
anomalies,
@@ -114,6 +119,7 @@
114119
# Regridding
115120
'regrid',
116121
# Point interpolation
122+
'extract_coordinate_points',
117123
'extract_point',
118124
'extract_location',
119125
# Masking missing values

esmvalcore/preprocessor/_regrid.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,3 +1094,43 @@ def get_reference_levels(filename, project, dataset, short_name, mip,
10941094
except iris.exceptions.CoordinateNotFoundError:
10951095
raise ValueError('z-coord not available in {}'.format(filename))
10961096
return coord.points.tolist()
1097+
1098+
1099+
def extract_coordinate_points(cube, definition, scheme):
1100+
"""Extract points from any coordinate with interpolation.
1101+
1102+
Multiple points can also be extracted, by supplying an array of
1103+
coordinates. The resulting point cube will match the respective
1104+
coordinates to those of the input coordinates.
1105+
If the input coordinate is a scalar, the dimension will be a
1106+
scalar in the output cube.
1107+
1108+
Parameters
1109+
----------
1110+
cube : cube
1111+
The source cube to extract a point from.
1112+
defintion : dict(str, float or array of float)
1113+
The coordinate - values pairs to extract
1114+
scheme : str
1115+
The interpolation scheme. 'linear' or 'nearest'. No default.
1116+
1117+
Returns
1118+
-------
1119+
:py:class:`~iris.cube.Cube`
1120+
Returns a cube with the extracted point(s), and with adjusted
1121+
latitude and longitude coordinates (see above). If desired point
1122+
outside values for at least one coordinate, this cube will have fully
1123+
masked data.
1124+
1125+
Raises
1126+
------
1127+
ValueError:
1128+
If the interpolation scheme is not provided or is not recognised.
1129+
"""
1130+
1131+
msg = f"Unknown interpolation scheme, got {scheme!r}."
1132+
scheme = POINT_INTERPOLATION_SCHEMES.get(scheme.lower())
1133+
if not scheme:
1134+
raise ValueError(msg)
1135+
cube = cube.interpolate(definition.items(), scheme=scheme)
1136+
return cube
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
"""
2+
Integration tests for the :func:`esmvalcore.preprocessor.regrid.regrid`
3+
function.
4+
5+
"""
6+
7+
import unittest
8+
9+
import iris
10+
import numpy as np
11+
12+
import tests
13+
from esmvalcore.preprocessor import extract_coordinate_points
14+
from tests.unit.preprocessor._regrid import _make_cube
15+
16+
17+
class Test(tests.Test):
18+
def setUp(self):
19+
"""Prepare tests."""
20+
shape = (3, 4, 4)
21+
data = np.arange(np.prod(shape)).reshape(shape)
22+
self.cube = _make_cube(data, dtype=np.float64, rotated=True)
23+
self.cs = iris.coord_systems.GeogCS(iris.fileformats.pp.EARTH_RADIUS)
24+
25+
def test_extract_point__single_linear(self):
26+
"""Test linear interpolation when extracting a single point"""
27+
point = extract_coordinate_points(
28+
self.cube,
29+
{'grid_latitude': 2.1, 'grid_longitude': 2.1},
30+
scheme='linear')
31+
self.assertEqual(point.shape, (3,))
32+
np.testing.assert_allclose(point.data, [5.5, 21.5, 37.5])
33+
34+
# Exactly centred between grid points.
35+
point = extract_coordinate_points(
36+
self.cube,
37+
{'grid_latitude': 2.5, 'grid_longitude': 2.5},
38+
scheme='linear')
39+
self.assertEqual(point.shape, (3,))
40+
np.testing.assert_allclose(point.data, [7.5, 23.5, 39.5])
41+
42+
# On a (edge) grid point.
43+
point = extract_coordinate_points(
44+
self.cube,
45+
{'grid_latitude': 4, 'grid_longitude': 4},
46+
scheme='linear')
47+
self.assertEqual(point.shape, (3,))
48+
np.testing.assert_allclose(point.data, [15, 31, 47])
49+
50+
# Test two points outside the valid area.
51+
# These should be masked, since we set up the interpolation
52+
# schemes that way.
53+
point = extract_coordinate_points(
54+
self.cube,
55+
{'grid_latitude': -1, 'grid_longitude': -1},
56+
scheme='linear')
57+
self.assertEqual(point.shape, (3,))
58+
masked = np.ma.array([np.nan] * 3, mask=True)
59+
self.assert_array_equal(point.data, masked)
60+
61+
point = extract_coordinate_points(
62+
self.cube,
63+
{'grid_latitude': 30, 'grid_longitude': 30},
64+
scheme='linear')
65+
self.assertEqual(point.shape, (3,))
66+
self.assert_array_equal(point.data, masked)
67+
68+
def test_extract_point__single_nearest(self):
69+
"""Test nearest match when extracting a single point"""
70+
71+
point = extract_coordinate_points(
72+
self.cube,
73+
{'grid_latitude': 2.1, 'grid_longitude': 2.1},
74+
scheme='nearest')
75+
self.assertEqual(point.shape, (3,))
76+
np.testing.assert_allclose(point.data, [5, 21, 37])
77+
78+
point = extract_coordinate_points(
79+
self.cube,
80+
{'grid_latitude': 4, 'grid_longitude': 4},
81+
scheme='nearest')
82+
self.assertEqual(point.shape, (3,))
83+
np.testing.assert_allclose(point.data, [15, 31, 47])
84+
85+
# Test two points outside the valid area
86+
point = extract_coordinate_points(
87+
self.cube,
88+
{'grid_latitude': -1, 'grid_longitude': -1},
89+
scheme='nearest')
90+
self.assertEqual(point.shape, (3,))
91+
masked = np.ma.array(np.empty(3, dtype=np.float64), mask=True)
92+
self.assert_array_equal(point.data, masked)
93+
94+
point = extract_coordinate_points(
95+
self.cube,
96+
{'grid_latitude': 30, 'grid_longitude': 30},
97+
scheme='nearest')
98+
self.assertEqual(point.shape, (3,))
99+
self.assert_array_equal(point.data, masked)
100+
101+
def test_extract_point__multiple_linear(self):
102+
"""Test linear interpolation for an array of one coordinate"""
103+
104+
# Test points on the grid edges, on a grid point, halfway and
105+
# one in between.
106+
coords = self.cube.coords(dim_coords=True)
107+
print([coord.standard_name for coord in coords])
108+
109+
point = extract_coordinate_points(
110+
self.cube,
111+
{'grid_latitude': [1, 1.1, 1.5, 2, 4],
112+
'grid_longitude': 2},
113+
scheme='linear')
114+
self.assertEqual(point.shape, (3, 5))
115+
# Longitude is not a dimension coordinate anymore.
116+
self.assertEqual(['air_pressure', 'grid_latitude'], [
117+
coord.standard_name for coord in point.coords(dim_coords=True)])
118+
np.testing.assert_allclose(point.data, [[1, 1.4, 3, 5, 13],
119+
[17, 17.4, 19., 21., 29],
120+
[33, 33.4, 35, 37, 45]])
121+
122+
point = extract_coordinate_points(
123+
self.cube,
124+
{'grid_latitude': 4,
125+
'grid_longitude': [1, 1.1, 1.5, 2, 4]},
126+
scheme='linear')
127+
self.assertEqual(point.shape, (3, 5))
128+
self.assertEqual(['air_pressure', 'grid_longitude'], [
129+
coord.standard_name for coord in point.coords(dim_coords=True)])
130+
np.testing.assert_allclose(point.data, [[12, 12.1, 12.5, 13, 15],
131+
[28, 28.1, 28.5, 29, 31],
132+
[44, 44.1, 44.5, 45, 47]])
133+
134+
# Test latitude and longitude points outside the grid.
135+
# These should all be masked.
136+
coords = self.cube.coords(dim_coords=True)
137+
point = extract_coordinate_points(
138+
self.cube,
139+
{'grid_latitude': [0, 10], 'grid_longitude': 3},
140+
scheme='linear')
141+
self.assertEqual(point.shape, (3, 2))
142+
masked = np.ma.array(np.empty((3, 2), dtype=np.float64), mask=True)
143+
self.assert_array_equal(point.data, masked)
144+
coords = self.cube.coords(dim_coords=True)
145+
point = extract_coordinate_points(
146+
self.cube,
147+
{'grid_latitude': 2, 'grid_longitude': [0, 10]},
148+
scheme='linear')
149+
coords = point.coords(dim_coords=True)
150+
self.assertEqual(point.shape, (3, 2))
151+
self.assert_array_equal(point.data, masked)
152+
153+
def test_extract_point__multiple_nearest(self):
154+
"""Test nearest match for an array of one coordinate"""
155+
156+
point = extract_coordinate_points(
157+
self.cube,
158+
{'grid_latitude': [1, 1.1, 1.5, 1.501, 2, 4],
159+
'grid_longitude': 2},
160+
scheme='nearest')
161+
self.assertEqual(point.shape, (3, 6))
162+
self.assertEqual(['air_pressure', 'grid_latitude'], [
163+
coord.standard_name for coord in point.coords(dim_coords=True)])
164+
np.testing.assert_allclose(point.data, [[1, 1, 1, 5, 5, 13],
165+
[17, 17, 17, 21, 21, 29],
166+
[33, 33, 33, 37, 37, 45]])
167+
point = extract_coordinate_points(
168+
self.cube,
169+
{'grid_latitude': 4,
170+
'grid_longitude': [1, 1.1, 1.5, 1.501, 2, 4]},
171+
scheme='nearest')
172+
self.assertEqual(point.shape, (3, 6))
173+
self.assertEqual(['air_pressure', 'grid_longitude'], [
174+
coord.standard_name for coord in point.coords(dim_coords=True)])
175+
np.testing.assert_allclose(point.data, [[12, 12, 12, 13, 13, 15],
176+
[28, 28, 28, 29, 29, 31],
177+
[44, 44, 44, 45, 45, 47]])
178+
point = extract_coordinate_points(
179+
self.cube,
180+
{'grid_latitude': [0, 10],
181+
'grid_longitude': 3},
182+
scheme='nearest')
183+
masked = np.ma.array(np.empty((3, 2), dtype=np.float64), mask=True)
184+
self.assertEqual(point.shape, (3, 2))
185+
self.assert_array_equal(point.data, masked)
186+
point = extract_coordinate_points(
187+
self.cube,
188+
{'grid_latitude': 2,
189+
'grid_longitude': [0, 10]},
190+
scheme='nearest')
191+
self.assertEqual(point.shape, (3, 2))
192+
self.assert_array_equal(point.data, masked)
193+
194+
def test_extract_point__multiple_both_linear(self):
195+
"""Test for both latitude and longitude arrays, with
196+
linear interpolation"""
197+
point = extract_coordinate_points(
198+
self.cube,
199+
{'grid_latitude': [0, 1.1, 1.5, 1.51, 4, 5],
200+
'grid_longitude': [0, 1.1, 1.5, 1.51, 4, 5]}, scheme='linear')
201+
self.assertEqual(point.data.shape, (3, 6, 6))
202+
203+
result = np.ma.array(np.empty((3, 6, 6), dtype=np.float64), mask=True)
204+
result[0, 1, 1:5] = [0.5, 0.9, 0.91, 3.4]
205+
result[0, 2, 1:5] = [2.1, 2.5, 2.51, 5.0]
206+
result[0, 3, 1:5] = [2.14, 2.54, 2.55, 5.04]
207+
result[0, 4, 1:5] = [12.1, 12.5, 12.51, 15.0]
208+
209+
result[1, 1, 1:5] = [16.5, 16.9, 16.91, 19.4]
210+
result[1, 2, 1:5] = [18.10, 18.5, 18.51, 21.0]
211+
result[1, 3, 1:5] = [18.14, 18.54, 18.55, 21.04]
212+
result[1, 4, 1:5] = [28.1, 28.5, 28.51, 31.0]
213+
214+
result[2, 1, 1:5] = [32.5, 32.9, 32.91, 35.4]
215+
result[2, 2, 1:5] = [34.1, 34.5, 34.51, 37]
216+
result[2, 3, 1:5] = [34.14, 34.54, 34.55, 37.04]
217+
result[2, 4, 1:5] = [44.1, 44.5, 44.51, 47]
218+
# Unmask the inner area of the result array.
219+
# The outer edges of the extracted points are outside the cube
220+
# grid, and should thus be masked.
221+
result.mask[:, 1:5, 1:5] = False
222+
223+
np.testing.assert_allclose(point.data, result)
224+
225+
def test_extract_point__multiple_both_nearest(self):
226+
"""Test for both latitude and longitude arrays, with nearest match"""
227+
point = extract_coordinate_points(
228+
self.cube,
229+
{'grid_latitude': [0, 1.1, 1.5, 1.51, 4, 5],
230+
'grid_longitude': [0, 1.1, 1.5, 1.51, 4, 5]},
231+
scheme='nearest')
232+
self.assertEqual(point.data.shape, (3, 6, 6))
233+
234+
result = np.ma.array(np.empty((3, 6, 6), dtype=np.float64), mask=True)
235+
result[0, 1, 1:5] = [0.0, 0.0, 1.0, 3.0]
236+
result[0, 2, 1:5] = [0.0, 0.0, 1.0, 3.0]
237+
result[0, 3, 1:5] = [4.0, 4.0, 5.0, 7.0]
238+
result[0, 4, 1:5] = [12.0, 12.0, 13.0, 15.0]
239+
240+
result[1, 1, 1:5] = [16.0, 16.0, 17.0, 19.0]
241+
result[1, 2, 1:5] = [16.0, 16.0, 17.0, 19.0]
242+
result[1, 3, 1:5] = [20.0, 20.0, 21.0, 23.0]
243+
result[1, 4, 1:5] = [28.0, 28.0, 29.0, 31.0]
244+
245+
result[2, 1, 1:5] = [32.0, 32.0, 33.0, 35.0]
246+
result[2, 2, 1:5] = [32.0, 32.0, 33.0, 35.0]
247+
result[2, 3, 1:5] = [36.0, 36.0, 37.0, 39.0]
248+
result[2, 4, 1:5] = [44.0, 44.0, 45.0, 47.0]
249+
result.mask[:, 1:5, 1:5] = False
250+
251+
np.testing.assert_allclose(point.data, result)
252+
253+
def test_wrong_interpolation_scheme(self):
254+
"""Test wrong interpolation scheme raises error."""
255+
self.assertRaises(
256+
ValueError,
257+
extract_coordinate_points,
258+
self.cube, {'grid_latitude': 0.}, 'wrong')
259+
260+
261+
if __name__ == '__main__':
262+
unittest.main()

0 commit comments

Comments
 (0)