build_scripts/dygis.R

#' R6 Class for processing a catchment to make a Dynamic TOPMODEL
#' @examples
#' ## The vignettes contains more examples of the method calls.
#' 
#' ## create temport directory for output
#' demo_dir <- tempfile("dygis")
#' dir.create(demo_dir)
#'
#' ## initialise processing
#' ctch <- dynatopGIS$new(file.path(demo_dir,"test"))
#'
#' ## add digital elevation and channel data
#' dem_file <- system.file("extdata", "SwindaleDTM40m.tif", package="dynatopGIS", mustWork = TRUE)
#' dem <- raster::raster(dem_file)
#' ctch$add_dem(dem)
#' channel_file <- system.file("extdata", "SwindaleRiverNetwork.shp",
#' package="dynatopGIS", mustWork = TRUE)
#' sp_lines <- rgdal::readOGR(channel_file)
#' property_names <- c(name="identifier",endNode="endNode",startNode="startNode",length="length")
#' chn <- convert_channel(sp_lines,property_names)
#' ctch$add_channel(chn)
#'
#' ## compute properties 
#' ctch$sink_fill() ## fill sinks in the catchment
#' \donttest{
#' ctch$compute_properties() # like topograpihc index and contour length
#' ctch$compute_flow_lengths()
#' }
#' ## classify and create a model
#' \donttest{
#' ctch$classify("atb_20","atb",cuts=20) # classify using the topographic index
#' ctch$get_method("atb_20") ## see the details of the classification
#' ctch$combine_classes("atb_20_band",c("atb_20","band")) ## combine classes
#' ctch$create_model(file.path(demo_dir,"new_model"),"atb_20_band","band") ## create a model
#' list.files(demo_dir,pattern="new_model*") ## look at the output files for the model
#' }
#' ## tidy up
#' unlink(demo_dir)
#' @export
dynatopGIS <- R6::R6Class(
    "dynatopGIS",
    public = list(
        #' @description Initialise a project, or reopen an existing project
        #'
        #' @param projFile basename of data files
        #'
        #' @details This loads the project data files found at \code{projFile} if present. If not the value is stored for later use. The project data files are given by \code{projfile,<tif,shp,json>}
        #'
        #' @return A new `dynatopGIS` object
        initialize = function(projFile){
            private$apply_initialize( tools::file_path_sans_ext(projFile) )
            invisible(self)
        },
        #' @description Get project meta data
        get_meta = function(){
            private$apply_get_meta()
        },
        #' @description Import a dem to the `dynatopGIS` object
        #'
        #' @param dem a \code{raster} layer object or the path to file containing one which is the DEM
        #' @param fill_na  should NA values in dem be filled. See details
        #' @param verbose Should additional progress information be printed
        #'
        #' @details If not a \code{raster} the DEM is read in using the raster package. If \code{fill_na} is \code{TRUE} all NA values other then those that link to the edge of the dem are filled so they can be identified as sinks.
        #'
        #' @return suitable for chaining              
        add_dem = function(dem,fill_na=TRUE){
            if(!("RasterLayer" %in% class(dem))){ dem <- raster::raster(as.character(dem)) }
            if(!("RasterLayer" %in% class(dem))){ stop("dem is not a RasterLayer") }
            private$apply_add_dem(dem,fill_na)
            invisible(self)
        },
        #' @description Import channel data to the `dynatopGIS` object
        #'
        #' @param channel a SpatialLinesDataFrame, SpatialPolygonsDataFrame or file path containing the channel information
        #' @param property_names named vector of columns of the spatial data frame to use for channel properties - see details
        #' @param default_width default width of a channel if not specified in property_names. Defaults to 2 metres.
        #'
        #' @details Takes the representation of the channel network as a SpatialPolygonsDataFrame with properties name, length, area, startNode, endNode and overlaying it on the DEM. In doing this a variable called id is created (or overwritten) other variables in the data frame are passed through unaltered.
        #'
        #' @return suitable for chaining
        add_channel = function(channel){
            if(!is(channel,"SpatialPolygonsDataFrame")){ channel <- raster::shapefile( as.character(channel) ) }
            if(!is(channel,"SpatialPolygonsDataFrame")){ stop("channel is not a SpatialPolygonsDataFrame") }

            private$apply_add_channel(channel)
            invisible(self)
        },
        #' @description Add a layer of geographical information
        #'
        #' @param layer the raster layer to add (see details)
        #' @param layer_name name to give to the layer
        #'
        #' @details The layer should either be a raster layer or a file that can be read by the \code{raster} package. The projection, resolution and extent are checked against the existing project data. Only layer names not already in use (or reserved) are allowed. If successful the layer is added to the project tif file.
        #' @return suitable for chaining
        add_layer = function(layer,layer_name=names(layer)){
            layer_name <- as.character(layer_name)
            if(!("RasterLayer" %in% class(layer))){ layer <- raster::raster(as.character(layer)) }
            if(!("RasterLayer" %in% class(layer))){ stop("layer is not a RasterLayer") }
            private$apply_add_layer(layer,layer_name)
            invisible(self)
        },
        #' @description Get a layer of geographical information or a list of layer names
        #' @param layer_name name of the layer give to the layer
        #' @return a `raster` layer of the requested information if layer_name is given else a vector of layer names
        get_layer = function(layer_name=character(0)){

            ## handle case where a list of layers is requested
            if( length(layer_name) == 0 ){
                return( names(private$brk) )
            }
            ## check layer name exists
            layer_name <- match.arg(layer_name,names(private$brk))
            
            ## make raster and return
            if(layer_name=="channel"){
                return( private$shp )
            }else{
                return( private$brk[[layer_name]] )
            }
        },
        #' @description Plot a layer
        #' @param layer_name the name of layer to plot
        #' @param add_channel should the channel be added to the plot
        #' @return a plot
        plot_layer = function(layer_name,add_channel=TRUE){
            lyr <- self$get_layer(layer_name)
            raster::plot( lyr, main = layer_name)
            if( add_channel ){
                raster::plot(private$shp, add=TRUE )
            }
        },
        #' @description The sink filling algorithm of Planchona and Darboux (2001)
        #'
        #' @param min_grad Minimum gradient between cell centres
        #' @param max_it maximum number of replacement cycles
        #' @param verbose print out additional diagnostic information
        #' @param hot_start start from filled_dem if it exists
        #' @details The algorithm implemented is that described in Planchona and Darboux, "A fast, simple and versatile algorithm to fill the depressions in digital elevation models" Catena 46 (2001). A pdf can be found at (<https://horizon.documentation.ird.fr/exl-doc/pleins_textes/pleins_textes_7/sous_copyright/010031925.pdf>).
        #'
        sink_fill = function(min_grad = 1e-4,max_it=1e6,verbose=FALSE, hot_start=FALSE){
            private$apply_sink_fill(min_grad,max_it,verbose,hot_start)
            invisible(self)
        },
        #' @description Computes statistics e.g. gradient, log(upslope area / gradient) for raster cells
        #'
        #' @param min_grad gradient that can be assigned to a pixel if it can't be computed
        #' @param verbose print out additional diagnostic information
        #'
        #' @details The algorithm passed through the cells in decreasing height. Min grad is applied to all cells. It is also used for missing gradients in pixels which are partially channel but have no upslope neighbours.
        compute_properties = function(min_grad = 1e-4,verbose=FALSE){
            private$apply_compute_properties(min_grad,verbose)
            invisible(self)
        },
        #' @description Computes flow length for each pixel to the channel
        #'
        #' @param verbose print out additional diagnostic information
        #'
        #' @details The algorithm passed through the cells in increasing height. For measures of flow length to the channel are computed. The shortest length (minimum length to channel through any flow path), the dominant length (the length taking the flow direction with the highest fraction for each pixel on the path) and expected flow length (flow length based on sum of downslope flow lengths based on fraction of flow to each cell) and band (strict sequence to ensure that all contributing cell have a higher band value). By definition cells in the channel that have no land area have a length (or band) of NA.
        compute_flow_lengths = function(verbose=FALSE){
            private$apply_flow_lengths(verbose)
            invisible(self)
        }, 
        #' @description Create a catchment classification based cutting an existing layer into classes
        #' @param layer_name name of the new layer to create
        #' @param base_layer name of the layer to be cut into classes
        #' @param cuts values on which to cut into classes. These should be numeric and define either the number of bands (single value) or breaks between band (multiple values).
        #'
        #' @details This applies the given cuts to the supplied landscape layer to produce areal groupings of the catchment. Cuts are implement using \code{raster::cut} with \code{include.lowest = TRUE}. Note that is specifying a vector of cuts values outside the limits will be set to NA.
        classify = function(layer_name,base_layer,cuts){
            private$apply_classify(as.character(layer_name), as.character(base_layer), cuts)
            invisible(self)
        },
        #' @description Combine any number of classifications based on unique combinations and burns
        #' @param layer_name name of the new layer to create
        #' @param pairs a vector of layer names to combine into new classes through unique combinations. Names should correspond to raster layers in the project directory.
        #' @param burns a vector of layer names which are to be burnt on
        #'
        #' @details This applies the given cuts to the supplied landscape layers to produce areal groupings of the catchment. Burns are added directly in the order they are given. Cuts are implement using \code{raster::cut} with \code{include.lowest = TRUE}. Note that is specifying a vector of cuts values outside the limits will be set to NA.
        combine_classes = function(layer_name,pairs,burns=NULL){
            private$apply_combine_classes(as.character(layer_name),
                                          as.character(pairs),
                                          as.character(burns))
            invisible(self)
        },
        #' @description Compute a Dynamic TOPMODEL
        #'
        #' @param layer_name name for the new model and layers
        #' @param class_layer the layer defining the topographic classes
        #' @param dist_layer the layer defining the distances to the channel
        #' @param transmissivity transmissivity profile to use
        #' @param channel_solver channel solver to use
        #' @param dist_delta used in computing flow linkages see details        
        #' @param rain_layer the layer defining the rainfall inputs
        #' @param rain_label Prepended to rain_layer values to give rainfall series name        
        #' @param pet_layer the layer defining the pet inputs
        #' @param pet_label Prepended to pet_layer values to give pet series name
        #' @param verbose print more details of progress
        #'
        #' @details The \code{class_layer} is used to define the HRUs. Flow between HRUs is based on the distance to a channel. For each HRU the shortest distance to a channel is computed. Flow from a HRU can only go to a HRU with a lower shortest distance to the channel. Flow from a HRU can occur from any raster cell within the HRU whose distance to the channel is within dist_delta of the shortest distance within the HRU.
        #' Setting the transmissivity and channel_solver options ensure the model is set up with the correct parameters present.
        #' The \code{rain_layer} (\code{pet_layer}) can contain the numeric id values of different rainfall (pet) series. If the value of \code{rain_layer} (\code{pet_layer}) is not \code{NULL} the weights used to compute an averaged input value for each HRU are computed, otherwise an input table for the models generated with the value "missing" used in place of the series name.
        create_model = function(layer_name,class_layer,dist_layer,
                                transmissivity = c("exp","bexp","cnst","dexp"),
                                dist_delta=0,
                                rain_layer=NULL,rain_label=character(0),
                                pet_layer=NULL,pet_label=character(0),
                                verbose=FALSE){
            ## check valid transmissivity and channel_solver
            transmissivity <- match.arg(transmissivity)
            
            private$apply_create_model(class_layer, dist_layer,dist_delta,
                                       rain_layer, rain_label,
                                       pet_layer, pet_label,
                                       layer_name,verbose,
                                       transmissivity)

            invisible(self)
        },
        #' @description get the version number
        #' @return a numeric version number
        #' @details the version number indicates the version of the algorithms within the object
        get_version = function(){
            private$version
        },
        #' @description get the cuts and burns used to classify
        #' @param layer_name the name of layer whose classification method is returned
        #' @return a list with two elements, cuts and burns
        get_method = function(layer_name=character(0)){
            if(length(layer_name)==0){
                return( private$layers )
            }
            layer_name <- match.arg(layer_name,names(private$layers))
            return( private$layers[[layer_name]] )
        }               
    ),
    private = list(
        version = "0.3.0",
        projFiles = character(0),
        brk = character(0),
        shp = character(0),
        layers = list(),
        reserved_layers = c("dem","channel","filled_dem",
                            "gradient","upslope_area","atb",
                            "band","shortest_flow_length",
                            "dominant_flow_length","expected_flow_length"),
        writeTIF = function(){
            brk <- terra::rast(private$brk)
            terra::writeRaster(brk,private$projFiles["tif"],overwrite=TRUE)
            NULL
        },
        writeJSON = function(){
            writeLines( jsonlite::toJSON(private$layers), private$projFiles["json"] )
        },
        readJSON = function(fn){
            if(!file.exists(fn)){
                warning("No method json file found")
            }
            jsonlite::fromJSON(fn)
        },
        ## check and read the project files
        apply_initialize = function(projFile){
            ## compute and label file names
            fn <- setNames( paste0(projFile,c(".tif",".shp",".json")), c("tif","shp","json"))
            ## see if files exist
            fExists <- setNames( file.exists( fn ), c("tif","shp","json"))
            ## if no files then start a new project
            if( !any(fExists) ){
                print( paste("Starting new project at", projFile) )
                private$projFiles <- fn
                return(NULL)
            }
            ## if there is no raster data file fail
            if(!fExists["tif"]){ stop("No tif file of raster data") }
            ## read in netcdf and check
            brk <- raster::brick( fn["tif"] )
            if( !all.equal(diff(raster::res(brk)),0) | raster::isLonLat(brk) ){
                stop("tif file is not valid: Processing currently only works on projected data with a square grid")
            }
            if( !("dem" %in% names(brk)) ){ stop("No dem layer in the NetCDF file") }
            ## read in shape file and check
            if(fExists["shp"]){
                shp <- raster::shapefile( fn["shp"] )
                if(!compareCRS(shp,brk)){ stop("Shape and NetCDF projections do not match") }
                if( !("channel" %in% names(brk)) ){
                    stop("No channel layer in the NetCDF file but channel shapefile specified")
                }
            }
            if(fExists["json"]){
                mth <- private$readJSON(fn["json"])
            }
            

            private$brk <- brk
            if(fExists["shp"]){ private$shp <- shp }
            if(fExists["json"]){ private$layers <- mth }
            private$projFiles <- fn
        },
        ## adding dem
        apply_add_dem = function(dem,fill_na){
            
            if("dem" %in% names(private$brk) ){
                stop("The DEM exists, start a new project")
            }
            
            ## add to ensure NA on each edge
            dem <- extend(dem,c(1,1),NA)
            ## check the projection is OK
            if( !all.equal(diff(raster::res(dem)),0) | raster::isLonLat(dem) ){
                stop("Processing currently only works on projected dem's with a square grid")
            }
            
            ## fill dem so only NA values are on boundaries
            if(fill_na){
                ## so all catchment dem value a number
                na_clumps <- raster::clump(is.na(dem)) # clumps of na values
                edge_values <- setdiff( unique(c(na_clumps[1,],
                                                 na_clumps[nrow(na_clumps),],
                                                 na_clumps[,1],
                                                 na_clumps[,ncol(na_clumps)])),
                                       NA) #those clumps on the edge to be ignored
                na_clumps[na_clumps%in%edge_values] <- NA # set to NA to ignore
                dem[!is.na(na_clumps)] <- -1e6+1 # set to low value to indicate missing
            }
            ## covnert to brick and save
            dem <- raster::brick(dem); names(dem) <- "dem"
            private$brk <- dem #raster::brick(private$projFiles["tif"])
            private$writeTIF()
        },
        ## add the channel
        apply_add_channel = function(chn){
            if( length(private$brk) == 0 ){
                stop("Project must be initialised with the DEM")
            }
            
            if( length(private$shp) > 0 ){
                stop("Channel already added - start a new project")
            }
            
            ## check the required field names are present
            if( !all( c("name","length","area","startNode","endNode") %in% names(chn)) ){
                stop("A required property name is not specified")
            }
            
            ## check projection of the chn
            if( !compareCRS(chn,private$brk) ){
                stop("Projection of channel object does not match that of project")
            }
            
            ## check if there is an id feild which will be overwritten
            if( ("id" %in% names(chn)) ){
                warning("The name id is reserved and will be overwritten")
            }

            ## ensure required properties are of correct type
            chn$name <- as.character(chn$name)
            chn$length <- as.numeric(chn$length)
            chn$area <- as.numeric(chn$area)
            chn$startNode <- as.character(chn$startNode)
            chn$endNode <- as.character(chn$endNode)
            if( !all(is.finite(chn$length)) ){
                stop("Some non-finite values of length found!")
            }
            
            ## arrange in id in order of flow direction - so lowest values at outlets of the network
            unds <- unique(c(chn$startNode,chn$endNode)) # unique nodes
            if(any(is.na(unds))){
                stop("The nodes in startNode and endNode must have unique, non-missing codes")
            }
            ## work out number of links from a node
            nFrom <- setNames(rep(0,length(unds)),unds)
            tmp <- table(chn$startNode)
            nFrom[names(tmp)] <- tmp
            max_id <- 0
            chn[["id"]] <- NA ## set id to NA
            while( any(nFrom==0) ){
                idx <- names(nFrom[nFrom==0])
                ## fill if of reaches which are at bottom
                tmp <- chn$endNode %in% idx
                chn$id[tmp] <- max_id + (1:sum(tmp))
                max_id <- max_id + sum(tmp)
                nFrom[idx] <- -1
                ## locate next nodes that are at bootom
                tmp <- table(chn$startNode[ tmp ])
                jj <- intersect(names(tmp),names(nFrom))
                nFrom[jj] <-  nFrom[jj] - tmp[jj]
            }

            ## create a raster of channel id numbers
            ## extract cells index, and fraction of river area in cell
            chn_rst <- private$brk[["dem"]]
            chn_rst[] <- NA
            ch_cell <- raster::extract(chn_rst,chn,weights=TRUE,
                                       cellnumbers=TRUE,na.rm=TRUE)
            ch_cell <- lapply(1:length(chn),function(ii){
                out <- as.data.frame(ch_cell[[ii]][,c("cell","weight")])
                out$weight <- out$weight * chn$area[ii]
                out$id <- chn$id[ii]
                out
            })
            ch_cell <- do.call(rbind,ch_cell)
            ch_cell <- split(ch_cell,ch_cell$cell)
            ch_cell <- lapply(ch_cell,function(x){x[which.max(x$weight),,drop=FALSE]})
            ch_cell <- do.call(rbind,ch_cell)
            chn_rst[ch_cell$cell] <- ch_cell$id

            private$brk[["channel"]] <- chn_rst
            private$shp <- chn
            private$writeTIF()
            raster::shapefile(chn,private$projFiles["shp"])            
        },
        ## Add a layer
        apply_add_layer=function(layer,layer_name){
            if( any(layer_name %in% private$reserved_layers) ){
                stop("Name is reserved")
            }
            if(!compareCRS(layer,private$brk)){ stop("Projection does not match project") }
            if(!all(res(layer)==res(private$brk))){ stop("Resolution does not match project") }
            if(!(extent(layer)==extent(private$brk))){
                ## try buffering it as for dem when read in
                layer <- extend(layer,c(1,1),NA)
            }
            if(!(extent(layer)==extent(private$brk))){ stop("Extent does not match project") }
            private$brk[[layer_name]] <- layer
            private$writeTIF()
            NULL
        },
        ## Sink fill
        apply_sink_fill = function(min_grad,max_it,verbose,hot_start){
            
            rq <- ifelse(hot_start,
                         c("filled_dem","channel"),
                         c("dem","channel"))
            if(!all(rq %in% names(private$brk))){
                stop("Not all required layers are available")
            }

            d <- ifelse(hot_start,"filled_dem","dem")
            d <- values( private$brk[[d]], format="matrix" )
            ch <- values( private$brk[["channel"]], format="matrix" )
           
            ## values that should be valid
            to_be_valid <- !is.na(d) & is.na(ch)  # all values not NA should have a valid height
            is_valid <- is.finite(ch) & !is.na(d) # TRUE if a channel cell for initialisation
            changed <- is_valid # cells changed at last iteration
            fd <- to_be_valid*Inf; fd[is_valid] <- d[is_valid]
            to_eval <- d; to_eval[] <- FALSE

            ## distance between cell centres
            rs <- raster::res( private$brk )
            dxy <- matrix(sqrt(sum(rs^2)),3,3)
            dxy[1,2] <- dxy[3,2] <- rs[2]
            dxy[2,1] <- dxy[2,3] <- rs[1]
            dxy[2,2] <- 0
            dxy <- min_grad*dxy
            
            it <- 1
            ## start of iteration loop
            while(any(changed[]) & it<=max_it){

                ## work out all the the cells to evaluate
                ## should be next to those that are changed
                to_eval[] <- FALSE
                idx <- which(changed,arr.ind=TRUE) # index of changed cells
                jdx <- idx
                for(ii in c(-1,0,1)){
                    for(jj in c(-1,0,1)){
                        if(ii==0 & jj==0){next}
                        ## adjust jdx
                        jdx[,1] <- idx[,1]+ii
                        jdx[,2] <- idx[,2]+jj
                        to_eval[jdx] <- !is_valid[jdx] #TRUE
                    }
                }
                to_eval <- to_eval & to_be_valid #& !is_valid
                if(verbose){
                    cat("Iteration",it,"\n")
                    cat("\t","Cells to evaluate:",sum(to_eval),"\n")
                    cat("\t","Percentage Complete:",
                        round(100*sum(is_valid)/sum(!is.na(d)),1),"\n") #to_be_valid),1),"\n")
                }
                ## alter min value for the evaluation cells
                idx <- which(to_eval,arr.ind=TRUE) # index of changed cells
                jdx <- idx
                mind <- rep(Inf,nrow(idx))
                for(ii in c(-1,0,1)){
                    for(jj in c(-1,0,1)){
                        if(ii==0 & jj==0){next}
                        ## adjust jdx
                        jdx[,1] <- idx[,1]+ii
                        jdx[,2] <- idx[,2]+jj
                        mind <- pmin(mind,fd[jdx] + dxy[ii+2,jj+2],na.rm=TRUE)
                    }
                }
                changed[] <- FALSE
                is_valid[idx] <- d[idx]>mind ## cells where the dem value is valid
                mind <- pmax(d[idx],mind) ## mind is now the replacemnt value
                changed[idx] <- mind < fd[idx]
                fd[idx] <- mind
                
                ## mind <- pmax(d[idx],mind)
                ## cdx <- mind < fd[idx]
                ## fd[idx] <- mind
                ## changed[] <- FALSE
                ## changed[idx[cdx,,drop=FALSE]] <- TRUE
                ## is_valid[idx] <- TRUE
                ## end of loop
                it <- it+1
                
            }

            rfd <- private$brk[["dem"]]; values(rfd) <- fd
            private$brk[["filled_dem"]] <- rfd
            
            private$writeTIF()
            
            if(it>max_it){ stop("Maximum number of iterations reached, sink filling not complete") }
            
        },
        ## Function to compute the properties
        apply_compute_properties = function(min_grad,verbose){

            rq <- c("filled_dem","channel")
            if(!all( rq %in% names( private$brk) )){
                stop("Not all required input layers have been generated \n",
                     "Try running sink_fill first")
            }
            
            ## load raster layer
            ## it is quickest to compute using blocks of dem as a raster
            ## however for small rasters we will just treat as a single block
            ## assumes the raster is padded with NA
            d <- values( private$brk[["filled_dem"]], format="matrix" )
            ch <- values( private$brk[["channel"]], format="matrix" )

            ## work out order to pass through the cells
            idx <- order(d,decreasing=TRUE,na.last=NA)
            n_to_eval <- length(idx)
            
            ## distances and contour lengths
            ## distance between cell centres
            rs <- raster::res( private$brk )
            dxy <- rep(sqrt(sum(rs^2)),8)
            dxy[c(2,7)] <- rs[1]; dxy[c(4,5)] <- rs[2]
            dcl <- c(0.35,0.5,0.35,0.5,0.5,0.35,0.5,0.35)*mean(rs)
            nr <- nrow(d); delta <- c(-nr-1,-nr,-nr+1,-1,1,nr-1,nr,nr+1)
            
            ## initialise output
            gr <- upa <- atb <- d*NA
            upa[is.finite(d)] <- prod(rs) ## initialise upslope area from resolution

            it <- 1
            if(verbose){
                print_step <- round(n_to_eval/20)
                next_print <- print_step
            }else{
                next_print <- Inf
            }
            
            
            for(ii in idx){
                ngh <- ii + delta ## neighbouring cells
                ## compute gradient
                grd <- (d[ii]-d[ngh])/dxy
                
                to_use <- is.finite(grd) & (grd > 0)
                if(any(to_use)){
                    gcl <- grd[to_use]*dcl[to_use]
                    ## gradient
                    gr[ii] <- max(sum(gcl) / sum(dcl[to_use]),min_grad)
                    ## topographic index
                    atb[ii] <- log(upa[ii]/gr[ii]) #log( upa[ii] / sum(gcl) )
                    ## fraction of flow in each direction
                    frc <- gcl/sum(gcl)
                    ## propogate area downslope
                    upa[ ngh[to_use] ]  <- upa[ ngh[to_use] ] + frc*upa[ii]
                }else{
                    if( !is.finite(ch[ii]) ){
                        ## a hillslope cell that drains nowhere - this is an error
                        stop(paste("Cell",k,"is a hillslope cell with no lower neighbours"))
                    }
                }    
                
                ## verbose output here
                if(it >= next_print){
                    cat(round(100*it / n_to_eval,1),
                        "% complete","\n")
                    next_print <- next_print+print_step
                }
                
                it <- it+1
            }

            ## save raster maps
            out <- private$brk[["dem"]];
            values(out) <- gr; private$brk[["gradient"]] <- out
            values(out) <- upa; private$brk[["upslope_area"]] <- out
            values(out) <- atb; private$brk[["atb"]] <- out
            private$writeTIF()          
        },
        ## work out flow lengths to channel
        apply_flow_lengths = function(verbose){

            rq <- c("filled_dem","channel")
            if(!all( rq %in% names( private$brk) )){
                stop("Not all required input layers have been generated \n",
                     "Try running sink_fill first")
            }

            ## load raster layer
            ## it is quickest to compute using blocks of dem as a raster
            ## however for small rasters we will just treat as a single block
            ## assumes the raster is padded with NA
            d <- values( private$brk[["filled_dem"]], format="matrix" )
            ch <- values( private$brk[["channel"]], format="matrix" )
            
            ## create some distance matrices
            sfl <- d; sfl[] <- NA
            dfl <- d; dfl[] <- NA
            efl <- d; efl[] <- NA
            bnd <- d; bnd[] <- NA

            ## distances and contour lengths
            ## distance between cell centres
            rs <- raster::res( private$brk )
            dxy <- rep(sqrt(sum(rs^2)),8)
            dxy[c(2,7)] <- rs[1]; dxy[c(4,5)] <- rs[2]
            dcl <- c(0.35,0.5,0.35,0.5,0.5,0.35,0.5,0.35)*mean(rs)
            nr <- nrow(d); delta <- c(-nr-1,-nr,-nr+1,-1,1,nr-1,nr,nr+1)
            
            ## if we go up in height order then we must have looked at all lower
            ## cells first
            idx <- order(d,na.last=NA)
            
            n_to_eval <- length(idx)

            it <- 1
            if(verbose){
                print_step <- round(n_to_eval/20)
                next_print <- print_step
            }else{
                next_print <- Inf
            }

            for(ii in idx){
                if(is.finite(ch[ii])){
                    ## just channel
                    bnd[ii] <- 0
                    sfl[ii]  <- dfl[ii] <- efl[ii] <- 0
                }else{
                    ## it is not a channel
                    jdx <- ii+delta
                    gcl <- (d[jdx]-d[ii])*dcl/dxy
                    is_lower <- is.finite(gcl) & gcl<0
                    kdx <- jdx[is_lower]
                    bnd[ii] <- max(bnd[kdx])+1
                    sfl[ii] <- min(sfl[kdx]+dxy[is_lower])
                    efl[ii] <- sum((efl[kdx]+dxy[is_lower])*gcl[is_lower])/sum(gcl[is_lower])
                    kdx <- which.min(gcl)
                    dfl[ii] <- dfl[jdx[kdx]]+dxy[kdx]
                }

                ## verbose output here
                if(it >= next_print){
                    cat(round(100*it / n_to_eval,1),
                        "% complete","\n")
                    next_print <- next_print+print_step
                }
                
                it <- it+1
            }


            ## save raster maps
            out <- private$brk[["dem"]];
            values(out) <- bnd; private$brk[["band"]] <- out
            values(out) <- sfl; private$brk[["shortest_flow_length"]] <- out
            values(out) <- dfl; private$brk[["dominant_flow_length"]] <- out
            values(out) <- efl; private$brk[["expected_flow_length"]] <- out
            private$writeTIF()
        },
        
        ## split_to_class
        apply_classify = function(layer_name,base_layer,cuts){

            ## check base layer exists
            if(!(base_layer %in% names(private$brk))){
                stop(paste(c("Missing layers:",base_layer,sep="\n")))
            }

            ## check layer_name isn't already used
            if(layer_name %in% names(private$brk)){
                stop("layer_name is already used")
            }

            ## load base layer and mask out channel
            x <-  mask( private$brk[[base_layer]], private$brk[["channel"]], inverse=TRUE)

            ## work out breaks
            brk <- as.numeric(cuts)
            if( length(brk)==1 | is.na(brk) ){
                ## this defines brks in the same way as cut would otherwise
                rng <- cellStats(x,range)
                brk <- seq(rng[1],rng[2],length=brk+1)
            }
            ## cut the raster
            x <- cut(x,breaks=brk,include.lowest=TRUE)

            private$brk[[layer_name]] <- x
            private$writeTIF()
            
            private$layers[[layer_name]] <- list(
                type="classification",
                layer=base_layer,
                cuts=brk)
            private$writeJSON()
        },
        ## split_to_class
        apply_combine_classes = function(layer_name,pairs,burns){
            
            ## check all cuts and burns are in possible layers
            rq <- c(pairs,burns)
            has_rq <- rq %in% names(private$brk)
            if(!all(has_rq)){
                stop(paste(c("Missing layers:",rq[!has_rq],sep="\n")))
            }

            ## check layer_name isn't already used
            if(layer_name %in% names(private$brk)){
                stop("layer_name is already used")
            }
                
            ## work out new pairings by cantor method then renumber
            init <- TRUE
            for(ii in pairs){
                ## x <- private$brk[[ii]]
                x <-  mask( private$brk[[ii]], private$brk[["channel"]], inverse=TRUE)
                if(init){
                    cp <- x
                    init <- FALSE
                }else{
                    cp <- 0.5*(cp+x)*(cp+x+1)+x
                    uq <- sort(unique(cp))
                    cp <- cut(cp, c(-Inf,(uq[-1]+uq[-length(uq)])/2,Inf))
                }
            }

            ## put all the burns into a single raster
            brn <- cp; brn[] <- NA
            for(ii in burns){
                x <-  mask( private$brk[[ii]], private$brk[["channel"]], inverse=TRUE)
                if(is.null(brn)){
                    brn <- x
                }else{
                    brn <- cover(x,brn)
                }
            }
            ## add burns to pairs
            cp <- cover(brn,cp)
            uq <- sort(unique(cp))
            cts <- c(-Inf,(uq[-1]+uq[-length(uq)])/2,Inf)
            cp <- cut(cp,cts)
            if(length(burns)>0){ brn <- cut(brn,cts) }
            
            
            ## make table of layer values - should be able to combine with above??
            cpv <- raster::getValues(cp) ## quicker when a vector
            uq <- sort(unique(cp)) ## unique values
            uqb <- unique(brn) ## unique burn values
            
            cuq <- rep(NA,length(uq)) ##index of unique values
            for(ii in which(is.finite(cpv))){
                jj <- cpv[ii]
                if(is.na(cuq[jj])){ cuq[jj] <- ii }
            }
            
            if(!all(is.finite(cuq))){
                stop("Error in computing combinations")
            }
            
            ## create data frame
            df <- data.frame(uq); names(df) <- layer_name
            for(ii in pairs){
                df[[ii]] <- private$brk[[ii]][cuq] ## read in raster
            }
            df$burns <- df[[layer_name]] %in% uqb
            

            ## ## add in burns
            ## for(ii in burns){
            ##     x <-  mask( private$brk[[ii]], private$brk[["channel"]], inverse=TRUE)
            ##     ## x <- private$brk[[ii]] ## read in raster
            ##     idx <- Which(is.finite(x))
            ##     cp[idx] <- x[idx]

            ##     ux <- sort(unique(x))
            ##     ## ux that are alreasy layer numbers
            ##     idx <- df[,layer_name] %in% ux
            ##     df[idx,ii] <- df[idx,layer_name]
            ##     ## for ux not in df[,layer_name]
            ##     ux <- ux[!(ux %in% df[,layer_name])]
            ##     y <- matrix(NA,length(ux),ncol(df))
            ##     colnames(y) <- colnames(df)
            ##     y[,layer_name] <- y[,ii] <- ux
            ##     df <- rbind(df,y)
                
            ## }
            
            private$brk[[layer_name]] <- cp
            private$writeTIF()
            private$layers[[layer_name]] <- list(type="combination",
                                                 groups=df)
            private$writeJSON()
        },
        
        ## create a model  
        apply_create_model = function(class_lyr,dist_lyr,delta_dist,
                                      rain_lyr,rainfall_label,
                                      pet_lyr,pet_label,layer_name,verbose,
                                      transmissivity){
            browser()
            rq <- c("atb","gradient","filled_dem","channel",
                    class_lyr,dist_lyr,
                    rain_lyr,pet_lyr)
            has_rq <- rq %in% names(private$brk)            
            if(!all(has_rq)){
                stop(paste(c("Missing layers:",rq[!has_rq],sep="\n")))
            }
            if(!(class_lyr %in% names(private$layers) )){
                stop(class_lyr, " is not a classification")
            }

            model <- list()
            
            ## read in classification and distance layers
            cls <-  private$brk[[class_lyr]]## channel values are NA
            dst <- private$brk[[dist_lyr]]

            if(verbose){ cat("Initialising model","\n") }
            ## start model data frame and add minimum distance
            model$hru <- private$layers[[class_lyr]]$groups
            min_dst <- zonal(dst,cls,min) # minimum distance for each classification
            idx <- match(model$hru[[class_lyr]],min_dst[,"zone"])
            names(model$hru) <- paste0("cls_",names(model$hru))
            model$hru$min_dst <- min_dst[idx, "value"]
            ##browser()
            model$hru$zone <- min_dst[idx, "zone"]
            if(!all(is.finite(model$hru$min_dst))){
                stop("Unable to compute a finite minimum distance for all HRUs")
            }

            ## add id and change classification map to match
            model$hru <- model$hru[order(model$hru$min_dst,decreasing=TRUE),] # longest distance in first row
            model$hru$id <- (nrow(model$hru):1) + max(private$shp$id) ## ensure id is greater then number of channels
            map <- raster::subs(cls,model$hru,by="zone",which="id")

            ## add the channel data
            model$hru <- merge(model$hru,private$shp[,c("id","name","length")],by="id",all=TRUE)
            model$hru$is_channel <- model$hru$id <= max(private$shp$id)
            model$hru$min_dst[model$hru$is_channel] <- 0
            map <- cover(private$brk[["channel"]], map)


            
            
            ## add tranmissivity dependent parameters
            par <- switch(transmissivity,
                          "exp" = c(r_sfmax = Inf, c_sf = 0.1, s_rzmax = 0.05, t_d = 7200,
                                    ln_t0 = -2, c_sz = NA, m = 0.04, D= NA, m_2 = NA, omega = NA,
                                    s_rz0 = 0.75, r_uz_sz0 = 1e-6, s_raf=0.0, t_raf=Inf),
                          "bexp" = c(r_sfmax = Inf, c_sf = 0.1, s_rzmax = 0.05, t_d = 7200,
                                     ln_t0 = -2, c_sz = NA, m = 0.04, D=5, m_2 = NA, omega = NA,
                                     s_rz0 = 0.75, r_uz_sz0 = 1e-6, s_raf=0, t_raf=Inf),
                          "dexp" = c(r_sfmax = Inf, c_sf = 0.1, s_rzmax = 0.05, t_d = 7200,
                                     ln_t0 = -2, c_sz = NA, m = 0.04, D= NA, m_2 = 0.0002, omega = 0.5,
                                     s_rz0 = 0.75, r_uz_sz0 = 1e-6, s_raf=0, t_raf=Inf),
                          "cnst" = c(r_sfmax = Inf, c_sf = 0.1, s_rzmax = 0.05, t_d = 7200,
                                     ln_t0 = NA, c_sz = 0.01, m = NA, D= 10, m_2 = NA, omega = NA,
                                     s_rz0 = 0.75, r_uz_sz0 = 1e-6, s_raf=0.0, t_raf=Inf),
                          stop("Unrecognised tranmissivity")
                          )
            model$hru$opt = transmissivity
            model$hru$par <- rep(list(par),nrow(model$hru))

            
            ## work out the properties
            if(verbose){ cat("Computing properties","\n") }
            ## check sorted
            model$hru <- model$hru[order(model$hru$id),]
            if( !all( model$hru$id == 1:nrow(model$hru)) ){
                stop("id should be sequential from 1")
            }
            
            ## it is quickest to compute using blocks of a raster
            ## however for small rasters we will just treat as a single block
            d <- values( private$brk[["filled_dem"]] , format="matrix" )
            mp <- values( map , format="matrix")
            dst <- values( dst , format="matrix")
            gr <- values( private$brk[["gradient"]] , format="matrix")
            atb <- values( private$brk[["atb"]] , format="matrix")

            ## distances and contour lengths
            ## distance between cell centres
            rs <- raster::res( private$brk )
            dxy <- rep(sqrt(sum(rs^2)),8)
            dxy[c(2,7)] <- rs[1]; dxy[c(4,5)] <- rs[2]
            dcl <- c(0.35,0.5,0.35,0.5,0.5,0.35,0.5,0.35)*mean(rs)
            nr <- nrow(map); delta <- c(-nr-1,-nr,-nr+1,-1,1,nr-1,nr,nr+1)
 
            ## initialise new variables
            model$hru$area <- model$hru$width <- model$hru$atb_bar <- model$hru$s_bar <- as.numeric(0)
            model$hru$width[model$hru$is_channel] <- NA

            flux <- rep( list(numeric(0)), nrow(model$hru))
            
            ## work out order to pass through the cells
            idx <- which(is.finite(mp))
            n_to_eval <- c(length(idx),0,length(idx)/10)
            browser()
            for(ii in idx){
                #print(ii)
                id <- mp[ii] ## id
                model$hru$area[id] <- model$hru$area[id] + 1
                model$hru$atb_bar[id] <- model$hru$atb_bar[id] + atb[ii]
                model$hru$s_bar[id] <- model$hru$s_bar[id] + gr[ii]

                if( dst[ii] <= (model$hru$min_dst[id] + delta_dist) ){
                    
                    ## look for flow direction      
                    ngh <- ii + delta ## neighbouring cells
                    nghid <- mp[ngh]
                    ## compute gradient
                    grd <- (d[ii]-d[ngh])/dxy
                    ## test if can be used
                    to_use <- is.finite(grd) & (grd > 0) & is.finite(nghid) & (nghid < id)
                    
                    if( any(to_use) ){
                        nghid <- paste(nghid[to_use])
                        tmp <- nghid[!nghid %in% names(flux[[id]])]
                        if(length(tmp)>0){
                            flux[[id]][ tmp ] <- 0
                        }
                        flux[[id]][ nghid ] <- flux[[id]][ nghid ] + grd[to_use]*dcl[to_use]
                        model$hru$width[id] <- model$hru$width[id] + sum(dcl[to_use])
                    }
                }

                n_to_eval[2] <- n_to_eval[2] + 1
                if( verbose & (n_to_eval[2] > n_to_eval[3]) ){
                    cat(round(100*n_to_eval[3] / n_to_eval[1],1),
                        "% complete","\n")
                    n_to_eval[3] <- n_to_eval[3] + n_to_eval[1]/10
                }
                
            }
            model$hru$s_bar <- model$hru$s_bar / model$hru$area
            model$hru$atb_bar <- model$hru$atb_bar / model$hru$area
            model$hru$area <- model$hru$area * prod(rs)

            ## check fluxes are valid and convert to form sz_flux
            nlink <- sapply(flux,length)
            idx <- which(nlink==0)
            if( any(idx %in% model$hru$id[!model$hru$is_channel]) ){
                    stop( "The following HRUs have no valid outflows and are not channels: \n",
                         paste(idx[ idx %in% model$hru$id[!model$hru$is_channel] ], collapse=", "))
            }
            for(ii in 1:length(flux)){
                if(nlink[ii]>0){
                    flux[[ii]] <- data.frame(from = as.integer(ii),
                                             to = as.integer(names(flux[[ii]])),
                                             frc = flux[[ii]]/sum(flux[[ii]]))
                }
            }
            
            model$sz_flow_direction <- do.call(rbind,flux)

            ## alter channel flux for surface
            if(verbose){ cat("Creating channel flow directions","\n") }
            n_to_eval <- c(length(private$shp$id),0,length(private$shp$id)/10)
            for(ii in private$shp$id){
                idx <- model$channel$startNode == model$channel$endNode[ii]
                if(any(idx)){
                    flux[[ii]] <- data.frame(from = as.integer(ii),
                                             to = as.integer( private$shp$id[idx] ),
                                             frc = rep(1/sum(idx),sum(idx)))
                }else{
                    flux[[ii]] <- NULL
                }

                
                n_to_eval[2] <- n_to_eval[2] + 1
                if( verbose & (n_to_eval[2] > n_to_eval[3]) ){
                    cat(round(100*n_to_eval[3] / n_to_eval[1],1),
                        "% complete","\n")
                    n_to_eval[3] <- n_to_eval[3] + n_to_eval[1]/10
                }

            }

            ## create surface flux table
            model$sf_flow_direction <- do.call(rbind,flux)

            ## ############################################
            ## Add outlet at all outlets from river network
            ## ############################################
            idx <- model$hru$id[ !(model$hru$id %in% model$sf_flow_direction$from) ]
            model$output_flux<- data.frame(id = idx, flux = "q_sf", frc = 1)
            model$time_series <- data.frame(
                name = paste("channel",idx,sep="_"),
                id = idx,
                flux = "q_sf"
            )
            
            ## ##################################
            ## Add point inflow table
            ## ##################################
            ## blank point inflow table
            if(verbose){ cat("Adding a blank point_inflow table","\n") }
            model$input_flux<- data.frame(
                name = character(0),
                id = integer(0),
                state = character(0),
                frc = numeric(0)
            )
            
            ## ##################################
            ## Compute precip weights
            ## ##################################
            if(verbose){ cat("Computing rainfall weights","\n") }
            if( is.null(rain_lyr) ){
                model$precip_input <- data.frame(
                    name = "precip",
                    id = model$hru$id,
                    frc = as.numeric(1) )
            }else{
                tmp <- crosstab(map,private$brk[[rain_lyr]],long=TRUE)
                names(tmp[[ii]]) <- c("id","name","cnt")                
                tmp <- tmp[order(tmp$id),]
                tmp$id <- as.integer(tmp$id)
                tmp$name <- paste0(rainfall_label,tmp$name)
                ## tmp is ordered by id so the following returns correct order
                tmp$frc <- unlist(tapply(tmp$cnt,tmp$id,FUN=function(x){x/sum(x)}),use.names=FALSE)
                ## set
                model$precip_input <- tmp[,c("id","name","frc")]
            }
            
            ## #################################
            ## Comnpute PET weights
            ## #################################
            if(verbose){ cat("Computing the pet weights","\n") }
            if( is.null(pet_lyr) ){
                model$pet_input <- data.frame(
                    name = "pet",
                    id =  model$hru$id,
                    frc = as.numeric(1) )
            }else{
                tmp <- crosstab(map,private$brk[[pet_lyr]],long=TRUE)
                names(tmp[[ii]]) <- c("id","name","cnt")                
                tmp <- tmp[order(tmp$id),]
                tmp$id <- as.integer(tmp$id)
                tmp$name <- paste0(pet_label,tmp$name)
                ## tmp is ordered by id so the following returns correct order
                tmp$frc <- unlist(tapply(tmp$cnt,tmp$id,FUN=function(x){x/sum(x)}),use.names=FALSE)
                ## set
                model$pet_input <- tmp[,c("id","name","frc")]
            }
            
            ## ############################
            ## save model
            
            brk <- raster::brick(map); names(brk) <- "hru"
            brk[["class"]] <- private$brk[[class_lyr]]
            brk[["channel"]] <- private$brk[["channel"]]
            if( !is.null(pet_lyr) ){ brk[["pet"]] <- private$brk[[pet_lyr]] }
            if( !is.null(rain_lyr) ){ brk[["precip"]] <- private$brk[[rain_lyr]] }
            brk <- terra::rast(brk)
            terra::writeRaster(brk,paste0(layer_name,".tif"),overwrite=TRUE)
            saveRDS(model,paste0(layer_name,".rds"))
        }        
    )
    )
waternumbers/dynatopGIS documentation built on May 11, 2024, 3:04 a.m.