Skip to content

Commit 200a4eb

Browse files
implement Emanuel method and add origin arg
1 parent 6e497ac commit 200a4eb

File tree

2 files changed

+56
-67
lines changed

2 files changed

+56
-67
lines changed

climada/hazard/tc_tracks.py

Lines changed: 35 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,11 @@ class Basin_bounds_storm(Enum):
286286
)
287287

288288

289+
BASINS_GDF = gpd.GeoDataFrame(
290+
{"basin": b, "geometry": b.value} for b in Basin_bounds_storm
291+
)
292+
293+
289294
class TCTracks:
290295
"""Contains tropical cyclone tracks.
291296
@@ -415,79 +420,58 @@ def subset(self, filterdict):
415420

416421
return out
417422

418-
BASINS_GDF = gpd.GeoDataFrame(
419-
{"basin": b, "geometry": b.value} for b in Basin_bounds_storm
420-
)
423+
def get_basins(track):
421424

422-
def get_basins(
423-
track,
424-
): # this is the method I had in mind for 1. and I'd guess it could be a performance boost
425425
track_coordinates = gpd.GeoDataFrame(
426426
geometry=gpd.points_from_xy(track.lon, track.lat)
427427
)
428428
return track_coordinates.sjoin(BASINS_GDF, how="left", predicate="within").basin
429429

430-
def subset_by_basin(self):
430+
def subset_by_basin(self, origin: bool = False):
431431
"""Subset all tropical cyclones tracks by basin.
432432
433-
This function iterates through the tropical cyclones in the dataset and assigns each cyclone
434-
to a basin based on its geographical location. It checks whether the cyclone's position
435-
(latitude and longitude) lies within the boundaries of any of the predefined basins and
436-
then groups the cyclones into separate categories for each basin. The resulting dictionary
437-
maps each basin's name to a list of tropical cyclones that fall within it.
433+
This function collects for every basin the tracks that crossed them. The resulting dictionary
434+
maps each basin's name to a list of tropical cyclones tracks that intersected them.
438435
439436
Parameters
440437
----------
441438
self : TCTtracks object
442-
The object instance containing the tropical cyclone data (`self.data`) to be processed.
439+
The object instance containing the tropical cyclone data (`self.data`).
440+
origin : bool
441+
Either True or False. If True, the outputs basin will contain only the tracks that originated there.
442+
If False, every track that crossed a basin will be present in the basin.
443443
444444
Returns
445445
-------
446446
dict_tc_basins : dict
447447
A dictionary where the keys are basin names (e.g., "NA", "EP", "WP", etc.) and the
448-
values are instances of the `TCTracks` class containing the tropical cyclones that
449-
belong to each basin.
450-
451-
Example:
452-
--------
453-
>>> tc = TCTracks.from_ibtracks("")
454-
>>> tc_basins = tc.subset_by_basin()
455-
>>> tc_basins["NA"] # to access tracks in the North Atlantic
448+
values are instances of the `TCTracks` class: effectively all tracks that intersected or originated
449+
in each basin, depending on the argument "origin".
450+
tracks_outside_basin : list
451+
A list of all tracks that did not cross any basin.
456452
457453
"""
458454

459-
# Initialize a defaultdict to store lists for each basin
460-
basins_dict = defaultdict(list)
455+
basins_dict: dict = defaultdict(list)
461456
tracks_outside_basin: list = []
462-
# Iterate over each tropical cyclone
463-
for track in self.data:
464-
lat, lon = track.lat.values[0], track.lon.values[0]
465-
origin_point = Point(lon, lat)
466-
point_in_basin = False
467-
468-
# Find the basin that contains the point
469-
for basin in Basin_bounds_storm:
470-
if basin.value.contains(origin_point):
471-
basins_dict[basin.name].append(track)
472-
point_in_basin = True
473-
break
474-
475-
if not point_in_basin:
476-
tracks_outside_basin.append(track.id_no)
477-
478-
if tracks_outside_basin:
479-
warnings.warn(
480-
f"A total of {len(tracks_outside_basin)} tracks did not originate in any of the \n"
481-
f"defined basins. IDs of the tracks outside the basins: {tracks_outside_basin}",
482-
UserWarning,
483-
)
484457

485-
# Create a dictionary with TCTracks for each basin
486-
dict_tc_basins = {
487-
basin_name: TCTracks(tc_list) for basin_name, tc_list in basins_dict.items()
488-
}
489-
490-
return dict_tc_basins
458+
for track in self.data:
459+
# if only origin basin is of interest (origin = True)
460+
if origin:
461+
origin_basin = TCTracks.get_basins(track)[0]
462+
if origin_basin:
463+
basins_dict[origin_basin.name].append(track)
464+
else:
465+
tracks_outside_basin.append(track)
466+
else: # if every basin crossed is of interest (origin = False)
467+
touched = TCTracks.get_basins(track).dropna().drop_duplicates()
468+
if touched.size:
469+
for basin in touched:
470+
basins_dict[basin.name].append(track)
471+
else:
472+
tracks_outside_basin.append(track)
473+
474+
return basins_dict, tracks_outside_basin
491475

492476
def subset_year(
493477
self,

climada/hazard/test/test_tc_tracks.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -878,23 +878,28 @@ def test_subset_basin(self):
878878
"""test the correct splitting of a single tc object into different tc objets by basin"""
879879

880880
tc_test = tc.TCTracks.from_simulations_emanuel(TEST_TRACK_EMANUEL)
881-
tc_test.data[-1].lat[0] = 0 # modify lat of track to exclude it from a basin
882881

883-
with self.assertWarnsRegex(
884-
UserWarning,
885-
"A total of 1 tracks did not originate in any of the \n"
886-
"defined basins. IDs of the tracks outside the basins: \[4\]",
887-
):
888-
dict_basins = tc_test.subset_by_basin()
889-
890-
self.assertEqual(dict_basins["EP"].data[0].lat[0].item(), 12.553)
891-
self.assertEqual(dict_basins["EP"].data[0].lon[0].item(), -109.445)
892-
self.assertEqual(dict_basins["SI"].data[0].lat[0].item(), -8.699)
893-
self.assertEqual(dict_basins["SI"].data[0].lon[0].item(), 52.761)
894-
self.assertEqual(dict_basins["WP"].data[0].lat[0].item(), 8.502)
895-
self.assertEqual(dict_basins["WP"].data[0].lon[0].item(), 164.909)
896-
self.assertEqual(dict_basins["WP"].data[1].lat[0].item(), 16.234)
897-
self.assertEqual(dict_basins["WP"].data[1].lon[0].item(), 116.424)
882+
# all basin
883+
dict_basins, tracks_outside_basin = tc_test.subset_by_basin(origin=False)
884+
885+
self.assertEqual(len(dict_basins["NA"]), 1)
886+
self.assertEqual(len(dict_basins["EP"]), 2)
887+
self.assertEqual(len(dict_basins["WP"]), 2)
888+
self.assertEqual(len(dict_basins["NI"]), 0)
889+
self.assertEqual(len(dict_basins["SI"]), 1)
890+
self.assertEqual(len(dict_basins["SP"]), 0)
891+
self.assertEqual(len(tracks_outside_basin), 0)
892+
893+
# only origin basin
894+
dict_basins, tracks_outside_basin = tc_test.subset_by_basin(origin=True)
895+
896+
self.assertEqual(len(dict_basins["NA"]), 0)
897+
self.assertEqual(len(dict_basins["EP"]), 2)
898+
self.assertEqual(len(dict_basins["WP"]), 2)
899+
self.assertEqual(len(dict_basins["NI"]), 0)
900+
self.assertEqual(len(dict_basins["SI"]), 1)
901+
self.assertEqual(len(dict_basins["SP"]), 0)
902+
self.assertEqual(len(tracks_outside_basin), 0)
898903

899904
def test_get_extent(self):
900905
"""Test extent/bounds attributes."""

0 commit comments

Comments
 (0)