-
Notifications
You must be signed in to change notification settings - Fork 161
Add tropical cyclones basin bounds #1031
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
9c9150b
a111937
f37212f
7e82b8f
030bcb8
73889b9
cadf062
5a5c128
6056e6b
6e497ac
200a4eb
a72876d
9ffa59c
53b5bd6
9ca2ce3
5695e89
1f10bbc
dc60e2f
26e02b0
734f968
795071b
f582249
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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 | ||
|
|
@@ -193,6 +195,97 @@ | |
| dataset using STORM. Scientific Data 7(1): 40.""" | ||
|
|
||
|
|
||
| class Basin(Enum): | ||
| """ | ||
| 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), | ||
| ] | ||
| ), | ||
| ] | ||
| ) | ||
|
|
||
|
|
||
| class TCTracks: | ||
| """Contains tropical cyclone tracks. | ||
|
|
||
|
|
@@ -322,6 +415,68 @@ def subset(self, filterdict): | |
|
|
||
| return out | ||
|
|
||
| def subset_by_basin(self): | ||
| """Subset all tropical cyclones tracks by basin. | ||
|
|
||
| This function iterates through the tropical cyclones in the dataset and assigns each cyclone | ||
| to a basin based on its geographical location. It checks whether the cyclone's position | ||
| (latitude and longitude) lies within the boundaries of any of the predefined basins and | ||
| then groups the cyclones into separate categories for each basin. The resulting dictionary | ||
| maps each basin's name to a list of tropical cyclones that fall within it. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| self : TCTtracks object | ||
| The object instance containing the tropical cyclone data (`self.data`) to be processed. | ||
|
|
||
| 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 containing the tropical cyclones that | ||
| belong to each basin. | ||
|
|
||
| Example: | ||
| -------- | ||
| >>> tc = TCTracks.from_ibtracks("") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as an example this is suboptimal: |
||
| >>> tc_basins = tc.split_by_basin() | ||
| >>> tc_basins["NA"] # to access tracks in the North Atlantic | ||
|
|
||
| """ | ||
|
|
||
| # Initialize a defaultdict to store lists for each basin | ||
| basins_dict = defaultdict(list) | ||
| tracks_outside_basin: list = [] | ||
| # Iterate over each tropical cyclone | ||
| for track in self.data: | ||
| lat, lon = track.lat.values[0], track.lon.values[0] | ||
| origin_point = Point(lon, lat) | ||
| point_in_basin = False | ||
|
|
||
| # Find the basin that contains the point | ||
| for basin in Basin: | ||
| if basin.value.contains(origin_point): | ||
| basins_dict[basin.name].append(track) | ||
| point_in_basin = True | ||
| break | ||
|
|
||
| if not point_in_basin: | ||
| tracks_outside_basin.append(track.id_no) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @NicolasColombi as to 1. and 2. BASINS_GDF = gpd.GeoDataFrame(
{'basin': b, 'geometry': b.value} for b in Basin
)
def get_basins(track): # this is the method I had in mind for 1. and I'd guess it could be a performance boost
track_coordinates = GeoDataFrame(
geometry=gpd.points_from_xy(track.lon, track.lat)
)
return track_coordinates.sjoin(BASINS_GDF , how='left', predicate='within').basin
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having that I'd write the track loop like this: for track in self.data:
touched = get_basins(track).dropna().drop_duplicates()
if touched.size:
for basin in touched:
basins_dict[basin].append(track)
else:
tracks_outside_basin.append(track)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @emanuel-schmid for the update, sorry for the (very) late reply. If I understand it correctly, if a track crosses multiple basin it will be present in the output dictionary in all the basins that it crossed, which is fine, just different than attributing the origin basin, as I had in mind. If we want only the origin, I guess we can still use your method but select only the first row of the gdf ? @chahank : Conceptually, do we want to assign basin to tracks based on the origin basin or all the basins they cross ?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no strong opinion, but I would say the second makes more sense as a default. Ideally, just allow for both.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok! I implemented Emanuel version and allowed the flexibility of choosing only the origin basin or not.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @chahank I will be on holidays for the next two week, I think this PR should be ready now. If you require additional changes I will be available from Sept.15, or if the changes are really small, I might carve out some time tomorrow. |
||
|
|
||
| if tracks_outside_basin: | ||
| warnings.warn( | ||
| f"A total of {len(tracks_outside_basin)} tracks did not originate in any of the \n" | ||
| f"defined basins. IDs of the tracks outside the basins: {tracks_outside_basin}", | ||
| UserWarning, | ||
| ) | ||
|
|
||
| # Create a dictionary with TCTracks for each basin | ||
| dict_tc_basins = { | ||
| basin_name: TCTracks(tc_list) for basin_name, tc_list in basins_dict.items() | ||
| } | ||
|
|
||
| return dict_tc_basins | ||
|
|
||
| def subset_year( | ||
| self, | ||
| start_date: tuple = (False, False, False), | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.