diff --git a/src/metpy/io/nexrad.py b/src/metpy/io/nexrad.py index ad4e649f293..91ff2cc8f06 100644 --- a/src/metpy/io/nexrad.py +++ b/src/metpy/io/nexrad.py @@ -267,8 +267,8 @@ def _read_data(self): # Check if we have any message segments still in the buffer if self._msg_buf: - log.warning('Remaining buffered messages segments for message type(s): %s', - ' '.join(map(str, self._msg_buf))) + log.warning('Remaining buffered message segments for message type(s): %s', + ' '.join(f'{typ} ({len(rem)})' for typ, rem in self._msg_buf.items())) del self._msg_buf @@ -305,14 +305,14 @@ def _decode_msg1(self, msg_hdr): hdr.surv_gate_width, hdr.surv_num_gates, 2.0, 66.0))) - if hdr.vel_offset: + if hdr.doppler_num_gates and hdr.vel_offset: read_info.append((hdr.vel_offset, self.msg1_data_hdr('VEL', hdr.doppler_first_gate, hdr.doppler_gate_width, hdr.doppler_num_gates, 1. / hdr.dop_res, 129.0))) - if hdr.sw_offset: + if hdr.doppler_num_gates and hdr.sw_offset: read_info.append((hdr.sw_offset, self.msg1_data_hdr('SW', hdr.doppler_first_gate, hdr.doppler_gate_width, @@ -452,14 +452,23 @@ def _decode_msg5(self, msg_hdr): def _decode_msg13(self, msg_hdr): data = self._buffer_segment(msg_hdr) if data: - date, time, num_el, *data = Struct('>{:d}h'.format(len(data) // 2)).unpack(data) - self.clutter_filter_bypass_map = {'datetime': nexrad_to_datetime(date, time), - 'data': []} + data = Struct('>{:d}h'.format(len(data) // 2)).unpack(data) + # Legacy format doesn't have date/time and has fewer azimuths + if data[0] <= 5: + num_el = data[0] + dt = None + num_az = 256 + offset = 1 + else: + date, time, num_el = data[:3] + # time is in "minutes since midnight", need to pass as ms since midnight + dt = nexrad_to_datetime(date, 60 * 1000 * time) + num_az = 360 + offset = 3 - num_az = 360 + self.clutter_filter_bypass_map = {'datetime': dt, 'data': []} chunk_size = 32 bit_conv = Bits(16) - offset = 0 for e in range(num_el): seg_num = data[offset] if seg_num != (e + 1): @@ -487,7 +496,14 @@ def _decode_msg15(self, msg_hdr): data = self._buffer_segment(msg_hdr) if data: date, time, num_el, *data = Struct('>{:d}h'.format(len(data) // 2)).unpack(data) - self.clutter_filter_map = {'datetime': nexrad_to_datetime(date, time), 'data': []} + if num_el == 0: + log.info('Message 15 num_el is 0--likely legacy clutter filter notch width. ' + 'Skipping...') + return + + # time is in "minutes since midnight", need to pass as ms since midnight + self.clutter_filter_map = {'datetime': nexrad_to_datetime(date, 60 * 1000 * time), + 'data': []} offset = 0 for _ in range(num_el): @@ -507,14 +523,18 @@ def _decode_msg18(self, msg_hdr): # buffer the segments until we have the whole thing. The data # will be returned concatenated when this is the case data = self._buffer_segment(msg_hdr) - if data: + + # Legacy versions don't even document this: + if data and self.vol_hdr.version[:8] not in (b'ARCHIVE2', b'AR2V0001'): from ._nexrad_msgs.msg18 import descriptions, fields self.rda_adaptation_desc = descriptions # Can't use NamedStruct because we have more than 255 items--this # is a CPython limit for arguments. msg_fmt = DictStruct(fields, '>') - self.rda = msg_fmt.unpack(data) + + # Be extra paranoid about passing too much data in case of legacy files + self.rda = msg_fmt.unpack(data[:msg_fmt.size]) for num in (11, 21, 31, 32, 300, 301): attr = f'VCPAT{num}' dat = self.rda[attr] diff --git a/src/metpy/static-data-manifest.txt b/src/metpy/static-data-manifest.txt index 860015767c5..50e6c61bafb 100644 --- a/src/metpy/static-data-manifest.txt +++ b/src/metpy/static-data-manifest.txt @@ -8,6 +8,7 @@ KICX_20170712_1458 94bd4f795832f056f7489a5562acf76de9a9cab1694549562cc3154abb225 KTLX19990503_235621.gz 7a097251bb7a15dbcdec75812812e41a86c5eb9850f55c3d91d120c2c61e046e KTLX20130520_201643_V06.gz 772e01b154a5c966982a6d0aa2fc78bc64f08a9b77165b74dc02d7aa5aa69275 KTLX20150530_000802_V06.bz2 d78689afc525c853dec8ccab4a4eccc2daacef5c7df198a35a3a791016e993b0 +KVWX_20050626_221551.gz 78f98f3c3660627abcb7ba073b1399af45b2b61b30ae7d64fa726fc6acf72d9c Level2_FOP1_20191223_003655.ar2v 4a086f6190c0d324612922e9fcb46262c3a1d825810caa8a11aca3823db17ae3 Level2_KDDC_20200823_204121.ar2v 17c9011be6155f454b4710f8d36ea8974bc39472016a5b663d8718fb87425a4e Level2_KFTG_20150430_1419.ar2v 77c3355c8a503561eb3cddc3854337e640d983a4acdfc27bdfbab60c0b18cfc1 diff --git a/staticdata/KVWX_20050626_221551.gz b/staticdata/KVWX_20050626_221551.gz new file mode 100644 index 00000000000..5360792ee78 Binary files /dev/null and b/staticdata/KVWX_20050626_221551.gz differ diff --git a/tests/io/test_nexrad.py b/tests/io/test_nexrad.py index 078d556132b..713763e5b0f 100644 --- a/tests/io/test_nexrad.py +++ b/tests/io/test_nexrad.py @@ -34,7 +34,8 @@ ('TDAL20191021021543V08.raw.gz', datetime(2019, 10, 21, 2, 15, 43), 10, 1, 3, 0), ('Level2_FOP1_20191223_003655.ar2v', datetime(2019, 12, 23, 0, 36, 55, 649000), - 16, 5, 7, 0)] + 16, 5, 7, 0), + ('KVWX_20050626_221551.gz', datetime(2005, 6, 26, 22, 15, 51), 11, 1, 3, 23)] # ids here fixes how things are presented in pycharm @@ -95,7 +96,7 @@ def test_msg15(): f = Level2File(get_test_data('KTLX20130520_201643_V06.gz', as_file_obj=False)) data = f.clutter_filter_map['data'] assert isinstance(data[0][0], list) - assert f.clutter_filter_map['datetime'] == datetime(2013, 5, 19, 0, 0, 0, 315000) + assert f.clutter_filter_map['datetime'] == datetime(2013, 5, 19, 5, 15, 0, 0) def test_single_chunk(caplog):