R/DENDRO_BASE_FUNCTIONS.R

Defines functions max.growth.rate max.growth.day lg5.deriv get.QH.resid .lg5.QH .lg5.ML.a pred.doy .get.lg5.resids .get.lg5.ML.wt .get.lg5.ML lg5.pred gap2dbh .difdendro gettruedbh get.alt.a fit.quantile.hull get.extra.metrics get.params .make.adjust get.optimized.dendro

Documented in fit.quantile.hull gap2dbh get.alt.a get.extra.metrics get.optimized.dendro get.params get.QH.resid gettruedbh lg5.deriv lg5.pred max.growth.day max.growth.rate pred.doy

###################################################
###  functions for Intensive dendrometer workflow
###################################################

#############################
#############################

#' Fit the LG5 function
#'
#' @param INPUT.data A data.frame of a specific form (see @details)
#' @param no.neg.growth logical denoting whether to skip any time series with negative trends (from a call to \emph{lm()})
#' @param cutoff Integer denoting the minimum sample size required to perform the optimization call.
#' @param unit Character string denoting the unit that the dbh is in. Defaults to "cm".
#' @param par.names  A character vector of the column names of the \emph{params} vector argument.
#' This is used to pull out actual parameters from other information.
#' @param OUTPUT.folder Character string for the target output folder.
#' @param param.table.name Character string for the parameter table output.
#' @param Dendro.data.name Character string for the data object, Dendro.complete, that is a complete data.frame table output.
#' @param Dendro.split.name Character string for the data object, Dendro.split, that is a list vector where
#' every entry is a separate time series of a band on a stem in a year.
#'
#' @description This is the wrapper for \emph{get.params()} which uses base \emph{optim()} (with two sequential methods) to
#' estimate the parameters of the logistic function. This follows McMahon and Parker 2014. This function organizes output,
#' skips removed datasets, fits a linear model to detect negative or non-significant growth, eliminates time series with
#' small sample sizes, and removes identified outliers (see @identify.outliers ).
#' @return Objects Saves four files to the OUTPUT folder as named above and collected stems.
#' @export
get.optimized.dendro <- function(INPUT.data,
  no.neg.growth = FALSE, cutoff = 4, unit = "cm",
  par.names = c("L", "K", "doyip", "r", "theta", "a", "b", "r.squared", "ts.sd"),
  OUTPUT.folder = ".",
  param.table.name = "Param_table.csv",
  Dendro.data.name = "Dendro_data.Rdata",
  Dendro.split.name = "Dendro_data_split.Rdata") {

  options(warn = -1)

  
  TREE.ID.YR <- paste(INPUT.data$TREE_ID, INPUT.data$BAND_NUM, 
    INPUT.data$YEAR, sep = "_")
  TREE.SITE.ID <- paste(as.character(INPUT.data$SITE),
    as.character(INPUT.data$TREE_ID), sep = "_")
  TS.UNIT.ix <- paste(INPUT.data$SITE, INPUT.data$TREE_ID, INPUT.data$YEAR)
  
  Dendro.split <- vector("list", length = length(unique(INPUT.data$UNIQUE_ID)))
  ind.dendro <- split(INPUT.data, f = INPUT.data$TREE_ID, drop = TRUE)

  n.obs <- length(ind.dendro)

  param.table <-c()
  pb <- txtProgressBar(style = 3)
  Dendro.tree <- vector("list", length(ind.dendro))
  ct <- 1
  for(i in 1:n.obs) {
    setTxtProgressBar(pb, i / n.obs, title = NULL, label = NULL)
    ind.data <- ind.dendro[[i]] # loads an individual (multiple years)
    if(nrow(ind.data) < cutoff) next
    low.band <- min(ind.data$BAND_NUM)
    ind.data$BAND_NUM <- ind.data$BAND_NUM - low.band + 1 

    ind.data$ORG_DBH[1] <- ind.data$ORG_DBH[which(!is.na(ind.data$ORG_DBH))[1]]
    # band.index <- as.numeric(table(ind.data$BAND_NUM))
    # ind.data$BAND_NUM <- unlist(mapply(rep, seq(length(band.index)), 
    #   length.out = band.index))
    ind.data$DBH_TRUE[1] <- ind.data$ORG_DBH[which(!is.na(ind.data$ORG_DBH))[1]]
  
    for(v in 2:length(ind.data$GAP_WIDTH)) {
      ind.data$DBH_TRUE[v] <- gettruedbh(gw1 = ind.data$GAP_WIDTH[v - 1],
        gw2 = ind.data$GAP_WIDTH[v], dbh1 = ind.data$DBH_TRUE[v - 1], unit = unit)
    }

    if(max(ind.data$BAND_NUM) > 1) {

      # FIX NEW BAND ISSUE
      nb.index <- which(!duplicated(ind.data$BAND_NUM))[-1]

      ind.data.band <- split(ind.data, ind.data$BAND_NUM)

      # NOW GET ALL OF THE CORRECT "DBHs" for all bands
      for(b in 2:length(ind.data.band)) {
        year.nb <- ind.data.band[[b]]$YEAR[1]
        year.ob <- ind.data.band[[b - 1]]$YEAR[1]
        old.band.data <- subset(ind.data.band[[b - 1]], YEAR == year.nb)
        if (dim(old.band.data)[1] != 0) {
         doy.band <- max(which(old.band.data$DOY <= ind.data.band[[b]]$DOY[1]))
         if(doy.band < 0) {
           ind.data.band[[b]]$DBH_TRUE[1] <- old.band.data$DBH_TRUE[1]
         } else {
           ind.data.band[[b]]$DBH_TRUE[1] <- old.band.data$DBH_TRUE[doy.band]
         }
       } else {
         ind.data.band[[b]]$DBH_TRUE[1] <-
         ind.data.band[[b - 1]]$DBH_TRUE[nrow(ind.data.band[[b - 1]])]
       }

       if(nrow(ind.data.band[[b]]) < 2) next #for case with a band with one measurement

       for(v in 2:nrow(ind.data.band[[b]])){
        ind.data.band[[b]]$DBH_TRUE[v] <- gettruedbh(gw1 = ind.data.band[[b]]$GAP_WIDTH[v - 1],
          gw2 =  ind.data.band[[b]]$GAP_WIDTH[v], dbh1 = ind.data.band[[b]]$DBH_TRUE[v - 1],
          unit = unit)
      }

    }
    ind.data <- unsplit(ind.data.band, ind.data$BAND_NUM)
  }

  Dendro.tree[[i]] <- ind.data
  ind.year.band <- split(ind.data,
    f = list(YEAR = ind.data$YEAR, BAND_NUM = ind.data$BAND_NUM), drop = TRUE)

  param.mat <- matrix(NA, length(ind.year.band), length(par.names))
  years <- vector("integer", length(ind.year.band))
  band.no <- vector("integer", length(ind.year.band))

  for(t in 1:length(ind.year.band)) {
    params <- rep(NA, 7)
    r.squared <- -99
    ts.data.tmp <- ind.year.band[[t]]
    if(any(ts.data.tmp$ADJUST == 1)) {
      ts.data.tmp <- .make.adjust(ts.data.tmp)
    }
    ts.data <- subset(ts.data.tmp, REMOVE == 0)
    ts.sd <- sd(ts.data$DBH_TRUE, na.rm = TRUE)
    years[t] <- ts.data$YEAR[1]
    band.no[t] <- ts.data$BAND_NUM[1]

    if (sum(!is.na(ts.data$DBH_TRUE)) < cutoff | any(ts.data$SKIP == 1)) {
      param.mat[t, ] <- c(params, r.squared, ts.sd)
      ct <- ct + 1
      next
    }

    # if (no.neg.growth == TRUE) {
    #   lm.out <- coef(lm(ts.data$DBH_TRUE ~ ts.data$DOY ))
    #   if(as.numeric(lm.out[2]) < 0) {
    #     ts.data$SKIP[1] <- 1
    #     param.mat[t, ] <- c(params, r.squared, ts.sd)
    #     ct <- ct + 1
    #     next
    #   }
    # }
    params <- get.params(ts.data)
    r.squared <- summary(lm(ts.data$DBH_TRUE ~ lg5.pred.a(params[6:7],
      params, doy = ts.data$DOY)))$r.squared
    param.mat[t, ] <- c(params, r.squared, ts.sd)
    Dendro.split[[ct]] <- ts.data
    ct <- ct + 1
  }
  param.tab.tmp <- data.frame(SITE = ts.data$SITE[1],
    YEAR = years, TREE_ID = ts.data$TREE_ID[1],
    BAND_NUM = band.no, UNIQUE_ID = ts.data$UNIQUE_ID[1],
    SP = ts.data$SP[1], param.mat)

  param.table <- rbind(param.table, param.tab.tmp)

}
  close(pb)
  Dendro.split <- Dendro.split[1:(ct - 1)]
  par.ind <- grep("X", names(param.table))
  names(param.table)[par.ind] <- par.names ## MAKE SURE THIS IS RIGHT ALWAYS
  alt.a <- get.alt.a(param.table)
  param.table$alt.a <- alt.a
  Dendro.complete <- do.call(rbind, Dendro.tree)
  write.csv(param.table, file = paste(OUTPUT.folder, param.table.name, sep = "/"),
    quote = FALSE, row.names = FALSE)
  save(Dendro.complete, file = paste(OUTPUT.folder, Dendro.data.name, sep = "/"))
  save(Dendro.split, file = paste(OUTPUT.folder, Dendro.split.name, sep = "/"))
  save(Dendro.tree, file = paste(OUTPUT.folder, "Dendro_Tree.Rdata", sep = "/"))
}

