Skip to content

Commit 3546a0b

Browse files
Merge pull request #993 from CLIMADA-project/feature/from_netcdf_fast
Feature/from netcdf fast
2 parents 316f33b + 8139460 commit 3546a0b

File tree

5 files changed

+168
-8
lines changed

5 files changed

+168
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Code freeze date: YYYY-MM-DD
1212

1313
### Added
1414

15+
- `climada.hazard.tc_tracks.TCTracks.from_FAST` function, add Australia basin (AU) [#993](https://github.com/CLIMADA-project/climada_python/pull/993)
1516
- Add `osm-flex` package to CLIMADA core [#981](https://github.com/CLIMADA-project/climada_python/pull/981)
1617
- `doc.tutorial.climada_entity_Exposures_osm.ipynb` tutorial explaining how to use `osm-flex`with CLIMADA
1718
- `climada.util.coordinates.bounding_box_global` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980)

climada/hazard/tc_tracks.py

Lines changed: 120 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
"SI": 1005,
163163
"WP": 1005,
164164
"SP": 1004,
165+
"AU": 1004,
165166
}
166167
"""Basin-specific default environmental pressure"""
167168

@@ -1619,6 +1620,118 @@ def from_netcdf(cls, folder_name):
16191620
data.append(track)
16201621
return cls(data)
16211622

