Skip to content

Commit c1f8847

Browse files
add .nc test file and update code
1 parent 67a6a66 commit c1f8847

File tree

3 files changed

+86
-97
lines changed

3 files changed

+86
-97
lines changed

climada/hazard/tc_tracks.py

Lines changed: 64 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,9 @@
5252
from shapely.geometry import LineString, MultiLineString, Point
5353
from sklearn.metrics import DistanceMetric
5454

55-
import climada.hazard.tc_tracks_synth
5655
import climada.util.coordinates as u_coord
5756
import climada.util.plot as u_plot
58-
from climada.hazard import Centroids
57+
from climada.hazard import Centroids, tc_tracks, tc_tracks_synth
5958

6059
# climada dependencies
6160
from climada.util import ureg
@@ -1621,46 +1620,50 @@ def from_netcdf(cls, folder_name):
16211620
return cls(data)
16221621

16231622
@staticmethod
1624-
def compute_central_pressure(basin, v_max):
1625-
"""Compute central pressure of tropical cyclone given the maximal
1626-
wind speed of the storm. Method needed to load tracks from_netcdf_fast.
1627-
1628-
Holland, Greg. (2008). A Revised Hurricane Pressure Wind Model.
1629-
Monthly Weather Review - MON WEATHER REV. 136. 10.1175/2008MWR2395.1.
1623+
def define_tc_category_fast(max_sust_wind: np.array, units: str = "kn") -> int:
1624+
"""Define category of the tropical cyclone according to Saffir-Simpson scale.
16301625
16311626
Parameters:
1632-
-----------
1633-
basin : str
1634-
Basin of generation of the TC
1635-
v_max : np.array
1636-
1D vector of maximal wind speed along the track
1627+
----------
1628+
max_wind : str
1629+
Maximal sustained wind speed
1630+
units: str
1631+
Wind speed units, kn or m/s. Default: kn
16371632
16381633
Returns:
1639-
--------
1640-
Pc : np.array
1641-
1D vector of central pressure along the track
1634+
-------
1635+
category : int
1636+
-1: "Tropical Depression",
1637+
0: "Tropical Storm",
1638+
1: "Hurricane Cat. 1",
1639+
2: "Hurricane Cat. 2",
1640+
3: "Hurricane Cat. 3",
1641+
4: "Hurricane Cat. 4",
1642+
5: "Hurricane Cat. 5",
16421643
"""
1643-
a = 3.4
1644-
Pn = np.full(len(v_max), BASIN_ENV_PRESSURE[basin])
1645-
Pc = Pn - (v_max ** (1000 / 644)) / a
16461644

1647-
return Pc
1645+
max_sust_wind = max_sust_wind.astype(
1646+
float
1647+
) # avoid casting errors if max_sust_wind is int
1648+
max_sust_wind *= 1.943844 if units == "m/s" else 1
16481649

1649-
@staticmethod
1650-
def compute_radius_max_winds():
1651-
pass
1650+
max_wind = np.nanmax(max_sust_wind)
1651+
category_test = np.full(len(SAFFIR_SIM_CAT), max_wind) < np.array(
1652+
SAFFIR_SIM_CAT
1653+
)
1654+
category = np.argmax(category_test) - 1
16521655

1653-
@staticmethod
1654-
def define_category_storm(self):
1655-
pass
1656+
return category
16561657

16571658
@classmethod
1658-
def from_netcdf_fast(cls, folder_name):
1659-
"""Create new TCTracks object from NetCDF files created with the FAST model
1660-
of Jonathan Lin.
1661-
GitHub Repository: https://github.com/linjonathan/tropical_cyclone_risk?
1659+
def from_fast(cls, folder_name: str):
1660+
"""Create a new TCTracks object from NetCDF files generated by the FAST model, modifying
1661+
the xr.array structure to ensure compatibility with CLIMADA, and calculating the central
1662+
pressure and radius of maximum wind.
1663+
1664+
Model GitHub Repository: https://github.com/linjonathan/tropical_cyclone_risk?
16621665
tab=readme-ov-file
1663-
Publication: https://agupubs.onlinelibrary.wiley.com/doi/epdf/10.1029/2023MS003686
1666+
Model Publication: https://agupubs.onlinelibrary.wiley.com/doi/epdf/10.1029/2023MS003686
16641667
16651668
Parameters:
16661669
----------
@@ -1672,7 +1675,7 @@ def from_netcdf_fast(cls, folder_name):
16721675
Returns:
16731676
-------
16741677
tracks : TCTracks
1675-
TCTracks obecjt with tracks data from the given directory of NetCDF files.
1678+
TCTracks object with tracks data from the given directory of NetCDF files.
16761679
"""
16771680

16781681
file_tr = get_file_names(folder_name)
@@ -1683,45 +1686,53 @@ def from_netcdf_fast(cls, folder_name):
16831686
continue
16841687
with xr.open_dataset(file) as ds:
16851688
for i in ds.n_trk:
1686-
# if storm_id:
1687-
# i == storm_id
16881689

16891690
# Select track
1690-
track = ds.sel(n_trk=i.item())
1691+
track = ds.sel(n_trk=i)
16911692

16921693
# Define coordinates
16931694
lat = track.lat_trks.data
16941695
lon = track.lon_trks.data
16951696
time = track.time.data
16961697

1698+
# Convert time to pandas Datetime "yyyy.mm.dd"
1699+
reference_time = (
1700+
f"{track.tc_years.item()}-{int(track.tc_month.item())}-01"
1701+
)
1702+
time = pd.to_datetime(time, unit="s", origin=reference_time).astype(
1703+
"datetime64[s]"
1704+
)
1705+
16971706
# Define variables
16981707
time_step_vector = np.full(time.shape[0], track.time.data[1])
1699-
max_sustained_wind = track.v_trks.data
1708+
max_sustained_wind_ms = track.v_trks.data
1709+
max_sustained_wind_knots = max_sustained_wind_ms * 1.943844
17001710
basin_vector = np.full(time.shape[0], track.tc_basins.data.item())
17011711
env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()]
17021712
env_pressure_vect = np.full(time.shape[0], env_pressure)
17031713

