|
28 | 28 | import re |
29 | 29 | import shutil |
30 | 30 | import warnings |
| 31 | + |
| 32 | +from collections import defaultdict |
| 33 | +from enum import Enum |
31 | 34 | from pathlib import Path |
32 | 35 | from typing import List, Optional |
33 | 36 |
|
|
50 | 53 | from matplotlib.collections import LineCollection |
51 | 54 | from matplotlib.colors import BoundaryNorm, ListedColormap |
52 | 55 | from matplotlib.lines import Line2D |
53 | | -from shapely.geometry import LineString, MultiLineString, Point |
| 56 | +from shapely.geometry import LineString, MultiLineString, Point, Polygon |
| 57 | +from shapely.ops import unary_union |
54 | 58 | from sklearn.metrics import DistanceMetric |
55 | 59 | from tqdm import tqdm |
56 | 60 |
|
|
194 | 198 | dataset using STORM. Scientific Data 7(1): 40.""" |
195 | 199 |
|
196 | 200 |
|
| 201 | +class BasinBoundsStorm(Enum): |
| 202 | + """ |
| 203 | + Store tropical cyclones basin geographical extent. |
| 204 | + The boundaries of the basin are represented as a polygon (using the `shapely` Polygon object) |
| 205 | + and follows the definition of the STORM dataset. Important note: tropical cyclone boundaries |
| 206 | + may vary bewteen datasets. The following boundaries follows the STORM definition: |
| 207 | + https://www.nature.com/articles/s41597-020-0381-2 |
| 208 | +
|
| 209 | + Attributes: |
| 210 | + ---------- |
| 211 | + *name : str |
| 212 | + The name of the tropical cyclone basin (e.g., "NA" for North Atlantic). |
| 213 | + *polygon : Polygon |
| 214 | + A shapely Polygon object that represents the geographical boundary of the basin. |
| 215 | +
|
| 216 | + """ |
| 217 | + |
| 218 | + NA = Polygon( |
| 219 | + [ |
| 220 | + (-100, 19), |
| 221 | + (-94.21951983987083, 17.039584804350312), |
| 222 | + (-88.75211790888072, 14.837521327451947), |
| 223 | + (-84.96610530622198, 12.214318798718033), |
| 224 | + (-84.89823142225451, 12.181148019885352), |
| 225 | + (-82.59052306410497, 8.777858931465238), |
| 226 | + (-81.09730008320902, 8.358383265470449), |
| 227 | + (-79.50226644452471, 9.196860922133856), |
| 228 | + (-78.58597052442947, 9.213610839871123), |
| 229 | + (-77.02487377167459, 7.299350879751048), |
| 230 | + (-77.02487377167459, 5), |
| 231 | + (0.0, 5.0), |
| 232 | + (0.0, 60.0), |
| 233 | + (-100.0, 60.0), |
| 234 | + (-100, 19), |
| 235 | + ] |
| 236 | + ) |
| 237 | + |
| 238 | + EP = Polygon( |
| 239 | + [ |
| 240 | + (-180.0, 5.0), |
| 241 | + (-77.02487377167459, 5), |
| 242 | + (-77.02487377167459, 7.299350879751048), |
| 243 | + (-78.58597052442947, 9.213610839871123), |
| 244 | + (-79.50226644452471, 9.196860922133856), |
| 245 | + (-81.09730008320902, 8.358383265470449), |
| 246 | + (-82.59052306410497, 8.777858931465238), |
| 247 | + (-84.89823142225451, 12.181148019885352), |
| 248 | + (-84.96610530622198, 12.214318798718033), |
| 249 | + (-88.75211790888072, 14.837521327451947), |
| 250 | + (-94.21951983987083, 17.039584804350312), |
| 251 | + (-100, 19), |
| 252 | + (-100.0, 60.0), |
| 253 | + (-180.0, 60.0), |
| 254 | + (-180.0, 5.0), |
| 255 | + ] |
| 256 | + ) |
| 257 | + |
| 258 | + WP = Polygon( |
| 259 | + [(100.0, 5.0), (180.0, 5.0), (180.0, 60.0), (100.0, 60.0), (100.0, 5.0)] |
| 260 | + ) |
| 261 | + |
| 262 | + NI = Polygon([(30.0, 5.0), (100.0, 5.0), (100.0, 60.0), (30.0, 60.0), (30.0, 5.0)]) |
| 263 | + |
| 264 | + SI = Polygon( |
| 265 | + [(10.0, -60.0), (135.0, -60.0), (135.0, -5.0), (10.0, -5.0), (10.0, -60.0)] |
| 266 | + ) |
| 267 | + |
| 268 | + SP = unary_union( |
| 269 | + [ |
| 270 | + Polygon( # west side of antimeridian |
| 271 | + [ |
| 272 | + (135.0, -60.0), |
| 273 | + (180.0, -60.0), |
| 274 | + (180.0, -5.0), |
| 275 | + (135.0, -5.0), |
| 276 | + (135.0, -60.0), |
| 277 | + ] |
| 278 | + ), |
| 279 | + Polygon( # east side |
| 280 | + [ |
| 281 | + (-180.0, -60.0), |
| 282 | + (-120.0, -60.0), |
| 283 | + (-120.0, -5.0), |
| 284 | + (-180.0, -5.0), |
| 285 | + (-180.0, -60.0), |
| 286 | + ] |
| 287 | + ), |
| 288 | + ] |
| 289 | + ) |
| 290 | + |
| 291 | + |
197 | 292 | class TCTracks: |
198 | 293 | """Contains tropical cyclone tracks. |
199 | 294 |
|
@@ -323,6 +418,96 @@ def subset(self, filterdict): |
323 | 418 |
|
324 | 419 | return out |
325 | 420 |
|
| 421 | + def get_basins(track): |
| 422 | + """Identify the tropical-cyclone basins crossed by a single track. |
| 423 | +
|
| 424 | + Provides the basin name for every point along the track. |
| 425 | + The basins are defined according to the STORM definition: |
| 426 | + https://www.nature.com/articles/s41597-020-0381-2 |
| 427 | + The names are: |
| 428 | + WP: West Pacific |
| 429 | + NA: North Atlantic |
| 430 | + NI: North Indian |
| 431 | + SP: South Pacific |
| 432 | + SI: South Indian |
| 433 | + EP: East Pacific |
| 434 | +
|
| 435 | + Parameters |
| 436 | + ---------- |
| 437 | + track : xarray.Dataset |
| 438 | + Tropical cyclone track |
| 439 | + Returns |
| 440 | + ------- |
| 441 | + pandas.Series |
| 442 | + A Series of basin identifiers (e.g., "NA", "EP", "WP", …), one for each |
| 443 | + track point. Points that fall outside any basin have a value of ``NaN``. |
| 444 | + The index matches the index of the input track coordinates.""" |
| 445 | + |
| 446 | + basins_gdf = gpd.GeoDataFrame( |
| 447 | + {"basin": b, "geometry": b.value} for b in BasinBoundsStorm |
| 448 | + ) |
| 449 | + |
| 450 | + # convert 0-360 to -180 +180 longitude |
| 451 | + lon = u_coord.lon_normalize(track.lon) |
| 452 | + |
| 453 | + track_coordinates = gpd.GeoDataFrame( |
| 454 | + geometry=gpd.points_from_xy(lon, track.lat) |
| 455 | + ) |
| 456 | + return track_coordinates.sjoin(basins_gdf, how="left", predicate="within").basin |
| 457 | + |
| 458 | + def subset_by_basin(self, origin: bool = False): |
| 459 | + """Subset all tropical cyclones tracks by basin. |
| 460 | +
|
| 461 | + This function collects for every basin the tracks that crossed them. The resulting dictionary |
| 462 | + maps each basin's name to a list of tropical cyclones tracks that intersected them. |
| 463 | +
|
| 464 | + Parameters |
| 465 | + ---------- |
| 466 | + self : TCTtracks object |
| 467 | + The object instance containing the tropical cyclone data (`self.data`). |
| 468 | + origin : bool |
| 469 | + Either True or False. If True, the outputs basin will contain only the tracks that originated there. |
| 470 | + If False, every track that crossed a basin will be present in the basin. |
| 471 | +
|
| 472 | + Returns |
| 473 | + ------- |
| 474 | + dict_tc_basins : dict |
| 475 | + A dictionary where the keys are basin names (e.g., "NA", "EP", "WP", etc.) and the |
| 476 | + values are instances of the `TCTracks` class: effectively all tracks that intersected or originated |
| 477 | + in each basin, depending on the argument "origin". |
| 478 | + tracks_outside_basin : list |
| 479 | + A list of all tracks that did not cross any basin. |
| 480 | +
|
| 481 | + """ |
| 482 | + |
| 483 | + basins_dict: dict = defaultdict(list) |
| 484 | + tracks_outside_basin: list = [] |
| 485 | + |
| 486 | + for track in self.data: |
| 487 | + # if only origin basin is of interest (origin = True) |
| 488 | + if origin: |
| 489 | + origin_basin = TCTracks.get_basins(track)[0] |
| 490 | + if not isinstance(origin_basin, float): # nan are evaluated as floats |
| 491 | + basins_dict[origin_basin.name].append(track) |
| 492 | + else: |
| 493 | + tracks_outside_basin.append(track) |
| 494 | + else: # if every basin crossed is of interest (origin = False) |
| 495 | + touched = TCTracks.get_basins(track).dropna().drop_duplicates() |
| 496 | + if touched.size: |
| 497 | + for basin in touched: |
| 498 | + basins_dict[basin.name].append(track) |
| 499 | + else: |
| 500 | + tracks_outside_basin.append(track) |
| 501 | + |
| 502 | + # return TCTracks objetcs |
| 503 | + for basin in BasinBoundsStorm: |
| 504 | + if not basins_dict[basin.name]: |
| 505 | + basins_dict[basin.name] = TCTracks([]) |
| 506 | + else: |
| 507 | + basins_dict[basin.name] = TCTracks(basins_dict[basin.name]) |
| 508 | + |
| 509 | + return basins_dict, TCTracks(tracks_outside_basin) |
| 510 | + |
326 | 511 | def subset_year( |
327 | 512 | self, |
328 | 513 | start_date: tuple = (False, False, False), |
|
0 commit comments