R/mix_like.R

#' Find log-component density convolution when error distribution is a
#' mixture.
#'
#' This function will find the log of the coefficients of the prior
#' mixing proportions in the ASH-likelihood when the model is
#' \eqn{betahat = g + errordist}, where \eqn{g} and \eqn{errordist}
#' are both mixtures of either normals or uniforms.
#'
#' There are four cases: (1) \code{g} and \code{errordist} are both of
#' class \code{normalmix}, (2) \code{g} is of class \code{normalmix}
#' and \code{errordist} is of class \code{unifmix}, (3) \code{g} is of
#' class \code{unifmix} and \code{errordist} is of class
#' \code{normalmix}, and (4) \code{g} and \code{errordist} are both of
#' class \code{unifmix}. All of these are supported. Though (2) and
#' (3) differ only in indexing.
#'
#' @param g A mixture density. Either of class \code{normalmix} or of
#'     class \code{unimix}. This is the prior.
#' @param errordist A list of objects of either class \code{normalmix}
#'     or \code{unimix}. The length of this list must be the length of
#'     \code{betahat}. \code{errordist[[i]]} is the \eqn{i}th error
#'     distribution of \code{betahat[i]}.
#' @param betahat A vector of numerics. The locations at which to
#'     evalutate the density.
#'
#' @return A matrix with row dimension \code{length(betahat)} and
#'     column dimension \code{length(errordist)}.
#'
#' @author David Gerard
log_compdens_conv_mix <- function(g, betahat, errordist) {
    class_e <- unique(sapply(errordist, class))
    class_g <- class(g)

    assertthat::are_equal(length(class_e), 1)
    assertthat::are_equal(length(class_g), 1)
    assertthat::assert_that(is.list(errordist))
    assertthat::are_equal(length(betahat), length(errordist))

    pisum <- sapply(errordist, FUN = function(x) { sum(x$pi) })
    assertthat::assert_that(all(abs(pisum - 1) < 10 ^ -14))

    ## make sure error distribution doesn't have point mass on zero
    if (class_e == "normalmix") {
        assertthat::assert_that(!any(sapply(errordist, FUN = function(x) { any(abs(x$sd) < 10 ^ -14) })))
    } else if (class_e == "unimix") {
        assertthat::assert_that(!any(sapply(errordist, FUN = function(x) { any(abs(x$a - x$b) < 10 ^ -14) })))
    }

    if (class_g == "normalmix" & class_e == "normalmix") {
        matrix_llik <- log_compdens_conv_mix.normalnormal(g, betahat, errordist)
    } else if (class_g == "normalmix" & class_e == "unimix") {
        matrix_llik <- log_compdens_conv_mix.normaluni(g, betahat, errordist)
    } else if (class_g == "unimix" & class_e == "normalmix") {
        matrix_llik <- log_compdens_conv_mix.uninormal(g, betahat, errordist)
    } else if (class_g == "unimix" & class_e == "unimix") {
        matrix_llik <- log_compdens_conv_mix.uniuni(g, betahat, errordist)
    } else {
        stop("Error: either g or errordist is of an unsupported class.")
    }
    return(matrix_llik)
}

#' Normal-mixture - normal-mixture convolution.
#'
#' @inheritParams log_compdens_conv_mix
#'
#' @author David Gerard
log_compdens_conv_mix.normalnormal <- function(g, betahat, errordist) {
    class_e <- unique(sapply(errordist, class))
    class_g <- class(g)

    assertthat::are_equal(length(class_e), 1)
    assertthat::are_equal(length(class_g), 1)
    assertthat::are_equal(class_e, "normalmix")
    assertthat::are_equal(class_g, "normalmix")
    assertthat::are_equal(length(betahat), length(errordist))

    n <- length(betahat)
    p <- length(g$mean)
    matrix_llik <- matrix(NA, nrow = p, ncol = n)
    for (index in 1:n) {
        matrix_llik[, index] <- nnconv_dense(g = g,
                                             errordist = errordist[[index]],
                                             x = betahat[index])
    }
    return(matrix_llik)
}


#' Normal-normal convolution log-density evaluated at a single point.
#'
#' @param g A \code{normalmix} object. This is the prior mixture.
#' @param errordist A \code{normalmix} object. This is the error mixture.
#' @param x A single numeric. This is the data.
#'
#' @author David Gerard
#'
#' @seealso \code{\link{log_compdens_conv_mix.normalnormal}}
nnconv_dense <- function(g, errordist, x) {
    assertthat::are_equal(class(g), "normalmix")
    assertthat::are_equal(class(errordist), "normalmix")
    assertthat::are_equal(length(x), 1)

    new_means <- outer(errordist$mean, g$mean, FUN = "+")
    new_sd    <- sqrt(outer(errordist$sd ^ 2, g$sd ^ 2, FUN = "+"))

    log_vals <- stats::dnorm(x = x, mean = new_means, sd = new_sd, log = TRUE)

    if (length(dim(log_vals)) == 2) {
        max_log_vals <- apply(log_vals, 2, max)
        like_vals <- log(colSums(exp(log_vals - max_log_vals) * errordist$pi)) + max_log_vals
    } else if (length(dim(log_vals)) == 0 ) {
        ## working with a scalar
        like_vals <- log_vals
    } else {
        stop("Contact David Gerard. This is a scenario I wasn't expecting")
    }

    return(like_vals)
}


#' Normal-mixture - uniform-mixture convolution.
#'
#' @inheritParams log_compdens_conv_mix
#'
#' @author David Gerard
log_compdens_conv_mix.normaluni <- function(g, betahat, errordist) {
    class_e <- unique(sapply(errordist, class))
    class_g <- class(g)

    assertthat::are_equal(length(class_e), 1)
    assertthat::are_equal(length(class_g), 1)
    assertthat::are_equal(class_e, "unimix")
    assertthat::are_equal(class_g, "normalmix")
    assertthat::are_equal(length(betahat), length(errordist))

    n <- length(betahat)
    p <- length(g$mean)
    matrix_llik <- matrix(NA, nrow = p, ncol = n)
    for (index in 1:n) {
        matrix_llik[, index] <- nuconv_dense(g = g,
                                             errordist = errordist[[index]],
                                             x = betahat[index])
    }
    return(matrix_llik)
}