.make.adjust <- function(ts.data) {
  which.adjust <- c(which(ts.data$ADJUST == 1), (nrow(ts.data) + 1))
  which.adjust.dbh <- cumsum(ts.data$DBH_TRUE[which.adjust] - ts.data$DBH_TRUE[which.adjust - 1])
  for(r in 1:(length(which.adjust) - 1)) {
    ts.data$DBH_TRUE[which.adjust[r] : (which.adjust[r + 1] - 1)] <-
    ts.data$DBH_TRUE[which.adjust[r] : (which.adjust[r + 1] - 1)] - which.adjust.dbh[r]
  }
  return(ts.data)
}


#' Estimates the maximum likelihood parameters for the 5-parameter logsitic function fit to dendrometer band data.
#'
#' @param ts.data  A dataframe of a time series of a single tree in a year.
#' Must have column variables \emph{DBH_TRUE} (numeric) and \emph{DOY} (integer), as well as other designations.
#' @description This is the core fitting algorithm, which uses base \emph{optim()} (with two sequential methods) to
#' estimate the parameters of the logistic function. This follows McMahon and Parker 2014.
#' @return Returns a numeric vector of parameters
#' @export
get.params <- function(ts.data) {
  dbh      <- ts.data$DBH_TRUE
  doy      <- as.integer(ts.data$DOY)
    # date     <- Dates(ts.data$DATE, "%m/%d/%y") # need to fix this for plots
  complete <- complete.cases(dbh, doy)
  dbh      <- as.numeric(dbh[complete])
  doy      <- as.numeric(doy[complete])

  doy.ip.hat <- doy[(which(dbh > mean(dbh)))[1]] # gets the doy for the mean (50%) growth

  optim.min <- c((min(dbh, na.rm = TRUE) * 0.99),
    quantile(dbh, 0.5, na.rm = TRUE), -50, 0, 0.01)

  optim.max <- c(min(dbh, na.rm = TRUE), max(dbh, na.rm = TRUE),
    350, 0.1, 15)
  resid.sd <- 0.02

  par.list <- list(L = min(dbh, na.rm = TRUE), K = max(dbh, na.rm = TRUE),
    doy.ip = doy.ip.hat, r = .08, theta = 1)
  params.start <- as.numeric(unlist(par.list))
  params <- params.start

  ##  THESE ARE THE CALLS TO OPTIM  ##
  # weighted values have false ML estimates, so the estimate is re-assessed based on the optimized parameters in an unweighted call

  lg5.output.LB <- optim(par = params, doy = doy, dbh = dbh,
    resid.sd = resid.sd, fn = .get.lg5.ML, method = "L-BFGS-B",
    lower = optim.min, upper = optim.max,
    hessian = FALSE, control = list(trace = 0))
  params <- lg5.output.LB$par

  lg5.output.LB.wt <- optim(par = params, doy = doy, dbh = dbh,
    resid.sd = resid.sd, fn = .get.lg5.ML.wt, method = "L-BFGS-B",
    lower = optim.min, upper = optim.max, hessian = TRUE,
    control = list(trace = 0))
  lg5.output.LB.wt$value <- .get.lg5.ML(params = lg5.output.LB.wt$par, doy, dbh,
    resid.sd = resid.sd)
  lg5.output.NM.wt <- optim(par = params, fn = .get.lg5.ML.wt, resid.sd = resid.sd,
    method = "Nelder-Mead", hessian = TRUE, control = list(trace = 0),
    doy = doy, dbh = dbh)
  lg5.output.NM.wt$value <- .get.lg5.ML(lg5.output.NM.wt$par, doy, dbh,
    resid.sd = resid.sd)

  ## CONSOLIDATE THE RESULTS  ##

  optim.output <- rbind(
    c(params.start, NA),
    # c(lg5.output.LB$par, lg5.output.LB$value),
    c(lg5.output.LB.wt$par, lg5.output.LB.wt$value),
    # c(lg5.output.NM$par, lg5.output.NM$value),
    c(lg5.output.NM.wt$par, lg5.output.NM.wt$value)
    # c(lg5.output.BFGS$par, lg5.output.BFGS$value)
  )

  winner.int <- 1 + match(min(optim.output[-1, 6], na.rm = T), optim.output[-1, 6])
  winner <- rep(".", length = dim(optim.output)[1])
  winner[1] <- NA
  winner[winner.int] <- "*"
  params <- optim.output[winner.int[1], c(1:5) ]
  ab <- optim(par = c(min(dbh), max(dbh)), fn = .lg5.ML.a, resid.sd = resid.sd,
    method = "Nelder-Mead", hessian = TRUE, control = list(trace = 0),
    doy = doy, dbh = dbh, params = params)$par
  ab[2] <- ifelse(ab[2] > params[2], params[2], ab[2])
  return(c(params, ab))

}


