Below is code for a simple inundation model. Sometimes called a bathtub model, this analysis estimates the extent and volume of inundation based on (1) specified elevation, (2) digital elevation model, and (3) location of depressional wetland. The analysis is loosely based on Jones et al., [2018] and the Topographic Depression Identification Model (TDI).
Normally, you would just read in a DEM for the analysis. However, for the purposes of this tutorial, we will make our own DEM with depresional wetlands. [So, just run this code. Don't worry too much about the contents for now!]
As always, setup your workspace and download appropriate packages.
#Clear Memory rm(list=ls(all=TRUE)) #Download packages library(raster) library(sp) library(rgdal) library(rgeos) library(actuar) library(poweRlaw) library(dplyr)
Define variables that describe the synthetic landscape
#lanscape size (Note, it will be a square) area<-1000 #ha #Size of cell_size cell (e.g. length of one side) cell_size<-3 #m #wetland depth invert<-1 #m #landscape slope slope<-0.001 #m/m
Create raster that using variables listed above
#Convert units to meters area<-area*10000 #convert to m^2 #Define coordinates x<-matrix(0, nrow=sqrt(area)/cell_size, ncol=sqrt(area)/cell_size) y<-matrix(0, nrow=sqrt(area)/cell_size, ncol=sqrt(area)/cell_size) z<-matrix(0, nrow=sqrt(area)/cell_size, ncol=sqrt(area)/cell_size) #Add coordinates for(i in 1:(sqrt(area)/cell_size)){ x[,i]<-i*cell_size-(cell_size/2) y[i,]<-i*cell_size-(cell_size/2) } #Add elevation data (valley slope) interp<-approxfun(data.frame(c(1,sqrt(area)/cell_size), c(100, 100-slope*sqrt(area)))) for(i in 1:(sqrt(area)/cell_size)){z[i,]<-interp(i)} #Add natural variation z_var<-matrix(rnorm(length(z), mean=1, sd=0.001), nrow=sqrt(area)/cell_size, ncol=sqrt(area)/cell_size) z<-z*z_var #Create Raster dem <-raster(z, xmn=range(x)[1], xmx=range(x)[2], ymn=range(y)[1], ymx=range(y)[2] ) #clean up workspace remove(list=c("x","y","z","i","slope"))
#Determine number, size, and location of wetlands~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #determine number and size of wet wetland_area<-area*0.10 #From Van Meter and Basu 2015 (Des Moines Lobe) #Iterate through wetland scenarios until correct area set.seed(1) n.wetlands<-10 pnts<-rep(wetland_area/n.wetlands, n.wetlands) #Randomly select location of each wetland pnts<-data.frame(coordinates(sampleRandom(dem,n.wetlands, sp=T)), pnts) colnames(pnts)<-c("x","y", "area_m2") #add wetid data pnts<-pnts[order(-pnts$area_m2),] pnts$WetID<-seq(1,length(pnts[,1])) #calculate wetland volume and depth p<-4 #shape factor from Hayashi and Kamp [200] pnts$volume_m3<-(0.25*(pnts$area_m2/10000)^1.4742)*10000 #Equation from Wu and Lane [2016] pnts$max_depth_m<-(pnts$volume_m3*(1+2/p))/pnts$area_m2 #add z data and create shp pnts$z<-raster::extract(dem, pnts[,c("x","y")]) #add data about the watershed depth pnts$ws_depth<-abs(rnorm(nrow(pnts),2,0.5)) #Create Function to add wetland shape (with bathymetry)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ fun<-function(WetID){ #Setup~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #define variables area_max<-pnts$area_m2[pnts$WetID==WetID] x<-pnts$x[pnts$WetID==WetID] y<-pnts$y[pnts$WetID==WetID] z<-pnts$z[pnts$WetID==WetID] ws_depth<-pnts$ws_depth[pnts$WetID==WetID] depth<-pnts$max_depth_m[pnts$WetID==WetID] #create circle function to define circle circle.fun<-function(area, x, y){ radius<-(area/pi)^.5 circle<-seq(0, 2 * pi, length.out = 2*pi*sqrt(area/pi)) circle<-cbind(x + radius * sin(circle), y + radius * cos(circle)) circle } #Represent bathymetry with points~~~~~~~~~~~~~~~~~~~~~~~~ #Calculate variables radius<-(area_max/pi)^0.5 n<-round(radius/cell_size) #create dataframe to house points bath<-data.frame(matrix(0, ncol=3)) colnames(bath)<-c("x","y","z") bath$x[1]<-x bath$y[1]<-y bath$z[1]<-z-depth #Add "rings" to bath df for(i in 1:(n+1)){ area_ring<-pi*(i*cell_size)^2 df<-data.frame(circle.fun(area_ring, x,y),depth*(i*cell_size/radius)^p+(z-depth)) colnames(df)<-c("x","y","z") bath<-rbind(bath,df) } #Adjust elevation for watershed invert elevation bath$z<-bath$z-ws_depth #Represent watershed surface~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Calculate variables ws_radius<-(area_max*10/pi)^0.5 m<-round((ws_radius-radius)/cell_size) #create dataframe to house points ws<-data.frame(matrix(0, ncol=3)) colnames(ws)<-c("x","y","z") #Add "rings" to ws df for(i in 0:m){ area_ring<-pi*(i*cell_size+radius)^2 df<-data.frame(circle.fun(area_ring, x,y),ws_depth/(ws_radius-radius)*(i*cell_size)+(z-ws_depth)) colnames(df)<-c("x","y","z") ws<-rbind(ws,df) } ws<-ws[-1,] #Create raster of Wetland Bathymetry~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #Conbime bathymetry and ws surface bath<-rbind(bath,ws) #Remove points that are > current dem surface bath$dem<-raster::extract(dem,bath[,1:2]) bath<-bath[bath$z<bath$dem,] #If any points are left: if(nrow(bath)>1){ #Create gird wetland.grd<-rasterize(bath[,1:2], dem, bath[,3]) #Create Clip clip<-wetland.grd*0 clip[is.na(clip)] <- 1 wetland.grd[is.na(wetland.grd)] <- 0 #append dem dem<-dem*clip+wetland.grd #assign dem to the global environment assign('dem', dem, envir = .GlobalEnv) } } #Run function for(i in 1:length(pnts[,1])){fun(i)} #Smooth with raster filter dem<-focal(dem, w=matrix(1/25,nrow=5,ncol=5))
Below is a plot of the synthetic landscape we will use for delineation today!
zData<-as.matrix(dem) x = (cell_size * (1:nrow(zData))) y = (cell_size * (1:ncol(zData))) nrzmat <- nrow(zData) nczmat <- ncol(zData) facetValues <- (zData[-1, -1] + zData[-1, -nczmat] + zData[-nrzmat, -1] + zData[-nrzmat, -nczmat])/4 nbcol <- 99 color <- c("grey",terrain.colors(nbcol)) facetcol <- cut(facetValues, nbcol+1) res = persp(x, y, z = zData*50, theta = 120, phi = 45, col = color[facetcol], scale = FALSE, expand = 0.75, ltheta = 75, shade = 0.75, border = NA, box = F, ticktype = "detailed")
Now that we have created our synthetic landscape, we can seperate it into discrete endorheic (i.e., internally draining) basins. This step requires the use of the RPyGeo package. It's worth noting, this package has to be run on a windows machine, requires both ArcGIS and Python, and require read/write access to the hard drive. For more information, read the documentation here.
Here, you will need to supply both path to python path and a seperate workspace. Note, the example below was configured for use on the SESYNC WinAnalytics Virtual machine. These paths will vary depending on your machine and specific access priveledges.
#Read RPyGeo Library require(RPyGeo) #Create python environment py.env<-rpygeo.build.env(python.path = "C:\\Python27\\ArcGIS10.2\\", workspace = "C:\\ScratchWorkspace\\", overwriteoutput = 1) #Set working directory to scratchworkspace setwd("C:\\ScratchWorkspace\\")
Here, you will pass files between the R and ArcGIS environments. When using Raster package functions (e.g., focal), the raster will need to be in the R environment. However, if you need to use ArcGIS geoprocessing tools, the raster has to be exported to the predefined workspace before completing the ArcGIS task. Once the new ArcGIS tasks has been completed, the updated file will then need to be read BACK into the R environment.
Cleare as mud right? See steps below:
#Convert DEM to planar coordinates dem@crs<-CRS("+proj=utm +zone=17 +ellps=WGS84 +datum=WGS84 +units=m +no_defs") #Note, in this case of this tutorial, we just simply assigned a planar coordinate system. #In real life, you will want to reproject using projectRaster() #Filter the DEM dem_filter.grd<- focal(dem, w=focalWeight(dem, 5, "Gauss")) dem_filter.grd<- focal(dem_filter.grd, w=focalWeight(dem, 5, "Gauss")) dem_filter.grd<- focal(dem_filter.grd, w=focalWeight(dem, 5, "Gauss")) dem_filter.grd@crs<-dem@crs writeRaster(dem_filter.grd, file="C:\\ScratchWorkspace\\dem_filter.asc", overwrite=T) #Fill sinks (up to 0.1m) rpygeo.geoprocessor(fun="Fill_sa", c("dem_filter.asc", "dem_fill", 0.3048/2), env=py.env) #Compute Flow Direction rpygeo.FlowDirection.sa("dem_fill","fdr_esri", env=py.env) #Identify Sink rpygeo.Sink.sa("fdr_esri", "sink", env=py.env) #Run basin tool rpygeo.geoprocessor(fun="Basin_sa", c("fdr_esri","basin"), env=py.env) #Convert sink and basin to polygons rpygeo.geoprocessor(fun="RasterToPoint_conversion", c("sink", "sink.shp", "VALUE"), env=py.env) rpygeo.geoprocessor(fun="RasterToPolygon_conversion", c("basin", "basin.shp", "NO_SIMPLIFY","VALUE"), env=py.env) #Bring basin shapes back into the R environment basin.shp<-readOGR("C:\\ScratchWorkspace\\.","basin") #For the sake of this exercise, lets remove non-wetland basins basin.shp<-basin.shp[rank(gArea(basin.shp, byid=T))>(length(basin.shp)-10), ]
First, we will create a function to estimate storage capacity for each basin based on wetland stage.
inundate.fun<-function(n){ #Select basin temp.shp<-basin.shp[n,] #Convert to raster res<-res(dem)[1] ext<-raster(extent(temp.shp), res=res) temp.grd<-rasterize(temp.shp, ext, field=1) temp.grd<-temp.grd*dem #Create Minimum Raster temp_min.grd<-temp.grd*0+minValue(temp.grd) #Create function to return conditional raster Con<-function(condition, trueValue, falseValue){ return(condition * trueValue + (!condition)*falseValue) } #Create function to calculate inundation area/volume inundate<-function(z){ area<-Con(temp.grd>(temp_min.grd+z),0,1) volume<-(((z+temp.grd)-temp_min.grd)*area)*res(area)[1]*res(area)[2] outflow<-cellStats(area*boundaries(temp_min.grd, type="inner"), 'sum') c(cellStats(area, 'sum')*res(area)[1]*res(area)[2], #area (m^2) cellStats(volume, 'sum'), #volume (m^3) outflow #Outflow length (3 m increments) ) } #Conduct inundation calculation and store results in df df<-c(n, #Unique identifer minValue(temp.grd), #Minimum Elevation xyFromCell(temp.grd,max(which.min(temp.grd)))[1], #X Value xyFromCell(temp.grd,max(which.min(temp.grd)))[2], #Y Value gArea(temp.shp), #area of shpape c(t(sapply(seq(0.1,3,0.1),inundate))) #Area, Volume, and Spill Area ) #print dataframe df }
Execute the function for the 10 wetlands!
df<-lapply(seq(1,10),inundate.fun) df<-data.frame(do.call(rbind, df)) colnames(df)<-c("basin.id", "min_elevation", "long", "lat", "basin_area", paste0("area_",seq(0.1,3,0.1)), paste0("volume_",seq(0.1,3,0.1)), paste0("spill_length",seq(0.1,3,0.1)))
Now, determine storage capacity based on spill boundary threshold. [Note, here we aren't goign to worry about ditches - ie potential storage capacity -- because we didn't insert ditches in to the synthetic landscape model.]
#Create function to estimate spill elevation spill.fun<-function(n){ #Gather overflow data df<-df[n,] df<-data.frame(seq(0.1,3, 0.1), as.numeric(paste(df[6:35])), as.numeric(paste(df[36:65])), as.numeric(paste(df[66:95]))) colnames(df)<-c("z","area", "volume", "spill") #Define change in spill length df$ds<-0 df$ds[1]<-0 df$ds[2:30]<-df$spill[2:30]-df$spill[1:29] #Print "spill" events #If no spill <2m if(sum(df$spill, na.rm=T)==0){ c(n,df$z[30], df$area[30], df$volume[30],df$z[30], df$area[30], df$volume[30]) }else{ #If no change in spill >=3 if(max(df$ds,na.rm=T)<3){ #If max spill length less than 3m if(max(df$spill, na.mr=T)>=3){ c(n, #Basin ID min(df$z[df$ds>=1]), #depth of ditch min(df$area[df$ds>=1]), #area of ditch min(df$volume[df$ds>=1]), #volume of ditch min(df$z[df$spill>3]), #depth of wetland min(df$area[df$spill>3]), #area of wetland min(df$volume[df$spill>3])) #volume of wetland }else{ c(n, #Basin ID min(df$z[df$ds>=1]), #depth of ditch min(df$area[df$ds>=1]), #area of ditch min(df$volume[df$ds>=1]), #volume of ditch min(df$z[df$ds>=1]), #depth of wetland min(df$area[df$ds>=1]), #area of wetland min(df$volume[df$ds>=1])) #volume of wetland } }else{ #If change in spill >3 c(n, #Basin ID min(df$z[df$ds>=1]), #depth of ditch min(df$area[df$ds>=1]), #area of ditch min(df$volume[df$ds>=1]), #volume of ditch min(df$z[df$ds>=3]), #depth of wetland min(df$area[df$ds>=3]), #area of wetland min(df$volume[df$ds>=3])) #volume of wetland } } } #Execute the function output<-lapply(seq(1,10),spill.fun) output<-data.frame(do.call(rbind, output)) colnames(output)<-c("n", "d_ditch", "a_ditch", "v_ditch", "d_wetland", "a_wetland", "v_wetland") #Merge with master dataframe df<-merge(df, output, by.x='basin.id', by.y='n')
Finally, the step you've been waiting for! Let's plot this bad boy!
Well, not so fast. First we have to create an output raster
#Create function to create inundatoin raster for each basin inundate.fun<-function(n){ #Select basin temp.shp<-basin.shp[n,] #Convert to raster res<-res(dem)[1] ext<-raster(extent(temp.shp), res=res) temp.grd<-rasterize(temp.shp, ext, field=1) temp.grd<-temp.grd*dem #Create Minimum Raster temp_min.grd<-temp.grd*0+minValue(temp.grd) #Create function to return conditional raster Con<-function(condition, trueValue, falseValue){ return(condition * trueValue + (!condition)*falseValue) } #Create function to calculate inundation area/volume depth<-df$d_wetland[n] inundation<-Con(temp.grd>(temp_min.grd+depth),0,1) #Change inundation extent inundation<-crop(extend(inundation, dem), dem) inundation[is.na(inundation)]<-0 #Export Inundation Raster inundation } #Now, use a loop and raster algebra to compline inundation for all 10 basins wetland<-dem*0 for(i in 1:10){ print(i) temp<-inundate.fun(i) wetland<-wetland+temp } wetland[wetland==0]<-NA
plot(dem) plot(basin.shp, add=T) plot(wetland, col="blue", add=T)
For more infomration, contact Nate Jones (njones@sesync.org)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.