#' Normal-uniform convolution log-density evaluated at a single point.
#'
#' @param g A \code{normalmix} object. This is the prior mixtrue.
#' @param errordist A \code{unimix} object. This is the error mixture.
#' @param x A single numeric. This is the data.
#'
#' @author David Gerard
#'
#' @seealso \code{\link{log_compdens_conv_mix.normaluni}}
nuconv_dense <- function(g, errordist, x) {
    assertthat::are_equal(class(g), "normalmix")
    assertthat::are_equal(class(errordist), "unimix")
    assertthat::are_equal(length(x), 1)


    left_means  <- outer(errordist$b, g$mean - x, FUN = "+")
    right_means <- outer(errordist$a, g$mean - x, FUN = "+")

    sd_mat <- matrix(rep(g$sd, length(errordist$b)), ncol = length(g$sd), byrow = TRUE)

    lp1 <- stats::pnorm(q = left_means, sd = sd_mat, log.p = TRUE)
    lp2 <- stats::pnorm(q = right_means, sd = sd_mat, log.p = TRUE)

    max_log <- apply(cbind(apply(lp1, 2, max), apply(lp2, 2, max)), 1, max)

    lp1_sub <- t(lp1) - max_log
    lp2_sub <- t(lp2) - max_log

    diff_vec <- 1 / (errordist$b - errordist$a)

    like_vals <- log(colSums( (diff_vec * errordist$pi) * t(exp(lp1_sub) - exp(lp2_sub)))) +
        max_log

    ## deal with place where point mass at zero
    which_zero <- abs(g$sd) < 10 ^ -14
    assertthat::assert_that(sum(which_zero) <= 1)
    like_vals[which_zero] <- log(sum(errordist$pi * (x >= errordist$a & x <= errordist$b) /
                                     (errordist$b - errordist$a)))

    return(like_vals)
}


#' Uniform-mixture - normal-mixture convolution.
#'
#' @inheritParams log_compdens_conv_mix
#'
#' @author David Gerard
log_compdens_conv_mix.uninormal <- function(g, betahat, errordist) {
    class_e <- unique(sapply(errordist, class))
    class_g <- class(g)

    assertthat::are_equal(length(class_e), 1)
    assertthat::are_equal(length(class_g), 1)
    assertthat::are_equal(class_e, "normalmix")
    assertthat::are_equal(class_g, "unimix")
    assertthat::are_equal(length(betahat), length(errordist))

    n <- length(betahat)
    p <- length(g$pi)
    matrix_llik <- matrix(NA, nrow = p, ncol = n)
    for (index in 1:n) {
        matrix_llik[, index] <- unconv_dense(g = g,
                                             errordist = errordist[[index]],
                                             x = betahat[index])
    }
    return(matrix_llik)
}


#' Uniform-normal convolution log-density evaluated at a single point.
#'
#' @param g A \code{unimix} object. This is the prior mixtrue.
#' @param errordist A \code{normalmix} object. This is the error mixture.
#' @param x A single numeric. This is the data.
#'
#' @author David Gerard
#'
#' @seealso \code{\link{log_compdens_conv_mix.uninormal}}
unconv_dense <- function(g, errordist, x) {
    assertthat::are_equal(class(g), "unimix")
    assertthat::are_equal(class(errordist), "normalmix")
    assertthat::are_equal(length(x), 1)

    left_means  <- outer(errordist$mean, g$b - x, FUN = "+")
    right_means <- outer(errordist$mean, g$a - x, FUN = "+")

    sd_mat <- matrix(rep(errordist$sd, length(g$b)), ncol = length(g$pi), byrow = FALSE)

    lp1 <- stats::pnorm(q = left_means, sd = sd_mat, log.p = TRUE)
    lp2 <- stats::pnorm(q = right_means, sd = sd_mat, log.p = TRUE)

    max_log <- apply(cbind(apply(lp1, 2, max), apply(lp2, 2, max)), 1, max)

    lp1_sub <- t(lp1) - max_log
    lp2_sub <- t(lp2) - max_log

    diff_vec <- 1 / (g$b - g$a)

    like_vals <- log(colSums( (errordist$pi) * t(exp(lp1_sub) - exp(lp2_sub)))) +
        max_log + log(diff_vec)

    ## deal with values where prior is point mass on zero.
    which_zero <- abs(g$a) < 10 ^ -14 & g$b < 10 ^ -14
    assertthat::assert_that(sum(which_zero) <= 1)
    zeropart <- stats::dnorm(x = x, mean = errordist$mean, sd = errordist$sd, log = TRUE)
    max_zeropart <- max(zeropart)
    like_vals[which_zero] <- log(sum(errordist$pi * exp(zeropart - max_zeropart))) + max_zeropart

    return(like_vals)
}



#' Uniform-mixture - uniform-mixture convolution.
#'
#' @inheritParams log_compdens_conv_mix
#'
#' @author David Gerard
log_compdens_conv_mix.uniuni <- function(g, betahat, errordist) {
    class_e <- unique(sapply(errordist, class))
    class_g <- class(g)

    assertthat::are_equal(length(class_e), 1)
    assertthat::are_equal(length(class_g), 1)
    assertthat::are_equal(class_e, "unimix")
    assertthat::are_equal(class_g, "unimix")
    assertthat::are_equal(length(betahat), length(errordist))

    n <- length(betahat)
    p <- length(g$pi)
    matrix_llik <- matrix(NA, nrow = p, ncol = n)
    for (index in 1:n) {
        matrix_llik[, index] <- uuconv_dense(g = g,
                                             errordist = errordist[[index]],
                                             x = betahat[index])
    }
    return(matrix_llik)
}


