Skip to content

Commit 030bcb8

Browse files
make basin a data class, add function to split by basin
1 parent 7e82b8f commit 030bcb8

File tree

1 file changed

+172
-55
lines changed

1 file changed

+172
-55
lines changed

climada/hazard/tc_tracks.py

Lines changed: 172 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@
2828
import re
2929
import shutil
3030
import warnings
31+
from collections import defaultdict
32+
from dataclasses import dataclass
3133
from operator import itemgetter
3234
from pathlib import Path
33-
from typing import List, Optional
35+
from typing import Dict, List, Optional
3436

3537
# additional libraries
3638
import cartopy.crs as ccrs
@@ -167,60 +169,6 @@
167169
}
168170
"""Basin-specific default environmental pressure"""
169171

170-
BASINS_BOUNDS = {
171-
"NA": Polygon(
172-
[
173-
(-100, 19),
174-
(-94.21951983987083, 17.039584804350312),
175-
(-88.75211790888072, 14.837521327451947),
176-
(-84.96610530622198, 12.214318798718033),
177-
(-84.89823142225451, 12.181148019885352),
178-
(-82.59052306410497, 8.777858931465238),
179-
(-81.09730008320902, 8.358383265470449),
180-
(-79.50226644452471, 9.196860922133856),
181-
(-78.58597052442947, 9.213610839871123),
182-
(-77.02487377167459, 7.299350879751048),
183-
(-77.02487377167459, 5),
184-
(0, 5.0),
185-
(0, 60.0),
186-
(-100.0, 60.0),
187-
(-100, 19),
188-
]
189-
),
190-
"EP": Polygon(
191-
[
192-
(-180.0, 5.0),
193-
(-77.02487377167459, 5),
194-
(-77.02487377167459, 7.299350879751048),
195-
(-78.58597052442947, 9.213610839871123),
196-
(-79.50226644452471, 9.196860922133856),
197-
(-81.09730008320902, 8.358383265470449),
198-
(-82.59052306410497, 8.777858931465238),
199-
(-84.89823142225451, 12.181148019885352),
200-
(-84.96610530622198, 12.214318798718033),
201-
(-88.75211790888072, 14.837521327451947),
202-
(-94.21951983987083, 17.039584804350312),
203-
(-100, 19),
204-
(-100.0, 60.0),
205-
(-180.0, 60.0),
206-
(-180.0, 5.0),
207-
]
208-
),
209-
"WP": Polygon(
210-
[(100.0, 5.0), (180.0, 5.0), (180.0, 60.0), (100.0, 60.0), (100.0, 5.0)]
211-
),
212-
"NI": Polygon(
213-
[(30.0, 5.0), (100.0, 5.0), (100.0, 60.0), (30.0, 60.0), (30.0, 5.0)]
214-
),
215-
"SI": Polygon(
216-
[(10.0, -60.0), (135.0, -60.0), (135.0, -5.0), (10.0, -5.0), (10.0, -60.0)]
217-
),
218-
"SP": Polygon(
219-
[(135.0, -60.0), (240.0, -60.0), (240.0, -5.0), (135.0, -5.0), (135.0, -60.0)]
220-
),
221-
}
222-
""" Basins latitude and longitude bounds. """
223-
224172
EMANUEL_RMW_CORR_FILES = [
225173
"temp_ccsm420thcal.mat",
226174
"temp_ccsm4rcp85_full.mat",
@@ -247,6 +195,126 @@
247195
dataset using STORM. Scientific Data 7(1): 40."""
248196

249197

198+
@dataclass
199+
class Basin:
200+
"""
201+
Store tropical cyclones basin geographical extent.
202+
203+
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)
207+
208+
Attributes:
209+
----------
210+
*name : str
211+
The name of the tropical cyclone basin (e.g., "NA" for North Atlantic).
212+
*polygon : Polygon
213+
A shapely Polygon object that represents the geographical boundary of the basin.
214+
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`.
220+
"""
221+
222+
name: str
223+
polygon: Polygon
224+
225+
def contains(self, point: Point) -> bool:
226+
"""
227+
Checks if a given point is inside the basin.
228+
229+
Parameters
230+
----------
231+
point : Point
232+
A shapely Point object representing the geographical location (longitude, latitude).
233+
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+
}
316+
317+
250318
class TCTracks:
251319
"""Contains tropical cyclone tracks.
252320
@@ -376,6 +444,55 @@ def subset(self, filterdict):
376444

377445
return out
378446

447+
def split_by_basin(self):
448+
"""Subset all tropical cyclones tracks by basin.
449+
450+
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.
455+
456+
Parameters
457+
----------
458+
self : TCTtracks object
459+
The object instance containing the tropical cyclone data (`self.data`) to be processed.
460+
461+
Returns
462+
-------
463+
dict_tc_basins : dict
464+
A dictionary where the keys are basin names (e.g., "NA", "EP", "WP", etc.) and the values are instances
465+
of the `TCTracks` class containing the tropical cyclones that belong to each basin.
466+
467+
Example:
468+
--------
469+
>>> tc = TCTracks.from_ibtracks("")
470+
>>> tc_basins = tc.split_by_basin()
471+
>>> tc_basins["NA"] # to access tracks in the North Atlantic
472+
473+
"""
474+
475+
# Initialize a defaultdict to store lists for each basin
476+
basins_dict = defaultdict(list)
477+
478+
# Iterate over each tropical cyclone
479+
for tc in self.data:
480+
lat, lon = tc.lat.values[0], tc.lon.values[0]
481+
origin_point = Point(lon, lat)
482+
483+
# Find the basin that contains the point
484+
for basin in BASINS.values():
485+
if basin.contains(origin_point):
486+
basins_dict[basin.name].append(tc)
487+
break
488+
489+
# Now create a dictionary with TCTracks for each basin
490+
dict_tc_basins = {
491+
basin_name: TCTracks(tc_list) for basin_name, tc_list in basins_dict.items()
492+
}
493+
494+
return dict_tc_basins
495+
379496
def subset_year(
380497
self,
381498
start_date: tuple = (False, False, False),

0 commit comments

Comments
 (0)