Continuous index-trajectory change detection (Sentinel-2 + BFAST) (#30)#33
Merged
NewGraphEnvironment merged 16 commits intoJul 9, 2026
Merged
Conversation
Continuous index-trajectory change detection (Sentinel-2 + BFAST). Four new exports across a config restructure, index registry, cube fetch mode, and a bfast reducer, plus a network-decoupled vignette. One branch, 0.3.0 release. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restructure dft_stac_config() so cube-capable sources carry a role->asset map (red/nir/swir16/mask), mask values, and reflectance scale/offset, marked with cube = TRUE. Categorical sources (io-lulc, esa-worldcover) keep their flat 4-field shape verbatim (backward compat, pinned by expect_named); the cube marker is absence = FALSE. Add sentinel-2-l2a (Planetary Computer band names B04/B08/B11/SCL). Guard dft_stac_fetch() to reject cube sources with a pointer to dft_stac_cube(). Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 (index registry): inst/indices/indices.csv holds spectral-index formulas over band roles in gdalcubes tinyexpr C syntax (pow(), not ^). dft_index_table() reads it (mirrors dft_class_table); dft_index_expr() resolves roles to per-source assets, folds the reflectance scale/offset into each band token (a non-zero offset does not cancel in a ratio index), and applies the expression to a cube. Phase 3 (cube fetch): dft_stac_cube() is the trajectory sibling of dft_stac_fetch() -- STAC query with intersects (unioned AOI) + eo:cloud_cover scene pre-filter + items_fetch pagination, an explicit AOI-bounded cube_view, SCL cloud masking, the index applied, and the masked index cube materialized to <source>/cube_<key>.nc keyed by every cube-affecting parameter. Returns the lazy cube for dft_rast_break(). Fresh-eyes code-check on each: the cube's intersects used only the first AOI feature (silent NoData holes for multi-feature AOIs) -- fixed by unioning. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reduces an index cube over time with bfast::bfastmonitor(), returning a
two-band SpatRaster of break date (decimal year) and magnitude (negative =
index drop = scour / veg loss).
Empirical gate (findings.md): the gdalcubes reduce_time R callback runs in a
spawned worker at EVERY parallel setting, and a closure over enclosing locals
fails there ("object 'band' not found"). The reducer is therefore built
self-contained -- build_break_reducer() embeds the testable .dft_break_pixel()
helper as a literal object (env detached to baseenv) and inlines every scalar
parameter via substitute(), with load_pkgs="bfast" -- proven correct at
parallel 1 and 2. ts start / seasonal frequency are derived from the cube time
axis in the main process; a caller frequency that disagrees with the cadence
errors. Degenerate pixels (all-NA / < min_obs) short-circuit before any bfast
call, so those paths test with no bfast dependency.
Relates to NewGraphEnvironment/sred#16
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The real-Sentinel-2 end-to-end run produced an entirely-NA kNDVI cube (and intermittently segfaulted the gdalcubes compute worker). Isolated to gdalcubes::filter_geom(): stepping through the pipeline, the raw bands, SCL masking, and the kNDVI index all yield valid data (kNDVI in 0..1) WITHOUT filter_geom, and the proven sibling dft_stac_fetch() works on the same AOI. Drop the in-pipeline polygon clip. The cube now spans the AOI bounding box (cloud-masked); callers clip the reduced raster with terra::mask(), matching how dft_stac_fetch() masks after reading. Verified end-to-end: 48-month cube has valid kNDVI throughout and dft_rast_break() reduces it (no segfault) to a plausible break raster (dates in the 2022-2023 monitoring window). Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…config (#30) For high-latitude floodplains the all-months fetch was slow (~30 min for 100 ha -- the Sentinel-2 streaming, not the compute) and injected winter snow/low-sun noise that has no vegetation signal. Focus the analysis on the growing season: - dft_stac_cube() gains `months` (calendar-month filter, default NULL/all). Fetching only e.g. June-September streams ~3x fewer scenes AND sharpens the signal; empty months become NA so the monthly series stays regular at frequency 12 for bfast. - Snow/ice (SCL 11) added to the default Sentinel-2 mask. - A GDAL /vsicurl config block (DISABLE_READDIR_ON_OPEN etc.), restored on exit, trims per-COG open latency. - dft_rast_break() gains `order` (bfastmonitor harmonic order); lower it for a growing-season-only series that samples a narrow part of the annual cycle. Both cache keys extended with the new params. Growing-season E2E (months=6:9, 2018-2023): 12 min vs 30, 24 clean summer observations per pixel, breaks dated to summer months. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Planetary Computer +1000 DN reflectance offset only applies from processing baseline 04.00 (2022-01-25 on); earlier scenes have no offset. A uniform offset was therefore wrong for pre-2022 scenes and produced a false whole-AOI index step at the boundary (validated: 99% of pixels showed a negative "break" dated to exactly the boundary -- an artifact, not vegetation). Fix (baseline-conditional split): - dft_stac_config() sentinel-2-l2a gains offset_boundary = "2022-01-25" and offset_before = 0. - dft_stac_cube() splits items at the boundary, corrects each side with its own offset, and coalesces onto the shared monthly grid. It now returns a terra SpatRaster stack (materialized GeoTIFF, time per layer) instead of a gdalcubes cube -- gdalcubes cannot read a terra-written NetCDF, so the coalesce lives in terra. - dft_rast_break() reduces the stack per pixel via parallel::mclapply (forked workers inherit the namespace and closures, so the whole gdalcubes reduce_time worker-serialization machinery -- build_break_reducer, load_pkgs -- is gone; .dft_break_pixel is unchanged). ts start / frequency derive from the layer times. Validated end-to-end on 2018-2023 growing-season Sentinel-2: split fired 75 pre / 85 post, per-year kNDVI now aligns across 2022 (2021=0.52, 2022=0.53), and breaks fell from 99% to 25% of usable pixels -- real change, not the boundary artifact. Addresses the offset half of #32. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flag that the Sentinel-2 (and Landsat template) cube sources read free Planetary Computer / Azure Open Data with no egress charge, prompting anyone adding a new source to check egress terms before shipping a requester-pays one. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vignettes/trajectory-break-detection.Rmd demonstrates the continuous index-trajectory pipeline on the same Neexdzii Kwa reach as the land-cover vignette. Following drift's data-raw pattern, data-raw/vignette_data_break.R does the network + bfast work (growing-season Sentinel-2 kNDVI 2018-2023 with the baseline-conditional offset split, reduced to break date + magnitude) and saves a small committed artifact (inst/testdata/neexdzii_break.rds, 132 KB) so every vignette chunk runs live under pkgdown with no network or bfast. The vignette shows the break-magnitude map (negative = scour / veg loss), the break-year map, and scour-vs-stable pixel kNDVI trajectories with the detected break marked. Renders clean. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .Rbuildignore: exclude planning/ and .claude/ from the built package (they belong in git for the SRED trail but are not package sources). - .gitignore: data-raw/.break_cache/ (local STAC cube cache). - Declare the rlang .data pronoun as a global (pre-existing note in dft_rast_classify's apply_remap), alongside eo:cloud_cover. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Continuous index-trajectory change detection (Sentinel-2 + BFAST) for floodplain reaches (#30). R CMD check: 0 errors / 0 warnings / 0 notes. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Continuous index-trajectory change detection shipped as v0.3.0. See the archive README for the outcome and the three empirical findings that reshaped the plan (worker-closure failure, filter_geom segfault, Sentinel-2 offset boundary). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Continuation of the archived #30 cycle (v0.3.0 / PR #33). Refines the trajectory vignette example to 2017-2023 (apples-to-apples with the LULC vignette) and to surface the known 2022-2023 logging cut, which the shipped 2018-2023 / monitor-from-2022 example did not (region-wide 2023 dip dominated). PWF headers carry the prior journey forward and reference the archive. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Redo of the trajectory vignette example so it is apples-to-apples with the land-cover vignette (same Neexdzii Kwa reach, same 2017-2023 window) and actually surfaces the logging the shipped 2018-start / monitor-from-2022 example buried under a region-wide 2023 dip. - data-raw/vignette_data_break.R: fetch 2017-2023 growing-season kNDVI (offset split), monitor from 2022 with 2017-2021 history (longer baseline -> 13k breaks vs 25k), and precompute a LULC ground-truth grouping (Trees -> Rangeland/Bare vs intact forest vs background) + agreement stats into a 140 KB artifact. - vignette reframed as the complement to LULC: LULC says what and roughly when (annual snapshots), the trajectory says exactly when (monthly). Shows the break map with LULC tree-loss outlined, an agreement table (tree-loss stands break 2x as often, 2.7x stronger, dated summer 2023), and the grouped trajectory -- with the basin-wide 2023 dip honestly acknowledged. Vignette re-builds clean under R CMD check; 286 tests pass. Lands on PR #33. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) Reconciling why the bfast trajectory disagreed with LULC's dramatic Trees->Rangeland *inside* the floodplain revealed the tool's real value for the project goal (floodplain veg change to justify leave-standing / restoration): the LULC "tree-loss" pixels were borderline (kNDVI baseline 0.43 vs 0.50 intact, median drop 0.037, only 2% a real crash). On this deciduous-riparian reach the categorical product overstates forest loss (label flips, not canopy removal), and the continuous trajectory's job is to QA that and to catch gradual degradation/recovery the annual labels cannot. - dft_rast_trend(): new export. Per-pixel robust Theil-Sen slope (index/yr) + Mann-Kendall significance over the full stack, via forked mclapply like dft_rast_break. Resistant to a single anomalous season. Guards for <2 layers / min_obs<2. Code-check verified the statistics. - Vignette rebuilt as "Trajectories as a Check on Land-Cover Change": a degradation/recovery trend map (floodplain is stable-to-greening), a QA table showing the mapped tree-loss carries a median trend of ~0 and only ~6% significant decline (a caution on the area numbers), the grouped trajectory, and an honest "when to use which" (strong on evergreen/abrupt/gradual, weak at deciduous-riparian-vs-grass). Fixed an inverted map palette in self-review. 302 tests pass; R CMD check clean (bar a spurious future-timestamp NOTE); lint clean. Lands on PR #33. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e map (#30) Rework the vignette from patch-level ground truth: real cutblocks in the floodplain coincide with red patches in the break/trend maps, and some red falls outside the LULC Trees->Rangeland polygons -- the continuous trajectory and the categorical map catch different real change, so they are complementary, not one correcting the other. - data-raw: derive the actual LULC Trees->Rangeland transition polygons (dft_rast_transition + dft_transition_vectors, filtered to the loss transition, not stable Trees->Trees) for the map + overlap check; drop the aggregate QA table and averaged group trajectory (whole-floodplain averaging hides the patchy signal). - vignette: neutral methods-first framing; break-magnitude map with the transition polygons outlined (red in/out/absent = confirmed / LULC-missed / unsupported); trend map relabelled per-year 2017-2023; and an interactive leaflet mirroring the land-cover vignette (Light/Esri/Google basemaps, AOI centering) with toggleable break, trend, and beige Trees->Rangeland layers to inspect the cutblocks on satellite imagery. R CMD check clean (bar the spurious future-timestamp NOTE); vignette builds; 302 tests pass; lint + code-check clean. Lands on PR #33. Relates to NewGraphEnvironment/sred#16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NewGraphEnvironment
added a commit
that referenced
this pull request
Jul 9, 2026
Continuation cycle that reworked the trajectory vignette (QA framing + dft_rast_trend + interactive map) and shipped v0.3.0 (PR #33). See the archive README for the outcome and the categorical-vs-continuous reconciliation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Summary
Adds a continuous change-detection pipeline alongside the categorical one (
v0.3.0;R CMD checkclean, 0/0/0):dft_stac_cube()— fetch a cloud-masked monthly spectral-index stack from Sentinel-2 (new role-based"sentinel-2-l2a"source indft_stac_config()). Growing-seasonmonthsfilter, snow masking,eo:cloud_coverscene pre-filter + pagination, and a baseline-conditional offset split at 2022-01-25 so multi-year series carry no false step. Returns aterraSpatRaster stack.dft_index_expr()/dft_index_table()— table-driven spectral-index registry (NDVI, kNDVI, NDMI) written over band roles, with reflectance scale/offset folded into the expression.dft_rast_break()— per-pixelbfast::bfastmonitor()reduction (forkedmclapply) to a two-band break-date / break-magnitude raster (negative = index drop = scour / veg loss).trajectory-break-detection.Rmdon the Neexdzii Kwa reach, loading a 132 KB committed artifact (data-raw pattern; no network/bfast at build).Empirical findings that reshaped the plan
Validated end-to-end on real Sentinel-2; three things could only be settled by running code:
reduce_timeR-callback in spawned workers where closures fail — ultimately mooted by the terra pivot.gdalcubes::filter_geom()segfaults / yields an all-NA cube on the pinned build → dropped; cube spans the AOI bbox, callers clip the result (AOI-polygon clip deferred to dft_stac_cube: restore AOI-polygon clip (filter_geom segfault workaround) #32).Related Issues
Test plan
devtools::test()— 286 pass (network/bfast-gated tests skip)R CMD check— 0 errors / 0 warnings / 0 noteslintr::lint_package()cleanNotes
10 feature commits + PWF baseline; per-commit fresh-eyes code-checks. Archived planning:
planning/archive/2026-07-issue-30-index-trajectory/.🤖 Generated with Claude Code
https://claude.ai/code/session_01DKqnW6Fni3tc44Lo5D5fJz