Search code examples
rmoving-averageterra

Error with custom function in terra::focal in R


I want to use a simple custom function in a moving window. I've successfully used terra::focal for this in the past, but am now running into an error:

Error in as.vector(x, "character") : cannot coerce type 'closure' to vector of type 'character'

I'm not clear why this is happening. Other examples of this I can find on SO suggest I'm missing parentheses after the function name, but including them returns a different error instead.

I'm not set on using terra::focal; an alternative that worked on a matrix that was as fast would also be fine. This isn't a spatial workflow, so I would actually prefer to avoid converting the matrix to spatRaster to use focal and then converting back again afterwards.


Example code:

# Custom function: if mean of the window is greater than the threshold 
# then return the value unchanged, otherwise return the minimum value in the window.
# (Returning value number 14, as this is the central cell in a 9 x 3 window)
mean.or.min <- function(x, ...){
  if(mean(x) > 10) {
    return(x[[14]])
  } else {
    x[[14]] <- min(x)
    return(x[[14]])
  }
}


# Create example data
set.seed(42)
temp.matrix <- matrix(data = sample(1:30, 600000, replace = TRUE), nrow = 200)

# Convert matrix to a spatRaster
temp.rast <- terra::rast(temp.matrix)

# Moving window
temp.rast.smoothed <- terra::focal(temp.rast, w = c(9,3), fun = mean.or.min, fillvalue = 0,  expand = TRUE)



Solution

  • As noted by IFRTM, this is due to a mishandling of warning. The warning should be that "expand is ignored for functions that are not 'built-in'". This has been fixed in the development version (1.7.33).

    you can work around it by not using expand=TRUE. The below works for me.

    # simplified function
    mean.or.min <- function(x, ...){
      if (mean(x) > 10) {
        x[14]
      } else {
        min(x, ...)
      }
    }
    
    library(terra)
    set.seed(42)
    temp.matrix <- matrix(data = sample(1:30, 60000, replace = TRUE), nrow = 200)
    r <- terra::rast(temp.matrix)
    x <- terra::focal(r, w=c(9,3), fun=mean.or.min, fillvalue=0)
    

    You can also do this:

    y <- ifel(focal(r, w=c(9,3), mean, fillvalue=0) > 10, r, 
              focal(r, w=c(9,3), min, fillvalue=0))
    

    Even though this calls focal twice, it is much faster because it uses built-in functions "mean" and "min" instead of an R-based function.

    If you cannot build this up from built-in functions, you could consider focalCpp. In this case that could be:

    library(Rcpp)
    cppFunction( 
        'NumericVector focalfun(NumericVector x, size_t ni, size_t nw) {
            NumericVector out(ni);
            // loop over cells
            double mxval = nw * 10;
            size_t start = 0;
            for (size_t i=0; i<ni; i++) {
                size_t end = start + nw;
                double vsum = 0;
                double vmin = 9999999;
                for (size_t j=start; j<end; j++) {
                    vsum += x[j];
                    if (x[j] < vmin) {
                        vmin = x[j];
                    }
                }
                if (vsum > mxval) {
                    out[i] = x[start+13];
                } else {
                    out[i] = vmin;
                }
                start = end;
            }
            return out;
        }'
    )
    
    z <- terra::focalCpp(r, w=c(9,3), fun=focalfun, fillvalue=0)