8  Multilayer Rasters: Cell–wise Operations

8.1 Introduction

8.2 Extracting Grid Time Series

Let us start by downloading the netCDF file for global daily precipitation for the year 2023 from CHIRPS, accessible through the link: https://data.chc.ucsb.edu/products/CHIRPS-2.0/global_daily/netcdf/p05/chirps-v2.0.2023.days_p05.nc. We can use terra::rast function to import the netCDF to the workspace as a multiband raster. Let us first familiarize ourselves with the dataset.

# Copied path of the raster
data_path <- "https://data.chc.ucsb.edu/products/CHIRPS-2.0/global_daily/netcdf/p05/chirps-v2.0.2023.days_p05.nc"

# Download the raster using download.file. Assign "daily_pcp_2023.nc" name to file
if (file.exists("daily_pcp_2023.nc")==FALSE){
  
 download.file(url = data_path, 
              method="curl",
              destfile = "daily_pcp_2023.nc") 
  
}

# Plot downloaded file
library(terra)
pcp=rast("daily_pcp_2023.nc")       # Import raster to the environment 

#~~~ Notice the attributes (esp. nlyr, i.e. number of layers, unit and time)
print(pcp) 
class       : SpatRaster 
dimensions  : 2000, 7200, 365  (nrow, ncol, nlyr)
resolution  : 0.05, 0.05  (x, y)
extent      : -180, 180, -50, 50  (xmin, xmax, ymin, ymax)
coord. ref. : lon/lat WGS 84 (CRS84) (OGC:CRS84) 
source      : daily_pcp_2023.nc 
varname     : precip (Climate Hazards group InfraRed Precipitation with Stations) 
names       : precip_1, precip_2, precip_3, precip_4, precip_5, precip_6, ... 
unit        :   mm/day,   mm/day,   mm/day,   mm/day,   mm/day,   mm/day, ... 
time (days) : 2023-01-01 to 2023-12-31 
#~~~ time variable in the netCDF indicating corresponding time of acquisition
head(time(pcp))  
[1] "2023-01-01" "2023-01-02" "2023-01-03" "2023-01-04" "2023-01-05"
[6] "2023-01-06"
# Import sample locations from contrasting hydroclimate
library(readxl)
loc= read_excel("./SampleData-master/location_points.xlsx")
print(loc)
# A tibble: 3 × 4
  Aridity   State     Longitude Latitude
  <chr>     <chr>         <dbl>    <dbl>
1 Humid     Louisiana     -92.7     34.3
2 Arid      Nevada       -116.      38.7
3 Semi-arid Kansas        -99.8     38.8
# Value of the lat & lon of the locations
latlon=loc[,3:4] 
print(latlon)
# A tibble: 3 × 2
  Longitude Latitude
      <dbl>    <dbl>
1     -92.7     34.3
2    -116.      38.7
3     -99.8     38.8
# Plot data for a specific layer
worldSHP=terra::vect(spData::world)    # Shapefile for CONUS

plot(pcp[[100]])   # Same as pcp[[which(time(pcp)=="2023-04-10")]]
plot(worldSHP, add=TRUE)
points(latlon, pch=19, col="red")

We have previously used terra::extract function to extract cell values from a raster for user-defined locations. However, unlike previous examples, precipitation data is imported from a netCDF, which has 365 layers, one for each day in the year 2023. So, when we use the extract function using point coordinates, 365 values for each location are extracted.

# Import sample locations from contrasting hydroclimate
library(readxl)
loc= read_excel("./SampleData-master/location_points.xlsx")
print(loc)
# A tibble: 3 × 4
  Aridity   State     Longitude Latitude
  <chr>     <chr>         <dbl>    <dbl>
1 Humid     Louisiana     -92.7     34.3
2 Arid      Nevada       -116.      38.7
3 Semi-arid Kansas        -99.8     38.8
# Value of the lat & lon of the locations
latlon=loc[,3:4] 
print(latlon)
# A tibble: 3 × 2
  Longitude Latitude
      <dbl>    <dbl>
1     -92.7     34.3
2    -116.      38.7
3     -99.8     38.8
# Extract time series using "terra::extract"
loc_pcp=terra::extract(pcp,
                       latlon,               #2-column matrix or data.frame with lat-long
                       method='bilinear')    # Use bilinear interpolation (or ngb) option
# View data sample
loc_pcp[,1:8] # View(loc_pcp)
  ID  precip_1 precip_2 precip_3 precip_4 precip_5 precip_6 precip_7
1  1 0.0000000 9.755308 46.80546 0.000000 0.000000 0.000000        0
2  2 7.3486811 0.000000  0.00000 1.276224 3.523982 1.272632        0
3  3 0.7002416 4.201450  0.00000 0.000000 0.000000 0.000000        0
# Plot hyetograph for the location in Louisiana
library(ggplot2)
pcp_df1=data.frame(time=time(pcp),
                   pcp=as.numeric(loc_pcp[1,-c(1)]))  # Select first row, exclude the first column

