|
28 | 28 | import re |
29 | 29 | import shutil |
30 | 30 | import warnings |
| 31 | +from collections import defaultdict |
| 32 | +from dataclasses import dataclass |
31 | 33 | from operator import itemgetter |
32 | 34 | from pathlib import Path |
33 | | -from typing import List, Optional |
| 35 | +from typing import Dict, List, Optional |
34 | 36 |
|
35 | 37 | # additional libraries |
36 | 38 | import cartopy.crs as ccrs |
|
167 | 169 | } |
168 | 170 | """Basin-specific default environmental pressure""" |
169 | 171 |
|
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 | | - |
224 | 172 | EMANUEL_RMW_CORR_FILES = [ |
225 | 173 | "temp_ccsm420thcal.mat", |
226 | 174 | "temp_ccsm4rcp85_full.mat", |
|
247 | 195 | dataset using STORM. Scientific Data 7(1): 40.""" |
248 | 196 |
|
249 | 197 |
|
| 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 | + |
250 | 318 | class TCTracks: |
251 | 319 | """Contains tropical cyclone tracks. |
252 | 320 |
|
@@ -376,6 +444,55 @@ def subset(self, filterdict): |
376 | 444 |
|
377 | 445 | return out |
378 | 446 |
|
| 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 | + |
379 | 496 | def subset_year( |
380 | 497 | self, |
381 | 498 | start_date: tuple = (False, False, False), |
|
0 commit comments