Skip to content

Commit fc43e60

Browse files
authored
ENH: add message 1 decoding to nexrad level2 reader (#267)
* ENH: add message 1 decoding to nexrad level2 reader * Add tests for message type 1 reader part * add history.md entry * proper doppler scaling
1 parent f5f1f6f commit fc43e60

File tree

4 files changed

+350
-70
lines changed

4 files changed

+350
-70
lines changed

docs/history.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* FIX: DataTree reader now works with sweeps containing different variables ({pull}`252`) by [@egouden](https://github.com/egouden).
1212
* FIX: Correct retrieval of intermediate records in nexrad level2 reader ({issue}`259`) ({pull}`261`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
1313
* FIX: Test for magic number BZhX1AY&SY (where X is any number between 0..9) when retrieving BZ2 record indices in nexrad level2 reader ({issue}`264`) ({pull}`266`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
14+
* ENH: Add message type 1 decoding to nexrad level 2 reader ({issue}`256`) ({pull}`267`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
1415

1516
## 0.8.0 (2024-11-04)
1617

tests/conftest.py

+15
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,21 @@ def nexradlevel2_file():
7777
return DATASETS.fetch("KATX20130717_195021_V06")
7878

7979

80+
@pytest.fixture(scope="session")
81+
def nexradlevel2_msg1_file(tmp_path_factory):
82+
fnamei = DATASETS.fetch("KLIX20050828_180149.gz")
83+
fnameo = os.path.join(
84+
tmp_path_factory.mktemp("data"), f"{os.path.basename(fnamei)[:-3]}_gz"
85+
)
86+
import gzip
87+
import shutil
88+
89+
with gzip.open(fnamei) as fin:
90+
with open(fnameo, "wb") as fout:
91+
shutil.copyfileobj(fin, fout)
92+
return fnameo
93+
94+
8095
@pytest.fixture(scope="session")
8196
def nexradlevel2_gzfile(tmp_path_factory):
8297
fnamei = DATASETS.fetch("KLBB20160601_150025_V06.gz")

tests/io/test_nexrad_level2.py

+172
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,120 @@ def test_open_nexradlevel2_file(nexradlevel2_files):
424424
assert len(head) == msg_31_header_length[i]
425425

426426

427+
def test_open_nexradlevel2_msg1_file(nexradlevel2_msg1_file):
428+
with NEXRADLevel2File(nexradlevel2_msg1_file) as fh:
429+
430+
# volume header
431+
assert fh.volume_header["tape"] == b"AR2V0001."
432+
assert fh.volume_header["extension"] == b"201"
433+
assert fh.volume_header["date"] == 3761373184
434+
assert fh.volume_header["time"] == 3362708995
435+
assert fh.volume_header["icao"] == b"KLIX"
436+
437+
# meta_header 15
438+
assert len(fh.meta_header["msg_15"]) == 62
439+
assert fh.meta_header["msg_15"][0]["size"] == 1208
440+
assert fh.meta_header["msg_15"][0]["channels"] == 0
441+
assert fh.meta_header["msg_15"][0]["type"] == 15
442+
assert fh.meta_header["msg_15"][0]["seq_id"] == 819
443+
assert fh.meta_header["msg_15"][0]["date"] == 13024
444+
assert fh.meta_header["msg_15"][0]["ms"] == 51522855
445+
assert fh.meta_header["msg_15"][0]["segments"] == 14
446+
assert fh.meta_header["msg_15"][0]["seg_num"] == 1
447+
assert fh.meta_header["msg_15"][0]["record_number"] == 0
448+
# meta_header 13
449+
assert len(fh.meta_header["msg_13"]) == 48
450+
assert fh.meta_header["msg_13"][0]["size"] == 1208
451+
assert fh.meta_header["msg_13"][0]["channels"] == 0
452+
assert fh.meta_header["msg_13"][0]["type"] == 13
453+
assert fh.meta_header["msg_13"][0]["seq_id"] == 0
454+
assert fh.meta_header["msg_13"][0]["date"] == 13023
455+
assert fh.meta_header["msg_13"][0]["ms"] == 43397314
456+
assert fh.meta_header["msg_13"][0]["segments"] == 14
457+
assert fh.meta_header["msg_13"][0]["seg_num"] == 1
458+
assert fh.meta_header["msg_13"][0]["record_number"] == 62
459+
# meta header 18
460+
assert len(fh.meta_header["msg_18"]) == 4
461+
assert fh.meta_header["msg_18"][0]["size"] == 1208
462+
assert fh.meta_header["msg_18"][0]["channels"] == 0
463+
assert fh.meta_header["msg_18"][0]["type"] == 18
464+
assert fh.meta_header["msg_18"][0]["seq_id"] == 0
465+
assert fh.meta_header["msg_18"][0]["date"] == 0
466+
assert fh.meta_header["msg_18"][0]["ms"] == 0
467+
assert fh.meta_header["msg_18"][0]["segments"] == 4
468+
assert fh.meta_header["msg_18"][0]["seg_num"] == 1
469+
assert fh.meta_header["msg_18"][0]["record_number"] == 110
470+
# meta header 3
471+
assert len(fh.meta_header["msg_3"]) == 1
472+
assert fh.meta_header["msg_3"][0]["size"] == 528
473+
assert fh.meta_header["msg_3"][0]["channels"] == 0
474+
assert fh.meta_header["msg_3"][0]["type"] == 3
475+
assert fh.meta_header["msg_3"][0]["seq_id"] == 5459
476+
assert fh.meta_header["msg_3"][0]["date"] == 13024
477+
assert fh.meta_header["msg_3"][0]["ms"] == 61897431
478+
assert fh.meta_header["msg_3"][0]["segments"] == 1
479+
assert fh.meta_header["msg_3"][0]["seg_num"] == 1
480+
assert fh.meta_header["msg_3"][0]["record_number"] == 114
481+
# meta header 5
482+
assert len(fh.meta_header["msg_5"]) == 1
483+
assert fh.meta_header["msg_5"][0]["size"] == 1208
484+
assert fh.meta_header["msg_5"][0]["channels"] == 0
485+
assert fh.meta_header["msg_5"][0]["type"] == 5
486+
assert fh.meta_header["msg_5"][0]["seq_id"] == 0
487+
assert fh.meta_header["msg_5"][0]["date"] == 0
488+
assert fh.meta_header["msg_5"][0]["ms"] == 0
489+
assert fh.meta_header["msg_5"][0]["segments"] == 1
490+
assert fh.meta_header["msg_5"][0]["seg_num"] == 1
491+
assert fh.meta_header["msg_5"][0]["record_number"] == 115
492+
assert fh.msg_5 == OrderedDict(
493+
[
494+
("message_size", 0),
495+
("pattern_type", 0),
496+
("pattern_number", 0),
497+
("number_elevation_cuts", 0),
498+
("clutter_map_group_number", 0),
499+
("doppler_velocity_resolution", 0),
500+
("pulse_width", 0),
501+
("elevation_data", []),
502+
]
503+
)
504+
505+
# meta header 2
506+
assert len(fh.meta_header["msg_2"]) == 1
507+
assert fh.meta_header["msg_2"][0]["size"] == 48
508+
assert fh.meta_header["msg_2"][0]["channels"] == 0
509+
assert fh.meta_header["msg_2"][0]["type"] == 2
510+
assert fh.meta_header["msg_2"][0]["seq_id"] == 29176
511+
assert fh.meta_header["msg_2"][0]["date"] == 13024
512+
assert fh.meta_header["msg_2"][0]["ms"] == 64889226
513+
assert fh.meta_header["msg_2"][0]["segments"] == 1
514+
assert fh.meta_header["msg_2"][0]["seg_num"] == 1
515+
assert fh.meta_header["msg_2"][0]["record_number"] == 116
516+
517+
# data header
518+
assert len(fh.data_header) == 5856
519+
msg_31_header_length = [
520+
367,
521+
367,
522+
367,
523+
367,
524+
367,
525+
367,
526+
367,
527+
367,
528+
366,
529+
367,
530+
366,
531+
366,
532+
365,
533+
364,
534+
363,
535+
362,
536+
]
537+
for i, head in enumerate(fh.msg_31_header):
538+
assert len(head) == msg_31_header_length[i]
539+
540+
427541
def test_open_nexradlevel2_datatree(nexradlevel2_file):
428542
# Define kwargs to pass into the function
429543
kwargs = {
@@ -485,6 +599,64 @@ def test_open_nexradlevel2_datatree(nexradlevel2_file):
485599
assert dtree.attrs["scan_name"] == "VCP-11"
486600

487601

602+
def test_open_nexradlevel2_msg1_datatree(nexradlevel2_msg1_file):
603+
# Define kwargs to pass into the function
604+
kwargs = {
605+
"sweep": [0, 1, 2, 5, 7], # Test with specific sweeps
606+
"first_dim": "auto",
607+
"reindex_angle": {
608+
"start_angle": 0.0,
609+
"stop_angle": 360.0,
610+
"angle_res": 1.0,
611+
"direction": 1, # Set a valid direction within reindex_angle
612+
},
613+
"fix_second_angle": True,
614+
"site_coords": True,
615+
}
616+
617+
# Call the function with an actual NEXRAD Level 2 file
618+
dtree = open_nexradlevel2_datatree(nexradlevel2_msg1_file, **kwargs)
619+
620+
# Assertions
621+
assert isinstance(dtree, DataTree), "Expected a DataTree instance"
622+
assert "/" in dtree.subtree, "Root group should be present in the DataTree"
623+
assert (
624+
"/radar_parameters" in dtree.subtree
625+
), "Radar parameters group should be in the DataTree"
626+
assert (
627+
"/georeferencing_correction" in dtree.subtree
628+
), "Georeferencing correction group should be in the DataTree"
629+
assert (
630+
"/radar_calibration" in dtree.subtree
631+
), "Radar calibration group should be in the DataTree"
632+
633+
# Check if at least one sweep group is attached (e.g., "/sweep_0")
634+
sweep_groups = [key for key in dtree.match("sweep_*")]
635+
assert len(sweep_groups) == 5, "Expected at least one sweep group in the DataTree"
636+
637+
# Verify a sample variable in one of the sweep groups (adjust as needed based on expected variables)
638+
sample_sweep = sweep_groups[0]
639+
assert len(dtree[sample_sweep].data_vars) == 6
640+
assert (
641+
"DBZH" in dtree[sample_sweep].data_vars
642+
), f"DBZH should be a data variable in {sample_sweep}"
643+
assert dtree[sample_sweep]["DBZH"].shape == (360, 460)
644+
# Validate coordinates are attached correctly
645+
assert (
646+
"latitude" in dtree[sample_sweep]
647+
), "Latitude should be attached to the root dataset"
648+
assert (
649+
"longitude" in dtree[sample_sweep]
650+
), "Longitude should be attached to the root dataset"
651+
assert (
652+
"altitude" in dtree[sample_sweep]
653+
), "Altitude should be attached to the root dataset"
654+
655+
assert len(dtree.attrs) == 10
656+
assert dtree.attrs["instrument_name"] == "KLIX"
657+
assert dtree.attrs["scan_name"] == "VCP-0"
658+
659+
488660
@pytest.mark.parametrize(
489661
"sweeps_input, expected_sweeps, should_raise",
490662
[

0 commit comments

Comments
 (0)