radarx 0.1.dev2__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.
- radarx/__init__.py +5 -0
- radarx/io/__init__.py +15 -0
- radarx/io/imd.py +645 -0
- radarx/radarx.py +1 -0
- radarx/version.py +16 -0
- radarx-0.1.dev2.dist-info/AUTHORS.rst +13 -0
- radarx-0.1.dev2.dist-info/LICENSE +22 -0
- radarx-0.1.dev2.dist-info/METADATA +31 -0
- radarx-0.1.dev2.dist-info/RECORD +11 -0
- radarx-0.1.dev2.dist-info/WHEEL +5 -0
- radarx-0.1.dev2.dist-info/top_level.txt +1 -0
radarx/__init__.py
ADDED
radarx/io/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# Copyright (c) 2024, Hamid Ali Syed.
|
|
3
|
+
# Distributed under the MIT License. See LICENSE for more info.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Xarray Backends
|
|
7
|
+
===============
|
|
8
|
+
|
|
9
|
+
.. toctree::
|
|
10
|
+
:maxdepth: 4
|
|
11
|
+
|
|
12
|
+
.. automodule:: radarx.io.imd
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from imd import * # noqa
|
radarx/io/imd.py
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IMD Data Reader
|
|
3
|
+
---------------
|
|
4
|
+
|
|
5
|
+
This module provides functionality to read and process single radar files from
|
|
6
|
+
the Indian Meteorological Department (IMD), returning a quasi-CF-Radial xarray
|
|
7
|
+
Dataset.
|
|
8
|
+
|
|
9
|
+
Author: Hamid Ali Syed
|
|
10
|
+
Email: hamidsyed37 [at] gmail [dot] com
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import itertools
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import xarray as xr
|
|
17
|
+
from datatree import DataTree
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"read_sweep" "read_volume",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_sweep(file):
|
|
25
|
+
"""
|
|
26
|
+
Reads a single radar file from IMD and processes it into a quasi-CF-Radial dataset.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
file : str or Path
|
|
31
|
+
Path to the radar file.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
ds : xarray.Dataset
|
|
36
|
+
Quasi-CF-Radial dataset with renamed coordinates, variables, and calculated fields.
|
|
37
|
+
"""
|
|
38
|
+
ds = xr.open_dataset(file, engine="netcdf4")
|
|
39
|
+
|
|
40
|
+
# Rename dimensions and variables for consistency with CF-Radial format
|
|
41
|
+
ds = ds.rename_dims({"radial": "azimuth", "bin": "range"})
|
|
42
|
+
ds = ds.rename_vars(
|
|
43
|
+
{
|
|
44
|
+
"radialAzim": "azimuth",
|
|
45
|
+
"radialElev": "elevation",
|
|
46
|
+
"elevationAngle": "fixed_angle",
|
|
47
|
+
"radialTime": "time",
|
|
48
|
+
"nyquist": "nyquist_velocity",
|
|
49
|
+
"unambigRange": "unambiguous_range",
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
ds = ds.set_coords(["azimuth", "elevation"])
|
|
53
|
+
|
|
54
|
+
# Rename site-related coordinates
|
|
55
|
+
site_coords = {"siteLat": "latitude", "siteLon": "longitude", "siteAlt": "altitude"}
|
|
56
|
+
ds = ds.rename_vars(site_coords)
|
|
57
|
+
|
|
58
|
+
# Add ray gate spacing variable
|
|
59
|
+
ds["ray_gate_spacing"] = xr.DataArray(
|
|
60
|
+
data=np.repeat(ds.gateSize.values, ds.azimuth.size),
|
|
61
|
+
dims=("azimuth",),
|
|
62
|
+
attrs={"long_name": "gate_spacing_for_ray", "units": "meters"},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Rename moments for radar variables
|
|
66
|
+
moments = {
|
|
67
|
+
"T": "DBT", # Total power
|
|
68
|
+
"Z": "DBZ", # Reflectivity
|
|
69
|
+
"V": "VEL", # Velocity
|
|
70
|
+
"W": "WIDTH", # Spectrum width
|
|
71
|
+
}
|
|
72
|
+
# Goes First in order
|
|
73
|
+
ds = ds.rename_vars(moments)
|
|
74
|
+
|
|
75
|
+
# Compute range and assign to dataset, Goes 2nd
|
|
76
|
+
ds = ds.pipe(_compute_range)
|
|
77
|
+
|
|
78
|
+
# Compute angle resolution, Goes 3rd
|
|
79
|
+
ds = ds.pipe(_angle_resolution)
|
|
80
|
+
|
|
81
|
+
# Determine scan type and mode, Goes 4th
|
|
82
|
+
ds = ds.pipe(_scantype)
|
|
83
|
+
|
|
84
|
+
# Time coverage handling, Goes 5th
|
|
85
|
+
ds = ds.pipe(_time_coverage)
|
|
86
|
+
|
|
87
|
+
# Handle sweep numbers, Goes 6th
|
|
88
|
+
ds = ds.pipe(_sweep_number)
|
|
89
|
+
|
|
90
|
+
# Assign start_end_ray_index, Goes 6th
|
|
91
|
+
ds = ds.pipe(_assign_sweep_metadata)
|
|
92
|
+
|
|
93
|
+
ds = ds.pipe(_assign_metadata)
|
|
94
|
+
|
|
95
|
+
ds = ds.swap_dims({"azimuth": "time"})
|
|
96
|
+
|
|
97
|
+
return ds
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def read_volume(files):
|
|
101
|
+
"""
|
|
102
|
+
Reads multiple files and creates a volume scan data.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
files : list of files or list of lists
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
DataTree of the volumes
|
|
111
|
+
"""
|
|
112
|
+
# Check if there are nested lists and merge them if needed
|
|
113
|
+
if any(isinstance(i, list) for i in files):
|
|
114
|
+
files = _merge_file_lists(files)
|
|
115
|
+
|
|
116
|
+
# Determine volume groups from files
|
|
117
|
+
grouped_dataset_list = _determine_volumes(files)
|
|
118
|
+
|
|
119
|
+
# Create a list to store the volumes
|
|
120
|
+
volume_datasets = []
|
|
121
|
+
|
|
122
|
+
# Iterate over each group of datasets (which represent a volume)
|
|
123
|
+
for datasets in grouped_dataset_list:
|
|
124
|
+
vol = create_volume(datasets) # Custom function to create the volume
|
|
125
|
+
if isinstance(vol, xr.Dataset):
|
|
126
|
+
volume_datasets.append(vol) # Only add if it's an xarray.Dataset
|
|
127
|
+
del vol # Cleanup after each volume is processed
|
|
128
|
+
|
|
129
|
+
# Construct the DataTree from the volume datasets (ensure that we pass datasets, not a list)
|
|
130
|
+
volumes = DataTree.from_dict(
|
|
131
|
+
{f"volume_{i}": volume for i, volume in enumerate(volume_datasets)}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return volumes
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _merge_file_lists(file_lists):
|
|
138
|
+
"""
|
|
139
|
+
Merges internal lists of files into a single list.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
file_lists : list of lists
|
|
144
|
+
A list containing multiple internal lists of file paths.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
merged_list : list
|
|
149
|
+
A single list containing all the file paths from the internal lists.
|
|
150
|
+
"""
|
|
151
|
+
# Use itertools.chain to flatten the list of lists
|
|
152
|
+
merged_list = list(itertools.chain(*file_lists))
|
|
153
|
+
|
|
154
|
+
return merged_list
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _compute_range(ds):
|
|
158
|
+
"""
|
|
159
|
+
Computes the range to each gate and adds it to the dataset.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
ds : xarray.Dataset
|
|
164
|
+
Input dataset.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
ds : xarray.Dataset
|
|
169
|
+
Dataset with computed range coordinates.
|
|
170
|
+
"""
|
|
171
|
+
# Using ds.sizes instead of ds.dims to avoid future warning
|
|
172
|
+
gate_count = ds.sizes["range"]
|
|
173
|
+
|
|
174
|
+
# Ensure proper conversion to avoid deprecation warning
|
|
175
|
+
ranges = np.arange(
|
|
176
|
+
ds["firstGateRange"].values,
|
|
177
|
+
gate_count * ds["gateSize"].values + ds["firstGateRange"].values,
|
|
178
|
+
ds["gateSize"].astype(int).values, # Ensure proper integer type conversion
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
ds.coords["range"] = xr.DataArray(
|
|
182
|
+
ranges,
|
|
183
|
+
dims=("range",),
|
|
184
|
+
attrs={
|
|
185
|
+
"standard_name": "range_to_center_of_measurement_volume",
|
|
186
|
+
"long_name": "Range from instrument to center of gate",
|
|
187
|
+
"units": "meters",
|
|
188
|
+
"spacing_is_constant": "true",
|
|
189
|
+
"meters_to_center_of_first_gate": ds["firstGateRange"].values,
|
|
190
|
+
"meters_between_gates": ds["gateSize"].values,
|
|
191
|
+
},
|
|
192
|
+
)
|
|
193
|
+
return ds
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _angle_resolution(ds):
|
|
197
|
+
"""
|
|
198
|
+
Adds angular resolution for each ray to the dataset.
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
ds : xarray.Dataset
|
|
203
|
+
Input dataset.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
ds : xarray.Dataset
|
|
208
|
+
Dataset with angular resolution variable.
|
|
209
|
+
"""
|
|
210
|
+
ds = ds.drop_dims("sweep", errors="ignore")
|
|
211
|
+
ds["ray_angle_res"] = xr.DataArray(
|
|
212
|
+
data=[ds["angleResolution"]],
|
|
213
|
+
dims=("sweep",),
|
|
214
|
+
attrs={"long_name": "angular_resolution_between_rays", "units": "degrees"},
|
|
215
|
+
)
|
|
216
|
+
ds = ds.drop_vars("angleResolution", errors="ignore")
|
|
217
|
+
return ds
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _scantype(ds):
|
|
221
|
+
"""
|
|
222
|
+
Determines the scan type based on the scanType variable.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
ds : xarray.Dataset
|
|
227
|
+
Input dataset.
|
|
228
|
+
|
|
229
|
+
Returns
|
|
230
|
+
-------
|
|
231
|
+
ds : xarray.Dataset
|
|
232
|
+
Dataset with scan type and sweep mode variables.
|
|
233
|
+
"""
|
|
234
|
+
scan_type_lookup = {
|
|
235
|
+
4: (b"azimuth_surveillance", b"ppi"),
|
|
236
|
+
7: (b"elevation_surveillance", b"rhi"),
|
|
237
|
+
1: (b"sector", b"ppi_sector"),
|
|
238
|
+
2: (b"manual_rhi", b"rhi_sector"),
|
|
239
|
+
0: (b"unknown", b"unknown"),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# Extract the scalar value from the array
|
|
243
|
+
scan_type = int(ds["scanType"].astype(int).values)
|
|
244
|
+
|
|
245
|
+
# Get sweep_mode and scan_type_name from lookup dictionary
|
|
246
|
+
sweep_mode, scan_type_name = scan_type_lookup.get(
|
|
247
|
+
scan_type, (b"unknown", b"unknown")
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
ds["sweep_mode"] = xr.DataArray(data=[sweep_mode], dims=("sweep"))
|
|
251
|
+
ds["scan_type"] = xr.DataArray(data=[scan_type_name], dims=("sweep"))
|
|
252
|
+
# ds = ds.drop_vars("scanType", errors="ignore")
|
|
253
|
+
ds["fixed_angle"] = xr.DataArray(data=[ds["fixed_angle"]], dims=("sweep",))
|
|
254
|
+
return ds
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _time_coverage(ds):
|
|
258
|
+
"""
|
|
259
|
+
Processes time coverage start and end times.
|
|
260
|
+
|
|
261
|
+
Parameters
|
|
262
|
+
----------
|
|
263
|
+
ds : xarray.Dataset
|
|
264
|
+
Input dataset.
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
ds : xarray.Dataset
|
|
269
|
+
Dataset with time coverage start and end variables.
|
|
270
|
+
"""
|
|
271
|
+
ds = ds.rename_vars({"esStartTime": "time_coverage_start"})
|
|
272
|
+
ds["time_coverage_start"] = xr.DataArray(
|
|
273
|
+
data=str(
|
|
274
|
+
ds.time_coverage_start.dt.strftime("%Y-%m-%dT%H:%M:%SZ").values
|
|
275
|
+
).encode("utf-8"),
|
|
276
|
+
attrs={"standard_name": "data_volume_start_time_utc"},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
ds["time_coverage_end"] = xr.DataArray(
|
|
280
|
+
data=str(ds.time.max().dt.strftime("%Y-%m-%dT%H:%M:%SZ").values).encode(
|
|
281
|
+
"utf-8"
|
|
282
|
+
),
|
|
283
|
+
attrs={"standard_name": "data_volume_end_time_utc"},
|
|
284
|
+
)
|
|
285
|
+
return ds
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _sweep_number(ds):
|
|
289
|
+
"""
|
|
290
|
+
Adds sweep number information to the dataset.
|
|
291
|
+
|
|
292
|
+
Parameters
|
|
293
|
+
----------
|
|
294
|
+
ds : xarray.Dataset
|
|
295
|
+
Input dataset.
|
|
296
|
+
|
|
297
|
+
Returns
|
|
298
|
+
-------
|
|
299
|
+
ds : xarray.Dataset
|
|
300
|
+
Dataset with sweep number variable.
|
|
301
|
+
"""
|
|
302
|
+
sweep_num = ds["elevationNumber"].astype(int).values
|
|
303
|
+
ds["sweep_number"] = xr.DataArray(
|
|
304
|
+
data=[sweep_num],
|
|
305
|
+
dims="sweep",
|
|
306
|
+
attrs={"long_name": "sweep_index_number_0_based", "units": ""},
|
|
307
|
+
)
|
|
308
|
+
ds = ds.drop_vars("elevationNumber")
|
|
309
|
+
return ds
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _assign_sweep_metadata(ds):
|
|
313
|
+
"""
|
|
314
|
+
Assigns and updates the sweep metadata after reading multiple files.
|
|
315
|
+
|
|
316
|
+
Parameters
|
|
317
|
+
----------
|
|
318
|
+
ds : xarray.Dataset
|
|
319
|
+
Combined dataset with multiple sweeps.
|
|
320
|
+
|
|
321
|
+
Returns
|
|
322
|
+
-------
|
|
323
|
+
ds : xarray.Dataset
|
|
324
|
+
Dataset with updated sweep metadata.
|
|
325
|
+
"""
|
|
326
|
+
num_sweeps = ds.sizes["sweep"]
|
|
327
|
+
sweep_start = np.arange(0, num_sweeps * ds.sizes["azimuth"], ds.sizes["azimuth"])
|
|
328
|
+
sweep_end = sweep_start + ds.sizes["azimuth"] - 1
|
|
329
|
+
|
|
330
|
+
ds["sweep_start_ray_index"] = xr.DataArray(
|
|
331
|
+
sweep_start,
|
|
332
|
+
dims="sweep",
|
|
333
|
+
attrs={"long_name": "Index of the first ray in the sweep"},
|
|
334
|
+
)
|
|
335
|
+
ds["sweep_end_ray_index"] = xr.DataArray(
|
|
336
|
+
sweep_end,
|
|
337
|
+
dims="sweep",
|
|
338
|
+
attrs={"long_name": "Index of the last ray in the sweep"},
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Update sweep number if not done already
|
|
342
|
+
if "sweep_number" not in ds:
|
|
343
|
+
ds["sweep_number"] = xr.DataArray(
|
|
344
|
+
np.arange(num_sweeps), dims="sweep", attrs={"long_name": "Sweep number"}
|
|
345
|
+
)
|
|
346
|
+
return ds
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _find_variables_by_coords(ds, required_dims):
|
|
350
|
+
"""
|
|
351
|
+
Finds variables in the dataset that have the specified dimensions.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
ds : xarray.Dataset
|
|
356
|
+
The input dataset.
|
|
357
|
+
|
|
358
|
+
required_dims : set
|
|
359
|
+
A set of required dimensions (e.g., {"range", "azimuth"}).
|
|
360
|
+
|
|
361
|
+
Returns
|
|
362
|
+
-------
|
|
363
|
+
matching_vars : list
|
|
364
|
+
List of variable names that have the specified dimensions.
|
|
365
|
+
|
|
366
|
+
Example
|
|
367
|
+
-------
|
|
368
|
+
Assuming `ds` is your xarray.Dataset and you want to find variables
|
|
369
|
+
with both "range" and "azimuth" dimensions
|
|
370
|
+
vars_with_rng_and_az = find_variables_by_coords(ds, {"range", "azimuth"})
|
|
371
|
+
"""
|
|
372
|
+
matching_vars = []
|
|
373
|
+
|
|
374
|
+
for var_name, var_data in ds.data_vars.items():
|
|
375
|
+
var_dims = set(var_data.dims)
|
|
376
|
+
if required_dims.issubset(var_dims):
|
|
377
|
+
matching_vars.append(var_name)
|
|
378
|
+
|
|
379
|
+
return matching_vars
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _assign_metadata(ds):
|
|
383
|
+
field_names = _find_variables_by_coords(ds, {"azimuth", "range"})
|
|
384
|
+
field_names = ", ".join(field_names)
|
|
385
|
+
ds.attrs = {
|
|
386
|
+
"Conventions": "CF/Radial instrument_parameters",
|
|
387
|
+
"version": "1.3",
|
|
388
|
+
"title": "",
|
|
389
|
+
"institution": "India Meteorological Department",
|
|
390
|
+
"references": "",
|
|
391
|
+
"source": "",
|
|
392
|
+
"comment": "im/radarx",
|
|
393
|
+
"instrument_name": "",
|
|
394
|
+
"history": "",
|
|
395
|
+
"field_names": field_names,
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return ds
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _determine_nsweeps(files):
|
|
402
|
+
"""
|
|
403
|
+
Determine the number of sweeps (nsweeps) and group files
|
|
404
|
+
accordingly into sweep_groups.
|
|
405
|
+
If the nsweeps cannot be determined from metadata, the
|
|
406
|
+
function will detect where the sweep elevations start
|
|
407
|
+
decreasing to identify the start of a new volume.
|
|
408
|
+
"""
|
|
409
|
+
import re
|
|
410
|
+
|
|
411
|
+
import xarray as xr
|
|
412
|
+
|
|
413
|
+
def _natural_sort_key(s, _re=re.compile(r"(\d+)")):
|
|
414
|
+
return [int(t) if i & 1 else t.lower() for i, t in enumerate(_re.split(s))]
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
# Check if files exist and open the first file to determine nsweeps
|
|
418
|
+
if len(files):
|
|
419
|
+
file = files[0]
|
|
420
|
+
else:
|
|
421
|
+
return []
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
ds = read_sweep(file)
|
|
425
|
+
except OSError:
|
|
426
|
+
ds = xr.open_dataset(file, engine="netcdf4")
|
|
427
|
+
|
|
428
|
+
# Try to retrieve nsweeps from the dataset
|
|
429
|
+
try:
|
|
430
|
+
nsweeps = ds.attrs["nsweeps"]
|
|
431
|
+
except KeyError:
|
|
432
|
+
nsweeps = ds.sizes["sweep"]
|
|
433
|
+
except KeyError:
|
|
434
|
+
nsweeps = ds["sweep"].size
|
|
435
|
+
except KeyError:
|
|
436
|
+
nsweeps = ds["fixed_angle"].size
|
|
437
|
+
|
|
438
|
+
# Sort the files naturally and group them by nsweeps
|
|
439
|
+
files = sorted(files, key=_natural_sort_key)
|
|
440
|
+
sweep_groups = []
|
|
441
|
+
for i in range(0, len(files), nsweeps):
|
|
442
|
+
sweep_groups.append(files[i : i + nsweeps])
|
|
443
|
+
|
|
444
|
+
del ds
|
|
445
|
+
|
|
446
|
+
return sweep_groups
|
|
447
|
+
|
|
448
|
+
except Exception:
|
|
449
|
+
# Fallback method if nsweeps cannot be determined from metadata
|
|
450
|
+
sweep_groups = []
|
|
451
|
+
sweep_times = []
|
|
452
|
+
sweep_elevs = []
|
|
453
|
+
files = sorted(files, key=_natural_sort_key)
|
|
454
|
+
|
|
455
|
+
# Collect sweep times and elevations
|
|
456
|
+
for file in files:
|
|
457
|
+
ds = xr.open_dataset(file, engine="netcdf4")
|
|
458
|
+
if "esStartTime" in ds:
|
|
459
|
+
estime = (
|
|
460
|
+
ds["esStartTime"].dt.strftime("%Y-%m-%dT%H:%M:%S").values.item()
|
|
461
|
+
)
|
|
462
|
+
elif "time_coverage_start" in ds:
|
|
463
|
+
estime = (
|
|
464
|
+
ds["time_coverage_start"]
|
|
465
|
+
.dt.strftime("%Y-%m-%dT%H:%M:%S")
|
|
466
|
+
.values.item()
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if "elevationAngle" in ds:
|
|
470
|
+
ele_ang = ds["elevationAngle"].values.item()
|
|
471
|
+
elif "fixed_angle" in ds:
|
|
472
|
+
ele_ang = ds["fixed_angle"].values.item()
|
|
473
|
+
sweep_times.append(estime)
|
|
474
|
+
sweep_elevs.append(ele_ang)
|
|
475
|
+
del ds
|
|
476
|
+
|
|
477
|
+
# Determine sweep groups based on elevation increases/decreases
|
|
478
|
+
current_group = []
|
|
479
|
+
nsweeps = 0
|
|
480
|
+
|
|
481
|
+
for i in range(1, len(sweep_elevs)):
|
|
482
|
+
current_group.append(files[i - 1])
|
|
483
|
+
|
|
484
|
+
# Check if the elevation decreases, indicating the start of a new volume
|
|
485
|
+
if sweep_elevs[i] < sweep_elevs[i - 1]:
|
|
486
|
+
nsweeps = len(current_group)
|
|
487
|
+
sweep_groups.append(current_group)
|
|
488
|
+
current_group = []
|
|
489
|
+
|
|
490
|
+
# Append the last group
|
|
491
|
+
current_group.append(files[-1])
|
|
492
|
+
sweep_groups.append(current_group)
|
|
493
|
+
|
|
494
|
+
return sweep_groups
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _determine_volumes(files):
|
|
498
|
+
"""Determine Volumes"""
|
|
499
|
+
swp_lists = _determine_nsweeps(files)
|
|
500
|
+
volumes = []
|
|
501
|
+
for i, swp_list in enumerate(swp_lists):
|
|
502
|
+
dataset = []
|
|
503
|
+
for file in swp_list:
|
|
504
|
+
try:
|
|
505
|
+
ds = read_sweep(file)
|
|
506
|
+
except OSError:
|
|
507
|
+
ds = xr.open_dataset(file)
|
|
508
|
+
dataset.append(ds)
|
|
509
|
+
del ds
|
|
510
|
+
volumes.append(dataset)
|
|
511
|
+
del dataset
|
|
512
|
+
return volumes
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def create_volume(dataset_list):
|
|
516
|
+
"""
|
|
517
|
+
Concatenate a list of datasets along the 'time' and 'sweep' dimensions.
|
|
518
|
+
Non-time-varying variables will be retained as single values.
|
|
519
|
+
Also updates `time_coverage_end` based on the maximum time value.
|
|
520
|
+
|
|
521
|
+
You have to give the list of sweeps which are already organized/sorted,
|
|
522
|
+
for that you can either make it manually, or use `_determine_volumes` function
|
|
523
|
+
|
|
524
|
+
Parameters
|
|
525
|
+
----------
|
|
526
|
+
dataset_list : list of xarray.Dataset
|
|
527
|
+
List of datasets representing individual sweeps.
|
|
528
|
+
|
|
529
|
+
Returns
|
|
530
|
+
-------
|
|
531
|
+
xarray.Dataset
|
|
532
|
+
Concatenated dataset with proper time and sweep dimensions.
|
|
533
|
+
"""
|
|
534
|
+
|
|
535
|
+
# Identify variables that should be concatenated along the sweep dimension
|
|
536
|
+
sweep_vars = [
|
|
537
|
+
"fixed_angle",
|
|
538
|
+
"ray_angle_res",
|
|
539
|
+
"sweep_mode",
|
|
540
|
+
"scan_type",
|
|
541
|
+
"sweep_number",
|
|
542
|
+
"sweep_start_ray_index",
|
|
543
|
+
"sweep_end_ray_index",
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
# Identify variables that are constant across sweeps and should not be concatenated
|
|
547
|
+
constant_vars = [
|
|
548
|
+
"time_coverage_start",
|
|
549
|
+
"latitude",
|
|
550
|
+
"longitude",
|
|
551
|
+
"altitude",
|
|
552
|
+
"firstGateRange",
|
|
553
|
+
"gateSize",
|
|
554
|
+
"nyquist_velocity",
|
|
555
|
+
"unambiguous_range",
|
|
556
|
+
"calibConst",
|
|
557
|
+
"radarConst",
|
|
558
|
+
"beamWidthHori",
|
|
559
|
+
"pulseWidth",
|
|
560
|
+
"bandWidth",
|
|
561
|
+
"filterDop",
|
|
562
|
+
"azimuthSpeed",
|
|
563
|
+
"highPRF",
|
|
564
|
+
"lowPRF",
|
|
565
|
+
"dwellTime",
|
|
566
|
+
"waveLength",
|
|
567
|
+
"calI0",
|
|
568
|
+
"calNoise",
|
|
569
|
+
"groundHeight",
|
|
570
|
+
"meltHeight",
|
|
571
|
+
"scanType",
|
|
572
|
+
"logNoise",
|
|
573
|
+
"linNoise",
|
|
574
|
+
"inphaseOffset",
|
|
575
|
+
"quadratureOffset",
|
|
576
|
+
"logSlope",
|
|
577
|
+
"logFilter",
|
|
578
|
+
"filterPntClt",
|
|
579
|
+
"filterThreshold",
|
|
580
|
+
"sampleNum",
|
|
581
|
+
"SQIThresh",
|
|
582
|
+
"LOGThresh",
|
|
583
|
+
"SIGThresh",
|
|
584
|
+
"CSRThresh",
|
|
585
|
+
"DBTThreshFlag",
|
|
586
|
+
"DBZThreshFlag",
|
|
587
|
+
"VELThreshFlag",
|
|
588
|
+
"WIDThreshFlag",
|
|
589
|
+
]
|
|
590
|
+
|
|
591
|
+
# Step 1: Concatenate the datasets along the time dimension
|
|
592
|
+
time_concat = xr.concat(
|
|
593
|
+
[ds.drop_vars(sweep_vars, errors="ignore") for ds in dataset_list], dim="time"
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
# Step 2: Handle sweep-specific variables (metadata)
|
|
597
|
+
sweep_datasets = []
|
|
598
|
+
for i, ds in enumerate(dataset_list):
|
|
599
|
+
sweep_ds = {}
|
|
600
|
+
for var in sweep_vars:
|
|
601
|
+
if var in ds:
|
|
602
|
+
# Expand dims to add a 'sweep' dimension if needed
|
|
603
|
+
if "sweep" not in ds[var].dims:
|
|
604
|
+
sweep_ds[var] = ds[var].expand_dims("sweep")
|
|
605
|
+
else:
|
|
606
|
+
sweep_ds[var] = ds[var]
|
|
607
|
+
|
|
608
|
+
# Update sweep start and end ray indices
|
|
609
|
+
sweep_start = i * ds.sizes["time"]
|
|
610
|
+
sweep_end = sweep_start + ds.sizes["time"] - 1
|
|
611
|
+
|
|
612
|
+
sweep_ds["sweep_start_ray_index"] = xr.DataArray(
|
|
613
|
+
np.array([sweep_start]),
|
|
614
|
+
dims="sweep",
|
|
615
|
+
attrs={"long_name": "Index of the first ray in the sweep"},
|
|
616
|
+
)
|
|
617
|
+
sweep_ds["sweep_end_ray_index"] = xr.DataArray(
|
|
618
|
+
np.array([sweep_end]),
|
|
619
|
+
dims="sweep",
|
|
620
|
+
attrs={"long_name": "Index of the last ray in the sweep"},
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Add the updated sweep metadata dataset to the list
|
|
624
|
+
sweep_datasets.append(xr.Dataset(sweep_ds))
|
|
625
|
+
|
|
626
|
+
# Step 3: Concatenate the sweep-specific datasets along the sweep dimension
|
|
627
|
+
sweep_concat = xr.concat(sweep_datasets, dim="sweep")
|
|
628
|
+
|
|
629
|
+
# Step 4: Merge the time-concatenated data, sweep-concatenated metadata, and constant variables
|
|
630
|
+
combined_ds = xr.merge([time_concat, sweep_concat])
|
|
631
|
+
|
|
632
|
+
# Step 5: Assign constant variables (they do not need to be concatenated)
|
|
633
|
+
for var in constant_vars:
|
|
634
|
+
if var in dataset_list[0]:
|
|
635
|
+
combined_ds[var] = dataset_list[0][var]
|
|
636
|
+
|
|
637
|
+
# Step 6: Update the 'time_coverage_end' variable with the maximum time value
|
|
638
|
+
combined_ds["time_coverage_end"] = xr.DataArray(
|
|
639
|
+
data=str(
|
|
640
|
+
combined_ds.time.max().dt.strftime("%Y-%m-%dT%H:%M:%SZ").values
|
|
641
|
+
).encode("utf-8"),
|
|
642
|
+
attrs={"standard_name": "data_volume_end_time_utc"},
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
return combined_ds
|
radarx/radarx.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Main module."""
|
radarx/version.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.1.dev2'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev2')
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Hamid Ali Syed
|
|
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.
|
|
22
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: radarx
|
|
3
|
+
Version: 0.1.dev2
|
|
4
|
+
Summary: Xarray based radar toolkit
|
|
5
|
+
Author-email: Hamid Ali Syed <hamidsyed37@gmail.com>
|
|
6
|
+
Maintainer-email: Hamid Ali Syed <hamidsyed37@gmail.com>
|
|
7
|
+
License: MIT license
|
|
8
|
+
Project-URL: bugs, https://github.com/syedhamidali/radarx/issues
|
|
9
|
+
Project-URL: changelog, https://github.com/syedhamidali/radarx/blob/master/changelog.md
|
|
10
|
+
Project-URL: homepage, https://github.com/syedhamidali/radarx
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
License-File: AUTHORS.rst
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: coverage; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff; extra == "dev"
|
|
31
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
radarx/__init__.py,sha256=QWCxSXc75OVIFZMdBmyS16rGb2-gwooHiZxME5mxL3E,129
|
|
2
|
+
radarx/radarx.py,sha256=h0hwdogXGFqerm-5ZPeT-irPn91pCcQRjiHThXsRzEk,19
|
|
3
|
+
radarx/version.py,sha256=qI8Vf33xG4VceR8oyHmT7zDIjATXRT7C3GYLCaQ4dbk,419
|
|
4
|
+
radarx/io/__init__.py,sha256=T54dKCI1Ry9H0JTxqTCM7beOv_gKMtbpmSuaEdLHdTw,254
|
|
5
|
+
radarx/io/imd.py,sha256=V1KcaPcocjh894QnjYejTTLAhTRiEeM081U2WB5YMuw,18448
|
|
6
|
+
radarx-0.1.dev2.dist-info/AUTHORS.rst,sha256=ofa-p8YxgkF9L_CBDywg145c4zJi4RLxp4Z2O0ftep8,161
|
|
7
|
+
radarx-0.1.dev2.dist-info/LICENSE,sha256=ZEEk5q67LfzfTDG9HPbSXWJ5UskfEKD9CihOh44XxwU,1073
|
|
8
|
+
radarx-0.1.dev2.dist-info/METADATA,sha256=ump2wHwpa6SbMwfRMzJ-WLR4poazfSQNOI0Ne_9ecvE,1284
|
|
9
|
+
radarx-0.1.dev2.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
|
10
|
+
radarx-0.1.dev2.dist-info/top_level.txt,sha256=UssaY3VGVe1ajQm-JktlZII5EhQKhXGqw-e5upCC3zk,7
|
|
11
|
+
radarx-0.1.dev2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
radarx
|