Description
Passing an already-constructed BaseCoord to get_coord_manager (and therefore to dc.Patch) does not preserve it. Any coord that is not a CoordRange is dumped back to a dict and re-inferred through get_coord, which snaps near-evenly-sampled values onto a regular range. Because that inference is tolerant, the result is not merely a different coordinate class — the sample values themselves are silently rewritten.
The re-parse is deliberate and documented in _get_coord (dascore/core/coordmanager.py, ~line 1225): array coords can be left non-canonical by slicing, so an evenly spaced subset should collapse back to a CoordRange. That intent is reasonable. The problem is that the canonicalization is implemented with get_coord, whose evenness test is all_diffs_close_enough (dascore/utils/misc.py:586):
return np.allclose(diffs, med, rtol=0.001)
So any coordinate whose spacing varies by less than 0.1% is collapsed, and its values are replaced by an idealized ramp. Canonicalization that changes data is lossy; it should be reserved for the exactly-even case where it is a pure representation change. The comment on that rtol already notes it "is a bit arbitrary."
This is the mechanism behind the caveat in #895: a reader can build an exact coordinate with get_exact_coord and still have the coord manager convert it back to a CoordRange, so exactness cannot be guaranteed at the reader layer. There it costs the real per-sample jitter in a Febus acquisition-window coordinate, but nothing about the behavior is Febus-specific — it applies to any coordinate carrying small, physically meaningful irregularity: GPS-derived positions, per-sample timing jitter, measured channel spacing.
Example
import numpy as np
import dascore as dc
from dascore.core.coords import CoordMonotonicArray
vals = np.array([0.0, 1.0, 2.0005, 3.0015, 4.002])
coord = CoordMonotonicArray(values=vals)
coord.values
# array([0. , 1. , 2.0005, 3.0015, 4.002 ]) <- exact, as constructed
cm = dc.get_coord_manager({"distance": coord}, dims=("distance",))
cm.coord_map["distance"].values
# array([0. , 1.0005, 2.001 , 3.0015, 4.002 ]) <- values 1 and 2 rewritten
Same through the public Patch constructor:
patch = dc.Patch(
data=np.zeros((5, 2)),
coords={"distance": coord, "time": dc.to_datetime64(np.arange(2.0))},
dims=("distance", "time"),
)
patch.get_coord("distance").values
# array([0. , 1.0005, 2.001 , 3.0015, 4.002 ])
Maximum absolute error here is 5e-4 on a nominal spacing of 1.0. There is no warning, and the coordinate reports itself as an evenly sampled CoordRange afterwards, so downstream code has no way to tell the spacing was invented.
Expected behavior
Constructing a coordinate explicitly and handing it to a coord manager should never change its values. Canonicalizing an array coord to a CoordRange is fine when the range reproduces the original values exactly — that is a representation change. When it does not, the original coordinate should be kept.
Concretely, cm.coord_map["distance"].values should equal vals in the example above, while an exactly evenly spaced CoordMonotonicArray should still collapse to a CoordRange as it does today.
Worth deciding separately: whether get_coord(data=...) should keep snapping this aggressively when inferring from a raw array. That path is at least documented as tolerant inference, so it is defensible; silently discarding a caller's explicit, exact coordinate is not.
Versions
- OS: Linux 6.8.0-137-generic (Ubuntu, glibc 2.39)
- DASCore Version: 0.1.21.dev8+g32cce2e5b (
dev)
- Python Version: 3.13.7
Description
Passing an already-constructed
BaseCoordtoget_coord_manager(and therefore todc.Patch) does not preserve it. Any coord that is not aCoordRangeis dumped back to a dict and re-inferred throughget_coord, which snaps near-evenly-sampled values onto a regular range. Because that inference is tolerant, the result is not merely a different coordinate class — the sample values themselves are silently rewritten.The re-parse is deliberate and documented in
_get_coord(dascore/core/coordmanager.py, ~line 1225): array coords can be left non-canonical by slicing, so an evenly spaced subset should collapse back to aCoordRange. That intent is reasonable. The problem is that the canonicalization is implemented withget_coord, whose evenness test isall_diffs_close_enough(dascore/utils/misc.py:586):So any coordinate whose spacing varies by less than 0.1% is collapsed, and its values are replaced by an idealized ramp. Canonicalization that changes data is lossy; it should be reserved for the exactly-even case where it is a pure representation change. The comment on that rtol already notes it "is a bit arbitrary."
This is the mechanism behind the caveat in #895: a reader can build an exact coordinate with
get_exact_coordand still have the coord manager convert it back to aCoordRange, so exactness cannot be guaranteed at the reader layer. There it costs the real per-sample jitter in a Febus acquisition-window coordinate, but nothing about the behavior is Febus-specific — it applies to any coordinate carrying small, physically meaningful irregularity: GPS-derived positions, per-sample timing jitter, measured channel spacing.Example
Same through the public
Patchconstructor:Maximum absolute error here is
5e-4on a nominal spacing of 1.0. There is no warning, and the coordinate reports itself as an evenly sampledCoordRangeafterwards, so downstream code has no way to tell the spacing was invented.Expected behavior
Constructing a coordinate explicitly and handing it to a coord manager should never change its values. Canonicalizing an array coord to a
CoordRangeis fine when the range reproduces the original values exactly — that is a representation change. When it does not, the original coordinate should be kept.Concretely,
cm.coord_map["distance"].valuesshould equalvalsin the example above, while an exactly evenly spacedCoordMonotonicArrayshould still collapse to aCoordRangeas it does today.Worth deciding separately: whether
get_coord(data=...)should keep snapping this aggressively when inferring from a raw array. That path is at least documented as tolerant inference, so it is defensible; silently discarding a caller's explicit, exact coordinate is not.Versions
dev)