Source code for improver.wind_calculations.wind_direction

# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# (C) British Crown copyright. The Met Office.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the copyright holder nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Module containing wind direction averaging plugins."""

from typing import Union

import iris
import numpy as np
from iris.coords import CellMethod
from iris.cube import Cube
from numpy import ndarray

from improver import PostProcessingPlugin
from improver.nbhood.nbhood import NeighbourhoodProcessing
from improver.utilities.cube_checker import check_cube_coordinates


[docs]class WindDirection(PostProcessingPlugin): """Plugin to calculate average wind direction from ensemble realizations. Science background: Taking an average wind direction is tricky since an average of two wind directions at 10 and 350 degrees is 180 when it should be 0 degrees. Converting the wind direction angles to complex numbers allows us to find a useful numerical average. :: z = a + bi a = r*Cos(theta) b = r*Sin(theta) r = radius The average of two complex numbers is NOT the ANGLE between two points it is the MIDPOINT in cartesian space. Therefore if there are two data points with radius=1 at 90 and 270 degrees then the midpoint is at (0,0) with radius=0 and therefore its average angle is meaningless. :: N | W---x------o------x---E | S In the rare case that a meaningless complex average is calculated, the code rejects the calculated complex average and simply uses the wind direction taken from the first ensemble realization. The steps are: 1) Take data from all ensemble realizations. 2) Convert the wind direction angles to complex numbers. 3) Find complex average and their radius values. 4) Convert the complex average back into degrees. 5) If any point has an radius of nearly zero - replace the calculated average with the wind direction from the first ensemble. Args: backup_method: Backup method to use if the complex numbers approach has low confidence. "first_realization" uses the value of realization zero. "neighbourhood" (default) recalculates using the complex numbers approach with additional realizations extracted from neighbouring grid points from all available realizations. """
[docs] def __init__(self, backup_method: str = "neighbourhood") -> None: """Initialise class.""" self.backup_methods = ["first_realization", "neighbourhood"] self.backup_method = backup_method if self.backup_method not in self.backup_methods: msg = "Invalid option for keyword backup_method " "({})".format( self.backup_method ) raise ValueError(msg) # Any points where the r-values are below the threshold is regarded as # containing ambigous data. self.r_thresh = 0.01 # Creates cubelists to hold data. self.wdir_cube_list = iris.cube.CubeList() self.r_vals_cube_list = iris.cube.CubeList() # Radius used in neighbourhood plugin as determined in IMPRO-491 self.nb_radius = 6000.0 # metres # Initialise neighbourhood plugin ready for use self.nbhood = NeighbourhoodProcessing( "square", self.nb_radius, weighted_mode=False )
def __repr__(self) -> str: """Represent the configured plugin instance as a string.""" return ( '<WindDirection: backup_method "{}"; neighbourhood radius "{}"m>' ).format(self.backup_method, self.nb_radius)
[docs] def _reset(self) -> None: """Empties working data objects""" self.realization_axis = None self.wdir_complex = None self.wdir_slice_mean = None self.wdir_mean_complex = None self.r_vals_slice = None
[docs] @staticmethod def deg_to_complex( angle_deg: Union[ndarray, float], radius: Union[ndarray, float] = 1 ) -> Union[ndarray, float]: """Converts degrees to complex values. The radius value can be used to weigh values - but it is set to 1 for now. Args: angle_deg: 3D array or float - wind direction angles in degrees. radius: 3D array or float - radius value for each point, default=1. Returns: 3D array or float - wind direction translated to complex numbers. """ # Convert from degrees to radians. angle_rad = np.deg2rad(angle_deg) # Derive real and imaginary components (also known as a and b) real = radius * np.cos(angle_rad) imag = radius * np.sin(angle_rad) # Combine components into a complex number and return. return real + 1j * imag
[docs] @staticmethod def complex_to_deg(complex_in: ndarray) -> ndarray: """Converts complex to degrees. The "np.angle" function returns negative numbers when the input is greater than 180. Therefore additional processing is needed to ensure that the angle is between 0-359. Args: complex_in: 3D array - wind direction angles in complex number form. Returns: 3D array - wind direction in angle form Raises: TypeError: If complex_in is not an array. """ if not isinstance(complex_in, np.ndarray): msg = "Input data is not a numpy array, but {}" raise TypeError(msg.format(type(complex_in))) angle = np.angle(complex_in, deg=True) # If angle negative value - add 360 degrees. angle = np.where(angle < 0, angle + 360, angle) # If angle == 360 - set to zero degrees. angle = np.where(np.isclose(angle, 360, atol=0.001), 0.0, angle) # We don't need 64 bit precision. angle = angle.astype(np.float32) return angle
[docs] def calc_wind_dir_mean(self) -> None: """Find the mean wind direction using complex average which actually signifies a point between all of the data points in POLAR coordinates - NOT the average DEGREE ANGLE. Uses: self.wdir_complex: 3D array or float - wind direction angles in degrees. self.realization_axis: Axis to collapse over. Defines: self.wdir_mean_complex: 3D array or float - wind direction angles as complex numbers collapsed along an axis using np.mean(). self.wdir_slice_mean: 3D array or float - wind direction angles in degrees collapsed along an axis using np.mean(). """ self.wdir_mean_complex = np.mean(self.wdir_complex, axis=self.realization_axis) self.wdir_slice_mean.data = self.complex_to_deg(self.wdir_mean_complex)
[docs] def find_r_values(self) -> None: """Find radius values from complex numbers. Takes input wind direction in complex values and returns array containing r values using Pythagoras theorem. Uses: self.wdir_mean_complex: 3D array or float - wind direction angles in complex numbers. self.wdir_slice_mean: 3D array or float - mean wind direction angles in complex numbers. Defines: self.r_vals_slice: Contains r values and inherits meta-data from self.wdir_slice_mean. """ r_vals = np.sqrt( np.square(self.wdir_mean_complex.real) + np.square(self.wdir_mean_complex.imag) ) self.r_vals_slice = self.wdir_slice_mean.copy(data=r_vals)
[docs] def wind_dir_decider(self, where_low_r: ndarray, wdir_cube: Cube) -> None: """If the wind direction is so widely scattered that the r value is nearly zero then this indicates that the average wind direction is essentially meaningless. We therefore substitute this meaningless average wind direction value for the wind direction calculated from a larger sample by smoothing across a neighbourhood of points before rerunning the main technique. This is invoked rarely (1 : 100 000) Args: where_low_r: Array of boolean values. True where original wind direction estimate has low confidence. These points are replaced according to self.backup_method wdir_cube: Contains array of wind direction data (realization, y, x) Uses: self.wdir_slice_mean: Containing average wind direction angle (in degrees). self.wdir_complex: 3D array - wind direction angles from ensembles (in complex). self.r_vals_slice.data: 2D array - Radius taken from average complex wind direction angle. self.r_thresh: Any r value below threshold is regarded as meaningless. self.realization_axis: Axis to collapse over. self.n_realizations: Number of realizations available in the plugin. Used to set the neighbourhood radius as this is used to adjust the radius again in the neighbourhooding plugin. Defines: self.wdir_slice_mean.data: 2D array - Wind direction degrees where ambigious values have been replaced with data from first ensemble realization. """ if self.backup_method == "neighbourhood": # Performs smoothing over a 6km square neighbourhood. # Then calculates the mean wind direction. child_class = WindDirection(backup_method="first_realization") child_class.wdir_complex = self.nbhood( wdir_cube.copy(data=self.wdir_complex) ).data child_class.realization_axis = self.realization_axis child_class.wdir_slice_mean = self.wdir_slice_mean.copy() child_class.calc_wind_dir_mean() improved_values = child_class.wdir_slice_mean.data else: # Takes realization zero (control member). improved_values = wdir_cube.extract(iris.Constraint(realization=0)).data # If the r-value is low - substitute average wind direction value for # the wind direction taken from the first ensemble realization. self.wdir_slice_mean.data = np.where( where_low_r, improved_values, self.wdir_slice_mean.data )
[docs] def process(self, cube_ens_wdir: Cube) -> Cube: """Create a cube containing the wind direction averaged over the ensemble realizations. Args: cube_ens_wdir: Cube containing wind direction from multiple ensemble realizations. Returns: - Cube containing the wind direction averaged from the ensemble realizations. Raises: TypeError: If cube_wdir is not a cube. """ if not isinstance(cube_ens_wdir, iris.cube.Cube): msg = "Wind direction input is not a cube, but {}" raise TypeError(msg.format(type(cube_ens_wdir))) try: cube_ens_wdir.convert_units("degrees") except ValueError as err: msg = "Input cube cannot be converted to degrees: {}".format(err) raise ValueError(msg) self.n_realizations = len(cube_ens_wdir.coord("realization").points) y_coord_name = cube_ens_wdir.coord(axis="y").name() x_coord_name = cube_ens_wdir.coord(axis="x").name() for wdir_slice in cube_ens_wdir.slices( ["realization", y_coord_name, x_coord_name] ): self._reset() # Extract wind direction data. self.wdir_complex = self.deg_to_complex(wdir_slice.data) (self.realization_axis,) = wdir_slice.coord_dims("realization") # Copies input cube and remove realization dimension to create # cubes for storing results. self.wdir_slice_mean = next(wdir_slice.slices_over("realization")) self.wdir_slice_mean.remove_coord("realization") # Derive average wind direction. self.calc_wind_dir_mean() # Find radius values for wind direction average. self.find_r_values() # Finds any meaningless averages and substitute with # the wind direction taken from the first ensemble realization. # Mask True if r values below threshold. where_low_r = np.where(self.r_vals_slice.data < self.r_thresh, True, False) # If the any point in the array contains poor r-values, # trigger decider function. if where_low_r.any(): self.wind_dir_decider(where_low_r, wdir_slice) # Append to cubelists. self.wdir_cube_list.append(self.wdir_slice_mean) # Combine cubelists into cube. cube_mean_wdir = self.wdir_cube_list.merge_cube() # Check that the dimensionality of coordinates of the output cube # matches the input cube. first_slice = next(cube_ens_wdir.slices_over(["realization"])) cube_mean_wdir = check_cube_coordinates(first_slice, cube_mean_wdir) # Change cube identifiers. cube_mean_wdir.add_cell_method(CellMethod("mean", coords="realization")) return cube_mean_wdir