Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9c9150b
add bounds
NicolasColombi Mar 23, 2025
a111937
update CHANGELOG
NicolasColombi Mar 23, 2025
f37212f
modify NA
NicolasColombi Mar 23, 2025
7e82b8f
modify NI
NicolasColombi Mar 24, 2025
030bcb8
make basin a data class, add function to split by basin
NicolasColombi Mar 24, 2025
73889b9
Merge branch 'develop' into feature/basins-bounds
NicolasColombi Mar 24, 2025
cadf062
convert data cls to enum cls
NicolasColombi Mar 25, 2025
5a5c128
Merge branch 'develop' into feature/basins-bounds
NicolasColombi Apr 8, 2025
6056e6b
add test and fix pylits
NicolasColombi Apr 8, 2025
6e497ac
rename enum class
NicolasColombi Apr 22, 2025
200a4eb
implement Emanuel method and add origin arg
NicolasColombi Aug 21, 2025
a72876d
Merge branch 'develop' into feature/basins-bounds
NicolasColombi Aug 21, 2025
9ffa59c
add docstrings, return TCTtracks, move basin GDF
NicolasColombi Sep 19, 2025
53b5bd6
Merge branch 'develop' into feature/basins-bounds
NicolasColombi Sep 19, 2025
9ca2ce3
update changelog
NicolasColombi Sep 19, 2025
5695e89
Update climada/hazard/tc_tracks.py
NicolasColombi Sep 23, 2025
1f10bbc
Update CHANGELOG.md
NicolasColombi Sep 23, 2025
dc60e2f
Update climada/hazard/tc_tracks.py
NicolasColombi Sep 23, 2025
26e02b0
Update climada/hazard/tc_tracks.py
NicolasColombi Sep 23, 2025
734f968
Update climada/hazard/tc_tracks.py
NicolasColombi Sep 23, 2025
795071b
update test and fix lon format bug
NicolasColombi Sep 23, 2025
f582249
Merge branch 'develop' into feature/basins-bounds
NicolasColombi Sep 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Removed:

### Added

