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 ADDED
@@ -0,0 +1,7 @@
1
+ # primary read routines
2
+ from .pyodim import read_odim
3
+ from .pyodim import read_write_odim
4
+
5
+ # helper routines
6
+ from .pyodim import copy_h5_data
7
+ from .pyodim import write_odim_str_attrib
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.5.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pyodim