Source code for improver.utilities.pad_spatial

# (C) Crown Copyright, Met Office. All rights reserved.
#
# This file is part of 'IMPROVER' and is released under the BSD 3-Clause license.
# See LICENSE in the root of the repository for full licensing details.
"""Utilities for spatial padding of iris cubes."""

from copy import deepcopy

import iris
import numpy as np
from cf_units import Unit
from iris.coords import Coord, DimCoord
from iris.cube import Cube
from numpy import ndarray

from improver.utilities.cube_checker import check_for_x_and_y_axes
from improver.utilities.cube_manipulation import (
    enforce_coordinate_ordering,
    get_dim_coord_names,
)
from improver.utilities.spatial import distance_to_number_of_grid_cells


[docs] def pad_coord(coord: Coord, width: int, method: str) -> Coord: """ Construct a new coordinate by extending the current coordinate by the padding width. Args: coord: Original coordinate which will be used as the basis of the new extended coordinate. width: The width of padding in grid cells (the extent of the neighbourhood radius in grid cells in a given direction). method: A string determining whether the coordinate is being expanded or contracted. Options: 'remove' to remove points from coord; 'add' to add points to coord. Returns: Coordinate with expanded or contracted length, to be added to the padded or unpadded iris cube. Raises: ValueError: Raise an error if non-uniform increments exist between grid points. """ orig_points = coord.points increment = orig_points[1:] - orig_points[:-1] if np.isclose(np.sum(np.diff(increment)), 0): increment = increment[0] else: msg = "Non-uniform increments between grid points: {}.".format(increment) raise ValueError(msg) if method == "add": num_of_new_points = len(orig_points) + width + width new_points = np.linspace( orig_points[0] - width * increment, orig_points[-1] + width * increment, num_of_new_points, dtype=np.float32, ) elif method == "remove": end_width = -width if width != 0 else None new_points = np.float32(orig_points[width:end_width]) new_points = new_points.astype(orig_points.dtype) new_points_bounds = np.array( [new_points - 0.5 * increment, new_points + 0.5 * increment], dtype=np.float32 ).T return coord.copy(points=new_points, bounds=new_points_bounds)
[docs] def create_cube_with_halo(cube: Cube, halo_radius: float) -> Cube: """ Create a template cube defining a new grid by adding a fixed width halo on all sides to the input cube grid. The cube contains no meaningful data. Args: cube: Cube on original grid halo_radius: Size of border to pad original grid, in metres Returns: New cube defining the halo-padded grid (data set to zero) """ halo_size_x = distance_to_number_of_grid_cells(cube, halo_radius, axis="x") halo_size_y = distance_to_number_of_grid_cells(cube, halo_radius, axis="y") # create padded x- and y- coordinates x_coord = pad_coord(cube.coord(axis="x"), halo_size_x, "add") y_coord = pad_coord(cube.coord(axis="y"), halo_size_y, "add") halo_cube = iris.cube.Cube( np.zeros((len(y_coord.points), len(x_coord.points)), dtype=np.float32), long_name="grid_with_halo", units=Unit("no_unit"), dim_coords_and_dims=[(y_coord, 0), (x_coord, 1)], ) return halo_cube
[docs] def _create_cube_with_padded_data( source_cube: Cube, data: ndarray, coord_x: DimCoord, coord_y: DimCoord ) -> Cube: """ Create a cube with newly created data where the metadata is copied from the input cube and the supplied x and y coordinates are added to the cube. Args: source_cube: Template cube used for copying metadata and non x and y axes coordinates. data: Data to be put into the new cube. coord_x: Coordinate to be added to the new cube to represent the x axis. coord_y: Coordinate to be added to the new cube to represent the y axis. Returns: Cube built from the template cube using the requested data and the supplied x and y axis coordinates. """ check_for_x_and_y_axes(source_cube) yname = source_cube.coord(axis="y").name() xname = source_cube.coord(axis="x").name() ycoord_dim = source_cube.coord_dims(yname) xcoord_dim = source_cube.coord_dims(xname) # inherit metadata (cube name, units, attributes etc) metadata_dict = deepcopy(source_cube.metadata._asdict()) new_cube = iris.cube.Cube(data, **metadata_dict) # inherit non-spatial coordinates for coord in source_cube.coords(): if coord.name() not in [yname, xname]: if source_cube.coords(coord, dim_coords=True): coord_dim = source_cube.coord_dims(coord) new_cube.add_dim_coord(coord, coord_dim) else: new_cube.add_aux_coord(coord) # update spatial coordinates if len(xcoord_dim) > 0: new_cube.add_dim_coord(coord_x, xcoord_dim) else: new_cube.add_aux_coord(coord_x) if len(ycoord_dim) > 0: new_cube.add_dim_coord(coord_y, ycoord_dim) else: new_cube.add_aux_coord(coord_y) return new_cube
[docs] def pad_cube_with_halo( cube: Cube, width_x: int, width_y: int, pad_method: str = "constant" ) -> Cube: """ Method to pad a halo around the data in an iris cube. If halo_with_data is False, the halo is filled with zeros. Otherwise the padding calculates a mean within half the padding width with which to fill the halo region. Args: cube: The original cube prior to applying padding. The cube should contain only x and y dimensions, so will generally be a slice of a cube. width_x: The width in x directions of the neighbourhood radius in grid cells. This will be the width of padding to be added to the numpy array. width_y: The width in y directions of the neighbourhood radius in grid cells. This will be the width of padding to be added to the numpy array. pad_method: The numpy.pad method with which to populate the halo. The default is 'constant' which will populate the region with zeros. All other np.pad methods are accepted, though they are not fully configurable. Returns: Cube containing the new padded cube, with appropriate changes to the cube's dimension coordinates. """ check_for_x_and_y_axes(cube) # Pad a halo around the original data with the extent of the halo # given by width_y and width_x. kwargs = { "stat_length": ((width_y // 2, width_y // 2), (width_x // 2, width_x // 2)) } if pad_method == "constant": kwargs = {"constant_values": (0.0, 0.0)} if pad_method == "symmetric": kwargs = {} padded_data = np.pad( cube.data, ((width_y, width_y), (width_x, width_x)), mode=pad_method, **kwargs ) coord_x = cube.coord(axis="x") padded_x_coord = pad_coord(coord_x, width_x, "add") coord_y = cube.coord(axis="y") padded_y_coord = pad_coord(coord_y, width_y, "add") padded_cube = _create_cube_with_padded_data( cube, padded_data, padded_x_coord, padded_y_coord ) return padded_cube
[docs] def remove_cube_halo(cube: Cube, halo_radius: float) -> Cube: """ Remove halo of halo_radius from a cube. This function converts the halo radius into the number of grid points in the x and y coordinate that need to be removed. It then calls remove_halo_from_cube which only acts on a cube with x and y coordinates so we need to slice the cube and them merge the cube back together ensuring the resulting cube has the same dimension coordinates. Args: cube: Cube on extended grid halo_radius: Size of border to remove, in metres Returns: New cube with the halo removed. """ halo_size_x = distance_to_number_of_grid_cells(cube, halo_radius, axis="x") halo_size_y = distance_to_number_of_grid_cells(cube, halo_radius, axis="y") result_slices = iris.cube.CubeList() for cube_slice in cube.slices([cube.coord(axis="y"), cube.coord(axis="x")]): cube_halo = remove_halo_from_cube(cube_slice, halo_size_x, halo_size_y) result_slices.append(cube_halo) result = result_slices.merge_cube() # re-promote any scalar dimensions lost in slice / merge req_dims = get_dim_coord_names(cube) present_dims = get_dim_coord_names(result) for coord in req_dims: if coord not in present_dims: result = iris.util.new_axis(result, coord) # re-order (needed if scalar dimensions have been re-added) enforce_coordinate_ordering(result, req_dims) return result
[docs] def remove_halo_from_cube(cube: Cube, width_x: float, width_y: float) -> Cube: """ Method to remove rows/columns from the edge of an iris cube. Used to 'unpad' cubes which have been previously padded by pad_cube_with_halo. Args: cube: The original cube to be trimmed of edge data. The cube should contain only x and y dimensions, so will generally be a slice of a cube. width_x: The width in x directions of the neighbourhood radius in grid cells. This will be the width of padding to be added to the numpy array. width_y: The width in y directions of the neighbourhood radius in grid cells. This will be the width of padding to be added to the numpy array. Returns: Cube containing the new trimmed cube, with appropriate changes to the cube's dimension coordinates. """ check_for_x_and_y_axes(cube) end_y = -width_y if width_y != 0 else None end_x = -width_x if width_x != 0 else None trimmed_data = cube.data[width_y:end_y, width_x:end_x] coord_x = cube.coord(axis="x") trimmed_x_coord = pad_coord(coord_x, width_x, "remove") coord_y = cube.coord(axis="y") trimmed_y_coord = pad_coord(coord_y, width_y, "remove") trimmed_cube = _create_cube_with_padded_data( cube, trimmed_data, trimmed_x_coord, trimmed_y_coord ) return trimmed_cube