def convert_cube_data_to_2d(forecast: Cube, coord: str = "realization", transpose: bool = True) -> ndarray: """ Function to convert data from a N-dimensional cube into a 2d numpy array. The result can be transposed, if required. Args: forecast: N-dimensional cube to be reshaped. coord: This dimension is retained as the second dimension by default, and the leading dimension if "transpose" is set to False. transpose: If True, the resulting flattened data is transposed. This will transpose a 2d array of the format [coord, :] to [:, coord]. If coord is not a dimension on the input cube, the resulting array will be 2d with items of length 1. Returns: Reshaped 2d array. """ forecast_data = [] if np.ma.is_masked(forecast.data): forecast.data = np.ma.filled(forecast.data, np.nan) for coord_slice in forecast.slices_over(coord): forecast_data.append(coord_slice.data.flatten()) if transpose: forecast_data = np.asarray(forecast_data).T return np.array(forecast_data)
def collapse_mask_coord(self, cube: Cube) -> Cube: """ Collapse the chosen coordinate with the available weights. The result of the neighbourhood processing is taken into account to renormalize any weights corresponding to a NaN in the result from neighbourhooding. In this case the weights are re-normalized so that we do not lose probability. Args: cube: Cube containing the array to which the square neighbourhood with a mask has been applied. Dimensions self.coord_for_masking, y and x. Returns: Cube containing the weighted mean from neighbourhood after collapsing the chosen coordinate. """ # Mask out any NaNs in the neighbourhood data so that Iris ignores # them when calculating the weighted mean. cube.data = ma.masked_invalid(cube.data, copy=False) # Collapse the coord_for_masking. Renormalization of the weights happen # within the underlying call to a numpy function within the Iris method. result = collapsed( cube, self.coord_for_masking, iris.analysis.MEAN, weights=self.collapse_weights.data, ) # Set masked invalid data points back to np.nans if np.ma.is_masked(result.data): result.data.data[result.data.mask] = np.nan # Remove references to self.coord_masked in the result cube. result.remove_coord(self.coord_for_masking) return result
def _rescale_unmasked_weights(self, weights: Cube, is_rescaled: Cube) -> None: """Increase weights of unmasked slices at locations where masked slices have been smoothed, so that the sum of weights over self.blend_coord is re-normalised (sums to 1) at each point and the relative weightings of multiple unmasked slices are preserved. Modifies weights cube in place. Args: weights: Cube of weights to which fuzzy smoothing has been applied to any masked slices is_rescaled: Cube matching weights.shape, with value of 1 where masked weights have been rescaled, and 0 where they are unchanged. """ rescaled_data = np.multiply(weights.data, is_rescaled.data) unscaled_data = np.multiply(weights.data, ~is_rescaled.data) unscaled_sum = np.sum(unscaled_data, axis=self.blend_axis) required_sum = 1.0 - np.sum(rescaled_data, axis=self.blend_axis) normalisation_factor = np.where(unscaled_sum > 0, np.divide(required_sum, unscaled_sum), 0) normalised_weights = ( np.multiply(unscaled_data, normalisation_factor) + rescaled_data) weights.data = normalised_weights.astype(FLOAT_DTYPE)
def merge_land_and_sea(calibrated_land_only: Cube, uncalibrated: Cube) -> None: """ Merge data that has been calibrated over the land with uncalibrated data. Calibrated data will have masked data over the sea which will need to be filled with the uncalibrated data. Args: calibrated_land_only: A cube that has been calibrated over the land, with sea points masked out. Either realizations, probabilities or percentiles. Data is modified in place. uncalibrated: A cube of uncalibrated data with valid data over the sea. Either realizations, probabilities or percentiles. Dimension coordinates must be the same as the calibrated_land_only cube. Raises: ValueError: If input cubes do not have the same input dimensions. """ # Check dimensions the same on both cubes. if calibrated_land_only.dim_coords != uncalibrated.dim_coords: message = "Input cubes do not have the same dimension coordinates" raise ValueError(message) # Merge data if calibrated_land_only data is masked. if np.ma.is_masked(calibrated_land_only.data): new_data = calibrated_land_only.data.data mask = calibrated_land_only.data.mask new_data[mask] = uncalibrated.data[mask] calibrated_land_only.data = new_data
def _normalise_initial_weights(self, weights: Cube) -> None: """Normalise weights so that they add up to 1 along the blend dimension at each spatial point. This is different from the normalisation that happens after the application of fuzzy smoothing near mask boundaries. Modifies weights cube in place. Array broadcasting relies on blend_coord being the leading dimension, as enforced in self._create_template_slice. Args: weights: 3D weights containing zeros for masked points, but before fuzzy smoothing """ weights_sum = np.sum(weights.data, axis=self.blend_axis) weights.data = np.where(weights_sum > 0, np.divide(weights.data, weights_sum), 0).astype(FLOAT_DTYPE)
def process(standard_landmask: Cube) -> Cube: """Read in the interpolated landmask and round values < 0.5 to False and values >=0.5 to True. Args: standard_landmask: input landmask on standard grid. Returns: output landmask of boolean values. """ mask_sea = standard_landmask.data < 0.5 standard_landmask.data[mask_sea] = False mask_land = standard_landmask.data > 0.0 standard_landmask.data[mask_land] = True standard_landmask.data = standard_landmask.data.astype(np.int8) standard_landmask.rename("land_binary_mask") return standard_landmask
def _run_recursion( cube: Cube, smoothing_coefficients_x: Cube, smoothing_coefficients_y: Cube, iterations: int, ) -> Cube: """ Method to run the recursive filter. Args: cube: 2D cube containing the input data to which the recursive filter will be applied. smoothing_coefficients_x: 2D cube containing array of smoothing_coefficient values that will be used when applying the recursive filter along the x-axis. smoothing_coefficients_y: 2D cube containing array of smoothing_coefficient values that will be used when applying the recursive filter along the y-axis. iterations: The number of iterations of the recursive filter Returns: Cube containing the smoothed field after the recursive filter method has been applied to the input cube. """ (x_index, ) = cube.coord_dims(cube.coord(axis="x").name()) (y_index, ) = cube.coord_dims(cube.coord(axis="y").name()) output = cube.data for _ in range(iterations): output = RecursiveFilter._recurse_forward( output, smoothing_coefficients_x.data, x_index) output = RecursiveFilter._recurse_backward( output, smoothing_coefficients_x.data, x_index) output = RecursiveFilter._recurse_forward( output, smoothing_coefficients_y.data, y_index) output = RecursiveFilter._recurse_backward( output, smoothing_coefficients_y.data, y_index) cube.data = output return cube
def _math_op_common( cube, operation_function, new_unit, new_dtype=None, in_place=False, skeleton_cube=False, ): from iris.cube import Cube _assert_is_cube(cube) if in_place and not skeleton_cube: if cube.has_lazy_data(): cube.data = operation_function(cube.lazy_data()) else: try: operation_function(cube.data, out=cube.data) except TypeError: # Non-ufunc function operation_function(cube.data) new_cube = cube else: data = operation_function(cube.core_data()) if skeleton_cube: # Simply wrap the resultant data in a cube, as no # cube metadata is required by the caller. new_cube = Cube(data) else: new_cube = cube.copy(data) # If the result of the operation is scalar and masked, we need to fix-up the dtype. if ( new_dtype is not None and not new_cube.has_lazy_data() and new_cube.data.shape == () and ma.is_masked(new_cube.data) ): new_cube.data = ma.masked_array(0, 1, dtype=new_dtype) _sanitise_metadata(new_cube, new_unit) return new_cube
def _standardise_dtypes_and_units(cube: Cube) -> None: """ Modify input cube in place to conform to mandatory dtype and unit standards. Args: cube: Cube to be updated in place """ def as_correct_dtype(obj: ndarray, required_dtype: dtype) -> ndarray: """ Returns an object updated if necessary to the required dtype Args: obj: The object to be updated required_dtype: The dtype required Returns: The updated object """ if obj.dtype != required_dtype: return obj.astype(required_dtype) return obj cube.data = as_correct_dtype(cube.data, get_required_dtype(cube)) for coord in cube.coords(): if coord.name() in TIME_COORDS and not check_units(coord): coord.convert_units(get_required_units(coord)) req_dtype = get_required_dtype(coord) # ensure points and bounds have the same dtype if np.issubdtype(req_dtype, np.integer): coord.points = round_close(coord.points) coord.points = as_correct_dtype(coord.points, req_dtype) if coord.has_bounds(): if np.issubdtype(req_dtype, np.integer): coord.bounds = round_close(coord.bounds) coord.bounds = as_correct_dtype(coord.bounds, req_dtype)
def apply_circular_kernel(self, cube: Cube, ranges: int) -> Cube: """ Method to apply a circular kernel to the data within the input cube in order to smooth the resulting field. Args: cube: Cube containing to array to apply CircularNeighbourhood processing to. ranges: Number of grid cells in the x and y direction used to create the kernel. Returns: Cube containing the smoothed field after the kernel has been applied. """ data = cube.data full_ranges = np.zeros([np.ndim(data)]) axes = [] for axis in ["x", "y"]: coord_name = cube.coord(axis=axis).name() axes.append(cube.coord_dims(coord_name)[0]) for axis in axes: full_ranges[axis] = ranges self.kernel = circular_kernel(full_ranges, ranges, self.weighted_mode) # Smooth the data by applying the kernel. if self.sum_or_fraction == "sum": total_area = 1.0 else: # sum_or_fraction is in fraction mode total_area = np.sum(self.kernel) cube.data = correlate(data, self.kernel, mode="nearest") / total_area return cube