Skip to content

Commit f42b220

Browse files
authored
Merge pull request #131 from rcjackson/space_sep
Fixes for different formats for gate ID metadata attributes
2 parents 5ec1bb1 + f118455 commit f42b220

3 files changed

Lines changed: 105 additions & 12 deletions

File tree

cmac/cmac_ppi_quicklooks.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
generate_radar_name, generate_radar_time_begin)
1717

1818
from .config import get_plot_values, get_field_names
19+
from .gate_id import get_gate_id_categories, gate_id_has_category
1920

2021
plt.switch_backend('agg')
2122

@@ -190,13 +191,12 @@ def _range(key, default):
190191

191192
# Four panel plot of gate_id, velocity_texture, reflectivity, and
192193
# cross_correlation_ratio.
193-
cat_dict = {}
194+
gate_id_field = radar.fields['gate_id']
195+
cat_dict = get_gate_id_categories(gate_id_field)
194196
print('##')
195197
print('## Keys for each gate id are as follows:')
196-
for i, pair_str in enumerate(radar.fields['gate_id']['notes'].split(',')):
197-
pair_str = pair_str.split(':')[1].strip()
198-
print('## ', str(pair_str))
199-
cat_dict.update({pair_str: i})
198+
for label in sorted(cat_dict, key=cat_dict.get):
199+
print('## ', str(label))
200200
sorted_cats = sorted(cat_dict.items(), key=operator.itemgetter(1))
201201
cat_colors = dict(cat_colors_cfg)
202202
lab_colors = [cat_colors[kitty[0]] for kitty in sorted_cats]
@@ -218,7 +218,8 @@ def _range(key, default):
218218
colors='k')
219219

220220
cbax = ax[0, 0]
221-
if 'ground_clutter' in radar.fields.keys() or 'terrain_blockage' in radar.fields['gate_id']['notes']:
221+
if ('ground_clutter' in radar.fields.keys()
222+
or gate_id_has_category(gate_id_field, 'terrain_blockage')):
222223
tick_locs = np.linspace(
223224
0, len(sorted_cats) - 1, len(sorted_cats)) + 0.5
224225
else:

cmac/cmac_rhi_quicklooks.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
generate_radar_name, generate_radar_time_begin)
1616

1717
from .config import get_plot_values, get_field_names
18+
from .gate_id import get_gate_id_categories, gate_id_has_category
1819

1920
plt.switch_backend('agg')
2021

@@ -106,13 +107,12 @@ def _range(key, default):
106107

107108
# Four panel plot of gate_id, velocity_texture, reflectivity, and
108109
# cross_correlation_ratio.
109-
cat_dict = {}
110+
gate_id_field = radar.fields['gate_id']
111+
cat_dict = get_gate_id_categories(gate_id_field)
110112
print('##')
111113
print('## Keys for each gate id are as follows:')
112-
for i, pair_str in enumerate(radar.fields['gate_id']['notes'].split(',')):
113-
pair_str = pair_str.split(':')[1].strip()
114-
print('## ', str(pair_str))
115-
cat_dict.update({pair_str: i})
114+
for label in sorted(cat_dict, key=cat_dict.get):
115+
print('## ', str(label))
116116
sorted_cats = sorted(cat_dict.items(), key=operator.itemgetter(1))
117117

118118
cat_colors = dict(cat_colors_cfg)
@@ -126,7 +126,8 @@ def _range(key, default):
126126
cmap=cmap, vmin=0, vmax=6)
127127

128128
cbax = ax[0, 0]
129-
if 'ground_clutter' in radar.fields.keys() or 'terrain_blockage' in radar.fields['gate_id']['notes']:
129+
if ('ground_clutter' in radar.fields.keys()
130+
or gate_id_has_category(gate_id_field, 'terrain_blockage')):
130131
tick_locs = np.linspace(
131132
0, len(sorted_cats) - 1, len(sorted_cats)) + 0.5
132133
else:

