2929import shutil
3030import warnings
3131from collections import defaultdict
32- from dataclasses import dataclass
33- from operator import itemgetter
32+ from enum import Enum
3433from pathlib import Path
35- from typing import Dict , List , Optional
34+ from typing import List , Optional
3635
3736# additional libraries
3837import cartopy .crs as ccrs
5352from matplotlib .colors import BoundaryNorm , ListedColormap
5453from matplotlib .lines import Line2D
5554from shapely .geometry import LineString , MultiLineString , Point , Polygon
55+ from shapely .ops import unary_union
5656from sklearn .metrics import DistanceMetric
5757
5858import climada .hazard .tc_tracks_synth
195195dataset using STORM. Scientific Data 7(1): 40."""
196196
197197
198- @dataclass
199- class Basin :
198+ class Basin (Enum ):
200199 """
201200 Store tropical cyclones basin geographical extent.
202-
203201 The boundaries of the basin are represented as a polygon (using the `shapely` Polygon object)
204- and follows the definition of the STORM dataset.
205- This class allows checking if a given geographical point (latitude, longitude)
206- lies inside the basin (e.g. the orgin location of a track)
202+ and follows the definition of the STORM dataset. Important note: tropical cyclone boundaries
203+ may vary bewteen datasets. The following boundaries follows the STORM definition:
204+ https://www.nature.com/articles/s41597-020-0381-2
207205
208206 Attributes:
209207 ----------
@@ -212,107 +210,80 @@ class Basin:
212210 *polygon : Polygon
213211 A shapely Polygon object that represents the geographical boundary of the basin.
214212
215- Methods:
216- -------
217- contains()
218- Returns `True` if the given point (latitude and longitude) is inside the basin's boundary,
219- otherwise returns `False`.
220213 """
221214
222- name : str
223- polygon : Polygon
215+ NA = Polygon (
216+ [
217+ (- 100 , 19 ),
218+ (- 94.21951983987083 , 17.039584804350312 ),
219+ (- 88.75211790888072 , 14.837521327451947 ),
220+ (- 84.96610530622198 , 12.214318798718033 ),
221+ (- 84.89823142225451 , 12.181148019885352 ),
222+ (- 82.59052306410497 , 8.777858931465238 ),
223+ (- 81.09730008320902 , 8.358383265470449 ),
224+ (- 79.50226644452471 , 9.196860922133856 ),
225+ (- 78.58597052442947 , 9.213610839871123 ),
226+ (- 77.02487377167459 , 7.299350879751048 ),
227+ (- 77.02487377167459 , 5 ),
228+ (0.0 , 5.0 ),
229+ (0.0 , 60.0 ),
230+ (- 100.0 , 60.0 ),
231+ (- 100 , 19 ),
232+ ]
233+ )
224234
225- def contains (self , point : Point ) -> bool :
226- """
227- Checks if a given point is inside the basin.
235+ EP = Polygon (
236+ [
237+ (- 180.0 , 5.0 ),
238+ (- 77.02487377167459 , 5 ),
239+ (- 77.02487377167459 , 7.299350879751048 ),
240+ (- 78.58597052442947 , 9.213610839871123 ),
241+ (- 79.50226644452471 , 9.196860922133856 ),
242+ (- 81.09730008320902 , 8.358383265470449 ),
243+ (- 82.59052306410497 , 8.777858931465238 ),
244+ (- 84.89823142225451 , 12.181148019885352 ),
245+ (- 84.96610530622198 , 12.214318798718033 ),
246+ (- 88.75211790888072 , 14.837521327451947 ),
247+ (- 94.21951983987083 , 17.039584804350312 ),
248+ (- 100 , 19 ),
249+ (- 100.0 , 60.0 ),
250+ (- 180.0 , 60.0 ),
251+ (- 180.0 , 5.0 ),
252+ ]
253+ )
228254
229- Parameters
230- ----------
231- point : Point
232- A shapely Point object representing the geographical location (longitude, latitude).
255+ WP = Polygon (
256+ [(100.0 , 5.0 ), (180.0 , 5.0 ), (180.0 , 60.0 ), (100.0 , 60.0 ), (100.0 , 5.0 )]
257+ )
233258
234- Returns
235- -------
236- bool
237- `True` if the point is inside the basin, `False` otherwise.
238- """
239- return self .polygon .contains (point )
240-
241-
242- BASINS : Dict [str , Basin ] = {
243- "NA" : Basin (
244- "NA" ,
245- Polygon (
246- [
247- (- 100 , 19 ),
248- (- 94.21951983987083 , 17.039584804350312 ),
249- (- 88.75211790888072 , 14.837521327451947 ),
250- (- 84.96610530622198 , 12.214318798718033 ),
251- (- 84.89823142225451 , 12.181148019885352 ),
252- (- 82.59052306410497 , 8.777858931465238 ),
253- (- 81.09730008320902 , 8.358383265470449 ),
254- (- 79.50226644452471 , 9.196860922133856 ),
255- (- 78.58597052442947 , 9.213610839871123 ),
256- (- 77.02487377167459 , 7.299350879751048 ),
257- (- 77.02487377167459 , 5 ),
258- (0 , 5.0 ),
259- (0 , 60.0 ),
260- (- 100.0 , 60.0 ),
261- (- 100 , 19 ),
262- ]
263- ),
264- ),
265- "EP" : Basin (
266- "EP" ,
267- Polygon (
268- [
269- (- 180.0 , 5.0 ),
270- (- 77.02487377167459 , 5 ),
271- (- 77.02487377167459 , 7.299350879751048 ),
272- (- 78.58597052442947 , 9.213610839871123 ),
273- (- 79.50226644452471 , 9.196860922133856 ),
274- (- 81.09730008320902 , 8.358383265470449 ),
275- (- 82.59052306410497 , 8.777858931465238 ),
276- (- 84.89823142225451 , 12.181148019885352 ),
277- (- 84.96610530622198 , 12.214318798718033 ),
278- (- 88.75211790888072 , 14.837521327451947 ),
279- (- 94.21951983987083 , 17.039584804350312 ),
280- (- 100 , 19 ),
281- (- 100.0 , 60.0 ),
282- (- 180.0 , 60.0 ),
283- (- 180.0 , 5.0 ),
284- ]
285- ),
286- ),
287- "WP" : Basin (
288- "WP" ,
289- Polygon (
290- [(100.0 , 5.0 ), (180.0 , 5.0 ), (180.0 , 60.0 ), (100.0 , 60.0 ), (100.0 , 5.0 )]
291- ),
292- ),
293- "NI" : Basin (
294- "NI" ,
295- Polygon ([(30.0 , 5.0 ), (100.0 , 5.0 ), (100.0 , 60.0 ), (30.0 , 60.0 ), (30.0 , 5.0 )]),
296- ),
297- "SI" : Basin (
298- "SI" ,
299- Polygon (
300- [(10.0 , - 60.0 ), (135.0 , - 60.0 ), (135.0 , - 5.0 ), (10.0 , - 5.0 ), (10.0 , - 60.0 )]
301- ),
302- ),
303- "SP" : Basin (
304- "SP" ,
305- Polygon (
306- [
307- (135.0 , - 60.0 ),
308- (240.0 , - 60.0 ),
309- (240.0 , - 5.0 ),
310- (135.0 , - 5.0 ),
311- (135.0 , - 60.0 ),
312- ]
313- ),
314- ),
315- }
259+ NI = Polygon ([(30.0 , 5.0 ), (100.0 , 5.0 ), (100.0 , 60.0 ), (30.0 , 60.0 ), (30.0 , 5.0 )])
260+
261+ SI = Polygon (
262+ [(10.0 , - 60.0 ), (135.0 , - 60.0 ), (135.0 , - 5.0 ), (10.0 , - 5.0 ), (10.0 , - 60.0 )]
263+ )
264+
265+ SP = unary_union (
266+ [
267+ Polygon ( # west side of antimeridian
268+ [
269+ (135.0 , - 60.0 ),
270+ (180.0 , - 60.0 ),
271+ (180.0 , - 5.0 ),
272+ (135.0 , - 5.0 ),
273+ (135.0 , - 60.0 ),
274+ ]
275+ ),
276+ Polygon ( # east side
277+ [
278+ (- 180.0 , - 60.0 ),
279+ (- 120.0 , - 60.0 ),
280+ (- 120.0 , - 5.0 ),
281+ (- 180.0 , - 5.0 ),
282+ (- 180.0 , - 60.0 ),
283+ ]
284+ ),
285+ ]
286+ )
316287
317288
318289class TCTracks :
@@ -444,14 +415,14 @@ def subset(self, filterdict):
444415
445416 return out
446417
447- def split_by_basin (self ):
418+ def subset_by_basin (self ):
448419 """Subset all tropical cyclones tracks by basin.
449420
450421 This function iterates through the tropical cyclones in the dataset and assigns each cyclone
451- to a basin based on its geographical location. It checks whether the cyclone's position (latitude
452- and longitude) lies within the boundaries of any of the predefined basins and then groups the cyclones
453- into separate categories for each basin. The resulting dictionary maps each basin's name to a list of
454- tropical cyclones that fall within it.
422+ to a basin based on its geographical location. It checks whether the cyclone's position
423+ (latitude and longitude) lies within the boundaries of any of the predefined basins and
424+ then groups the cyclones into separate categories for each basin. The resulting dictionary
425+ maps each basin's name to a list of tropical cyclones that fall within it.
455426
456427 Parameters
457428 ----------
@@ -474,19 +445,31 @@ def split_by_basin(self):
474445
475446 # Initialize a defaultdict to store lists for each basin
476447 basins_dict = defaultdict (list )
477-
448+ tracks_outside_basin : list = []
478449 # Iterate over each tropical cyclone
479450 for tc in self .data :
480451 lat , lon = tc .lat .values [0 ], tc .lon .values [0 ]
481452 origin_point = Point (lon , lat )
453+ point_in_basin = False
482454
483455 # Find the basin that contains the point
484- for basin in BASINS . values () :
485- if basin .contains (origin_point ):
456+ for basin in Basin :
457+ if basin .value . contains (origin_point ):
486458 basins_dict [basin .name ].append (tc )
459+ point_in_basin = True
487460 break
488461
489- # Now create a dictionary with TCTracks for each basin
462+ if not point_in_basin :
463+ tracks_outside_basin .append (tc .id_no )
464+
465+ if tracks_outside_basin :
466+ warnings .warn (
467+ f"A total of { len (tracks_outside_basin )} tracks did not originate in any of the "
468+ f"defined basins. IDs of the tracks outside the basins: { tracks_outside_basin } " ,
469+ UserWarning ,
470+ )
471+
472+ # Create a dictionary with TCTracks for each basin
490473 dict_tc_basins = {
491474 basin_name : TCTracks (tc_list ) for basin_name , tc_list in basins_dict .items ()
492475 }
0 commit comments