1704-
# Define hurricaine category and other attributes
1705-
max_sustained_wind_kn = np.nanmax(
1706-
max_sustained_wind * 1.943844
1707-
) # convert from m/s to knots
1708-
category_test = np.full(
1709-
len(SAFFIR_SIM_CAT), max_sustained_wind_kn
1710-
) < np.array(SAFFIR_SIM_CAT)
1711-
category = np.argmax(category_test) - 1
1712-
id_no = track.n_trk.item()
1713-
# Define central pressure
1714-
central_pressure = TCTracks.compute_central_pressure(
1715-
v_max=max_sustained_wind, basin=track.tc_basins.data.item()
1714+
cen_pres_missing = np.full(lat.shape, np.nan)
1715+
rmw_missing = np.full(lat.shape, np.nan)
1716+
cen_pres = tc_tracks._estimate_pressure(
1717+
cen_pres_missing, lat, lon, max_sustained_wind_knots
17161718
)
1719+
rmw = tc_tracks.estimate_rmw(rmw_missing, cen_pres)
1720+
1721+
# Define attributes
1722+
category = tc_tracks.TCTracks.define_tc_category_fast(
1723+
max_sustained_wind_knots
1724+
)
1725+
id_no = track.n_trk.item()
1726+
track_name = f"storm_{id_no}"
17171727

17181728
data.append(
17191729
xr.Dataset(
17201730
{
17211731
"time_step": ("time", time_step_vector),
1722-
"max_sustained_wind": ("time", max_sustained_wind),
1732+
"max_sustained_wind": ("time", max_sustained_wind_ms),
1733+
"central_pressure": ("time", cen_pres),
1734+
"radius_max_wind": ("time", rmw),
17231735
"environmental_pressure": ("time", env_pressure_vect),
1724-
"central_pressure": ("time", central_pressure),
17251736
"basin": ("time", basin_vector),
17261737
},
17271738
coords={
@@ -1732,8 +1743,10 @@ def from_netcdf_fast(cls, folder_name):
17321743
attrs={
17331744
"max_sustained_wind_unit": "m/s",
17341745
"central_pressure_unit": "hPa",
1746+
"name": track_name,
1747+
"sid": id_no,
1748+
"orig_event_flag": True,
17351749
"data_provider": "FAST",
1736-
"orig_event_flag": False,
17371750
"id_no": id_no,
17381751
"category": category,
17391752
},
60.3 KB
Binary file not shown.

climada/hazard/test/test_tc_tracks.py

Lines changed: 22 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -45,41 +45,11 @@
4545
TEST_TRACK_EMANUEL = DATA_DIR.joinpath("emanuel_test_tracks.mat")
4646
TEST_TRACK_EMANUEL_CORR = DATA_DIR.joinpath("temp_mpircp85cal_full.mat")
4747
TEST_TRACK_CHAZ = DATA_DIR.joinpath("chaz_test_tracks.nc")
48+
TEST_TRACK_FAST = DATA_DIR.joinpath("FAST_test_tracks.nc")
4849
TEST_TRACK_STORM = DATA_DIR.joinpath("storm_test_tracks.txt")
4950
TEST_TRACKS_ANTIMERIDIAN = DATA_DIR.joinpath("tracks-antimeridian")
5051
TEST_TRACKS_LEGACY_HDF5 = DATA_DIR.joinpath("tctracks_hdf5_legacy.nc")
5152

52-
TEST_TRACKS_FAST_dummy = xr.Dataset(
53-
data_vars={
54-
"lon_trks": (("n_trk", "time"), np.random.uniform(-180, 180, size=(20, 361))),
55-
"lat_trks": (("n_trk", "time"), np.random.uniform(-90, 90, size=(20, 361))),
56-
"u250_trks": (("n_trk", "time"), np.random.randn(20, 361)),
57-
"v250_trks": (("n_trk", "time"), np.random.randn(20, 361)),
58-
"u850_trks": (("n_trk", "time"), np.random.randn(20, 361)),
59-
"v850_trks": (("n_trk", "time"), np.random.randn(20, 361)),
60-
"v_trks": (("n_trk", "time"), np.random.randn(20, 361)),
61-
"m_trks": (("n_trk", "time"), np.random.randn(20, 361)),
62-
"vmax_trks": (("n_trk", "time"), np.random.randn(20, 361)),
63-
"tc_month": (("n_trk",), np.random.randint(1, 13, size=20)),
64-
"tc_basins": (
65-
("n_trk",),
66-
np.full(20, "WP"),
67-
),
68-
"tc_years": (("n_trk",), np.full(20, 2025)),
69-
"seeds_per_month": (
70-
("year", "basin", "month"),
71-
np.random.randint(0, 5, size=(1, 7, 12)),
72-
),
73-
},
74-
coords={
75-
"n_trk": np.arange(20),
76-
"time": np.linspace(0, 1.296e6, 361),
77-
"year": [2025],
78-
"basin": ["AU", "EP", "NA", "NI", "SI", "SP", "WP"],
79-
"month": np.arange(1, 13),
80-
},
81-
)
82-
8353

8454
class TestIbtracs(unittest.TestCase):
8555
"""Test reading and model of TC from IBTrACS files"""
@@ -663,24 +633,32 @@ def test_from_simulations_storm(self):
663633
tc_track = tc.TCTracks.from_simulations_storm(TEST_TRACK_STORM, years=[7])
664634
self.assertEqual(len(tc_track.data), 0)
665635

666-
def test_compute_central_pressure(self):
667-
pass
636+
def test_define_tc_category_fast(self):
637+
"""test that the correct category is assigned to a tc."""
638+
639+
max_wind = np.array([20, 72, 36, 50]) # knots
640+
category1 = tc.TCTracks.define_tc_category_fast(max_wind)
641+
category2 = tc.TCTracks.define_tc_category_fast(
642+
max_sust_wind=max_wind, units="m/s"
643+
)
644+
self.assertEqual(category1, 1)
645+
self.assertEqual(category2, 5)
668646

669-
def test_from_netcdf_fast(self):
670-
"""test the import of netcdf files from fast model"""
647+
def test_from_fast(self):
648+
"""test the correct import of netcdf files from fast model and the conversion to a
649+
different xr.array structure compatible with CLIMADA."""
671650

672-
# create dummy .nc file to be read
673-
file_path = DATA_DIR.joinpath("fast_test_tracks.nc")
674-
TEST_TRACKS_FAST_dummy.to_netcdf(file_path)
675-
tc_track = tc.TCTracks.from_netcdf_fast(file_path)
651+
tc_track = tc.TCTracks.from_fast(TEST_TRACK_FAST)
676652

677653
expected_attributes = {
678654
"max_sustained_wind_unit": "m/s",
679-
"central_pressure": "hPa",
655+
"central_pressure_unit": "hPa",
656+
"name": "storm_0",
657+
"sid": 0,
658+
"orig_event_flag": True,
680659
"data_provider": "FAST",
681-
"orig_event_flag": False,
682660
"id_no": 0,
683-
"category": -1,
661+
"category": 0,
684662
}
685663

686664
self.assertIsInstance(
@@ -694,13 +672,11 @@ def test_from_netcdf_fast(self):
694672
xr.Dataset,
695673
"tc_track.data[0] not an instance of xarray.Dataset",
696674
)
697-
self.assertEqual(len(tc_track.data), 20)
675+
self.assertEqual(len(tc_track.data), 5)
698676
self.assertEqual(tc_track.data[0].attrs, expected_attributes)
699-
self.assertEqual(tc_track.data[0].environmental_pressure.data[0], 1005)
677+
self.assertEqual(tc_track.data[0].environmental_pressure.data[0], 1010)
700678
self.assertEqual(list(tc_track.data[0].coords.keys()), ["time", "lat", "lon"])
701679

702-
os.remove(file_path)
703-
704680
def test_to_geodataframe_points(self):
705681
"""Conversion of TCTracks to GeoDataFrame using Points."""
706682
tc_track = tc.TCTracks.from_processed_ibtracs_csv(TEST_TRACK)

0 commit comments

Comments
 (0)