Skip to content

Fix layer-mask NoDataInBounds at low zoom (MD mode)#51

Merged
scottstanie merged 1 commit intoopera-adt:mainfrom
scottstanie:fix/layer-mask-pyramid-mismatch
Apr 24, 2026
Merged

Fix layer-mask NoDataInBounds at low zoom (MD mode)#51
scottstanie merged 1 commit intoopera-adt:mainfrom
scottstanie:fix/layer-mask-pyramid-mismatch

Conversation

@scottstanie
Copy link
Copy Markdown
Collaborator

Bug

When a layer mask is active and the user zooms out so the entire raster fits the viewport, the velocity overlay disappears and the backend logs:

File "/.../titiler/core/factory.py", line 959, in tile
    with self.reader( …
File "<attrs generated methods rio_tiler.io.xarray.XarrayReader>", line 36, in __init__
File "/.../rio_tiler/io/xarray.py", line 84, in __attrs_post_init__
    self.bounds = tuple(self.input.rio.bounds())
File "/.../rioxarray/rioxarray.py", line 1006, in _internal_bounds
    raise NoDataInBounds(
rioxarray.exceptions.NoDataInBounds: Unable to determine bounds from coordinates. Data variable: velocity

At high zoom the layer renders fine — symptom only shows once the requested tile is at a low enough zoom that dataset_for_tile_zoom(tile_z) returns a coarsened pyramid level.

Root cause

_apply_layer_masks_md looked up mask variables in state.dataset (level 0):

mask_da = state.dataset[var]
…
da = da.where(mask_da >= threshold)

But the tile-rendering caller in XarrayPathDependency resolves da from target.dataset_for_tile_zoom(tile_z), which returns a coarser Dataset when zoomed out. da.where(mask_da) then outer-aligns two y/x grids whose cell centres don't coincide. The resulting coordinates are interleaved and non-monotonic — rioxarray's _internal_bounds() can't extract a uniform resolution from them and raises.

reproject_match (used by the custom-mask path) doesn't apply here because the layer-mask path went straight through where.

Fix

Added source_ds kwarg to _apply_layer_masks_md. Tile-rendering callsite now passes the same pyramid-level Dataset that da came from. The other three callers (/point, /buffer_timeseries, /trend_analysis) all work on state.dataset directly and keep relying on the default — they're at level 0 already, so no behavior change.

Test plan

  • ruff / ruff-format / mypy pass
  • Reviewer to verify on the affected dataset (the test box I had loaded was COG mode, not MD — different code path; couldn't reproduce locally without a Zarr cube). Repro: load a --stack-file cube.zarr, apply a closure_coherence ≥ 0.5 mask, zoom out until the whole raster is in view. Before this PR: overlay vanishes + 500s in log. After: overlay continues to render at every zoom.

🤖 Generated with Claude Code

`_apply_layer_masks_md` always pulled mask variables from
`state.dataset` (the level-0 Dataset), but tile-rendering callers
resolve the data variable from `target.dataset_for_tile_zoom(tile_z)`,
which returns a *coarser* pyramid level when zoom is low.

`da.where(mask_da)` then has to outer-align two grids whose y/x
coordinates don't share any cell centres. The result has interleaved
non-monotonic coordinates that rioxarray's `_internal_bounds` can't
turn into a uniform resolution — request 500s with NoDataInBounds in
the rio_tiler XarrayReader's `__attrs_post_init__`.

Symptom: at high zoom (pyramid level matches level 0) the layer
renders fine; zooming out so the entire raster fits in view makes the
overlay disappear and floods the log with the trace above.

Fix: add a `source_ds` parameter to `_apply_layer_masks_md`. The
tile-rendering callsite in `XarrayPathDependency` now passes the same
pyramid-level Dataset that `da` came from. Other three callers
(`/point`, `/buffer_timeseries`, `/trend_analysis`) keep operating on
`state.dataset` and rely on the default — they already use level 0
exclusively, so no behaviour change there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@scottstanie scottstanie merged commit f864de7 into opera-adt:main Apr 24, 2026
0 of 2 checks passed
@scottstanie scottstanie deleted the fix/layer-mask-pyramid-mismatch branch April 24, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant