From 32a7cffd124778183f5714a519550049660c3c35 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Fri, 6 Dec 2024 15:37:10 +0100 Subject: [PATCH 01/23] first version of method from_netcdf_fast and set up the test --- climada/hazard/tc_tracks.py | 101 ++++++++++++++++++++++++-- climada/hazard/test/test_tc_tracks.py | 44 +++++++++++ 2 files changed, 138 insertions(+), 7 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 963d282cd3..8946d0dd08 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -162,6 +162,7 @@ "SI": 1005, "WP": 1005, "SP": 1004, + "AU": 1004, } """Basin-specific default environmental pressure""" @@ -1619,6 +1620,92 @@ def from_netcdf(cls, folder_name): data.append(track) return cls(data) + @classmethod + def from_netcdf_fast(cls, folder_name): + """Create new TCTracks object from NetCDF files created with the FAST model + of Jonathan Lin. + + GitHub Repository: https://github.com/linjonathan/tropical_cyclone_risk? + tab=readme-ov-file + Publication: https://agupubs.onlinelibrary.wiley.com/doi/epdf/10.1029/2023MS003686 + + Parameters: + ---------- + folder_name : str + Folder name from where to read files. + + Returns: + ------- + tracks : TCTracks + TCTracks obecjt with tracks data from the given directory of NetCDF files. + """ + + file_tr = get_file_names(folder_name) + LOGGER.info("Reading %s files.", len(file_tr)) + data = [] + for file in file_tr: + if Path(file).suffix != ".nc": + continue + with xr.open_dataset(file) as ds: + for i in ds.n_trk: + # Select track + track = ds.sel(n_trk=i.item()) + + # Define coordinates + lat = track.lat_trks.data + lon = track.lon_trks.data + time = track.time.data + + # Define variables + time_step_vector = np.full(time.shape[0], track.time.data[1]) + max_sustained_wind = track.v_trks.data + basin_vector = np.full(time.shape[0], track.tc_basins.data.item()) + central_pressure = np.nan # work in progress: get them from model + radius_max_wind = np.nan # work in progress: get them from model + env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()] + env_pressure_vect = np.full(time.shape[0], env_pressure) + + # Define hurricaine category and other attributes + max_sustained_wind_kn = np.nanmax( + max_sustained_wind * 1.943844 + ) # convert from m/s to knots + category_test = np.full( + len(SAFFIR_SIM_CAT), max_sustained_wind_kn + ) < np.array(SAFFIR_SIM_CAT) + category = np.argmax(category_test) - 1 + track_name = track.n_trk.item() + id_no = track.n_trk.item() + + data.append( + xr.Dataset( + { + "time_step": ("time", time_step_vector), + "max_sustained_wind": ("time", max_sustained_wind), + # "central_pressure": ("time", central_pressure), + # "radius_max_wind": ("time", radius_max_wind), + "environmental_pressure": ("time", env_pressure_vect), + "basin": ("time", basin_vector), + }, + coords={ + "time": ("time", time), + "lat": ("time", lat), + "lon": ("time", lon), + }, + attrs={ + "max_sustained_wind_unit": "kn", + "central_pressure_unit": "mb", + "name": track_name, + "sid": track_name, + "orig_event_flag": False, + "data_provider": "FAST", + "id_no": id_no, + "category": category, + }, + ) + ) + + return cls(data) + def write_hdf5(self, file_name, complevel=5): """Write TC tracks in NetCDF4-compliant HDF5 format. @@ -2665,12 +2752,12 @@ def ibtracs_fit_param(explained, explanatory, year_range=(1980, 2019), order=1): return sm_results -def ibtracs_track_agency(ds_sel): +def ibtracs_track_agency(track): """Get preferred IBTrACS agency for each entry in the dataset. Parameters ---------- - ds_sel : xarray.Dataset + track : xarray.Dataset Subselection of original IBTrACS NetCDF dataset. Returns @@ -2678,7 +2765,7 @@ def ibtracs_track_agency(ds_sel): agency_pref : list of str Names of IBTrACS agencies in order of preference. track_agency_ix : xarray.DataArray of ints - For each entry in `ds_sel`, the agency to use, given as an index into `agency_pref`. + For each entry in `track`, the agency to use, given as an index into `agency_pref`. """ agency_pref = ["wmo"] + IBTRACS_AGENCIES agency_map = {a.encode("utf-8"): i for i, a in enumerate(agency_pref)} @@ -2687,11 +2774,11 @@ def ibtracs_track_agency(ds_sel): ) agency_map[b""] = agency_map[b"wmo"] agency_fun = lambda x: agency_map[x] - if "track_agency" not in ds_sel.data_vars.keys(): - ds_sel["track_agency"] = ds_sel["wmo_agency"].where( - ds_sel["wmo_agency"] != b"", ds_sel["usa_agency"] + if "track_agency" not in track.data_vars.keys(): + track["track_agency"] = track["wmo_agency"].where( + track["wmo_agency"] != b"", track["usa_agency"] ) - track_agency_ix = xr.apply_ufunc(agency_fun, ds_sel["track_agency"], vectorize=True) + track_agency_ix = xr.apply_ufunc(agency_fun, track["track_agency"], vectorize=True) return agency_pref, track_agency_ix diff --git a/climada/hazard/test/test_tc_tracks.py b/climada/hazard/test/test_tc_tracks.py index c42d5a7a14..952e643d47 100644 --- a/climada/hazard/test/test_tc_tracks.py +++ b/climada/hazard/test/test_tc_tracks.py @@ -19,6 +19,7 @@ Test tc_tracks module. """ +import os import unittest from datetime import datetime as dt @@ -48,6 +49,37 @@ TEST_TRACKS_ANTIMERIDIAN = DATA_DIR.joinpath("tracks-antimeridian") TEST_TRACKS_LEGACY_HDF5 = DATA_DIR.joinpath("tctracks_hdf5_legacy.nc") +TEST_TRACKS_FAST_dummy = xr.Dataset( + data_vars={ + "lon_trks": (("n_trk", "time"), np.random.uniform(-180, 180, size=(20, 361))), + "lat_trks": (("n_trk", "time"), np.random.uniform(-90, 90, size=(20, 361))), + "u250_trks": (("n_trk", "time"), np.random.randn(20, 361)), + "v250_trks": (("n_trk", "time"), np.random.randn(20, 361)), + "u850_trks": (("n_trk", "time"), np.random.randn(20, 361)), + "v850_trks": (("n_trk", "time"), np.random.randn(20, 361)), + "v_trks": (("n_trk", "time"), np.random.randn(20, 361)), + "m_trks": (("n_trk", "time"), np.random.randn(20, 361)), + "vmax_trks": (("n_trk", "time"), np.random.randn(20, 361)), + "tc_month": (("n_trk",), np.random.randint(1, 13, size=20)), + "tc_basins": ( + ("n_trk",), + np.random.choice(["AU", "EP", "NA", "NI", "SI", "SP", "WP"], size=20), + ), + "tc_years": (("n_trk",), np.full(20, 2025)), + "seeds_per_month": ( + ("year", "basin", "month"), + np.random.randint(0, 5, size=(1, 7, 12)), + ), + }, + coords={ + "n_trk": np.arange(20), + "time": np.linspace(0, 1.296e6, 361), + "year": [2025], + "basin": ["AU", "EP", "NA", "NI", "SI", "SP", "WP"], + "month": np.arange(1, 13), + }, +) + class TestIbtracs(unittest.TestCase): """Test reading and model of TC from IBTrACS files""" @@ -609,6 +641,18 @@ def test_from_simulations_storm(self): tc_track = tc.TCTracks.from_simulations_storm(TEST_TRACK_STORM, years=[7]) self.assertEqual(len(tc_track.data), 0) + def test_from_netcdf_fast(self): + """test the import of netcdf files from fast model""" + + # create dummy .nc file to read, delate it in the end + file_path = DATA_DIR.joinpath("fast_test_tracks.nc") + TEST_TRACKS_FAST_dummy.to_netcdf(file_path) + tc_track = tc.TCTracks.from_netcdf_fast(file_path) + # test various instances + + # remove file + os.remove(file_path) + def test_to_geodataframe_points(self): """Conversion of TCTracks to GeoDataFrame using Points.""" tc_track = tc.TCTracks.from_processed_ibtracs_csv(TEST_TRACK) From f157b92d0328d316e07c762978aa2bbd11597e00 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Fri, 13 Dec 2024 10:15:04 +0100 Subject: [PATCH 02/23] balck --- climada/hazard/tc_tracks.py | 58 ++++++++++++++++++++++----- climada/hazard/test/test_tc_tracks.py | 34 ++++++++++++++-- 2 files changed, 77 insertions(+), 15 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 8946d0dd08..6c1fd2a08e 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1620,11 +1620,44 @@ def from_netcdf(cls, folder_name): data.append(track) return cls(data) + @staticmethod + def compute_central_pressure(basin, v_max): + """Compute central pressure of tropical cyclone given the maximal + wind speed of the storm. Method needed to load tracks from_netcdf_fast. + + Holland, Greg. (2008). A Revised Hurricane Pressure Wind Model. + Monthly Weather Review - MON WEATHER REV. 136. 10.1175/2008MWR2395.1. + + Parameters: + ----------- + basin : str + Basin of generation of the TC + v_max : np.array + 1D vector of maximal wind speed along the track + + Returns: + -------- + Pc : np.array + 1D vector of central pressure along the track + """ + a = 3.4 + Pn = np.full(len(v_max), BASIN_ENV_PRESSURE[basin]) + Pc = Pn - (v_max ** (1000 / 644)) / a + + return Pc + + @staticmethod + def compute_radius_max_winds(): + pass + + @staticmethod + def define_category_storm(self): + pass + @classmethod def from_netcdf_fast(cls, folder_name): """Create new TCTracks object from NetCDF files created with the FAST model of Jonathan Lin. - GitHub Repository: https://github.com/linjonathan/tropical_cyclone_risk? tab=readme-ov-file Publication: https://agupubs.onlinelibrary.wiley.com/doi/epdf/10.1029/2023MS003686 @@ -1633,6 +1666,8 @@ def from_netcdf_fast(cls, folder_name): ---------- folder_name : str Folder name from where to read files. + storm_id : int + Number of the simulated storm Returns: ------- @@ -1648,6 +1683,9 @@ def from_netcdf_fast(cls, folder_name): continue with xr.open_dataset(file) as ds: for i in ds.n_trk: + # if storm_id: + # i == storm_id + # Select track track = ds.sel(n_trk=i.item()) @@ -1660,8 +1698,6 @@ def from_netcdf_fast(cls, folder_name): time_step_vector = np.full(time.shape[0], track.time.data[1]) max_sustained_wind = track.v_trks.data basin_vector = np.full(time.shape[0], track.tc_basins.data.item()) - central_pressure = np.nan # work in progress: get them from model - radius_max_wind = np.nan # work in progress: get them from model env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()] env_pressure_vect = np.full(time.shape[0], env_pressure) @@ -1673,17 +1709,19 @@ def from_netcdf_fast(cls, folder_name): len(SAFFIR_SIM_CAT), max_sustained_wind_kn ) < np.array(SAFFIR_SIM_CAT) category = np.argmax(category_test) - 1 - track_name = track.n_trk.item() id_no = track.n_trk.item() + # Define central pressure + central_pressure = TCTracks.compute_central_pressure( + v_max=max_sustained_wind, basin=track.tc_basins.data.item() + ) data.append( xr.Dataset( { "time_step": ("time", time_step_vector), "max_sustained_wind": ("time", max_sustained_wind), - # "central_pressure": ("time", central_pressure), - # "radius_max_wind": ("time", radius_max_wind), "environmental_pressure": ("time", env_pressure_vect), + "central_pressure": ("time", central_pressure), "basin": ("time", basin_vector), }, coords={ @@ -1692,12 +1730,10 @@ def from_netcdf_fast(cls, folder_name): "lon": ("time", lon), }, attrs={ - "max_sustained_wind_unit": "kn", - "central_pressure_unit": "mb", - "name": track_name, - "sid": track_name, - "orig_event_flag": False, + "max_sustained_wind_unit": "m/s", + "central_pressure_unit": "hPa", "data_provider": "FAST", + "orig_event_flag": False, "id_no": id_no, "category": category, }, diff --git a/climada/hazard/test/test_tc_tracks.py b/climada/hazard/test/test_tc_tracks.py index 952e643d47..e928b93ed1 100644 --- a/climada/hazard/test/test_tc_tracks.py +++ b/climada/hazard/test/test_tc_tracks.py @@ -63,7 +63,7 @@ "tc_month": (("n_trk",), np.random.randint(1, 13, size=20)), "tc_basins": ( ("n_trk",), - np.random.choice(["AU", "EP", "NA", "NI", "SI", "SP", "WP"], size=20), + np.full(20, "WP"), ), "tc_years": (("n_trk",), np.full(20, 2025)), "seeds_per_month": ( @@ -641,16 +641,42 @@ def test_from_simulations_storm(self): tc_track = tc.TCTracks.from_simulations_storm(TEST_TRACK_STORM, years=[7]) self.assertEqual(len(tc_track.data), 0) + def test_compute_central_pressure(self): + pass + def test_from_netcdf_fast(self): """test the import of netcdf files from fast model""" - # create dummy .nc file to read, delate it in the end + # create dummy .nc file to be read file_path = DATA_DIR.joinpath("fast_test_tracks.nc") TEST_TRACKS_FAST_dummy.to_netcdf(file_path) tc_track = tc.TCTracks.from_netcdf_fast(file_path) - # test various instances - # remove file + expected_attributes = { + "max_sustained_wind_unit": "m/s", + "central_pressure": "hPa", + "data_provider": "FAST", + "orig_event_flag": False, + "id_no": 0, + "category": -1, + } + + self.assertIsInstance( + tc_track, tc.TCTracks, "tc_track is not an instance of TCTracks" + ) + self.assertIsInstance( + tc_track.data, list, "tc_track.data is not an instance of list" + ) + self.assertIsInstance( + tc_track.data[0], + xr.Dataset, + "tc_track.data[0] not an instance of xarray.Dataset", + ) + self.assertEqual(len(tc_track.data), 20) + self.assertEqual(tc_track.data[0].attrs, expected_attributes) + self.assertEqual(tc_track.data[0].environmental_pressure.data[0], 1005) + self.assertEqual(list(tc_track.data[0].coords.keys()), ["time", "lat", "lon"]) + os.remove(file_path) def test_to_geodataframe_points(self): From c1f8847dc25592d735ab7752246b86fcfdcbbab7 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Thu, 16 Jan 2025 11:30:31 +0100 Subject: [PATCH 03/23] add .nc test file and update code --- climada/hazard/tc_tracks.py | 115 +++++++++++-------- climada/hazard/test/data/FAST_test_tracks.nc | Bin 0 -> 61704 bytes climada/hazard/test/test_tc_tracks.py | 68 ++++------- 3 files changed, 86 insertions(+), 97 deletions(-) create mode 100644 climada/hazard/test/data/FAST_test_tracks.nc diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 5394d7e8f5..383e06ae13 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -52,10 +52,9 @@ from shapely.geometry import LineString, MultiLineString, Point from sklearn.metrics import DistanceMetric -import climada.hazard.tc_tracks_synth import climada.util.coordinates as u_coord import climada.util.plot as u_plot -from climada.hazard import Centroids +from climada.hazard import Centroids, tc_tracks, tc_tracks_synth # climada dependencies from climada.util import ureg @@ -1621,46 +1620,50 @@ def from_netcdf(cls, folder_name): return cls(data) @staticmethod - def compute_central_pressure(basin, v_max): - """Compute central pressure of tropical cyclone given the maximal - wind speed of the storm. Method needed to load tracks from_netcdf_fast. - - Holland, Greg. (2008). A Revised Hurricane Pressure Wind Model. - Monthly Weather Review - MON WEATHER REV. 136. 10.1175/2008MWR2395.1. + def define_tc_category_fast(max_sust_wind: np.array, units: str = "kn") -> int: + """Define category of the tropical cyclone according to Saffir-Simpson scale. Parameters: - ----------- - basin : str - Basin of generation of the TC - v_max : np.array - 1D vector of maximal wind speed along the track + ---------- + max_wind : str + Maximal sustained wind speed + units: str + Wind speed units, kn or m/s. Default: kn Returns: - -------- - Pc : np.array - 1D vector of central pressure along the track + ------- + category : int + -1: "Tropical Depression", + 0: "Tropical Storm", + 1: "Hurricane Cat. 1", + 2: "Hurricane Cat. 2", + 3: "Hurricane Cat. 3", + 4: "Hurricane Cat. 4", + 5: "Hurricane Cat. 5", """ - a = 3.4 - Pn = np.full(len(v_max), BASIN_ENV_PRESSURE[basin]) - Pc = Pn - (v_max ** (1000 / 644)) / a - return Pc + max_sust_wind = max_sust_wind.astype( + float + ) # avoid casting errors if max_sust_wind is int + max_sust_wind *= 1.943844 if units == "m/s" else 1 - @staticmethod - def compute_radius_max_winds(): - pass + max_wind = np.nanmax(max_sust_wind) + category_test = np.full(len(SAFFIR_SIM_CAT), max_wind) < np.array( + SAFFIR_SIM_CAT + ) + category = np.argmax(category_test) - 1 - @staticmethod - def define_category_storm(self): - pass + return category @classmethod - def from_netcdf_fast(cls, folder_name): - """Create new TCTracks object from NetCDF files created with the FAST model - of Jonathan Lin. - GitHub Repository: https://github.com/linjonathan/tropical_cyclone_risk? + def from_fast(cls, folder_name: str): + """Create a new TCTracks object from NetCDF files generated by the FAST model, modifying + the xr.array structure to ensure compatibility with CLIMADA, and calculating the central + pressure and radius of maximum wind. + + Model GitHub Repository: https://github.com/linjonathan/tropical_cyclone_risk? tab=readme-ov-file - Publication: https://agupubs.onlinelibrary.wiley.com/doi/epdf/10.1029/2023MS003686 + Model Publication: https://agupubs.onlinelibrary.wiley.com/doi/epdf/10.1029/2023MS003686 Parameters: ---------- @@ -1672,7 +1675,7 @@ def from_netcdf_fast(cls, folder_name): Returns: ------- tracks : TCTracks - TCTracks obecjt with tracks data from the given directory of NetCDF files. + TCTracks object with tracks data from the given directory of NetCDF files. """ file_tr = get_file_names(folder_name) @@ -1683,45 +1686,53 @@ def from_netcdf_fast(cls, folder_name): continue with xr.open_dataset(file) as ds: for i in ds.n_trk: - # if storm_id: - # i == storm_id # Select track - track = ds.sel(n_trk=i.item()) + track = ds.sel(n_trk=i) # Define coordinates lat = track.lat_trks.data lon = track.lon_trks.data time = track.time.data + # Convert time to pandas Datetime "yyyy.mm.dd" + reference_time = ( + f"{track.tc_years.item()}-{int(track.tc_month.item())}-01" + ) + time = pd.to_datetime(time, unit="s", origin=reference_time).astype( + "datetime64[s]" + ) + # Define variables time_step_vector = np.full(time.shape[0], track.time.data[1]) - max_sustained_wind = track.v_trks.data + max_sustained_wind_ms = track.v_trks.data + max_sustained_wind_knots = max_sustained_wind_ms * 1.943844 basin_vector = np.full(time.shape[0], track.tc_basins.data.item()) env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()] env_pressure_vect = np.full(time.shape[0], env_pressure) - # Define hurricaine category and other attributes - max_sustained_wind_kn = np.nanmax( - max_sustained_wind * 1.943844 - ) # convert from m/s to knots - category_test = np.full( - len(SAFFIR_SIM_CAT), max_sustained_wind_kn - ) < np.array(SAFFIR_SIM_CAT) - category = np.argmax(category_test) - 1 - id_no = track.n_trk.item() - # Define central pressure - central_pressure = TCTracks.compute_central_pressure( - v_max=max_sustained_wind, basin=track.tc_basins.data.item() + cen_pres_missing = np.full(lat.shape, np.nan) + rmw_missing = np.full(lat.shape, np.nan) + cen_pres = tc_tracks._estimate_pressure( + cen_pres_missing, lat, lon, max_sustained_wind_knots ) + rmw = tc_tracks.estimate_rmw(rmw_missing, cen_pres) + + # Define attributes + category = tc_tracks.TCTracks.define_tc_category_fast( + max_sustained_wind_knots + ) + id_no = track.n_trk.item() + track_name = f"storm_{id_no}" data.append( xr.Dataset( { "time_step": ("time", time_step_vector), - "max_sustained_wind": ("time", max_sustained_wind), + "max_sustained_wind": ("time", max_sustained_wind_ms), + "central_pressure": ("time", cen_pres), + "radius_max_wind": ("time", rmw), "environmental_pressure": ("time", env_pressure_vect), - "central_pressure": ("time", central_pressure), "basin": ("time", basin_vector), }, coords={ @@ -1732,8 +1743,10 @@ def from_netcdf_fast(cls, folder_name): attrs={ "max_sustained_wind_unit": "m/s", "central_pressure_unit": "hPa", + "name": track_name, + "sid": id_no, + "orig_event_flag": True, "data_provider": "FAST", - "orig_event_flag": False, "id_no": id_no, "category": category, }, diff --git a/climada/hazard/test/data/FAST_test_tracks.nc b/climada/hazard/test/data/FAST_test_tracks.nc new file mode 100644 index 0000000000000000000000000000000000000000..dfe9d8b718e7a8c4d588620e44a8cf4fc5f577d4 GIT binary patch literal 61704 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 WmrPPl;Ba)m^67se!!P{ZSN{VGmFv;~ literal 0 HcmV?d00001 diff --git a/climada/hazard/test/test_tc_tracks.py b/climada/hazard/test/test_tc_tracks.py index 8949c48c7c..388efc75e0 100644 --- a/climada/hazard/test/test_tc_tracks.py +++ b/climada/hazard/test/test_tc_tracks.py @@ -45,41 +45,11 @@ TEST_TRACK_EMANUEL = DATA_DIR.joinpath("emanuel_test_tracks.mat") TEST_TRACK_EMANUEL_CORR = DATA_DIR.joinpath("temp_mpircp85cal_full.mat") TEST_TRACK_CHAZ = DATA_DIR.joinpath("chaz_test_tracks.nc") +TEST_TRACK_FAST = DATA_DIR.joinpath("FAST_test_tracks.nc") TEST_TRACK_STORM = DATA_DIR.joinpath("storm_test_tracks.txt") TEST_TRACKS_ANTIMERIDIAN = DATA_DIR.joinpath("tracks-antimeridian") TEST_TRACKS_LEGACY_HDF5 = DATA_DIR.joinpath("tctracks_hdf5_legacy.nc") -TEST_TRACKS_FAST_dummy = xr.Dataset( - data_vars={ - "lon_trks": (("n_trk", "time"), np.random.uniform(-180, 180, size=(20, 361))), - "lat_trks": (("n_trk", "time"), np.random.uniform(-90, 90, size=(20, 361))), - "u250_trks": (("n_trk", "time"), np.random.randn(20, 361)), - "v250_trks": (("n_trk", "time"), np.random.randn(20, 361)), - "u850_trks": (("n_trk", "time"), np.random.randn(20, 361)), - "v850_trks": (("n_trk", "time"), np.random.randn(20, 361)), - "v_trks": (("n_trk", "time"), np.random.randn(20, 361)), - "m_trks": (("n_trk", "time"), np.random.randn(20, 361)), - "vmax_trks": (("n_trk", "time"), np.random.randn(20, 361)), - "tc_month": (("n_trk",), np.random.randint(1, 13, size=20)), - "tc_basins": ( - ("n_trk",), - np.full(20, "WP"), - ), - "tc_years": (("n_trk",), np.full(20, 2025)), - "seeds_per_month": ( - ("year", "basin", "month"), - np.random.randint(0, 5, size=(1, 7, 12)), - ), - }, - coords={ - "n_trk": np.arange(20), - "time": np.linspace(0, 1.296e6, 361), - "year": [2025], - "basin": ["AU", "EP", "NA", "NI", "SI", "SP", "WP"], - "month": np.arange(1, 13), - }, -) - class TestIbtracs(unittest.TestCase): """Test reading and model of TC from IBTrACS files""" @@ -663,24 +633,32 @@ def test_from_simulations_storm(self): tc_track = tc.TCTracks.from_simulations_storm(TEST_TRACK_STORM, years=[7]) self.assertEqual(len(tc_track.data), 0) - def test_compute_central_pressure(self): - pass + def test_define_tc_category_fast(self): + """test that the correct category is assigned to a tc.""" + + max_wind = np.array([20, 72, 36, 50]) # knots + category1 = tc.TCTracks.define_tc_category_fast(max_wind) + category2 = tc.TCTracks.define_tc_category_fast( + max_sust_wind=max_wind, units="m/s" + ) + self.assertEqual(category1, 1) + self.assertEqual(category2, 5) - def test_from_netcdf_fast(self): - """test the import of netcdf files from fast model""" + def test_from_fast(self): + """test the correct import of netcdf files from fast model and the conversion to a + different xr.array structure compatible with CLIMADA.""" - # create dummy .nc file to be read - file_path = DATA_DIR.joinpath("fast_test_tracks.nc") - TEST_TRACKS_FAST_dummy.to_netcdf(file_path) - tc_track = tc.TCTracks.from_netcdf_fast(file_path) + tc_track = tc.TCTracks.from_fast(TEST_TRACK_FAST) expected_attributes = { "max_sustained_wind_unit": "m/s", - "central_pressure": "hPa", + "central_pressure_unit": "hPa", + "name": "storm_0", + "sid": 0, + "orig_event_flag": True, "data_provider": "FAST", - "orig_event_flag": False, "id_no": 0, - "category": -1, + "category": 0, } self.assertIsInstance( @@ -694,13 +672,11 @@ def test_from_netcdf_fast(self): xr.Dataset, "tc_track.data[0] not an instance of xarray.Dataset", ) - self.assertEqual(len(tc_track.data), 20) + self.assertEqual(len(tc_track.data), 5) self.assertEqual(tc_track.data[0].attrs, expected_attributes) - self.assertEqual(tc_track.data[0].environmental_pressure.data[0], 1005) + self.assertEqual(tc_track.data[0].environmental_pressure.data[0], 1010) self.assertEqual(list(tc_track.data[0].coords.keys()), ["time", "lat", "lon"]) - os.remove(file_path) - def test_to_geodataframe_points(self): """Conversion of TCTracks to GeoDataFrame using Points.""" tc_track = tc.TCTracks.from_processed_ibtracs_csv(TEST_TRACK) From bd588c841383394c5a199018eb49de7e74ab4755 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi <115944312+NicolasColombi@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:51:25 +0100 Subject: [PATCH 04/23] Update tc_tracks.py fix self import --- climada/hazard/tc_tracks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 383e06ae13..86189ec868 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -54,7 +54,7 @@ import climada.util.coordinates as u_coord import climada.util.plot as u_plot -from climada.hazard import Centroids, tc_tracks, tc_tracks_synth +from climada.hazard import Centroids, tc_tracks_synth # climada dependencies from climada.util import ureg From 6e075dfc497ccbd47d86b15835397640542a459e Mon Sep 17 00:00:00 2001 From: Nicolas Colombi <115944312+NicolasColombi@users.noreply.github.com> Date: Fri, 17 Jan 2025 11:24:19 +0100 Subject: [PATCH 05/23] Update tc_tracks.py pylint --- climada/hazard/tc_tracks.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 86189ec868..1bd78bc446 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -52,9 +52,10 @@ from shapely.geometry import LineString, MultiLineString, Point from sklearn.metrics import DistanceMetric +import climada.hazard.tc_tracks_synth import climada.util.coordinates as u_coord import climada.util.plot as u_plot -from climada.hazard import Centroids, tc_tracks_synth +from climada.hazard import Centroids # climada dependencies from climada.util import ureg @@ -1684,11 +1685,11 @@ def from_fast(cls, folder_name: str): for file in file_tr: if Path(file).suffix != ".nc": continue - with xr.open_dataset(file) as ds: - for i in ds.n_trk: + with xr.open_dataset(file) as data: + for i in data.n_trk: # Select track - track = ds.sel(n_trk=i) + track = data.sel(n_trk=i) # Define coordinates lat = track.lat_trks.data @@ -1713,13 +1714,13 @@ def from_fast(cls, folder_name: str): cen_pres_missing = np.full(lat.shape, np.nan) rmw_missing = np.full(lat.shape, np.nan) - cen_pres = tc_tracks._estimate_pressure( + cen_pres = _estimate_pressure( cen_pres_missing, lat, lon, max_sustained_wind_knots ) - rmw = tc_tracks.estimate_rmw(rmw_missing, cen_pres) + rmw = estimate_rmw(rmw_missing, cen_pres) # Define attributes - category = tc_tracks.TCTracks.define_tc_category_fast( + category = TCTracks.define_tc_category_fast( max_sustained_wind_knots ) id_no = track.n_trk.item() From 8b31d7aaff8d4dcbe380f0546b6589471be22066 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Fri, 17 Jan 2025 11:56:31 +0100 Subject: [PATCH 06/23] pylint to many locals --- climada/hazard/tc_tracks.py | 56 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 1bd78bc446..3bd6e6b797 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1679,62 +1679,60 @@ def from_fast(cls, folder_name: str): TCTracks object with tracks data from the given directory of NetCDF files. """ - file_tr = get_file_names(folder_name) - LOGGER.info("Reading %s files.", len(file_tr)) + LOGGER.info("Reading %s files.", len(get_file_names(folder_name))) data = [] - for file in file_tr: + for file in get_file_names(folder_name): if Path(file).suffix != ".nc": continue - with xr.open_dataset(file) as data: - for i in data.n_trk: + with xr.open_dataset(file) as dataset: + for i in dataset.n_trk: # Select track - track = data.sel(n_trk=i) + track = dataset.sel(n_trk=i) # Define coordinates lat = track.lat_trks.data lon = track.lon_trks.data - time = track.time.data # Convert time to pandas Datetime "yyyy.mm.dd" reference_time = ( f"{track.tc_years.item()}-{int(track.tc_month.item())}-01" ) - time = pd.to_datetime(time, unit="s", origin=reference_time).astype( - "datetime64[s]" - ) + time = pd.to_datetime( + track.time.data, unit="s", origin=reference_time + ).astype("datetime64[s]") # Define variables - time_step_vector = np.full(time.shape[0], track.time.data[1]) - max_sustained_wind_ms = track.v_trks.data - max_sustained_wind_knots = max_sustained_wind_ms * 1.943844 - basin_vector = np.full(time.shape[0], track.tc_basins.data.item()) + max_sustained_wind_knots = track.v_trks.data * 1.943844 env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()] - env_pressure_vect = np.full(time.shape[0], env_pressure) - - cen_pres_missing = np.full(lat.shape, np.nan) - rmw_missing = np.full(lat.shape, np.nan) cen_pres = _estimate_pressure( - cen_pres_missing, lat, lon, max_sustained_wind_knots + np.full(lat.shape, np.nan), lat, lon, max_sustained_wind_knots ) - rmw = estimate_rmw(rmw_missing, cen_pres) + rmw = estimate_rmw(np.full(lat.shape, np.nan), cen_pres) # Define attributes category = TCTracks.define_tc_category_fast( max_sustained_wind_knots ) - id_no = track.n_trk.item() - track_name = f"storm_{id_no}" data.append( xr.Dataset( { - "time_step": ("time", time_step_vector), - "max_sustained_wind": ("time", max_sustained_wind_ms), + "time_step": ( + "time", + np.full(time.shape[0], track.time.data[1]), + ), + "max_sustained_wind": ("time", track.v_trks.data), "central_pressure": ("time", cen_pres), "radius_max_wind": ("time", rmw), - "environmental_pressure": ("time", env_pressure_vect), - "basin": ("time", basin_vector), + "environmental_pressure": ( + "time", + np.full(time.shape[0], env_pressure), + ), + "basin": ( + "time", + np.full(time.shape[0], track.tc_basins.data.item()), + ), }, coords={ "time": ("time", time), @@ -1744,11 +1742,11 @@ def from_fast(cls, folder_name: str): attrs={ "max_sustained_wind_unit": "m/s", "central_pressure_unit": "hPa", - "name": track_name, - "sid": id_no, + "name": "storm_%s" % track.n_trk.item(), + "sid": track.n_trk.item(), "orig_event_flag": True, "data_provider": "FAST", - "id_no": id_no, + "id_no": track.n_trk.item(), "category": category, }, ) From edfffc539889726fe6a08fceaafbb4c6505271ea Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Wed, 22 Jan 2025 11:46:42 +0100 Subject: [PATCH 07/23] rename from_fast and fix pylint to many locals --- climada/hazard/tc_tracks.py | 19 +++++++++---------- climada/hazard/test/test_tc_tracks.py | 4 ++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 3bd6e6b797..1ffa6e46c2 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1657,7 +1657,7 @@ def define_tc_category_fast(max_sust_wind: np.array, units: str = "kn") -> int: return category @classmethod - def from_fast(cls, folder_name: str): + def from_FAST(cls, folder_name: str): """Create a new TCTracks object from NetCDF files generated by the FAST model, modifying the xr.array structure to ensure compatibility with CLIMADA, and calculating the central pressure and radius of maximum wind. @@ -1708,12 +1708,6 @@ def from_fast(cls, folder_name: str): cen_pres = _estimate_pressure( np.full(lat.shape, np.nan), lat, lon, max_sustained_wind_knots ) - rmw = estimate_rmw(np.full(lat.shape, np.nan), cen_pres) - - # Define attributes - category = TCTracks.define_tc_category_fast( - max_sustained_wind_knots - ) data.append( xr.Dataset( @@ -1724,7 +1718,10 @@ def from_fast(cls, folder_name: str): ), "max_sustained_wind": ("time", track.v_trks.data), "central_pressure": ("time", cen_pres), - "radius_max_wind": ("time", rmw), + "radius_max_wind": ( + "time", + estimate_rmw(np.full(lat.shape, np.nan), cen_pres), + ), "environmental_pressure": ( "time", np.full(time.shape[0], env_pressure), @@ -1742,12 +1739,14 @@ def from_fast(cls, folder_name: str): attrs={ "max_sustained_wind_unit": "m/s", "central_pressure_unit": "hPa", - "name": "storm_%s" % track.n_trk.item(), + "name": f"storm_{track.n_trk.item()}", "sid": track.n_trk.item(), "orig_event_flag": True, "data_provider": "FAST", "id_no": track.n_trk.item(), - "category": category, + "category": TCTracks.define_tc_category_fast( + max_sustained_wind_knots + ), }, ) ) diff --git a/climada/hazard/test/test_tc_tracks.py b/climada/hazard/test/test_tc_tracks.py index 388efc75e0..6c39c8c98a 100644 --- a/climada/hazard/test/test_tc_tracks.py +++ b/climada/hazard/test/test_tc_tracks.py @@ -644,11 +644,11 @@ def test_define_tc_category_fast(self): self.assertEqual(category1, 1) self.assertEqual(category2, 5) - def test_from_fast(self): + def test_from_FAST(self): """test the correct import of netcdf files from fast model and the conversion to a different xr.array structure compatible with CLIMADA.""" - tc_track = tc.TCTracks.from_fast(TEST_TRACK_FAST) + tc_track = tc.TCTracks.from_FAST(TEST_TRACK_FAST) expected_attributes = { "max_sustained_wind_unit": "m/s", From 7f21fea25a4c561ce2ef5b79da173e2dd617f0a4 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi <115944312+NicolasColombi@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:49:44 +0100 Subject: [PATCH 08/23] Update tc_tracks.py --- climada/hazard/tc_tracks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 1ffa6e46c2..1f8b4509f6 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1621,7 +1621,7 @@ def from_netcdf(cls, folder_name): return cls(data) @staticmethod - def define_tc_category_fast(max_sust_wind: np.array, units: str = "kn") -> int: + def define_tc_category_FAST(max_sust_wind: np.array, units: str = "kn") -> int: """Define category of the tropical cyclone according to Saffir-Simpson scale. Parameters: @@ -1744,7 +1744,7 @@ def from_FAST(cls, folder_name: str): "orig_event_flag": True, "data_provider": "FAST", "id_no": track.n_trk.item(), - "category": TCTracks.define_tc_category_fast( + "category": TCTracks.define_tc_category_FAST( max_sustained_wind_knots ), }, From 520487f7e0e0ad51063dc0ef7256a733b365c8ca Mon Sep 17 00:00:00 2001 From: Nicolas Colombi <115944312+NicolasColombi@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:51:30 +0100 Subject: [PATCH 09/23] Update test_tc_tracks.py --- climada/hazard/test/test_tc_tracks.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/climada/hazard/test/test_tc_tracks.py b/climada/hazard/test/test_tc_tracks.py index 6c39c8c98a..0b86775f02 100644 --- a/climada/hazard/test/test_tc_tracks.py +++ b/climada/hazard/test/test_tc_tracks.py @@ -633,19 +633,19 @@ def test_from_simulations_storm(self): tc_track = tc.TCTracks.from_simulations_storm(TEST_TRACK_STORM, years=[7]) self.assertEqual(len(tc_track.data), 0) - def test_define_tc_category_fast(self): - """test that the correct category is assigned to a tc.""" + def test_define_tc_category_FAST(self): + """test that the correct category is assigned to a TC from FAST model.""" max_wind = np.array([20, 72, 36, 50]) # knots - category1 = tc.TCTracks.define_tc_category_fast(max_wind) - category2 = tc.TCTracks.define_tc_category_fast( + category1 = tc.TCTracks.define_tc_category_FAST(max_wind) + category2 = tc.TCTracks.define_tc_category_FAST( max_sust_wind=max_wind, units="m/s" ) self.assertEqual(category1, 1) self.assertEqual(category2, 5) def test_from_FAST(self): - """test the correct import of netcdf files from fast model and the conversion to a + """test the correct import of netcdf files from FAST model and the conversion to a different xr.array structure compatible with CLIMADA.""" tc_track = tc.TCTracks.from_FAST(TEST_TRACK_FAST) From b93843b04c7f7a7fff2f8802fa7ebcf83cda24b9 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Wed, 22 Jan 2025 12:11:52 +0100 Subject: [PATCH 10/23] update documentation --- doc/tutorial/climada_hazard_TropCyclone.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tutorial/climada_hazard_TropCyclone.ipynb b/doc/tutorial/climada_hazard_TropCyclone.ipynb index 0613fa7fde..28d80d1ac0 100644 --- a/doc/tutorial/climada_hazard_TropCyclone.ipynb +++ b/doc/tutorial/climada_hazard_TropCyclone.ipynb @@ -1802,7 +1802,7 @@ " \n", "### d) Load TC tracks from other sources\n", "\n", - "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." + "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." ] }, { From 10e5e281368aa0654a94e86045a8c34062742d3f Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Wed, 22 Jan 2025 12:16:48 +0100 Subject: [PATCH 11/23] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd94a20630..13b8b347c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Code freeze date: YYYY-MM-DD ### Added +- `climada.hazard.tc_tracks.TCTracks.from_FAST` function [#993](https://github.com/CLIMADA-project/climada_python/pull/993) - Add `osm-flex` package to CLIMADA core [#981](https://github.com/CLIMADA-project/climada_python/pull/981) - `doc.tutorial.climada_entity_Exposures_osm.ipynb` tutorial explaining how to use `osm-flex`with CLIMADA - `climada.util.coordinates.bounding_box_global` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980) From 58b60b5c25bcd7d8062278ab9201d70525812d1c Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Wed, 22 Jan 2025 15:55:46 +0100 Subject: [PATCH 12/23] update tests --- climada/hazard/tc_tracks.py | 4 ++-- climada/hazard/test/test_tc_tracks.py | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 1f8b4509f6..dbac5c3359 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1703,7 +1703,7 @@ def from_FAST(cls, folder_name: str): ).astype("datetime64[s]") # Define variables - max_sustained_wind_knots = track.v_trks.data * 1.943844 + max_sustained_wind_knots = track.vmax_trks.data * 1.943844 env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()] cen_pres = _estimate_pressure( np.full(lat.shape, np.nan), lat, lon, max_sustained_wind_knots @@ -1716,7 +1716,7 @@ def from_FAST(cls, folder_name: str): "time", np.full(time.shape[0], track.time.data[1]), ), - "max_sustained_wind": ("time", track.v_trks.data), + "max_sustained_wind": ("time", track.vmax_trks.data), "central_pressure": ("time", cen_pres), "radius_max_wind": ( "time", diff --git a/climada/hazard/test/test_tc_tracks.py b/climada/hazard/test/test_tc_tracks.py index 0b86775f02..a4d96b61dc 100644 --- a/climada/hazard/test/test_tc_tracks.py +++ b/climada/hazard/test/test_tc_tracks.py @@ -658,7 +658,7 @@ def test_from_FAST(self): "orig_event_flag": True, "data_provider": "FAST", "id_no": 0, - "category": 0, + "category": 1, } self.assertIsInstance( @@ -674,8 +674,20 @@ def test_from_FAST(self): ) self.assertEqual(len(tc_track.data), 5) self.assertEqual(tc_track.data[0].attrs, expected_attributes) - self.assertEqual(tc_track.data[0].environmental_pressure.data[0], 1010) self.assertEqual(list(tc_track.data[0].coords.keys()), ["time", "lat", "lon"]) + self.assertEqual( + tc_track.data[0].time.values[0], + np.datetime64("2025-09-01T00:00:00.000000000"), + ) + self.assertEqual(tc_track.data[0].lat.values[0], 17.863591350508266) + self.assertEqual(tc_track.data[0].lon.values[0], 288.2355824168037) + self.assertEqual(len(tc_track.data[0].time), 121) + self.assertEqual(tc_track.data[0].time_step[0], 10800) + self.assertEqual( + tc_track.data[0].max_sustained_wind.values[10], 24.71636959089841 + ) + self.assertEqual(tc_track.data[0].environmental_pressure.data[0], 1010) + self.assertEqual(tc_track.data[0].basin[0], "NA") def test_to_geodataframe_points(self): """Conversion of TCTracks to GeoDataFrame using Points.""" From 1e65bc17e84efbd1cf60d4f828d648b8bf4f9221 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Mon, 27 Jan 2025 10:24:50 +0100 Subject: [PATCH 13/23] fix bug plot --- climada/hazard/tc_tracks.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index dbac5c3359..469f6dfc72 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1689,11 +1689,15 @@ def from_FAST(cls, folder_name: str): # Select track track = dataset.sel(n_trk=i) - - # Define coordinates + # chunk dataset at first NaN value + lon = track.lon_trks.data + last_valid_index = np.where(np.isnan(lon))[0][0] + track = track.isel(time=slice(0, last_valid_index)) + # Select lat, lon lat = track.lat_trks.data lon = track.lon_trks.data - + # convert lon from 0-360 to -180 - 180 + lon = ((lon + 180) % 360) - 180 # Convert time to pandas Datetime "yyyy.mm.dd" reference_time = ( f"{track.tc_years.item()}-{int(track.tc_month.item())}-01" From 43c1c35dfd8dccab735a0d4e7ea5d15599ba1ca2 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Mon, 27 Jan 2025 10:49:25 +0100 Subject: [PATCH 14/23] update tests --- climada/hazard/tc_tracks.py | 3 +-- climada/hazard/test/test_tc_tracks.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 469f6dfc72..4fcd53ec57 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1696,7 +1696,7 @@ def from_FAST(cls, folder_name: str): # Select lat, lon lat = track.lat_trks.data lon = track.lon_trks.data - # convert lon from 0-360 to -180 - 180 + # Convert lon from 0-360 to -180 - 180 lon = ((lon + 180) % 360) - 180 # Convert time to pandas Datetime "yyyy.mm.dd" reference_time = ( @@ -1705,7 +1705,6 @@ def from_FAST(cls, folder_name: str): time = pd.to_datetime( track.time.data, unit="s", origin=reference_time ).astype("datetime64[s]") - # Define variables max_sustained_wind_knots = track.vmax_trks.data * 1.943844 env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()] diff --git a/climada/hazard/test/test_tc_tracks.py b/climada/hazard/test/test_tc_tracks.py index a4d96b61dc..fba84b6a82 100644 --- a/climada/hazard/test/test_tc_tracks.py +++ b/climada/hazard/test/test_tc_tracks.py @@ -680,8 +680,8 @@ def test_from_FAST(self): np.datetime64("2025-09-01T00:00:00.000000000"), ) self.assertEqual(tc_track.data[0].lat.values[0], 17.863591350508266) - self.assertEqual(tc_track.data[0].lon.values[0], 288.2355824168037) - self.assertEqual(len(tc_track.data[0].time), 121) + self.assertEqual(tc_track.data[0].lon.values[0], -71.76441758319629) + self.assertEqual(len(tc_track.data[0].time), 35) self.assertEqual(tc_track.data[0].time_step[0], 10800) self.assertEqual( tc_track.data[0].max_sustained_wind.values[10], 24.71636959089841 From 1504128595ffa67714297a725295b45d7ae7795e Mon Sep 17 00:00:00 2001 From: Nicolas Colombi <115944312+NicolasColombi@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:34:34 +0100 Subject: [PATCH 15/23] Update test_tc_tracks.py --- climada/hazard/test/test_tc_tracks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/climada/hazard/test/test_tc_tracks.py b/climada/hazard/test/test_tc_tracks.py index fba84b6a82..8330771f39 100644 --- a/climada/hazard/test/test_tc_tracks.py +++ b/climada/hazard/test/test_tc_tracks.py @@ -19,7 +19,6 @@ Test tc_tracks module. """ -import os import unittest from datetime import datetime as dt From a428419ffca8cd4799f117c8b2e55f2866fbc22c Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Wed, 5 Feb 2025 09:31:19 +0100 Subject: [PATCH 16/23] add iteration over year --- climada/hazard/tc_tracks.py | 143 +++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 66 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 4fcd53ec57..d8624c410f 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1685,74 +1685,85 @@ def from_FAST(cls, folder_name: str): if Path(file).suffix != ".nc": continue with xr.open_dataset(file) as dataset: - for i in dataset.n_trk: - - # Select track - track = dataset.sel(n_trk=i) - # chunk dataset at first NaN value - lon = track.lon_trks.data - last_valid_index = np.where(np.isnan(lon))[0][0] - track = track.isel(time=slice(0, last_valid_index)) - # Select lat, lon - lat = track.lat_trks.data - lon = track.lon_trks.data - # Convert lon from 0-360 to -180 - 180 - lon = ((lon + 180) % 360) - 180 - # Convert time to pandas Datetime "yyyy.mm.dd" - reference_time = ( - f"{track.tc_years.item()}-{int(track.tc_month.item())}-01" - ) - time = pd.to_datetime( - track.time.data, unit="s", origin=reference_time - ).astype("datetime64[s]") - # Define variables - max_sustained_wind_knots = track.vmax_trks.data * 1.943844 - env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()] - cen_pres = _estimate_pressure( - np.full(lat.shape, np.nan), lat, lon, max_sustained_wind_knots - ) + for year in dataset.year: + for i in dataset.n_trk: + + # Select track + track = dataset.sel(n_trk=i, year=year) + # chunk dataset at first NaN value + lon = track.lon_trks.data + last_valid_index = np.where(np.isfinite(lon))[0][-1] + track = track.isel(time=slice(0, last_valid_index + 1)) + # Select lat, lon + lat = track.lat_trks.data + lon = track.lon_trks.data + # Convert lon from 0-360 to -180 - 180 + lon = ((lon + 180) % 360) - 180 + # Convert time to pandas Datetime "yyyy.mm.dd" + reference_time = ( + f"{track.tc_years.item()}-{int(track.tc_month.item())}-01" + ) + time = pd.to_datetime( + track.time.data, unit="s", origin=reference_time + ).astype("datetime64[s]") + # Define variables + max_sustained_wind_knots = track.vmax_trks.data * 1.943844 + env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()] + cen_pres = _estimate_pressure( + np.full(lat.shape, np.nan), + lat, + lon, + max_sustained_wind_knots, + ) - data.append( - xr.Dataset( - { - "time_step": ( - "time", - np.full(time.shape[0], track.time.data[1]), - ), - "max_sustained_wind": ("time", track.vmax_trks.data), - "central_pressure": ("time", cen_pres), - "radius_max_wind": ( - "time", - estimate_rmw(np.full(lat.shape, np.nan), cen_pres), - ), - "environmental_pressure": ( - "time", - np.full(time.shape[0], env_pressure), - ), - "basin": ( - "time", - np.full(time.shape[0], track.tc_basins.data.item()), - ), - }, - coords={ - "time": ("time", time), - "lat": ("time", lat), - "lon": ("time", lon), - }, - attrs={ - "max_sustained_wind_unit": "m/s", - "central_pressure_unit": "hPa", - "name": f"storm_{track.n_trk.item()}", - "sid": track.n_trk.item(), - "orig_event_flag": True, - "data_provider": "FAST", - "id_no": track.n_trk.item(), - "category": TCTracks.define_tc_category_FAST( - max_sustained_wind_knots - ), - }, + data.append( + xr.Dataset( + { + "time_step": ( + "time", + np.full(time.shape[0], track.time.data[1]), + ), + "max_sustained_wind": ( + "time", + track.vmax_trks.data, + ), + "central_pressure": ("time", cen_pres), + "radius_max_wind": ( + "time", + estimate_rmw( + np.full(lat.shape, np.nan), cen_pres + ), + ), + "environmental_pressure": ( + "time", + np.full(time.shape[0], env_pressure), + ), + "basin": ( + "time", + np.full( + time.shape[0], track.tc_basins.data.item() + ), + ), + }, + coords={ + "time": ("time", time), + "lat": ("time", lat), + "lon": ("time", lon), + }, + attrs={ + "max_sustained_wind_unit": "m/s", + "central_pressure_unit": "hPa", + "name": f"storm_{track.n_trk.item()}", + "sid": track.n_trk.item(), + "orig_event_flag": True, + "data_provider": "FAST", + "id_no": track.n_trk.item(), + "category": TCTracks.define_tc_category_FAST( + max_sustained_wind_knots + ), + }, + ) ) - ) return cls(data) From cc79d57bac34d0e8108b714a1bb2c4c81621e7a6 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Wed, 5 Feb 2025 15:38:01 +0100 Subject: [PATCH 17/23] remove assign category --- climada/hazard/tc_tracks.py | 44 +++------------------------ climada/hazard/test/test_tc_tracks.py | 11 ------- 2 files changed, 4 insertions(+), 51 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index d8624c410f..22eed5a8b0 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1620,42 +1620,6 @@ def from_netcdf(cls, folder_name): data.append(track) return cls(data) - @staticmethod - def define_tc_category_FAST(max_sust_wind: np.array, units: str = "kn") -> int: - """Define category of the tropical cyclone according to Saffir-Simpson scale. - - Parameters: - ---------- - max_wind : str - Maximal sustained wind speed - units: str - Wind speed units, kn or m/s. Default: kn - - Returns: - ------- - category : int - -1: "Tropical Depression", - 0: "Tropical Storm", - 1: "Hurricane Cat. 1", - 2: "Hurricane Cat. 2", - 3: "Hurricane Cat. 3", - 4: "Hurricane Cat. 4", - 5: "Hurricane Cat. 5", - """ - - max_sust_wind = max_sust_wind.astype( - float - ) # avoid casting errors if max_sust_wind is int - max_sust_wind *= 1.943844 if units == "m/s" else 1 - - max_wind = np.nanmax(max_sust_wind) - category_test = np.full(len(SAFFIR_SIM_CAT), max_wind) < np.array( - SAFFIR_SIM_CAT - ) - category = np.argmax(category_test) - 1 - - return category - @classmethod def from_FAST(cls, folder_name: str): """Create a new TCTracks object from NetCDF files generated by the FAST model, modifying @@ -1707,13 +1671,13 @@ def from_FAST(cls, folder_name: str): track.time.data, unit="s", origin=reference_time ).astype("datetime64[s]") # Define variables - max_sustained_wind_knots = track.vmax_trks.data * 1.943844 + max_wind_kn = track.vmax_trks.data * 1.943844 env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()] cen_pres = _estimate_pressure( np.full(lat.shape, np.nan), lat, lon, - max_sustained_wind_knots, + max_wind_kn, ) data.append( @@ -1758,8 +1722,8 @@ def from_FAST(cls, folder_name: str): "orig_event_flag": True, "data_provider": "FAST", "id_no": track.n_trk.item(), - "category": TCTracks.define_tc_category_FAST( - max_sustained_wind_knots + "category": set_category( + max_wind_kn, wind_unit="kn", saffir_scale=None ), }, ) diff --git a/climada/hazard/test/test_tc_tracks.py b/climada/hazard/test/test_tc_tracks.py index 8330771f39..2828fbfe3b 100644 --- a/climada/hazard/test/test_tc_tracks.py +++ b/climada/hazard/test/test_tc_tracks.py @@ -632,17 +632,6 @@ def test_from_simulations_storm(self): tc_track = tc.TCTracks.from_simulations_storm(TEST_TRACK_STORM, years=[7]) self.assertEqual(len(tc_track.data), 0) - def test_define_tc_category_FAST(self): - """test that the correct category is assigned to a TC from FAST model.""" - - max_wind = np.array([20, 72, 36, 50]) # knots - category1 = tc.TCTracks.define_tc_category_FAST(max_wind) - category2 = tc.TCTracks.define_tc_category_FAST( - max_sust_wind=max_wind, units="m/s" - ) - self.assertEqual(category1, 1) - self.assertEqual(category2, 5) - def test_from_FAST(self): """test the correct import of netcdf files from FAST model and the conversion to a different xr.array structure compatible with CLIMADA.""" From 894bb07f61e455be4908c631df729c318bdfb55b Mon Sep 17 00:00:00 2001 From: Nicolas Colombi <115944312+NicolasColombi@users.noreply.github.com> Date: Fri, 7 Feb 2025 07:32:41 +0100 Subject: [PATCH 18/23] Update climada/hazard/tc_tracks.py Co-authored-by: Samuel Juhel <10011382+spjuhel@users.noreply.github.com> --- climada/hazard/tc_tracks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 22eed5a8b0..64c99e5a57 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1643,7 +1643,7 @@ def from_FAST(cls, folder_name: str): TCTracks object with tracks data from the given directory of NetCDF files. """ - LOGGER.info("Reading %s files.", len(get_file_names(folder_name))) + LOGGER.info(f"Reading {len(get_file_names(folder_name))} files.") data = [] for file in get_file_names(folder_name): if Path(file).suffix != ".nc": From 45996db583bd25137e97a205a48216ada5a4e91a Mon Sep 17 00:00:00 2001 From: Nicolas Colombi <115944312+NicolasColombi@users.noreply.github.com> Date: Fri, 7 Feb 2025 07:32:50 +0100 Subject: [PATCH 19/23] Update climada/hazard/tc_tracks.py Co-authored-by: Samuel Juhel <10011382+spjuhel@users.noreply.github.com> --- climada/hazard/tc_tracks.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 64c99e5a57..de654766ee 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1645,9 +1645,7 @@ def from_FAST(cls, folder_name: str): LOGGER.info(f"Reading {len(get_file_names(folder_name))} files.") data = [] - for file in get_file_names(folder_name): - if Path(file).suffix != ".nc": - continue + for file in Path(folder_name).glob("*.nc"): with xr.open_dataset(file) as dataset: for year in dataset.year: for i in dataset.n_trk: From 829957a1ff70be97c6977d46b6e25bcd144aad43 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi <115944312+NicolasColombi@users.noreply.github.com> Date: Fri, 7 Feb 2025 07:37:13 +0100 Subject: [PATCH 20/23] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b8b347c7..e8f8084d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Code freeze date: YYYY-MM-DD ### Added -- `climada.hazard.tc_tracks.TCTracks.from_FAST` function [#993](https://github.com/CLIMADA-project/climada_python/pull/993) +- `climada.hazard.tc_tracks.TCTracks.from_FAST` function, add Australia basin (AU) [#993](https://github.com/CLIMADA-project/climada_python/pull/993) - Add `osm-flex` package to CLIMADA core [#981](https://github.com/CLIMADA-project/climada_python/pull/981) - `doc.tutorial.climada_entity_Exposures_osm.ipynb` tutorial explaining how to use `osm-flex`with CLIMADA - `climada.util.coordinates.bounding_box_global` function [#980](https://github.com/CLIMADA-project/climada_python/pull/980) From b9218b886e5366dbbfdc8af647bad50538039be6 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Fri, 7 Feb 2025 07:46:04 +0100 Subject: [PATCH 21/23] define m/s to kn variable --- climada/hazard/tc_tracks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index de654766ee..96c9b8cd7a 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1669,7 +1669,8 @@ def from_FAST(cls, folder_name: str): track.time.data, unit="s", origin=reference_time ).astype("datetime64[s]") # Define variables - max_wind_kn = track.vmax_trks.data * 1.943844 + ms_to_kn = 1.943844 + max_wind_kn = track.vmax_trks.data * ms_to_kn env_pressure = BASIN_ENV_PRESSURE[track.tc_basins.data.item()] cen_pres = _estimate_pressure( np.full(lat.shape, np.nan), From 67b99cd8f3c20b99339f5b83351b883fa0a1d3e0 Mon Sep 17 00:00:00 2001 From: Nicolas Colombi <115944312+NicolasColombi@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:10:47 +0100 Subject: [PATCH 22/23] Update climada/hazard/tc_tracks.py Co-authored-by: Samuel Juhel <10011382+spjuhel@users.noreply.github.com> --- climada/hazard/tc_tracks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 96c9b8cd7a..45adefe040 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1643,7 +1643,7 @@ def from_FAST(cls, folder_name: str): TCTracks object with tracks data from the given directory of NetCDF files. """ - LOGGER.info(f"Reading {len(get_file_names(folder_name))} files.") + LOGGER.info("Reading %s files.", len(get_file_names(folder_name))) data = [] for file in Path(folder_name).glob("*.nc"): with xr.open_dataset(file) as dataset: From 50c7218262cb6dd1bfe3629d7fab9e102f863c7a Mon Sep 17 00:00:00 2001 From: Nicolas Colombi Date: Wed, 12 Feb 2025 13:37:54 +0100 Subject: [PATCH 23/23] revert changes to open files --- climada/hazard/tc_tracks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index 45adefe040..ec63d0d906 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -1645,7 +1645,9 @@ def from_FAST(cls, folder_name: str): LOGGER.info("Reading %s files.", len(get_file_names(folder_name))) data = [] - for file in Path(folder_name).glob("*.nc"): + for file in get_file_names(folder_name): + if Path(file).suffix != ".nc": + continue with xr.open_dataset(file) as dataset: for year in dataset.year: for i in dataset.n_trk: