8
8
9
9
import logging
10
10
import os
11
+ from collections .abc import Iterable
12
+ from typing import Literal , Optional
11
13
12
14
import cartopy .io .shapereader as shpreader
13
15
import dask .array as da
14
16
import iris
17
+ import iris .util
15
18
import numpy as np
16
19
import shapely .vectorized as shp_vect
17
20
from iris .analysis import Aggregator
21
+ from iris .cube import Cube
18
22
from iris .util import rolling_window
19
23
24
+ from esmvalcore .preprocessor ._shared import get_array_module
25
+
20
26
from ._supplementary_vars import register_supplementaries
21
27
22
28
logger = logging .getLogger (__name__ )
23
29
24
30
25
- def _get_fx_mask (fx_data , fx_option , mask_type ):
31
+ def _get_fx_mask (
32
+ fx_data : np .ndarray | da .Array ,
33
+ fx_option : Literal ['land' , 'sea' , 'landsea' , 'ice' ],
34
+ mask_type : Literal ['sftlf' , 'sftof' , 'sftgif' ],
35
+ ) -> np .ndarray | da .Array :
26
36
"""Build a percentage-thresholded mask from an fx file."""
27
- inmask = da .zeros_like (fx_data , bool )
37
+ inmask = np .zeros_like (fx_data , bool ) # respects dask through dispatch
28
38
if mask_type == 'sftlf' :
29
39
if fx_option == 'land' :
30
40
# Mask land out
@@ -50,22 +60,29 @@ def _get_fx_mask(fx_data, fx_option, mask_type):
50
60
return inmask
51
61
52
62
53
- def _apply_fx_mask (fx_mask , var_data ):
54
- """Apply the fx data extracted mask on the actual processed data."""
55
- # Apply mask across
56
- old_mask = da .ma .getmaskarray (var_data )
57
- mask = old_mask | fx_mask
58
- var_data = da .ma .masked_array (var_data , mask = mask )
59
- # maybe fill_value=1e+20
60
-
61
- return var_data
63
+ def _apply_mask (
64
+ mask : np .ndarray | da .Array ,
65
+ array : np .ndarray | da .Array ,
66
+ dim_map : Optional [Iterable [int ]] = None ,
67
+ ) -> np .ndarray | da .Array :
68
+ """Apply a (broadcasted) mask on an array."""
69
+ npx = get_array_module (mask , array )
70
+ if dim_map is not None :
71
+ if isinstance (array , da .Array ):
72
+ chunks = array .chunks
73
+ else :
74
+ chunks = None
75
+ mask = iris .util .broadcast_to_shape (
76
+ mask , array .shape , dim_map , chunks = chunks
77
+ )
78
+ return npx .ma .masked_where (mask , array )
62
79
63
80
64
81
@register_supplementaries (
65
82
variables = ['sftlf' , 'sftof' ],
66
83
required = 'prefer_at_least_one' ,
67
84
)
68
- def mask_landsea (cube , mask_out ) :
85
+ def mask_landsea (cube : Cube , mask_out : Literal [ 'land' , 'sea' ]) -> Cube :
69
86
"""Mask out either land mass or sea (oceans, seas and lakes).
70
87
71
88
It uses dedicated ancillary variables (sftlf or sftof) or,
@@ -78,16 +95,15 @@ def mask_landsea(cube, mask_out):
78
95
79
96
Parameters
80
97
----------
81
- cube: iris.cube.Cube
82
- data cube to be masked. If the cube has an
98
+ cube:
99
+ Data cube to be masked. If the cube has an
83
100
:class:`iris.coords.AncillaryVariable` with standard name
84
101
``'land_area_fraction'`` or ``'sea_area_fraction'`` that will be used.
85
102
If both are present, only the 'land_area_fraction' will be used. If the
86
103
ancillary variable is not available, the mask will be calculated from
87
104
Natural Earth shapefiles.
88
-
89
- mask_out: str
90
- either "land" to mask out land mass or "sea" to mask out seas.
105
+ mask_out:
106
+ Either ``'land'`` to mask out land mass or ``'sea'`` to mask out seas.
91
107
92
108
Returns
93
109
-------
@@ -112,35 +128,40 @@ def mask_landsea(cube, mask_out):
112
128
}
113
129
114
130
# preserve importance order: try stflf first then sftof
115
- fx_cube = None
131
+ ancillary_var = None
116
132
try :
117
- fx_cube = cube .ancillary_variable ('land_area_fraction' )
133
+ ancillary_var = cube .ancillary_variable ('land_area_fraction' )
118
134
except iris .exceptions .AncillaryVariableNotFoundError :
119
135
try :
120
- fx_cube = cube .ancillary_variable ('sea_area_fraction' )
136
+ ancillary_var = cube .ancillary_variable ('sea_area_fraction' )
121
137
except iris .exceptions .AncillaryVariableNotFoundError :
122
- logger .debug ('Ancillary variables land/sea area fraction not '
123
- 'found in cube. Check fx_file availability.' )
124
-
125
- if fx_cube :
126
- fx_cube_data = da .broadcast_to (fx_cube .core_data (), cube .shape )
127
- landsea_mask = _get_fx_mask (fx_cube_data , mask_out ,
128
- fx_cube .var_name )
129
- cube .data = _apply_fx_mask (landsea_mask , cube .core_data ())
130
- logger .debug ("Applying land-sea mask: %s" , fx_cube .var_name )
138
+ logger .debug (
139
+ "Ancillary variables land/sea area fraction not found in "
140
+ "cube. Check fx_file availability."
141
+ )
142
+
143
+ if ancillary_var :
144
+ landsea_mask = _get_fx_mask (
145
+ ancillary_var .core_data (), mask_out , ancillary_var .var_name
146
+ )
147
+ cube .data = _apply_mask (
148
+ landsea_mask ,
149
+ cube .core_data (),
150
+ cube .ancillary_variable_dims (ancillary_var ),
151
+ )
152
+ logger .debug ("Applying land-sea mask: %s" , ancillary_var .var_name )
131
153
else :
132
154
if cube .coord ('longitude' ).points .ndim < 2 :
133
- cube = _mask_with_shp (cube , shapefiles [mask_out ], [
134
- 0 ,
135
- ])
155
+ cube = _mask_with_shp (cube , shapefiles [mask_out ], [0 ])
136
156
logger .debug (
137
157
"Applying land-sea mask from Natural Earth shapefile: \n %s" ,
138
158
shapefiles [mask_out ],
139
159
)
140
160
else :
141
- msg = ("Use of shapefiles with irregular grids not yet "
142
- "implemented, land-sea mask not applied." )
143
- raise ValueError (msg )
161
+ raise ValueError (
162
+ "Use of shapefiles with irregular grids not yet implemented, "
163
+ "land-sea mask not applied."
164
+ )
144
165
145
166
return cube
146
167
@@ -149,7 +170,7 @@ def mask_landsea(cube, mask_out):
149
170
variables = ['sftgif' ],
150
171
required = 'require_at_least_one' ,
151
172
)
152
- def mask_landseaice (cube , mask_out ) :
173
+ def mask_landseaice (cube : Cube , mask_out : Literal [ 'landsea' , 'ice' ]) -> Cube :
153
174
"""Mask out either landsea (combined) or ice.
154
175
155
176
Function that masks out either landsea (land and seas) or ice (Antarctica,
@@ -159,13 +180,13 @@ def mask_landseaice(cube, mask_out):
159
180
160
181
Parameters
161
182
----------
162
- cube: iris.cube.Cube
163
- data cube to be masked. It should have an
183
+ cube:
184
+ Data cube to be masked. It should have an
164
185
:class:`iris.coords.AncillaryVariable` with standard name
165
186
``'land_ice_area_fraction'``.
166
-
167
187
mask_out: str
168
- either "landsea" to mask out landsea or "ice" to mask out ice.
188
+ Either ``'landsea'`` to mask out land and oceans or ``'ice'`` to mask
189
+ out ice.
169
190
170
191
Returns
171
192
-------
@@ -178,20 +199,26 @@ def mask_landseaice(cube, mask_out):
178
199
Error raised if landsea-ice mask not found as an ancillary variable.
179
200
"""
180
201
# sftgif is the only one so far but users can set others
181
- fx_cube = None
202
+ ancillary_var = None
182
203
try :
183
- fx_cube = cube .ancillary_variable ('land_ice_area_fraction' )
204
+ ancillary_var = cube .ancillary_variable ('land_ice_area_fraction' )
184
205
except iris .exceptions .AncillaryVariableNotFoundError :
185
- logger .debug ('Ancillary variable land ice area fraction '
186
- 'not found in cube. Check fx_file availability.' )
187
- if fx_cube :
188
- fx_cube_data = da .broadcast_to (fx_cube .core_data (), cube .shape )
189
- landice_mask = _get_fx_mask (fx_cube_data , mask_out , fx_cube .var_name )
190
- cube .data = _apply_fx_mask (landice_mask , cube .core_data ())
206
+ logger .debug (
207
+ "Ancillary variable land ice area fraction not found in cube. "
208
+ "Check fx_file availability."
209
+ )
210
+ if ancillary_var :
211
+ landseaice_mask = _get_fx_mask (
212
+ ancillary_var .core_data (), mask_out , ancillary_var .var_name
213
+ )
214
+ cube .data = _apply_mask (
215
+ landseaice_mask ,
216
+ cube .core_data (),
217
+ cube .ancillary_variable_dims (ancillary_var ),
218
+ )
191
219
logger .debug ("Applying landsea-ice mask: sftgif" )
192
220
else :
193
- msg = "Landsea-ice mask could not be found. Stopping. "
194
- raise ValueError (msg )
221
+ raise ValueError ("Landsea-ice mask could not be found. Stopping." )
195
222
196
223
return cube
197
224
@@ -285,9 +312,10 @@ def _mask_with_shp(cube, shapefilename, region_indices=None):
285
312
# Create a set of x,y points from the cube
286
313
# 1D regular grids
287
314
if cube .coord ('longitude' ).points .ndim < 2 :
288
- x_p , y_p = da .meshgrid (
315
+ x_p , y_p = np .meshgrid (
289
316
cube .coord (axis = 'X' ).points ,
290
- cube .coord (axis = 'Y' ).points )
317
+ cube .coord (axis = 'Y' ).points ,
318
+ )
291
319
# 2D irregular grids; spit an error for now
292
320
else :
293
321
msg = ("No fx-files found (sftlf or sftof)!"
@@ -296,14 +324,14 @@ def _mask_with_shp(cube, shapefilename, region_indices=None):
296
324
raise ValueError (msg )
297
325
298
326
# Wrap around longitude coordinate to match data
299
- x_p_180 = da .where (x_p >= 180. , x_p - 360. , x_p )
327
+ x_p_180 = np .where (x_p >= 180. , x_p - 360. , x_p )
300
328
301
329
# the NE mask has no points at x = -180 and y = +/-90
302
330
# so we will fool it and apply the mask at (-179, -89, 89) instead
303
- x_p_180 = da .where (x_p_180 == - 180. , x_p_180 + 1. , x_p_180 )
331
+ x_p_180 = np .where (x_p_180 == - 180. , x_p_180 + 1. , x_p_180 )
304
332
305
- y_p_0 = da .where (y_p == - 90. , y_p + 1. , y_p )
306
- y_p_90 = da .where (y_p_0 == 90. , y_p_0 - 1. , y_p_0 )
333
+ y_p_0 = np .where (y_p == - 90. , y_p + 1. , y_p )
334
+ y_p_90 = np .where (y_p_0 == 90. , y_p_0 - 1. , y_p_0 )
307
335
308
336
mask = None
309
337
for region in regions :
@@ -313,13 +341,14 @@ def _mask_with_shp(cube, shapefilename, region_indices=None):
313
341
else :
314
342
mask |= shp_vect .contains (region , x_p_180 , y_p_90 )
315
343
316
- mask = da .array (mask )
317
- iris .util .broadcast_to_shape (mask , cube .shape , cube .coord_dims ('latitude' )
318
- + cube .coord_dims ('longitude' ))
344
+ if cube .has_lazy_data ():
345
+ mask = da .array (mask )
319
346
320
- old_mask = da .ma .getmaskarray (cube .core_data ())
321
- mask = old_mask | mask
322
- cube .data = da .ma .masked_array (cube .core_data (), mask = mask )
347
+ cube .data = _apply_mask (
348
+ mask ,
349
+ cube .core_data (),
350
+ cube .coord_dims ('latitude' ) + cube .coord_dims ('longitude' ),
351
+ )
323
352
324
353
return cube
325
354
0 commit comments