# ggplot
ggplot(pcp_df1,aes(x=time,y=pcp)) + 
  geom_bar(stat = 'identity')+
  theme_bw()+
  ylab("Precipitation [mm/day]")+
  xlab("Time [Days]")

# Export the extracted data as CSV
write.csv(loc, "extracted_pcp2023.csv")

8.3 Cell–wise Operation on All Layers

The terra::app() function applies a function to each cell of a raster and is used to summarize (e.g., calculating the sum) the values of multiple layers into one layer. We will use the NDVI netCDF generated in Sec 7.1.2 for the year 2016 to calculate global cellwise statistics.

#Let's look at the help section for app()
# ?terra::app

# Import NDVI layers from the NDVI generated in 7.1.2
ndviStack = rast("NDVI.nc")
ndviStack
class       : SpatRaster 
dimensions  : 456, 964, 23  (nrow, ncol, nlyr)
resolution  : 0.373444, 0.373444  (x, y)
extent      : -180, 180, -85.24595, 85.0445  (xmin, xmax, ymin, ymax)
coord. ref. : lon/lat WGS 84 (EPSG:4326) 
source      : NDVI.nc 
names       : NDVI_1, NDVI_2, NDVI_3, NDVI_4, NDVI_5, NDVI_6, ... 
unit        :    [-],    [-],    [-],    [-],    [-],    [-], ... 
# Subset raster stack/brick (notice the double [[]] bracket and similarity to lists)
ndviStack_sub=ndviStack[[c(1,3,5,10,12)]] #Select 1st, 3rd, 5th, 10th and 12th layers

# Calculate mean of each grid cell across all layers
mean_ras = app(ndviStack_sub, fun=mean, na.rm = T)

# Calculate sum of each grid cell across all layers
sum_ras = app(ndviStack_sub, fun=sum, na.rm = T)

#~~ A user-defined function for mean, variance and skewness
my_fun = function(x){ 
  meanVal=mean(x, na.rm=TRUE)              # Mean 
  varVal=var(x, na.rm=TRUE)                # Variance
  skewVal=moments::skewness(x, na.rm=TRUE) # Skewness
  output=c(meanVal,varVal,skewVal)         # Combine all statistics
  names(output)=c("Mean", "Var","Skew")    # Rename output variables
  return(output)                           # Return output
} 

# Apply user function to each cell across all layers
stat_ras = app(ndviStack_sub, fun=my_fun)

# Plot statistics
coastlines = vect("./SampleData-master/ne_10m_coastline/ne_10m_coastline.shp")
library(cetcolor)
colpal = cetcolor::cet_pal(20, name = "r2")  

plot(stat_ras, 
     col = rev(colpal), # Rev argument reverses the Palette (from Bu-> Rd to Rd -> Bu)
     asp = NA,          # Aspect ratio: NA, fill to plot space
     nc = 2,            # Number of columns to arrange plots
     fun = function(){plot(coastlines, add=TRUE)} # Add coastline boundary
)

8.4 Cell–wise Operation on Layer Groups

terra::tapp() is an extension of app(), allowing us to select a subset of layers for which we want to perform a certain operation. Let’s have the first two layers as group 1 and the next three as group 2. Function will be applied to each group separately and 2 layers of output will be generated.

#The layers are combined based on indexing.
stat_ras = terra::tapp(ndviStack_sub,
                     index=c(1,1,2,2,2),
                     fun= mean)

# Try other functions: "sum", "mean", "median", "modal", "which", "which.min", "which.max", "min", "max", "prod", "any", "all", "sd", "first".

names(stat_ras) = c("Mean_of_rasters_1_to_2", "Mean_of_rasters_3_to_5")
# Two layers are formed, one for each group of indices

# Lets plot the two output rasters
plot(stat_ras, 
     col = rev(colpal), # Rev argument reverses the Palette (from Bu-> Rd to Rd -> Bu)
     asp = 1,           # Aspect ratio: 1, i.e. X==Y
     nc = 1,            # Number of columns to arrange plots
     fun = function(){plot(coastlines, add=TRUE)} # Add coastline boundary
)

8.5 Layers as Function Arguments

The terra::lapp() function allows to apply a function to each cell using layers as arguments.

#Let's look at the help section for app()
# ?terra::lapp

#User defined function for finding difference
diff_fun = function(a, b){ return(a-b) }

diff_rast = lapp(ndviStack_sub[[c(4, 2)]], fun = diff_fun)

#Plot NDVI difference
plot(diff_rast, 
     col = rev(colpal), # Rev argument reverses the Palette (from Bu-> Rd to Rd -> Bu)
     asp = 1,           # Aspect ratio: 1, i.e. X==Y
     fun = function(){plot(coastlines, add=TRUE)} # Add coastline boundary
)