#' Extract extra growth metrics.
#' @param param.table The output data.frame from the optimization *seealso* get.optimized.dendro
#' @param Dendro.split A list vector where every entry is a separate time series of a band on a stem in a year.
#' @param par.names  A character vector of the column names of the \emph{params} vector argument.
#' This is used to pull out actual parameters from other information.
#' @param OUTPUT.folder Character string for the target output folder.
#' @param param.table.name Character string for the parameter table output.
#' @param Dendro.data.name Character string for the data object, Dendro.complete, that is a complete data.frame table output.
#' @param Quantile.hull.name Character string for the data object, QH.Rdata, that contains the Quantile Hull results.
#' @description Fits a Quantile Hull to the data and extracts extra growth and
#'    phenology metrics from dendrometer band time series.
#'
#' @return Saves four files to the OUTPUT folder as named above and collected stems.
#' @export
get.extra.metrics <- function(
  param.table,
  Dendro.split,
  par.names = c("L", "K", "doyip", "r", "theta", "a", "b", "r.squared",
    "ts.sd", "alt.a"),
  OUTPUT.folder      = "OUTPUT",
  param.table.name   = "Param_table_complete.csv",
  Dendro.split.name  = "Dendro_split.Rdata",
  Dendro.data.name   = "Dendro_data.Rdata",
  Quantile.hull.name = "Quantile.hull.Rdata"
) {

  QH.list       <- vector("list", nrow(param.table))

  WD.sum          <- rep(NA, nrow(param.table))
  D.sum           <- rep(NA, nrow(param.table))
  RGR             <- rep(NA, nrow(param.table))
  GR              <- rep(NA, nrow(param.table))
  max.grow.day  <- rep(NA, nrow(param.table))
  max.grow.rate <- rep(NA, nrow(param.table))
  Size            <- rep(NA, nrow(param.table))
  Size.alt        <- rep(NA, nrow(param.table))
  fifty.doy       <- rep(NA, nrow(param.table))
  deficit         <- rep(NA, nrow(param.table))
  start.doy       <- rep(NA, nrow(param.table))
  start.doy.alt   <- rep(NA, nrow(param.table))
  stop.doy        <- rep(NA, nrow(param.table))
  doy.05          <- rep(NA, nrow(param.table))
  doy.95          <- rep(NA, nrow(param.table))
  doy.10          <- rep(NA, nrow(param.table))
  doy.90          <- rep(NA, nrow(param.table))

  new.metrics.df <- data.frame() # this collects new parameters and will be joined
# with params.table at the bottom (in the form of Results.mat)

  seq.l <- 200
  resid.sd <- 0.1
  days <- seq(365)
  deviation.val <- 0.1
  pb <- txtProgressBar(style = 3)

  for(i in 1:nrow(param.table)) {

    setTxtProgressBar(pb, i / nrow(param.table),
      title = NULL, label = sprintf("%s :: %s", Site, Year))
    if(is.null(Dendro.split[[i]])) next
    ts.data <- Dendro.split[[i]]
    ts.data$resids.vec <- rep(NA, times = nrow(ts.data))
    param.data <- param.table[i, ]
    if(any(is.na(param.data[par.names[1:5]]))) {
      Dendro.split[[i]] <- ts.data
      next
    }

    par.col <-  names(param.table) %in% par.names
    params <- param.table[i, par.col]
    params.numeric <- as.numeric(params)

    Site <- as.character(param.data$SITE)
    Year <- as.integer(param.data$YEAR[1])

    site.year <- paste(Site, Year, sep = "-")

    dbh <- ts.data$DBH_TRUE
    doy <- ts.data$DOY


    start.doy[i]     <- pred.doy(params = params, a = params$a)
    stop.doy[i]      <- pred.doy(params = params, a = params$b)
    if(!is.na(params$alt.a) & params$alt.a > 0)
      start.doy.alt[i] <- pred.doy(params = params, a = params$alt.a)

    try.hull <- try(fit.quantile.hull(dbh, doy, params,
      quant = 0.8), TRUE)

    if(class(try.hull) != "try-error") {
      QH.list[[i]] <- try.hull
      D.sum[i] <- sum(try.hull$Deficit)
      WD.sum[i] <- sum(try.hull$Weighted.deficit)
    }

    # Other summary stats
    ts.data$resids.vec <- .get.lg5.resids(params.numeric, doy, dbh)
    ts.data$std.resids <- scale(ts.data$resids.vec)
    RGR[i] <- as.numeric(log(params$b) - log(params$a))
    GR[i] <- as.numeric(params$b - params$a)
    max.grow.day[i] <- max.growth.day(as.numeric(params))
    max.grow.rate[i] <- max.growth.rate(params)
    Size[i] <- params$a
    Size.alt[i] <- params$alt.a
    start.doy[i] <- round(pred.doy(params, params$a))
    stop.doy[i] <- round(pred.doy(params, params$b))
    .deriv <- lg5.deriv(paras = params, days, growth = params$b - params$a)
    fifty.doy[i] <- round(pred.doy(params, mean(c(params$a, params$b))))
    doy.05[i] <- round(pred.doy(params, params$a + 0.05 * GR[i]))
    doy.95[i] <- round(pred.doy(params, params$a + 0.95 * GR[i]))
    doy.10[i] <- round(pred.doy(params, params$a + 0.10 * GR[i]))
    doy.90[i] <- round(pred.doy(params, params$a + 0.90 * GR[i]))
    Dendro.split[[i]] <- ts.data

  }
  close(pb)

  tmp.df <- data.frame(WD = WD.sum, RGR = RGR, GR = GR,
    Max.growth.day = max.grow.day, Max.growth.rate = max.grow.rate,
    Size.a = Size, Size.alt.a = Size.alt,
    Start.g = start.doy, Stop.g = stop.doy,
    Median.g = fifty.doy, DOY.05 = doy.05, DOY.95 = doy.95,
    DOY.10 = doy.10, DOY.90 = doy.90)

  new.metrics.df <- data.frame(cbind(param.table, tmp.df))

  new.metrics.df$GS_Length <- new.metrics.df$Stop.g - new.metrics.df$Start.g
  new.metrics.df$GS.90 <- new.metrics.df$DOY.95 - new.metrics.df$DOY.05
  new.metrics.df$GS.80 <- new.metrics.df$DOY.90 - new.metrics.df$DOY.10

  write.csv(new.metrics.df, file = paste(OUTPUT.folder, param.table.name, sep = "/"),
    quote = FALSE, row.names = FALSE)
  Dendro.complete <- do.call(rbind, Dendro.split)

  save(Dendro.complete, file = paste(OUTPUT.folder, Dendro.data.name, sep = "/"))
  save(Dendro.split, file = paste(OUTPUT.folder, Dendro.split.name, sep = "/"))
  save(QH.list, file = paste(OUTPUT.folder, Quantile.hull.name, sep = "/"))
}




