def test_lon_normalize(self):
        """Test the longitude normalization function"""
        data = np.array([-180, 20.1, -30, 190, -350])

        # test in place operation
        lon_normalize(data)
        self.assertTrue(np.allclose(data, [180, 20.1, -30, -170, 10]))

        # test with specific center and return value
        data = lon_normalize(data, center=-170)
        self.assertTrue(np.allclose(data, [-180, -339.9, -30, -170, 10]))
    def test_lon_normalize(self):
        """Test the longitude normalization function"""
        data = np.array([-180, 20.1, -30, 190, -350])

        # test in place operation
        lon_normalize(data)
        np.testing.assert_array_almost_equal(data, [180, 20.1, -30, -170, 10])

        # test with specific center and return value
        data = lon_normalize(data, center=-170)
        np.testing.assert_array_almost_equal(data,
                                             [-180, -339.9, -30, -170, 10])

        # test with center determined automatically (which is 280.05)
        data = lon_normalize(data, center=None)
        np.testing.assert_array_almost_equal(data, [180, 380.1, 330, 190, 370])
    def select(self, reg_id=None, extent=None, sel_cen=None):
        """Return Centroids with points in the given reg_id or within mask

        Parameters
        ----------
        reg_id : int
            region to filter according to region_id values
        extent : tuple
            Format (min_lon, max_lon, min_lat, max_lat) tuple.
            If min_lon > lon_max, the extend crosses the antimeridian and is
            [lon_max, 180] + [-180, lon_min]
            Borders are inclusive.
        sel_cen : np.array
            1-dim mask, overrides reg_id and extent

        Returns
        -------
        cen : Centroids
            Sub-selection of this object
        """

        if sel_cen is None:
            sel_cen = np.ones_like(self.region_id, dtype=bool)
            if reg_id:
                sel_cen &= np.isin(self.region_id, reg_id)
            if extent:
                lon_min, lon_max, lat_min, lat_max = extent
                lon_max += 360 if lon_min > lon_max else 0
                lon_normalized = u_coord.lon_normalize(self.lon.copy(),
                                                       center=0.5 *
                                                       (lon_min + lon_max))
                sel_cen &= ((lon_normalized >= lon_min) &
                            (lon_normalized <= lon_max) & (self.lat >= lat_min)
                            & (self.lat <= lat_max))

        if not self.lat.size or not self.lon.size:
            self.set_meta_to_lat_lon()

        centr = Centroids()
        centr.set_lat_lon(self.lat[sel_cen], self.lon[sel_cen],
                          self.geometry.crs)
        if self.area_pixel.size:
            centr.area_pixel = self.area_pixel[sel_cen]
        if self.region_id.size:
            centr.region_id = self.region_id[sel_cen]
        if self.on_land.size:
            centr.on_land = self.on_land[sel_cen]
        if self.dist_coast.size:
            centr.dist_coast = self.dist_coast[sel_cen]
        return centr
    def test_latlon_bounds(self):
        """Test latlon_bounds function"""
        lat, lon = np.array([0, -2, 5]), np.array([-179, 175, 178])
        bounds = latlon_bounds(lat, lon)
        self.assertEqual(bounds, (175, -2, 181, 5))
        bounds = latlon_bounds(lat, lon, buffer=1)
        self.assertEqual(bounds, (174, -3, 182, 6))

        # buffer exceeding antimeridian
        lat, lon = np.array([0, -2.1, 5]), np.array([-179.5, -175, -178])
        bounds = latlon_bounds(lat, lon, buffer=1)
        self.assertEqual(bounds, (179.5, -3.1, 186, 6))

        # longitude values need to be normalized before they lie between computed bounds:
        lon_mid = 0.5 * (bounds[0] + bounds[2])
        lon = lon_normalize(lon, center=lon_mid)
        self.assertTrue(np.all((bounds[0] <= lon) & (lon <= bounds[2])))

        # data covering almost the whole longitudinal range
        lat, lon = np.linspace(-90, 90, 180), np.linspace(-180.0, 179, 360)
        bounds = latlon_bounds(lat, lon)
        self.assertEqual(bounds, (-179, -90, 180, 90))
        bounds = latlon_bounds(lat, lon, buffer=1)
        self.assertEqual(bounds, (-180, -90, 180, 90))