1623+
@classmethod
1624+
def from_FAST(cls, folder_name: str):
1625+
"""Create a new TCTracks object from NetCDF files generated by the FAST model, modifying
1626+
the xr.array structure to ensure compatibility with CLIMADA, and calculating the central
1627+
pressure and radius of maximum wind.
1628+
1629+
Model GitHub Repository: https://github.com/linjonathan/tropical_cyclone_risk?
1630+
tab=readme-ov-file
1631+
Model Publication: https://agupubs.onlinelibrary.wiley.com/doi/epdf/10.1029/2023MS003686
1632+
1633+
Parameters:
1634+
----------
1635+
folder_name : str
1636+
Folder name from where to read files.
1637+
storm_id : int
1638+
Number of the simulated storm
1639+
1640+
Returns:
1641+
-------
1642+
tracks : TCTracks
1643+
TCTracks object with tracks data from the given directory of NetCDF files.
1644+
"""
1645+
1646+
LOGGER.info("Reading %s files.", len(get_file_names(folder_name)))
1647+
data = []
1648+
for file in get_file_names(folder_name):
1649+
if Path(file).suffix != ".nc":
1650+
continue
1651+
with xr.open_dataset(file) as dataset:
1652+
for year in dataset.year:
1653+
for i in dataset.n_trk:
1654+
1655+
# Select track
1656+
track = dataset.sel(n_trk=i, year=year)
1657+
# chunk dataset at first NaN value
1658+
lon = track.lon_trks.data
1659+
last_valid_index = np.where(np.isfinite(lon))[0][-1]
1660+
track = track.isel(time=slice(0, last_valid_index + 1))
1661+
# Select lat, lon
1662+
lat = track.lat_trks.data
1663+
lon = track.lon_trks.data
1664+
# Convert lon from 0-360 to -180 - 180
1665+
lon = ((lon + 180) % 360) - 180
1666+
# Convert time to pandas Datetime "yyyy.mm.dd"
1667+
reference_time = (
1668+
f"{track.tc_years.item()}-{int(track.tc_month.item())}-01"
1669+
)
1670+
time = pd.to_datetime(
1671+
track.time.data, unit="s", origin=reference_time
1672+
).astype("datetime64[s]")
1673+
# Define variables
1674+
ms_to_kn = 1.943844
1675+
max_wind_kn = track.vmax_trks.data * ms_to_kn
1676+
env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()]
1677+
cen_pres = _estimate_pressure(
1678+
np.full(lat.shape, np.nan),
1679+
lat,
1680+
lon,
1681+
max_wind_kn,
1682+
)
1683+
1684+
data.append(
1685+
xr.Dataset(
1686+
{
1687+
"time_step": (
1688+
"time",
1689+
np.full(time.shape[0], track.time.data[1]),
1690+
),
1691+
"max_sustained_wind": (
1692+
"time",
1693+
track.vmax_trks.data,
1694+
),
1695+
"central_pressure": ("time", cen_pres),
1696+
"radius_max_wind": (
1697+
"time",
1698+
estimate_rmw(
1699+
np.full(lat.shape, np.nan), cen_pres
1700+
),
1701+
),
1702+
"environmental_pressure": (
1703+
"time",
1704+
np.full(time.shape[0], env_pressure),
1705+
),
1706+
"basin": (
1707+
"time",
1708+
np.full(
1709+
time.shape[0], track.tc_basins.data.item()
1710+
),
1711+
),
1712+
},
1713+
coords={
1714+
"time": ("time", time),
1715+
"lat": ("time", lat),
1716+
"lon": ("time", lon),
1717+
},
1718+
attrs={
1719+
"max_sustained_wind_unit": "m/s",
1720+
"central_pressure_unit": "hPa",
1721+
"name": f"storm_{track.n_trk.item()}",
1722+
"sid": track.n_trk.item(),
1723+
"orig_event_flag": True,
1724+
"data_provider": "FAST",
1725+
"id_no": track.n_trk.item(),
1726+
"category": set_category(
1727+
max_wind_kn, wind_unit="kn", saffir_scale=None
1728+
),
1729+
},
1730+
)
1731+
)
1732+
1733+
return cls(data)
1734+
16221735
def write_hdf5(self, file_name, complevel=5):
16231736
"""Write TC tracks in NetCDF4-compliant HDF5 format.
16241737
@@ -2665,20 +2778,20 @@ def ibtracs_fit_param(explained, explanatory, year_range=(1980, 2019), order=1):
26652778
return sm_results
26662779

26672780

2668-
def ibtracs_track_agency(ds_sel):
2781+
def ibtracs_track_agency(track):
26692782
"""Get preferred IBTrACS agency for each entry in the dataset.
26702783
26712784
Parameters
26722785
----------
2673-
ds_sel : xarray.Dataset
2786+
track : xarray.Dataset
26742787
Subselection of original IBTrACS NetCDF dataset.
26752788
26762789
Returns
26772790
-------
26782791
agency_pref : list of str
26792792
Names of IBTrACS agencies in order of preference.
26802793
track_agency_ix : xarray.DataArray of ints
2681-
For each entry in `ds_sel`, the agency to use, given as an index into `agency_pref`.
2794+
For each entry in `track`, the agency to use, given as an index into `agency_pref`.
26822795
"""
26832796
agency_pref = ["wmo"] + IBTRACS_AGENCIES
26842797
agency_map = {a.encode("utf-8"): i for i, a in enumerate(agency_pref)}
@@ -2687,11 +2800,11 @@ def ibtracs_track_agency(ds_sel):
26872800
)
26882801
agency_map[b""] = agency_map[b"wmo"]
26892802
agency_fun = lambda x: agency_map[x]
2690-
if "track_agency" not in ds_sel.data_vars.keys():
2691-
ds_sel["track_agency"] = ds_sel["wmo_agency"].where(
2692-
ds_sel["wmo_agency"] != b"", ds_sel["usa_agency"]
2803+
if "track_agency" not in track.data_vars.keys():
2804+
track["track_agency"] = track["wmo_agency"].where(
2805+
track["wmo_agency"] != b"", track["usa_agency"]
26932806
)
2694-
track_agency_ix = xr.apply_ufunc(agency_fun, ds_sel["track_agency"], vectorize=True)
2807+
track_agency_ix = xr.apply_ufunc(agency_fun, track["track_agency"], vectorize=True)
26952808
return agency_pref, track_agency_ix
26962809

26972810

60.3 KB
Binary file not shown.

climada/hazard/test/test_tc_tracks.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
TEST_TRACK_EMANUEL = DATA_DIR.joinpath("emanuel_test_tracks.mat")
4545
TEST_TRACK_EMANUEL_CORR = DATA_DIR.joinpath("temp_mpircp85cal_full.mat")
4646
TEST_TRACK_CHAZ = DATA_DIR.joinpath("chaz_test_tracks.nc")
47+
TEST_TRACK_FAST = DATA_DIR.joinpath("FAST_test_tracks.nc")
4748
TEST_TRACK_STORM = DATA_DIR.joinpath("storm_test_tracks.txt")
4849
TEST_TRACKS_ANTIMERIDIAN = DATA_DIR.joinpath("tracks-antimeridian")
4950
TEST_TRACKS_LEGACY_HDF5 = DATA_DIR.joinpath("tctracks_hdf5_legacy.nc")
@@ -631,6 +632,51 @@ def test_from_simulations_storm(self):
631632
tc_track = tc.TCTracks.from_simulations_storm(TEST_TRACK_STORM, years=[7])
632633
self.assertEqual(len(tc_track.data), 0)
633634

635+
def test_from_FAST(self):
636+
"""test the correct import of netcdf files from FAST model and the conversion to a
637+
different xr.array structure compatible with CLIMADA."""
638+
639+
tc_track = tc.TCTracks.from_FAST(TEST_TRACK_FAST)
640+
641+
expected_attributes = {
642+
"max_sustained_wind_unit": "m/s",
643+
"central_pressure_unit": "hPa",
644+
"name": "storm_0",
645+
"sid": 0,
646+
"orig_event_flag": True,
647+
"data_provider": "FAST",
648+
"id_no": 0,
649+
"category": 1,
650+
}
651+
652+
self.assertIsInstance(
653+
tc_track, tc.TCTracks, "tc_track is not an instance of TCTracks"
654+
)
655+
self.assertIsInstance(
656+
tc_track.data, list, "tc_track.data is not an instance of list"
657+
)
658+
self.assertIsInstance(
659+
tc_track.data[0],
660+
xr.Dataset,
661+
"tc_track.data[0] not an instance of xarray.Dataset",
662+
)
663+
self.assertEqual(len(tc_track.data), 5)
664+
self.assertEqual(tc_track.data[0].attrs, expected_attributes)
665+
self.assertEqual(list(tc_track.data[0].coords.keys()), ["time", "lat", "lon"])
666+
self.assertEqual(
667+
tc_track.data[0].time.values[0],
668+
np.datetime64("2025-09-01T00:00:00.000000000"),
669+
)
670+
self.assertEqual(tc_track.data[0].lat.values[0], 17.863591350508266)
671+
self.assertEqual(tc_track.data[0].lon.values[0], -71.76441758319629)
672+
self.assertEqual(len(tc_track.data[0].time), 35)
673+
self.assertEqual(tc_track.data[0].time_step[0], 10800)
674+
self.assertEqual(
675+
tc_track.data[0].max_sustained_wind.values[10], 24.71636959089841
676+
)
677+
self.assertEqual(tc_track.data[0].environmental_pressure.data[0], 1010)
678+
self.assertEqual(tc_track.data[0].basin[0], "NA")
679+
634680
def test_to_geodataframe_points(self):
635681
"""Conversion of TCTracks to GeoDataFrame using Points."""
636682
tc_track = tc.TCTracks.from_processed_ibtracs_csv(TEST_TRACK)

doc/tutorial/climada_hazard_TropCyclone.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1802,7 +1802,7 @@
18021802
"<a id='Part1.d'></a> \n",
18031803
"### d) Load TC tracks from other sources\n",
18041804
"\n",
1805-
"In addition to the [historical records of TCs (IBTrACS)](#Part1.a), the [probabilistic extension](#Part1.b) of these tracks, and the [ECMWF Forecast tracks](#Part1.c), CLIMADA also features functions to read in synthetic TC tracks from other sources. These include synthetic storm tracks from Kerry Emanuel's coupled statistical-dynamical model (Emanuel et al., 2006 as used in Geiger et al., 2016), synthetic storm tracks from a second coupled statistical-dynamical model (CHAZ) (as described in Lee et al., 2018), and synthetic storm tracks from a fully statistical model (STORM) Bloemendaal et al., 2020). However, these functions are partly under development and/or targeted at advanced users of CLIMADA in the context of very specific use cases. They are thus not covered in this tutorial."
1805+
"In addition to the [historical records of TCs (IBTrACS)](#Part1.a), the [probabilistic extension](#Part1.b) of these tracks, and the [ECMWF Forecast tracks](#Part1.c), CLIMADA also features functions to read in synthetic TC tracks from other sources. These include synthetic storm tracks from Kerry Emanuel's coupled statistical-dynamical model (Emanuel et al., 2006 as used in Geiger et al., 2016), from an open source derivative of Kerry Emanuel's model [FAST](https://github.com/linjonathan/tropical_cyclone_risk?tab=readme-ov-file), synthetic storm tracks from a second coupled statistical-dynamical model (CHAZ) (as described in Lee et al., 2018), and synthetic storm tracks from a fully statistical model (STORM) Bloemendaal et al., 2020). However, these functions are partly under development and/or targeted at advanced users of CLIMADA in the context of very specific use cases. They are thus not covered in this tutorial."
18061806
]
18071807
},
18081808
{

0 commit comments

Comments
 (0)