diff --git a/ci/unittests.yml b/ci/unittests.yml index 9291f8eb..4ffc7a79 100644 --- a/ci/unittests.yml +++ b/ci/unittests.yml @@ -7,6 +7,7 @@ dependencies: - cmweather - coverage - dask + - fsspec - h5netcdf - h5py - lat_lon_parser diff --git a/docs/history.md b/docs/history.md index 14b70185..b6f1e9f4 100644 --- a/docs/history.md +++ b/docs/history.md @@ -3,6 +3,7 @@ ## Development Version (unreleased) * MNT: Update GitHub actions, address DeprecationWarnings ({pull}`153`) by [@kmuehlbauer](https://github.com/kmuehlbauer). +* MNT: restructure odim.py/gamic.py, add test_odim.py/test_gamic.py ({pull}`154`) by [@kmuehlbauer](https://github.com/kmuehlbauer). ## 0.4.3 (2024-02-24) diff --git a/requirements.txt b/requirements.txt index 6ef54b93..13c7c0b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ cmweather dask -h5netcdf -h5py +h5netcdf >= 1.0.0 +h5py >= 3.0.0 lat_lon_parser netCDF4 numpy diff --git a/requirements_dev.txt b/requirements_dev.txt index e20aeb40..99ceb535 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -9,3 +9,4 @@ twine pytest black isort +fsspec diff --git a/tests/io/test_gamic.py b/tests/io/test_gamic.py new file mode 100644 index 00000000..eb1460ab --- /dev/null +++ b/tests/io/test_gamic.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# Copyright (c) 2024, openradar developers. +# Distributed under the MIT License. See LICENSE for more info. + +"""Tests for `io.backends.gamic` module. + +ported from wradlib +""" + +import numpy as np +import pytest + +from xradar.io.backends import gamic + + +def create_ray_header(nrays=360): + dtype = { + "names": [ + "azimuth_start", + "azimuth_stop", + "elevation_start", + "elevation_stop", + "timestamp", + ], + "formats": ["= 360] -= 360 + if nrays == 361: + arr = np.insert(arr, 10, (arr[10] + arr[9]) / 2, axis=0) + return arr + + +def create_how(nrays=360, stopaz=False): + how = dict(startazA=create_startazA(nrays)) + if stopaz: + how.update(stopazA=create_stopazA(nrays)) + return how + + +@pytest.mark.parametrize("stopaz", [False, True]) +def test_get_azimuth_how(stopaz): + how = create_how(stopaz=stopaz) + actual = odim._get_azimuth_how(how) + wanted = np.arange(0.5, 360, 1.0) + np.testing.assert_equal(actual, wanted) + + +@pytest.mark.parametrize("nrays", [180, 240, 360, 720]) +def test_get_azimuth_where(nrays): + where = dict(nrays=nrays) + actual = odim._get_azimuth_where(where) + udiff = np.unique(np.diff(actual)) + assert len(actual) == nrays + assert len(udiff) == 1 + assert udiff[0] == 360.0 / nrays + + +@pytest.mark.parametrize( + "ang", + [("az_angle", "elevation"), ("az_angle", "elevation"), ("elangle", "azimuth")], +) +def test_get_fixed_dim_and_angle(ang): + where = {ang[0]: 1.0} + dim, angle = odim._get_fixed_dim_and_angle(where) + assert dim == ang[1] + assert angle == 1.0 + + +def create_el_how(rhi): + if rhi: + return dict(startelA=1.0, stopelA=2.0) + else: + return dict(elangles=1.5) + + +@pytest.mark.parametrize("rhi", [True, False]) +def test_get_elevation_how(rhi): + how = create_el_how(rhi) + el = odim._get_elevation_how(how) + assert el == 1.5 + + +def test_get_elevation_where(): + where = dict(nrays=360, elangle=0.5) + actual = odim._get_elevation_where(where) + udiff = np.unique(actual) + assert len(actual) == 360 + assert len(udiff) == 1 + assert udiff[0] == 0.5 + assert actual.dtype == np.float32 + + +def test_get_time_how(): + how = dict(startazT=np.array([10, 20, 30]), stopazT=np.array([20, 30, 40])) + time = odim._get_time_how(how) + np.testing.assert_array_equal(time, np.array([15.0, 25.0, 35.0])) + + +@pytest.mark.parametrize("a1gate", [(0, 946684800.0416666), (10, 946684829.2083472)]) +@pytest.mark.parametrize("enddate", [True, False]) +def test_get_time_what(a1gate, enddate): + what = dict( + startdate="20000101", + starttime="000000", + ) + if enddate: + what.update(enddate="20000101", endtime="000030") + a1g = a1gate[1] + else: + a1g = 946684800.0 + where = dict(nrays=360, a1gate=a1gate[0]) + if not enddate: + check = pytest.warns( + UserWarning, match="Equal ODIM `starttime` and `endtime` values" + ) + else: + check = nullcontext() + with check: + time = odim._get_time_what(what, where) + assert time[0] == a1g + assert len(time) == 360 + + +@pytest.mark.parametrize("rscale", [100, 150, 300, 1000]) +def test_get_range(rscale): + where = dict(nbins=10, rstart=0, rscale=rscale) + rng, cent_first, bin_range = odim._get_range(where) + assert np.unique(np.diff(rng))[0] == rscale + assert cent_first == rscale / 2 + assert bin_range == rscale + + +@pytest.mark.parametrize( + "point", + [ + ("start", np.datetime64("2000-01-01T00:00:00", "s")), + ("end", np.datetime64("2000-01-01T00:00:30", "s")), + ], +) +def test_get_time(point): + what = dict( + startdate="20000101", starttime="000000", enddate="20000101", endtime="000030" + ) + time = odim._get_time(what, point=point[0]) + assert time == point[1] + + +def test_get_a1gate(): + where = dict(a1gate=20) + assert odim._get_a1gate(where) == 20 + + +def test_OdimH5NetCDFMetadata(odim_file): + store = odim.OdimStore.open(odim_file, group="sweep_0") + with pytest.warns(DeprecationWarning): + assert store.substore[0].root.first_dim == "azimuth" diff --git a/tests/test_util.py b/tests/test_util.py index 20baaf77..b3828791 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2022, openradar developers. +# Copyright (c) 2022-2024, openradar developers. # Distributed under the MIT License. See LICENSE for more info. """Tests for `xradar` util package.""" @@ -68,9 +68,9 @@ def test_extract_angle_parameters(): "expected_number_rays": 360, "first_angle": "azimuth", "max_angle": np.array(359.5111083984375), - "max_time": np.datetime64("2018-06-01T05:43:08.042000128"), + "max_time": np.datetime64("2018-06-01T05:43:08.042000000"), "min_angle": np.array(0.52459716796875), - "min_time": np.datetime64("2018-06-01T05:42:44.042000128"), + "min_time": np.datetime64("2018-06-01T05:42:44.042000000"), "missing_rays": np.array(False), "second_angle": "elevation", "start_angle": 0, diff --git a/xradar/io/backends/gamic.py b/xradar/io/backends/gamic.py index ec5e142f..4931bf18 100644 --- a/xradar/io/backends/gamic.py +++ b/xradar/io/backends/gamic.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2022, openradar developers. +# Copyright (c) 2022-2024, openradar developers. # Distributed under the MIT License. See LICENSE for more info. """ @@ -40,7 +40,6 @@ import numpy as np import xarray as xr from datatree import DataTree -from packaging.version import Version from xarray.backends.common import ( AbstractDataStore, BackendEntrypoint, @@ -55,18 +54,14 @@ from ... import util from ...model import ( - get_altitude_attrs, get_azimuth_attrs, get_elevation_attrs, - get_latitude_attrs, - get_longitude_attrs, - get_range_attrs, get_time_attrs, moment_attrs, sweep_vars_mapping, ) from .common import _assign_root, _attach_sweep_groups, _fix_angle, _get_h5group_names -from .odim import H5NetCDFArrayWrapper, _get_h5netcdf_encoding +from .odim import H5NetCDFArrayWrapper, _get_h5netcdf_encoding, _H5NetCDFMetadata HDF5_LOCK = SerializableLock() @@ -132,25 +127,50 @@ def _get_gamic_variable_name_and_attrs(attrs, dtype): return name, attrs -def _get_ray_header_data(dimensions, data, encoding): - ray_header = Variable(dimensions, data, {}, encoding) +def _get_range(how): + ngates = how["bin_count"] + bin_range = how["range_step"] * how["range_samples"] + cent_first = bin_range / 2.0 + range_data = np.arange( + cent_first, + bin_range * ngates, + bin_range, + dtype="float32", + ) + return range_data, cent_first, bin_range - azstart = ray_header.values["azimuth_start"] - azstop = ray_header.values["azimuth_stop"] + +def _get_fixed_dim_and_angle(how): + dims = {0: "elevation", 1: "azimuth"} + try: + dim = 1 + angle = np.round(how[dims[0]], decimals=1) + except KeyError: + dim = 0 + angle = np.round(how[dims[1]], decimals=1) + + return dims[dim], angle + + +def _get_azimuth(ray_header): + azstart = ray_header["azimuth_start"] + azstop = ray_header["azimuth_stop"] zero_index = np.where(azstop < azstart) azstop[zero_index[0]] += 360 - azimuth = (azstart + azstop) / 2.0 + return (azstart + azstop) / 2.0 + - elstart = ray_header.values["elevation_start"] - elstop = ray_header.values["elevation_stop"] - elevation = (elstart + elstop) / 2.0 +def _get_elevation(ray_header): + elstart = ray_header["elevation_start"] + elstop = ray_header["elevation_stop"] + return (elstart + elstop) / 2.0 - time = ray_header.values["timestamp"] / 1e6 - return {"azimuth": azimuth, "elevation": elevation, "time": time} +def _get_time(ray_header): + return ray_header["timestamp"] -class _GamicH5NetCDFMetadata: +class _GamicH5NetCDFMetadata(_H5NetCDFMetadata): """Wrapper around OdimH5 data fileobj for easy access of metadata. Parameters @@ -165,129 +185,51 @@ class _GamicH5NetCDFMetadata: object : metadata object """ - def __init__(self, fileobj, group): - self._root = fileobj - self._group = group - - @property - def first_dim(self): - dim, _ = self._get_fixed_dim_and_angle() - return dim - - def get_variable_dimensions(self, dims): - dimensions = [] - for n, _ in enumerate(dims): - if n == 0: - dimensions.append(self.first_dim) - elif n == 1: - dimensions.append("range") - else: - pass - return tuple(dimensions) - def coordinates(self, dimensions, data, encoding): - ray_header = _get_ray_header_data(dimensions, data, encoding) - dim, angle = self.fixed_dim_and_angle - - dims = ("azimuth", "elevation") - if dim == dims[1]: - dims = (dims[1], dims[0]) - - sweep_mode = "azimuth_surveillance" if dim == "azimuth" else "rhi" - sweep_number = int(self._group.split("/")[0][4:]) - prt_mode = "not_set" - follow_mode = "not_set" - - range_data, cent_first, bin_range = self.range - range_attrs = get_range_attrs(range_data) - - lon, lat, alt = self.site_coords - - coordinates = { - "azimuth": Variable( - (dims[0],), - ray_header["azimuth"], - get_azimuth_attrs(), - ), - "elevation": Variable( - (dims[0],), - ray_header["elevation"], - get_elevation_attrs(), - ), - "time": Variable( - (dims[0],), ray_header["time"], get_time_attrs("1970-01-01T00:00:00Z") - ), - "range": Variable(("range",), range_data, range_attrs), - "sweep_mode": Variable((), sweep_mode), - "sweep_number": Variable((), sweep_number), - "prt_mode": Variable((), prt_mode), - "follow_mode": Variable((), follow_mode), - "sweep_fixed_angle": Variable((), angle), - "longitude": Variable((), lon, get_longitude_attrs()), - "latitude": Variable((), lat, get_latitude_attrs()), - "altitude": Variable((), alt, get_altitude_attrs()), - } - - return coordinates - - @property - def site_coords(self): - return self._get_site_coords() - - @property - def time(self): - return self._get_time() + self._get_ray_header_data(dimensions, data, encoding) + return super().coordinates + + def _get_ray_header_data(self, dimensions, data, encoding): + ray_header = Variable(dimensions, data, {}, encoding) + self._azimuth = Variable( + (self.dim0,), + _get_azimuth(ray_header.values), + get_azimuth_attrs(), + ) - @property - def fixed_dim_and_angle(self): - return self._get_fixed_dim_and_angle() + self._elevation = Variable( + (self.dim0,), + _get_elevation(ray_header.values), + get_elevation_attrs(), + ) - @property - def range(self): - return self._get_range() + # keep microsecond resolution + self._time = Variable( + (self.dim0,), + _get_time(ray_header.values), + get_time_attrs("1970-01-01T00:00:00Z", "microseconds"), + ) @property - def what(self): - return self._get_dset_what() + def grp(self): + return self._root[self._group] def _get_fixed_dim_and_angle(self): - how = self._root[self._group]["how"].attrs - dims = {0: "elevation", 1: "azimuth"} - try: - dim = 1 - angle = np.round(how[dims[0]], decimals=1) - except KeyError: - dim = 0 - angle = np.round(how[dims[1]], decimals=1) - - return dims[dim], angle + return _get_fixed_dim_and_angle(self.how) def _get_range(self): - how = self._root[self._group]["how"].attrs - range_samples = how["range_samples"] - range_step = how["range_step"] - ngates = how["bin_count"] - bin_range = range_step * range_samples - cent_first = bin_range / 2.0 - range_data = np.arange( - cent_first, - bin_range * ngates, - bin_range, - dtype="float32", - ) - return range_data, cent_first, bin_range + return _get_range(self.how) def _get_time(self): - start = self._root[self._group]["how"].attrs["timestamp"] + start = self.how["timestamp"] start = dateutil.parser.parse(start) - start = start.replace(tzinfo=dt.timezone.utc).timestamp() + start = np.array(start.replace(tzinfo=dt.timezone.utc)).astype("= Version("0.8.0"): - kwargs["phony_dims"] = phony_dims - else: - raise ValueError( - "h5netcdf backend keyword argument 'phony_dims' needs " - "h5netcdf >= 0.8.0." - ) - if Version(h5netcdf.__version__) >= Version("0.10.0") and Version( - h5netcdf.core.h5py.__version__ - ) >= Version("3.0.0"): - kwargs["decode_vlen_strings"] = decode_vlen_strings + kwargs["phony_dims"] = phony_dims + + kwargs["decode_vlen_strings"] = decode_vlen_strings if lock is None: if util.has_import("dask"): @@ -400,8 +335,6 @@ def get_variables(self): ) def get_attrs(self): - # dim, angle = self.root.fixed_dim_and_angle - # attributes = {"fixed_angle": angle.item()} return FrozenDict() diff --git a/xradar/io/backends/odim.py b/xradar/io/backends/odim.py index 4aee848c..df3515d1 100644 --- a/xradar/io/backends/odim.py +++ b/xradar/io/backends/odim.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2022, openradar developers. +# Copyright (c) 2022-2024, openradar developers. # Distributed under the MIT License. See LICENSE for more info. """ @@ -34,12 +34,12 @@ import datetime as dt import io +import warnings import h5netcdf import numpy as np import xarray as xr from datatree import DataTree -from packaging.version import Version from xarray.backends.common import ( AbstractDataStore, BackendArray, @@ -88,8 +88,119 @@ def _calculate_angle_res(dim): return np.round(np.nanmean(angle_diff_wanted), decimals=2) -class _OdimH5NetCDFMetadata: - """Wrapper around OdimH5 data fileobj for easy access of metadata. +def _get_azimuth_how(how): + startaz = how["startazA"] + stopaz = how.get("stopazA", False) + if stopaz is False: + # stopazA missing + # create from startazA + stopaz = np.roll(startaz, -1) + stopaz[-1] += 360 + zero_index = np.where(stopaz < startaz) + stopaz[zero_index[0]] += 360 + azimuth_data = (startaz + stopaz) / 2.0 + azimuth_data[azimuth_data >= 360] -= 360 + return azimuth_data + + +def _get_azimuth_where(where): + res = 360.0 / where["nrays"] + return np.arange(res / 2.0, 360.0, res, dtype="float32") + + +def _get_fixed_dim_and_angle(where): + dim = "elevation" + + # try RHI first + angle_keys = ["az_angle", "azangle"] + angle = None + for ak in angle_keys: + angle = where.get(ak, None) + if angle is not None: + break + if angle is None: + dim = "azimuth" + angle = where["elangle"] + + # do not round angle + # angle = np.round(angle, decimals=1) + return dim, angle + + +def _get_elevation_how(how): + startel = how.get("startelA", False) + stopel = how.get("stopelA", False) + if startel is not False and stopel is not False: + elevation_data = (startel + stopel) / 2.0 + else: + elevation_data = how["elangles"] + return elevation_data + + +def _get_elevation_where(where): + return np.ones(where["nrays"], dtype="float32") * where["elangle"] + + +def _get_time_how(how): + return (how["startazT"] + how["stopazT"]) / 2.0 + + +def _get_time_what(what, where, nrays=None): + startdate = _maybe_decode(what["startdate"]) + starttime = _maybe_decode(what["starttime"]) + # take care for missing enddate/endtime + # see https://github.com/wradlib/wradlib/issues/563 + enddate = _maybe_decode(what.get("enddate", what["startdate"])) + endtime = _maybe_decode(what.get("endtime", what["starttime"])) + start = dt.datetime.strptime(startdate + starttime, "%Y%m%d%H%M%S") + end = dt.datetime.strptime(enddate + endtime, "%Y%m%d%H%M%S") + start = start.replace(tzinfo=dt.timezone.utc).timestamp() + end = end.replace(tzinfo=dt.timezone.utc).timestamp() + if nrays is None: + nrays = where["nrays"] + if start == end: + import warnings + + warnings.warn( + "xradar: Equal ODIM `starttime` and `endtime` " + "values. Can't determine correct sweep start-, " + "end- and raytimes.", + UserWarning, + ) + + time_data = np.ones(nrays) * start + else: + delta = (end - start) / nrays + time_data = np.arange(start + delta / 2.0, end, delta) + time_data = np.roll(time_data, shift=+where["a1gate"]) + return time_data + + +def _get_range(where): + ngates = where["nbins"] + range_start = where["rstart"] * 1000.0 + bin_range = where["rscale"] + cent_first = range_start + bin_range / 2.0 + range_data = np.arange( + cent_first, range_start + bin_range * ngates, bin_range, dtype="float32" + ) + return range_data, cent_first, bin_range + + +def _get_time(what, point="start"): + startdate = _maybe_decode(what[f"{point}date"]) + starttime = _maybe_decode(what[f"{point}time"]) + start = dt.datetime.strptime(startdate + starttime, "%Y%m%d%H%M%S") + start = np.array(start.replace(tzinfo=dt.timezone.utc)).astype("= 360] -= 360 - return azimuth_data - - def _get_azimuth_where(self): - grp = self._group.split("/")[0] - nrays = self._root[grp]["where"].attrs["nrays"] - res = 360.0 / nrays - azimuth_data = np.arange(res / 2.0, 360.0, res, dtype="float32") - return azimuth_data + @property + def grp(self): + return self._root[self._group.split("/")[0]] def _get_fixed_dim_and_angle(self): - grp = self._group.split("/")[0] - dim = "elevation" - - # try RHI first - angle_keys = ["az_angle", "azangle"] - angle = None - for ak in angle_keys: - angle = self._root[grp]["where"].attrs.get(ak, None) - if angle is not None: - break - if angle is None: - dim = "azimuth" - angle = self._root[grp]["where"].attrs["elangle"] - - # do not round angle - # angle = np.round(angle, decimals=1) - return dim, angle - - def _get_elevation_how(self): - grp = self._group.split("/")[0] - how = self._root[grp]["how"].attrs - startaz = how.get("startelA", False) - stopaz = how.get("stopelA", False) - if startaz is not False and stopaz is not False: - elevation_data = (startaz + stopaz) / 2.0 - else: - elevation_data = how["elangles"] - return elevation_data - - def _get_elevation_where(self): - grp = self._group.split("/")[0] - nrays = self._root[grp]["where"].attrs["nrays"] - elangle = self._root[grp]["where"].attrs["elangle"] - elevation_data = np.ones(nrays, dtype="float32") * elangle - return elevation_data - - def _get_time_how(self): - grp = self._group.split("/")[0] - startT = self._root[grp]["how"].attrs["startazT"] - stopT = self._root[grp]["how"].attrs["stopazT"] - time_data = (startT + stopT) / 2.0 - return time_data - - def _get_time_what(self, nrays=None): - grp = self._group.split("/")[0] - what = self._root[grp]["what"].attrs - startdate = _maybe_decode(what["startdate"]) - starttime = _maybe_decode(what["starttime"]) - # take care for missing enddate/endtime - # see https://github.com/wradlib/wradlib/issues/563 - enddate = _maybe_decode(what.get("enddate", what["startdate"])) - endtime = _maybe_decode(what.get("endtime", what["starttime"])) - start = dt.datetime.strptime(startdate + starttime, "%Y%m%d%H%M%S") - end = dt.datetime.strptime(enddate + endtime, "%Y%m%d%H%M%S") - start = start.replace(tzinfo=dt.timezone.utc).timestamp() - end = end.replace(tzinfo=dt.timezone.utc).timestamp() - if nrays is None: - nrays = self._root[grp]["where"].attrs["nrays"] - if start == end: - import warnings - - warnings.warn( - "xradar: Equal ODIM `starttime` and `endtime` " - "values. Can't determine correct sweep start-, " - "end- and raytimes.", - UserWarning, - ) - - time_data = np.ones(nrays) * start - else: - delta = (end - start) / nrays - time_data = np.arange(start + delta / 2.0, end, delta) - time_data = np.roll(time_data, shift=+self.a1gate) - return time_data + return _get_fixed_dim_and_angle(self.where) def _get_ray_times(self, nrays=None): try: - time_data = self._get_time_how() + time_data = _get_time_how(self.how) self._need_time_recalc = False except (AttributeError, KeyError, TypeError): - time_data = self._get_time_what(nrays=nrays) + time_data = _get_time_what(self.what, self.where, nrays) self._need_time_recalc = True return time_data def _get_range(self): - grp = self._group.split("/")[0] - where = self._root[grp]["where"].attrs - ngates = where["nbins"] - range_start = where["rstart"] * 1000.0 - bin_range = where["rscale"] - cent_first = range_start + bin_range / 2.0 - range_data = np.arange( - cent_first, range_start + bin_range * ngates, bin_range, dtype="float32" - ) - return range_data, cent_first, bin_range + return _get_range(self.where) def _get_time(self, point="start"): - grp = self._group.split("/")[0] - what = self._root[grp]["what"].attrs - startdate = _maybe_decode(what[f"{point}date"]) - starttime = _maybe_decode(what[f"{point}time"]) - start = dt.datetime.strptime(startdate + starttime, "%Y%m%d%H%M%S") - start = start.replace(tzinfo=dt.timezone.utc).timestamp() - return start - - def _get_a1gate(self): - grp = self._group.split("/")[0] - a1gate = self._root[grp]["where"].attrs["a1gate"] - return a1gate - - def _get_site_coords(self): - lon = self._root["where"].attrs["lon"] - lat = self._root["where"].attrs["lat"] - alt = self._root["where"].attrs["height"] - return lon, lat, alt + return _get_time(self.what, point=point) def _get_dset_what(self): attrs = {} @@ -363,27 +434,32 @@ def _get_dset_what(self): @property def a1gate(self): - return self._get_a1gate() + return _get_a1gate(self.where) @property - def azimuth(self): + def _azimuth(self): try: - azimuth = self._get_azimuth_how() + azimuth = _get_azimuth_how(self.how) except (AttributeError, KeyError, TypeError): - azimuth = self._get_azimuth_where() - return azimuth + azimuth = _get_azimuth_where(self.where) + return Variable((self.dim0,), azimuth, get_azimuth_attrs()) @property - def elevation(self): + def _elevation(self): try: - elevation = self._get_elevation_how() + elevation = _get_elevation_how(self.how) except (AttributeError, KeyError, TypeError): - elevation = self._get_elevation_where() - return elevation + elevation = _get_elevation_where(self.where) + return Variable((self.dim0,), elevation, get_elevation_attrs()) + + @property + def _time(self): + return Variable((self.dim0,), self._get_ray_times(), get_time_attrs()) @property - def ray_times(self): - return self._get_ray_times() + def _sweep_number(self): + """Return sweep number.""" + return int(self._group.split("/")[0][7:]) - 1 class H5NetCDFArrayWrapper(BackendArray): @@ -464,6 +540,7 @@ def _get_h5netcdf_encoding(self, var): vlen_dtype = h5py.check_dtype(vlen=var.dtype) if vlen_dtype is str: encoding["dtype"] = str + # todo: this might be removed, since xarray is capable from version 2023.X.Y elif vlen_dtype is not None: # pragma: no cover # xarray doesn't support writing arbitrary vlen dtypes yet. pass @@ -528,7 +605,7 @@ def open_store_variable(self, name, var): data = indexing.LazilyOuterIndexedArray(H5NetCDFArrayWrapper(name, self)) encoding = _get_h5netcdf_encoding(self, var) encoding["group"] = self._group - name, attrs = _get_odim_variable_name_and_attrs(name, self.root.what) + name, attrs = _get_odim_variable_name_and_attrs(name, self.root.ds_what) return name, Variable(dimensions, data, attrs, encoding) @@ -596,17 +673,8 @@ def open( kwargs = {"invalid_netcdf": invalid_netcdf} if phony_dims is not None: - if Version(h5netcdf.__version__) >= Version("0.8.0"): - kwargs["phony_dims"] = phony_dims - else: - raise ValueError( - "h5netcdf backend keyword argument 'phony_dims' needs " - "h5netcdf >= 0.8.0." - ) - if Version(h5netcdf.__version__) >= Version("0.10.0") and Version( - h5netcdf.core.h5py.__version__ - ) >= Version("3.0.0"): - kwargs["decode_vlen_strings"] = decode_vlen_strings + kwargs["phony_dims"] = phony_dims + kwargs["decode_vlen_strings"] = decode_vlen_strings if lock is None: if util.has_import("dask"): @@ -664,10 +732,7 @@ def get_variables(self): ) def get_attrs(self): - dim, angle = self.substore[0].root.fixed_dim_and_angle - attributes = {} - # attributes["fixed_angle"] = angle.item() - return FrozenDict(attributes) + return FrozenDict() class OdimBackendEntrypoint(BackendEntrypoint): diff --git a/xradar/model.py b/xradar/model.py index ed37b641..c5d91f4f 100644 --- a/xradar/model.py +++ b/xradar/model.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2022-2023, openradar developers. +# Copyright (c) 2022-2024, openradar developers. # Distributed under the MIT License. See LICENSE for more info. """ @@ -701,10 +701,10 @@ def get_elevation_attrs(ele=None): return el_attrs -def get_time_attrs(date_str): +def get_time_attrs(date_str="1970-01-01T00:00:00Z", date_unit="seconds"): time_attrs = { "standard_name": "time", - "units": f"seconds since {date_str}", + "units": f"{date_unit} since {date_str}", } return time_attrs