diff --git a/benchmarks/benchmarks/regridding.py b/benchmarks/benchmarks/regridding.py index c315119c11..44bd1b6c95 100644 --- a/benchmarks/benchmarks/regridding.py +++ b/benchmarks/benchmarks/regridding.py @@ -12,8 +12,11 @@ # importing anything else from iris import tests # isort:skip +import numpy as np + import iris -from iris.analysis import AreaWeighted +from iris.analysis import AreaWeighted, PointInCell +from iris.coords import AuxCoord class HorizontalChunkedRegridding: @@ -53,3 +56,48 @@ def time_regrid_area_w_new_grid(self) -> None: out = self.chunked_cube.regrid(self.template_cube, self.scheme_area_w) # Realise data out.data + + +class CurvilinearRegridding: + def setup(self) -> None: + # Prepare a cube and a template + + cube_file_path = tests.get_data_path( + ["NetCDF", "regrid", "regrid_xyt.nc"] + ) + self.cube = iris.load_cube(cube_file_path) + + # Make the source cube curvilinear + x_coord = self.cube.coord("longitude") + y_coord = self.cube.coord("latitude") + xx, yy = np.meshgrid(x_coord.points, y_coord.points) + self.cube.remove_coord(x_coord) + self.cube.remove_coord(y_coord) + x_coord_2d = AuxCoord( + xx, + standard_name=x_coord.standard_name, + units=x_coord.units, + coord_system=x_coord.coord_system, + ) + y_coord_2d = AuxCoord( + yy, + standard_name=y_coord.standard_name, + units=y_coord.units, + coord_system=y_coord.coord_system, + ) + self.cube.add_aux_coord(x_coord_2d, (1, 2)) + self.cube.add_aux_coord(y_coord_2d, (1, 2)) + + template_file_path = tests.get_data_path( + ["NetCDF", "regrid", "regrid_template_global_latlon.nc"] + ) + self.template_cube = iris.load_cube(template_file_path) + + # Prepare a regridding scheme + self.scheme_pic = PointInCell() + + def time_regrid_pic(self) -> None: + # Regrid the cube onto the template. + out = self.cube.regrid(self.template_cube, self.scheme_pic) + # Realise the data + out.data diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index d6ff5d1418..9ee6826f67 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -62,6 +62,9 @@ This document explains the changes made to Iris for this release both dim and aux coords of the same type e.g. ``longitude`` and ``grid_longitude``. (:issue:`3916`, :pull:`5029`). +#. `@stephenworsley`_ added the ability to regrid derived coordinates with the + :obj:`~iris.analysis.PointInCell` regridding scheme. (:pull:`4807`) + 🐛 Bugs Fixed ============= @@ -135,6 +138,10 @@ This document explains the changes made to Iris for this release :meth:`iris.coords.Coord.intersect`. (:pull:`4955`) +#. `@stephenworsley`_ improved the speed of the :obj:`~iris.analysis.PointInCell` + regridding scheme. (:pull:`4807`) + + 🔥 Deprecations =============== diff --git a/lib/iris/analysis/_area_weighted.py b/lib/iris/analysis/_area_weighted.py index 8381185e58..edbfd41ef9 100644 --- a/lib/iris/analysis/_area_weighted.py +++ b/lib/iris/analysis/_area_weighted.py @@ -11,7 +11,7 @@ from iris._lazy_data import map_complete_blocks from iris.analysis._interpolation import get_xy_dim_coords, snapshot_grid -from iris.analysis._regrid import RectilinearRegridder +from iris.analysis._regrid import RectilinearRegridder, _create_cube import iris.analysis.cartography import iris.coord_systems from iris.util import _meshgrid @@ -1111,18 +1111,32 @@ def _regrid_area_weighted_rectilinear_src_and_grid__perform( ) # Wrap up the data as a Cube. - regrid_callback = RectilinearRegridder._regrid - new_cube = RectilinearRegridder._create_cube( + + _regrid_callback = functools.partial( + RectilinearRegridder._regrid, + src_x_coord=src_x, + src_y_coord=src_y, + sample_grid_x=meshgrid_x, + sample_grid_y=meshgrid_y, + ) + # TODO: investigate if an area weighted callback would be more appropriate. + # _regrid_callback = functools.partial( + # _regrid_area_weighted_array, + # weights_info=weights_info, + # index_info=index_info, + # mdtol=mdtol, + # ) + + def regrid_callback(*args, **kwargs): + _data, dims = args + return _regrid_callback(_data, *dims, **kwargs) + + new_cube = _create_cube( new_data, src_cube, - src_x_dim, - src_y_dim, - src_x, - src_y, - grid_x, - grid_y, - meshgrid_x, - meshgrid_y, + [src_x_dim, src_y_dim], + [grid_x, grid_y], + 2, regrid_callback, ) diff --git a/lib/iris/analysis/_regrid.py b/lib/iris/analysis/_regrid.py index 5c7439b0ce..f1891a48e4 100644 --- a/lib/iris/analysis/_regrid.py +++ b/lib/iris/analysis/_regrid.py @@ -11,7 +11,6 @@ import numpy as np import numpy.ma as ma from scipy.sparse import csc_matrix -from scipy.sparse import diags as sparse_diags from iris._lazy_data import map_complete_blocks from iris.analysis._interpolation import ( @@ -21,7 +20,7 @@ snapshot_grid, ) from iris.analysis._scipy_interpolate import _RegularGridInterpolator -from iris.util import _meshgrid +from iris.util import _meshgrid, guess_coord_axis def _transform_xy_arrays(crs_from, x, y, crs_to): @@ -52,18 +51,20 @@ def _regrid_weighted_curvilinear_to_rectilinear__prepare( 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. + The 'regrid info' returned can be re-used over many cubes. """ - 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) + sl = [0] * grid_cube.ndim + sl[grid_cube.coord_dims(tx)[0]] = np.s_[:] + sl[grid_cube.coord_dims(ty)[0]] = np.s_[:] + grid_cube = grid_cube[tuple(sl)] + if sx.units != sy.units: msg = ( "The source cube x ({!r}) and y ({!r}) coordinates must " @@ -287,83 +288,108 @@ def _regrid_indices(cells, depth, points): return regrid_info -def _regrid_weighted_curvilinear_to_rectilinear__perform( - src_cube, regrid_info +def _curvilinear_to_rectilinear_regrid_data( + data, + dims, + regrid_info, ): """ - Second (regrid) part of 'regrid_weighted_curvilinear_to_rectilinear'. + Part of 'regrid_weighted_curvilinear_to_rectilinear' which acts on the data. - Perform the prepared regrid calculation on a single 2d cube. + Perform the prepared regrid calculation on an array. """ - from iris.cube import Cube - sparse_matrix, sum_weights, rows, grid_cube = regrid_info + inds = list(range(-len(dims), 0)) + data = np.moveaxis(data, dims, inds) + data_shape = data.shape + grid_size = np.prod([data_shape[ind] for ind in inds]) + # Calculate the numerator of the weighted mean (M, 1). - is_masked = ma.isMaskedArray(src_cube.data) + is_masked = ma.isMaskedArray(data) + sum_weights = None if not is_masked: - data = src_cube.data + data = data else: # Use raw data array - data = src_cube.data.data + r_data = data.data # Check if there are any masked source points to take account of. - is_masked = np.ma.is_masked(src_cube.data) + is_masked = ma.is_masked(data) if is_masked: # Zero any masked source points so they add nothing in output sums. - mask = src_cube.data.mask - data[mask] = 0.0 + mask = data.mask + r_data[mask] = 0.0 # Calculate a new 'sum_weights' to allow for missing source points. # N.B. it is more efficient to use the original once-calculated # sparse matrix, but in this case we can't. # Hopefully, this post-multiplying by the validities is less costly # than repeating the whole sparse calculation. - valid_src_cells = ~mask.flat[:] - src_cell_validity_factors = sparse_diags( - np.array(valid_src_cells, dtype=int), 0 - ) - valid_weights = sparse_matrix * src_cell_validity_factors - sum_weights = valid_weights.sum(axis=1).getA() - # Work out where output cells are missing all contributions. - # This allows for where 'rows' contains output cells that have no - # data because of missing input points. - zero_sums = sum_weights == 0.0 - # Make sure we can still divide by sum_weights[rows]. - sum_weights[zero_sums] = 1.0 + valid_src_cells = ~mask.reshape(-1, grid_size) + sum_weights = valid_src_cells @ sparse_matrix.T + data = r_data + if sum_weights is None: + sum_weights = ( + np.ones(data_shape).reshape(-1, grid_size) @ sparse_matrix.T + ) + # Work out where output cells are missing all contributions. + # This allows for where 'rows' contains output cells that have no + # data because of missing input points. + zero_sums = sum_weights == 0.0 + # Make sure we can still divide by sum_weights[rows]. + sum_weights[zero_sums] = 1.0 # Calculate sum in each target cell, over contributions from each source # cell. - numerator = sparse_matrix * data.reshape(-1, 1) - - # Create a template for the weighted mean result. - weighted_mean = ma.masked_all(numerator.shape, dtype=numerator.dtype) - - # Calculate final results in all relevant places. - weighted_mean[rows] = numerator[rows] / sum_weights[rows] - if is_masked: - # Ensure masked points where relevant source cells were all missing. - if np.any(zero_sums): - # Make masked if it wasn't. - weighted_mean = np.ma.asarray(weighted_mean) - # Mask where contributing sums were zero. - weighted_mean[zero_sums] = np.ma.masked - - # Construct the final regridded weighted mean cube. + numerator = data.reshape(-1, grid_size) @ sparse_matrix.T + + weighted_mean = numerator / sum_weights + # Ensure masked points where relevant source cells were all missing. + weighted_mean = ma.asarray(weighted_mean) + if np.any(zero_sums): + # Mask where contributing sums were zero. + weighted_mean[zero_sums] = ma.masked + + new_data_shape = list(data_shape) + for dim, length in zip(inds, grid_cube.shape): + new_data_shape[dim] = length + if len(dims) == 1: + new_data_shape.append(grid_cube.shape[1]) + dims = (dims[0], dims[0] + 1) + if len(dims) > 2: + new_data_shape = new_data_shape[: 2 - len(dims)] + dims = dims[:2] + + result = weighted_mean.reshape(new_data_shape) + result = np.moveaxis(result, [-2, -1], dims) + return result + + +def _regrid_weighted_curvilinear_to_rectilinear__perform( + src_cube, regrid_info +): + """ + Second (regrid) part of 'regrid_weighted_curvilinear_to_rectilinear'. + + Perform the prepared regrid calculation on a single cube. + + """ + dims = src_cube.coord_dims( + CurvilinearRegridder._get_horizontal_coord(src_cube, "x") + ) + result_data = _curvilinear_to_rectilinear_regrid_data( + src_cube.data, dims, regrid_info + ) + grid_cube = regrid_info[-1] tx = grid_cube.coord(axis="x", dim_coords=True) ty = grid_cube.coord(axis="y", dim_coords=True) - (tx_dim,) = grid_cube.coord_dims(tx) - (ty_dim,) = grid_cube.coord_dims(ty) - dim_coords_and_dims = list(zip((ty.copy(), tx.copy()), (ty_dim, tx_dim))) - cube = Cube( - weighted_mean.reshape(grid_cube.shape), - dim_coords_and_dims=dim_coords_and_dims, + regrid_callback = functools.partial( + _curvilinear_to_rectilinear_regrid_data, regrid_info=regrid_info ) - cube.metadata = copy.deepcopy(src_cube.metadata) - - for coord in src_cube.coords(dimensions=()): - cube.add_aux_coord(coord.copy()) - - return cube + result = _create_cube( + result_data, src_cube, dims, (ty.copy(), tx.copy()), 2, regrid_callback + ) + return result class CurvilinearRegridder: @@ -457,7 +483,7 @@ def __call__(self, src): point-in-cell regridding. """ - from iris.cube import Cube, CubeList + from iris.cube import Cube # Validity checks. if not isinstance(src, Cube): @@ -473,30 +499,18 @@ def __call__(self, src): "The given cube is not defined on the same " "source grid as this regridder." ) - - # Call the regridder function. - # This includes repeating over any non-XY dimensions, because the - # underlying routine does not support this. - # FOR NOW: we will use cube.slices and merge to achieve this, - # though that is not a terribly efficient method ... - # TODO: create a template result cube and paste data slices into it, - # which would be more efficient. - result_slices = CubeList([]) - for slice_cube in src.slices(sx): - if self._regrid_info is None: - # Calculate the basic regrid info just once. - self._regrid_info = ( - _regrid_weighted_curvilinear_to_rectilinear__prepare( - slice_cube, self.weights, self._target_cube - ) - ) - slice_result = ( - _regrid_weighted_curvilinear_to_rectilinear__perform( - slice_cube, self._regrid_info + slice_cube = next(src.slices(sx)) + if self._regrid_info is None: + # Calculate the basic regrid info just once. + self._regrid_info = ( + _regrid_weighted_curvilinear_to_rectilinear__prepare( + slice_cube, self.weights, self._target_cube ) ) - result_slices.append(slice_result) - result = result_slices.merge_cube() + result = _regrid_weighted_curvilinear_to_rectilinear__perform( + src, self._regrid_info + ) + return result @@ -688,11 +702,23 @@ def _regrid( # Prepare the result data array shape = list(src_data.shape) - assert shape[x_dim] == src_x_coord.shape[0] - assert shape[y_dim] == src_y_coord.shape[0] - - shape[y_dim] = sample_grid_x.shape[0] - shape[x_dim] = sample_grid_x.shape[1] + final_shape = shape.copy() + if x_dim is not None: + assert shape[x_dim] == src_x_coord.shape[0] + shape[x_dim] = sample_grid_x.shape[1] + final_shape[x_dim] = shape[x_dim] + else: + shape.append(1) + x_dim = len(shape) - 1 + src_data = np.expand_dims(src_data, -1) + if y_dim is not None: + assert shape[y_dim] == src_y_coord.shape[0] + shape[y_dim] = sample_grid_x.shape[0] + final_shape[y_dim] = shape[y_dim] + else: + shape.append(1) + y_dim = len(shape) - 1 + src_data = np.expand_dims(src_data, -1) dtype = src_data.dtype if method == "linear": @@ -714,7 +740,11 @@ def _regrid( if src_x_coord.points.size > 1 else False ) - reverse_y = src_y_coord.points[0] > src_y_coord.points[1] + reverse_y = ( + src_y_coord.points[0] > src_y_coord.points[1] + if src_y_coord.points.size > 1 + else False + ) flip_index = [slice(None)] * src_data.ndim if reverse_x: src_x_coord = src_x_coord[::-1] @@ -733,7 +763,7 @@ def _regrid( # Slice out the first full 2D piece of data for construction of the # interpolator. - index = [0] * src_data.ndim + index = [0] * len(shape) index[x_dim] = index[y_dim] = slice(None) initial_data = src_data[tuple(index)] if y_dim < x_dim: @@ -808,166 +838,21 @@ def interpolate(data): if ma.isMaskedArray(data) or mode.force_mask: # NB. np.ma.getmaskarray returns an array of `False` if # `src_subset` is not a masked array. - src_mask = np.ma.getmaskarray(src_subset) + src_mask = ma.getmaskarray(src_subset) interpolator.fill_value = mode.mask_fill_value mask_fraction = interpolate(src_mask) new_mask = mask_fraction > 0 - if np.ma.isMaskedArray(data): + if ma.isMaskedArray(data): data.mask[tuple(index)] = new_mask elif np.any(new_mask): # Set mask=False to ensure we have an expanded mask array. - data = np.ma.MaskedArray(data, mask=False) + data = ma.MaskedArray(data, mask=False) data.mask[tuple(index)] = new_mask + data = data.reshape(final_shape) return data - @staticmethod - def _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 a new Cube for the result of regridding the source Cube onto - the new grid. - - All the metadata and coordinates of the result Cube are copied from - the source Cube, with two exceptions: - - Grid dimension coordinates are copied from the grid Cube. - - Auxiliary coordinates which span the grid dimensions are - ignored, except where they provide a reference surface for an - :class:`iris.aux_factory.AuxCoordFactory`. - - Args: - - * data: - The regridded data as an N-dimensional NumPy array. - * src: - The source Cube. - * x_dim: - The X dimension within the source Cube. - * y_dim: - The Y dimension within the source Cube. - * src_x_coord: - The X :class:`iris.coords.DimCoord`. - * src_y_coord: - The Y :class:`iris.coords.DimCoord`. - * grid_x_coord: - The :class:`iris.coords.DimCoord` for the new grid's X - coordinate. - * grid_y_coord: - The :class:`iris.coords.DimCoord` for the new grid's Y - coordinate. - * sample_grid_x: - A 2-dimensional array of sample X values. - * sample_grid_y: - A 2-dimensional array of sample Y values. - * regrid_callback: - The routine that will be used to calculate the interpolated - values of any reference surfaces. - - Returns: - The new, regridded Cube. - - """ - from iris.cube import Cube - - # - # XXX: At the moment requires to be a static method as used by - # experimental regrid_area_weighted_rectilinear_src_and_grid - # - # Create a result cube with the appropriate metadata - result = Cube(data) - result.metadata = copy.deepcopy(src.metadata) - - # Copy across all the coordinates which don't span the grid. - # Record a mapping from old coordinate IDs to new coordinates, - # for subsequent use in creating updated aux_factories. - coord_mapping = {} - - def copy_coords(src_coords, add_method): - for coord in src_coords: - dims = src.coord_dims(coord) - if coord == src_x_coord: - coord = grid_x_coord - elif coord == src_y_coord: - coord = grid_y_coord - elif x_dim in dims or y_dim in dims: - continue - result_coord = coord.copy() - add_method(result_coord, dims) - coord_mapping[id(coord)] = result_coord - - copy_coords(src.dim_coords, result.add_dim_coord) - copy_coords(src.aux_coords, result.add_aux_coord) - - def regrid_reference_surface( - src_surface_coord, - surface_dims, - x_dim, - y_dim, - src_x_coord, - src_y_coord, - sample_grid_x, - sample_grid_y, - regrid_callback, - ): - # Determine which of the reference surface's dimensions span the X - # and Y dimensions of the source cube. - surface_x_dim = surface_dims.index(x_dim) - surface_y_dim = surface_dims.index(y_dim) - surface = regrid_callback( - src_surface_coord.points, - surface_x_dim, - surface_y_dim, - src_x_coord, - src_y_coord, - sample_grid_x, - sample_grid_y, - ) - surface_coord = src_surface_coord.copy(surface) - return surface_coord - - # Copy across any AuxFactory instances, and regrid their reference - # surfaces where required. - for factory in src.aux_factories: - for coord in factory.dependencies.values(): - if coord is None: - continue - dims = src.coord_dims(coord) - if x_dim in dims and y_dim in dims: - result_coord = regrid_reference_surface( - coord, - dims, - x_dim, - y_dim, - src_x_coord, - src_y_coord, - sample_grid_x, - sample_grid_y, - regrid_callback, - ) - result.add_aux_coord(result_coord, dims) - coord_mapping[id(coord)] = result_coord - try: - result.add_aux_factory(factory.updated(coord_mapping)) - except KeyError: - msg = ( - "Cannot update aux_factory {!r} because of dropped" - " coordinates.".format(factory.name()) - ) - warnings.warn(msg) - return result - def _check_units(self, coord): from iris.coord_systems import GeogCS, RotatedGeogCS @@ -1089,20 +974,168 @@ def __call__(self, src): ) # Wrap up the data as a Cube. - regrid_callback = functools.partial( - self._regrid, method=self._method, extrapolation_mode="nan" + _regrid_callback = functools.partial( + self._regrid, + 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="nan", ) - result = self._create_cube( + + def regrid_callback(*args, **kwargs): + _data, dims = args + return _regrid_callback(_data, *dims, **kwargs) + + result = _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, + [x_dim, y_dim], + [grid_x_coord, grid_y_coord], + 2, regrid_callback, ) return result + + +def _create_cube( + data, src, src_dims, tgt_coords, num_tgt_dims, regrid_callback +): + r""" + Return a new cube for the result of regridding. + Returned cube represents the result of regridding the source cube + onto the horizontal coordinates (e.g. latitude) of the target cube. + All the metadata and coordinates of the result cube are copied from + the source cube, with two exceptions: + - Horizontal coordinates are copied from the target cube. + - Auxiliary coordinates which span the grid dimensions are + ignored. + Parameters + ---------- + data : array + The regridded data as an N-dimensional NumPy array. + src : cube + The source Cube. + src_dims : tuple of int + The dimensions of the X and Y coordinate within the source Cube. + tgt_coords : tuple of :class:`iris.coords.Coord`\\ 's + Either two 1D :class:`iris.coords.DimCoord`\\ 's, two 1D + :class:`iris.experimental.ugrid.DimCoord`\\ 's or two ND + :class:`iris.coords.AuxCoord`\\ 's representing the new grid's + X and Y coordinates. + num_tgt_dims : int + The number of dimensions that the `tgt_coords` span. + regrid_callback : callable + The routine that will be used to calculate the interpolated + values of any reference surfaces. + Returns + ------- + cube + A new iris.cube.Cube instance. + """ + from iris.coords import DimCoord + from iris.cube import Cube + + result = Cube(data) + + if len(src_dims) >= 2: + grid_dim_x, grid_dim_y = src_dims[:2] + elif len(src_dims) == 1: + grid_dim_x = src_dims[0] + grid_dim_y = grid_dim_x + 1 + + if num_tgt_dims == 1: + grid_dim_x = grid_dim_y = min(src_dims) + for tgt_coord, dim in zip(tgt_coords, (grid_dim_x, grid_dim_y)): + if len(tgt_coord.shape) == 1: + if isinstance(tgt_coord, DimCoord) and dim is not None: + result.add_dim_coord(tgt_coord, dim) + else: + result.add_aux_coord(tgt_coord, dim) + else: + result.add_aux_coord(tgt_coord, (grid_dim_y, grid_dim_x)) + + result.metadata = copy.deepcopy(src.metadata) + + # Copy across all the coordinates which don't span the grid. + # Record a mapping from old coordinate IDs to new coordinates, + # for subsequent use in creating updated aux_factories. + + coord_mapping = {} + + def copy_coords(src_coords, add_method): + for coord in src_coords: + dims = src.coord_dims(coord) + if set(src_dims).intersection(set(dims)): + continue + if guess_coord_axis(coord) in ["X", "Y"]: + continue + + def dim_offset(dim): + offset = sum( + [ + d <= dim + for d in (grid_dim_x, grid_dim_y) + if d is not None + ] + ) + if offset and num_tgt_dims == 1: + offset -= 1 + offset -= sum([d <= dim for d in src_dims if d is not None]) + return dim + offset + + dims = [dim_offset(dim) for dim in dims] + result_coord = coord.copy() + # Add result_coord to the owner of add_method. + add_method(result_coord, dims) + coord_mapping[id(coord)] = result_coord + + copy_coords(src.dim_coords, result.add_dim_coord) + copy_coords(src.aux_coords, result.add_aux_coord) + + def regrid_reference_surface( + src_surface_coord, + surface_dims, + src_dims, + regrid_callback, + ): + # Determine which of the reference surface's dimensions span the X + # and Y dimensions of the source cube. + relative_surface_dims = [ + surface_dims.index(dim) if dim is not None else None + for dim in src_dims + ] + surface = regrid_callback( + src_surface_coord.points, + relative_surface_dims, + ) + surface_coord = src_surface_coord.copy(surface) + return surface_coord + + # Copy across any AuxFactory instances, and regrid their reference + # surfaces where required. + for factory in src.aux_factories: + for coord in factory.dependencies.values(): + if coord is None: + continue + dims = src.coord_dims(coord) + if set(src_dims).intersection(dims): + result_coord = regrid_reference_surface( + coord, + dims, + src_dims, + regrid_callback, + ) + result.add_aux_coord(result_coord, dims) + coord_mapping[id(coord)] = result_coord + try: + result.add_aux_factory(factory.updated(coord_mapping)) + except KeyError: + msg = ( + "Cannot update aux_factory {!r} because of dropped" + " coordinates.".format(factory.name()) + ) + warnings.warn(msg) + + return result diff --git a/lib/iris/experimental/regrid_conservative.py b/lib/iris/experimental/regrid_conservative.py index bfa048ddf0..fdc23c7bc4 100644 --- a/lib/iris/experimental/regrid_conservative.py +++ b/lib/iris/experimental/regrid_conservative.py @@ -17,13 +17,15 @@ """ +import functools + import cartopy.crs as ccrs import numpy as np import iris from iris._deprecation import warn_deprecated from iris.analysis._interpolation import get_xy_dim_coords -from iris.analysis._regrid import RectilinearRegridder +from iris.analysis._regrid import RectilinearRegridder, _create_cube from iris.util import _meshgrid wmsg = ( @@ -329,16 +331,23 @@ def _valid_units(coord): # 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], + _regrid_callback = functools.partial( + RectilinearRegridder._regrid, 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_callback(*args, **kwargs): + _data, dims = args + return _regrid_callback(_data, *dims, **kwargs) + + return _create_cube( + fullcube_data, + source_cube, + [src_dims_xy[0], src_dims_xy[1]], + [dst_coords[0], dst_coords[1]], + 2, + regrid_callback, ) diff --git a/lib/iris/tests/results/experimental/regrid/regrid_area_weighted_rectilinear_src_and_grid/const_lat_cross_section.cml b/lib/iris/tests/results/experimental/regrid/regrid_area_weighted_rectilinear_src_and_grid/const_lat_cross_section.cml index b41c0e48c7..cc9deb4260 100644 --- a/lib/iris/tests/results/experimental/regrid/regrid_area_weighted_rectilinear_src_and_grid/const_lat_cross_section.cml +++ b/lib/iris/tests/results/experimental/regrid/regrid_area_weighted_rectilinear_src_and_grid/const_lat_cross_section.cml @@ -5,6 +5,60 @@ + + + + + + + @@ -65,6 +119,12 @@ [0.993097, 0.989272], [0.989272, 0.984692]]" id="a5c170db" long_name="sigma" points="[0.999424, 0.997504, 0.99482, 0.991375, 0.987171]" shape="(5,)" units="Unit('1')" value_type="float32"/> + + + diff --git a/lib/iris/tests/results/experimental/regrid/regrid_area_weighted_rectilinear_src_and_grid/const_lon_cross_section.cml b/lib/iris/tests/results/experimental/regrid/regrid_area_weighted_rectilinear_src_and_grid/const_lon_cross_section.cml index 8617be9372..fb3d2cdbcf 100644 --- a/lib/iris/tests/results/experimental/regrid/regrid_area_weighted_rectilinear_src_and_grid/const_lon_cross_section.cml +++ b/lib/iris/tests/results/experimental/regrid/regrid_area_weighted_rectilinear_src_and_grid/const_lon_cross_section.cml @@ -5,6 +5,65 @@ + + + + + + + @@ -59,6 +118,11 @@ [0.993097, 0.989272], [0.989272, 0.984692]]" id="a5c170db" long_name="sigma" points="[0.999424, 0.997504, 0.99482, 0.991375, 0.987171]" shape="(5,)" units="Unit('1')" value_type="float32"/> + + + diff --git a/lib/iris/tests/unit/analysis/regrid/test__CurvilinearRegridder.py b/lib/iris/tests/unit/analysis/regrid/test__CurvilinearRegridder.py index 68db839d06..9b0160aee4 100644 --- a/lib/iris/tests/unit/analysis/regrid/test__CurvilinearRegridder.py +++ b/lib/iris/tests/unit/analysis/regrid/test__CurvilinearRegridder.py @@ -15,11 +15,12 @@ from iris.analysis._regrid import CurvilinearRegridder as Regridder from iris.analysis.cartography import rotate_pole +from iris.aux_factory import HybridHeightFactory from iris.coord_systems import GeogCS, RotatedGeogCS from iris.coords import AuxCoord, DimCoord from iris.cube import Cube from iris.fileformats.pp import EARTH_RADIUS -from iris.tests.stock import global_pp, lat_lon_cube +from iris.tests.stock import global_pp, lat_lon_cube, realistic_4d RESULT_DIR = ("analysis", "regrid") @@ -169,6 +170,88 @@ def test_caching(self): ) +class Test__derived_coord(tests.IrisTest): + def setUp(self): + src = realistic_4d()[0] + tgt = realistic_4d() + new_lon, new_lat = np.meshgrid( + src.coord("grid_longitude").points, + src.coord("grid_latitude").points, + ) + coord_system = src.coord("grid_latitude").coord_system + lat = AuxCoord( + new_lat, standard_name="latitude", coord_system=coord_system + ) + lon = AuxCoord( + new_lon, standard_name="longitude", coord_system=coord_system + ) + lat_t = AuxCoord( + new_lat.T, standard_name="latitude", coord_system=coord_system + ) + lon_t = AuxCoord( + new_lon.T, standard_name="longitude", coord_system=coord_system + ) + + src.remove_coord("grid_latitude") + src.remove_coord("grid_longitude") + src_t = src.copy() + src.add_aux_coord(lat, [1, 2]) + src.add_aux_coord(lon, [1, 2]) + src_t.add_aux_coord(lat_t, [2, 1]) + src_t.add_aux_coord(lon_t, [2, 1]) + self.src = src.copy() + self.src_t = src_t + self.tgt = tgt + self.altitude = src.coord("altitude") + transposed_src = src.copy() + transposed_src.transpose([0, 2, 1]) + self.altitude_transposed = transposed_src.coord("altitude") + + def test_no_transpose(self): + rg = Regridder(self.src, self.tgt) + res = rg(self.src) + + assert len(res.aux_factories) == 1 and isinstance( + res.aux_factories[0], HybridHeightFactory + ) + assert np.allclose(res.coord("altitude").points, self.altitude.points) + + def test_cube_transposed(self): + rg = Regridder(self.src, self.tgt) + transposed_cube = self.src.copy() + transposed_cube.transpose([0, 2, 1]) + res = rg(transposed_cube) + + assert len(res.aux_factories) == 1 and isinstance( + res.aux_factories[0], HybridHeightFactory + ) + assert np.allclose( + res.coord("altitude").points, self.altitude_transposed.points + ) + + def test_coord_transposed(self): + rg = Regridder(self.src_t, self.tgt) + res = rg(self.src_t) + + assert len(res.aux_factories) == 1 and isinstance( + res.aux_factories[0], HybridHeightFactory + ) + assert np.allclose( + res.coord("altitude").points, self.altitude_transposed.points + ) + + def test_both_transposed(self): + rg = Regridder(self.src_t, self.tgt) + transposed_cube = self.src_t.copy() + transposed_cube.transpose([0, 2, 1]) + res = rg(transposed_cube) + + assert len(res.aux_factories) == 1 and isinstance( + res.aux_factories[0], HybridHeightFactory + ) + assert np.allclose(res.coord("altitude").points, self.altitude.points) + + @tests.skip_data class Test___call____bad_src(tests.IrisTest): def setUp(self): @@ -219,7 +302,7 @@ def test_multidim(self): grid_cube.add_dim_coord(grid_y_coord, 0) grid_cube.add_dim_coord(grid_x_coord, 1) - # Define some key points in true-lat/lon thta have known positions + # Define some key points in true-lat/lon that have known positions # First 3x2 points in the centre of each output cell. x_centres, y_centres = np.meshgrid( grid_x_coord.points, grid_y_coord.points