#' Uniform-uniform convolution log-density evaluated at a single point.
#'
#' @param g A \code{unimix} object. This is the prior mixtrue.
#' @param errordist A \code{unimix} object. This is the error mixture.
#' @param x A single numeric. This is the data.
#'
#' @author David Gerard
#'
#' @seealso \code{\link{log_compdens_conv_mix.uniuni}}
uuconv_dense <- function(g, errordist, x) {
    assertthat::are_equal(class(g), "unimix")
    assertthat::are_equal(class(errordist), "unimix")
    assertthat::are_equal(length(x), 1)

    firstmat <- outer(rep(1, length = length(errordist$a)), g$b)
    secondmat <- outer(x - errordist$a, rep(1, length = length(g$b)))
    minval <- matrix(apply(cbind(c(firstmat), c(secondmat)),
                           1, min), nrow = length(errordist$a))

    firstmat <- outer(rep(1, length = length(errordist$b)), g$a)
    secondmat <- outer(x - errordist$b, rep(1, length = length(g$a)))
    maxval <- matrix(apply(cbind(c(firstmat), c(secondmat)),
                           1, max), nrow = length(errordist$a))

    numerator <- outer(errordist$b - errordist$a, g$b - g$a, FUN = "*")

    like_dens_mat <- (minval - maxval) / numerator
    like_dens_mat[like_dens_mat < 0] <- 0

    like_vals <- log(colSums(errordist$pi * like_dens_mat))

    ## deal with place where point mass at zero
    which_zero <- abs(g$a) < 10 ^ -14 & abs(g$b) < 10 ^ -14
    assertthat::assert_that(sum(which_zero) <= 1)
    like_vals[which_zero] <- log(sum(errordist$pi * (x >= errordist$a & x <= errordist$b) /
                                     (errordist$b - errordist$a)))
    return(like_vals)
}




#' Generate the posterior distribution when the error distribution is
#' a mixture.
#'
#' @param g The prior distribution. Either of class \code{"normalmix"}
#'     or \code{"unimix"}.
#' @param betahat The observations, a vector of numerics.
#' @param errordist A list of mixture distributions.
#'
#' @return A list of arrays containing the parameters for the posterior.
#' The first dimension is the observations, the second is the mixture
#' components for the prior, and the third is the mixture components of
#' the error.
#'
#'     \code{weights} The mixing proportions
#'
#'     \code{means} The location parameter if the posterior is either
#'     a mixture of normals or truncated normals.
#'
#'     \code{variances} The scale parameter if the posterior is either
#'     a mixture of normals or truncated normals.
#'
#'     \code{lower} The lower bound of support if the posterior is
#'     either a mixture of truncated normals of uniforms.
#'
#'     \code{upper} The upper bound of support if the posterior is
#'     either a mixture of truncated normals of uniforms.
#'
#' @author David Gerard
post_mix_dist <- function(g, betahat, errordist) {
    class_e <- unique(sapply(errordist, class))
    class_g <- class(g)

    assertthat::are_equal(length(class_e), 1)
    assertthat::are_equal(length(class_g), 1)
    assertthat::assert_that(is.list(errordist))
    assertthat::are_equal(length(betahat), length(errordist))

    pisum <- sapply(errordist, FUN = function(x) { sum(x$pi) })
    assertthat::assert_that(all(abs(pisum - 1) < 10 ^ -14))

    ## make sure error distribution doesn't have point mass on zero
    if (class_e == "normalmix") {
        assertthat::assert_that(!any(sapply(errordist, FUN = function(x) { any(x$sd == 0)})))
    } else if (class_e == "unimix") {
        assertthat::assert_that(!any(sapply(errordist, FUN = function(x) { any(x$a == 0 & x$b == 0)})))
    }

    if (class_g == "normalmix" & class_e == "normalmix") {
        post_dist <- post_mix_dist.normalnormal(g, betahat, errordist)
    } else if (class_g == "normalmix" & class_e == "unimix") {
        post_dist <- post_mix_dist.normaluni(g, betahat, errordist)
    } else if (class_g == "unimix" & class_e == "normalmix") {
        post_dist <- post_mix_dist.uninormal(g, betahat, errordist)
    } else if (class_g == "unimix" & class_e == "unimix") {
        post_dist <- post_mix_dist.uniuni(g, betahat, errordist)
    } else {
        stop("Error: either g or errordist is of an unsupported class.")
    }
    return(post_dist)
}

#' Normal-mixture - normal-mixture posterior calculation.
#'
#'
#' @inheritParams post_mix_dist
#'
#' @author David Gerard
post_mix_dist.normalnormal <- function(g, betahat, errordist) {
    n  <- length(betahat)
    K  <- length(g$pi)
    Lj <- length(errordist[[1]]$pi)

    assertthat::are_equal(n, length(errordist))

    carray   <- array(NA, dim = c(n, K, Lj))
    weights_array  <- array(NA, dim = c(n, K, Lj))
    means_array    <- array(NA, dim = c(n, K, Lj))
    variances_array <- array(NA, dim = c(n, K, Lj))

    ## compute carray
    for (seindex in 1:n) {
        cerr  <- errordist[[seindex]]
        cmean <- outer(g$mean, cerr$mean, FUN = "+")
        csd   <- sqrt(outer(g$sd ^ 2, cerr$sd ^ 2, FUN = "+"))
        carray[seindex, , ] <- stats::dnorm(x = betahat[seindex], mean = cmean, sd = csd)
    }


    which_pointmass <- abs(g$sd) < 10 ^ -14

    ## compute weights_array, means_array, and variances_array
    for (seindex in 1:n) {
        cerr  <- errordist[[seindex]]
        uncalibrated_weights_array <- outer(g$pi, cerr$pi, FUN = "*") * carray[seindex, , ]
        weights_array[seindex, , ] <- uncalibrated_weights_array / sum(uncalibrated_weights_array)

        current_var  <- 1 / outer(1 / g$sd ^ 2, 1 / cerr$sd ^ 2, FUN = "+")
        current_mean <- outer(g$mean / g$sd ^ 2, (betahat[seindex] - cerr$mean) /
                                                 cerr$sd ^ 2, FUN = "+") * current_var
        current_mean[which_pointmass, ] <- g$mean[which_pointmass]

        means_array[seindex, , ]     <- current_mean
        variances_array[seindex, , ] <- current_var
    }

    post_list <- list(weights = weights_array,
                      means = means_array,
                      variances = variances_array)
    class(post_list) <- "normalmix_array"
    return(post_list)
}

#' Normal-mixture (prior) - uniform-mixture (error) posterior calculation.
#'
#'
#' @inheritParams post_mix_dist
#'
#' @author David Gerard
post_mix_dist.normaluni <- function(g, betahat, errordist) {
    n  <- length(betahat)
    K  <- length(g$pi)
    Lj <- length(errordist[[1]]$pi)

    assertthat::are_equal(class(g), "normalmix")
    assertthat::are_equal(unique(sapply(errordist, class)), "unimix")
    assertthat::are_equal(n, length(errordist))

    carray          <- array(NA, dim = c(n, K, Lj))
    weights_array   <- array(NA, dim = c(n, K, Lj))
    means_array     <- array(NA, dim = c(n, K, Lj))
    variances_array <- array(NA, dim = c(n, K, Lj))
    lower_array     <- array(NA, dim = c(n, K, Lj))
    upper_array     <- array(NA, dim = c(n, K, Lj))

    which_pointmass <- abs(g$sd) < 10 ^ -14

    ## compute carray
    for (seindex in 1:n) {
        cerr <- errordist[[seindex]]
        left_vals <- outer(g$mean, cerr$b - betahat[seindex], FUN = "+") / g$sd
        right_vals <- outer(g$mean, cerr$a - betahat[seindex], FUN = "+") / g$sd

        current_c <- t(t(stats::pnorm(q = left_vals) -
                         stats::pnorm(q = right_vals)) / (cerr$b - cerr$a))

        current_c[which_pointmass, ] <-
            (betahat[seindex] >= cerr$a & betahat[seindex] <= cerr$b) / (cerr$b - cerr$a)

        carray[seindex, , ] <- current_c
    }

    ## means, variances, weights, lower and upper
    for (seindex in 1:n) {
        cerr  <- errordist[[seindex]]
        uncalibrated_weights_array <- outer(g$pi, cerr$pi, FUN = "*") * carray[seindex, , ]
        weights_array[seindex, , ] <- uncalibrated_weights_array / sum(uncalibrated_weights_array)

        current_mean <- matrix(rep(g$mean, Lj), nrow = K)
        current_var  <- matrix(rep(g$sd ^ 2, Lj), nrow = K)
        current_lower <- matrix(rep(betahat[seindex] - cerr$b, K), nrow = K, byrow = TRUE)
        current_upper <- matrix(rep(betahat[seindex] - cerr$a, K), nrow = K, byrow = TRUE)

        means_array[seindex, , ]     <- current_mean
        variances_array[seindex, , ] <- current_var
        lower_array[seindex, , ]     <- current_lower
        upper_array[seindex, , ]     <- current_upper
    }

    assertthat::assert_that(all(lower_array <= upper_array))
    
    post_list <- list(weights = weights_array,
                      means = means_array,
                      variances = variances_array,
                      lower = lower_array,
                      upper = upper_array)
    class(post_list) <- "truncnormalmix_array"
    return(post_list)
}


#' Uniform-mixture (prior) - normal-mixture (error) posterior calculation.
#'
#'
#' @inheritParams post_mix_dist
#'
#' @author David Gerard
post_mix_dist.uninormal <- function(g, betahat, errordist) {
    n  <- length(betahat)
    K  <- length(g$pi)
    Lj <- length(errordist[[1]]$pi)

    assertthat::are_equal(class(g), "unimix")
    assertthat::are_equal(unique(sapply(errordist, class)), "normalmix")
    assertthat::are_equal(n, length(errordist))

    carray          <- array(NA, dim = c(n, K, Lj))
    weights_array   <- array(NA, dim = c(n, K, Lj))
    means_array     <- array(NA, dim = c(n, K, Lj))
    variances_array <- array(NA, dim = c(n, K, Lj))
    lower_array     <- array(NA, dim = c(n, K, Lj))
    upper_array     <- array(NA, dim = c(n, K, Lj))

    which_pointmass <- abs(g$a) < 10 ^ -14 & abs(g$b) < 10 ^ -14

    ## compute carray
    for (seindex in 1:n) {
        cerr <- errordist[[seindex]]
        left_vals <- t(outer(cerr$mean - betahat[seindex], g$b, FUN = "+") / cerr$sd)
        right_vals <- t(outer(cerr$mean - betahat[seindex], g$a, FUN = "+") / cerr$sd)

        current_c <- (stats::pnorm(left_vals) - stats::pnorm(right_vals)) / (g$b - g$a)

        current_c[which_pointmass, ] <- stats::dnorm(x = betahat[seindex],
                                                     mean = cerr$mean,
                                                     sd = cerr$sd)
        carray[seindex, , ] <- current_c
    }

    ## means, variances, weights, lower and upper
    for (seindex in 1:n) {
        cerr  <- errordist[[seindex]]
        uncalibrated_weights_array <- outer(g$pi, cerr$pi, FUN = "*") * carray[seindex, , ]
        weights_array[seindex, , ] <- uncalibrated_weights_array / sum(uncalibrated_weights_array)

        current_mean  <- matrix(rep(betahat[seindex] - cerr$mean, K), nrow = K, byrow = TRUE)
        current_var   <- matrix(rep(cerr$sd ^ 2, K), nrow = K, byrow = TRUE)
        current_lower <- matrix(rep(g$a, Lj), nrow = K, byrow = FALSE)
        current_upper <- matrix(rep(g$b, Lj), nrow = K, byrow = FALSE)

        means_array[seindex, , ]     <- current_mean
        variances_array[seindex, , ] <- current_var
        lower_array[seindex, , ]     <- current_lower
        upper_array[seindex, , ]     <- current_upper
    }

    post_list <- list(weights = weights_array,
                      means = means_array,
                      variances = variances_array,
                      lower = lower_array,
                      upper = upper_array)
    class(post_list) <- "truncnormalmix_array"
    return(post_list)
}


