We compare fastfocal()
against
terra::focal()
across a range of raster sizes and kernel
radii. To keep this vignette fast for CRAN, we load precomputed results
if available and provide optional code (disabled) to reproduce full
benchmarks locally.
library(fastfocal)
library(terra)
library(dplyr)
raster_sizes <- c(100, 250, 500, 1000, 2500, 5000)
kernel_sizes <- seq(100, 1000, 100)
replicates <- 1
res_m <- 30
crs_m <- "EPSG:3857"
set.seed(888)
Each raster is square with 30 m resolution.
rasters <- lapply(raster_sizes, function(size) {
ext_x <- size * res_m
ext_y <- size * res_m
r <- rast(nrows = size, ncols = size, extent = ext(0, ext_x, 0, ext_y), crs = crs_m)
values(r) <- runif(ncell(r))
r
})
names(rasters) <- as.character(raster_sizes)
oldpar <- par(no.readonly = TRUE)
par(mfrow = c(2, 3), mar = c(2, 2, 3, 1))
raster_labels <- paste0(raster_sizes, "x", raster_sizes)
for (i in seq_along(rasters)) {
plot(rasters[[i]], main = paste("Raster:", raster_labels[i]))
}
Note: running the full grid below can take a while. It is disabled to keep CRAN checks fast. Uncomment to run locally.
grid <- expand.grid(
raster_size = raster_sizes,
d = kernel_sizes,
method = c("fastfocal", "terra"),
stringsAsFactors = FALSE
)
dir.create("benchmark_chunks", showWarnings = FALSE)
benchmark_row <- function(idx) {
size <- grid$raster_size[idx]
d <- grid$d[idx]
method <- grid$method[idx]
fname <- sprintf("benchmark_chunks/%s_%d_%dm.csv", method, size, d)
if (file.exists(fname)) return(NULL)
r <- rasters[[as.character(size)]]
times <- sapply(seq_len(replicates), function(i) {
t0 <- Sys.time()
if (method == "fastfocal") {
fastfocal(x = r, d = d, w = "circle", fun = "mean", engine = "auto", pad = "auto")
} else {
w <- focalMat(r, d, type = "circle")
if (all(w == 0)) return(NA_real_)
focal(r, w = w, fun = mean, na.rm = TRUE, na.policy = "omit")
}
as.numeric(difftime(Sys.time(), t0, units = "secs"))
})
chunk_df <- data.frame(method = method, raster_size = size, d = d, time = times)
write.csv(chunk_df, file = fname, row.names = FALSE)
}
invisible(sapply(seq_len(nrow(grid)), benchmark_row))
# After running, you can combine chunks into a single CSV under inst/extdata/benchmark.csv
bench_path <- system.file("extdata", "benchmark.csv", package = "fastfocal")
if (nzchar(bench_path) && file.exists(bench_path)) {
df <- read.csv(bench_path)
} else {
# Fallback tiny demo dataset for CRAN if extdata is not installed
df <- expand.grid(
method = c("fastfocal", "terra"),
raster_size = c(250, 500, 1000),
d = c(100, 300, 500),
KEEP.OUT.ATTRS = FALSE,
stringsAsFactors = FALSE
)
set.seed(1)
df$time <- ifelse(df$method == "fastfocal",
runif(nrow(df), 0.05, 0.20),
runif(nrow(df), 0.08, 0.35))
}
stopifnot(all(c("method","raster_size","d","time") %in% names(df)))
# --- summary ---
summary_df <- df %>%
group_by(method, raster_size, d) %>%
summarize(
mean_time = mean(time, na.rm = TRUE),
se_time = sd(time, na.rm = TRUE) / sqrt(sum(is.finite(time))),
.groups = "drop"
) %>%
mutate(raster_label = factor(
paste0(raster_size, "x", raster_size),
levels = paste0(sort(unique(df$raster_size)), "x", sort(unique(df$raster_size)))
))
oldpar <- par(no.readonly = TRUE)
layout(matrix(1:6, nrow = 2, byrow = TRUE))
par(mar = c(4, 4, 3, 1))
cols <- c("fastfocal" = "#0072B2", "terra" = "#D55E00")
raster_labels <- levels(summary_df$raster_label)
for (label in raster_labels) {
subdf <- subset(summary_df, raster_label == label)
if (nrow(subdf) == 0) next
plot(NA,
xlim = range(subdf$d),
ylim = range(subdf$mean_time + subdf$se_time, na.rm = TRUE),
xlab = "Kernel size (m)", ylab = "Mean time (s)",
main = paste("Raster:", label))
methods <- unique(subdf$method)
for (m in methods) {
data <- subdf[subdf$method == m, ]
lines(data$d, data$mean_time, col = cols[m], type = "b", pch = 16)
max_time <- max(subdf$mean_time, na.rm = TRUE)
min_se <- 0.001 * max_time
se <- ifelse(is.na(data$se_time), 0, data$se_time)
se_final <- pmax(se, min_se)
suppressWarnings(arrows(
x0 = data$d,
y0 = data$mean_time - se_final,
x1 = data$d,
y1 = data$mean_time + se_final,
angle = 90, code = 3, length = 0.05, col = cols[m]
))
}
legend("topleft", legend = methods, col = cols[methods], pch = 16, lty = 1, bty = "n")
}
Compare a single case at moderate size.
test_r <- rasters[["1000"]]
kernel_d <- 500
# fastfocal
r_fast <- fastfocal(test_r, d = kernel_d, w = "circle", fun = "mean", engine = "auto", pad = "auto")
# terra::focal
w <- focalMat(test_r, kernel_d, type = "circle")
r_terra <- focal(test_r, w = w, fun = mean, na.rm = TRUE, na.policy = "omit")
# Differences
r_diff <- abs(r_fast - r_terra)
v_diff <- values(r_diff)
mean_diff <- mean(v_diff, na.rm = TRUE)
max_diff <- max(v_diff, na.rm = TRUE)
cat("Mean difference:", round(mean_diff, 6), "\n")
#> Mean difference: 0
cat("Max difference :", round(max_diff, 6), "\n")
#> Max difference : 0
oldpar <- par(no.readonly = TRUE)
par(mfrow = c(2, 2), mar = c(2, 2, 3, 2))
plot(test_r, main = "Original", col = terrain.colors(20))
plot(r_terra, main = "terra::focal (500 m)", col = terrain.colors(20))
plot(r_fast, main = "fastfocal (500 m)", col = terrain.colors(20))
plot(r_diff, main = "Absolute difference", col = hcl.colors(20, "YlOrRd", rev = TRUE))
sessionInfo()
#> R version 4.5.1 (2025-06-13 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26100)
#>
#> Matrix products: default
#> LAPACK version 3.12.1
#>
#> locale:
#> [1] LC_COLLATE=C
#> [2] LC_CTYPE=English_United States.utf8
#> [3] LC_MONETARY=English_United States.utf8
#> [4] LC_NUMERIC=C
#> [5] LC_TIME=English_United States.utf8
#>
#> time zone: America/New_York
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] dplyr_1.1.4 terra_1.8-60 fastfocal_0.1.3
#>
#> loaded via a namespace (and not attached):
#> [1] vctrs_0.6.5 cli_3.6.5 knitr_1.50 rlang_1.1.6
#> [5] xfun_0.52 generics_0.1.4 jsonlite_2.0.0 glue_1.8.0
#> [9] htmltools_0.5.8.1 sass_0.4.10 rmarkdown_2.29 evaluate_1.0.5
#> [13] jquerylib_0.1.4 tibble_3.3.0 fastmap_1.2.0 yaml_2.3.10
#> [17] lifecycle_1.0.4 compiler_4.5.1 codetools_0.2-20 pkgconfig_2.0.3
#> [21] Rcpp_1.1.0 rstudioapi_0.17.1 digest_0.6.37 R6_2.6.1
#> [25] tidyselect_1.2.1 pillar_1.11.0 magrittr_2.0.3 bslib_0.9.0
#> [29] tools_4.5.1 cachem_1.1.0
To cite the package:
Wan, H. Y. (2025). fastfocal: Fast Multi-scale Raster Extraction and Moving Window Analysis with FFT. R package version 0.1.3. Zenodo. https://doi.org/10.5281/zenodo.17074691
citation("fastfocal")
#> To cite fastfocal in publications, please use:
#>
#> Wan HY (2025). _fastfocal: Fast Multi-scale Raster Extraction and
#> Moving Window Analysis with FFT_. doi:10.5281/zenodo.17074691
#> <https://doi.org/10.5281/zenodo.17074691>, R package version 0.1.3,
#> <https://hoyiwan.github.io/fastfocal/>.
#>
#> A BibTeX entry for LaTeX users is
#>
#> @Manual{,
#> title = {fastfocal: Fast Multi-scale Raster Extraction and Moving Window Analysis with FFT},
#> author = {Ho Yi Wan},
#> year = {2025},
#> note = {R package version 0.1.3},
#> doi = {10.5281/zenodo.17074691},
#> url = {https://hoyiwan.github.io/fastfocal/},
#> publisher = {Zenodo},
#> }