Source code for improver.regrid.landsea2

# (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.
"""
Plugin to regrid using custom nearest and bilinear methods, both with
land-sea awareness
"""

import numpy as np
from iris.cube import Cube

from improver import PostProcessingPlugin
from improver.regrid.bilinear import (
    adjust_for_surface_mismatch,
    apply_weights,
    basic_indexes,
    basic_weights,
)
from improver.regrid.grid import (
    calculate_input_grid_spacing,
    classify_input_surface_type,
    classify_output_surface_type,
    create_regrid_cube,
    ensure_ascending_coord,
    flatten_spatial_dimensions,
    group_target_points_with_source_domain,
    latlon_from_cube,
    mask_target_points_outside_source_domain,
    similar_surface_classify,
    slice_cube_by_domain,
    slice_mask_cube_by_domain,
    unflatten_spatial_dimensions,
)
from improver.regrid.nearest import nearest_regrid, nearest_with_mask_regrid
from improver.utilities.spatial import transform_grid_to_lat_lon

NEAREST = "nearest"
BILINEAR = "bilinear"
WITH_MASK = "-with-mask"
BILINEAR2 = f"{BILINEAR}-2"
NEAREST2 = f"{NEAREST}-2"
NEAREST_MASK2 = f"{NEAREST}{WITH_MASK}-2"
BILINEAR_MASK2 = f"{BILINEAR}{WITH_MASK}-2"
NUM_NEIGHBOURS = 4


