Skip to content

Commit 28c16de

Browse files
authored
ENH: read_mmcr method to read ARM MMCR b1-level data files (#1704)
* ENH: `read_mmcr` method to read ARM MMCR b1-level data files * STY: pep8 * STY: linting * STY: linting * STY: linting * FIX: add options to read dual-pol channel data; PRT from file instead of hardwired handbook values
1 parent 510f750 commit 28c16de

File tree

2 files changed

+265
-0
lines changed

2 files changed

+265
-0
lines changed

pyart/aux_io/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""
1515

1616
from .arm_vpt import read_kazr # noqa
17+
from .arm_vpt import read_mmcr # noqa
1718
from .d3r_gcpex_nc import read_d3r_gcpex_nc # noqa
1819
from .edge_netcdf import read_edge_netcdf # noqa
1920
from .gamic_hdf5 import read_gamic # noqa

pyart/aux_io/arm_vpt.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,267 @@ def read_kazr(
218218
elevation,
219219
instrument_parameters=instrument_parameters,
220220
)
221+
222+
223+
def read_mmcr(
224+
filename,
225+
field_names=None,
226+
additional_metadata=None,
227+
file_field_names=False,
228+
exclude_fields=None,
229+
include_fields=None,
230+
mode_names=None,
231+
mode_to_extract=None,
232+
):
233+
"""
234+
Read millimeter wavelength cloud radar (MMCR) b1 NetCDF ingest data.
235+
236+
Parameters
237+
----------
238+
filename : str
239+
Name of NetCDF file to read data from.
240+
field_names : dict, optional
241+
Dictionary mapping field names in the file names to radar field names.
242+
Unlike other read functions, fields not in this dictionary or having a
243+
value of None are still included in the radar.fields dictionary, to
244+
exclude them use the `exclude_fields` parameter. Fields which are
245+
mapped by this dictionary will be renamed from key to value.
246+
additional_metadata : dict of dicts, optional
247+
This parameter is not used, it is included for uniformity.
248+
file_field_names : bool, optional
249+
True to force the use of the field names from the file in which
250+
case the `field_names` parameter is ignored. False will use to
251+
`field_names` parameter to rename fields.
252+
exclude_fields : list or None, optional
253+
List of fields to exclude from the radar object. This is applied
254+
after the `file_field_names` and `field_names` parameters. Set
255+
to None to include all fields specified by include_fields.
256+
include_fields : list or None, optional
257+
List of fields to include from the radar object. This is applied
258+
after the `file_field_names` and `field_names` parameters. Set
259+
to None to include all fields not specified by exclude_fields.
260+
mode_names: dict
261+
Keys are mode dimension indices. Values are the corresponding mode
262+
abbreviations and prt. Using default values if None.
263+
mode_to_extract: str or None
264+
Abbreviation of mode to extract.
265+
Extracting all data for all modes if None.
266+
267+
Returns
268+
-------
269+
radar_out : Radar or dict of Radar objects
270+
If `mode_to_extract` is None, returns a dict with keys representing operated modes and
271+
values the corresponding `Radar` object.
272+
If `mode_to_extract` is specified returning the `Radar` object corresponding to the that
273+
mode.
274+
275+
"""
276+
277+
if mode_names is None:
278+
mode_names = {
279+
1: "bl",
280+
2: "ci",
281+
3: "ge",
282+
4: "pr",
283+
5: "dualpol_ch0", # Dual-pol receiver 0
284+
6: "dualpol_ch1", # Dual-pol receiver 1
285+
}
286+
287+
# create metadata retrieval object
288+
filemetadata = FileMetadata(
289+
"cfradial",
290+
field_names,
291+
additional_metadata,
292+
file_field_names,
293+
exclude_fields,
294+
include_fields,
295+
)
296+
297+
# read the data
298+
ncobj = netCDF4.Dataset(filename)
299+
ncvars = ncobj.variables
300+
301+
# 4.1 Global attribute -> move to metadata dictionary
302+
metadata = {k: getattr(ncobj, k) for k in ncobj.ncattrs()}
303+
metadata["n_gates_vary"] = "false"
304+
305+
# 4.2 Dimensions (do nothing)
306+
307+
# 4.3 Global variable -> move to metadata dictionary
308+
if "volume_number" in ncvars:
309+
metadata["volume_number"] = int(ncvars["volume_number"][:])
310+
else:
311+
metadata["volume_number"] = 0
312+
313+
global_vars = {
314+
"platform_type": "fixed",
315+
"instrument_type": "radar",
316+
"primary_axis": "axis_z",
317+
}
318+
# ignore time_* global variables, these are calculated from the time
319+
# variable when the file is written.
320+
for var, default_value in global_vars.items():
321+
if var in ncvars:
322+
metadata[var] = str(netCDF4.chartostring(ncvars[var][:]))
323+
else:
324+
metadata[var] = default_value
325+
326+
# 4.4 coordinate variables -> create attribute dictionaries (`_range` is generated in loop below)
327+
328+
# 4.5 Ray dimension variables
329+
330+
# 4.6 Location variables -> create attribute dictionaries
331+
# the only difference in this section to cfradial.read_cfradial is the
332+
# minor variable name differences:
333+
# latitude -> lat
334+
# longitude -> lon
335+
# altitdue -> alt
336+
latitude = cfradial._ncvar_to_dict(ncvars["lat"])
337+
longitude = cfradial._ncvar_to_dict(ncvars["lon"])
338+
altitude = cfradial._ncvar_to_dict(ncvars["alt"])
339+
340+
# 4.7 Sweep variables -> create atrribute dictionaries
341+
# this is the section that needed the most work since the initial NetCDF
342+
# file did not contain any sweep information
343+
sweep_number = filemetadata("sweep_number")
344+
sweep_number["data"] = np.array([0], dtype=np.int32)
345+
346+
sweep_mode = filemetadata("sweep_mode")
347+
sweep_mode["data"] = np.array(["vertical_pointing"], dtype=str)
348+
349+
fixed_angle = filemetadata("fixed_angle")
350+
fixed_angle["data"] = np.array([90.0], dtype=np.float32)
351+
352+
sweep_start_ray_index = filemetadata("sweep_start_ray_index")
353+
sweep_start_ray_index["data"] = np.array([0], dtype=np.int32)
354+
355+
# first sweep mode determines scan_type
356+
# this module is specific to vertically-pointing data
357+
scan_type = "vpt"
358+
359+
# 4.8 Sensor pointing variables -> create attribute dictionaries
360+
# this section also required some changes since the initial NetCDF did not
361+
# contain any sensor pointing variables
362+
363+
# 4.9 Moving platform geo-reference variables
364+
365+
# 4.10 Moments field data variables -> field attribute dictionary
366+
# all variables with dimensions of 'time', 'heights' are fields
367+
keys = [k for k, v in ncvars.items() if v.dimensions == ("time", "heights")]
368+
369+
# Determine if one or more modes are to be extracted and generate Radar object(s) output
370+
if mode_to_extract is None:
371+
modes = np.unique(ncvars["ModeNum"])
372+
radar_out = {}
373+
else:
374+
modes = [key for key in mode_names.keys() if mode_names[key] == mode_to_extract]
375+
for mode in modes:
376+
mode_sample_indices = ncvars["ModeNum"] == np.array(mode)
377+
active_range = ncvars["heights"][mode, :] > 0.0
378+
range_arr = ncvars["heights"][mode, active_range] - altitude["data"]
379+
time = cfradial._ncvar_to_dict(ncvars["time"])
380+
time["data"] = ncvars["time"][mode_sample_indices]
381+
_range = {
382+
"long_name": "Range (center of radar sample volume)",
383+
"units": "m AGL",
384+
"missing_value": -9999.0,
385+
"data": range_arr,
386+
}
387+
388+
fields = {}
389+
for key in keys:
390+
field_name = filemetadata.get_field_name(key)
391+
if field_name is None:
392+
if exclude_fields is not None and key in exclude_fields:
393+
continue
394+
if include_fields is not None and key not in include_fields:
395+
continue
396+
field_name = key
397+
398+
d = {
399+
k: getattr(ncvars[key], k)
400+
for k in ncvars[key].ncattrs()
401+
if k not in ["scale_factor", "add_offset"]
402+
}
403+
d["data"] = ncvars[key][mode_sample_indices, active_range]
404+
fields[field_name] = d
405+
406+
# 4.5 instrument_parameters sub-convention -> instrument_parameters dict
407+
# this section needed multiple changes and/or additions since the
408+
# instrument parameters were primarily located in the global attributes
409+
# this section is likely still incomplete
410+
411+
sweep_end_ray_index = filemetadata("sweep_end_ray_index")
412+
sweep_end_ray_index["data"] = np.array([time["data"].size - 1], dtype=np.int32)
413+
414+
azimuth = filemetadata("azimuth")
415+
azimuth["data"] = 0.0 * np.ones(time["data"].size, dtype=np.float32)
416+
417+
elevation = filemetadata("elevation")
418+
elevation["data"] = 90.0 * np.ones(time["data"].size, dtype=np.float32)
419+
420+
omega = float(ncobj.radar_operating_frequency.split()[0])
421+
frequency = filemetadata("frequency")
422+
frequency["data"] = np.array([omega / 1e9], dtype=np.float32)
423+
424+
prt_mode = filemetadata("prt_mode")
425+
prt_mode["data"] = np.array(["fixed"], dtype=str)
426+
427+
prt = filemetadata("prt")
428+
prt["data"] = np.full(
429+
time["data"].size, ncvars["InterPulsePeriod"][mode] * 1e-9, dtype=np.float32
430+
)
431+
432+
nyquist_velocity = filemetadata("nyquist_velocity")
433+
nyquist_velocity["data"] = np.full(
434+
time["data"].size, ncvars["NyquistVelocity"][mode], dtype=np.float32
435+
)
436+
437+
n_samples = filemetadata("n_samples")
438+
n_samples["data"] = np.full(
439+
time["data"].size, ncvars["NumSpectralAverages"][mode], dtype=np.float32
440+
)
441+
442+
# 4.6 radar_parameters sub-convention -> instrument_parameters dict
443+
# this section needed multiple changes and/or additions since the
444+
# radar instrument parameters were primarily located in the global
445+
# attributes
446+
# this section is likely still incomplete
447+
instrument_parameters = {
448+
"frequency": frequency,
449+
"prt_mode": prt_mode,
450+
"prt": prt,
451+
"nyquist_velocity": nyquist_velocity,
452+
"n_samples": n_samples,
453+
}
454+
455+
# 4.7 lidar_parameters sub-convention -> skip
456+
# 4.8 radar_calibration sub-convention -> skip
457+
458+
Radar_tmp = Radar(
459+
time,
460+
_range,
461+
fields,
462+
metadata,
463+
scan_type,
464+
latitude,
465+
longitude,
466+
altitude,
467+
sweep_number,
468+
sweep_mode,
469+
fixed_angle,
470+
sweep_start_ray_index,
471+
sweep_end_ray_index,
472+
azimuth,
473+
elevation,
474+
instrument_parameters=instrument_parameters,
475+
)
476+
if mode_to_extract is None:
477+
radar_out[mode_names[mode]] = Radar_tmp
478+
else:
479+
radar_out = Radar_tmp
480+
481+
# close NetCDF object
482+
ncobj.close()
483+
484+
return radar_out

0 commit comments

Comments
 (0)