#' Uniform-mixture - uniform-mixture posterior calculation.
#'
#'
#' @inheritParams post_mix_dist
#'
#' @author David Gerard
post_mix_dist.uniuni <- function(g, betahat, errordist) {
    n  <- length(betahat)
    K  <- length(g$pi)
    Lj <- length(errordist[[1]]$pi)

    assertthat::are_equal(class(g), "unimix")
    assertthat::are_equal(unique(sapply(errordist, class)), "unimix")
    assertthat::are_equal(n, length(errordist))

    carray          <- array(NA, dim = c(n, K, Lj))
    weights_array   <- array(NA, dim = c(n, K, Lj))
    lower_array     <- array(NA, dim = c(n, K, Lj))
    upper_array     <- array(NA, dim = c(n, K, Lj))

    which_pointmass <- abs(g$a) < 10 ^ -14 & abs(g$b) < 10 ^ -14

    ## compute carray
    for (seindex in 1:n) {
        cerr <- errordist[[seindex]]

        topleft <- matrix(apply(cbind(c(matrix(rep(cerr$b, K), nrow = K, byrow = TRUE)),
                               c(matrix(rep(betahat[seindex] - g$a, Lj), nrow = K))),
                               1, min), nrow = K)

        topright <- matrix(apply(cbind(c(matrix(rep(cerr$a, K), nrow = K, byrow = TRUE)),
                               c(matrix(rep(betahat[seindex] - g$b, Lj), nrow = K))),
                               1, max), nrow = K)

        denom <- outer(g$b - g$a, cerr$b - cerr$a, FUN = "*")
        current_c <- (topleft - topright) / denom

        current_c[which_pointmass, ] <-
            (betahat[seindex] >= cerr$a & betahat[seindex] <= cerr$b) / (cerr$b - cerr$a)

        current_c[current_c < 0] <- 0

        carray[seindex, , ] <- current_c
    }


    ## weights, lower, and upper
    for (seindex in 1:n) {
        cerr  <- errordist[[seindex]]
        uncalibrated_weights_array <- outer(g$pi, cerr$pi, FUN = "*") * carray[seindex, , ]
        weights_array[seindex, , ] <- uncalibrated_weights_array / sum(uncalibrated_weights_array)

        lower_current <- matrix(apply(cbind(c(matrix(rep(betahat[seindex] - cerr$b, K),
                                                     nrow = K, byrow = TRUE)),
                                            c(matrix(rep(g$a, Lj), nrow = K, byrow = FALSE))),
                                      1, max), nrow = K)

        upper_current <- matrix(apply(cbind(c(matrix(rep(betahat[seindex] - cerr$a, K),
                                                     nrow = K, byrow = TRUE)),
                                            c(matrix(rep(g$b, Lj), nrow = K, byrow = FALSE))),
                                      1, min), nrow = K)

        which_neg <- upper_current < lower_current

        weights_array[seindex, , ][which_neg] <- 0
        lower_current[which_neg] <- 42 ## arbitrary since have weight zero
        upper_current[which_neg] <- 43
        
        assertthat::assert_that(abs(sum(weights_array[seindex, ,]) - 1) < 10 ^ -14) ## this will break it if above code actually does anything

        lower_array[seindex, , ] <- lower_current
        upper_array[seindex, , ] <- upper_current

        lower_array[seindex, which_pointmass, ] <- 0
        upper_array[seindex, which_pointmass, ] <- 0
    }
    assertthat::assert_that(all(upper_array >= lower_array))

    post_list <- list(weights = weights_array,
                      lower = lower_array,
                      upper = upper_array)

    class(post_list) <- "unimix_array"
    return(post_list)
}


#' Compute mean from a mix_array.
#'
#' @param mixdist Of class \code{unimix_array},
#'     \code{normalmix_array}, or \code{truncnormalmix_array}. These
#'     are the classes of the output from \code{\link{post_mix_dist}}.
#'
#' @author David Gerard
mix_mean_array <- function(mixdist) {
      UseMethod("mix_mean_array")
}
mix_mean_array.default <- function(mixdist) {
    stop(paste("Error: Invalid class", class(mixdist), "for argument in", match.call()))
}
mix_mean_array.normalmix_array <- function(mixdist) {
    return(apply(mixdist$means * mixdist$weights, 1, sum))
}
mix_mean_array.truncnormalmix_array <- function(mixdist) {
    which_pointmass <- abs(mixdist$variances) < 10 ^ -14 | (abs(mixdist$lower) < 10 ^ -14 & abs(mixdist$upper) < 10 ^ -14)

    sdarray <- sqrt(mixdist$variances)
    alpha <- (mixdist$lower - mixdist$means) / sdarray
    beta  <- (mixdist$upper - mixdist$means) / sdarray

    ## numerically unstable way
    ## Z <- stats::pnorm(beta) - stats::pnorm(alpha)
    ## num <- stats::dnorm(alpha) - stats::dnorm(beta)
    ## postmeans <- mixdist$mean + num / Z * sdarray
    ## postmeans[which_pointmass] <- 0
    ## outvec1 <- apply(postmeans * mixdist$weights, 1, sum)
    ## badmeans <- postmeans == Inf

    ## numerically stable way
    which_switch <- alpha > 0
    Z                  <- array(NA, dim = dim(alpha))
    num                <- array(NA, dim = dim(alpha))
    Z[!which_switch]   <- stats::pnorm(beta[!which_switch]) - stats::pnorm(alpha[!which_switch])
    num[!which_switch] <- stats::dnorm(alpha[!which_switch]) - stats::dnorm(beta[!which_switch])
    Z[which_switch]    <- stats::pnorm(-alpha[which_switch]) - stats::pnorm(-beta[which_switch])
    num[which_switch]  <- stats::dnorm(-alpha[which_switch]) - stats::dnorm(-beta[which_switch])
    postmeans <- mixdist$mean + num / Z * sdarray
    postmeans[which_pointmass] <- 0
    
    ## Stupid hack to deal with numerical instability. Might not be correct:
    postmeans[is.nan(postmeans)] <- 0
    postmeans[postmeans == Inf] <- 0
    postmeans[postmeans == -Inf] <- 0
    
    outvec <- apply(postmeans * mixdist$weights, 1, sum)
    
    ## truncnorm method
    ## postmeans2 <- truncnorm::etruncnorm(a = mixdist$lower, b = mixdist$upper,
    ##                                    mean = mixdist$means, sd = sqrt(mixdist$variances))
    ## postmeans2 <- array(postmeans2, dim = dim(mixdist$means))

    return(outvec)
}
mix_mean_array.unimix_array <- function(mixdist) {
    return(apply( (mixdist$upper + mixdist$lower) / 2 * mixdist$weights, 1, sum))
}