#' Fit Quantile Hull to subset of time series data
#'
#' @param dbh Numeric vector of "TRUE_DBH" values
#' @param doy Integer vector of days of the year dbh values collected.
#' @param params Data.frame (vector with names) containing parameters.
#' @param quant Numeric scalar denoting the quantile threshold above which the hull will be estimated.
#' @param resid.sd Numeric scalar that constrains the optimization algorithm in \emph{optim()}.
#'
#' @return List with output from the optim function and other metrics.
#' @export
fit.quantile.hull <- function(dbh, doy, params, quant = 0.8, resid.sd = 0.02) {
  dbh <- as.numeric(dbh)
  complete <- complete.cases(dbh)
  dbh <- dbh[complete]
  doy <- doy[complete]
  paras <- as.numeric(params[1:5])
  a <- as.numeric(params[6])
  b <- as.numeric(params[7])
  growth <- b - a
  growth.quants <- quantile(seq(a, b, length = 1000),
    c(0.05, 0.1, 0.5, 0.9, 0.95))
  doy.quants <- pred.doy(paras, growth.quants)
  new.a <- growth.quants[1]
  new.a <- growth.quants[1]
  doy.start <- which(doy > doy.quants[1])
  doy.stop <- which(doy < doy.quants[5])

  curve.pure <- intersect(doy.start, doy.stop)

  doyP <- as.numeric(c(pred.doy(paras, growth.quants[1]), doy[curve.pure],
    pred.doy(paras, growth.quants[5])))
  dbhP <- as.numeric(c(growth.quants[1], dbh[curve.pure], growth.quants[5]))
  # residsP <- .get.lg5.resids(params = paras, doy = doy, dbh = dbh)
  residsP <- .get.lg5.resids(params = paras, doy = doyP, dbh = dbhP)
  ln.data <- length(residsP)
  top.resids <- unique(c(1, which(residsP >= quantile(residsP, quant)),
    length(residsP)))
  doyP2 <- doyP[top.resids]
  dbhP2 <- dbhP[top.resids]

  SSstart <- function(doy = doyP2, dbh = dbhP2) {
    lm.fit <- lm(dbhP2 ~ doyP2 + I(doyP2^2))
    new.doy <- seq(range(doyP2)[1], range(doyP2)[2])
    new.dbh <- predict(lm.fit, newdata = data.frame(doyP2 = new.doy),
      type = c("response"))
  }
  optim.min <- as.numeric(c((min(dbhP2, na.rm = TRUE) * 0.99),
    quantile(dbhP2, 0.5, na.rm = TRUE), 0, 0, 0.01))

  optim.max <- as.numeric(c(min(dbhP2, na.rm = TRUE), max(dbhP2, na.rm = TRUE),
    350, 0.1, 15))

  OH.fit <- optim(par = paras, fn = .get.lg5.ML, resid.sd = resid.sd,
    method = "L-BFGS-B", lower = optim.min, upper = optim.max,
    hessian = FALSE, control = list(trace = 0), doy = doyP2, dbh = dbhP2)
  deriv.list <- lg5.deriv(OH.fit$par, doyP, growth = (log(b) - log(a)),
    shift = 0.05)
  resids.hull <- scale(.get.lg5.resids(OH.fit$par, doyP, dbhP))
  weighted.deficit <- resids.hull * deriv.list
  OH.list <- list(doyP2 = doyP2, dbhP2 = dbhP2, doyP = doyP, dbhP = dbhP,
    OH.fit = OH.fit, Derivatives = deriv.list, Deficit = resids.hull,
    Weighted.deficit = weighted.deficit)
  return(OH.list)
}

