The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.
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},
#> }
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.