Sibling of dft_stac_fetch() for continuous change detection. Where
dft_stac_fetch() materializes one categorical raster per year, this builds
a sub-annual reflectance cube, masks clouds, computes a spectral index over
band roles, and returns the index time series as a SpatRaster (one layer
per time step) — the input to dft_rast_break() for per-pixel trajectory
breakpoint detection.
Usage
dft_stac_cube(
aoi,
source = "sentinel-2-l2a",
index = "kndvi",
datetime = NULL,
res = 10,
crs = NULL,
dt = "P1M",
aggregation = "median",
resampling = "bilinear",
clip = TRUE,
cloud_cover_max = 60,
months = NULL,
mask_values = NULL,
tile_size = NULL,
parallel = NULL,
cache_dir = NULL,
force = FALSE,
sign_fn = rstac::sign_planetary_computer()
)Arguments
- aoi
An
sfpolygon defining the area of interest.- source
Character. A cube source name for
dft_stac_config()(default"sentinel-2-l2a"). Must be a source withcube = TRUE.- index
Character. Spectral index from
dft_index_table()(default"kndvi"). Determines which band roles (and thus assets) are fetched.- datetime
Character. ISO 8601 interval
"start/end". WhenNULL, usesavailable_datetimefromdft_stac_config().- res
Numeric. Output pixel size in CRS units (default 10).
- crs
Character. Target CRS as an EPSG string. When
NULL, auto-detected from the AOI centroid's UTM zone.- dt
Character. ISO 8601 duration for the temporal aggregation window (default
"P1M", monthly). The cadencedft_rast_break()'sfrequencymust agree with.- aggregation
Character. Temporal aggregation for multiple scenes in one
dtwindow (default"median").- resampling
Character. Spatial resampling (default
"bilinear").- clip
Logical. When
TRUE(default), clip the returned stack to the AOI polygon withterra::mask(), sodft_rast_break()/dft_rast_trend()reduce only in-polygon pixels. The clip keeps every cell the polygon touches —terra::mask()defaults totouches = TRUE— so it is inclusive at the boundary by up to one cell, deliberately, so a thin corridor is not eroded. Cells the polygon does not touch becomeNAon every layer. That is a different rule from a cell-centre clip, and worth 15.5% of the analysed footprint on the packaged AOI (#47), so it matters to anything reporting boundary hectares. SetFALSEto keep the wider extent (e.g. for surrounding context, or to mask later with a different polygon). This clips the output — with the defaulttile_size = NULLthe full bbox of COGs is still streamed either way, soclip = FALSEreturns the full bounding box. Whentile_sizeis set the read is tiled, soclip = FALSEreturns the AOI-intersecting tile union (a stair-stepped superset of the polygon withNAwhere empty tiles were skipped), not a gap-free bounding box.- cloud_cover_max
Numeric. Scene-level
eo:cloud_covermaximum percent for the STAC pre-filter (default 60).- months
Integer vector of calendar months (1-12) to keep, or
NULL(default) for all. Restricting to the growing season (e.g.6:9) both sharpens the vegetation signal — snow and low-sun winter scenes carry no vegetation information — and cuts the number of scenes streamed. Months with no retained scenes becomeNAin the monthly cube, so the per-pixel series stays regular atfrequency = 12fordft_rast_break(). Prefer a longerdatetimewindow when using this, so enough growing-season history remains to fit a stable BFAST baseline.- mask_values
Integer vector of mask-band classes to exclude. When
NULL, usesmask_valuesfromdft_stac_config()(e.g. Sentinel-2 SCL cloud / shadow / cirrus classes).- tile_size
Numeric or
NULL(default). Edge length, in CRS units (metres for the default UTM CRS), of the read-tiling grid (#38). WhenNULL, one cube is streamed over the whole AOI bounding box (the read scales with the bbox, not the AOI). When set, the bbox is split into a grid oftile_size-square tiles and only tiles that intersect the AOI polygon are streamed, then mosaicked — so a thin, diagonal AOI (e.g. a floodplain corridor) reads close to its footprint. Snapped to a multiple ofres. Smaller tiles waste less bbox but cost more per-tile round trips; there is no auto-tuning. The cube always caches a.tifeither way; a tiled read keys distinctly (see the caching note above), so untiled caches are untouched andtile_size = NULLis byte-for-byte the previous behavior. This is the continuous-path twin ofdft_stac_fetch()'stile_size. Benchmarked against the alternatives on the packaged AOI (drift#47) it is the slowest option — 1263.6 s and 3213 range requests against an untiled 236.8 s / 462, because every tile rebuilds the image collection and reopens the COGs — so preferparallelfor speed and reach fortile_sizeonly when peak memory, not wall clock, is the constraint. Because the cube resamples with bilinear, a tiled cube faithfully reproduces the untiled cube (the per-pixel reducers are unaffected) but lands on a bbox-anchored grid that is sub-pixel-offset from — not pixel-identical to — the untiled cube.- parallel
Integer, or
NULL(default) to auto-detect asmin(4, cores - 1), flooring to 1 where the core count is undetectable. Number of gdalcubes worker processes used for the read. The COG stream is the dominant cost and it parallelizes well: measured on the packaged AOI, a 4-month monthly kNDVI cube took 236.8 s atparallel = 1, 115.8 s at 4 and 96.0 s at 8 — and the output is byte-identical at every setting (correlation 1.000, max absolute difference 0), so this is a pure cost knob and does not enter the cache key. Capped at 4 by default rather than the full core count because each worker holds chunks in memory; raise it on a machine with headroom, or set1for the previous single-threaded behaviour. Prior to v0.9.0 drift never calledgdalcubes::gdalcubes_options()at all, so every fetch ran single-threaded (drift#47). gdalcubes also derives its default chunk size from this value, so raising it makes chunks finer as a side effect — measured in isolation on this AOI that is a cost (343.7 s / 693 requests at 128 px against 236.9 s / 462 at the default), so the speedup is attributable to concurrency. WhenNULLand a session-levelgdalcubes::gdalcubes_options()parallelabove 1 is already set, that value is honoured rather than overridden.- cache_dir
Character. Cache directory. When
NULL, usesdft_cache_path().- force
Logical. Re-fetch even if cached, replacing the cached raster (default
FALSE). The replacement is atomic, so an interrupted forced re-fetch leaves the previous entry intact rather than destroying it — which matters here, where rebuilding is a multi-hour stream.- sign_fn
A signing function for STAC assets. Default is
rstac::sign_planetary_computer().
Value
A terra::SpatRaster index stack — one layer per time step, with a
time value per layer — cached as a GeoTIFF. By default (clip = TRUE) the
stack is clipped to the AOI polygon (cloud-masked; every cell the polygon
touches is kept, cells it does not touch are NA — see clip), so the
reduced raster from dft_rast_break() is already polygon-tight;
pass clip = FALSE for the full AOI bounding box (or, with tile_size
set, the AOI-intersecting tile union). For sources with a
reflectance-offset baseline boundary (Sentinel-2), items are split at the
boundary and offset-corrected per side, so a series crossing it carries no
artificial index step.
Details
The index stack is materialized once to a GeoTIFF under dft_cache_path()
as <source>/cube_<key>.tif, keyed by a hash of the AOI geometry and every
cube-affecting parameter (including clip and tile_size, so a tiled read
keys apart from an untiled one). Because it is invariant to dft_rast_break()'s
parameters, caching it here makes bfast parameter sweeps cheap — they re-read
the local raster instead of re-streaming COGs.
Three STAC-query specifics distinguish cube mode from dft_stac_fetch():
pagination via rstac::items_fetch() is mandatory (a monthly multi-year query
returns hundreds of items; a single page silently truncates); the query uses
intersects with the AOI geometry, not a bounding box (floodplain polygons
are highly non-rectangular); and a scene-level eo:cloud_cover pre-filter
shrinks the collection before any pixel is read, complementing per-pixel mask
filtering.
See also
dft_rast_break() (the reducer that consumes this cube),
dft_index_expr() (the index applied), dft_stac_fetch() (categorical
sibling).
Examples
if (FALSE) { # \dontrun{
# Monthly kNDVI cube for a floodplain reach (requires network + gdalcubes)
aoi <- sf::st_read(system.file("extdata", "example_aoi.gpkg", package = "drift"))
cube <- dft_stac_cube(
aoi,
source = "sentinel-2-l2a",
index = "kndvi",
datetime = "2019-01-01/2023-12-31",
dt = "P1M"
)
breaks <- dft_rast_break(cube, start = c(2022, 1))
} # }