##############################
## Auxiliary functions
##############################

#' Gets the starting diameter from the year before
#'
#' @param param.tab data.frame of parameter values for every year for a particular TREE_ID
#'
#' @return Numeric vector of diameter values (or codes).
#' @export
get.alt.a <- function(param.tab) {
  TREE.ID.F <- factor(param.tab$UNIQUE_ID, levels = unique(param.tab$UNIQUE_ID))
  param.split <- split(param.tab, f = TREE.ID.F, drop = FALSE)
  new.alt.a <- c()
  dim1 <- c()
  ln.alt.a <- c()
  for(id in 1:length(param.split)) {
    alt.a <- rep(-99, length(param.split[[id]]$a))
    dim1[id] <- dim(param.split[[id]])[1]
    if(length(alt.a) > 1) {
      alt.a[2:length(alt.a)] <- param.split[[id]]$b[1:(length(alt.a) - 1)]
    }
    new.alt.a <- c(new.alt.a, alt.a)
    ln.alt.a[id] <- length(alt.a)
  }
  return(new.alt.a)
}



#' Corrects for the chord
#'
#' @param gw1 Numeric gap width at time 1
#' @param gw2 Numeric gap width at time 2
#' @param dbh1 DBH at time 1
#'
#' @return Numeric scalar for the corrected DBH
#' @export
gettruedbh <- function(gw1, gw2, dbh1, unit = "cm") {
  dbh1 <- ifelse(unit == "cm", dbh1 * 10, dbh1)
  rhs  <- dbh1 * (pi - asin(gw1 / dbh1))
  #rhs is the length of the dendrometer band at time 1
  dbh2 <- optimize(.difdendro, interval = c(0, dbh1 + 2 * gw2),
    gw2 = gw2, rhs = rhs)
  dbh2.min <- ifelse(unit == "cm", dbh2$minimum / 10, dbh2$minimum)
  return(dbh2.min)
}

