# 29 March 2004

# Author: Andrew Merton
# Email : merton@stat.colostate.edu

# Software to accompany the paper: "Model Selection for Geostatistical Models",
# Jennifer A. Hoeting, Richard A. Davis, Andrew A. Merton, and
# Sandra E. Thompson,  (2006) "Ecological Applications", 16(1), pp. 87-98.

# ---------------------------------------------------------------------------- #
# The following three functions have been made available for estimating the
# Matern correlation coefficients theta1(range) and theta2(smoothness).  Details
# for each function are listed below.
# -----
# FUNCTION COR.MATERN: Calculates the Matern correlation matix as parameterized
# by Hoeting et al.

# Inputs:
#   theta - Vector (or array) containing theta1, theta2, and theta3.
#             Theta1 = range.
#             Theta2 = smoothness.
#             Theta3 = nugget.
#   D     - Matrix (or array) of distances between sites. Typically D is an
#           (nxn) matrix with zeros along the main diagonal.

# Output:
#   The function returns a matrix (or array) of size D containing the Matern
#   correlations.
# -----
cor.matern <- function(theta,D) {
  if (is.vector(D)) names(D) <- NULL
  if (is.matrix(D)) dimnames(D) <- list(NULL,NULL)
  th1 <- theta[1]                             # Range.
  th2 <- theta[2]                             # Smoothness.
  th3 <- ifelse(length(theta)==3,theta[3],0)  # Nugget effect.

  u <- 2*D*sqrt(th2)/th1
  u <- ifelse(u>0,(1-th3)*u^th2/(2^(th2-1)*gamma(th2))*besselK(u,th2),1)
  return(u)
}
# ---------------------------------------------------------------------------- #
# FUNCTION LOG.PROFILE: Evalutes the negative of the log profile likelihood
# function.  This function is minimized by fitmatern(...) to find the optimal
# values for theta1 and theta2.

# Inputs:
#   theta - Vector (or array) containing theta1 and theta2.
#   D     - Matrix of distances between sites. The matrix must be square (nxn)
#           where n = length of the response vector.  D should be  symmetric
#           with zeros on the main diagonal (which corresponds to the Euclidian
#           distance between site i and site j where i = j).
#   X     - Design matrix. This matrix must be (nxp) where the first column is
#           a column of ones, i.e., there a p-1 explanatory variables. NA's are
#           not allowed.
#   Z     - An (nx1) response vector (or array).

# Output:
#   The function returns the value of the -log profile likelihood. see Hoeting
#   et al for more details.
# -----
log.profile <- function(theta,D,X,Z) {
# Compute the correlation matrix.
  n       <- length(Z)
  psi     <- cor.matern(theta,D)
  L       <- t(chol(psi))
  L.inv   <- forwardsolve(L,diag(nrow=n))
  L.det   <- prod(diag(L))
  psi.inv <- t(L.inv)%*%L.inv

# Compute the MLE for beta.
  beta    <- solve(t(X)%*%psi.inv%*%X)%*%t(X)%*%psi.inv%*%Z

# Compute the MLE for sigma.
  resid   <- Z-X%*%beta
  sigma2  <- t(resid)%*%psi.inv%*%resid/n

# Evaluate -log-profile likelihood.
  prof    <- log(L.det)+n*log(sigma2)/2+n/2

  return(max(prof,-1e+50))   # Return -1e+50 if there is an error.
}
# ---------------------------------------------------------------------------- #
# FUNCTION FITMATERN: Determins the optimal values for theta1 and theta2 that
# maximize the log profile likelihood. This function uses a constrained
# optimization routine and may need to be adjusted for some problems.
# Note: Maximize(log.profile) = Minimize(-log.profile).

# Inputs:
#   X      - (Optional) design matrix. If provided by the user, it must have a
#            column of ones as the first column. If ommited from the function
#            call, the routine automatically creates an (nx1) vector of ones.
#            This is useful for fitting the Matern coefficients to a data set
#            that has already had the mean trend removed.
#   Z      - Vector (or array) of responses (nx1).
#   D      - Matrix (nxn) of Euclidian distances between all pairs of points.
#            It should be symmetrical with zeros along the main diagonal.
#   theta0 - Vector (or array) containing initial guesses for theta1 and theta2.

# Outputs:
#   A list containing the maximum likelihood estimates (MLEs) for beta,
#   sigma2, theta (theta1,theta2), and the AICC. For further details, see
#   Hoeting et al.
# -----
fitmatern <- function(X=NULL,Z,D,theta0) {
# Preliminaries:
  n <- length(Z)
  if (is.null(X))   {X<-matrix(1,nrow=n)}
  if (is.vector(X)) {names(X)<-NULL;p<-1}
  if (is.matrix(X)) {dimnames(X)<-list(NULL,NULL);p<-length(X[1,])}
  if (is.vector(Z)) {names(Z)<-NULL}
  if (is.matrix(Z)) {dimnames(Z)<-list(NULL,NULL)}
  D.max    <- max(D)

# Maximize the log profile likelihood, i.e. minimize the -log prof likelihood.
  prof.max <- optim(theta0,log.profile,method="L-BFGS-B",
                    lower=c(0.0001,0.0001),upper=c(D.max,5),D=D,X=X,Z=Z)

# Return beta, sigma2, theta, and AICC.
  k       <- 2
  theta   <- prof.max$par
  psi     <- cor.matern(theta,D)  # Make correlation matrix.
  L       <- t(chol(psi))         # Cholesky decomposition.
  L.inv   <- forwardsolve(L,diag(nrow=n))
  psi.inv <- t(L.inv)%*%L.inv
  beta    <- solve(t(X)%*%psi.inv%*%X)%*%t(X)%*%psi.inv%*%Z
  resid   <- Z-X%*%beta
  sigma2  <- t(resid)%*%psi.inv%*%resid/n
  AICC    <- 2*prof.max$value + 2*n*(p+k+1)/(n-p-k-2)

  output  <- list(beta,sigma2,theta,AICC)
  names(output) <- list("beta","sigma2","theta","AICC")
  return(output)
}
# ---------------------------------------------------------------------------- #
