library(tilework)
library(terra)
f <- system.file("ex/elev.tif", package = "terra")
r <- rast(f)
tp <- pixelTilePlan(pxdims = dim(r)[1:2], nrows = 20L, ncols = 20L)A tilePlan defines the geometry of a tiling. Three
higher-level structures control which tiles get processed and
how:
| Class | Purpose | Parallelization |
|---|---|---|
tileSelection |
Lazy index subset of a plan | Same as underlying tilePlan |
tileGroup |
Named groups of tiles | Across groups or within groups |
tileIterator |
Stateful streaming in batches | Batches distributed across workers |
These are not mutually exclusive — a tileGroup can be
wrapped in a tileIterator, and a tileGroup
itself wraps a tilePlan.
Before introducing the higher-level structures, it helps to see what
tileApply looks like on a plain tilePlan. This
is the baseline that tileSelection, tileGroup,
and tileIterator all build on.
results <- tileApply(r, tiles = tp, FUN = function(x, .I, .R, .C) {
list(
mean = terra::global(x, "mean", na.rm = TRUE)[[1L]],
tile = .I,
row = .R,
col = .C
)
})
#> Warning: Your code is running sequentially. For better performance, consider using a
#> parallel plan like:
#> future::plan(future::multisession)
#> To silence this warning, set options("tilework.warn_sequential" = FALSE)
str(results[[1L]])
#> List of 4
#> $ mean: num 476
#> $ tile: int 1
#> $ row : num 1
#> $ col : int 1FUN receives the extracted tile data as its first
argument. Four special parameters are available and injected
automatically when present in formals(FUN):
| Injectable | Type | Value |
|---|---|---|
.I |
integer |
Flat tile index |
.R |
integer |
Tile row in the grid |
.C |
integer |
Tile column in the grid |
.TILE |
SpatExtent or integer[4] |
Tile bounds object |
Every tile is processed independently. When a future
plan with multiple workers is active, tiles are distributed across
workers automatically. The higher-level structures below give you
control over which tiles are processed, in what order,
and with what per-worker state.
tileSelectiontileSelection is the result of drop = FALSE
indexing on a tilePlan. It stores a subset of tile indices
without computing any bounds, and is passed directly to
tileApply like a plan.
# Select specific tiles without materialising bounds
sel <- tp[c(1, 5, 9, 13), drop = FALSE]
sel
#> <tileSelection>: pixelTilePlan
#> 4 tiles
length(sel)
#> [1] 4results <- tileApply(r, tiles = sel, FUN = function(x, .I) {
terra::global(x, "mean", na.rm = TRUE)[[1L]]
})
#> Warning: Your code is running sequentially. For better performance, consider using a
#> parallel plan like:
#> future::plan(future::multisession)
#> To silence this warning, set options("tilework.warn_sequential" = FALSE)
str(results)
#> List of 4
#> $ : num 476
#> $ : num NaN
#> $ : num 265
#> $ : num 314When to use: You have a plan but only want to process a known subset — for example, tiles that intersect a region of interest, tiles flagged in metadata, or a manual quality-control subset. It avoids rebuilding the plan and keeps the original tile indices as identifiers.
tileGrouptileGroup organizes tiles from a plan into named groups.
Each group is a logical partition — groups can overlap, and not all
tiles need to be assigned to a group.
nr <- nrow(tp)
nc <- ncol(tp)
tg <- tileGroup(tp, groups = list(
interior = list(2:(nr - 1L), 2:(nc - 1L)), # ij indexing
border = c( # vector indexing
# top and bottom rows
seq_len(nc),
seq(length(tp) - nc + 1L, length(tp)),
# left and right columns (excluding corners)
seq(nc + 1L, length(tp) - nc, by = nc),
seq(nc + nc, length(tp) - 1L, by = nc)
)
))
tg
#> <tileGroup>: pixelTilePlan
#> groups-------------------------
#> interior : 9 tiles
#> border : 16 tilesGroups accept the same indexing styles as the underlying plan, and
they can be mixed within the same tileGroup:
list(rows, cols) that
selects by grid position, expanding all row × col combinations.
list(2:(nr-1), 2:(nc-1)) means all interior rows × all
interior cols.tileApply on a tileGroup accepts a
parallel_strategy argument:
"groups" (default) — groups are
distributed across workers. Each worker processes all tiles in its
assigned group sequentially. This is the right choice when:
setup_FUN which runs once per group/workerresults_groups <- tileApply(r,
tiles = tg,
parallel_strategy = "groups",
setup_FUN = function(.GROUP) {
# Runs once per group — expensive setup goes here
list(group_name = .GROUP, initialized_at = Sys.time())
},
FUN = function(x, .I, .GROUP, .SETUP_OUT) {
list(
tile = .I,
group = .GROUP,
mean = terra::global(x, "mean", na.rm = TRUE)[[1L]]
)
}
)
#> Warning: Your code is running sequentially. For better performance, consider using a
#> parallel plan like:
#> future::plan(future::multisession)
#> To silence this warning, set options("tilework.warn_sequential" = FALSE)
# Results are a named list with one element per group
names(results_groups)
#> [1] "interior" "border"
length(results_groups$interior)
#> [1] 9"tiles" — groups are processed
sequentially, but tiles within each group are distributed
across workers. This is the right choice when:
setup_FUN available
here)results_tiles <- tileApply(r,
tiles = tg,
parallel_strategy = "tiles",
FUN = function(x, .I, .GROUP) {
terra::global(x, "mean", na.rm = TRUE)[[1L]]
}
)
#> Warning: Your code is running sequentially. For better performance, consider using a
#> parallel plan like:
#> future::plan(future::multisession)
#> To silence this warning, set options("tilework.warn_sequential" = FALSE)
names(results_tiles)
#> [1] "interior" "border"group_FUNAn optional group_FUN is applied to each group’s result
list after all tiles in that group are processed. This is a convenient
hook for within-group aggregation, avoiding a separate post-processing
pass.
results_agg <- tileApply(r,
tiles = tg,
FUN = function(x, .I) {
terra::global(x, "mean", na.rm = TRUE)[[1L]]
},
group_FUN = function(group_results, .GROUP) {
vals <- unlist(group_results)
list(
group = .GROUP,
n = length(vals),
mean = mean(vals, na.rm = TRUE),
range = range(vals, na.rm = TRUE)
)
}
)
#> Warning: Your code is running sequentially. For better performance, consider using a
#> parallel plan like:
#> future::plan(future::multisession)
#> To silence this warning, set options("tilework.warn_sequential" = FALSE)
str(results_agg, max.level = 2)
#> List of 2
#> $ interior:List of 4
#> ..$ group: chr "interior"
#> ..$ n : int 9
#> ..$ mean : num 323
#> ..$ range: num [1:2] 265 411
#> $ border :List of 4
#> ..$ group: chr "border"
#> ..$ n : int 16
#> ..$ mean : num 356
#> ..$ range: num [1:2] 222 476When to use: Any time tiles have a natural logical partition — different tissue regions, different processing requirements, edge vs. interior tiles (as in the ML vignette), or simply grouping tiles for separate output files.
tileIteratortileIterator wraps any tilePlan or
tileGroup (with $active set) and adds stateful
position tracking. Instead of processing all tiles at once, it yields
batches of a fixed size on demand.
iter <- tileIterator(tp, batch_size = 5L)
iter
#> Object of class tileIterator
#> tiles : pixelTilePlan
#> position : 0
#> bound : [1, 25]
#> batch_size : 5
#> progress : 0%
#> remaining : 25For full control, iterate manually with $has_next and
$next_batch():
tileApplyPassing a tileIterator to tileApply
triggers the streaming dispatch path. iterSplit()
partitions the iterator’s remaining range across
future::nbrOfWorkers() sub-iterators. Each worker
independently streams through its assigned tiles in batches.
iter$reset()
results <- tileApply(r,
tiles = iter,
FUN = function(batch, .I, .BATCH, .POSITION) {
# batch is a list of SpatRaster tiles
# .I — tile indices in this batch
# .BATCH — batch number within this worker
# .POSITION — c(start_index, end_index) of this batch
lapply(batch, function(tile) {
terra::global(tile, "mean", na.rm = TRUE)[[1L]]
})
}
)
#> Warning: Your code is running sequentially. For better performance, consider using a
#> parallel plan like:
#> future::plan(future::multisession)
#> To silence this warning, set options("tilework.warn_sequential" = FALSE)setup_FUN for per-worker initializationWhen tileApply distributes work across workers,
setup_FUN runs once on each worker before batch processing
begins. Its output is passed to every FUN call as
.SETUP_OUT. This is the mechanism for loading a model,
opening a database connection, or any other expensive per-worker
setup.
iter$reset()
results <- tileApply(r,
tiles = iter,
setup_FUN = function(.W, .X) {
# .W — worker index
# .X — the input data object
list(worker = .W, nrow = nrow(.X), ncol = ncol(.X))
},
FUN = function(batch, .SETUP_OUT) {
worker_info <- .SETUP_OUT
lapply(batch, function(tile) {
terra::global(tile, "mean", na.rm = TRUE)[[1L]]
})
}
)
#> Warning: Your code is running sequentially. For better performance, consider using a
#> parallel plan like:
#> future::plan(future::multisession)
#> To silence this warning, set options("tilework.warn_sequential" = FALSE)iterSplit() divides an iterator’s remaining range into
n independent sub-iterators with no shared state. This is
what tileApply calls internally, but it is also available
directly for custom parallel workflows.
iter$reset()
sub_iters <- iterSplit(iter, n = 3L)
# Each sub-iterator covers a non-overlapping range
sapply(sub_iters, function(x) x$bound)
#> [,1] [,2] [,3]
#> [1,] 1 10 18
#> [2,] 9 17 25
sapply(sub_iters, function(x) x$remaining)
#> [1] 9 8 8When to use: Large datasets where loading all tile data simultaneously would exhaust memory; workflows requiring checkpointing (serialize the iterator mid-run and resume later); ML inference pipelines where batch size must match the model’s expected input count. See the ML feature extraction vignette for a full worked example.
| Situation | Recommended |
|---|---|
| Process a fixed known subset of tiles | tileSelection |
| Tiles have distinct logical groupings with different logic | tileGroup |
| Few groups, many tiles per group, tile processing is expensive | tileGroup,
parallel_strategy = "tiles" |
| Many groups, tiles per group is small, setup cost per group matters | tileGroup, parallel_strategy = "groups"
with setup_FUN |
| Dataset too large to hold all tile data in memory at once | tileIterator |
| Worker warmup is expensive (model loading) and batching is needed | tileIterator with setup_FUN |
| Checkpoint and resume a long-running job | tileIterator (serialize and restore) |
All tileApply methods delegate to
future.apply::future_lapply. The active future
plan controls how work is distributed.
library(future)
# Sequential (default, no parallelism)
plan(sequential)
# Local multicore (forking, fast on Linux/macOS)
plan(multicore, workers = 4L)
# Local multisession (separate R processes, works everywhere)
plan(multisession, workers = 4L)
# Mirai backend (lower overhead, faster for many workers)
plan(future.mirai::mirai_multisession, workers = 4L)
# Reset to sequential when done
plan(sequential)tileGroup with parallel_strategy = "groups"
parallelizes at the group level — each future worker
handles one group. With parallel_strategy = "tiles", each
worker handles one tile within the current group.
tileIterator always parallelizes at the worker level:
iterSplit divides the tile range across
nbrOfWorkers() sub-iterators, and each worker streams
through its assigned range independently.