.difdendro <- function(dbh2, gw2, rhs) {
  lhs <- dbh2 * (pi - asin(gw2 / dbh2))
  return(abs(lhs - rhs))
}

##---------------------------------------------------------------
#' Gets dbh from gap width given org.dbh.
#'
#' @param gap.width Numeric vector of gap width measurements
#' @param org.dbh Numeric scalar of the original dbh of the tree (when bands were installed)
#' @param unit Character string denoting the unit that the dbh is in. Defaults to "cm".
#' @return Numeric vector of DBH_TRUE values
#' @export
gap2dbh <- function(gap.width, org.dbh, unit = "cm") {
  if(unit == "cm") {
    dbh.vec <- org.dbh + (((gap.width - gap.width[1]) / pi))
  } else {
    dbh.vec <- org.dbh + (((gap.width - gap.width[1]) / 10 / pi))
  }
  return(dbh.vec)
}

###################################################
### lg5.functions
###################################################
# These functions do many things related to the LG5 function (see vignette)

#' Predicts the dbh from doy and a parameter set
#'
#' @param params Numeric vector of parameter values
#' @param doy Integer vector of days of the year
#'
#' @return Numeric scalar or vector of dbh values
#' @export
lg5.pred <- function(params, doy) {
  paras <- names(params) %in% c("L", "K", "doyip", "r", "theta", "a", "b", "alt.a")
  L <- params[1] # min(dbh, na.rm = T)
  K <- params[2]
  doy.ip <- params[3]
  r <- params[4]
  theta <- params[5]
  dbh <- vector(length = length(doy))
  dbh <- L + ((K - L) / (1 + 1/theta * exp(-(r * (doy - doy.ip) / theta)) ^ theta))
  return(dbh)
}

