@@ -2089,6 +2089,164 @@ def _one_interp_data(track, time_step_h, land_geom=None):
20892089 track_land_params (track_int , land_geom )
20902090 return track_int
20912091
2092+ def compute_track_density (
2093+ self ,
2094+ res : int = 5 ,
2095+ bounds : tuple = None ,
2096+ genesis : bool = False ,
2097+ filter_tracks : bool = True ,
2098+ wind_min : float = None ,
2099+ wind_max : float = None ,
2100+ ) -> tuple [np .ndarray , np .ndarray , np .ndarray ]:
2101+ """Compute tropical cyclone track density. Before using this function,
2102+ apply the same temporal resolution to all tracks by calling :py:meth:`equal_timestep` on the
2103+ TCTrack object. Due to the computational cost of the this function, it is not recommended to
2104+ use a grid resolution higher tha 0.1°. First, this function creates 2D bins of the specified
2105+ resolution (e.g. 1° x 1°). Second, since tracks are not lines but a series of points, it counts
2106+ the number of points per bin. Lastly, it returns the absolute count per bin.
2107+ To plot the output of this function use :py:meth:`plot_track_density`.
2108+
2109+ Parameters:
2110+ ----------
2111+ tc_track: TCTracks object
2112+ track object containing a list of all tracks
2113+ res: int (optional), default: 5°
2114+ resolution in degrees of the grid bins in which the density will be computed
2115+ bounds: tuple, (optional) dafault: None
2116+ (lon_min, lat_min, lon_max, lat_max) latitude and longitude bounds.
2117+ genesis: bool, (optional) default = False
2118+ If true the function computes the track density of only the genesis location of tracks
2119+ filter_tracks: bool (optional) default: True
2120+ If True the track density is computed as the number of different tracks crossing a grid
2121+ cell. If False, the track density takes into account how long the track stayed in each
2122+ grid cell. Hence slower tracks increase the density if the parameter is set to False.
2123+ wind_min: float (optional), default: None
2124+ Minimum wind speed above which to select tracks (inclusive).
2125+ wind_max: float (optional), default: None
2126+ Maximal wind speed below which to select tracks (exclusive if wind_min is also provided,
2127+ otherwise inclusive).
2128+ Returns:
2129+ -------
2130+ hist_count: np.ndarray
2131+ 2D matrix containing the the absolute count per grid cell of track point or the normalized
2132+ number of track points, depending on the norm parameter.
2133+ lat_bins: np.ndarray
2134+ latitude bins in which the point were counted
2135+ lon_bins: np.ndarray
2136+ longitude bins in which the point were counted
2137+
2138+ Example:
2139+ --------
2140+ >>> tc_tracks = TCTrack.from_ibtracs_netcdf("path_to_file")
2141+ >>> tc_tracks.equal_timestep(time_step_h = 1)
2142+ >>> hist_count, _, _ = compute_track_density(tc_track = tc_tracks, res = 1)
2143+ >>> ax = plot_track_density(hist_count)
2144+
2145+ """
2146+
2147+ limit_ratio : float = (
2148+ 1.12 * 1.1
2149+ ) # record tc speed 112km/h -> 1.12°/h + 10% margin
2150+ time_value : float = self .data [0 ].time_step [0 ].values .astype (float )
2151+
2152+ if time_value > (res / limit_ratio ):
2153+ warnings .warn (
2154+ "The time step is too big for the current resolution. For the desired resolution, \n "
2155+ f"apply a time step of { res / limit_ratio } h."
2156+ )
2157+ elif res < 0.1 :
2158+ warnings .warn (
2159+ "The resolution is too high. The computation might take several minutes \n "
2160+ "to hours. Consider using a resolution below 0.1°."
2161+ )
2162+
2163+ # define grid resolution and bounds for density computation
2164+ if not bounds :
2165+ lon_min , lat_min , lon_max , lat_max = - 180 , - 90 , 180 , 90
2166+ else :
2167+ lon_min , lat_min , lon_max , lat_max = (
2168+ bounds [0 ],
2169+ bounds [1 ],
2170+ bounds [2 ],
2171+ bounds [3 ],
2172+ )
2173+
2174+ lat_bins : np .ndarray = np .linspace (lat_min , lat_max , int (180 / res ))
2175+ lon_bins : np .ndarray = np .linspace (lon_min , lon_max , int (360 / res ))
2176+
2177+ # compute 2D density
2178+ if genesis :
2179+ hist_count = _compute_genesis_density (
2180+ tc_track = self , lat_bins = lat_bins , lon_bins = lon_bins
2181+ )
2182+ else :
2183+ hist_count = np .zeros ((len (lat_bins ) - 1 , len (lon_bins ) - 1 ))
2184+ for track in tqdm (self .data , desc = "Processing Tracks" ):
2185+
2186+ # select according to wind speed
2187+ wind_speed = track .max_sustained_wind .values
2188+ if wind_min and wind_max :
2189+ index = np .where (
2190+ (wind_speed >= wind_min ) & (wind_speed < wind_max )
2191+ )[0 ]
2192+ elif wind_min and not wind_max :
2193+ index = np .where (wind_speed >= wind_min )[0 ]
2194+ elif wind_max and not wind_min :
2195+ index = np .where (wind_speed <= wind_max )[0 ]
2196+ else :
2197+ index = slice (None ) # select all the track
2198+
2199+ # compute 2D density
2200+ hist_new , _ , _ = np .histogram2d (
2201+ track .lat .values [index ],
2202+ track .lon .values [index ],
2203+ bins = [lat_bins , lon_bins ],
2204+ density = False ,
2205+ )
2206+ if filter_tracks :
2207+ hist_new [hist_new > 1 ] = 1
2208+
2209+ hist_count += hist_new
2210+
2211+ return hist_count , lat_bins , lon_bins
2212+
2213+
2214+ def _compute_genesis_density (
2215+ tc_track : TCTracks , lat_bins : np .ndarray , lon_bins : np .ndarray
2216+ ) -> np .ndarray :
2217+ """Compute the density of track genesis locations. This function works under the hood
2218+ of :py:meth:`compute_track_density`. If it is called with the parameter genesis = True,
2219+ the function return the number of genesis points per grid cell.
2220+
2221+ Parameters:
2222+ -----------
2223+
2224+ tc_track: TCT track object
2225+ TC track object containing a list of all tracks.
2226+ lat_bins: 1D np.array
2227+ array containg the latitude bins.
2228+ lon_bins: 1D np.array
2229+ array containg the longitude bins.
2230+
2231+ Returns:
2232+ --------
2233+ hist_count: 2D np.array
2234+ array containing the number of genesis points per grid cell
2235+ """
2236+ # Extract the first lat and lon from each dataset
2237+ first_lats = np .array ([ds .lat .values [0 ] for ds in tc_track .data ])
2238+ first_lons = np .array ([ds .lon .values [0 ] for ds in tc_track .data ])
2239+
2240+ # compute 2D density of genesis points
2241+ hist_count , _ , _ = np .histogram2d (
2242+ first_lats ,
2243+ first_lons ,
2244+ bins = [lat_bins , lon_bins ],
2245+ density = False ,
2246+ )
2247+
2248+ return hist_count
2249+
20922250
20932251def _raise_if_legacy_or_unknown_hdf5_format (file_name ):
20942252 """Raise an exception if the HDF5 format of the file is not supported
@@ -3094,196 +3252,3 @@ def _zlib_from_dataarray(data_var: xr.DataArray) -> bool:
30943252 if np .issubdtype (data_var .dtype , float ) or np .issubdtype (data_var .dtype , int ):
30953253 return True
30963254 return False
3097-
3098-
3099- def compute_track_density (
3100- tc_track : TCTracks ,
3101- res : int = 5 ,
3102- bounds : tuple = None ,
3103- genesis : bool = False ,
3104- norm : str = None ,
3105- filter_tracks : bool = True ,
3106- wind_min : float = None ,
3107- wind_max : float = None ,
3108- ) -> tuple [np .ndarray , np .ndarray , np .ndarray ]:
3109- """Compute absolute and normalized tropical cyclone track density. Before using this function,
3110- apply the same temporal resolution to all tracks by calling :py:meth:`equal_timestep` on the
3111- TCTrack object. Due to the computational cost of the this function, it is not recommended to
3112- use a grid resolution higher tha 0.1°. First, this function creates 2D bins of the specified
3113- resolution (e.g. 1° x 1°). Second, since tracks are not lines but a series of points, it counts
3114- the number of points per bin. Lastly, it returns the absolute or normalized count per bin.
3115- To plot the output of this function use :py:meth:`plot_track_density`.
3116-
3117- Parameters:
3118- ----------
3119- tc_track: TCTracks object
3120- track object containing a list of all tracks
3121- res: int (optional), default: 5°
3122- resolution in degrees of the grid bins in which the density will be computed
3123- bounds: tuple, (optional) dafault: None
3124- (lon_min, lat_min, lon_max, lat_max) latitude and longitude bounds.
3125- genesis: bool, (optional) default = False
3126- If true the function computes the track density of only the genesis location of tracks
3127- norm: str (optional), default: None
3128- If None the function returns the number of samples in each bin. If True, it normalize the
3129- bin count as specified: if norm = area -> normalize by gird cell area. If norm = sum ->
3130- normalize by the total sum across all bins.
3131- filter_tracks: bool (optional) default: True
3132- If True the track density is computed as the number of different tracks crossing a grid
3133- cell. If False, the track density takes into account how long the track stayed in each
3134- grid cell. Hence slower tracks increase the density if the parameter is set to False.
3135- wind_min: float (optional), default: None
3136- Minimum wind speed above which to select tracks (inclusive).
3137- wind_max: float (optional), default: None
3138- Maximal wind speed below which to select tracks (exclusive if wind_min is also provided,
3139- otherwise inclusive).
3140- Returns:
3141- -------
3142- hist_count: np.ndarray
3143- 2D matrix containing the the absolute count per grid cell of track point or the normalized
3144- number of track points, depending on the norm parameter.
3145- lat_bins: np.ndarray
3146- latitude bins in which the point were counted
3147- lon_bins: np.ndarray
3148- longitude bins in which the point were counted
3149-
3150- Example:
3151- --------
3152- >>> tc_tracks = TCTrack.from_ibtracs_netcdf("path_to_file")
3153- >>> tc_tracks.equal_timestep(time_step_h = 1)
3154- >>> hist_count, *_ = compute_track_density(tc_track = tc_tracks, res = 1)
3155- >>> ax = plot_track_density(hist_count)
3156-
3157- """
3158-
3159- limit_ratio : float = 1.12 * 1.1 # record tc speed 112km/h -> 1.12°/h + 10% margin
3160- time_value : float = tc_track .data [0 ].time_step [0 ].values .astype (float )
3161-
3162- if time_value > (res / limit_ratio ):
3163- warnings .warn (
3164- "The time step is too big for the current resolution. For the desired resolution, \n "
3165- f"apply a time step of { res / limit_ratio } h."
3166- )
3167- elif res < 0.1 :
3168- warnings .warn (
3169- "The resolution is too high. The computation might take several minutes \n "
3170- "to hours. Consider using a resolution below 0.1°."
3171- )
3172-
3173- # define grid resolution and bounds for density computation
3174- if not bounds :
3175- lon_min , lat_min , lon_max , lat_max = - 180 , - 90 , 180 , 90
3176- else :
3177- lon_min , lat_min , lon_max , lat_max = bounds [0 ], bounds [1 ], bounds [2 ], bounds [3 ]
3178-
3179- lat_bins : np .ndarray = np .linspace (lat_min , lat_max , int (180 / res ))
3180- lon_bins : np .ndarray = np .linspace (lon_min , lon_max , int (360 / res ))
3181-
3182- # compute 2D density
3183- if genesis :
3184- hist_count = compute_genesis_density (
3185- tc_track = tc_track , lat_bins = lat_bins , lon_bins = lon_bins
3186- )
3187- else :
3188- hist_count = np .zeros ((len (lat_bins ) - 1 , len (lon_bins ) - 1 ))
3189- for track in tqdm (tc_track .data , desc = "Processing Tracks" ):
3190-
3191- # select according to wind speed
3192- wind_speed = track .max_sustained_wind .values
3193- if wind_min and wind_max :
3194- index = np .where ((wind_speed >= wind_min ) & (wind_speed < wind_max ))[0 ]
3195- elif wind_min and not wind_max :
3196- index = np .where (wind_speed >= wind_min )[0 ]
3197- elif wind_max and not wind_min :
3198- index = np .where (wind_speed <= wind_max )[0 ]
3199- else :
3200- index = slice (None ) # select all the track
3201-
3202- # compute 2D density
3203- hist_new , _ , _ = np .histogram2d (
3204- track .lat .values [index ],
3205- track .lon .values [index ],
3206- bins = [lat_bins , lon_bins ],
3207- density = False ,
3208- )
3209- if filter_tracks :
3210- hist_new [hist_new > 1 ] = 1
3211-
3212- hist_count += hist_new
3213-
3214- if norm :
3215- hist_count = normalize_hist (res = res , hist_count = hist_count , norm = norm )
3216-
3217- return hist_count , lat_bins , lon_bins
3218-
3219-
3220- def compute_genesis_density (
3221- tc_track : TCTracks , lat_bins : np .ndarray , lon_bins : np .ndarray
3222- ) -> np .ndarray :
3223- """Compute the density of track genesis locations. This function works under the hood
3224- of :py:meth:`compute_track_density`. If it is called with the parameter genesis = True,
3225- the function return the number of genesis points per grid cell.
3226-
3227- Parameters:
3228- -----------
3229-
3230- tc_track: TCT track object
3231- TC track object containing a list of all tracks.
3232- lat_bins: 1D np.array
3233- array containg the latitude bins.
3234- lon_bins: 1D np.array
3235- array containg the longitude bins.
3236-
3237- Returns:
3238- --------
3239- hist_count: 2D np.array
3240- array containing the number of genesis points per grid cell
3241- """
3242- # Extract the first lat and lon from each dataset
3243- first_lats = np .array ([ds .lat .values [0 ] for ds in tc_track .data ])
3244- first_lons = np .array ([ds .lon .values [0 ] for ds in tc_track .data ])
3245-
3246- # compute 2D density of genesis points
3247- hist_count , _ , _ = np .histogram2d (
3248- first_lats ,
3249- first_lons ,
3250- bins = [lat_bins , lon_bins ],
3251- density = False ,
3252- )
3253-
3254- return hist_count
3255-
3256-
3257- def normalize_hist (
3258- res : int ,
3259- hist_count : np .ndarray ,
3260- norm : str ,
3261- ) -> np .ndarray :
3262- """Normalize the number of points per grid cell by the grid cell area or by the total sum of
3263- the histogram count.
3264-
3265- Parameters:
3266- -----------
3267- res: float
3268- resolution of grid cells
3269- hist_count: 2D np.array
3270- Array containing the count of tracks or genesis locations
3271- norm: str
3272- if norm = area normalize by gird cell area, if norm = sum normalize by the total sum
3273- Returns:
3274- ---------
3275-
3276- """
3277-
3278- if norm == "area" :
3279- grid_area = u_coord .compute_grid_cell_area (res = res )
3280- norm_hist : np .ndarray = hist_count / grid_area
3281- elif norm == "sum" :
3282- norm_hist : np .ndarray = hist_count / hist_count .sum ()
3283- else :
3284- raise ValueError (
3285- "Invalid value for input parameter 'norm':\n "
3286- "it should be either 'area' or 'sum'"
3287- )
3288-
3289- return norm_hist
0 commit comments