cmac/gate_id.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""
2+
Helpers for interpreting the category metadata attached to a ``gate_id``
3+
(hydrometeor ID) radar field.
4+
5+
CMAC's own gate id fields document their categories with a ``notes``
6+
attribute, which in practice shows up in a few different shapes:
7+
8+
- ``"0: multi_trip, 1: rain, 2: snow"`` -- comma separated ``"index: label"``
9+
pairs, colon separated.
10+
- ``"0 multi_trip, 1 rain, 2 snow"`` -- comma separated ``"index label"``
11+
pairs, whitespace separated.
12+
- ``"multi_trip rain snow melting no_scatter clutter terrain_blockage"`` --
13+
a plain, unindexed list of labels in order, with no indices or commas at
14+
all.
15+
16+
Fields that follow the CF conventions instead (or radar objects re-read
17+
from a file that converted ``notes`` on save) may document the same
18+
information with a ``flag_meanings`` attribute and a parallel
19+
``flag_values`` attribute (the matching integer codes). ``flag_meanings``
20+
is generally comma separated, though a plain whitespace separated string
21+
is also accepted.
22+
"""
23+
24+
import re
25+
26+
_PAIR_SEP_RE = re.compile(r'[:\s]+')
27+
28+
29+
def _label_from_pair(pair_str):
30+
"""Extract the category label from a single ``"index: label"`` pair,
31+
where the index/label separator is a colon, whitespace, or both."""
32+
parts = _PAIR_SEP_RE.split(pair_str.strip(), maxsplit=1)
33+
return parts[-1].strip()
34+
35+
36+
def _split_list(text):
37+
"""Split a comma or whitespace separated list of labels into its
38+
individual, stripped entries."""
39+
if ',' in text:
40+
parts = text.split(',')
41+
else:
42+
parts = text.split()
43+
return [part.strip() for part in parts if part.strip()]
44+
45+
46+
def _labels_from_notes(notes):
47+
"""Return the ordered list of category labels encoded in a ``notes``
48+
attribute, handling both indexed ``"index: label"``/``"index label"``
49+
pairs and a plain, unindexed list of labels."""
50+
pieces = [p.strip() for p in notes.split(',') if p.strip()]
51+
if len(pieces) > 1 or (pieces and ':' in pieces[0]):
52+
return [_label_from_pair(piece) for piece in pieces]
53+
return _split_list(notes)
54+
55+
56+
def get_gate_id_categories(gate_id_field):
57+
"""
58+
Return a dict mapping each gate id category label to its integer code.
59+
60+
Parameters
61+
----------
62+
gate_id_field : dict
63+
A Py-ART field dictionary, e.g. ``radar.fields['gate_id']``.
64+
65+
"""
66+
if 'notes' in gate_id_field:
67+
labels = _labels_from_notes(gate_id_field['notes'])
68+
return {label: i for i, label in enumerate(labels)}
69+
70+
if 'flag_meanings' in gate_id_field and 'flag_values' in gate_id_field:
71+
labels = _split_list(gate_id_field['flag_meanings'])
72+
values = gate_id_field['flag_values']
73+
return {label: int(value) for label, value in zip(labels, values)}
74+
75+
raise KeyError(
76+
"The 'gate_id' field must have either a 'notes' attribute or "
77+
"'flag_values'/'flag_meanings' attributes describing its "
78+
"categories.")
79+
80+
81+
def gate_id_has_category(gate_id_field, category):
82+
"""
83+
Return True if ``category`` is one of the documented categories of a
84+
``gate_id`` field, whether documented via ``notes`` or via
85+
``flag_meanings``.
86+
"""
87+
if 'notes' in gate_id_field:
88+
return category in _labels_from_notes(gate_id_field['notes'])
89+
if 'flag_meanings' in gate_id_field:
90+
return category in _split_list(gate_id_field['flag_meanings'])
91+
return False

0 commit comments

Comments
 (0)