[docs] class RegridWithLandSeaMask(PostProcessingPlugin): """ Nearest-neighbour and bilinear regridding with or without land-sea mask awareness. When land-sea mask considered, surface-type-mismatched source points are excluded from field regridding calculation for target points. Note: regrid_mode options are "nearest-2", "nearest-with-mask-2","bilinear-2", and "bilinear-with-mask-2" in this class. """
[docs] def __init__( self, regrid_mode: str = "bilinear-2", vicinity_radius: float = 25000.0 ): """ Initialise class Args: regrid_mode: Mode of interpolation in regridding. Valid options are "bilinear-2", "nearest-2","nearest-with-mask-2" and "bilinear-with-mask-2". The last two options trigger adjustment of regridded points to match source points in terms of land / sea type. vicinity_radius: Radius of vicinity to search for a coastline, in metres. """ self.regrid_mode = regrid_mode self.vicinity = vicinity_radius
[docs] def process(self, cube_in: Cube, cube_in_mask: Cube, cube_out_mask: Cube) -> Cube: """ Regridding considering land_sea mask. please note cube_in must use lats/lons rectlinear system(GeogCS). cube_in_mask and cube_in could be different resolution. cube_out could be either in lats/lons rectlinear system or LambertAzimuthalEqualArea system. Grid points in cube_out domain but not in cube_in domain will be masked. Args: cube_in: Cube of data to be regridded. cube_in_mask: Cube of land_binary_mask data ((land:1, sea:0). used to determine where the input model data is representing land and sea points. cube_out_mask: Cube of land_binary_mask data on target grid (land:1, sea:0). Returns: Regridded result cube. """ # if cube_in's coordinate descending, make it assending. # if mask considered, reverse mask cube's coordinate if descending cube_in = ensure_ascending_coord(cube_in) if WITH_MASK in self.regrid_mode: cube_in_mask = ensure_ascending_coord(cube_in_mask) # check if input source grid is on even-spacing, ascending lat/lon system # return grid spacing for latitude and logitude lat_spacing, lon_spacing = calculate_input_grid_spacing(cube_in) # Gather output latitude/longitudes from output template cube if ( cube_out_mask.coord(axis="x").standard_name == "projection_x_coordinate" and cube_out_mask.coord(axis="y").standard_name == "projection_y_coordinate" ): out_latlons = np.dstack(transform_grid_to_lat_lon(cube_out_mask)).reshape( (-1, 2) ) else: out_latlons = latlon_from_cube(cube_out_mask) # Subset the input cube so that extra spatial area beyond the output is removed # This is a performance optimisation to reduce the size of the dataset being processed total_out_point_num = out_latlons.shape[0] lat_max, lon_max = out_latlons.max(axis=0) lat_min, lon_min = out_latlons.min(axis=0) if WITH_MASK in self.regrid_mode: cube_in, cube_in_mask = slice_mask_cube_by_domain( cube_in, cube_in_mask, (lat_max, lon_max, lat_min, lon_min) ) else: # not WITH_MASK cube_in = slice_cube_by_domain( cube_in, (lat_max, lon_max, lat_min, lon_min) ) # group cube_out's grid points into outside or inside cube_in's domain (outside_input_domain_index, inside_input_domain_index) = ( group_target_points_with_source_domain(cube_in, out_latlons) ) # exclude out-of-input-domain target point here if len(outside_input_domain_index) > 0: out_latlons = out_latlons[inside_input_domain_index] # Gather input latitude/longitudes from input cube in_latlons = latlon_from_cube(cube_in) # Number of grid points in X dimension is used to work out length of flattened array # stripes for finding surrounding points for bilinear interpolation in_lons_size = cube_in.coord(axis="x").shape[0] # longitude # Reshape input data so that spatial dimensions can be handled as one in_values, lats_index, lons_index = flatten_spatial_dimensions(cube_in) # Locate nearby input points for output points indexes = basic_indexes( out_latlons, in_latlons, in_lons_size, lat_spacing, lon_spacing ) if WITH_MASK in self.regrid_mode: in_classified = classify_input_surface_type(cube_in_mask, in_latlons) out_classified = classify_output_surface_type(cube_out_mask) if len(outside_input_domain_index) > 0: out_classified = out_classified[inside_input_domain_index] # Identify mismatched surface types from input and output classifications surface_type_mask = similar_surface_classify( in_classified, out_classified, indexes ) # Initialise distances and weights to zero. Weights are only used for the bilinear case distances = np.zeros((out_latlons.shape[0], NUM_NEIGHBOURS), dtype=np.float32) weights = np.zeros((out_latlons.shape[0], NUM_NEIGHBOURS), dtype=np.float32) # handle nearest option if NEAREST in self.regrid_mode: for i in range(NUM_NEIGHBOURS): distances[:, i] = np.square( in_latlons[indexes[:, i], 0] - out_latlons[:, 0] ) + np.square(in_latlons[indexes[:, i], 1] - out_latlons[:, 1]) # for nearest-with-mask-2,adjust indexes and distance for mismatched # surface type location if WITH_MASK in self.regrid_mode: distances, indexes = nearest_with_mask_regrid( distances, indexes, surface_type_mask, in_latlons, out_latlons, in_classified, out_classified, self.vicinity, ) # apply nearest distance rule output_flat = nearest_regrid(distances, indexes, in_values) elif BILINEAR in self.regrid_mode: # Assume all four nearby points are same surface type and calculate default weights # These will be updated for mask/mismatched surface type further below index_range = np.arange(weights.shape[0]) weights[index_range] = basic_weights( index_range, indexes, out_latlons, in_latlons, lat_spacing, lon_spacing ) if WITH_MASK in self.regrid_mode: # For bilinear-with-mask-2, adjust weights and indexes for mismatched # surface type locations weights, indexes = adjust_for_surface_mismatch( in_latlons, out_latlons, in_classified, out_classified, weights, indexes, surface_type_mask, in_lons_size, self.vicinity, lat_spacing, lon_spacing, ) # apply bilinear rule output_flat = apply_weights(indexes, in_values, weights) # check if we need mask cube_out grid points which are out of cube_in range if len(outside_input_domain_index) > 0: output_flat = mask_target_points_outside_source_domain( total_out_point_num, outside_input_domain_index, inside_input_domain_index, output_flat, ) # Un-flatten spatial dimensions and put into output cube output_array = unflatten_spatial_dimensions( output_flat, cube_out_mask, in_values, lats_index, lons_index ) output_cube = create_regrid_cube(output_array, cube_in, cube_out_mask) return output_cube