|
| 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