From 6fedaf296fc52689a510cd314ace9b8facc77a7f Mon Sep 17 00:00:00 2001 From: Thomas Vogt <57705593+tovogt@users.noreply.github.com> Date: Tue, 27 Jun 2023 11:43:11 +0200 Subject: [PATCH] TCTracks: improve hdf5 I/O (#735) Use more efficient file format for cycling TCTracks objects. This change is not backwards compatible. Users will have to reload the original data and store the TCTracks objects again. --------- Co-authored-by: emanuel-schmid Co-authored-by: Lukas Riedel <34276446+peanutfun@users.noreply.github.com> --- CHANGELOG.md | 1 + climada/hazard/tc_tracks.py | 189 ++++++++---------- .../hazard/test/data/tctracks_hdf5_legacy.nc | Bin 0 -> 89975 bytes climada/hazard/test/test_tc_tracks.py | 6 + 4 files changed, 93 insertions(+), 103 deletions(-) create mode 100644 climada/hazard/test/data/tctracks_hdf5_legacy.nc diff --git a/CHANGELOG.md b/CHANGELOG.md index 9105b34c6..4f2c1ef9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ Removed: - Users can opt-out of the climada specific logging definitions and freely configure logging to their will, by setting the config value `logging.managed` to `false`. [#724](https://github.com/CLIMADA-project/climada_python/pull/724) - Add option to read additional variables from IBTrACS when using `TCTracks.from_ibtracs_netcdf` [#728](https://github.com/CLIMADA-project/climada_python/pull/728) - The `haz_type` attribute has been moved from `climada.hazard.tag.Tag` to `climada.hazard.Hazard` itself. [#736](https://github.com/CLIMADA-project/climada_python/pull/736) +- New file format for `TCTracks` I/O with better performance. This change is not backwards compatible: If you stored `TCTracks` objects with `TCTracks.write_hdf5`, reload the original data and store them again. [#735](https://github.com/CLIMADA-project/climada_python/pull/735) ### Fixed diff --git a/climada/hazard/tc_tracks.py b/climada/hazard/tc_tracks.py index ccf959920..5e7b3916b 100644 --- a/climada/hazard/tc_tracks.py +++ b/climada/hazard/tc_tracks.py @@ -22,12 +22,10 @@ __all__ = ['CAT_NAMES', 'SAFFIR_SIM_CAT', 'TCTracks', 'set_category'] # standard libraries -import contextlib import datetime as dt import itertools import logging from typing import Optional, List -import pathlib import re import shutil import warnings @@ -53,10 +51,6 @@ from sklearn.metrics import DistanceMetric import statsmodels.api as sm import xarray as xr -from xarray.backends import NetCDF4DataStore -from xarray.backends.api import dump_to_store -from xarray.backends.common import ArrayWriter -from xarray.backends.store import StoreBackendEntrypoint # climada dependencies from climada.util import ureg @@ -1366,21 +1360,27 @@ def write_hdf5(self, file_name, complevel=5): Specifies a compression level (0-9) for the zlib compression of the data. A value of 0 or None disables compression. Default: 5 """ - # change dtype from bool to int to be NetCDF4-compliant, this is undone later + data = [] for track in self.data: + # convert "time" into a data variable and use a neutral name for the steps + track = track.rename(time="step") + track["time"] = ("step", track["step"].values) + track["step"] = np.arange(track.sizes["step"]) + # change dtype from bool to int to be NetCDF4-compliant track.attrs['orig_event_flag'] = int(track.attrs['orig_event_flag']) - try: - encoding = { - f'track{i}': {var: dict(zlib=True, complevel=complevel) for var in track.data_vars} - for i, track in enumerate(self.data) - } - ds_dict = {f'track{i}': track for i, track in enumerate(self.data)} - LOGGER.info('Writing %d tracks to %s', self.size, file_name) - _xr_to_netcdf_multi(file_name, ds_dict, encoding=encoding) - finally: - # ensure to undo the temporal change of dtype from above - for track in self.data: - track.attrs['orig_event_flag'] = bool(track.attrs['orig_event_flag']) + data.append(track) + + # concatenate all data sets along new dimension "storm" + ds_combined = xr.combine_nested(data, concat_dim=["storm"]) + ds_combined["storm"] = np.arange(ds_combined.sizes["storm"]) + + # convert attributes to data variables of combined dataset + df_attrs = pd.DataFrame([t.attrs for t in data], index=ds_combined["storm"].to_series()) + ds_combined = xr.merge([ds_combined, df_attrs.to_xarray()]) + + encoding = {v: dict(zlib=True, complevel=complevel) for v in ds_combined.data_vars} + LOGGER.info('Writing %d tracks to %s', self.size, file_name) + ds_combined.to_netcdf(file_name, encoding=encoding) @classmethod def from_hdf5(cls, file_name): @@ -1396,16 +1396,32 @@ def from_hdf5(cls, file_name): tracks : TCTracks TCTracks with data from the given HDF5 file. """ - ds_dict = _xr_open_dataset_multi(file_name, prefix="track") - track_no = sorted(int(key[5:]) for key in ds_dict.keys()) + _raise_if_legacy_or_unknown_hdf5_format(file_name) + ds_combined = xr.open_dataset(file_name) + + # when writing ' 0: + # The legacy format only has data in the subgroups, not in the root. + # => This cannot be the legacy file format! + return + # After this line, it is sure that the format is not supported (since there is no data in the + # root group). Before raising an exception, we double-check if it is the legacy format. + try: + # Check if the file has groups 'track{i}' by trying to access the first group. + with xr.open_dataset(file_name, group="track0") as ds_track: + # Check if the group actually contains track data: + is_legacy = ( + "time" in ds_track.dims + and "max_sustained_wind" in ds_track.variables + ) + except OSError as err: + if "group not found" in str(err): + # No 'track0' group. => This cannot be the legacy file format! + is_legacy = False + else: + # A different (unexpected) error occurred. => Re-raise the exception. + raise + + raise ValueError( + ( + f"The file you try to read ({file_name}) is in a format that is no longer" + " supported by CLIMADA. Please store the data again using" + " TCTracks.write_hdf5. If you struggle to convert the data, please open an" + " issue on GitHub." + ) if is_legacy else ( + f"Unknown HDF5/NetCDF file format: {file_name}" + ) + ) def _read_one_gettelman(nc_data, i_track): """Read a single track from Andrew Gettelman's NetCDF dataset diff --git a/climada/hazard/test/data/tctracks_hdf5_legacy.nc b/climada/hazard/test/data/tctracks_hdf5_legacy.nc new file mode 100644 index 0000000000000000000000000000000000000000..a588e9c3e586d296f77c9240ccf6ac2023bab10c GIT binary patch literal 89975 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