def test_dist_approx_pass(self): """Test approximate distance functions""" data = np.array([ # lat1, lon1, lat2, lon2, dist, dist_sph [45.5, -32.1, 14, 56, 7702.88906574, 8750.64119051], [45.5, 147.8, 14, -124, 7709.82781473, 8758.34146833], [45.5, 507.9, 14, -124, 7702.88906574, 8750.64119051], [45.5, -212.2, 14, -124, 7709.82781473, 8758.34146833], [-3, -130.1, 4, -30.5, 11079.7217421, 11087.0352544], ]) compute_dist = np.stack([ dist_approx(data[:, None, 0], data[:, None, 1], data[:, None, 2], data[:, None, 3], method="equirect")[:, 0, 0], dist_approx(data[:, None, 0], data[:, None, 1], data[:, None, 2], data[:, None, 3], method="geosphere")[:, 0, 0], ], axis=-1) self.assertEqual(compute_dist.shape[0], data.shape[0]) for d, cd in zip(data[:, 4:], compute_dist): self.assertAlmostEqual(d[0], cd[0]) self.assertAlmostEqual(d[1], cd[1]) for units, factor in zip(["radian", "degree", "km"], [np.radians(1.0), 1.0, ONE_LAT_KM]): factor /= ONE_LAT_KM compute_dist = np.stack([ dist_approx(data[:, None, 0], data[:, None, 1], data[:, None, 2], data[:, None, 3], method="equirect", units=units)[:, 0, 0], dist_approx(data[:, None, 0], data[:, None, 1], data[:, None, 2], data[:, None, 3], method="geosphere", units=units)[:, 0, 0], ], axis=-1) self.assertEqual(compute_dist.shape[0], data.shape[0]) for d, cd in zip(data[:, 4:], compute_dist): self.assertAlmostEqual(d[0] * factor, cd[0]) self.assertAlmostEqual(d[1] * factor, cd[1])
def test_dist_approx_log_pass(self): """Test log-functionality of approximate distance functions""" data = np.array([ # lat1, lon1, lat2, lon2, dist, dist_sph [0, 0, 0, 1, 111.12, 111.12], [-13, 179, 5, -179, 2011.84774049, 2012.30698122], ]) for i, method in enumerate(["equirect", "geosphere"]): for units, factor in zip(["radian", "degree", "km"], [np.radians(1.0), 1.0, ONE_LAT_KM]): factor /= ONE_LAT_KM dist, vec = dist_approx(data[:, None, 0], data[:, None, 1], data[:, None, 2], data[:, None, 3], log=True, method=method, units=units) dist, vec = dist[:, 0, 0], vec[:, 0, 0] np.testing.assert_allclose(np.linalg.norm(vec, axis=-1), dist) np.testing.assert_allclose(dist, data[:, 4 + i] * factor) # both points on equator (no change in latitude) self.assertAlmostEqual(vec[0, 0], 0) # longitude from 179 to -179 is positive (!) in lon-direction np.testing.assert_array_less(100, vec[1, :] / factor)
def test_dist_approx_pass(self): """Test approximate distance functions""" data = np.array([ # lat1, lon1, lat2, lon2, dist, dist_sph [45.5, -32.2, 14, 56, 7709.827814738594, 8758.34146833], [45.5, 147.8, 14, -124, 7709.827814738594, 8758.34146833], [45.5, 507.8, 14, -124, 7709.827814738594, 8758.34146833], [45.5, -212.2, 14, -124, 7709.827814738594, 8758.34146833], ]) compute_dist = np.stack([ dist_approx(data[:, None, 0], data[:, None, 1], data[:, None, 2], data[:, None, 3], method="equirect")[:, 0, 0], dist_approx(data[:, None, 0], data[:, None, 1], data[:, None, 2], data[:, None, 3], method="geosphere")[:, 0, 0], ], axis=-1) self.assertEqual(compute_dist.shape[0], data.shape[0]) for d, cd in zip(data[:, 4:], compute_dist): self.assertAlmostEqual(d[0], cd[0]) self.assertAlmostEqual(d[1], cd[1]) data = np.array([ # lat1, lon1, lat2, lon2, dist, dist_sph [0, 0, 0, 1, 111.12, 111.12], [-13, 179, 5, -179, 2011.84774049, 2012.30698122], ]) for i, method in enumerate(["equirect", "geosphere"]): dist, vec = dist_approx(data[:, None, 0], data[:, None, 1], data[:, None, 2], data[:, None, 3], log=True, method=method) dist, vec = dist[:, 0, 0], vec[:, 0, 0] self.assertTrue(np.allclose(np.linalg.norm(vec, axis=-1), dist)) self.assertTrue(np.allclose(dist, data[:, 4 + i])) # both points on equator (no change in latitude) self.assertAlmostEqual(vec[0, 0], 0) # longitude from 179 to -179 is positive (!) in lon-direction self.assertTrue(np.all(vec[1, :] > 100))
def _vtrans(t_lat, t_lon, t_tstep, metric="equirect"): """Translational vector and velocity at each track node. Parameters ---------- t_lat : np.array track latitudes t_lon : np.array track longitudes t_tstep : np.array track time steps 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 ------- v_trans_norm : np.array Same shape as input, the first velocity is always 0. v_trans : np.array Directional vectors of velocity. """ v_trans = np.zeros((t_lat.size, 2)) v_trans_norm = np.zeros((t_lat.size, )) norm, vec = u_coord.dist_approx(t_lat[:-1, None], t_lon[:-1, None], t_lat[1:, None], t_lon[1:, None], log=True, normalize=False, method=metric) v_trans[1:, :] = vec[:, 0, 0] v_trans[1:, :] *= KMH_TO_MS / t_tstep[1:, None] v_trans_norm[1:] = norm[:, 0, 0] v_trans_norm[1:] *= KMH_TO_MS / t_tstep[1:] # limit to 30 nautical miles per hour msk = (v_trans_norm > 30 * KN_TO_MS) fact = 30 * KN_TO_MS / v_trans_norm[msk] v_trans[msk, :] *= fact[:, None] v_trans_norm[msk] *= fact return v_trans_norm, v_trans
def _vtrans(t_lat, t_lon, t_tstep): """Translational vector and velocity at each track node. Parameters ---------- t_lat : np.array track latitudes t_lon : np.array track longitudes t_tstep : np.array track time steps Returns ------- v_trans_norm : np.array Same shape as input, the first velocity is always 0. v_trans : np.array Directional vectors of velocity. """ v_trans = np.zeros((t_lat.size, 2)) v_trans_norm = np.zeros((t_lat.size, )) norm, vec = dist_approx(t_lat[:-1, None], t_lon[:-1, None], t_lat[1:, None], t_lon[1:, None], log=True, method="geosphere") v_trans[1:, :] = vec[:, 0, 0] v_trans[1:, :] *= KMH_TO_MS / t_tstep[1:, None] v_trans_norm[1:] = norm[:, 0, 0] v_trans_norm[1:] *= KMH_TO_MS / t_tstep[1:] # limit to 30 nautical miles per hour msk = (v_trans_norm > 30 * KN_TO_MS) fact = 30 * KN_TO_MS / v_trans_norm[msk] v_trans[msk, :] *= fact[:, None] v_trans_norm[msk] *= fact return v_trans_norm, v_trans
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 compute_windfields(track, centroids, model): """Compute 1-minute sustained winds (in m/s) at 10 meters above ground Parameters: track (xr.Dataset): track infomation centroids (2d np.array): each row is a centroid [lat, lon] model (int): Holland model selection according to MODEL_VANG Returns: np.array """ # copies of track data 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' ] ] ncentroids = centroids.shape[0] npositions = t_lat.shape[0] windfields = np.zeros((npositions, ncentroids, 2)) if t_lon.size < 2: return windfields # never use longitudes at -180 degrees or below t_lon[t_lon <= -180] += 360 # only use longitudes above 180, if 180 degree border is crossed if t_lon.min() > 180: t_lon -= 360 # restrict to centroids in rectangular bounding box around track track_centr_msk = _close_centroids(t_lat, t_lon, centroids) track_centr_idx = track_centr_msk.nonzero()[0] track_centr = centroids[track_centr_msk] if track_centr.shape[0] == 0: return windfields # compute distances and vectors to all centroids d_centr, v_centr = [ ar[0] for ar in dist_approx(t_lat[None], t_lon[None], track_centr[None, :, 0], track_centr[None, :, 1], log=True, method="geosphere") ] # exclude centroids that are too far from or too close to the eye close_centr = (d_centr < CENTR_NODE_MAX_DIST_KM) & (d_centr > 1e-2) if not np.any(close_centr): return windfields v_centr_normed = np.zeros_like(v_centr) v_centr_normed[close_centr] = v_centr[close_centr] / d_centr[close_centr, 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) 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 if model == 0: hol_b = _bs_hol08(v_trans_norm[1:], t_env[1:], t_cen[1:], prev_pres, t_lat[1:], t_tstep[1:]) else: raise NotImplementedError # derive angular velocity v_ang_norm = _stat_holland(d_centr[1:], t_rad[1:], hol_b, t_env[1:], t_cen[1:], t_lat[1:], close_centr[1:]) 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[1:]] = v_ang_norm[close_centr[1:], None] \ * v_ang_dir[close_centr[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] = np.fmin( 1, t_rad_bc[close_centr] / d_centr[close_centr]) # 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[1:, track_centr_idx, :] = v_full return windfields