diff --git a/cmac/cmac_ppi_quicklooks.py b/cmac/cmac_ppi_quicklooks.py index 6855b05..30f99d3 100644 --- a/cmac/cmac_ppi_quicklooks.py +++ b/cmac/cmac_ppi_quicklooks.py @@ -16,6 +16,7 @@ generate_radar_name, generate_radar_time_begin) from .config import get_plot_values, get_field_names +from .gate_id import get_gate_id_categories, gate_id_has_category plt.switch_backend('agg') @@ -190,13 +191,12 @@ def _range(key, default): # Four panel plot of gate_id, velocity_texture, reflectivity, and # cross_correlation_ratio. - cat_dict = {} + gate_id_field = radar.fields['gate_id'] + cat_dict = get_gate_id_categories(gate_id_field) print('##') print('## Keys for each gate id are as follows:') - for i, pair_str in enumerate(radar.fields['gate_id']['notes'].split(',')): - pair_str = pair_str.split(':')[1].strip() - print('## ', str(pair_str)) - cat_dict.update({pair_str: i}) + for label in sorted(cat_dict, key=cat_dict.get): + print('## ', str(label)) sorted_cats = sorted(cat_dict.items(), key=operator.itemgetter(1)) cat_colors = dict(cat_colors_cfg) lab_colors = [cat_colors[kitty[0]] for kitty in sorted_cats] @@ -218,7 +218,8 @@ def _range(key, default): colors='k') cbax = ax[0, 0] - if 'ground_clutter' in radar.fields.keys() or 'terrain_blockage' in radar.fields['gate_id']['notes']: + if ('ground_clutter' in radar.fields.keys() + or gate_id_has_category(gate_id_field, 'terrain_blockage')): tick_locs = np.linspace( 0, len(sorted_cats) - 1, len(sorted_cats)) + 0.5 else: diff --git a/cmac/cmac_rhi_quicklooks.py b/cmac/cmac_rhi_quicklooks.py index 73051e7..fcbb5f7 100644 --- a/cmac/cmac_rhi_quicklooks.py +++ b/cmac/cmac_rhi_quicklooks.py @@ -15,6 +15,7 @@ generate_radar_name, generate_radar_time_begin) from .config import get_plot_values, get_field_names +from .gate_id import get_gate_id_categories, gate_id_has_category plt.switch_backend('agg') @@ -106,13 +107,12 @@ def _range(key, default): # Four panel plot of gate_id, velocity_texture, reflectivity, and # cross_correlation_ratio. - cat_dict = {} + gate_id_field = radar.fields['gate_id'] + cat_dict = get_gate_id_categories(gate_id_field) print('##') print('## Keys for each gate id are as follows:') - for i, pair_str in enumerate(radar.fields['gate_id']['notes'].split(',')): - pair_str = pair_str.split(':')[1].strip() - print('## ', str(pair_str)) - cat_dict.update({pair_str: i}) + for label in sorted(cat_dict, key=cat_dict.get): + print('## ', str(label)) sorted_cats = sorted(cat_dict.items(), key=operator.itemgetter(1)) cat_colors = dict(cat_colors_cfg) @@ -126,7 +126,8 @@ def _range(key, default): cmap=cmap, vmin=0, vmax=6) cbax = ax[0, 0] - if 'ground_clutter' in radar.fields.keys() or 'terrain_blockage' in radar.fields['gate_id']['notes']: + if ('ground_clutter' in radar.fields.keys() + or gate_id_has_category(gate_id_field, 'terrain_blockage')): tick_locs = np.linspace( 0, len(sorted_cats) - 1, len(sorted_cats)) + 0.5 else: diff --git a/cmac/gate_id.py b/cmac/gate_id.py new file mode 100644 index 0000000..fe11f4d --- /dev/null +++ b/cmac/gate_id.py @@ -0,0 +1,91 @@ +""" +Helpers for interpreting the category metadata attached to a ``gate_id`` +(hydrometeor ID) radar field. + +CMAC's own gate id fields document their categories with a ``notes`` +attribute, which in practice shows up in a few different shapes: + +- ``"0: multi_trip, 1: rain, 2: snow"`` -- comma separated ``"index: label"`` + pairs, colon separated. +- ``"0 multi_trip, 1 rain, 2 snow"`` -- comma separated ``"index label"`` + pairs, whitespace separated. +- ``"multi_trip rain snow melting no_scatter clutter terrain_blockage"`` -- + a plain, unindexed list of labels in order, with no indices or commas at + all. + +Fields that follow the CF conventions instead (or radar objects re-read +from a file that converted ``notes`` on save) may document the same +information with a ``flag_meanings`` attribute and a parallel +``flag_values`` attribute (the matching integer codes). ``flag_meanings`` +is generally comma separated, though a plain whitespace separated string +is also accepted. +""" + +import re + +_PAIR_SEP_RE = re.compile(r'[:\s]+') + + +def _label_from_pair(pair_str): + """Extract the category label from a single ``"index: label"`` pair, + where the index/label separator is a colon, whitespace, or both.""" + parts = _PAIR_SEP_RE.split(pair_str.strip(), maxsplit=1) + return parts[-1].strip() + + +def _split_list(text): + """Split a comma or whitespace separated list of labels into its + individual, stripped entries.""" + if ',' in text: + parts = text.split(',') + else: + parts = text.split() + return [part.strip() for part in parts if part.strip()] + + +def _labels_from_notes(notes): + """Return the ordered list of category labels encoded in a ``notes`` + attribute, handling both indexed ``"index: label"``/``"index label"`` + pairs and a plain, unindexed list of labels.""" + pieces = [p.strip() for p in notes.split(',') if p.strip()] + if len(pieces) > 1 or (pieces and ':' in pieces[0]): + return [_label_from_pair(piece) for piece in pieces] + return _split_list(notes) + + +def get_gate_id_categories(gate_id_field): + """ + Return a dict mapping each gate id category label to its integer code. + + Parameters + ---------- + gate_id_field : dict + A Py-ART field dictionary, e.g. ``radar.fields['gate_id']``. + + """ + if 'notes' in gate_id_field: + labels = _labels_from_notes(gate_id_field['notes']) + return {label: i for i, label in enumerate(labels)} + + if 'flag_meanings' in gate_id_field and 'flag_values' in gate_id_field: + labels = _split_list(gate_id_field['flag_meanings']) + values = gate_id_field['flag_values'] + return {label: int(value) for label, value in zip(labels, values)} + + raise KeyError( + "The 'gate_id' field must have either a 'notes' attribute or " + "'flag_values'/'flag_meanings' attributes describing its " + "categories.") + + +def gate_id_has_category(gate_id_field, category): + """ + Return True if ``category`` is one of the documented categories of a + ``gate_id`` field, whether documented via ``notes`` or via + ``flag_meanings``. + """ + if 'notes' in gate_id_field: + return category in _labels_from_notes(gate_id_field['notes']) + if 'flag_meanings' in gate_id_field: + return category in _split_list(gate_id_field['flag_meanings']) + return False