pyodim 0.5.0__py3-none-any.whl
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/__init__.py +7 -0
- pyodim/__version__.py +1 -0
- pyodim/pyodim.py +622 -0
- pyodim-0.5.0.dist-info/LICENSE +21 -0
- pyodim-0.5.0.dist-info/METADATA +74 -0
- pyodim-0.5.0.dist-info/RECORD +8 -0
- pyodim-0.5.0.dist-info/WHEEL +5 -0
- pyodim-0.5.0.dist-info/top_level.txt +1 -0
pyodim/__init__.py
ADDED
pyodim/__version__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
pyodim/pyodim.py
ADDED
|
@@ -0,0 +1,622 @@
|
|
|
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: 12/02/2024
|
|
9
|
+
|
|
10
|
+
.. autosummary::
|
|
11
|
+
:toctree: generated/
|
|
12
|
+
|
|
13
|
+
buffer
|
|
14
|
+
cartesian_to_geographic
|
|
15
|
+
check_nyquist
|
|
16
|
+
coord_from_metadata
|
|
17
|
+
field_metadata
|
|
18
|
+
generate_timestamp
|
|
19
|
+
get_dataset_metadata
|
|
20
|
+
get_root_metadata
|
|
21
|
+
radar_coordinates_to_xyz
|
|
22
|
+
read_odim_slice
|
|
23
|
+
read_odim
|
|
24
|
+
"""
|
|
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 buffer(func):
|
|
39
|
+
"""
|
|
40
|
+
Decorator to catch and kill error message. Almost want to name the function
|
|
41
|
+
dont_fail.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def wrapper(*args, **kwargs):
|
|
45
|
+
try:
|
|
46
|
+
rslt = func(*args, **kwargs)
|
|
47
|
+
except Exception:
|
|
48
|
+
traceback.print_exc()
|
|
49
|
+
rslt = None
|
|
50
|
+
return rslt
|
|
51
|
+
|
|
52
|
+
return wrapper
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def cartesian_to_geographic(x: np.ndarray, y: np.ndarray, lon0: float, lat0: float) -> Tuple[np.ndarray, np.ndarray]:
|
|
56
|
+
"""
|
|
57
|
+
Transform cartesian coordinates to lat/lon using the Azimuth Equidistant
|
|
58
|
+
projection.
|
|
59
|
+
|
|
60
|
+
Parameters:
|
|
61
|
+
===========
|
|
62
|
+
x: ndarray
|
|
63
|
+
x-axis cartesian coordinates.
|
|
64
|
+
y: ndarray
|
|
65
|
+
y-axis cartesian coordinates. Same dimension as x
|
|
66
|
+
lon0: float
|
|
67
|
+
Radar site longitude.
|
|
68
|
+
lat0: float
|
|
69
|
+
Radar site latitude.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
lon: ndarray
|
|
73
|
+
Longitude of each gate.
|
|
74
|
+
lat: ndarray
|
|
75
|
+
Latitude of each gate.
|
|
76
|
+
"""
|
|
77
|
+
georef = pyproj.Proj(f"+proj=aeqd +lon_0={lon0} +lat_0={lat0} +ellps=WGS84")
|
|
78
|
+
lon, lat = georef(x, y, inverse=True)
|
|
79
|
+
lon = lon.astype(np.float32)
|
|
80
|
+
lat = lat.astype(np.float32)
|
|
81
|
+
return lon, lat
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@buffer
|
|
85
|
+
def check_nyquist(dset: xr.Dataset) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Check if the dataset Nyquist velocity corresponds to the PRF information.
|
|
88
|
+
"""
|
|
89
|
+
wavelength = dset.attrs["wavelength"]
|
|
90
|
+
prf = dset.attrs["highprf"]
|
|
91
|
+
nyquist = dset.attrs["NI"]
|
|
92
|
+
ny_int = 1e-2 * prf * wavelength / 4
|
|
93
|
+
|
|
94
|
+
assert np.abs(nyquist - ny_int) < 0.5, "Nyquist not consistent with PRF"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def coord_from_metadata(metadata: Dict) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
98
|
+
"""
|
|
99
|
+
Create the radar coordinates from the ODIM H5 metadata specification.
|
|
100
|
+
|
|
101
|
+
Parameter:
|
|
102
|
+
==========
|
|
103
|
+
metadata: dict()
|
|
104
|
+
Metadata dictionnary containing the specific ODIM H5 keys: astart,
|
|
105
|
+
nrays, nbins, rstart, rscale, elangle.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
========
|
|
109
|
+
r: ndarray<nbins>
|
|
110
|
+
Sweep range
|
|
111
|
+
azimuth: ndarray<nrays>
|
|
112
|
+
Sweep azimuth
|
|
113
|
+
elev: float
|
|
114
|
+
Sweep elevation
|
|
115
|
+
"""
|
|
116
|
+
da = 360 / metadata["nrays"]
|
|
117
|
+
azimuth = np.linspace(metadata["astart"] + da / 2, 360 - da, metadata["nrays"], dtype=np.float32)
|
|
118
|
+
|
|
119
|
+
# rstart is in KM !!! STUPID.
|
|
120
|
+
rstart_center = 1e3 * metadata["rstart"] + metadata["rscale"] / 2
|
|
121
|
+
r = np.arange(
|
|
122
|
+
rstart_center, rstart_center + metadata["nbins"] * metadata["rscale"], metadata["rscale"], dtype=np.float32
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
elev = np.array([metadata["elangle"]], dtype=np.float32)
|
|
126
|
+
return r, azimuth, elev
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def field_metadata(quantity_name: str) -> Dict:
|
|
130
|
+
"""
|
|
131
|
+
Populate metadata for common fields using Py-ART get_metadata() function.
|
|
132
|
+
(Optionnal).
|
|
133
|
+
|
|
134
|
+
Parameter:
|
|
135
|
+
==========
|
|
136
|
+
quantity_name: str
|
|
137
|
+
ODIM H5 quantity attribute name.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
========
|
|
141
|
+
attrs: dict()
|
|
142
|
+
Metadata dictionnary.
|
|
143
|
+
"""
|
|
144
|
+
metadata = {
|
|
145
|
+
"TH": {"units": "dBZ", "standard_name": "equivalent_reflectivity_factor", "long_name": "Total power"},
|
|
146
|
+
"TV": {"units": "dBZ", "standard_name": "equivalent_reflectivity_factor", "long_name": "Total power"},
|
|
147
|
+
"DBZH": {"units": "dBZ", "standard_name": "equivalent_reflectivity_factor", "long_name": "Reflectivity"},
|
|
148
|
+
"DBZH_CLEAN": {"units": "dBZ", "standard_name": "equivalent_reflectivity_factor", "long_name": "Reflectivity"},
|
|
149
|
+
"DBZV": {"units": "dBZ", "standard_name": "equivalent_reflectivity_factor", "long_name": "Reflectivity"},
|
|
150
|
+
"ZDR": {
|
|
151
|
+
"units": "dB",
|
|
152
|
+
"standard_name": "log_differential_reflectivity_hv",
|
|
153
|
+
"long_name": "Differential reflectivity",
|
|
154
|
+
},
|
|
155
|
+
"RHOHV": {
|
|
156
|
+
"units": "ratio",
|
|
157
|
+
"standard_name": "cross_correlation_ratio_hv",
|
|
158
|
+
"long_name": "Cross correlation ratio (RHOHV)",
|
|
159
|
+
"valid_max": 1.0,
|
|
160
|
+
"valid_min": 0.0,
|
|
161
|
+
},
|
|
162
|
+
"LDR": {
|
|
163
|
+
"units": "dB",
|
|
164
|
+
"standard_name": "log_linear_depolarization_ratio_hv",
|
|
165
|
+
"long_name": "Linear depolarization ratio",
|
|
166
|
+
},
|
|
167
|
+
"PHIDP": {
|
|
168
|
+
"units": "degrees",
|
|
169
|
+
"standard_name": "differential_phase_hv",
|
|
170
|
+
"long_name": "Differential phase (PhiDP)",
|
|
171
|
+
"valid_max": 180.0,
|
|
172
|
+
"valid_min": -180.0,
|
|
173
|
+
},
|
|
174
|
+
"KDP": {
|
|
175
|
+
"units": "degrees/km",
|
|
176
|
+
"standard_name": "specific_differential_phase_hv",
|
|
177
|
+
"long_name": "Specific differential phase (KDP)",
|
|
178
|
+
},
|
|
179
|
+
"SQI": {
|
|
180
|
+
"units": "ratio",
|
|
181
|
+
"standard_name": "normalized_coherent_power",
|
|
182
|
+
"long_name": "Normalized coherent power",
|
|
183
|
+
"valid_max": 1.0,
|
|
184
|
+
"valid_min": 0.0,
|
|
185
|
+
"comment": "Also know as signal quality index (SQI)",
|
|
186
|
+
},
|
|
187
|
+
"SNR": {"units": "dB", "standard_name": "signal_to_noise_ratio", "long_name": "Signal to noise ratio"},
|
|
188
|
+
"SNRH": {"units": "dB", "standard_name": "signal_to_noise_ratio", "long_name": "Signal to noise ratio"},
|
|
189
|
+
"VRAD": {
|
|
190
|
+
"units": "meters_per_second",
|
|
191
|
+
"standard_name": "radial_velocity",
|
|
192
|
+
"long_name": "Mean dopper velocity",
|
|
193
|
+
},
|
|
194
|
+
"VRADH": {
|
|
195
|
+
"units": "meters_per_second",
|
|
196
|
+
"standard_name": "radial_velocity",
|
|
197
|
+
"long_name": "Mean dopper velocity",
|
|
198
|
+
},
|
|
199
|
+
"VRADDH": {
|
|
200
|
+
"units": "meters_per_second",
|
|
201
|
+
"standard_name": "corrected_radial_velocity",
|
|
202
|
+
"long_name": "Corrected mean doppler velocity",
|
|
203
|
+
},
|
|
204
|
+
"VRADV": {
|
|
205
|
+
"units": "meters_per_second",
|
|
206
|
+
"standard_name": "radial_velocity",
|
|
207
|
+
"long_name": "Mean dopper velocity",
|
|
208
|
+
},
|
|
209
|
+
"WRAD": {
|
|
210
|
+
"units": "meters_per_second",
|
|
211
|
+
"standard_name": "doppler_spectrum_width",
|
|
212
|
+
"long_name": "Doppler spectrum width",
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
attrs = metadata[quantity_name]
|
|
218
|
+
except KeyError:
|
|
219
|
+
return {}
|
|
220
|
+
|
|
221
|
+
return attrs
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def generate_timestamp(stime: str, etime: str, nrays: int, a1gate: int) -> np.ndarray:
|
|
225
|
+
"""
|
|
226
|
+
Generate timestamp for each ray.
|
|
227
|
+
|
|
228
|
+
Parameters:
|
|
229
|
+
===========
|
|
230
|
+
stime: str
|
|
231
|
+
Sweep starting time.
|
|
232
|
+
etime:
|
|
233
|
+
Sweep ending time.
|
|
234
|
+
nrays: int
|
|
235
|
+
Number of rays in sweep.
|
|
236
|
+
a1gate: int
|
|
237
|
+
Azimuth of the ray measured first by the radar.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
========
|
|
241
|
+
trange: Timestamp<nrays>
|
|
242
|
+
Timestamp for each ray.
|
|
243
|
+
"""
|
|
244
|
+
sdtime = datetime.datetime.strptime(stime, "%Y%m%d_%H%M%S")
|
|
245
|
+
edtime = datetime.datetime.strptime(etime, "%Y%m%d_%H%M%S")
|
|
246
|
+
trange = pd.date_range(sdtime, edtime, nrays)
|
|
247
|
+
|
|
248
|
+
return np.roll(trange, a1gate)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_dataset_metadata(hfile, dataset: str = "dataset1") -> Tuple[Dict, Dict]:
|
|
252
|
+
"""
|
|
253
|
+
Get the dataset metadata of the ODIM H5 file.
|
|
254
|
+
|
|
255
|
+
Parameters:
|
|
256
|
+
===========
|
|
257
|
+
hfile: h5py.File
|
|
258
|
+
H5 file identifier.
|
|
259
|
+
dataset: str
|
|
260
|
+
Key of the dataset for which to extract the metadata
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
========
|
|
264
|
+
metadata: dict
|
|
265
|
+
General metadata of the dataset.
|
|
266
|
+
coordinates_metadata: dict
|
|
267
|
+
Coordinates-specific metadata.
|
|
268
|
+
"""
|
|
269
|
+
metadata = dict()
|
|
270
|
+
coordinates_metadata = dict()
|
|
271
|
+
|
|
272
|
+
# NB: do not try/except KeyError for h5py attrs: it leaks [h5py issue 2350]
|
|
273
|
+
|
|
274
|
+
# General metadata
|
|
275
|
+
ds_how = hfile[f"/{dataset}/how"]
|
|
276
|
+
for k in {"NI", "highprf", "product"} & ds_how.attrs.keys():
|
|
277
|
+
metadata[k] = ds_how.attrs[k]
|
|
278
|
+
|
|
279
|
+
sdate = hfile[f"/{dataset}/what"].attrs["startdate"].decode("utf-8")
|
|
280
|
+
stime = hfile[f"/{dataset}/what"].attrs["starttime"].decode("utf-8")
|
|
281
|
+
edate = hfile[f"/{dataset}/what"].attrs["enddate"].decode("utf-8")
|
|
282
|
+
etime = hfile[f"/{dataset}/what"].attrs["endtime"].decode("utf-8")
|
|
283
|
+
metadata["start_time"] = f"{sdate}_{stime}"
|
|
284
|
+
metadata["end_time"] = f"{edate}_{etime}"
|
|
285
|
+
|
|
286
|
+
# Coordinates:
|
|
287
|
+
if "astart" in ds_how.attrs:
|
|
288
|
+
coordinates_metadata["astart"] = ds_how.attrs["astart"]
|
|
289
|
+
else:
|
|
290
|
+
# Optional coordinates (!).
|
|
291
|
+
coordinates_metadata["astart"] = 0
|
|
292
|
+
coordinates_metadata["a1gate"] = hfile[f"/{dataset}/where"].attrs["a1gate"]
|
|
293
|
+
coordinates_metadata["nrays"] = hfile[f"/{dataset}/where"].attrs["nrays"]
|
|
294
|
+
|
|
295
|
+
coordinates_metadata["rstart"] = hfile[f"/{dataset}/where"].attrs["rstart"]
|
|
296
|
+
coordinates_metadata["rscale"] = hfile[f"/{dataset}/where"].attrs["rscale"]
|
|
297
|
+
coordinates_metadata["nbins"] = hfile[f"/{dataset}/where"].attrs["nbins"]
|
|
298
|
+
|
|
299
|
+
coordinates_metadata["elangle"] = hfile[f"/{dataset}/where"].attrs["elangle"]
|
|
300
|
+
|
|
301
|
+
return metadata, coordinates_metadata
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def get_root_metadata(hfile) -> Dict:
|
|
305
|
+
"""
|
|
306
|
+
Get the metadata at the root of the ODIM H5 file.
|
|
307
|
+
|
|
308
|
+
Parameters:
|
|
309
|
+
===========
|
|
310
|
+
hfile: h5py.File
|
|
311
|
+
H5 file identifier.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
========
|
|
315
|
+
rootmetadata: dict
|
|
316
|
+
Metadata at the root of the ODIM H5 file.
|
|
317
|
+
"""
|
|
318
|
+
rootmetadata = {}
|
|
319
|
+
|
|
320
|
+
# NB: do not try/except KeyError for h5py attrs: it leaks [h5py issue 2350]
|
|
321
|
+
|
|
322
|
+
# Root
|
|
323
|
+
rootmetadata["Conventions"] = hfile.attrs["Conventions"].decode("utf-8")
|
|
324
|
+
|
|
325
|
+
# Where
|
|
326
|
+
rootmetadata["latitude"] = hfile["/where"].attrs["lat"]
|
|
327
|
+
rootmetadata["longitude"] = hfile["/where"].attrs["lon"]
|
|
328
|
+
rootmetadata["height"] = hfile["/where"].attrs["height"]
|
|
329
|
+
|
|
330
|
+
# What
|
|
331
|
+
sdate = hfile["/what"].attrs["date"].decode("utf-8")
|
|
332
|
+
stime = hfile["/what"].attrs["time"].decode("utf-8")
|
|
333
|
+
rootmetadata["date"] = datetime.datetime.strptime(sdate + stime, "%Y%m%d%H%M%S").isoformat()
|
|
334
|
+
for k in {"object", "source", "version"} & hfile["/what"].attrs.keys():
|
|
335
|
+
rootmetadata[k] = hfile["/what"].attrs[k].decode("utf-8")
|
|
336
|
+
|
|
337
|
+
# How
|
|
338
|
+
for k in {"beamwH", "beamwV", "rpm", "wavelength"} & hfile["/how"].attrs.keys():
|
|
339
|
+
rootmetadata[k] = hfile["/how"].attrs[k]
|
|
340
|
+
|
|
341
|
+
if "copyright" in hfile["/how"].attrs:
|
|
342
|
+
rootmetadata["copyright"] = hfile["/how"].attrs["copyright"].decode("utf-8")
|
|
343
|
+
|
|
344
|
+
return rootmetadata
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def radar_coordinates_to_xyz(
|
|
348
|
+
r: np.ndarray, azimuth: np.ndarray, elevation: np.ndarray
|
|
349
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
350
|
+
"""
|
|
351
|
+
Transform radar coordinates to cartesian coordinates.
|
|
352
|
+
|
|
353
|
+
Parameters:
|
|
354
|
+
===========
|
|
355
|
+
r: ndarray<nbins>
|
|
356
|
+
Sweep range.
|
|
357
|
+
azimuth: ndarray<nrays>
|
|
358
|
+
Sweep azimuth.
|
|
359
|
+
elevation: float
|
|
360
|
+
Sweep elevation.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
========
|
|
364
|
+
x, y, z: ndarray<nrays, nbins>
|
|
365
|
+
XYZ cartesian coordinates.
|
|
366
|
+
"""
|
|
367
|
+
# To proper spherical coordinates.
|
|
368
|
+
theta = np.deg2rad(90 - elevation)
|
|
369
|
+
phi = 450 - azimuth
|
|
370
|
+
phi[phi >= 360] -= 360
|
|
371
|
+
phi = np.deg2rad(phi)
|
|
372
|
+
|
|
373
|
+
R, PHI = np.meshgrid(r, phi)
|
|
374
|
+
R = R.astype(np.float32)
|
|
375
|
+
PHI = PHI.astype(np.float32)
|
|
376
|
+
|
|
377
|
+
x = R * np.sin(theta) * np.cos(PHI)
|
|
378
|
+
y = R * np.sin(theta) * np.sin(PHI)
|
|
379
|
+
z = R * np.cos(theta)
|
|
380
|
+
|
|
381
|
+
return x, y, z
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def read_odim_slice(
|
|
385
|
+
odim_file: str,
|
|
386
|
+
nslice: int = 0,
|
|
387
|
+
include_fields: List = [],
|
|
388
|
+
exclude_fields: List = [],
|
|
389
|
+
check_NI: bool = False,
|
|
390
|
+
read_write: bool = False,
|
|
391
|
+
) -> xr.Dataset:
|
|
392
|
+
"""
|
|
393
|
+
Read into an xarray dataset one sweep of the ODIM file.
|
|
394
|
+
|
|
395
|
+
Parameters:
|
|
396
|
+
===========
|
|
397
|
+
odim_file: str
|
|
398
|
+
ODIM H5 filename.
|
|
399
|
+
nslice: int
|
|
400
|
+
Slice number we want to extract (start indexing at 0).
|
|
401
|
+
include_fields: list
|
|
402
|
+
Specific fields to be exclusively read.
|
|
403
|
+
exclude_fields: list
|
|
404
|
+
Specific fields to be excluded from reading.
|
|
405
|
+
check_NI: bool
|
|
406
|
+
Check NI parameter in ODIM file and compare it to the PRF.
|
|
407
|
+
read_write: open in read-write mode if True
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
========
|
|
411
|
+
dataset: xarray.Dataset
|
|
412
|
+
xarray dataset of one sweep of the ODIM file.
|
|
413
|
+
"""
|
|
414
|
+
rw_mode = "r+" if read_write else "r"
|
|
415
|
+
with h5py.File(odim_file, rw_mode) as hfile:
|
|
416
|
+
return read_odim_slice_h5(
|
|
417
|
+
hfile,
|
|
418
|
+
nslice=nslice,
|
|
419
|
+
include_fields=include_fields,
|
|
420
|
+
exclude_fields=exclude_fields,
|
|
421
|
+
check_NI=check_NI,
|
|
422
|
+
read_write=read_write,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def read_odim_slice_h5(
|
|
427
|
+
hfile: str,
|
|
428
|
+
nslice: int = 0,
|
|
429
|
+
include_fields: List = [],
|
|
430
|
+
exclude_fields: List = [],
|
|
431
|
+
check_NI: bool = False,
|
|
432
|
+
read_write: bool = False,
|
|
433
|
+
) -> xr.Dataset:
|
|
434
|
+
# if nslice == 0:
|
|
435
|
+
# raise ValueError('Slice numbering start at 1.')
|
|
436
|
+
if type(include_fields) is not list:
|
|
437
|
+
raise TypeError("Argument `include_fields` should be a list")
|
|
438
|
+
|
|
439
|
+
# Number of sweep in dataset
|
|
440
|
+
nsweep = len([k for k in hfile["/"].keys() if k.startswith("dataset")])
|
|
441
|
+
assert nslice <= nsweep, f"Wrong slice number asked. Only {nsweep} available."
|
|
442
|
+
|
|
443
|
+
# Order datasets by increasing elevation and time
|
|
444
|
+
sweeps = dict()
|
|
445
|
+
for key in hfile["/"].keys():
|
|
446
|
+
if key.startswith("dataset"):
|
|
447
|
+
sweeps[key] = (hfile[f"/{key}/where"].attrs["elangle"], hfile[f"/{key}/what"].attrs["starttime"])
|
|
448
|
+
|
|
449
|
+
sorted_keys = sorted(sweeps, key=lambda k: sweeps[k])
|
|
450
|
+
rootkey = sorted_keys[nslice]
|
|
451
|
+
|
|
452
|
+
# Retrieve dataset metadata and coordinates metadata.
|
|
453
|
+
metadata, coordinates_metadata = get_dataset_metadata(hfile, rootkey)
|
|
454
|
+
# remember tilt id
|
|
455
|
+
metadata["id"] = rootkey
|
|
456
|
+
|
|
457
|
+
dataset = xr.Dataset()
|
|
458
|
+
dataset.attrs = get_root_metadata(hfile)
|
|
459
|
+
dataset.attrs.update(metadata)
|
|
460
|
+
if check_NI:
|
|
461
|
+
try:
|
|
462
|
+
check_nyquist(dataset)
|
|
463
|
+
except AssertionError:
|
|
464
|
+
print("Nyquist not consistent with PRF")
|
|
465
|
+
pass
|
|
466
|
+
|
|
467
|
+
for datakey in hfile[f"/{rootkey}"].keys():
|
|
468
|
+
if not datakey.startswith("data"):
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
gain = hfile[f"/{rootkey}/{datakey}/what"].attrs["gain"]
|
|
472
|
+
nodata = hfile[f"/{rootkey}/{datakey}/what"].attrs["nodata"]
|
|
473
|
+
offset = hfile[f"/{rootkey}/{datakey}/what"].attrs["offset"]
|
|
474
|
+
name = hfile[f"/{rootkey}/{datakey}/what"].attrs["quantity"].decode("utf-8")
|
|
475
|
+
# Check if field should be read.
|
|
476
|
+
if len(include_fields) > 0:
|
|
477
|
+
if name not in include_fields:
|
|
478
|
+
continue
|
|
479
|
+
if name in exclude_fields:
|
|
480
|
+
continue
|
|
481
|
+
|
|
482
|
+
data_value = hfile[f"/{rootkey}/{datakey}/data"][:].astype(np.float32)
|
|
483
|
+
data_value = gain * np.ma.masked_equal(data_value, nodata) + offset
|
|
484
|
+
dataset = dataset.merge({name: (("azimuth", "range"), data_value)})
|
|
485
|
+
dataset[name].attrs = field_metadata(name)
|
|
486
|
+
# remember dataset id
|
|
487
|
+
dataset[name].attrs["id"] = datakey
|
|
488
|
+
|
|
489
|
+
time = generate_timestamp(
|
|
490
|
+
metadata["start_time"], metadata["end_time"], coordinates_metadata["nrays"], coordinates_metadata["a1gate"]
|
|
491
|
+
)
|
|
492
|
+
r, azi, elev = coord_from_metadata(coordinates_metadata)
|
|
493
|
+
x, y, z = radar_coordinates_to_xyz(r, azi, elev)
|
|
494
|
+
longitude, latitude = cartesian_to_geographic(x, y, dataset.attrs["longitude"], dataset.attrs["latitude"])
|
|
495
|
+
|
|
496
|
+
dataset = dataset.merge(
|
|
497
|
+
{
|
|
498
|
+
"range": (("range"), r),
|
|
499
|
+
"azimuth": (("azimuth"), azi),
|
|
500
|
+
"elevation": (("elevation"), elev),
|
|
501
|
+
"time": (("time"), time),
|
|
502
|
+
"x": (("azimuth", "range"), x),
|
|
503
|
+
"y": (("azimuth", "range"), y),
|
|
504
|
+
"z": (("azimuth", "range"), z + dataset.attrs["height"]),
|
|
505
|
+
"longitude": (("azimuth", "range"), longitude),
|
|
506
|
+
"latitude": (("azimuth", "range"), latitude),
|
|
507
|
+
}
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
return dataset
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def read_write_odim(
|
|
514
|
+
odim_file: str,
|
|
515
|
+
lazy_load: bool = True,
|
|
516
|
+
read_write: bool = False,
|
|
517
|
+
**kwargs,
|
|
518
|
+
):
|
|
519
|
+
"""Read an ODIM H5 file and return h5py handle.
|
|
520
|
+
|
|
521
|
+
@param read_write: open in read-write mode if True.
|
|
522
|
+
@see read_odim().
|
|
523
|
+
"""
|
|
524
|
+
rw_mode = "r+" if read_write else "r"
|
|
525
|
+
try:
|
|
526
|
+
hfile = h5py.File(odim_file, rw_mode)
|
|
527
|
+
except Exception as e:
|
|
528
|
+
return None
|
|
529
|
+
|
|
530
|
+
nsweep = len([k for k in hfile["/"].keys() if k.startswith("dataset")])
|
|
531
|
+
|
|
532
|
+
radar = []
|
|
533
|
+
for sweep in range(0, nsweep):
|
|
534
|
+
c = dask.delayed(read_odim_slice_h5)(hfile, sweep, **kwargs)
|
|
535
|
+
radar.append(c)
|
|
536
|
+
|
|
537
|
+
if not lazy_load:
|
|
538
|
+
radar = [r.compute() for r in radar]
|
|
539
|
+
|
|
540
|
+
return (radar, hfile)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def read_odim(
|
|
544
|
+
odim_file: str,
|
|
545
|
+
lazy_load: bool = True,
|
|
546
|
+
**kwargs,
|
|
547
|
+
) -> List:
|
|
548
|
+
"""
|
|
549
|
+
Read an ODIM H5 file.
|
|
550
|
+
|
|
551
|
+
Parameters:
|
|
552
|
+
===========
|
|
553
|
+
odim_file: str
|
|
554
|
+
ODIM H5 filename.
|
|
555
|
+
lazy_load: bool
|
|
556
|
+
Lazily load the data if true, read and load in memory the entire dataset
|
|
557
|
+
if false.
|
|
558
|
+
include_fields: list
|
|
559
|
+
Specific fields to be exclusively read.
|
|
560
|
+
exclude_fields: list
|
|
561
|
+
Specific fields to be excluded from reading.
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
========
|
|
565
|
+
radar: list
|
|
566
|
+
List of xarray datasets, each item in a the list is one sweep of the
|
|
567
|
+
radar data (ordered from lowest elevation scan to highest).
|
|
568
|
+
"""
|
|
569
|
+
(radar, _) = read_write_odim(odim_file, lazy_load=lazy_load, **kwargs)
|
|
570
|
+
return radar
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def odim_str_type_id(text_bytes):
|
|
574
|
+
"""Generate ODIM-conformant h5py string type ID for `text_bytes`."""
|
|
575
|
+
# h5py default string type is STRPAD STR_NULLPAD
|
|
576
|
+
# ODIM spec string type is STRPAD STR_NULLTERM
|
|
577
|
+
type_id = h5py.h5t.TypeID.copy(h5py.h5t.C_S1)
|
|
578
|
+
type_id.set_strpad(h5py.h5t.STR_NULLTERM)
|
|
579
|
+
type_id.set_size(len(text_bytes) + 1)
|
|
580
|
+
return type_id
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def write_odim_str_attrib(group, attrib_name: str, text: str):
|
|
584
|
+
"""Write ODIM-conformant h5py string attribute.
|
|
585
|
+
|
|
586
|
+
@param group: h5py group
|
|
587
|
+
@param attrib_name: attribute name
|
|
588
|
+
@param text: attribute value
|
|
589
|
+
"""
|
|
590
|
+
if attrib_name in group.attrs:
|
|
591
|
+
del group.attrs[attrib_name]
|
|
592
|
+
|
|
593
|
+
group_id = group.id
|
|
594
|
+
text_bytes = text.encode("utf-8")
|
|
595
|
+
type_id = odim_str_type_id(text_bytes)
|
|
596
|
+
space = h5py.h5s.create(h5py.h5s.SCALAR)
|
|
597
|
+
att_id = h5py.h5a.create(group_id, attrib_name.encode("utf-8"), type_id, space)
|
|
598
|
+
text_array = np.array(text_bytes)
|
|
599
|
+
att_id.write(text_array)
|
|
600
|
+
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def copy_h5_data(h5_tilt, orig_id: str):
|
|
605
|
+
"""Add a data array to `h5_tilt` by copying data with `orig_id`.
|
|
606
|
+
|
|
607
|
+
@param h5_tilt: h5py sweep we're adding to.
|
|
608
|
+
@param orig_id: id of original data.
|
|
609
|
+
|
|
610
|
+
@return id of new data.
|
|
611
|
+
"""
|
|
612
|
+
# current data_count
|
|
613
|
+
data_count = len([k for k in h5_tilt.keys() if k.startswith("data")])
|
|
614
|
+
|
|
615
|
+
# use data_count for data_id
|
|
616
|
+
data_id = f"data{data_count + 1}"
|
|
617
|
+
|
|
618
|
+
# duplicate original
|
|
619
|
+
h5_tilt.copy(orig_id, data_id)
|
|
620
|
+
|
|
621
|
+
# return id
|
|
622
|
+
return data_id
|
|
@@ -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.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyodim
|
|
3
|
+
Version: 0.5.0
|
|
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
|
+
License: ISC
|
|
9
|
+
Keywords: odim h5 radar file reader
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy >=1.21.0
|
|
22
|
+
Requires-Dist: dask >=2020.12.0
|
|
23
|
+
Requires-Dist: xarray >=0.18.0
|
|
24
|
+
Requires-Dist: pyproj >=3.1.0
|
|
25
|
+
Requires-Dist: h5py >=3.0
|
|
26
|
+
Requires-Dist: pytest
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# pyodim
|
|
32
|
+
|
|
33
|
+
`pyodim` is a Python library for reading ODIM H5 radar files, transforming them into xarray datasets with geographic coordinates. This library is designed for users needing direct access to ODIM H5 files, providing tools to read and process radar data.
|
|
34
|
+
|
|
35
|
+
## Table of Contents
|
|
36
|
+
- [Overview](#overview)
|
|
37
|
+
- [Installation](#installation)
|
|
38
|
+
- [Usage](#usage)
|
|
39
|
+
|
|
40
|
+
## Overview
|
|
41
|
+
The `pyodim` library provides essential functions for handling ODIM H5 radar data. It reads radar sweeps and converts them into xarray datasets, handling various metadata and radar coordinates transformations. The main function, `read_odim`, enables easy access to radar data in a format compatible with Python's data analysis ecosystem.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
`pyodim` is available on PyPI:
|
|
46
|
+
```bash
|
|
47
|
+
pip install pyodim
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
It requires the following packages: `h5py pyproj pandas numpy xarray dask`.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
The main entry point for pyodim is the read_odim function, which reads a sweep from an ODIM H5 file and outputs an xarray dataset.
|
|
55
|
+
|
|
56
|
+
### Example
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from pyodim import read_odim
|
|
60
|
+
|
|
61
|
+
# Read an ODIM H5 file
|
|
62
|
+
dataset = read_odim("radar_file.h5", nslice=0)
|
|
63
|
+
print(dataset)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`read_odim` takes the following parameters:
|
|
67
|
+
- `odim_file` (str): Path to the ODIM H5 file.
|
|
68
|
+
- `nslice` (int, optional): Sweep number to read (default is 0).
|
|
69
|
+
- `include_fields` (List, optional): Fields to read.
|
|
70
|
+
- `exclude_fields` (List, optional): Fields to exclude.
|
|
71
|
+
- `check_NI` (bool, optional): Check Nyquist parameter consistency (default is False).
|
|
72
|
+
- `read_write` (bool, optional): Open in read-write mode if True.
|
|
73
|
+
|
|
74
|
+
Feel free to contribute to pyodim by submitting issues or pull requests.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pyodim/__init__.py,sha256=-jazD3XqQHi_tmszeldh5t0k2xeC8hqpOmpPFpzbkMg,184
|
|
2
|
+
pyodim/__version__.py,sha256=LBK46heutvn3KmsCrKIYu8RQikbfnjZaj2xFrXaeCzQ,22
|
|
3
|
+
pyodim/pyodim.py,sha256=6Rnb0-IdEcXIU1Ce9-VUBpE85OlC3BQP9ySzuTnflqU,18761
|
|
4
|
+
pyodim-0.5.0.dist-info/LICENSE,sha256=jbsdHkz-4lt0hJ69934yYXHYzxKf9QFXT1Y-jHT38z4,1070
|
|
5
|
+
pyodim-0.5.0.dist-info/METADATA,sha256=huBA5cjMYzKCe20-KUTnxXrfk15nRKdPPQLhqnCBqSA,2654
|
|
6
|
+
pyodim-0.5.0.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
|
7
|
+
pyodim-0.5.0.dist-info/top_level.txt,sha256=iwGwb4TuEh9FraL4yhk5QdxaV7yIlybKgvo6-hVHpIs,7
|
|
8
|
+
pyodim-0.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyodim
|