#' Compute the probability of zero.
#'
#' @inheritParams mix_mean_array
#'
#' @author David Gerard
mix_probzero_array <- function(mixdist) {
    UseMethod("mix_probzero_array")
}
mix_probzero_array.default <- function(mixdist) {
    stop(paste("Error: Invalid class", class(mixdist), "for argument in", match.call()))
}
mix_probzero_array.normalmix_array <- function(mixdist) {
    which_pointmass <- abs(mixdist$variances) < 10 ^ -14
    return(apply(mixdist$weights * which_pointmass, 1, sum))
}
mix_probzero_array.truncnormalmix_array <- function(mixdist) {
    which_pointmass <- abs(mixdist$variances) < 10 ^ -14 | (abs(mixdist$lower) < 10 ^ -14 & abs(mixdist$upper) < 10 ^ -14)
    return(apply(mixdist$weights * which_pointmass, 1, sum))
}
mix_probzero_array.unimix_array <- function(mixdist) {
    which_pointmass <- abs(mixdist$lower) < 10 ^ -14 & abs(mixdist$upper) < 10 ^ -14
    return(apply(mixdist$weights * which_pointmass, 1, sum))
}



#' Compute SD from a mix_array.
#'
#' @param mixdist Of class \code{unimix_array},
#'     \code{normalmix_array}, or \code{truncnormalmix_array}. These
#'     are the classes of the output from \code{\link{post_mix_dist}}.
#'
#'
#' @return The posterior standard deviations of the location
#'     parameters.
#'
#' @author David Gerard
mix_sd_array <- function(mixdist) {
      UseMethod("mix_sd_array")
}
mix_sd_array.normalmix_array <- function(mixdist) {
    second_moment <- apply(mixdist$weights * (mixdist$means ^ 2 + mixdist$variances), 1, sum)
    first_moment2 <- apply(mixdist$weights * mixdist$means, 1, sum) ^ 2
    return(sqrt(second_moment - first_moment2))
}
mix_sd_array.truncnormalmix_array <- function(mixdist) {
    which_pointmass <- abs(mixdist$variances) < 10 ^ -14 | (abs(mixdist$lower) < 10 ^ -14 & abs(mixdist$upper) < 10 ^ -14)

    sdarray <- sqrt(mixdist$variances)
    alpha <- (mixdist$lower - mixdist$means) / sdarray
    beta  <- (mixdist$upper - mixdist$means) / sdarray
    which_switch <- alpha > 0
    Z                  <- array(NA, dim = dim(alpha))
    num                <- array(NA, dim = dim(alpha))
    Z[!which_switch]   <- stats::pnorm(beta[!which_switch]) - stats::pnorm(alpha[!which_switch])
    num[!which_switch] <- stats::dnorm(alpha[!which_switch]) - stats::dnorm(beta[!which_switch])
    Z[which_switch]    <- stats::pnorm(-alpha[which_switch]) - stats::pnorm(-beta[which_switch])
    num[which_switch]  <- stats::dnorm(-alpha[which_switch]) - stats::dnorm(-beta[which_switch])
    postmeans <- mixdist$mean + num / Z * sdarray
    postmeans[which_pointmass] <- 0

    ## truncnorm method
    ## postmeans <- truncnorm::etruncnorm(a = mixdist$lower, b = mixdist$upper,
    ##                                    mean = mixdist$means, sd = sqrt(mixdist$variances))
    ## postmeans <- array(postmeans, dim = dim(mixdist$means))

    postvars <- (1 + (alpha * stats::dnorm(alpha) - beta * stats::dnorm(beta)) / Z -
                 ( (stats::dnorm(alpha) - stats::dnorm(beta)) / Z) ^ 2) * mixdist$variances
    postvars[which_pointmass] <- 0

    ## truncnorm method
    ## postvars <- truncnorm::vtruncnorm(a = mixdist$lower, b = mixdist$upper,
    ##                                   mean = mixdist$means, sd = sqrt(mixdist$variances))
    ## postvars <- array(postvars, dim = dim(mixdist$means))

    second_moment <- apply(mixdist$weights * (postmeans ^ 2 + postvars), 1, sum)
    first_moment2 <- apply(mixdist$weights * postmeans, 1, sum) ^ 2
    return(sqrt(second_moment - first_moment2))
}
mix_sd_array.unimix_array <- function(mixdist) {
    which_pointmass <- abs(mixdist$lower) < 10 ^ -14 & abs(mixdist$upper) < 10 ^ -14
    postmeans <- (mixdist$upper + mixdist$lower) / 2
    postvars  <- (mixdist$upper - mixdist$lower) ^ 2 / 12
    postmeans[which_pointmass] <- 0
    postvars[which_pointmass] <- 0
    second_moment <- apply(mixdist$weights * (postmeans ^ 2 + postvars), 1, sum)
    first_moment2 <- apply(mixdist$weights * postmeans, 1, sum) ^ 2
    return(sqrt(second_moment - first_moment2))
}