Exemple #5
0
def compute_windfields(track, centroids, model, metric="equirect"):
    """Compute 1-minute sustained winds (in m/s) at 10 meters above ground

    In a first step, centroids within reach of the track are determined so that wind fields will
    only be computed and returned for those centroids.

    Parameters
    ----------
    track : xr.Dataset
        Track infomation.
    centroids : 2d np.array
        Each row is a centroid [lat, lon].
        Centroids that are not within reach of the track are ignored.
    model : int
        Holland model selection according to MODEL_VANG.
    metric : str, optional
        Specify an approximation method to use for earth distances: "equirect" (faster) or
        "geosphere" (more accurate). See `dist_approx` function in `climada.util.coordinates`.
        Default: "equirect".

    Returns
    -------
    windfields : np.array of shape (npositions, nreachable, 2)
        Directional wind fields for each track position on those centroids within reach
        of the TC track.
    reachable_centr_idx : np.array of shape (nreachable,)
        List of indices of input centroids within reach of the TC track.
    """
    # copies of track data
    # Note that max wind records are not used in the Holland wind field models!
    t_lat, t_lon, t_tstep, t_rad, t_env, t_cen = [
        track[ar].values.copy() for ar in [
            'lat', 'lon', 'time_step', 'radius_max_wind',
            'environmental_pressure', 'central_pressure'
        ]
    ]

    # start with the assumption that no centroids are within reach
    npositions = t_lat.shape[0]
    reachable_centr_idx = np.zeros((0, ), dtype=np.int64)
    windfields = np.zeros((npositions, 0, 2), dtype=np.float64)

    # the wind field model requires at least two track positions because translational speed
    # as well as the change in pressure are required
    if npositions < 2:
        return windfields, reachable_centr_idx

    # normalize longitude values (improves performance of `dist_approx` and `_close_centroids`)
    mid_lon = 0.5 * sum(u_coord.lon_bounds(t_lon))
    u_coord.lon_normalize(t_lon, center=mid_lon)
    u_coord.lon_normalize(centroids[:, 1], center=mid_lon)

    # restrict to centroids within rectangular bounding boxes around track positions
    track_centr_msk = _close_centroids(t_lat, t_lon, centroids)
    track_centr = centroids[track_centr_msk]
    nreachable = track_centr.shape[0]
    if nreachable == 0:
        return windfields, reachable_centr_idx

    # compute distances and vectors to all centroids
    [d_centr], [v_centr] = u_coord.dist_approx(t_lat[None],
                                               t_lon[None],
                                               track_centr[None, :, 0],
                                               track_centr[None, :, 1],
                                               log=True,
                                               normalize=False,
                                               method=metric)

    # exclude centroids that are too far from or too close to the eye
    close_centr_msk = (d_centr < CENTR_NODE_MAX_DIST_KM) & (d_centr > 1e-2)
    if not np.any(close_centr_msk):
        return windfields, reachable_centr_idx
    v_centr_normed = np.zeros_like(v_centr)
    v_centr_normed[close_centr_msk] = v_centr[close_centr_msk] / d_centr[
        close_centr_msk, None]

    # make sure that central pressure never exceeds environmental pressure
    pres_exceed_msk = (t_cen > t_env)
    t_cen[pres_exceed_msk] = t_env[pres_exceed_msk]

    # extrapolate radius of max wind from pressure if not given
    t_rad[:] = estimate_rmw(t_rad, t_cen) * NM_TO_KM

    # translational speed of track at every node
    v_trans = _vtrans(t_lat, t_lon, t_tstep, metric=metric)
    v_trans_norm = v_trans[0]

    # adjust pressure at previous track point
    prev_pres = t_cen[:-1].copy()
    msk = (prev_pres < 850)
    prev_pres[msk] = t_cen[1:][msk]

    # compute b-value and derive (absolute) angular velocity
    if model == MODEL_VANG['H1980']:
        # convert recorded surface winds to gradient-level winds without translational influence
        t_vmax = track.max_sustained_wind.values.copy()
        t_gradient_winds = np.fmax(
            0, t_vmax - v_trans_norm) / GRADIENT_LEVEL_TO_SURFACE_WINDS
        hol_b = _B_holland_1980(t_gradient_winds[1:], t_env[1:], t_cen[1:])
        v_ang_norm = _stat_holland_1980(d_centr[1:], t_rad[1:], hol_b,
                                        t_env[1:], t_cen[1:], t_lat[1:],
                                        close_centr_msk[1:])
        v_ang_norm *= GRADIENT_LEVEL_TO_SURFACE_WINDS
    elif model == MODEL_VANG['H08']:
        # this model doesn't use the recorded surface winds
        hol_b = _bs_holland_2008(v_trans_norm[1:], t_env[1:], t_cen[1:],
                                 prev_pres, t_lat[1:], t_tstep[1:])
        v_ang_norm = _stat_holland_1980(d_centr[1:], t_rad[1:], hol_b,
                                        t_env[1:], t_cen[1:], t_lat[1:],
                                        close_centr_msk[1:])
    elif model == MODEL_VANG['H10']:
        # this model doesn't use the recorded surface winds
        hol_b = _bs_holland_2008(v_trans_norm[1:], t_env[1:], t_cen[1:],
                                 prev_pres, t_lat[1:], t_tstep[1:])
        t_vmax = _v_max_s_holland_2008(t_env[1:], t_cen[1:], hol_b)
        hol_x = _x_holland_2010(d_centr[1:], t_rad[1:], t_vmax, hol_b,
                                close_centr_msk[1:])
        v_ang_norm = _stat_holland_2010(d_centr[1:], t_vmax, t_rad[1:], hol_b,
                                        close_centr_msk[1:], hol_x)
    else:
        raise NotImplementedError

    # vectorial angular velocity
    hemisphere = 'N'
    if np.count_nonzero(t_lat < 0) > np.count_nonzero(t_lat > 0):
        hemisphere = 'S'
    v_ang_rotate = [1.0, -1.0] if hemisphere == 'N' else [-1.0, 1.0]
    v_ang_dir = np.array(v_ang_rotate)[..., :] * v_centr_normed[1:, :, ::-1]
    v_ang = np.zeros_like(v_ang_dir)
    v_ang[close_centr_msk[1:]] = v_ang_norm[close_centr_msk[1:], None] \
                                 * v_ang_dir[close_centr_msk[1:]]

    # Influence of translational speed decreases with distance from eye.
    # The "absorbing factor" is according to the following paper (see Fig. 7):
    #
    #   Mouton, F., & Nordbeck, O. (1999). Cyclone Database Manager. A tool
    #   for converting point data from cyclone observations into tracks and
    #   wind speed profiles in a GIS. UNED/GRID-Geneva.
    #   https://unepgrid.ch/en/resource/19B7D302
    #
    t_rad_bc = np.broadcast_arrays(t_rad[:, None], d_centr)[0]
    v_trans_corr = np.zeros_like(d_centr)
    v_trans_corr[close_centr_msk] = np.fmin(
        1, t_rad_bc[close_centr_msk] / d_centr[close_centr_msk])

    # add angular and corrected translational velocity vectors
    v_full = v_trans[1][1:, None, :] * v_trans_corr[1:, :, None] + v_ang
    v_full[np.isnan(v_full)] = 0

    windfields = np.zeros((npositions, nreachable, 2), dtype=np.float64)
    windfields[1:, :, :] = v_full
    [reachable_centr_idx] = track_centr_msk.nonzero()
    return windfields, reachable_centr_idx
Exemple #6
0
    def set_from_tracks(self,
                        tracks,
                        centroids=None,
                        description='',
                        model='H08',
                        ignore_distance_to_coast=False,
                        store_windfields=False,
                        metric="equirect"):
        """
        Clear and fill with windfields from specified tracks.

        This function sets the `TropCyclone.intensity` attribute to contain, for each centroid,
        the maximum wind speed (1-minute sustained winds at 10 meters above ground) experienced
        over the whole period of each TC event in m/s. The wind speed is set to 0 if it doesn't
        exceed the threshold in `TropCyclone.intensity_thres`.

        The `TropCyclone.category` attribute is set to the value of the `category`-attribute
        of each of the given track data sets.

        The `TropCyclone.basin` attribute is set to the genesis basin for each event, which
        is the first value of the `basin`-variable in each of the given track data sets.

        Optionally, the time dependent, vectorial winds can be stored using the `store_windfields`
        function parameter (see below).

        Parameters
        ----------
        tracks : TCTracks
            Tracks of storm events.
        centroids : Centroids, optional
            Centroids where to model TC. Default: global centroids at 360 arc-seconds resolution.
        description : str, optional
            Description of the event set. Default: "".
        model : str, optional
            Parametric wind field model to use: one of "H1980" (the prominent Holland 1980 model),
            "H08" (Holland 1980 with b-value from Holland 2008), or "H10" (Holland et al. 2010).
            Default: "H08".
        ignore_distance_to_coast : boolean, optional
            If True, centroids far from coast are not ignored. Default: False.
        store_windfields : boolean, optional
            If True, the Hazard object gets a list `windfields` of sparse matrices. For each track,
            the full velocity vectors at each centroid and track position are stored in a sparse
            matrix of shape (npositions,  ncentroids * 2) that can be reshaped to a full ndarray
            of shape (npositions, ncentroids, 2). Default: False.
        metric : str, optional
            Specify an approximation method to use for earth distances:
            * "equirect": Distance according to sinusoidal projection. Fast, but inaccurate for
              large distances and high latitudes.
            * "geosphere": Exact spherical distance. Much more accurate at all distances, but slow.
            Default: "equirect".

        Raises
        ------
        ValueError
        """
        num_tracks = tracks.size
        if centroids is None:
            centroids = Centroids.from_base_grid(res_as=360, land=False)

        if not centroids.coord.size:
            centroids.set_meta_to_lat_lon()

        if ignore_distance_to_coast:
            # Select centroids with lat < 61
            coastal_idx = (np.abs(centroids.lat) < 61).nonzero()[0]
        else:
            # Select centroids which are inside INLAND_MAX_DIST_KM and lat < 61
            if not centroids.dist_coast.size:
                centroids.set_dist_coast()
            coastal_idx = ((centroids.dist_coast < INLAND_MAX_DIST_KM * 1000)
                           & (np.abs(centroids.lat) < 61)).nonzero()[0]

        # Restrict to coastal centroids within reach of any of the tracks
        t_lon_min, t_lat_min, t_lon_max, t_lat_max = tracks.get_bounds(
            deg_buffer=CENTR_NODE_MAX_DIST_DEG)
        t_mid_lon = 0.5 * (t_lon_min + t_lon_max)
        coastal_centroids = centroids.coord[coastal_idx]
        u_coord.lon_normalize(coastal_centroids[:, 1], center=t_mid_lon)
        coastal_idx = coastal_idx[((t_lon_min <= coastal_centroids[:, 1])
                                   & (coastal_centroids[:, 1] <= t_lon_max)
                                   & (t_lat_min <= coastal_centroids[:, 0])
                                   & (coastal_centroids[:, 0] <= t_lat_max))]

        LOGGER.info('Mapping %s tracks to %s coastal centroids.',
                    str(tracks.size), str(coastal_idx.size))
        if self.pool:
            chunksize = min(num_tracks // self.pool.ncpus, 1000)
            tc_haz = self.pool.map(self._tc_from_track,
                                   tracks.data,
                                   itertools.repeat(centroids, num_tracks),
                                   itertools.repeat(coastal_idx, num_tracks),
                                   itertools.repeat(model, num_tracks),
                                   itertools.repeat(store_windfields,
                                                    num_tracks),
                                   itertools.repeat(metric, num_tracks),
                                   chunksize=chunksize)
        else:
            last_perc = 0
            tc_haz = []
            for track in tracks.data:
                perc = 100 * len(tc_haz) / len(tracks.data)
                if perc - last_perc >= 10:
                    LOGGER.info("Progress: %d%%", perc)
                    last_perc = perc
                self.append(
                    self._tc_from_track(track,
                                        centroids,
                                        coastal_idx,
                                        model=model,
                                        store_windfields=store_windfields,
                                        metric=metric))
            if last_perc < 100:
                LOGGER.info("Progress: 100%")
        LOGGER.debug('Compute frequency.')
        self.frequency_from_tracks(tracks.data)
        self.tag.description = description
Exemple #7
0
    def set_from_tracks(self, tracks, centroids=None, description='',
                        model='H08', ignore_distance_to_coast=False,
                        store_windfields=False, metric="equirect"):
        """Clear and fill with windfields from specified tracks.

        Parameters
        ----------
        tracks : TCTracks
            Tracks of storm events.
        centroids : Centroids, optional
            Centroids where to model TC. Default: global centroids at 360 arc-seconds resolution.
        description : str, optional
            Description of the event set. Default: "".
        model : str, optional
            Model to compute gust. Currently only 'H08' is supported for the one implemented in
            `_stat_holland` according to Greg Holland. Default: "H08".
        ignore_distance_to_coast : boolean, optional
            If True, centroids far from coast are not ignored. Default: False.
        store_windfields : boolean, optional
            If True, the Hazard object gets a list `windfields` of sparse matrices. For each track,
            the full velocity vectors at each centroid and track position are stored in a sparse
            matrix of shape (npositions,  ncentroids * 2) that can be reshaped to a full ndarray
            of shape (npositions, ncentroids, 2). Default: False.
        metric : str, optional
            Specify an approximation method to use for earth distances:
            * "equirect": Distance according to sinusoidal projection. Fast, but inaccurate for
              large distances and high latitudes.
            * "geosphere": Exact spherical distance. Much more accurate at all distances, but slow.
            Default: "equirect".

        Raises
        ------
        ValueError
        """
        num_tracks = tracks.size
        if centroids is None:
            centroids = Centroids.from_base_grid(res_as=360, land=False)

        if not centroids.coord.size:
            centroids.set_meta_to_lat_lon()

        if ignore_distance_to_coast:
            # Select centroids with lat < 61
            coastal_idx = (np.abs(centroids.lat) < 61).nonzero()[0]
        else:
            # Select centroids which are inside INLAND_MAX_DIST_KM and lat < 61
            if not centroids.dist_coast.size:
                centroids.set_dist_coast()
            coastal_idx = ((centroids.dist_coast < INLAND_MAX_DIST_KM * 1000)
                           & (np.abs(centroids.lat) < 61)).nonzero()[0]

        # Restrict to coastal centroids within reach of any of the tracks
        t_lon_min, t_lat_min, t_lon_max, t_lat_max = tracks.get_bounds(
            deg_buffer=CENTR_NODE_MAX_DIST_DEG)
        t_mid_lon = 0.5 * (t_lon_min + t_lon_max)
        coastal_centroids = centroids.coord[coastal_idx]
        u_coord.lon_normalize(coastal_centroids[:, 1], center=t_mid_lon)
        coastal_idx = coastal_idx[((t_lon_min <= coastal_centroids[:, 1])
                                   & (coastal_centroids[:, 1] <= t_lon_max)
                                   & (t_lat_min <= coastal_centroids[:, 0])
                                   & (coastal_centroids[:, 0] <= t_lat_max))]

        LOGGER.info('Mapping %s tracks to %s coastal centroids.', str(tracks.size),
                    str(coastal_idx.size))
        if self.pool:
            chunksize = min(num_tracks // self.pool.ncpus, 1000)
            tc_haz = self.pool.map(
                self._tc_from_track, tracks.data,
                itertools.repeat(centroids, num_tracks),
                itertools.repeat(coastal_idx, num_tracks),
                itertools.repeat(model, num_tracks),
                itertools.repeat(store_windfields, num_tracks),
                itertools.repeat(metric, num_tracks),
                chunksize=chunksize)
        else:
            last_perc = 0
            tc_haz = []
            for track in tracks.data:
                perc = 100 * len(tc_haz) / len(tracks.data)
                if perc - last_perc >= 10:
                    LOGGER.info("Progress: %d%%", perc)
                    last_perc = perc
                tc_haz.append(
                    self._tc_from_track(track, centroids, coastal_idx,
                                        model=model, store_windfields=store_windfields,
                                        metric=metric))
            if last_perc < 100:
                LOGGER.info("Progress: 100%")
        LOGGER.debug('Append events.')
        self.concatenate(tc_haz)
        LOGGER.debug('Compute frequency.')
        self.frequency_from_tracks(tracks.data)
        self.tag.description = description