.get.lg5.ML <- function(params, doy, dbh, resid.sd) {
	pred.dbh <- lg5.pred(params, doy)
	pred.ML <-  -sum(dnorm(dbh, pred.dbh, resid.sd, log = T))
	return(pred.ML)
}

.get.lg5.ML.wt <- function(params, doy, dbh, resid.sd) {
	wts <- 1 / dnorm(abs(seq(-2, 2, length = length(doy))), 0, 1)
	pred.dbh <- lg5.pred(params, doy)
	pred.ML <- -sum((wts * dnorm(dbh, pred.dbh, resid.sd, log = T)))
	return(pred.ML)
}

.get.lg5.resids <- function(params, doy, dbh) {
  para <- as.numeric(params)
  lg5.resid <- dbh - lg5.pred(para, doy)
  return(lg5.resid)
}


#' Predicts the day of the year given a diameter and parameter values
#'
#' @param params Numeric vector of parameter values.
#' @param a Numeric scalar of diameter for which a doy is wanted
#'
#' @return returns day of year
#' @export
pred.doy <- function(params, a) {
	params <- as.numeric(params)
	L <- params[1] # min(dbh, na.rm = T)
	K <- params[2]
	doy.ip <- params[3]
	r <- params[4]
	theta <- params[5]
	a.par <- a
	.expr1 <- (K - L) / (a.par - L)
	.expr2 <- doy.ip * r - theta * log(((.expr1 - 1) * theta) ^ (1/theta))
	.expr3 <- .expr2 / r
	dcrit <- .expr3

	return(dcrit)
}