#' Compute cdf from a mix_array.
#'
#' @param mixdist Of class \code{unimix_array},
#'     \code{normalmix_array}, or \code{truncnormalmix_array}. These
#'     are the classes of the output from \code{\link{post_mix_dist}}.
#' @param q The quantile.
#'
#' @return The probability of being less than or equal to q.
#'
#' @author David Gerard
mix_cdf_array <- function(mixdist, q) {
      UseMethod("mix_cdf_array")
}
mix_cdf_array.normalmix_array <- function(mixdist, q) {
    pless <- stats::pnorm(q = q, mean = mixdist$means, sd = sqrt(mixdist$variances))
    return(apply(pless * mixdist$weights, 1, sum))
}
mix_cdf_array.truncnormalmix_array <- function(mixdist, q) {
    which_pointmass <- abs(mixdist$variances) < 10 ^ -14 | (abs(mixdist$lower) < 10 ^ -14 & abs(mixdist$upper) < 10 ^ -14)

    sdarray <- sqrt(mixdist$variances)
    alpha <- (mixdist$lower - mixdist$means) / sdarray
    beta  <- (mixdist$upper - mixdist$means) / sdarray
    xi    <- (q - mixdist$mean) / sdarray
    which_switch <- alpha > 0
    Z                  <- array(NA, dim = dim(alpha))
    num                <- array(NA, dim = dim(alpha))
    Z[!which_switch]   <- stats::pnorm(beta[!which_switch]) - stats::pnorm(alpha[!which_switch])
    num[!which_switch] <- stats::pnorm(xi[!which_switch]) - stats::pnorm(alpha[!which_switch])
    Z[which_switch]    <- stats::pnorm(-alpha[which_switch]) - stats::pnorm(-beta[which_switch])
    num[which_switch]  <- stats::pnorm(-alpha[which_switch]) - stats::pnorm(-xi[which_switch])
    pless <- num / Z
    pless[q < mixdist$lower] <- 0
    pless[q > mixdist$upper] <- 1
    pless[which_pointmass] <- (q >= 0) * 1
    
    ## stupid hack that needs to be fixed
    pless[is.nan(pless)] <- 0
    
    assertthat::assert_that(all(!is.nan(pless)))
    

    ## truncnorm package way
    ## pless1 <- truncnorm::ptruncnorm(q = q, a = mixdist$lower, b = mixdist$upper,
    ##                                mean = mixdist$means, sd = sqrt(mixdist$variances))
    ## pless1 <- array(pless1, dim = dim(mixdist$means))
    ## pless1[which_pointmass] <- (q >= 0) * 1

    return(apply(pless * mixdist$weights, 1, sum))
}
mix_cdf_array.unimix_array <- function(mixdist, q) {
    which_pointmass <- abs(mixdist$lower) < 10 ^ -14 & abs(mixdist$upper) < 10 ^ -14
    pless <- stats::punif(q = q, min = mixdist$lower, max = mixdist$upper)
    pless[which_pointmass] <- (q >= 0) * 1
    return(apply(pless * mixdist$weights, 1, sum))
}


#' Calculate log-likelihood.
#'
#' @author David Gerard
#'
#' @inheritParams log_compdens_conv_mix
calc_loglik_array <- function(g, betahat, errordist) {
    matrix_llik <- log_compdens_conv_mix(g, betahat, errordist)
    maxmat <- apply(matrix_llik, 2, max)
    matrix_llik_norm <- t(t(matrix_llik) - maxmat)
    llike <- sum(log(colSums(exp(matrix_llik_norm) * g$pi)) + maxmat)
    return(llike)
}

#' Calculate the log-likelihood when all null.
#'
#' @author David Gerard
#'
#' @inheritParams log_compdens_conv_mix
calc_nulllik_array <- function(betahat, errordist) {
    g <- normalmix(pi = 1, mean = 0, sd = 0)
    matrix_llik <- log_compdens_conv_mix(g, betahat, errordist)
    return(sum(matrix_llik))
}



#' Generate an object of class \code{normalmix} that is an
#' approximation to a t-distribution.
#'
#' This is based on the representation of the t as an inverse-gamma
#' scaled-mixture of Gaussians.
#'
#' @param mu The mean of the t-distribution.
#' @param sig The scale parameter of the t-distribution.
#' @param df The degrees of freedom of the t-distribution.
#' @param gridsize The number of mixture components to use. The larger
#'     the more accurate the approximation, but the higher the
#'     computational load --- especially if you intend to use this for
#'     \code{\link{stramash.workhorse}}.
#' @param eps A positive numeric. The tolerance from 0 and 1 for the
#'     grid in the probability space.
#'
#' @export
#'
#' @author David Gerard
t_to_mix <- function(mu, sig, df, gridsize = 20, eps = 10 ^ -6) {
    pgrid <- seq(eps, 1 - eps, length = gridsize + 1)

    shape_param <- df / 2
    rate_param  <- df * sig ^ 2 / 2

    mean_grid      <- rep(mu, length = gridsize)
    temp_grid      <- stats::qgamma(p = pgrid, shape = shape_param, rate = rate_param)
    precision_grid <- (temp_grid[2:length(temp_grid)] +
                       temp_grid[1:(length(temp_grid) - 1)]) / 2

    weight_grid <- rep(1 / gridsize, length = gridsize)

    tapprox <- normalmix(pi = weight_grid, mean = mean_grid, sd = 1 / sqrt(precision_grid))

    return(tapprox)
}



#' Generate an object of class \code{normalmix} that is an
#' approximation to a Laplace distribution.
#'
#' This is based on the representation of the Laplace distribution as
#' an exponential scale mixtures of normals.
#'
#' @param mu The location of the Laplace distribution.
#' @param sig The scale parameter of the Laplace distribution.
#' @param gridsize The number of mixture components to use. The larger
#'     the more accurate the approximation, but the higher the
#'     computational load --- especially if you intend to use this for
#'     \code{\link{stramash.workhorse}}.
#' @param eps A positive numeric. The tolerance from 0 and 1 for the
#'     grid in the probability space.
#'
#' @export
#'
#' @author David Gerard
laplace_to_mix <- function(mu, sig, gridsize = 20, eps = 10 ^ -6) {
    if (!requireNamespace("VGAM", quietly = TRUE)) {
        stop("VGAM must be installed to use laplace_to_mix")
    }
    pgrid <- seq(eps, 1 - eps, length = gridsize + 1)

    mean_grid <- rep(mu, length = gridsize)
    temp_grid <- VGAM::qrayleigh(p = pgrid, scale = sig)
    sd_grid  <- (temp_grid[2:length(temp_grid)] +
                  temp_grid[1:(length(temp_grid) - 1)]) / 2

    weight_grid <- rep(1 / gridsize, length = gridsize)

    lapprox <- normalmix(pi = weight_grid, mean = mean_grid, sd = sd_grid)

    return(lapprox)
}

