pyodim 0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyodim-0.4/.gitignore ADDED
@@ -0,0 +1,130 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+ .vscode/settings.json
pyodim-0.4/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Valentin Louf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pyodim-0.4/PKG-INFO ADDED
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.1
2
+ Name: pyodim
3
+ Version: 0.4
4
+ Summary: An ODIM hdf5 file reader.
5
+ Home-page: https://github.com/vlouf/pyodim
6
+ Author: Valentin Louf
7
+ Author-email: valentin.louf@bom.gov.au
8
+ Project-URL: Bug Reports, https://github.com/vlouf/pyodim/issues
9
+ Project-URL: Source, https://github.com/vlouf/pyodim/
10
+ Keywords: odim h5 file reader
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+
23
+ # PYODIM
24
+
25
+ A simple ODIM H5 (Opera Data Information Model - Hierarchical Data Format
26
+ version 5) radar reader in Python. It outputs the radar date in an xarray
27
+ Dataset. The goal is to be as barebone as possible while providing an xarray,
28
+ so that it can easily scale up (production) or just a very lean reader for
29
+ quicklooks into the data.
30
+
31
+ ## Example
32
+
33
+ ```python
34
+
35
+ import pyodim
36
+
37
+ filename = "/path/to/radar/file.h5"
38
+ rset = pyodim.read_odim(filename, lazy_load=True) # Lazy load does not load the data in memory to save time if you are interested in only a specific sweep
39
+ radar = rset[0].compute() # All sweeps are in a list (elevation ascending), so the 1st item of the list is the bottom sweep.
40
+ print(radar)
41
+ ```
42
+
43
+ This code will output the radar data for the first sweep (lowest elevation
44
+ scan) in the radar file as an xarray Dataset, for example:
45
+
46
+ ```python
47
+ <xarray.Dataset>
48
+ Dimensions: (azimuth: 360, range: 1283, elevation: 1, time: 360)
49
+ Coordinates:
50
+ * range (range) float32 125.0 375.0 625.0 ... 3.204e+05 3.206e+05
51
+ * azimuth (azimuth) float32 0.01111 1.011 2.011 ... 357.0 358.0 359.0
52
+ * elevation (elevation) float32 0.5
53
+ * time (time) datetime64[ns] 2024-01-16T02:49:29.337047353 ... 2024-...
54
+ Data variables: (12/17)
55
+ DBZH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
56
+ SNRH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
57
+ VRADH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
58
+ WRADH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
59
+ KDP (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
60
+ ... ...
61
+ x (azimuth, range) float32 0.02424 0.07271 ... -5.595e+03
62
+ y (azimuth, range) float32 125.0 375.0 ... 3.203e+05 3.206e+05
63
+ z (azimuth, range) float32 45.09 47.27 ... 2.84e+03 2.842e+03
64
+ longitude (azimuth, range) float32 144.8 144.8 144.8 ... 144.7 144.7 144.7
65
+ latitude (azimuth, range) float32 -37.85 -37.85 -37.85 ... -34.97 -34.96
66
+ Attributes: (12/15)
67
+ Conventions: ODIM_H5/V2_4
68
+ latitude: -37.85200119018555
69
+ longitude: 144.75199890136722
70
+ height: 44.0
71
+ date: 2024-01-16T02:45:21
72
+ object: PVOL
73
+ ... ...
74
+ beamwV: 1.0
75
+ wavelength: 10.489999771118164
76
+ NI: 36.5052
77
+ highprf: 696.0
78
+ start_time: 20240116_024928
79
+ end_time: 20240116_024958
80
+ ```
81
+
82
+ PYODIM reads the ODIM radar data and metadata. It also provides some quality of
83
+ life feature like automatically creating the x, y, z and latitude/longitude
84
+ coordinates for all radar gates (using the azimuthal equidistant - `aeqd` - projection). By
85
+ default it uses `dask.delayed` to *lazily* load the data, meaning that it will
86
+ not actually load all the sweeps in memory, as for many applications we often
87
+ don't need all the radar sweeps. Instead, by calling the `.compute()` method
88
+ like in the example above, PYODIM will only read and load in memory the sweep
89
+ that you want, saving time. If you want to read all the sweeps at once in a
90
+ list, then just set `lazy_load=False`.
91
+
92
+ ## Dependencies
93
+
94
+ Mandatory:
95
+ - [numpy][1]
96
+ - [xarray][2]
97
+ - [pyproj][3]
98
+ - [dask][4]
99
+
100
+ And optionally (it will automatically populate the fields metadata using pyart
101
+ if available):
102
+ - [Py-ART][5]
103
+
104
+ [1]: http://www.scipy.org/
105
+ [2]: http://numba.pydata.org
106
+ [3]: https://pypi.org/project/pyproj/
107
+ [4]: https://www.dask.org/
108
+ [5]: https://github.com/ARM-DOE/pyart
pyodim-0.4/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # PYODIM
2
+
3
+ A simple ODIM H5 (Opera Data Information Model - Hierarchical Data Format
4
+ version 5) radar reader in Python. It outputs the radar date in an xarray
5
+ Dataset. The goal is to be as barebone as possible while providing an xarray,
6
+ so that it can easily scale up (production) or just a very lean reader for
7
+ quicklooks into the data.
8
+
9
+ ## Example
10
+
11
+ ```python
12
+
13
+ import pyodim
14
+
15
+ filename = "/path/to/radar/file.h5"
16
+ rset = pyodim.read_odim(filename, lazy_load=True) # Lazy load does not load the data in memory to save time if you are interested in only a specific sweep
17
+ radar = rset[0].compute() # All sweeps are in a list (elevation ascending), so the 1st item of the list is the bottom sweep.
18
+ print(radar)
19
+ ```
20
+
21
+ This code will output the radar data for the first sweep (lowest elevation
22
+ scan) in the radar file as an xarray Dataset, for example:
23
+
24
+ ```python
25
+ <xarray.Dataset>
26
+ Dimensions: (azimuth: 360, range: 1283, elevation: 1, time: 360)
27
+ Coordinates:
28
+ * range (range) float32 125.0 375.0 625.0 ... 3.204e+05 3.206e+05
29
+ * azimuth (azimuth) float32 0.01111 1.011 2.011 ... 357.0 358.0 359.0
30
+ * elevation (elevation) float32 0.5
31
+ * time (time) datetime64[ns] 2024-01-16T02:49:29.337047353 ... 2024-...
32
+ Data variables: (12/17)
33
+ DBZH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
34
+ SNRH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
35
+ VRADH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
36
+ WRADH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
37
+ KDP (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
38
+ ... ...
39
+ x (azimuth, range) float32 0.02424 0.07271 ... -5.595e+03
40
+ y (azimuth, range) float32 125.0 375.0 ... 3.203e+05 3.206e+05
41
+ z (azimuth, range) float32 45.09 47.27 ... 2.84e+03 2.842e+03
42
+ longitude (azimuth, range) float32 144.8 144.8 144.8 ... 144.7 144.7 144.7
43
+ latitude (azimuth, range) float32 -37.85 -37.85 -37.85 ... -34.97 -34.96
44
+ Attributes: (12/15)
45
+ Conventions: ODIM_H5/V2_4
46
+ latitude: -37.85200119018555
47
+ longitude: 144.75199890136722
48
+ height: 44.0
49
+ date: 2024-01-16T02:45:21
50
+ object: PVOL
51
+ ... ...
52
+ beamwV: 1.0
53
+ wavelength: 10.489999771118164
54
+ NI: 36.5052
55
+ highprf: 696.0
56
+ start_time: 20240116_024928
57
+ end_time: 20240116_024958
58
+ ```
59
+
60
+ PYODIM reads the ODIM radar data and metadata. It also provides some quality of
61
+ life feature like automatically creating the x, y, z and latitude/longitude
62
+ coordinates for all radar gates (using the azimuthal equidistant - `aeqd` - projection). By
63
+ default it uses `dask.delayed` to *lazily* load the data, meaning that it will
64
+ not actually load all the sweeps in memory, as for many applications we often
65
+ don't need all the radar sweeps. Instead, by calling the `.compute()` method
66
+ like in the example above, PYODIM will only read and load in memory the sweep
67
+ that you want, saving time. If you want to read all the sweeps at once in a
68
+ list, then just set `lazy_load=False`.
69
+
70
+ ## Dependencies
71
+
72
+ Mandatory:
73
+ - [numpy][1]
74
+ - [xarray][2]
75
+ - [pyproj][3]
76
+ - [dask][4]
77
+
78
+ And optionally (it will automatically populate the fields metadata using pyart
79
+ if available):
80
+ - [Py-ART][5]
81
+
82
+ [1]: http://www.scipy.org/
83
+ [2]: http://numba.pydata.org
84
+ [3]: https://pypi.org/project/pyproj/
85
+ [4]: https://www.dask.org/
86
+ [5]: https://github.com/ARM-DOE/pyart
@@ -0,0 +1 @@
1
+ from .pyodim import read_odim
@@ -0,0 +1 @@
1
+ __version__ = '0.3'
@@ -0,0 +1,511 @@
1
+ """
2
+ Natively reading ODIM H5 radar files in Python.
3
+
4
+ @title: pyodim
5
+ @author: Valentin Louf <valentin.louf@bom.gov.au>
6
+ @institutions: Bureau of Meteorology and Monash University.
7
+ @creation: 21/01/2020
8
+ @date: 19/02/2021
9
+
10
+ .. autosummary::
11
+ :toctree: generated/
12
+
13
+ _to_str
14
+ buffer
15
+ cartesian_to_geographic
16
+ check_nyquist
17
+ coord_from_metadata
18
+ field_metadata
19
+ generate_timestamp
20
+ get_dataset_metadata
21
+ get_root_metadata
22
+ radar_coordinates_to_xyz
23
+ read_odim_slice
24
+ read_odim
25
+ """
26
+ import datetime
27
+ import traceback
28
+ from typing import Dict, List, Tuple
29
+
30
+ import dask
31
+ import h5py
32
+ import pyproj
33
+ import pandas as pd
34
+ import numpy as np
35
+ import xarray as xr
36
+
37
+
38
+ def _to_str(t: bytes) -> str:
39
+ """
40
+ Transform binary into string.
41
+ """
42
+ return t.decode("utf-8")
43
+
44
+
45
+ def buffer(func):
46
+ """
47
+ Decorator to catch and kill error message. Almost want to name the function
48
+ dont_fail.
49
+ """
50
+
51
+ def wrapper(*args, **kwargs):
52
+ try:
53
+ rslt = func(*args, **kwargs)
54
+ except Exception:
55
+ traceback.print_exc()
56
+ rslt = None
57
+ return rslt
58
+
59
+ return wrapper
60
+
61
+
62
+ def cartesian_to_geographic(x: np.ndarray, y: np.ndarray, lon0: float, lat0: float) -> Tuple[np.ndarray, np.ndarray]:
63
+ """
64
+ Transform cartesian coordinates to lat/lon using the Azimuth Equidistant
65
+ projection.
66
+
67
+ Parameters:
68
+ ===========
69
+ x: ndarray
70
+ x-axis cartesian coordinates.
71
+ y: ndarray
72
+ y-axis cartesian coordinates. Same dimension as x
73
+ lon0: float
74
+ Radar site longitude.
75
+ lat0: float
76
+ Radar site latitude.
77
+
78
+ Returns:
79
+ lon: ndarray
80
+ Longitude of each gate.
81
+ lat: ndarray
82
+ Latitude of each gate.
83
+ """
84
+ georef = pyproj.Proj(f"+proj=aeqd +lon_0={lon0} +lat_0={lat0} +ellps=WGS84")
85
+ lon, lat = georef(x, y, inverse=True)
86
+ lon = lon.astype(np.float32)
87
+ lat = lat.astype(np.float32)
88
+ return lon, lat
89
+
90
+
91
+ @buffer
92
+ def check_nyquist(dset: xr.Dataset) -> None:
93
+ """
94
+ Check if the dataset Nyquist velocity corresponds to the PRF information.
95
+ """
96
+ wavelength = dset.attrs["wavelength"]
97
+ prf = dset.attrs["highprf"]
98
+ nyquist = dset.attrs["NI"]
99
+ ny_int = 1e-2 * prf * wavelength / 4
100
+
101
+ assert np.abs(nyquist - ny_int) < 0.5, "Nyquist not consistent with PRF"
102
+
103
+
104
+ def coord_from_metadata(metadata: Dict) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
105
+ """
106
+ Create the radar coordinates from the ODIM H5 metadata specification.
107
+
108
+ Parameter:
109
+ ==========
110
+ metadata: dict()
111
+ Metadata dictionnary containing the specific ODIM H5 keys: astart,
112
+ nrays, nbins, rstart, rscale, elangle.
113
+
114
+ Returns:
115
+ ========
116
+ r: ndarray<nbins>
117
+ Sweep range
118
+ azimuth: ndarray<nrays>
119
+ Sweep azimuth
120
+ elev: float
121
+ Sweep elevation
122
+ """
123
+ da = 360 / metadata["nrays"]
124
+ azimuth = np.linspace(metadata["astart"] + da / 2, 360 - da, metadata["nrays"], dtype=np.float32)
125
+
126
+ # rstart is in KM !!! STUPID.
127
+ rstart_center = 1e3 * metadata["rstart"] + metadata["rscale"] / 2
128
+ r = np.arange(
129
+ rstart_center, rstart_center + metadata["nbins"] * metadata["rscale"], metadata["rscale"], dtype=np.float32
130
+ )
131
+
132
+ elev = np.array([metadata["elangle"]], dtype=np.float32)
133
+ return r, azimuth, elev
134
+
135
+
136
+ def field_metadata(quantity_name: str) -> Dict:
137
+ """
138
+ Populate metadata for common fields using Py-ART get_metadata() function.
139
+ (Optionnal).
140
+
141
+ Parameter:
142
+ ==========
143
+ quantity_name: str
144
+ ODIM H5 quantity attribute name.
145
+
146
+ Returns:
147
+ ========
148
+ attrs: dict()
149
+ Metadata dictionnary.
150
+ """
151
+ try:
152
+ from pyart.config import get_metadata
153
+ except Exception:
154
+ return {}
155
+ ODIM_H5_FIELD_NAMES = {
156
+ "TH": "total_power", # uncorrected reflectivity, horizontal
157
+ "TV": "total_power", # uncorrected reflectivity, vertical
158
+ "DBZH": "reflectivity", # corrected reflectivity, horizontal
159
+ "DBZH_CLEAN": "reflectivity", # corrected reflectivity, horizontal
160
+ "DBZV": "reflectivity", # corrected reflectivity, vertical
161
+ "CLASS": "hydrometeor_classification",
162
+ "ZDR": "differential_reflectivity", # differential reflectivity
163
+ "RHOHV": "cross_correlation_ratio",
164
+ "LDR": "linear_polarization_ratio",
165
+ "PHIDP": "differential_phase",
166
+ "KDP": "specific_differential_phase",
167
+ "SQI": "normalized_coherent_power",
168
+ "SNR": "signal_to_noise_ratio",
169
+ "SNRH": "signal_to_noise_ratio",
170
+ "VRAD": "velocity", # radial velocity, marked for deprecation in ODIM HDF5 2.2
171
+ "VRADH": "velocity", # radial velocity, horizontal polarisation
172
+ "VRADDH": "corrected_velocity", # radial velocity, horizontal polarisation
173
+ "VRADV": "velocity", # radial velocity, vertical polarisation
174
+ "WRAD": "spectrum_width",
175
+ "QIND": "quality_index",
176
+ }
177
+
178
+ try:
179
+ fname = ODIM_H5_FIELD_NAMES[quantity_name]
180
+ attrs = get_metadata(fname)
181
+ attrs.pop("coordinates")
182
+ except KeyError:
183
+ return {}
184
+
185
+ return attrs
186
+
187
+
188
+ def generate_timestamp(stime: str, etime: str, nrays: int, a1gate: int) -> np.ndarray:
189
+ """
190
+ Generate timestamp for each ray.
191
+
192
+ Parameters:
193
+ ===========
194
+ stime: str
195
+ Sweep starting time.
196
+ etime:
197
+ Sweep ending time.
198
+ nrays: int
199
+ Number of rays in sweep.
200
+ a1gate: int
201
+ Azimuth of the ray measured first by the radar.
202
+
203
+ Returns:
204
+ ========
205
+ trange: Timestamp<nrays>
206
+ Timestamp for each ray.
207
+ """
208
+ sdtime = datetime.datetime.strptime(stime, "%Y%m%d_%H%M%S")
209
+ edtime = datetime.datetime.strptime(etime, "%Y%m%d_%H%M%S")
210
+ trange = pd.date_range(sdtime, edtime, nrays)
211
+
212
+ return np.roll(trange, a1gate)
213
+
214
+
215
+ def get_dataset_metadata(hfile, dataset: str = "dataset1") -> Tuple[Dict, Dict]:
216
+ """
217
+ Get the dataset metadata of the ODIM H5 file.
218
+
219
+ Parameters:
220
+ ===========
221
+ hfile: h5py.File
222
+ H5 file identifier.
223
+ dataset: str
224
+ Key of the dataset for which to extract the metadata
225
+
226
+ Returns:
227
+ ========
228
+ metadata: dict
229
+ General metadata of the dataset.
230
+ coordinates_metadata: dict
231
+ Coordinates-specific metadata.
232
+ """
233
+ metadata = dict()
234
+ coordinates_metadata = dict()
235
+ # General metadata
236
+ for k in ["NI", "highprf", "product"]:
237
+ try:
238
+ metadata[k] = hfile[f"/{dataset}/how"].attrs[k]
239
+ except Exception:
240
+ pass
241
+
242
+ sdate = _to_str(hfile[f"/{dataset}/what"].attrs["startdate"])
243
+ stime = _to_str(hfile[f"/{dataset}/what"].attrs["starttime"])
244
+ try:
245
+ edate = _to_str(hfile[f"/{dataset}/what"].attrs["enddate"])
246
+ etime = _to_str(hfile[f"/{dataset}/what"].attrs["endtime"])
247
+ except Exception:
248
+ edate = sdate
249
+ etime = stime
250
+ metadata["start_time"] = f"{sdate}_{stime}"
251
+ metadata["end_time"] = f"{edate}_{etime}"
252
+
253
+ # Coordinates:
254
+ try:
255
+ coordinates_metadata["astart"] = hfile[f"/{dataset}/how"].attrs["astart"]
256
+ except KeyError:
257
+ # Optional coordinates (!).
258
+ coordinates_metadata["astart"] = 0
259
+ coordinates_metadata["a1gate"] = hfile[f"/{dataset}/where"].attrs["a1gate"]
260
+ coordinates_metadata["nrays"] = hfile[f"/{dataset}/where"].attrs["nrays"]
261
+
262
+ coordinates_metadata["rstart"] = hfile[f"/{dataset}/where"].attrs["rstart"]
263
+ coordinates_metadata["rscale"] = hfile[f"/{dataset}/where"].attrs["rscale"]
264
+ coordinates_metadata["nbins"] = hfile[f"/{dataset}/where"].attrs["nbins"]
265
+
266
+ coordinates_metadata["elangle"] = hfile[f"/{dataset}/where"].attrs["elangle"]
267
+
268
+ return metadata, coordinates_metadata
269
+
270
+
271
+ def get_root_metadata(hfile) -> Dict:
272
+ """
273
+ Get the metadata at the root of the ODIM H5 file.
274
+
275
+ Parameters:
276
+ ===========
277
+ hfile: h5py.File
278
+ H5 file identifier.
279
+
280
+ Returns:
281
+ ========
282
+ rootmetadata: dict
283
+ Metadata at the root of the ODIM H5 file.
284
+ """
285
+ rootmetadata = {}
286
+ # Root
287
+ try:
288
+ rootmetadata["Conventions"] = _to_str(hfile.attrs["Conventions"])
289
+ except KeyError:
290
+ rootmetadata["Conventions"] = None
291
+
292
+ # Where
293
+ rootmetadata["latitude"] = hfile["/where"].attrs["lat"]
294
+ rootmetadata["longitude"] = hfile["/where"].attrs["lon"]
295
+ rootmetadata["height"] = hfile["/where"].attrs["height"]
296
+
297
+ # What
298
+ try:
299
+ sdate = _to_str(hfile["/what"].attrs["date"])
300
+ stime = _to_str(hfile["/what"].attrs["time"])
301
+ rootmetadata["date"] = datetime.datetime.strptime(sdate + stime, "%Y%m%d%H%M%S").isoformat()
302
+ except KeyError:
303
+ rootmetadata["date"] = None
304
+ for k in ["object", "source", "version"]:
305
+ try:
306
+ rootmetadata[k] = _to_str(hfile["/what"].attrs[k])
307
+ except KeyError:
308
+ pass
309
+
310
+ # How
311
+ for k in ["beamwH", "beamwV", "rpm", "wavelength"]:
312
+ try:
313
+ rootmetadata[k] = hfile["/how"].attrs[k]
314
+ except KeyError:
315
+ pass
316
+
317
+ try:
318
+ rootmetadata["copyright"] = _to_str(hfile["/how"].attrs["copyright"])
319
+ except KeyError:
320
+ pass
321
+
322
+ return rootmetadata
323
+
324
+
325
+ def radar_coordinates_to_xyz(
326
+ r: np.ndarray, azimuth: np.ndarray, elevation: np.ndarray
327
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
328
+ """
329
+ Transform radar coordinates to cartesian coordinates.
330
+
331
+ Parameters:
332
+ ===========
333
+ r: ndarray<nbins>
334
+ Sweep range.
335
+ azimuth: ndarray<nrays>
336
+ Sweep azimuth.
337
+ elevation: float
338
+ Sweep elevation.
339
+
340
+ Returns:
341
+ ========
342
+ x, y, z: ndarray<nrays, nbins>
343
+ XYZ cartesian coordinates.
344
+ """
345
+ # To proper spherical coordinates.
346
+ theta = np.deg2rad(90 - elevation)
347
+ phi = 450 - azimuth
348
+ phi[phi >= 360] -= 360
349
+ phi = np.deg2rad(phi)
350
+
351
+ R, PHI = np.meshgrid(r, phi)
352
+ R = R.astype(np.float32)
353
+ PHI = PHI.astype(np.float32)
354
+
355
+ x = R * np.sin(theta) * np.cos(PHI)
356
+ y = R * np.sin(theta) * np.sin(PHI)
357
+ z = R * np.cos(theta)
358
+
359
+ return x, y, z
360
+
361
+
362
+ def read_odim_slice(
363
+ odim_file: str, nslice: int = 0, include_fields: List = [], exclude_fields: List = [], check_NI: bool = False
364
+ ) -> xr.Dataset:
365
+ """
366
+ Read into an xarray dataset one sweep of the ODIM file.
367
+
368
+ Parameters:
369
+ ===========
370
+ odim_file: str
371
+ ODIM H5 filename.
372
+ nslice: int
373
+ Slice number we want to extract (start indexing at 0).
374
+ include_fields: list
375
+ Specific fields to be exclusively read.
376
+ exclude_fields: list
377
+ Specific fields to be excluded from reading.
378
+ check_NI: bool
379
+ Check NI parameter in ODIM file and compare it to the PRF.
380
+
381
+ Returns:
382
+ ========
383
+ dataset: xarray.Dataset
384
+ xarray dataset of one sweep of the ODIM file.
385
+ """
386
+ # if nslice == 0:
387
+ # raise ValueError('Slice numbering start at 1.')
388
+ if type(include_fields) is not list:
389
+ raise TypeError("Argument `include_fields` should be a list")
390
+
391
+ with h5py.File(odim_file, "r") as hfile:
392
+ # Number of sweep in dataset
393
+ nsweep = len([k for k in hfile["/"].keys() if k.startswith("dataset")])
394
+ assert nslice <= nsweep, f"Wrong slice number asked. Only {nsweep} available."
395
+
396
+ # Order datasets by increasing elevations.
397
+ sweeps = dict()
398
+ for key in hfile["/"].keys():
399
+ if key.startswith("dataset"):
400
+ sweeps[key] = hfile[f"/{key}/where"].attrs["elangle"]
401
+
402
+ sorted_keys = sorted(sweeps, key=lambda k: sweeps[k])
403
+ rootkey = sorted_keys[nslice]
404
+
405
+ # Retrieve dataset metadata and coordinates metadata.
406
+ metadata, coordinates_metadata = get_dataset_metadata(hfile, rootkey)
407
+
408
+ dataset = xr.Dataset()
409
+ dataset.attrs = get_root_metadata(hfile)
410
+ dataset.attrs.update(metadata)
411
+ if check_NI:
412
+ try:
413
+ check_nyquist(dataset)
414
+ except AssertionError:
415
+ print("Nyquist not consistent with PRF")
416
+ pass
417
+
418
+ for datakey in hfile[f"/{rootkey}"].keys():
419
+ if not datakey.startswith("data"):
420
+ continue
421
+
422
+ try:
423
+ gain = hfile[f"/{rootkey}/{datakey}/what"].attrs["gain"]
424
+ except KeyError:
425
+ gain = 1
426
+ try:
427
+ nodata = hfile[f"/{rootkey}/{datakey}/what"].attrs["nodata"]
428
+ except KeyError:
429
+ nodata = np.NaN
430
+ try:
431
+ offset = hfile[f"/{rootkey}/{datakey}/what"].attrs["offset"]
432
+ except KeyError:
433
+ offset = 0
434
+ name = _to_str(hfile[f"/{rootkey}/{datakey}/what"].attrs["quantity"])
435
+ # Check if field should be read.
436
+ if len(include_fields) > 0:
437
+ if name not in include_fields:
438
+ continue
439
+ if name in exclude_fields:
440
+ continue
441
+
442
+ dtype = hfile[f"/{rootkey}/{datakey}/data"][:].dtype
443
+ data_value = hfile[f"/{rootkey}/{datakey}/data"][:].astype(float).copy()
444
+ data_value[np.abs(data_value - nodata) <= 0.1] = np.NaN
445
+ data_value = gain * data_value + offset
446
+ data_value[np.abs(data_value - nodata * gain) <= 0.1] = np.NaN
447
+ dataset = dataset.merge({name: (("azimuth", "range"), data_value)})
448
+ dataset[name].attrs = field_metadata(name)
449
+ dataset[name].attrs["gain"] = np.float32(gain)
450
+ dataset[name].attrs["nodata"] = nodata
451
+ dataset[name].attrs["offset"] = offset
452
+ dataset[name].attrs["dtype"] = dtype
453
+
454
+ time = generate_timestamp(
455
+ metadata["start_time"], metadata["end_time"], coordinates_metadata["nrays"], coordinates_metadata["a1gate"]
456
+ )
457
+ r, azi, elev = coord_from_metadata(coordinates_metadata)
458
+ x, y, z = radar_coordinates_to_xyz(r, azi, elev)
459
+ longitude, latitude = cartesian_to_geographic(x, y, dataset.attrs["longitude"], dataset.attrs["latitude"])
460
+
461
+ dataset = dataset.merge(
462
+ {
463
+ "range": (("range"), r),
464
+ "azimuth": (("azimuth"), azi),
465
+ "elevation": (("elevation"), elev),
466
+ "time": (("time"), time),
467
+ "x": (("azimuth", "range"), x),
468
+ "y": (("azimuth", "range"), y),
469
+ "z": (("azimuth", "range"), z + dataset.attrs["height"]),
470
+ "longitude": (("azimuth", "range"), longitude),
471
+ "latitude": (("azimuth", "range"), latitude),
472
+ }
473
+ )
474
+
475
+ return dataset
476
+
477
+
478
+ def read_odim(odim_file: str, lazy_load: bool = True, **kwargs) -> List:
479
+ """
480
+ Read an ODIM H5 file.
481
+
482
+ Parameters:
483
+ ===========
484
+ odim_file: str
485
+ ODIM H5 filename.
486
+ lazy_load: bool
487
+ Lazily load the data if true, read and load in memory the entire dataset
488
+ if false.
489
+ include_fields: list
490
+ Specific fields to be exclusively read.
491
+ exclude_fields: list
492
+ Specific fields to be excluded from reading.
493
+
494
+ Returns:
495
+ ========
496
+ radar: list
497
+ List of xarray datasets, each item in a the list is one sweep of the
498
+ radar data (ordered from lowest elevation scan to highest).
499
+ """
500
+ with h5py.File(odim_file, "r") as hfile:
501
+ nsweep = len([k for k in hfile["/"].keys() if k.startswith("dataset")])
502
+
503
+ radar = []
504
+ for sweep in range(0, nsweep):
505
+ c = dask.delayed(read_odim_slice)(odim_file, sweep, **kwargs)
506
+ radar.append(c)
507
+
508
+ if not lazy_load:
509
+ radar = [r.compute() for r in radar]
510
+
511
+ return radar
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.1
2
+ Name: pyodim
3
+ Version: 0.4
4
+ Summary: An ODIM hdf5 file reader.
5
+ Home-page: https://github.com/vlouf/pyodim
6
+ Author: Valentin Louf
7
+ Author-email: valentin.louf@bom.gov.au
8
+ Project-URL: Bug Reports, https://github.com/vlouf/pyodim/issues
9
+ Project-URL: Source, https://github.com/vlouf/pyodim/
10
+ Keywords: odim h5 file reader
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+
23
+ # PYODIM
24
+
25
+ A simple ODIM H5 (Opera Data Information Model - Hierarchical Data Format
26
+ version 5) radar reader in Python. It outputs the radar date in an xarray
27
+ Dataset. The goal is to be as barebone as possible while providing an xarray,
28
+ so that it can easily scale up (production) or just a very lean reader for
29
+ quicklooks into the data.
30
+
31
+ ## Example
32
+
33
+ ```python
34
+
35
+ import pyodim
36
+
37
+ filename = "/path/to/radar/file.h5"
38
+ rset = pyodim.read_odim(filename, lazy_load=True) # Lazy load does not load the data in memory to save time if you are interested in only a specific sweep
39
+ radar = rset[0].compute() # All sweeps are in a list (elevation ascending), so the 1st item of the list is the bottom sweep.
40
+ print(radar)
41
+ ```
42
+
43
+ This code will output the radar data for the first sweep (lowest elevation
44
+ scan) in the radar file as an xarray Dataset, for example:
45
+
46
+ ```python
47
+ <xarray.Dataset>
48
+ Dimensions: (azimuth: 360, range: 1283, elevation: 1, time: 360)
49
+ Coordinates:
50
+ * range (range) float32 125.0 375.0 625.0 ... 3.204e+05 3.206e+05
51
+ * azimuth (azimuth) float32 0.01111 1.011 2.011 ... 357.0 358.0 359.0
52
+ * elevation (elevation) float32 0.5
53
+ * time (time) datetime64[ns] 2024-01-16T02:49:29.337047353 ... 2024-...
54
+ Data variables: (12/17)
55
+ DBZH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
56
+ SNRH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
57
+ VRADH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
58
+ WRADH (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
59
+ KDP (azimuth, range) float64 nan nan nan nan nan ... nan nan nan nan
60
+ ... ...
61
+ x (azimuth, range) float32 0.02424 0.07271 ... -5.595e+03
62
+ y (azimuth, range) float32 125.0 375.0 ... 3.203e+05 3.206e+05
63
+ z (azimuth, range) float32 45.09 47.27 ... 2.84e+03 2.842e+03
64
+ longitude (azimuth, range) float32 144.8 144.8 144.8 ... 144.7 144.7 144.7
65
+ latitude (azimuth, range) float32 -37.85 -37.85 -37.85 ... -34.97 -34.96
66
+ Attributes: (12/15)
67
+ Conventions: ODIM_H5/V2_4
68
+ latitude: -37.85200119018555
69
+ longitude: 144.75199890136722
70
+ height: 44.0
71
+ date: 2024-01-16T02:45:21
72
+ object: PVOL
73
+ ... ...
74
+ beamwV: 1.0
75
+ wavelength: 10.489999771118164
76
+ NI: 36.5052
77
+ highprf: 696.0
78
+ start_time: 20240116_024928
79
+ end_time: 20240116_024958
80
+ ```
81
+
82
+ PYODIM reads the ODIM radar data and metadata. It also provides some quality of
83
+ life feature like automatically creating the x, y, z and latitude/longitude
84
+ coordinates for all radar gates (using the azimuthal equidistant - `aeqd` - projection). By
85
+ default it uses `dask.delayed` to *lazily* load the data, meaning that it will
86
+ not actually load all the sweeps in memory, as for many applications we often
87
+ don't need all the radar sweeps. Instead, by calling the `.compute()` method
88
+ like in the example above, PYODIM will only read and load in memory the sweep
89
+ that you want, saving time. If you want to read all the sweeps at once in a
90
+ list, then just set `lazy_load=False`.
91
+
92
+ ## Dependencies
93
+
94
+ Mandatory:
95
+ - [numpy][1]
96
+ - [xarray][2]
97
+ - [pyproj][3]
98
+ - [dask][4]
99
+
100
+ And optionally (it will automatically populate the fields metadata using pyart
101
+ if available):
102
+ - [Py-ART][5]
103
+
104
+ [1]: http://www.scipy.org/
105
+ [2]: http://numba.pydata.org
106
+ [3]: https://pypi.org/project/pyproj/
107
+ [4]: https://www.dask.org/
108
+ [5]: https://github.com/ARM-DOE/pyart
@@ -0,0 +1,12 @@
1
+ .gitignore
2
+ LICENSE
3
+ README.md
4
+ setup.py
5
+ pyodim/__init__.py
6
+ pyodim/__version__.py
7
+ pyodim/pyodim.py
8
+ pyodim.egg-info/PKG-INFO
9
+ pyodim.egg-info/SOURCES.txt
10
+ pyodim.egg-info/dependency_links.txt
11
+ pyodim.egg-info/requires.txt
12
+ pyodim.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ numpy
2
+ dask
3
+ xarray
4
+ h5py
5
+ pyproj
@@ -0,0 +1 @@
1
+ pyodim
pyodim-0.4/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
pyodim-0.4/setup.py ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from os import path
5
+ from setuptools import find_packages, setup
6
+
7
+
8
+ here = path.abspath(path.dirname(__file__))
9
+ # Get the long description from the README file
10
+ with open(path.join(here, "README.md"), encoding="utf-8") as f:
11
+ long_description = f.read()
12
+
13
+
14
+ setup(
15
+ name="pyodim",
16
+ version="0.4",
17
+ description="An ODIM hdf5 file reader.",
18
+ long_description=long_description,
19
+ long_description_content_type="text/markdown",
20
+ url="https://github.com/vlouf/pyodim",
21
+ author="Valentin Louf",
22
+ author_email="valentin.louf@bom.gov.au",
23
+ classifiers=[
24
+ "Development Status :: 4 - Beta",
25
+ "Intended Audience :: Science/Research",
26
+ "Topic :: Scientific/Engineering :: Atmospheric Science",
27
+ "License :: OSI Approved :: MIT License",
28
+ "Programming Language :: Python :: 3.7",
29
+ "Programming Language :: Python :: 3.8",
30
+ "Programming Language :: Python :: 3.9",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ ],
34
+ keywords="odim h5 file reader", # Optional
35
+ packages=find_packages(exclude=["contrib", "docs", "tests"]),
36
+ install_requires= ["numpy", "dask", "xarray", "h5py", "pyproj"],
37
+ project_urls={
38
+ "Bug Reports": "https://github.com/vlouf/pyodim/issues",
39
+ "Source": "https://github.com/vlouf/pyodim/",
40
+ },
41
+ )