- `climada.hazard.tc_tracks.BASINS_BOUNDS` dictionary [#1031](https://github.com/CLIMADA-project/climada_python/pull/1031)
- `climada.hazard.tc_tracks.TCTracks.subset_years` function [#1023](https://github.com/CLIMADA-project/climada_python/pull/1023)
- `climada.hazard.tc_tracks.TCTracks.from_FAST` function, add Australia basin (AU) [#993](https://github.com/CLIMADA-project/climada_python/pull/993)
Comment thread
chahank marked this conversation as resolved.
Outdated
- Add `osm-flex` package to CLIMADA core [#981](https://github.com/CLIMADA-project/climada_python/pull/981)
Expand Down
155 changes: 153 additions & 2 deletions climada/hazard/tc_tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
import re
import shutil
import warnings
from operator import itemgetter
from collections import defaultdict
from enum import Enum
from pathlib import Path
from typing import List, Optional

Expand All @@ -50,7 +51,8 @@
from matplotlib.collections import LineCollection
from matplotlib.colors import BoundaryNorm, ListedColormap
from matplotlib.lines import Line2D
from shapely.geometry import LineString, MultiLineString, Point
from shapely.geometry import LineString, MultiLineString, Point, Polygon
from shapely.ops import unary_union
from sklearn.metrics import DistanceMetric

import climada.hazard.tc_tracks_synth
Expand Down Expand Up @@ -193,7 +195,103 @@
dataset using STORM. Scientific Data 7(1): 40."""


class Basin_bounds_storm(Enum):

Check warning on line 198 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

invalid-name

LOW: Class name "Basin_bounds_storm" doesn't conform to '[A-Z_][a-zA-Z0-9]+$' pattern
Raw output
Used when the name doesn't match the regular expression associated to its type(constant, variable, class...).
Comment thread
NicolasColombi marked this conversation as resolved.
Outdated
"""
Store tropical cyclones basin geographical extent.
The boundaries of the basin are represented as a polygon (using the `shapely` Polygon object)
and follows the definition of the STORM dataset. Important note: tropical cyclone boundaries
may vary bewteen datasets. The following boundaries follows the STORM definition:
https://www.nature.com/articles/s41597-020-0381-2

Attributes:
----------
*name : str
The name of the tropical cyclone basin (e.g., "NA" for North Atlantic).
*polygon : Polygon
A shapely Polygon object that represents the geographical boundary of the basin.

"""

NA = Polygon(
[
(-100, 19),
(-94.21951983987083, 17.039584804350312),
(-88.75211790888072, 14.837521327451947),
(-84.96610530622198, 12.214318798718033),
(-84.89823142225451, 12.181148019885352),
(-82.59052306410497, 8.777858931465238),
(-81.09730008320902, 8.358383265470449),
(-79.50226644452471, 9.196860922133856),
(-78.58597052442947, 9.213610839871123),
(-77.02487377167459, 7.299350879751048),
(-77.02487377167459, 5),
(0.0, 5.0),
(0.0, 60.0),
(-100.0, 60.0),
(-100, 19),
]
)

EP = Polygon(
[
(-180.0, 5.0),
(-77.02487377167459, 5),
(-77.02487377167459, 7.299350879751048),
(-78.58597052442947, 9.213610839871123),
(-79.50226644452471, 9.196860922133856),
(-81.09730008320902, 8.358383265470449),
(-82.59052306410497, 8.777858931465238),
(-84.89823142225451, 12.181148019885352),
(-84.96610530622198, 12.214318798718033),
(-88.75211790888072, 14.837521327451947),
(-94.21951983987083, 17.039584804350312),
(-100, 19),
(-100.0, 60.0),
(-180.0, 60.0),
(-180.0, 5.0),
]
)

WP = Polygon(
[(100.0, 5.0), (180.0, 5.0), (180.0, 60.0), (100.0, 60.0), (100.0, 5.0)]
)

NI = Polygon([(30.0, 5.0), (100.0, 5.0), (100.0, 60.0), (30.0, 60.0), (30.0, 5.0)])

SI = Polygon(
[(10.0, -60.0), (135.0, -60.0), (135.0, -5.0), (10.0, -5.0), (10.0, -60.0)]
)

SP = unary_union(
[
Polygon( # west side of antimeridian
[
(135.0, -60.0),
(180.0, -60.0),
(180.0, -5.0),
(135.0, -5.0),
(135.0, -60.0),
]
),
Polygon( # east side
[
(-180.0, -60.0),
(-120.0, -60.0),
(-120.0, -5.0),
(-180.0, -5.0),
(-180.0, -60.0),
]
),
]
)


BASINS_GDF = gpd.GeoDataFrame(
{"basin": b, "geometry": b.value} for b in Basin_bounds_storm
)

@chahank chahank Aug 28, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This beats the purpose of the enum class a bit. Please do without it if possible (I know this was proposed by @emanuel-schmid , but this is just a very minor change and allows for future flexibility). This should be part of the method get_basins. The latter should also have a parameter source or similar, which at the moment can only have the value STORM. The method subset_by_basin should also have that.

@NicolasColombi NicolasColombi Sep 19, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure to understand: do you want to remove this or move it underget_basin ? can you clarify ? for the moment, I moved it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think the naming is not well chosen because it is only one way to define basins, but let's keep it as is for the moment. Thanks!



class TCTracks:

Check warning on line 294 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

too-many-public-methods

LOW: Too many public methods (36/20)
Raw output
Used when class has too many public methods, try to reduce this to get asimpler (and so easier to use) class.
"""Contains tropical cyclone tracks.

Attributes
Expand Down Expand Up @@ -322,7 +420,60 @@

return out

def get_basins(track):

Check warning on line 423 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

missing-function-docstring

LOW: Missing function or method docstring
Raw output
no description found

Check warning on line 423 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

no-self-argument

HIGH: Method 'get_basins' should have "self" as first argument
Raw output
Used when a method has an attribute different the "self" as first argument.This is considered as an error since this is a so common convention that youshouldn't break it!
Comment thread
chahank marked this conversation as resolved.

track_coordinates = gpd.GeoDataFrame(
geometry=gpd.points_from_xy(track.lon, track.lat)

Check warning on line 426 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

no-member

HIGH: Instance of 'TCTracks' has no 'lon' member
Raw output
Used when a variable is accessed for an unexistent member.

Check warning on line 426 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

no-member

HIGH: Instance of 'TCTracks' has no 'lat' member
Raw output
Used when a variable is accessed for an unexistent member.
)
return track_coordinates.sjoin(BASINS_GDF, how="left", predicate="within").basin

def subset_by_basin(self, origin: bool = False):
"""Subset all tropical cyclones tracks by basin.

This function collects for every basin the tracks that crossed them. The resulting dictionary

Check warning on line 433 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (101/100)
Raw output
Used when a line is longer than a given number of characters.
maps each basin's name to a list of tropical cyclones tracks that intersected them.

Parameters
----------
self : TCTtracks object
The object instance containing the tropical cyclone data (`self.data`).
origin : bool
Either True or False. If True, the outputs basin will contain only the tracks that originated there.

Check warning on line 441 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (112/100)
Raw output
Used when a line is longer than a given number of characters.
If False, every track that crossed a basin will be present in the basin.

Returns
-------
dict_tc_basins : dict
A dictionary where the keys are basin names (e.g., "NA", "EP", "WP", etc.) and the
values are instances of the `TCTracks` class: effectively all tracks that intersected or originated

Check warning on line 448 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

line-too-long

LOW: Line too long (111/100)
Raw output
Used when a line is longer than a given number of characters.
in each basin, depending on the argument "origin".
Comment thread
chahank marked this conversation as resolved.
tracks_outside_basin : list
A list of all tracks that did not cross any basin.

"""

basins_dict: dict = defaultdict(list)
tracks_outside_basin: list = []

for track in self.data:
# if only origin basin is of interest (origin = True)
if origin:
origin_basin = TCTracks.get_basins(track)[0]
if origin_basin:
basins_dict[origin_basin.name].append(track)
else:
tracks_outside_basin.append(track)

Check warning on line 465 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Code Coverage

Not covered line

Line 465 is not covered by tests
else: # if every basin crossed is of interest (origin = False)
touched = TCTracks.get_basins(track).dropna().drop_duplicates()
if touched.size:
for basin in touched:
basins_dict[basin.name].append(track)
else:
tracks_outside_basin.append(track)

Check warning on line 472 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Code Coverage

Not covered line

Line 472 is not covered by tests

return basins_dict, tracks_outside_basin

def subset_year(

Check warning on line 476 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

too-complex

LOW: 'subset_year' is too complex. The McCabe rating is 11
Raw output
no description found

Check warning on line 476 in climada/hazard/tc_tracks.py

View check run for this annotation

Jenkins - WCR / Pylint

too-many-locals

LOW: Too many local variables (20/15)
Raw output
Used when a function or method has too many local variables.
self,
start_date: tuple = (False, False, False),
end_date: tuple = (False, False, False),
Expand Down
27 changes: 27 additions & 0 deletions climada/hazard/test/test_tc_tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,33 @@ def test_subset_years(self):
):
tc_test.subset_year((2100, False, False), (2150, False, False))

def test_subset_basin(self):
"""test the correct splitting of a single tc object into different tc objets by basin"""

tc_test = tc.TCTracks.from_simulations_emanuel(TEST_TRACK_EMANUEL)

# all basin
dict_basins, tracks_outside_basin = tc_test.subset_by_basin(origin=False)

self.assertEqual(len(dict_basins["NA"]), 1)
self.assertEqual(len(dict_basins["EP"]), 2)
self.assertEqual(len(dict_basins["WP"]), 2)
self.assertEqual(len(dict_basins["NI"]), 0)
self.assertEqual(len(dict_basins["SI"]), 1)
self.assertEqual(len(dict_basins["SP"]), 0)
self.assertEqual(len(tracks_outside_basin), 0)

# only origin basin
dict_basins, tracks_outside_basin = tc_test.subset_by_basin(origin=True)

self.assertEqual(len(dict_basins["NA"]), 0)
self.assertEqual(len(dict_basins["EP"]), 2)
self.assertEqual(len(dict_basins["WP"]), 2)
self.assertEqual(len(dict_basins["NI"]), 0)
self.assertEqual(len(dict_basins["SI"]), 1)
self.assertEqual(len(dict_basins["SP"]), 0)
self.assertEqual(len(tracks_outside_basin), 0)

Comment thread
chahank marked this conversation as resolved.
def test_get_extent(self):
"""Test extent/bounds attributes."""
storms = ["1988169N14259", "2002073S16161", "2002143S07157"]
Expand Down
Loading