#' Random draw from a mixture of normals.
#'
#' @param n the number of samples to draw.
#' @param mixdense An object of class \code{normalmix}.
#'
#' @author David Gerard
#'
#' @export
rnormalmix <- function(n, mixdense) {
    assertthat::are_equal(class(mixdense), "normalmix")
    k <- length(mixdense$pi)
    which_norm <- sample(1:k, size = n, prob = mixdense$pi, replace = TRUE)
    samp <- stats::rnorm(n = n, mean = mixdense$mean[which_norm], sd = mixdense$sd[which_norm])
    return(samp)
}

#' CDF function for a mixture of normals.
#'
#' @param q The quantile.
#' @param mixdense An object of class \code{normalmix}.
#'
#' @author David Gerard
#'
#' @export
pnormalmix <- function(q, mixdense) {
    assertthat::are_equal(class(mixdense), "normalmix")
    pout <- sum(mixdense$pi * stats::pnorm(q = q, mean = mixdense$mean, sd = mixdense$sd))
    return(pout)
}

#' Density function for a mixture of normals.
#'
#' @param x The location to calculate the density.
#' @param mixdense An object of class \code{normalmix}.
#'
#' @author David Gerard
#'
#' @export
dnormalmix <- function(x, mixdense) {
    assertthat::are_equal(class(mixdense), "normalmix")
    dout <- sum(mixdense$pi * stats::dnorm(x = x, mean = mixdense$mean, sd = mixdense$sd))
    return(dout)
}

#' Random draw from a mixture of uniforms.
#'
#' @param n the number of samples to draw.
#' @param mixdense An object of class \code{unimix}.
#'
#' @author David Gerard
#'
#' @export
runimix <- function(n, mixdense) {
    assertthat::are_equal(class(mixdense), "unimix")
    k <- length(mixdense$pi)
    which_uni <- sample(1:k, size = n, prob = mixdense$pi, replace = TRUE)
    samp <- stats::runif(n = n, min = mixdense$a[which_uni], max = mixdense$b[which_uni])
    return(samp)
}

#' Density function for mixture of uniforms.
#'
#' @param x The location to calculate the density.
#' @param mixdense An object of class \code{unimix}.
#'
#' @author David Gerard
#'
#' @export
dunimix <- function(x, mixdense) {
    assertthat::are_equal(class(mixdense), "unimix")
    dout <- colSums(sapply(X = x, FUN = stats::dunif,
                           min = mixdense$a, max = mixdense$b) *
                    mixdense$pi)
    return(dout)
}

#' Probability function for mixture of uniforms.
#'
#' @param p The location to calculate the density.
#' @param mixdense An object of class \code{unimix}.
#'
#' @author David Gerard
#'
#' @export
punimix <- function(p, mixdense) {
    assertthat::are_equal(class(mixdense), "unimix")
    dout <- colSums(sapply(X = p, FUN = stats::punif,
                           min = mixdense$a, max = mixdense$b) *
                    mixdense$pi)
    return(dout)
}


#' Standard deviation of either class \code{\link[ashr]{normalmix}} or
#'     class \code{\link[ashr]{unimix}}
#'
#' @param m An object either of class \code{\link[ashr]{normalmix}} or
#'     of class \code{\link[ashr]{unimix}}.
#'
#' @author David Gerard
#'
#' @export
mixsd <- function(m) {
    UseMethod("mixsd")
}
mixsd.default <- function(m) {
    stop(paste("Error: Invalid class", class(m), "for argument in", match.call()))
}
mixsd.normalmix <- function(m) {
    sdout <- sqrt(sum((m$mean ^ 2 + m$sd ^ 2) * m$pi) - sum(m$mean * m$pi) ^ 2)
    return(sdout)
}
mixsd.unimix <- function(m) {
    mean_val <- (m$a + m$b) / 2
    var_val  <- (m$b - m$a) ^ 2 / 12
    sdout <- sqrt(sum((mean_val ^ 2 +  var_val) * m$pi) - sum(mean_val * m$pi) ^ 2)
    return(sdout)
}



#' Mean of either class \code{\link[ashr]{normalmix}} or class
#'     \code{\link[ashr]{unimix}}
#'
#' @param m An object either of class \code{\link[ashr]{normalmix}} or
#'     of class \code{\link[ashr]{unimix}}.
#'
#' @author David Gerard
#'
#' @export
mixmean <- function(m) {
    UseMethod("mixmean")
}
mixmean.default <- function(m) {
    stop(paste("Error: Invalid class", class(m), "for argument in", match.call()))
}
mixmean.normalmix <- function(m) {
    return(sum(m$mean * m$pi))
}
mixmean.unimix <- function(m) {
    return(sum((m$a + m$b) / 2 * m$pi))
}

#' Approximate the density function of any distribution given its
#' quantile function.
#'
#' @param q_func A quantile function.
#' @param gridsize The number of points to approximate.
#' @param eps A positive numeric. The tolerance from 0 or 1 to start
#'     the grid.
#'
quantile_to_mix <- function(q_func, gridsize = 20, eps = 10 ^ -6) {
    assertthat::assert_that(eps > 0)
    assertthat::assert_that(is.function(q_func))
    pgrid <- seq(eps, 1 - eps, length = gridsize + 1)
    quants <- do.call(what = q_func, args = list(p = pgrid))
    lower_vals <- quants[1:(length(quants) - 1)]
    upper_vals <- quants[2:length(quants)]
    weights <- rep(1 / gridsize, gridsize)
    mixdense <- unimix(pi = weights, a = lower_vals, b = upper_vals)
    return(mixdense)
}
dcgerard/stramash documentation built on May 15, 2019, 1:24 a.m.