def test_multiple_coords(self): cube = iris.tests.stock.realistic_4d() cs = iris.coord_systems.GeogCS(6371229) time_coord = cube.coord('time') time_dims = cube.coord_dims(time_coord) lat_coord = iris.coords.DimCoord(np.arange(time_coord.shape[0]), standard_name='latitude', units='degrees', coord_system=cs) cube.remove_coord(time_coord) cube.add_dim_coord(lat_coord, time_dims) model_level_coord = cube.coord('model_level_number') model_level_dims = cube.coord_dims(model_level_coord) lon_coord = iris.coords.DimCoord(np.arange(model_level_coord.shape[0]), standard_name='longitude', units='degrees', coord_system=cs) cube.remove_coord(model_level_coord) cube.add_dim_coord(lon_coord, model_level_dims) with self.assertRaises(ValueError): get_xy_dim_coords(cube) cube.remove_coord('grid_latitude') cube.remove_coord('grid_longitude') x, y = get_xy_dim_coords(cube) self.assertIs(x, lon_coord) self.assertIs(y, lat_coord)
def test_different_coordsystem(self): cube = iris.tests.stock.lat_lon_cube() lat_cs = copy.copy(cube.coord('latitude').coord_system) lat_cs.semi_major_axis = 7000000 cube.coord('latitude').coord_system = lat_cs lon_cs = copy.copy(cube.coord('longitude').coord_system) lon_cs.semi_major_axis = 7000001 cube.coord('longitude').coord_system = lon_cs with self.assertRaises(ValueError): get_xy_dim_coords(cube)
def test_projection_coords(self): cube = iris.tests.stock.lat_lon_cube() cube.coord('longitude').rename('projection_x_coordinate') cube.coord('latitude').rename('projection_y_coordinate') x, y = get_xy_dim_coords(cube) self.assertIs(x, cube.coord('projection_x_coordinate')) self.assertIs(y, cube.coord('projection_y_coordinate'))
def __call__(self, cube): """ Regrid this :class:`~iris.cube.Cube` onto the target grid of this :class:`AreaWeightedRegridder`. The given cube must be defined with the same grid as the source grid used to create this :class:`AreaWeightedRegridder`. Args: * cube: A :class:`~iris.cube.Cube` to be regridded. Returns: A cube defined with the horizontal dimensions of the target and the other dimensions from this cube. The data values of this cube will be converted to values on the new grid using area-weighted regridding. """ if get_xy_dim_coords(cube) != self._src_grid: raise ValueError('The given cube is not defined on the same ' 'source grid as this regridder.') return eregrid.regrid_area_weighted_rectilinear_src_and_grid( cube, self._target_grid_cube, mdtol=self._mdtol)
def test_no_coordsystem(self): cube = iris.tests.stock.lat_lon_cube() for coord in cube.coords(): coord.coord_system = None x, y = get_xy_dim_coords(cube) self.assertIs(x, cube.coord('longitude')) self.assertIs(y, cube.coord('latitude'))
def __init__(self, src_grid, tgt_grid, mdtol=0): """ Create regridder for conversions between source grid and target grid. Parameters ---------- src_grid_cube : cube The rectilinear iris cube providing the source grid. target_grid_cube : cube The rectilinear iris cube providing the target grid. mdtol : float, optional Tolerance of missing data. The value returned in each element of the returned array will be masked if the fraction of masked data exceeds mdtol. mdtol=0 means no missing data is tolerated while mdtol=1 will mean the resulting element will be masked if and only if all the contributing elements of data are masked. Defaults to 0. """ if not (0 <= mdtol <= 1): msg = "Value for mdtol must be in range 0 - 1, got {}." raise ValueError(msg.format(mdtol)) self.mdtol = mdtol regrid_info = _regrid_rectilinear_to_rectilinear__prepare( src_grid, tgt_grid) # Store regrid info. self.grid_x = regrid_info.x_coord self.grid_y = regrid_info.y_coord self.regridder = regrid_info.regridder # Record the source grid. self.src_grid = get_xy_dim_coords(src_grid)
def __call__(self, cube): """ Regrid this :class:`~iris.cube.Cube` on to the target grid of this :class:`LinearRegridder`. The given cube must be defined with the same grid as the source grid used to create this :class:`LinearRegridder`. Args: * cube: A :class:`~iris.cube.Cube` to be regridded. Returns: A cube defined with the horizontal dimensions of the target and the other dimensions from this cube. The data values of this cube will be converted to values on the new grid using linear interpolation. """ if get_xy_dim_coords(cube) != self._src_grid: raise ValueError('The given cube is not defined on the same ' 'source grid as this regridder.') return eregrid.regrid_bilinear_rectilinear_src_and_grid( cube, self._target_grid_cube, extrapolation_mode=self._extrapolation_mode)
def __init__(self, src_cube, tgt_grid_cube, method, projection=None): """ Create a regridder for conversions between the source and target grids. Args: * src_cube: The :class:`~iris.cube.Cube` providing the source points. * tgt_grid_cube: The :class:`~iris.cube.Cube` providing the target grid. * method: Either 'linear' or 'nearest'. * projection: The projection in which the interpolation is performed. If None, a PlateCarree projection is used. Defaults to None. """ # Validity checks. if not isinstance(src_cube, iris.cube.Cube): raise TypeError("'src_cube' must be a Cube") if not isinstance(tgt_grid_cube, iris.cube.Cube): raise TypeError("'tgt_grid_cube' must be a Cube") # Snapshot the state of the target cube to ensure that the regridder # is impervious to external changes to the original source cubes. self._tgt_grid = snapshot_grid(tgt_grid_cube) # Check the target grid units. for coord in self._tgt_grid: self._check_units(coord) # Whether to use linear or nearest-neighbour interpolation. if method not in ('linear', 'nearest'): msg = 'Regridding method {!r} not supported.'.format(method) raise ValueError(msg) self._method = method src_x_coord, src_y_coord = get_xy_coords(src_cube) if src_x_coord.coord_system != src_y_coord.coord_system: raise ValueError("'src_cube' lateral geographic coordinates have " "differing coordinate sytems.") if src_x_coord.coord_system is None: raise ValueError("'src_cube' lateral geographic coordinates have " "no coordinate sytem.") tgt_x_coord, tgt_y_coord = get_xy_dim_coords(tgt_grid_cube) if tgt_x_coord.coord_system != tgt_y_coord.coord_system: raise ValueError("'tgt_grid_cube' lateral geographic coordinates " "have differing coordinate sytems.") if tgt_x_coord.coord_system is None: raise ValueError("'tgt_grid_cube' lateral geographic coordinates " "have no coordinate sytem.") if projection is None: globe = src_x_coord.coord_system.as_cartopy_globe() projection = ccrs.Sinusoidal(globe=globe) self._projection = projection
def _regrid_rectilinear_to_rectilinear__prepare(src_grid_cube, tgt_grid_cube): tgt_x, tgt_y = get_xy_dim_coords(tgt_grid_cube) src_x, src_y = get_xy_dim_coords(src_grid_cube) grid_x_dim = src_grid_cube.coord_dims(src_x)[0] grid_y_dim = src_grid_cube.coord_dims(src_y)[0] srcinfo = _cube_to_GridInfo(src_grid_cube) tgtinfo = _cube_to_GridInfo(tgt_grid_cube) regridder = Regridder(srcinfo, tgtinfo) regrid_info = RegridInfo( x_dim=grid_x_dim, y_dim=grid_y_dim, x_coord=tgt_x, y_coord=tgt_y, regridder=regridder, ) return regrid_info
def __call__(self, cube): """ Regrid this :class:`~iris.cube.Cube` onto the target grid of this :class:`AreaWeightedRegridder`. The given cube must be defined with the same grid as the source grid used to create this :class:`AreaWeightedRegridder`. If the source cube has lazy data, the returned cube will also have lazy data. Args: * cube: A :class:`~iris.cube.Cube` to be regridded. Returns: A cube defined with the horizontal dimensions of the target and the other dimensions from this cube. The data values of this cube will be converted to values on the new grid using area-weighted regridding. .. note:: If the source cube has lazy data, `chunks <https://docs.dask.org/en/latest/array-chunks.html>`__ in the horizontal dimensions will be combined before regridding. """ from iris.experimental.regrid import ( _regrid_area_weighted_rectilinear_src_and_grid__perform, ) src_x, src_y = get_xy_dim_coords(cube) if (src_x, src_y) != self._src_grid: raise ValueError("The given cube is not defined on the same " "source grid as this regridder.") src_x_dim = cube.coord_dims(src_x)[0] src_y_dim = cube.coord_dims(src_y)[0] _regrid_info = ( src_x, src_y, src_x_dim, src_y_dim, self.grid_x, self.grid_y, self.meshgrid_x, self.meshgrid_y, self.weights_info, ) return _regrid_area_weighted_rectilinear_src_and_grid__perform( cube, _regrid_info, mdtol=self._mdtol)
def __call__(self, cube): """ Regrid this :class:`~iris.cube.Cube` onto the target grid of this :class:`AreaWeightedRegridder`. The given cube must be defined with the same grid as the source grid used to create this :class:`AreaWeightedRegridder`. Args: * cube: A :class:`~iris.cube.Cube` to be regridded. Returns: A cube defined with the horizontal dimensions of the target and the other dimensions from this cube. The data values of this cube will be converted to values on the new grid using area-weighted regridding. """ src_x, src_y = get_xy_dim_coords(cube) if (src_x, src_y) != self._src_grid: raise ValueError( "The given cube is not defined on the same " "source grid as this regridder." ) src_x_dim = cube.coord_dims(src_x)[0] src_y_dim = cube.coord_dims(src_y)[0] _regrid_info = ( src_x, src_y, src_x_dim, src_y_dim, self.grid_x, self.grid_y, self.meshgrid_x, self.meshgrid_y, self.weights_info, ) return eregrid._regrid_area_weighted_rectilinear_src_and_grid__perform( cube, _regrid_info, mdtol=self._mdtol )
def __call__(self, cube): """ Regrid this cube onto the target grid of this regridder instance. The given cube must be defined with the same grid as the source cube used to create this ESMFAreaWeightedRegridder instance. Parameters ---------- cube : cube A iris.cube.Cube instance to be regridded. Returns ------- A cube defined with the horizontal dimensions of the target and the other dimensions from this cube. The data values of this cube will be converted to values on the new grid using area-weighted regridding via ESMF generated weights. """ src_x, src_y = get_xy_dim_coords(cube) # Check the source grid matches that used in initialisation if self.src_grid != (src_x, src_y): raise ValueError("The given cube is not defined on the same " "source grid as this regridder.") grid_x_dim = cube.coord_dims(src_x)[0] grid_y_dim = cube.coord_dims(src_y)[0] regrid_info = RegridInfo( x_dim=grid_x_dim, y_dim=grid_y_dim, x_coord=self.grid_x, y_coord=self.grid_y, regridder=self.regridder, ) return _regrid_rectilinear_to_rectilinear__perform( cube, regrid_info, self.mdtol)
def __call__(self, cube): """ Regrid this :class:`~iris.cube.Cube` onto the target grid of this :class:`AreaWeightedRegridder`. The given cube must be defined with the same grid as the source grid used to create this :class:`AreaWeightedRegridder`. Args: * cube: A :class:`~iris.cube.Cube` to be regridded. Returns: A cube defined with the horizontal dimensions of the target and the other dimensions from this cube. The data values of this cube will be converted to values on the new grid using area-weighted regridding. """ if get_xy_dim_coords(cube) != self._src_grid: raise ValueError("The given cube is not defined on the same " "source grid as this regridder.") return eregrid.regrid_area_weighted_rectilinear_src_and_grid(cube, self._target_grid_cube, mdtol=self._mdtol)
def test_lat_lon(self): cube = iris.tests.stock.lat_lon_cube() x, y = get_xy_dim_coords(cube) self.assertIs(x, cube.coord('longitude')) self.assertIs(y, cube.coord('latitude'))
def test_grid_lat_lon(self): cube = iris.tests.stock.realistic_4d() x, y = get_xy_dim_coords(cube) self.assertIs(x, cube.coord('grid_longitude')) self.assertIs(y, cube.coord('grid_latitude'))
def test_one_coordsystem(self): cube = iris.tests.stock.lat_lon_cube() cube.coord('longitude').coord_system = None with self.assertRaises(ValueError): get_xy_dim_coords(cube)
def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube): """ Return a new cube with the data values calculated using the weighted mean of data values from :data:`src_cube` and the weights from :data:`weights` regridded onto the horizontal grid of :data:`grid_cube`. This function requires that the :data:`src_cube` has a curvilinear horizontal grid and the target :data:`grid_cube` is rectilinear i.e. expressed in terms of two orthogonal 1D horizontal coordinates. Both grids must be in the same coordinate system, and the :data:`grid_cube` must have horizontal coordinates that are both bounded and contiguous. Note that, for any given target :data:`grid_cube` cell, only the points from the :data:`src_cube` that are bound by that cell will contribute to the cell result. The bounded extent of the :data:`src_cube` will not be considered here. A target :data:`grid_cube` cell result will be calculated as, :math:`\sum (src\_cube.data_{ij} * weights_{ij}) / \sum weights_{ij}`, for all :math:`ij` :data:`src_cube` points that are bound by that cell. .. warning:: * Only 2D cubes are supported. * All coordinates that span the :data:`src_cube` that don't define the horizontal curvilinear grid will be ignored. * The :class:`iris.unit.Unit` of the horizontal grid coordinates must be either :data:`degrees` or :data:`radians`. Args: * src_cube: A :class:`iris.cube.Cube` instance that defines the source variable grid to be regridded. * weights: A :class:`numpy.ndarray` instance that defines the weights for the source variable grid cells. Must have the same shape as the :data:`src_cube.data`. * grid_cube: A :class:`iris.cube.Cube` instance that defines the target rectilinear grid. Returns: A :class:`iris.cube.Cube` instance. """ if src_cube.shape != weights.shape: msg = 'The source cube and weights require the same data shape.' raise ValueError(msg) if src_cube.aux_factories: msg = 'All source cube derived coordinates will be ignored.' warnings.warn(msg) # Get the source cube x and y 2D auxiliary coordinates. sx, sy = src_cube.coord(axis='x'), src_cube.coord(axis='y') # Get the target grid cube x and y dimension coordinates. tx, ty = get_xy_dim_coords(grid_cube) if sx.units.modulus is None or sy.units.modulus is None or \ sx.units != sy.units: msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \ 'have units of degrees or radians.' raise ValueError(msg.format(sx.name(), sy.name())) if tx.units.modulus is None or ty.units.modulus is None or \ tx.units != ty.units: msg = 'The target grid cube x ({!r}) and y ({!r}) coordinates must ' \ 'have units of degrees or radians.' raise ValueError(msg.format(tx.name(), ty.name())) if sx.units != tx.units: msg = 'The source cube and target grid cube must have x and y ' \ 'coordinates with the same units.' raise ValueError(msg) if sx.ndim != sy.ndim: msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \ 'have the same dimensionality.' raise ValueError(msg.format(sx.name(), sy.name())) if sx.ndim != 2: msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \ 'be 2D auxiliary coordinates.' raise ValueError(msg.format(sx.name(), sy.name())) if sx.coord_system != sy.coord_system: msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \ 'have the same coordinate system.' raise ValueError(msg.format(sx.name(), sy.name())) if sx.coord_system != tx.coord_system and \ sx.coord_system is not None and \ tx.coord_system is not None: msg = 'The source cube and target grid cube must have the same ' \ 'coordinate system.' raise ValueError(msg) if not tx.has_bounds() or not tx.is_contiguous(): msg = 'The target grid cube x ({!r})coordinate requires ' \ 'contiguous bounds.' raise ValueError(msg.format(tx.name())) if not ty.has_bounds() or not ty.is_contiguous(): msg = 'The target grid cube y ({!r}) coordinate requires ' \ 'contiguous bounds.' raise ValueError(msg.format(ty.name())) def _src_align_and_flatten(coord): # Return a flattened, unmasked copy of a coordinate's points array that # will align with a flattened version of the source cube's data. points = coord.points if src_cube.coord_dims(coord) == (1, 0): points = points.T if points.shape != src_cube.shape: msg = 'The shape of the points array of !r is not compatible ' \ 'with the shape of !r.'.format(coord.name(), src_cube.name()) raise ValueError(msg) return np.asarray(points.flatten()) # Align and flatten the coordinate points of the source space. sx_points = _src_align_and_flatten(sx) sy_points = _src_align_and_flatten(sy) # Match the source cube x coordinate range to the target grid # cube x coordinate range. min_sx, min_tx = np.min(sx.points), np.min(tx.points) modulus = sx.units.modulus if min_sx < 0 and min_tx >= 0: indices = np.where(sx_points < 0) sx_points[indices] += modulus elif min_sx >= 0 and min_tx < 0: indices = np.where(sx_points > (modulus / 2)) sx_points[indices] -= modulus # Create target grid cube x and y cell boundaries. tx_depth, ty_depth = tx.points.size, ty.points.size tx_dim, = grid_cube.coord_dims(tx) ty_dim, = grid_cube.coord_dims(ty) tx_cells = np.concatenate((tx.bounds[:, 0], tx.bounds[-1, 1].reshape(1))) ty_cells = np.concatenate((ty.bounds[:, 0], ty.bounds[-1, 1].reshape(1))) # Determine the target grid cube x and y cells that bound # the source cube x and y points. def _regrid_indices(cells, depth, points): # Calculate the minimum difference in cell extent. extent = np.min(np.diff(cells)) if extent == 0: # Detected an dimension coordinate with an invalid # zero length cell extent. msg = 'The target grid cube {} ({!r}) coordinate contains ' \ 'a zero length cell extent.' axis, name = 'x', tx.name() if points is sy_points: axis, name = 'y', ty.name() raise ValueError(msg.format(axis, name)) elif extent > 0: # The cells of the dimension coordinate are in ascending order. indices = np.searchsorted(cells, points, side='right') - 1 else: # The cells of the dimension coordinate are in descending order. # np.searchsorted() requires ascending order, so we require to # account for this restriction. cells = cells[::-1] right = np.searchsorted(cells, points, side='right') left = np.searchsorted(cells, points, side='left') indices = depth - right # Only those points that exactly match the left-hand cell bound # will differ between 'left' and 'right'. Thus their appropriate # target cell location requires to be recalculated to give the # correct descending [upper, lower) interval cell, source to target # regrid behaviour. delta = np.where(left != right)[0] if delta.size: indices[delta] = depth - left[delta] return indices x_indices = _regrid_indices(tx_cells, tx_depth, sx_points) y_indices = _regrid_indices(ty_cells, ty_depth, sy_points) # Now construct a sparse M x N matix, where M is the flattened target # space, and N is the flattened source space. The sparse matrix will then # be populated with those source cube points that contribute to a specific # target cube cell. # Determine the valid indices and their offsets in M x N space. if ma.isMaskedArray(src_cube.data): # Calculate the valid M offsets, accounting for the source cube mask. mask = ~src_cube.data.mask.flatten() cols = np.where((y_indices >= 0) & (y_indices < ty_depth) & (x_indices >= 0) & (x_indices < tx_depth) & mask)[0] else: # Calculate the valid M offsets. cols = np.where((y_indices >= 0) & (y_indices < ty_depth) & (x_indices >= 0) & (x_indices < tx_depth))[0] # Reduce the indices to only those that are valid. x_indices = x_indices[cols] y_indices = y_indices[cols] # Calculate the valid N offsets. if ty_dim < tx_dim: rows = y_indices * tx.points.size + x_indices else: rows = x_indices * ty.points.size + y_indices # Calculate the associated valid weights. weights_flat = weights.flatten() data = weights_flat[cols] # Build our sparse M x N matrix of weights. sparse_matrix = csc_matrix((data, (rows, cols)), shape=(grid_cube.data.size, src_cube.data.size)) # Performing a sparse sum to collapse the matrix to (M, 1). sum_weights = sparse_matrix.sum(axis=1).getA() # Determine the rows (flattened target indices) that have a # contribution from one or more source points. rows = np.nonzero(sum_weights) # Calculate the numerator of the weighted mean (M, 1). numerator = sparse_matrix * src_cube.data.reshape(-1, 1) # Calculate the weighted mean payload. weighted_mean = ma.masked_all(numerator.shape, dtype=numerator.dtype) weighted_mean[rows] = numerator[rows] / sum_weights[rows] # Construct the final regridded weighted mean cube. dim_coords_and_dims = list(zip((ty.copy(), tx.copy()), (ty_dim, tx_dim))) cube = iris.cube.Cube(weighted_mean.reshape(grid_cube.shape), dim_coords_and_dims=dim_coords_and_dims) cube.metadata = copy.deepcopy(src_cube.metadata) for coord in src_cube.coords(dimensions=()): cube.add_aux_coord(coord.copy()) return cube
def __call__(self, src): """ Regrid this :class:`~iris.cube.Cube` on to the target grid of this :class:`RectilinearRegridder`. The given cube must be defined with the same grid as the source grid used to create this :class:`RectilinearRegridder`. If the source cube has lazy data, the returned cube will also have lazy data. Args: * src: A :class:`~iris.cube.Cube` to be regridded. Returns: A cube defined with the horizontal dimensions of the target and the other dimensions from this cube. The data values of this cube will be converted to values on the new grid using either nearest-neighbour or linear interpolation. .. note:: If the source cube has lazy data, `chunks <https://docs.dask.org/en/latest/array-chunks.html>`__ in the horizontal dimensions will be combined before regridding. """ from iris.cube import Cube # Validity checks. if not isinstance(src, Cube): raise TypeError("'src' must be a Cube") if get_xy_dim_coords(src) != self._src_grid: raise ValueError("The given cube is not defined on the same " "source grid as this regridder.") src_x_coord, src_y_coord = get_xy_dim_coords(src) grid_x_coord, grid_y_coord = self._tgt_grid src_cs = src_x_coord.coord_system grid_cs = grid_x_coord.coord_system if src_cs is None and grid_cs is None: if not (src_x_coord.is_compatible(grid_x_coord) and src_y_coord.is_compatible(grid_y_coord)): raise ValueError("The rectilinear grid coordinates of the " "given cube and target grid have no " "coordinate system but they do not have " "matching coordinate metadata.") elif src_cs is None or grid_cs is None: raise ValueError("The rectilinear grid coordinates of the given " "cube and target grid must either both have " "coordinate systems or both have no coordinate " "system but with matching coordinate metadata.") # Check the source grid units. for coord in (src_x_coord, src_y_coord): self._check_units(coord) # Convert the grid to a 2D sample grid in the src CRS. sample_grid = self._sample_grid(src_cs, grid_x_coord, grid_y_coord) sample_grid_x, sample_grid_y = sample_grid # Compute the interpolated data values. x_dim = src.coord_dims(src_x_coord)[0] y_dim = src.coord_dims(src_y_coord)[0] # Define regrid function regrid = functools.partial( self._regrid, x_dim=x_dim, y_dim=y_dim, src_x_coord=src_x_coord, src_y_coord=src_y_coord, sample_grid_x=sample_grid_x, sample_grid_y=sample_grid_y, method=self._method, extrapolation_mode=self._extrapolation_mode, ) data = map_complete_blocks(src, regrid, (y_dim, x_dim), sample_grid_x.shape) # Wrap up the data as a Cube. regrid_callback = functools.partial(self._regrid, method=self._method, extrapolation_mode="nan") result = self._create_cube( data, src, x_dim, y_dim, src_x_coord, src_y_coord, grid_x_coord, grid_y_coord, sample_grid_x, sample_grid_y, regrid_callback, ) return result
def regrid_bilinear_rectilinear_src_and_grid(src, grid, extrapolation_mode='mask'): """ Return a new Cube that is the result of regridding the source Cube onto the grid of the grid Cube using bilinear interpolation. Both the source and grid Cubes must be defined on rectilinear grids. Auxiliary coordinates which span the grid dimensions are ignored, except where they provide a reference surface for an :class:`iris.aux_factory.AuxCoordFactory`. Args: * src: The source :class:`iris.cube.Cube` providing the data. * grid: The :class:`iris.cube.Cube` which defines the new grid. Kwargs: * extrapolation_mode: Must be one of the following strings: * 'linear' - The extrapolation points will be calculated by extending the gradient of the closest two points. * 'nan' - The extrapolation points will be be set to NaN. * 'error' - A ValueError exception will be raised, notifying an attempt to extrapolate. * 'mask' - The extrapolation points will always be masked, even if the source data is not a MaskedArray. * 'nanmask' - If the source data is a MaskedArray the extrapolation points will be masked. Otherwise they will be set to NaN. The default mode of extrapolation is 'mask'. Returns: The :class:`iris.cube.Cube` resulting from regridding the source data onto the grid defined by the grid Cube. """ # Validity checks if not isinstance(src, iris.cube.Cube): raise TypeError("'src' must be a Cube") if not isinstance(grid, iris.cube.Cube): raise TypeError("'grid' must be a Cube") src_x_coord, src_y_coord = get_xy_dim_coords(src) grid_x_coord, grid_y_coord = get_xy_dim_coords(grid) src_cs = src_x_coord.coord_system grid_cs = grid_x_coord.coord_system if src_cs is None and grid_cs is None: if not (src_x_coord.is_compatible(grid_x_coord) and src_y_coord.is_compatible(grid_y_coord)): raise ValueError("The rectilinear grid coordinates of the 'src' " "and 'grid' Cubes have no coordinate system but " "they do not have matching coordinate metadata.") elif src_cs is None or grid_cs is None: raise ValueError("The rectilinear grid coordinates of the 'src' and " "'grid' Cubes must either both have coordinate " "systems or both have no coordinate system but with " "matching coordinate metadata.") def _check_units(coord): if coord.coord_system is None: # No restriction on units. pass elif isinstance(coord.coord_system, (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS)): # Units for lat-lon or rotated pole must be 'degrees'. Note # that 'degrees_east' etc. are equal to 'degrees'. if coord.units != 'degrees': msg = "Unsupported units for coordinate system. " \ "Expected 'degrees' got {!r}.".format(coord.units) raise ValueError(msg) else: # Units for other coord systems must be equal to metres. if coord.units != 'm': msg = "Unsupported units for coordinate system. " \ "Expected 'metres' got {!r}.".format(coord.units) raise ValueError(msg) for coord in (src_x_coord, src_y_coord, grid_x_coord, grid_y_coord): _check_units(coord) if extrapolation_mode not in EXTRAPOLATION_MODES: raise ValueError('Invalid extrapolation mode.') # Convert the grid to a 2D sample grid in the src CRS. sample_grid = RectilinearRegridder._sample_grid(src_cs, grid_x_coord, grid_y_coord) sample_grid_x, sample_grid_y = sample_grid # Compute the interpolated data values. x_dim = src.coord_dims(src_x_coord)[0] y_dim = src.coord_dims(src_y_coord)[0] data = RectilinearRegridder._regrid_bilinear_array(src.data, x_dim, y_dim, src_x_coord, src_y_coord, sample_grid_x, sample_grid_y, extrapolation_mode) # Wrap up the data as a Cube. regrid_callback = partial(RectilinearRegridder._regrid_bilinear_array, extrapolation_mode='nan') result = RectilinearRegridder._create_cube(data, src, x_dim, y_dim, src_x_coord, src_y_coord, grid_x_coord, grid_y_coord, sample_grid_x, sample_grid_y, regrid_callback) return result
def __call__(self, src): """ Regrid this :class:`~iris.cube.Cube` on to the target grid of this :class:`RectilinearRegridder`. The given cube must be defined with the same grid as the source grid used to create this :class:`RectilinearRegridder`. Args: * src: A :class:`~iris.cube.Cube` to be regridded. Returns: A cube defined with the horizontal dimensions of the target and the other dimensions from this cube. The data values of this cube will be converted to values on the new grid using either nearest-neighbour or linear interpolation. """ # Validity checks. if not isinstance(src, iris.cube.Cube): raise TypeError("'src' must be a Cube") if get_xy_dim_coords(src) != self._src_grid: raise ValueError('The given cube is not defined on the same ' 'source grid as this regridder.') src_x_coord, src_y_coord = get_xy_dim_coords(src) grid_x_coord, grid_y_coord = self._tgt_grid src_cs = src_x_coord.coord_system grid_cs = grid_x_coord.coord_system if src_cs is None and grid_cs is None: if not (src_x_coord.is_compatible(grid_x_coord) and src_y_coord.is_compatible(grid_y_coord)): raise ValueError("The rectilinear grid coordinates of the " "given cube and target grid have no " "coordinate system but they do not have " "matching coordinate metadata.") elif src_cs is None or grid_cs is None: raise ValueError("The rectilinear grid coordinates of the given " "cube and target grid must either both have " "coordinate systems or both have no coordinate " "system but with matching coordinate metadata.") # Check the source grid units. for coord in (src_x_coord, src_y_coord): self._check_units(coord) # Convert the grid to a 2D sample grid in the src CRS. sample_grid = self._sample_grid(src_cs, grid_x_coord, grid_y_coord) sample_grid_x, sample_grid_y = sample_grid # Compute the interpolated data values. x_dim = src.coord_dims(src_x_coord)[0] y_dim = src.coord_dims(src_y_coord)[0] data = self._regrid(src.data, x_dim, y_dim, src_x_coord, src_y_coord, sample_grid_x, sample_grid_y, self._method, self._extrapolation_mode) # Wrap up the data as a Cube. regrid_callback = functools.partial(self._regrid, method=self._method, extrapolation_mode='nan') result = self._create_cube(data, src, x_dim, y_dim, src_x_coord, src_y_coord, grid_x_coord, grid_y_coord, sample_grid_x, sample_grid_y, regrid_callback) return result
def test_missing_y_coord(self): cube = iris.tests.stock.realistic_4d() cube.remove_coord('grid_latitude') with self.assertRaises(ValueError): get_xy_dim_coords(cube)
def __call__(self, src_cube): """ Regrid the provided :class:`~iris.cube.Cube` on to the target grid of this :class:`AreaWeightedRegridder`. The supplied :class:`~iris.cube.Cube` must be defined with the same grid as the source grid used to create this :class:`AreaWeightedRegridder`. Args: * src_cube: A :class:`~iris.cube.Cube` to be regridded. Returns: A :class:`~iris.cube.Cube` defined with the horizontal dimensions of the target and the other dimensions from the supplied source :class:`~iris.cube.Cube`. The data values of the supplied source :class:`~iris.cube.Cube` will be converted to values on the new grid using conservative area-weighted regridding. """ # Sanity check the supplied source cube. if not isinstance(src_cube, iris.cube.Cube): raise TypeError('The source must be a cube.') # Get the source cube x and y coordinates. sx, sy = get_xy_dim_coords(src_cube) if (sx, sy) != self._src_grid: emsg = 'The source cube is not defined on the same source grid ' \ 'as this regridder.' raise ValueError(emsg) if sx.coord_system is None: msg = 'The source cube requires a native coordinate system.' raise ValueError(msg) # Convert the contiguous bounds of the grid to the source crs. gxx, gyy = np.meshgrid(self._gx.contiguous_bounds(), self._gy.contiguous_bounds()) # Now calculate and cache the grid bounds in the source crs. if self._gx_bounds is None or self._gy_bounds is None: if sx.coord_system == self._gx.coord_system: self._gx_bounds, self._gy_bounds = gxx, gyy else: from_crs = self._gx.coord_system.as_cartopy_crs() to_crs = sx.coord_system.as_cartopy_crs() xyz = to_crs.transform_points(from_crs, gxx, gyy) self._gx_bounds, self._gy_bounds = xyz[..., 0], xyz[..., 1] # Calculate and cache the source contiguous bounds. if self._sx_bounds is None or self._sy_bounds is None: self._sx_bounds = sx.contiguous_bounds() self._sy_bounds = sy.contiguous_bounds() sx_dim = src_cube.coord_dims(sx)[0] sy_dim = src_cube.coord_dims(sy)[0] # Perform the regrid. result = agg(src_cube.data, sx.points, self._sx_bounds, sy.points, self._sy_bounds, sx_dim, sy_dim, self._gx_bounds, self._gy_bounds) # # XXX: Need to deal the factories when constructing result cube. # result_cube = iris.cube.Cube(result) result_cube.metadata = copy.deepcopy(src_cube.metadata) coord_mapping = {} def copy_coords(coords, add_coord): for coord in coords: dims = src_cube.coord_dims(coord) if coord is sx: coord = self._gx elif coord is sy: coord = self._gy elif sx_dim in dims or sy_dim in dims: continue result_coord = coord.copy() add_coord(result_coord, dims) coord_mapping[id(coord)] = result_coord copy_coords(src_cube.dim_coords, result_cube.add_dim_coord) copy_coords(src_cube.aux_coords, result_cube.add_aux_coord) return result_cube
def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube): """ Return a new cube with the data values calculated using the weighted mean of data values from :data:`src_cube` and the weights from :data:`weights` regridded onto the horizontal grid of :data:`grid_cube`. This function requires that the :data:`src_cube` has a curvilinear horizontal grid and the target :data:`grid_cube` is rectilinear i.e. expressed in terms of two orthogonal 1D horizontal coordinates. Both grids must be in the same coordinate system, and the :data:`grid_cube` must have horizontal coordinates that are both bounded and contiguous. Note that, for any given target :data:`grid_cube` cell, only the points from the :data:`src_cube` that are bound by that cell will contribute to the cell result. The bounded extent of the :data:`src_cube` will not be considered here. A target :data:`grid_cube` cell result will be calculated as, :math:`\sum (src\_cube.data_{ij} * weights_{ij}) / \sum weights_{ij}`, for all :math:`ij` :data:`src_cube` points that are bound by that cell. .. warning:: * Only 2D cubes are supported. * All coordinates that span the :data:`src_cube` that don't define the horizontal curvilinear grid will be ignored. * The :class:`iris.unit.Unit` of the horizontal grid coordinates must be either :data:`degrees` or :data:`radians`. Args: * src_cube: A :class:`iris.cube.Cube` instance that defines the source variable grid to be regridded. * weights: A :class:`numpy.ndarray` instance that defines the weights for the source variable grid cells. Must have the same shape as the :data:`src_cube.data`. * grid_cube: A :class:`iris.cube.Cube` instance that defines the target rectilinear grid. Returns: A :class:`iris.cube.Cube` instance. """ if src_cube.shape != weights.shape: msg = 'The source cube and weights require the same data shape.' raise ValueError(msg) if src_cube.aux_factories: msg = 'All source cube derived coordinates will be ignored.' warnings.warn(msg) # Get the source cube x and y 2D auxiliary coordinates. sx, sy = src_cube.coord(axis='x'), src_cube.coord(axis='y') # Get the target grid cube x and y dimension coordinates. tx, ty = get_xy_dim_coords(grid_cube) if sx.units.modulus is None or sy.units.modulus is None or \ sx.units != sy.units: msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \ 'have units of degrees or radians.' raise ValueError(msg.format(sx.name(), sy.name())) if tx.units.modulus is None or ty.units.modulus is None or \ tx.units != ty.units: msg = 'The target grid cube x ({!r}) and y ({!r}) coordinates must ' \ 'have units of degrees or radians.' raise ValueError(msg.format(tx.name(), ty.name())) if sx.units != tx.units: msg = 'The source cube and target grid cube must have x and y ' \ 'coordinates with the same units.' raise ValueError(msg) if sx.ndim != sy.ndim: msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \ 'have the same dimensionality.' raise ValueError(msg.format(sx.name(), sy.name())) if sx.ndim != 2: msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \ 'be 2D auxiliary coordinates.' raise ValueError(msg.format(sx.name(), sy.name())) if sx.coord_system != sy.coord_system: msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \ 'have the same coordinate system.' raise ValueError(msg.format(sx.name(), sy.name())) if sx.coord_system != tx.coord_system and \ sx.coord_system is not None and \ tx.coord_system is not None: msg = 'The source cube and target grid cube must have the same ' \ 'coordinate system.' raise ValueError(msg) if not tx.has_bounds() or not tx.is_contiguous(): msg = 'The target grid cube x ({!r})coordinate requires ' \ 'contiguous bounds.' raise ValueError(msg.format(tx.name())) if not ty.has_bounds() or not ty.is_contiguous(): msg = 'The target grid cube y ({!r}) coordinate requires ' \ 'contiguous bounds.' raise ValueError(msg.format(ty.name())) def _src_align_and_flatten(coord): # Return a flattened, unmasked copy of a coordinate's points array that # will align with a flattened version of the source cube's data. points = coord.points if src_cube.coord_dims(coord) == (1, 0): points = points.T if points.shape != src_cube.shape: msg = 'The shape of the points array of !r is not compatible ' \ 'with the shape of !r.'.format(coord.name(), src_cube.name()) raise ValueError(msg) return np.asarray(points.flatten()) # Align and flatten the coordinate points of the source space. sx_points = _src_align_and_flatten(sx) sy_points = _src_align_and_flatten(sy) # Match the source cube x coordinate range to the target grid # cube x coordinate range. min_sx, min_tx = np.min(sx.points), np.min(tx.points) modulus = sx.units.modulus if min_sx < 0 and min_tx >= 0: indices = np.where(sx_points < 0) sx_points[indices] += modulus elif min_sx >= 0 and min_tx < 0: indices = np.where(sx_points > (modulus / 2)) sx_points[indices] -= modulus # Create target grid cube x and y cell boundaries. tx_depth, ty_depth = tx.points.size, ty.points.size tx_dim, = grid_cube.coord_dims(tx) ty_dim, = grid_cube.coord_dims(ty) tx_cells = np.concatenate((tx.bounds[:, 0], tx.bounds[-1, 1].reshape(1))) ty_cells = np.concatenate((ty.bounds[:, 0], ty.bounds[-1, 1].reshape(1))) # Determine the target grid cube x and y cells that bound # the source cube x and y points. def _regrid_indices(cells, depth, points): # Calculate the minimum difference in cell extent. extent = np.min(np.diff(cells)) if extent == 0: # Detected an dimension coordinate with an invalid # zero length cell extent. msg = 'The target grid cube {} ({!r}) coordinate contains ' \ 'a zero length cell extent.' axis, name = 'x', tx.name() if points is sy_points: axis, name = 'y', ty.name() raise ValueError(msg.format(axis, name)) elif extent > 0: # The cells of the dimension coordinate are in ascending order. indices = np.searchsorted(cells, points, side='right') - 1 else: # The cells of the dimension coordinate are in descending order. # np.searchsorted() requires ascending order, so we require to # account for this restriction. cells = cells[::-1] right = np.searchsorted(cells, points, side='right') left = np.searchsorted(cells, points, side='left') indices = depth - right # Only those points that exactly match the left-hand cell bound # will differ between 'left' and 'right'. Thus their appropriate # target cell location requires to be recalculated to give the # correct descending [upper, lower) interval cell, source to target # regrid behaviour. delta = np.where(left != right)[0] if delta.size: indices[delta] = depth - left[delta] return indices x_indices = _regrid_indices(tx_cells, tx_depth, sx_points) y_indices = _regrid_indices(ty_cells, ty_depth, sy_points) # Now construct a sparse M x N matix, where M is the flattened target # space, and N is the flattened source space. The sparse matrix will then # be populated with those source cube points that contribute to a specific # target cube cell. # Determine the valid indices and their offsets in M x N space. if ma.isMaskedArray(src_cube.data): # Calculate the valid M offsets, accounting for the source cube mask. mask = ~src_cube.data.mask.flatten() cols = np.where((y_indices >= 0) & (y_indices < ty_depth) & (x_indices >= 0) & (x_indices < tx_depth) & mask)[0] else: # Calculate the valid M offsets. cols = np.where((y_indices >= 0) & (y_indices < ty_depth) & (x_indices >= 0) & (x_indices < tx_depth))[0] # Reduce the indices to only those that are valid. x_indices = x_indices[cols] y_indices = y_indices[cols] # Calculate the valid N offsets. if ty_dim < tx_dim: rows = y_indices * tx.points.size + x_indices else: rows = x_indices * ty.points.size + y_indices # Calculate the associated valid weights. weights_flat = weights.flatten() data = weights_flat[cols] # Build our sparse M x N matrix of weights. sparse_matrix = csc_matrix((data, (rows, cols)), shape=(grid_cube.data.size, src_cube.data.size)) # Performing a sparse sum to collapse the matrix to (M, 1). sum_weights = sparse_matrix.sum(axis=1).getA() # Determine the rows (flattened target indices) that have a # contribution from one or more source points. rows = np.nonzero(sum_weights) # Calculate the numerator of the weighted mean (M, 1). numerator = sparse_matrix * src_cube.data.reshape(-1, 1) # Calculate the weighted mean payload. weighted_mean = ma.masked_all(numerator.shape, dtype=numerator.dtype) weighted_mean[rows] = numerator[rows] / sum_weights[rows] # Construct the final regridded weighted mean cube. dim_coords_and_dims = list(zip((ty.copy(), tx.copy()), (ty_dim, tx_dim))) cube = iris.cube.Cube(weighted_mean.reshape(grid_cube.shape), dim_coords_and_dims=dim_coords_and_dims) cube.metadata = copy.deepcopy(src_cube.metadata) for coord in src_cube.coords(dimensions=()): cube.add_aux_coord(coord.copy()) return cube
def regrid_conservative_via_esmpy(source_cube, grid_cube): """ Perform a conservative regridding with ESMPy. Regrids the data of a source cube onto a new grid defined by a destination cube. Args: * source_cube (:class:`iris.cube.Cube`): Source data. Must have two identifiable horizontal dimension coordinates. * grid_cube (:class:`iris.cube.Cube`): Define the target horizontal grid: Only the horizontal dimension coordinates are actually used. Returns: A new cube derived from source_cube, regridded onto the specified horizontal grid. Any additional coordinates which map onto the horizontal dimensions are removed, while all other metadata is retained. If there are coordinate factories with 2d horizontal reference surfaces, the reference surfaces are also regridded, using ordinary bilinear interpolation. .. note:: Both source and destination cubes must have two dimension coordinates identified with axes 'X' and 'Y' which share a coord_system with a Cartopy CRS. The grids are defined by :meth:`iris.coords.Coord.contiguous_bounds` of these. .. note:: Initialises the ESMF Manager, if it was not already called. This implements default Manager operations (e.g. logging). To alter this, make a prior call to ESMF.Manager(). """ # Lazy import so we can build the docs with no ESMF. import ESMF # Get source + target XY coordinate pairs and check they are suitable. src_coords = get_xy_dim_coords(source_cube) dst_coords = get_xy_dim_coords(grid_cube) src_cs = src_coords[0].coord_system grid_cs = dst_coords[0].coord_system if src_cs is None or grid_cs is None: raise ValueError("Both 'src' and 'grid' Cubes must have a" " coordinate system for their rectilinear grid" " coordinates.") if src_cs.as_cartopy_crs() is None or grid_cs.as_cartopy_crs() is None: raise ValueError("Both 'src' and 'grid' Cubes coord_systems must have " "a valid associated Cartopy CRS.") def _valid_units(coord): if isinstance(coord.coord_system, (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS)): valid_units = 'degrees' else: valid_units = 'm' return coord.units == valid_units if not all(_valid_units(coord) for coord in src_coords + dst_coords): raise ValueError("Unsupported units: must be 'degrees' or 'm'.") # Initialise the ESMF manager in case it was not already done. ESMF.Manager() # Create a data array for the output cube. src_dims_xy = [source_cube.coord_dims(coord)[0] for coord in src_coords] # Size matches source, except for X+Y dimensions dst_shape = np.array(source_cube.shape) dst_shape[src_dims_xy] = [coord.shape[0] for coord in dst_coords] # NOTE: result array is masked -- fix this afterward if all unmasked fullcube_data = np.ma.zeros(dst_shape) # Iterate 2d slices over all possible indices of the 'other' dimensions all_other_dims = [i_dim for i_dim in range(source_cube.ndim) if i_dim not in src_dims_xy] all_combinations_of_other_inds = np.ndindex(*dst_shape[all_other_dims]) for other_indices in all_combinations_of_other_inds: # Construct a tuple of slices to address the 2d xy field slice_indices_array = np.array([slice(None)] * source_cube.ndim) slice_indices_array[all_other_dims] = other_indices slice_indices_tuple = tuple(slice_indices_array) # Get the source data, reformed into the right dimension order, (x,y). src_data_2d = source_cube.data[slice_indices_tuple] if (src_dims_xy[0] > src_dims_xy[1]): src_data_2d = src_data_2d.transpose() # Work out whether we have missing data to define a source grid mask. if np.ma.is_masked(src_data_2d): srcdata_mask = np.ma.getmask(src_data_2d) else: srcdata_mask = None # Construct ESMF Field objects on source and destination grids. src_field = _make_esmpy_field(src_coords[0], src_coords[1], data=src_data_2d, mask=srcdata_mask) dst_field = _make_esmpy_field(dst_coords[0], dst_coords[1]) # Make Field for destination coverage fraction (for missing data calc). coverage_field = ESMF.Field(dst_field.grid, 'validmask_dst') # Do the actual regrid with ESMF. mask_flag_values = np.array([1], dtype=np.int32) regrid_method = ESMF.Regrid(src_field, dst_field, src_mask_values=mask_flag_values, regrid_method=ESMF.RegridMethod.CONSERVE, unmapped_action=ESMF.UnmappedAction.IGNORE, dst_frac_field=coverage_field) regrid_method(src_field, dst_field) data = np.ma.masked_array(dst_field.data) # Convert destination 'coverage fraction' into a missing-data mask. # Set = wherever part of cell goes outside source grid, or overlaps a # masked source cell. coverage_tolerance_threshold = 1.0 - 1.0e-8 data.mask = coverage_field.data < coverage_tolerance_threshold # Transpose ESMF result dims (X,Y) back to the order of the source if (src_dims_xy[0] > src_dims_xy[1]): data = data.transpose() # Paste regridded slice back into parent array fullcube_data[slice_indices_tuple] = data # Remove the data mask if completely unused. if not np.ma.is_masked(fullcube_data): fullcube_data = np.array(fullcube_data) # Generate a full 2d sample grid, as required for regridding orography # NOTE: as seen in "regrid_bilinear_rectilinear_src_and_grid" # TODO: can this not also be wound into the _create_cube method ? src_cs = src_coords[0].coord_system sample_grid_x, sample_grid_y = RectilinearRegridder._sample_grid( src_cs, dst_coords[0], dst_coords[1]) # Return result as a new cube based on the source. # TODO: please tidy this interface !!! return RectilinearRegridder._create_cube( fullcube_data, src=source_cube, x_dim=src_dims_xy[0], y_dim=src_dims_xy[1], src_x_coord=src_coords[0], src_y_coord=src_coords[1], grid_x_coord=dst_coords[0], grid_y_coord=dst_coords[1], sample_grid_x=sample_grid_x, sample_grid_y=sample_grid_y, regrid_callback=RectilinearRegridder._regrid)
def regrid_conservative_via_esmpy(source_cube, grid_cube): """ Perform a conservative regridding with ESMPy. Regrids the data of a source cube onto a new grid defined by a destination cube. Args: * source_cube (:class:`iris.cube.Cube`): Source data. Must have two identifiable horizontal dimension coordinates. * grid_cube (:class:`iris.cube.Cube`): Define the target horizontal grid: Only the horizontal dimension coordinates are actually used. Returns: A new cube derived from source_cube, regridded onto the specified horizontal grid. Any additional coordinates which map onto the horizontal dimensions are removed, while all other metadata is retained. If there are coordinate factories with 2d horizontal reference surfaces, the reference surfaces are also regridded, using ordinary bilinear interpolation. .. note:: Both source and destination cubes must have two dimension coordinates identified with axes 'X' and 'Y' which share a coord_system with a Cartopy CRS. The grids are defined by :meth:`iris.coords.Coord.contiguous_bounds` of these. .. note:: Initialises the ESMF Manager, if it was not already called. This implements default Manager operations (e.g. logging). To alter this, make a prior call to ESMF.Manager(). """ # Lazy import so we can build the docs with no ESMF. import ESMF # Get source + target XY coordinate pairs and check they are suitable. src_coords = get_xy_dim_coords(source_cube) dst_coords = get_xy_dim_coords(grid_cube) src_cs = src_coords[0].coord_system grid_cs = dst_coords[0].coord_system if src_cs is None or grid_cs is None: raise ValueError("Both 'src' and 'grid' Cubes must have a" " coordinate system for their rectilinear grid" " coordinates.") if src_cs.as_cartopy_crs() is None or grid_cs.as_cartopy_crs() is None: raise ValueError("Both 'src' and 'grid' Cubes coord_systems must have " "a valid associated Cartopy CRS.") def _valid_units(coord): if isinstance(coord.coord_system, (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS)): valid_units = 'degrees' else: valid_units = 'm' return coord.units == valid_units if not all(_valid_units(coord) for coord in src_coords + dst_coords): raise ValueError("Unsupported units: must be 'degrees' or 'm'.") # Initialise the ESMF manager in case it was not already done. ESMF.Manager() # Create a data array for the output cube. src_dims_xy = [source_cube.coord_dims(coord)[0] for coord in src_coords] # Size matches source, except for X+Y dimensions dst_shape = np.array(source_cube.shape) dst_shape[src_dims_xy] = [coord.shape[0] for coord in dst_coords] # NOTE: result array is masked -- fix this afterward if all unmasked fullcube_data = np.ma.zeros(dst_shape) # Iterate 2d slices over all possible indices of the 'other' dimensions all_other_dims = [i_dim for i_dim in range(source_cube.ndim) if i_dim not in src_dims_xy] all_combinations_of_other_inds = np.ndindex(*dst_shape[all_other_dims]) for other_indices in all_combinations_of_other_inds: # Construct a tuple of slices to address the 2d xy field slice_indices_array = np.array([slice(None)] * source_cube.ndim) slice_indices_array[all_other_dims] = other_indices slice_indices_tuple = tuple(slice_indices_array) # Get the source data, reformed into the right dimension order, (x,y). src_data_2d = source_cube.data[slice_indices_tuple] if (src_dims_xy[0] > src_dims_xy[1]): src_data_2d = src_data_2d.transpose() # Work out whether we have missing data to define a source grid mask. if np.ma.is_masked(src_data_2d): srcdata_mask = np.ma.getmask(src_data_2d) else: srcdata_mask = None # Construct ESMF Field objects on source and destination grids. src_field = _make_esmpy_field(src_coords[0], src_coords[1], data=src_data_2d, mask=srcdata_mask) dst_field = _make_esmpy_field(dst_coords[0], dst_coords[1]) # Make Field for destination coverage fraction (for missing data calc). coverage_field = ESMF.Field(dst_field.grid, 'validmask_dst') # Do the actual regrid with ESMF. mask_flag_values = np.array([1], dtype=np.int32) regrid_method = ESMF.Regrid(src_field, dst_field, src_mask_values=mask_flag_values, regrid_method=ESMF.RegridMethod.CONSERVE, unmapped_action=ESMF.UnmappedAction.IGNORE, dst_frac_field=coverage_field) regrid_method(src_field, dst_field) data = dst_field.data # Convert destination 'coverage fraction' into a missing-data mask. # Set = wherever part of cell goes outside source grid, or overlaps a # masked source cell. coverage_tolerance_threshold = 1.0 - 1.0e-8 data.mask = coverage_field.data < coverage_tolerance_threshold # Transpose ESMF result dims (X,Y) back to the order of the source if (src_dims_xy[0] > src_dims_xy[1]): data = data.transpose() # Paste regridded slice back into parent array fullcube_data[slice_indices_tuple] = data # Remove the data mask if completely unused. if not np.ma.is_masked(fullcube_data): fullcube_data = np.array(fullcube_data) # Generate a full 2d sample grid, as required for regridding orography # NOTE: as seen in "regrid_bilinear_rectilinear_src_and_grid" # TODO: can this not also be wound into the _create_cube method ? src_cs = src_coords[0].coord_system sample_grid_x, sample_grid_y = RectilinearRegridder._sample_grid( src_cs, dst_coords[0], dst_coords[1]) # Return result as a new cube based on the source. # TODO: please tidy this interface !!! return RectilinearRegridder._create_cube( fullcube_data, src=source_cube, x_dim=src_dims_xy[0], y_dim=src_dims_xy[1], src_x_coord=src_coords[0], src_y_coord=src_coords[1], grid_x_coord=dst_coords[0], grid_y_coord=dst_coords[1], sample_grid_x=sample_grid_x, sample_grid_y=sample_grid_y, regrid_callback=RectilinearRegridder._regrid)
def __call__(self, src_cube): """ Regrid the provided :class:`~iris.cube.Cube` on to the target grid of this :class:`AreaWeightedRegridder`. The supplied :class:`~iris.cube.Cube` must be defined with the same grid as the source grid used to create this :class:`AreaWeightedRegridder`. Args: * src_cube: A :class:`~iris.cube.Cube` to be regridded. Returns: A :class:`~iris.cube.Cube` defined with the horizontal dimensions of the target and the other dimensions from the supplied source :class:`~iris.cube.Cube`. The data values of the supplied source :class:`~iris.cube.Cube` will be converted to values on the new grid using conservative area-weighted regridding. """ # Sanity check the supplied source cube. if not isinstance(src_cube, iris.cube.Cube): raise TypeError('The source must be a cube.') # Get the source cube x and y coordinates. sx, sy = get_xy_dim_coords(src_cube) if (sx, sy) != self._src_grid: emsg = 'The source cube is not defined on the same source grid ' \ 'as this regridder.' raise ValueError(emsg) if sx.coord_system is None: msg = 'The source cube requires a native coordinate system.' raise ValueError(msg) # Convert the contiguous bounds of the grid to the source crs. gxx, gyy = np.meshgrid(self._gx.contiguous_bounds(), self._gy.contiguous_bounds()) # Now calculate and cache the grid bounds in the source crs. if self._gx_bounds is None or self._gy_bounds is None: if sx.coord_system == self._gx.coord_system: self._gx_bounds, self._gy_bounds = gxx, gyy else: from_crs = self._gx.coord_system.as_cartopy_crs() to_crs = sx.coord_system.as_cartopy_crs() xyz = to_crs.transform_points(from_crs, gxx, gyy) self._gx_bounds, self._gy_bounds = xyz[..., 0], xyz[..., 1] # Calculate and cache the source contiguous bounds. if self._sx_bounds is None or self._sy_bounds is None: self._sx_bounds = sx.contiguous_bounds() self._sy_bounds = sy.contiguous_bounds() sx_dim = src_cube.coord_dims(sx)[0] sy_dim = src_cube.coord_dims(sy)[0] # Perform the regrid. result = agg(src_cube.data, sx.points, self._sx_bounds, sy.points, self._sy_bounds, sx_dim, sy_dim, self._gx_bounds, self._gy_bounds) # # XXX: Need to deal the factories when constructing result cube. # result_cube = iris.cube.Cube(result) result_cube.metadata = copy.deepcopy(src_cube.metadata) coord_mapping = {} def copy_coords(coords, add_coord): for coord in coords: dims = src_cube.coord_dims(coord) if coord is sx: coord = self._gx elif coord is sy: coord = self._gy elif sx_dim in dims or sy_dim in dims: continue result_coord = coord.copy() add_coord(result_coord, dims) coord_mapping[id(coord)] = result_coord copy_coords(src_cube.dim_coords, result_cube.add_dim_coord) copy_coords(src_cube.aux_coords, result_cube.add_aux_coord) return result_cube
def _regrid_weighted_curvilinear_to_rectilinear__prepare( src_cube, weights, grid_cube): """ First (setup) part of 'regrid_weighted_curvilinear_to_rectilinear'. Check inputs and calculate the sparse regrid matrix and related info. The 'regrid info' returned can be re-used over many 2d slices. """ if src_cube.aux_factories: msg = "All source cube derived coordinates will be ignored." warnings.warn(msg) # Get the source cube x and y 2D auxiliary coordinates. sx, sy = src_cube.coord(axis="x"), src_cube.coord(axis="y") # Get the target grid cube x and y dimension coordinates. tx, ty = get_xy_dim_coords(grid_cube) if sx.units != sy.units: msg = ("The source cube x ({!r}) and y ({!r}) coordinates must " "have the same units.") raise ValueError(msg.format(sx.name(), sy.name())) if src_cube.coord_dims(sx) != src_cube.coord_dims(sy): msg = ("The source cube x ({!r}) and y ({!r}) coordinates must " "map onto the same cube dimensions.") raise ValueError(msg.format(sx.name(), sy.name())) if sx.coord_system != sy.coord_system: msg = ("The source cube x ({!r}) and y ({!r}) coordinates must " "have the same coordinate system.") raise ValueError(msg.format(sx.name(), sy.name())) if sx.coord_system is None: msg = ("The source X and Y coordinates must have a defined " "coordinate system.") raise ValueError(msg) if tx.units != ty.units: msg = ("The target grid cube x ({!r}) and y ({!r}) coordinates must " "have the same units.") raise ValueError(msg.format(tx.name(), ty.name())) if tx.coord_system is None: msg = ("The target X and Y coordinates must have a defined " "coordinate system.") raise ValueError(msg) if tx.coord_system != ty.coord_system: msg = ("The target grid cube x ({!r}) and y ({!r}) coordinates must " "have the same coordinate system.") raise ValueError(msg.format(tx.name(), ty.name())) if weights is None: weights = np.ones(sx.shape) if weights.shape != sx.shape: msg = ("Provided weights must have the same shape as the X and Y " "coordinates.") raise ValueError(msg) if not tx.has_bounds() or not tx.is_contiguous(): msg = ("The target grid cube x ({!r})coordinate requires " "contiguous bounds.") raise ValueError(msg.format(tx.name())) if not ty.has_bounds() or not ty.is_contiguous(): msg = ("The target grid cube y ({!r}) coordinate requires " "contiguous bounds.") raise ValueError(msg.format(ty.name())) def _src_align_and_flatten(coord): # Return a flattened, unmasked copy of a coordinate's points array that # will align with a flattened version of the source cube's data. # # PP-TODO: Should work with any cube dimensions for X and Y coords. # Probably needs fixing anyway? # points = coord.points if src_cube.coord_dims(coord) == (1, 0): points = points.T if points.shape != src_cube.shape: msg = ("The shape of the points array of {!r} is not compatible " "with the shape of {!r}.") raise ValueError(msg.format(coord.name(), src_cube.name())) return np.asarray(points.flatten()) # Align and flatten the coordinate points of the source space. sx_points = _src_align_and_flatten(sx) sy_points = _src_align_and_flatten(sy) # Transform source X and Y points into the target coord-system, if needed. if sx.coord_system != tx.coord_system: src_crs = sx.coord_system.as_cartopy_projection() tgt_crs = tx.coord_system.as_cartopy_projection() sx_points, sy_points = _transform_xy_arrays(src_crs, sx_points, sy_points, tgt_crs) # # TODO: how does this work with scaled units ?? # e.g. if crs is latlon, units could be degrees OR radians ? # # Wrap modular values (e.g. longitudes) if required. modulus = sx.units.modulus if modulus is not None: # Match the source cube x coordinate range to the target grid # cube x coordinate range. min_sx, min_tx = np.min(sx.points), np.min(tx.points) if min_sx < 0 and min_tx >= 0: indices = np.where(sx_points < 0) # Ensure += doesn't raise a TypeError if not np.can_cast(modulus, sx_points.dtype): sx_points = sx_points.astype(type(modulus), casting="safe") sx_points[indices] += modulus elif min_sx >= 0 and min_tx < 0: indices = np.where(sx_points > (modulus / 2)) # Ensure -= doesn't raise a TypeError if not np.can_cast(modulus, sx_points.dtype): sx_points = sx_points.astype(type(modulus), casting="safe") sx_points[indices] -= modulus # Create target grid cube x and y cell boundaries. tx_depth, ty_depth = tx.points.size, ty.points.size (tx_dim, ) = grid_cube.coord_dims(tx) (ty_dim, ) = grid_cube.coord_dims(ty) tx_cells = np.concatenate((tx.bounds[:, 0], tx.bounds[-1, 1].reshape(1))) ty_cells = np.concatenate((ty.bounds[:, 0], ty.bounds[-1, 1].reshape(1))) # Determine the target grid cube x and y cells that bound # the source cube x and y points. def _regrid_indices(cells, depth, points): # Calculate the minimum difference in cell extent. extent = np.min(np.diff(cells)) if extent == 0: # Detected an dimension coordinate with an invalid # zero length cell extent. msg = ("The target grid cube {} ({!r}) coordinate contains " "a zero length cell extent.") axis, name = "x", tx.name() if points is sy_points: axis, name = "y", ty.name() raise ValueError(msg.format(axis, name)) elif extent > 0: # The cells of the dimension coordinate are in ascending order. indices = np.searchsorted(cells, points, side="right") - 1 else: # The cells of the dimension coordinate are in descending order. # np.searchsorted() requires ascending order, so we require to # account for this restriction. cells = cells[::-1] right = np.searchsorted(cells, points, side="right") left = np.searchsorted(cells, points, side="left") indices = depth - right # Only those points that exactly match the left-hand cell bound # will differ between 'left' and 'right'. Thus their appropriate # target cell location requires to be recalculated to give the # correct descending [upper, lower) interval cell, source to target # regrid behaviour. delta = np.where(left != right)[0] if delta.size: indices[delta] = depth - left[delta] return indices x_indices = _regrid_indices(tx_cells, tx_depth, sx_points) y_indices = _regrid_indices(ty_cells, ty_depth, sy_points) # Now construct a sparse M x N matix, where M is the flattened target # space, and N is the flattened source space. The sparse matrix will then # be populated with those source cube points that contribute to a specific # target cube cell. # Determine the valid indices and their offsets in M x N space. # Calculate the valid M offsets. cols = np.where((y_indices >= 0) & (y_indices < ty_depth) & (x_indices >= 0) & (x_indices < tx_depth))[0] # Reduce the indices to only those that are valid. x_indices = x_indices[cols] y_indices = y_indices[cols] # Calculate the valid N offsets. if ty_dim < tx_dim: rows = y_indices * tx.points.size + x_indices else: rows = x_indices * ty.points.size + y_indices # Calculate the associated valid weights. weights_flat = weights.flatten() data = weights_flat[cols] # Build our sparse M x N matrix of weights. sparse_matrix = csc_matrix((data, (rows, cols)), shape=(grid_cube.data.size, src_cube.data.size)) # Performing a sparse sum to collapse the matrix to (M, 1). sum_weights = sparse_matrix.sum(axis=1).getA() # Determine the rows (flattened target indices) that have a # contribution from one or more source points. rows = np.nonzero(sum_weights) # NOTE: when source points are masked, this 'sum_weights' is possibly # incorrect and needs re-calculating. Likewise 'rows' may cover target # cells which happen to get no data. This is dealt with by adjusting as # required in the '__perform' function, below. regrid_info = (sparse_matrix, sum_weights, rows, grid_cube) return regrid_info
def __call__(self, src): """ Regrid this :class:`~iris.cube.Cube` on to the target grid of this :class:`RectilinearRegridder`. The given cube must be defined with the same grid as the source grid used to create this :class:`RectilinearRegridder`. Args: * src: A :class:`~iris.cube.Cube` to be regridded. Returns: A cube defined with the horizontal dimensions of the target and the other dimensions from this cube. The data values of this cube will be converted to values on the new grid using either nearest-neighbour or linear interpolation. """ # Validity checks. if not isinstance(src, iris.cube.Cube): raise TypeError("'src' must be a Cube") if get_xy_dim_coords(src) != self._src_grid: raise ValueError('The given cube is not defined on the same ' 'source grid as this regridder.') src_x_coord, src_y_coord = get_xy_dim_coords(src) grid_x_coord, grid_y_coord = self._tgt_grid src_cs = src_x_coord.coord_system grid_cs = grid_x_coord.coord_system if src_cs is None and grid_cs is None: if not (src_x_coord.is_compatible(grid_x_coord) and src_y_coord.is_compatible(grid_y_coord)): raise ValueError("The rectilinear grid coordinates of the " "given cube and target grid have no " "coordinate system but they do not have " "matching coordinate metadata.") elif src_cs is None or grid_cs is None: raise ValueError("The rectilinear grid coordinates of the given " "cube and target grid must either both have " "coordinate systems or both have no coordinate " "system but with matching coordinate metadata.") # Check the source grid units. for coord in (src_x_coord, src_y_coord): self._check_units(coord) # Convert the grid to a 2D sample grid in the src CRS. sample_grid = self._sample_grid(src_cs, grid_x_coord, grid_y_coord) sample_grid_x, sample_grid_y = sample_grid # Compute the interpolated data values. x_dim = src.coord_dims(src_x_coord)[0] y_dim = src.coord_dims(src_y_coord)[0] data = self._regrid(src.data, x_dim, y_dim, src_x_coord, src_y_coord, sample_grid_x, sample_grid_y, self._method, self._extrapolation_mode) # Wrap up the data as a Cube. regrid_callback = functools.partial(self._regrid, method=self._method, extrapolation_mode='nan') result = self._create_cube(data, src, x_dim, y_dim, src_x_coord, src_y_coord, grid_x_coord, grid_y_coord, sample_grid_x, sample_grid_y, regrid_callback) return result
def spatial_chunked_regrid( src_cube, tgt_cube, scheme, min_src_chunk_size=2, max_src_chunk_size=dask.config.get("array.chunk-size"), min_tgt_chunk_size=None, max_tgt_chunk_size=dask.config.get("array.chunk-size"), tol=1e-16, ): """Spatially chunked regridding using dask. Only the y-coordinate is chunked. This is done because the x-coordinate may be circular (global), which may require additional logic to be implemented. Args: src_cube (iris.cube.Cube): Cube to be regridded onto the coordinate system defined by the target cube. tgt_cube (iris.cube.Cube): Target cube. This is solely required to specify the target coordinate system and may contain dummy data. scheme: The type of regridding to use to regrid the source cube onto the target grid, e.g. `iris.analysis.Linear`, `iris.analysis.Nearest`, and `iris.analysis.AreaWeighted`. min_src_chunk_size (None, int): Minimum source cube chunk size along the y-dimension, specified in terms of the number of elements per chunk along this axis. Note that some regridders, e.g. `iris.analysis.Linear()` require at least a chunk size of 2 here. max_src_chunk_size (None, int, str): The maximum size of chunks along the source cube's y-dimension. Can be given in bytes, e.g. '10MB' or '100KB'. If None is given, the chunks will be as large as possible. min_tgt_chunk_size (None, int): Analogous to `min_src_chunk_size` for the target cube. max_tgt_chunk_size (None, int, str): Analogous to `max_src_chunk_size` for the target cube. Raises: TypeError: If `src_cube` does not have lazy data. ValueError: If the source cube is not 2D. ValueError: If the source or target cube do not define x and y coordinates. ValueError: If source and target cubes do not define their x and y coordinates along the same dimensions. ValueError: If any of the x, y coordinates are not monotonic. ValueError: If the given maximum chunk sizes are smaller than required for the regridding of a single data chunk. """ if not src_cube.has_lazy_data(): raise TypeError("Source cube needs to have lazy data.") if src_cube.core_data().ndim != 2: raise ValueError("Source cube data needs to be 2D.") coord_err = "{name} cube needs to define x and y coordinates." try: src_x_coord, src_y_coord = get_xy_dim_coords(src_cube) except Exception as exc: raise ValueError(coord_err.format("Source")) from exc try: tgt_x_coord, tgt_y_coord = get_xy_dim_coords(tgt_cube) except Exception as exc: raise ValueError(coord_err.format("Target")) from exc y_dim = src_y_dim = src_cube.coord_dims(src_y_coord)[0] x_dim = src_x_dim = src_cube.coord_dims(src_x_coord)[0] tgt_y_dim = tgt_cube.coord_dims(tgt_y_coord)[0] tgt_x_dim = tgt_cube.coord_dims(tgt_x_coord)[0] if (src_y_dim, src_x_dim) != (tgt_y_dim, tgt_x_dim): raise ValueError("Coordinates are not aligned.") monotonic_err_msg = "{:}-coordinate needs to be monotonic." src_x_coord_monotonic, src_x_coord_direction = iris.util.monotonic( src_x_coord.points, return_direction=True) if not src_x_coord_monotonic: raise ValueError(monotonic_err_msg.format("Source x")) if src_x_coord_direction < 0: # Coordinate is monotonically decreasing, so we need to invert it. flip_slice = [slice(None)] * src_cube.ndim flip_slice[src_x_dim] = slice(None, None, -1) src_cube = src_cube[tuple(flip_slice)] src_x_coord, src_y_coord = get_xy_dim_coords(src_cube) src_x_coord.bounds = src_x_coord.bounds[:, ::-1] src_y_coord_monotonic, src_y_coord_direction = iris.util.monotonic( src_y_coord.points, return_direction=True) if not src_y_coord_monotonic: raise ValueError(monotonic_err_msg.format("Source y")) if src_y_coord_direction < 0: # Coordinate is monotonically decreasing, so we need to invert it. flip_slice = [slice(None)] * src_cube.ndim flip_slice[src_y_dim] = slice(None, None, -1) src_cube = src_cube[tuple(flip_slice)] src_x_coord, src_y_coord = get_xy_dim_coords(src_cube) src_y_coord.bounds = src_y_coord.bounds[:, ::-1] tgt_x_coord_monotonic, tgt_x_coord_direction = iris.util.monotonic( tgt_x_coord.points, return_direction=True) if not tgt_x_coord_monotonic: raise ValueError(monotonic_err_msg.format("Target x")) if tgt_x_coord_direction < 0: # Coordinate is monotonically decreasing, so we need to invert it. flip_slice = [slice(None)] * tgt_cube.ndim flip_slice[tgt_x_dim] = slice(None, None, -1) tgt_cube = tgt_cube[tuple(flip_slice)] tgt_x_coord, tgt_y_coord = get_xy_dim_coords(tgt_cube) tgt_x_coord.bounds = tgt_x_coord.bounds[:, ::-1] tgt_y_coord_monotonic, tgt_y_coord_direction = iris.util.monotonic( tgt_y_coord.points, return_direction=True) if not tgt_y_coord_monotonic: raise ValueError(monotonic_err_msg.format("Target y")) if tgt_y_coord_direction < 0: # Coordinate is monotonically decreasing, so we need to invert it. flip_slice = [slice(None)] * tgt_cube.ndim flip_slice[tgt_y_dim] = slice(None, None, -1) tgt_cube = tgt_cube[tuple(flip_slice)] tgt_x_coord, tgt_y_coord = get_xy_dim_coords(tgt_cube) tgt_y_coord.bounds = tgt_y_coord.bounds[:, ::-1] max_src_chunk_size = convert_chunk_size( max_src_chunk_size, # The number of elements along the non-chunked dimension. factor=src_cube.shape[x_dim], dtype=src_cube.dtype, masked=isinstance(src_cube.core_data()._meta, np.ma.MaskedArray), ) max_tgt_chunk_size = convert_chunk_size( max_tgt_chunk_size, # The number of elements along the non-chunked dimension. factor=tgt_cube.shape[x_dim], # NOTE: Is this true? dtype=src_cube.dtype, # Set masked to True here since we will add a mask later in all cases. masked=True, ) max_chunk_msg = ( "Maximum {:} chunk size was smaller than the minimum required for a single " "chunk.") if max_src_chunk_size == 0: raise ValueError(max_chunk_msg.format("source")) if max_tgt_chunk_size == 0: raise ValueError(max_chunk_msg.format("target")) # Calculate all possible chunks along the y dimension. overlap_indices, valid_src_y_slice, valid_tgt_y_slice = get_overlapping( src_y_coord.contiguous_bounds(), tgt_y_coord.contiguous_bounds(), tol=tol, ) # Some regridding methods care about cells with points outside of overlapping # bounds, like `iris.analysis.Linear()`. Include an additional source cell on # either end if possible to account for this. # NOTE: These additions may be superfluous, but would require re-writing # `get_overlapping()` to determine. if valid_src_y_slice.start > 0: overlap_indices[valid_tgt_y_slice][0].insert( 0, overlap_indices[valid_tgt_y_slice][0][0] - 1) if valid_src_y_slice.stop < src_y_coord.shape[0]: overlap_indices[valid_tgt_y_slice][-1].append( overlap_indices[valid_tgt_y_slice][-1][-1] + 1) valid_src_y_slice = slice( max(0, valid_src_y_slice.start - 1), min(src_y_coord.shape[0], valid_src_y_slice.stop + 1), ) cell_numbers, overlap_y = get_cell_numbers(overlap_indices) cell_mapping = get_valid_cell_mapping(cell_numbers[valid_tgt_y_slice]) tgt_y_slices = [] src_y_chunks = [] tgt_y_chunks = [] for tgt_cells, src_cells in cell_mapping.items(): tgt_y_slices.append(slice(tgt_cells[0], tgt_cells[-1] + 1)) src_y_chunks.append(len(src_cells)) tgt_y_chunks.append(len(tgt_cells)) # XXX: This override is sometimes needed due to floating point errors, e.g. for # test_regrid case # 100_200-50_120--90_90--90_90--180_180--180_180-AreaWeighted_1-Block-20KB # where tgt_cube.coord('latitude')[24].bounds[0][1] is 3.55e-15 instead of 0, # causing src_cube.coord('latitude')[50] (with bounds [0, 1.8] to be required to # match the masking behaviour in Iris regrid even though this is not expected to # influence the final result to the small overlap. Another solution is to decrease # the `tol` parameter of `get_overlapping()`, in this case below 3.55e-16 # (e.g. 1e-16). # overlap_y = True src_y_chunks, tgt_y_slices = calculate_blocks( src_y_chunks, tgt_y_chunks, tgt_y_slices, min_src_chunk_size, max_src_chunk_size, min_tgt_chunk_size, max_tgt_chunk_size, ) valid_src_slice = [slice(None)] * src_cube.ndim valid_src_slice[src_y_dim] = valid_src_y_slice valid_src_slice = tuple(valid_src_slice) # Re-chunk the data and coordinate along the y-dimension. block_src_data = src_cube.core_data()[valid_src_slice].rechunk(( src_y_chunks, -1, )) # 2D arrays are created here to enable consistent map_overlap behaviour. block_src_y_pnts = (da.from_array( src_y_coord.points[valid_src_y_slice]).rechunk( (src_y_chunks, )).reshape(-1, 1)) block_src_low_y_bnds = (da.from_array( src_y_coord.bounds[valid_src_y_slice, 0]).rechunk( (src_y_chunks, )).reshape(-1, 1)) block_src_upp_y_bnds = (da.from_array( src_y_coord.bounds[valid_src_y_slice, 1]).rechunk( (src_y_chunks, )).reshape(-1, 1)) chunks_spec = [None] * 2 chunks_spec[x_dim] = tgt_x_coord.shape[0] chunks_spec[y_dim] = tuple(slice_len(s) for s in tgt_y_slices) chunks_spec = tuple(chunks_spec) output = da.map_overlap( regrid_chunk, block_src_data, block_src_y_pnts, block_src_low_y_bnds, block_src_upp_y_bnds, # Store metadata for the y-coordinate for which points and bounds will be # filled in during the course of blocked regridding. src_y_coord_metadata=src_y_coord.metadata, src_x_coord=src_x_coord, src_cube_metadata=src_cube.metadata, tgt_y_coord=tgt_y_coord[valid_tgt_y_slice], tgt_x_coord=tgt_x_coord, tgt_y_slices=tgt_y_slices, tgt_cube_metadata=tgt_cube.metadata, y_dim=y_dim, x_dim=x_dim, scheme=scheme, depth={ # The y-coordinate may need to be overlapped. y_dim: 1 if overlap_y else 0, # The x-coordinate will not be overlapped as it is never chunked. x_dim: 0, }, boundary="none", trim=False, dtype=np.float64, chunks=chunks_spec, meta=np.array([], dtype=np.float64), ) if not isinstance(output._meta, np.ma.MaskedArray): # XXX: Ideally this should not be needed, but the mask appears to vanish in # some cases. output = da.ma.masked_array(output, mask=False) if slice_len(valid_tgt_y_slice) < tgt_y_coord.shape[0]: # Embed the output data calculated above into the final target cube by padding # with masked data as necessary. seq = [] if valid_tgt_y_slice.start > 0: # Pad at the start. start_pad_shape = [tgt_x_coord.shape[0]] * 2 start_pad_shape[y_dim] = valid_tgt_y_slice.start seq.append( da.ma.masked_array( da.zeros(start_pad_shape), mask=True, )) seq.append(output) if valid_tgt_y_slice.stop < tgt_y_coord.shape[0]: # Pad at the end. end_pad_shape = [tgt_x_coord.shape[0]] * 2 end_pad_shape[ y_dim] = tgt_y_coord.shape[0] - valid_tgt_y_slice.stop seq.append(da.ma.masked_array( da.zeros(end_pad_shape), mask=True, )) output = da.concatenate(seq, axis=y_dim) return tgt_cube.copy(data=output)