#' Returns starting and stopping diameter values for a year for a tree
#'
#' @param a Numeric scalar or vector of 2 elements with starting and stopping values
#' @param params Numeric vector of parameters
#' @param doy Integer (or numeric) vector of days of the year for which the curve was fit.
#' @param asymptote which side to get value for (should be removed).
#'
#' @return Numeric scalar (or vector of 2 scalars) for starting and stoping sizes
#' @export
lg5.pred.a <- function (a, params, doy, asymptote = "both") {
	asymptote <- ifelse(length(a) > 1, "both", asymptote)
	L <- params[1] # min(dbh, na.rm = T)
	K <- params[2]
	doy.ip <- params[3]
	r <- params[4]
	theta <- params[5]
	diam <- vector(length = length(doy))
	if(asymptote == "lower") {
		d.crit <- pred.doy(params, a)
		diam[which(doy <= d.crit)] <- a
		diam[which(doy > d.crit)] <- L + ((K - L) / (1 + 1/theta * exp(-(r * (doy[which(doy > d.crit)] - doy.ip) / theta)) ^ theta))
	}else{
		if(asymptote == "upper") {
			d.crit <- pred.doy(params, a)
			diam[which(doy >= d.crit)] <- a
			diam[which(doy < d.crit)] <- L + ((K - L) / (1 + 1/theta * exp(-(r * (doy[which(doy < d.crit)] - doy.ip) / theta)) ^ theta))
		}else{
			if(asymptote == "both") {
				d.crit <- pred.doy(params, a)
				is.nan.dc <- is.nan(d.crit)
				d.crit[is.nan.dc] <- 365
				diam[which(doy <= d.crit[1])] <- a[1]
				diam[which(doy >= d.crit[2])] <- a[2]
				doy.mid <- doy[which(doy > d.crit[1] & doy < d.crit[2])]
				diam[which(doy > d.crit[1] & doy < d.crit[2])] <-
				L + ((K - L) / (1 + 1/theta * exp(-(r * (doy.mid - doy.ip) / theta)) ^ theta))
			}
		}
	}
	return(diam)
}

.lg5.ML.a <- function(a, params, doy, dbh, resid.sd, asymptote = "both") {
	pred.dbh <- lg5.pred.a(a, params, doy)
	pred.ML <- -sum(dnorm(dbh, pred.dbh, resid.sd, log = T))
}


.lg5.QH <- function(paras, doyCP, dbhCP) {
	pred.dbh <- lg5.pred(paras, doyCP)
	pred.ND <-  sum(pred.dbh - dbhCP) # for "Negative Difference"
	return(pred.ND)
}

#' Get quantile hull 'residuals'
#'
#' @param params Numeric vector of parameters.
#' @param doy Day of the Year
#' @param dbh Sequence of DBH values
#' @param log.rate Logical whether rates are logged
#'
#' @return Numeric vector of residuals
#' @export
get.QH.resid <- function(params, doy, dbh, log.rate = FALSE) {
	lg5.pred <- lg5.pred(params, doy)
	rates <- lg5.deriv(params, doy)
	if(log.rate) {
		resids <- lg5.pred - dbh * log(rates + 0.000001)
	}else{
		resids <- lg5.pred - dbh * rates
	}
	return(resids)
}

#' Derivative of LG5 function
#'
#' @param paras Numeric vector of parameters for the lg5 function
#' @param doy Integer vector of days of the year dbh values collected.
#' @param growth Unclear.
#' @param shift Numeric scalar that gives half the size of the window the derivitive is taken over (in days).
#' @description This function takes the numerical derivative.
#' Default values return a single day of growth. Using the 'growth'
#' argument, the derivative can be returned, scaled by annual growth.
#' @return returns numeric vectore of derivatives
#' @export
lg5.deriv <- function(paras, doy, growth = 1, shift = 0.5) {
  paras = as.numeric(paras)
  .loVal <- lg5.pred(paras, (doy - shift))
  .hiVal <- lg5.pred(paras, (doy + shift))
  deriv.lg5 <- (.hiVal - .loVal) / (2 * shift)
  return(deriv.lg5 / growth)
}

#' Find day of the year of maximum growth
#'
#' @param params Numeric vector of parameters.
#'
#' @return Numeric scalar of the fastest day.
#' @export
max.growth.day <- function(params) {
  paras <- as.numeric(params)
  days <- seq(365)
  .deriv <- lg5.deriv(paras, days)
  fastest.day <- max(days[which( .deriv == max(.deriv))], na.rm = TRUE)
  return(fastest.day)
}

#' Find rate of maximum growth
#'
#' @param params Numeric vector of parameters.
#'
#' @return Numeric scalar of the fastest growth rate
#' @export
max.growth.rate <- function(params) {
  paras <- as.numeric(params)
  doy.a <- pred.doy(params, params$a)
  days <- try(seq(round(doy.a), 365), silent = TRUE)
  if(class(days) == "try-error") {
    return(NA)
  } else {
    .deriv <- lg5.deriv(paras, days)
    growth.rate <- max(.deriv, na.rm = TRUE)
    return(growth.rate)
  }
}
seanmcm/RDendrom documentation built on March 2, 2024, 3:53 p.m.