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
aux_io.read_odim_h5:rstartassumed to be in km, but ODIM_H5 2.4 specifies metresSummary
The ODIM_H5 reader in
pyart/aux_io/odim_h5.pyhard-codes a km→m conversion whenderiving the range coordinate from the
rstartattribute. This was correct forODIM_H5 ≤ 2.3, where
rstart(range to the start of the first bin) was expressed inkilometres. As of ODIM_H5 2.4, the model was aligned with the CfRadial2
conventions and switched to SI units, so
rstartis now in metres.For a 2.4 file with a non-zero
rstart, Py-ART therefore multiplies analready-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, insideread_odim_h5, in the block that derives therange from
where/rstart+where/rscale(currently ~L266 and ~L270):Both the
1e3 *factor and the* 1000.0factor assumerstartis in km.(
rscaleis unaffected — it has always been specified in metres, in every ODIMversion.)
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
rstartmoved from km (≤ 2.3) to metres (2.4). This alsoremoves a long-standing inconsistency in the model, where
rstartwas in km whilethe neighbouring
rscalewas in metres.For precedent,
xradarhandles exactly this case by scalingrstarton thedeclared version string
(
[xradar.io.backends.odim._get_range](https://docs.openradarscience.org/projects/xradar/en/stable/notebooks/ODIM_H5.html)):Impact
rstart. Because many radars reportrstart = 0, and1e3 * 0 == 0regardless 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.
~1000 × rstartmetres (e.g.rstart = 125 m→ first gate placed at ~125 kminstead of ~125 m). Gate spacing (
rscale) remains correct, so the whole ray istranslated radially rather than stretched.
will be misregistered for affected files, with no error raised.
Proposed fix
(1) retire aux_io.odim_h5 reader
OR
(2)
Normalise
rstartto metres once, then use the normalised value in both places.The check below treats any
rstart < 10as a legacy kilometre value (a first-binstart 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 = 0maps to
0either way, so the common case is untouched.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
Conventionsattribute, ~L172) and it is in scope where the rangeis computed, so a version-based switch matching the
xradarapproach is availablewith no extra parsing:
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 < 10interpreted as km). Happy to go with theversion switch instead — or a combination (version string as primary, magnitude
as a sanity fallback) — if maintainers prefer.
References
xradarODIM backend range handling (version-basedrstartscaling): https://docs.openradarscience.org/projects/xradar/en/stable/notebooks/ODIM_H5.htmlpyart/aux_io/odim_h5.py→read_odim_h5