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))
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
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
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