Skip to content

aux_io.read_odim_h5: rstart assumed to be in km, but ODIM_H5 2.4 specifies metres #1869

Description

@joshua-wx

aux_io.read_odim_h5: rstart assumed to be in km, but ODIM_H5 2.4 specifies metres

Summary

The ODIM_H5 reader in pyart/aux_io/odim_h5.py hard-codes a km→m conversion when
deriving the range coordinate from the rstart attribute. This was correct for
ODIM_H5 ≤ 2.3, where rstart (range to the start of the first bin) was expressed in
kilometres. As of ODIM_H5 2.4, the model was aligned with the CfRadial2
conventions and switched to SI units, so rstart is now in metres.

For a 2.4 file with a non-zero rstart, Py-ART therefore multiplies an
already-metre value by 1000, placing the first gate ~1000× too far out and
shifting the entire range array radially. The error is silent (no exception, no
warning) and only surfaces on inspection of the geolocated data.

Affected code

pyart/aux_io/odim_h5.py, inside read_odim_h5, in the block that derives the
range from where/rstart + where/rscale (currently ~L266 and ~L270):

rscenter = 1e3 * rstart[0] + rscale[0] / 2
_range["data"] = np.arange(
    rscenter, rscenter + max_nbins * rscale[0], rscale[0], dtype="float32"
)
_range["meters_to_center_of_first_gate"] = rstart[0] * 1000.0
_range["meters_between_gates"] = float(rscale[0])

Both the 1e3 * factor and the * 1000.0 factor assume rstart is in km.
(rscale is unaffected — it has always been specified in metres, in every ODIM
version.)

Background: the ODIM 2.4 unit change

ODIM_H5 2.4 states that the new attributes and quantities follow the CfRadial2
conventions and that SI units are applied where applicable
([OPERA ODIM_H5 v2.4 spec](https://www.eumetnet.eu/wp-content/uploads/2021/07/ODIM_H5_v2.4.pdf)).
In practice this means rstart moved from km (≤ 2.3) to metres (2.4). This also
removes a long-standing inconsistency in the model, where rstart was in km while
the neighbouring rscale was in metres.

For precedent, xradar handles exactly this case by scaling rstart on the
declared version string
([xradar.io.backends.odim._get_range](https://docs.openradarscience.org/projects/xradar/en/stable/notebooks/ODIM_H5.html)):

def _get_range(where, odim_version=None):
    scale = 1000.0
    if odim_version == "ODIM_H5/V2_4":
        scale = 1.0
    ...
    range_start = where["rstart"] * scale

Impact

  • When it bites: files declaring ODIM_H5 2.4 and carrying a non-zero
    rstart. Because many radars report rstart = 0, and 1e3 * 0 == 0
    regardless of units, the bug is masked for a large fraction of files — which is
    probably why it hasn't surfaced yet. It will silently corrupt the subset of 2.4
    files that specify a non-zero range start.
  • Magnitude of the error: the first-gate center is offset by
    ~1000 × rstart metres (e.g. rstart = 125 m → first gate placed at ~125 km
    instead of ~125 m). Gate spacing (rscale) remains correct, so the whole ray is
    translated radially rather than stretched.
  • Downstream: any gridding, geolocation, or comparison against other datasets
    will be misregistered for affected files, with no error raised.

Proposed fix

(1) retire aux_io.odim_h5 reader

OR

(2)
Normalise rstart to metres once, then use the normalised value in both places.
The check below treats any rstart < 10 as a legacy kilometre value (a first-bin
start of ≥ 10 km is not physically meaningful, whereas a non-zero metre start is
typically ≥ ~100 m, so the two ranges don't overlap in practice). rstart = 0
maps to 0 either way, so the common case is untouched.

# Normalise rstart to metres.
#   ODIM_H5 <= 2.3 : rstart in km
#   ODIM_H5 2.4    : rstart in metres (CfRadial2 / SI alignment)
# A value-based check is used rather than the declared version because the
# ODIM version string is unreliable across operational vendors.
rstart_m = float(rstart[0])
if rstart_m < 10:  # legacy km value
    rstart_m *= 1e3

rscenter = rstart_m + rscale[0] / 2
_range["data"] = np.arange(
    rscenter, rscenter + max_nbins * rscale[0], rscale[0], dtype="float32"
)
_range["meters_to_center_of_first_gate"] = rstart_m
_range["meters_between_gates"] = float(rscale[0])

This preserves existing behaviour for all ≤ 2.3 files and for any file with
rstart = 0, while correctly handling 2.4 files with a non-zero start.

Alternative considered: switch on the declared version

Py-ART already parses the ODIM version string into metadata["odim_conventions"]
(from the root Conventions attribute, ~L172) and it is in scope where the range
is computed, so a version-based switch matching the xradar approach is available
with no extra parsing:

scale = 1.0 if metadata["odim_conventions"] == "ODIM_H5/V2_4" else 1e3
rstart_m = float(rstart[0]) * scale

Trade-off. The version string is the "correct" signal in principle, but in
practice a non-trivial fraction of operational ODIM files mislabel or omit their
version (or advertise ≤ 2.3 while using SI units, and vice versa). The value-based
heuristic is robust to that, at the cost of a small, physically-implausible
ambiguity window (0 < rstart < 10 interpreted as km). Happy to go with the
version switch instead — or a combination (version string as primary, magnitude
as a sanity fallback) — if maintainers prefer.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions