Dated reference imagery: dft_stac_composite, RGB map layers, NDWI/MNDWI (#79) - #86
Merged
NewGraphEnvironment merged 29 commits intoSep 29, 2026
Merged
Conversation
gdalcubes was archived on CRAN on 2026-09-16, which broke the pkgdown workflow's dependency resolve and user installs of the STAC functions. Upstream master (0.7.5) carries the filter_geom fix from our fork, so Remotes points at appelmar/gdalcubes with no suffix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Since #48 the cache lives under <cache>/v2/<source>, but five opt-in network assertions still listed <cache>/<source> and so failed on every run. Found verifying gdalcubes 0.7.5; every data assertion passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
rlang::check_installed() offered a bare-name install, which now means
CRAN and fails. The three gdalcubes guards share one helper whose error
gives pak::pak("appelmar/gdalcubes"). Found by code-check round 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Adds the green and blue Sentinel-2 roles (B03, B02) that the water indices and true-colour composites need. Tests evaluate the resolved expressions over known reflectance and over post-2022 DN through the offset, so they check the value and not only the string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Session setup, the cache read gate, the STAC item query with its offset split, and the tiled or untiled assembly move into internal helpers. The assembly takes a pixel function, so the composite coming next can share every line of the read path. Behaviour is unchanged: offline cube tests pass and a live untiled cube is byte-identical to main's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Each year's window (a run of calendar months) is reduced to a cloud-masked median of the requested band roles, as surface reflectance, through the read path shared with dft_stac_cube. Cached as a COG so it can be served by titiler unchanged. From the plan review: - chips around sample points are one call per point, not one tiled call over all points, which would span the floodplain under one key; - a window straddling the 2022-01-25 offset change is refused rather than returned as a pre-side cover; - an empty year is dropped with a warning; - the STAC query uses explicit times, since a date-only end bound drops the last day's scenes; - the network e2e cache paths now follow the #48 scheme dir; - a test pins how dft_stac_cube calls its cache key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
rgb takes composites as rasters (leafem) or COG URLs (titiler), drawn beneath the classified layers with one stretch shared across years, so a difference between years is in the data. x may be NULL for an imagery-only map. Classified and transition layers now reach leaflet in EPSG:3857: with project = FALSE leaflet assumes Mercator pixels, so the 4326 grid was misregistered against anything drawn beneath it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
terra reads gdalcubes' multi-variable NetCDF with its variables in alphabetical order, so a true-colour request came back blue, green, red. The live e2e's layer-order guard caught it; renaming by position would have swapped red and blue silently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
The first call returned the in-memory double stack while the cache holds Float32, so a cache hit differed in the eighth significant figure. The live e2e's same-values-on-second-call assertion caught it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
From code-check round 1: - Stamping time before the COG write made terra emit a .aux.json sidecar that cache_write_atomic() does not move, so every composite build left an orphan in the cache. Time is now stamped only on read. A new offline test drives the whole build and write path with the STAC query and gdalcubes assembly stubbed. - With x = NULL the transition legend sat inside the land-cover legend's gate and was dropped, and a map with no AOI and COG imagery was not centred. Both now follow the transition raster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Code-check round 2: the round-1 sidecar fix only stopped the code stamping a time, but the stack already carries one read from the gdalcubes NetCDF, so the orphan persisted on the real path. The test fixture had no time and could not reach it. - The composite strips time before writing and re-stamps on read. - cache_write_atomic() now moves a terra .aux.json the same way it moves a GDAL .aux.xml. A COG gets one for time, units, varnames, longnames, metags or scoff, so a later metadata addition cannot strand one again. - Both fixtures now carry a time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Code-check round 3 enumerated every raster write in R/ and every test stub against the object it stands in for. There was no production defect, but two tests could not reach what they claimed to cover: - the offline build stub gave every band the same value, so dropping composite_layers_order() stayed green (verified with that mutant); - the network test listed only *.tif, so a sidecar beside the COG was invisible to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
The BULK scale test failed in 9 s: Planetary Computer returns HTTP 413 when the POST search body passes about 1 MiB (measured: accepted at 21,348 vertices / 860 KB, rejected at 26,508 / 1.07 MB). BULK's floodplain is 104,584 vertices, so neither the cube nor the composite could query it at all. Above 20,000 vertices the intersects geometry is now the convex hull, a superset that changes no pixel over the AOI. Below that the query is exactly the union, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
100 chips over the BULK floodplain, 2023 Jul-Aug: 84.1 min, median 40.2 s per chip, peak RSS 0.48 GiB. The cost is COG reads, not the STAC query (#85). The floodplain-wide stage follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Planetary Computer SAS tokens last about 45 minutes and features were signed once, at query time. The BULK floodplain composite read for 54 minutes over 30 tiles, and 15 tiles came back with every chunk failed. rstac's signer refreshes an expired token, so the features are now re-signed per extent. Verified live: corrupted tokens read 102,364 cells after re-signing, 0 without. Detecting failed chunk reads is #87; gdalcubes reports them only on worker stderr, which R cannot reliably capture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
…re (#79, #88) Run 4 read all 30 tiles in 3 h 16 min with no chunk failures (the per-extent re-signing holds at scale), peak RSS 3.90 GiB, then failed: terra::mask could not read the merged mosaic's temp GeoTIFF. It was not reproduced offline, and the evidence went with the session tempdir, so the stage now inventories temp files and gdalinfo -checksum on failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN
NewGraphEnvironment
deleted the
79-dated-reference-imagery-true-colour-comp
branch
September 29, 2026 00:58
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #79
Stacked on #84 (#80, gdalcubes from GitHub). Merge #84 first; its four commits disappear from this diff once it lands.
Why
floodplains#93 needs dated reference imagery to run a stratified accuracy assessment of IO LULC at sample points across whole floodplains. The Esri and Google basemaps have no capture date. drift could fetch Sentinel-2, but only as a single-index monthly cube.
What
dft_stac_composite(), new. For each year's run of calendar months it returns a cloud-masked median of any band roles, as surface reflectance. It shares the read path withdft_stac_cube(), which was refactored into shared helpers; a live untiled cube from main and from the refactor match cell for cell (max diff 0). The cache is written as a COG.dft_map_interactive(rgb =): composites as rasters (leafem) or COG URLs (titiler), drawn beneath the land cover with one per-band stretch shared across years.xmay beNULL.dft_index_table(), with thegreenandblueroles.intersectsand ignores the CQL2 cloud filter;earthdatalogin's default credentials now return 401;Defects found and fixed here
project = FALSE, so they were misregistered against anything beneath them.aux.jsonthatcache_write_atomic()stranded. The R1 fix was incomplete: the time comes from the NetCDF reader, and the fixture had none.aux.jsontoox = NULLAlso fixed: five opt-in network assertions left on the pre-#48 cache path (they are on #84 too).
Verification
devtools::test(): FAIL 0, SKIP 15, PASS 1113.lintr: 0 lints on changed lines.pkgdown::check_pkgdown(): clean.DRIFT_TEST_NETWORK=true, cube (incl. tiled vs untiled): PASS 110.DRIFT_TEST_NETWORK=true, composite: PASS 64. It checks 3 bands in order, reflectance values,LAYOUT=COG, a declared NoData, identical values on a cache hit, and that false-colour NIR is more than 2× red..aux.jsonmove, layer reorder call, transition legend, transition centring./code-check: three rounds, and the loop was ended by an enumeration.R/(none emits a sidecar on real output) and every test stub against its real upstream. It found 2 test gaps, now fixed.BULK scale test (
data-raw/benchmark_composite_bulk.R, RSS sampled every 2 s)tile_size = 20000terra::maskcould not read the merged mosaic's temp file. Open as #88: not reproduced offline (a synthetic 30-tile merge and mask with the real polygon passes in 0.7 min), and the benchmark now inventories temp files on failure. Run 2's COG-write failure was probably the same thingChip cost is COG reads, not the query (1.4–2.5 s of about 50), so concurrency is #85.
Follow-ups filed
Known limitation at merge: a floodplain-wide composite over an AOI the size of BULK does not yet complete (#88). Per-point chips, the path floodplains#93 needs first, and reach-scale composites do.
The last three commits are NEWS, the PWF archive (
planning/archive/2026-09-issue-79-dated-reference-imagery/, whose README has the measurement record) andRelease v0.18.0.Relates to NewGraphEnvironment/sred-2025-2026#16
🤖 Generated with Claude Code
https://claude.ai/code/session_01PRhUJsuKABLfpBGktPoiBN