Nothing
library(testthat)
library(likelihood.model)
# Helper function to get parameter estimates from MLE result
# fisher_mle stores estimates in $par, accessible via coef()
get_mle_params <- function(mle_result) {
# Use coef() for fisher_mle objects
result <- coef(mle_result)
if (!is.null(result)) {
return(result)
}
# Fallback: try $par directly
if (!is.null(mle_result$par)) {
return(mle_result$par)
}
return(NULL)
}
# =============================================================================
# IS_LIKELIHOOD_MODEL TESTS
# =============================================================================
test_that("is_likelihood_model correctly identifies likelihood models", {
# exponential_lifetime is a likelihood model
model_exp <- exponential_lifetime("t")
expect_true(is_likelihood_model(model_exp))
# Non-likelihood model objects return FALSE
expect_false(is_likelihood_model(list()))
expect_false(is_likelihood_model(data.frame()))
expect_false(is_likelihood_model(NULL))
expect_false(is_likelihood_model("not a model"))
expect_false(is_likelihood_model(42))
})
# =============================================================================
# FIT.LIKELIHOOD_MODEL TESTS
# =============================================================================
test_that("fit.likelihood_model finds correct MLE for exponential data", {
set.seed(999)
true_rate <- 2.0
n <- 200
df <- data.frame(t = rexp(n, rate = true_rate))
model <- exponential_lifetime("t")
result <- fit(model)(df)
estimated <- get_mle_params(result)
# Closed-form MLE: lambda_hat = n / sum(t)
expect_equal(unname(estimated), n / sum(df$t), tolerance = 1e-12)
# Should be close to true value
expect_lt(abs(estimated[1] - true_rate), 0.5)
})
# =============================================================================
# SAMPLER.LIKELIHOOD_MODEL TESTS
# =============================================================================
test_that("sampler.likelihood_model generates bootstrap samples", {
skip_if_not_installed("boot")
set.seed(1002)
df <- data.frame(t = rexp(200, rate = 2))
model <- exponential_lifetime("t")
# Create sampler
model_sampler <- sampler(model, df = df, par = c(lambda = 2))
# Generate bootstrap samples
boot_result <- model_sampler(n = 10)
# Should return a fisher_boot object
expect_true(inherits(boot_result, "fisher_boot"))
expect_true(inherits(boot_result, "fisher_mle"))
# Bootstrap estimates should be stored in replicates
expect_equal(nrow(boot_result$replicates), 10)
expect_equal(ncol(boot_result$replicates), 1) # rate only
# coef() should return original MLE
expect_equal(length(coef(boot_result)), 1)
})
test_that("sampler.likelihood_model bootstrap distribution is centered near MLE", {
skip_if_not_installed("boot")
set.seed(1003)
true_rate <- 2.0
df <- data.frame(t = rexp(200, rate = true_rate))
model <- exponential_lifetime("t")
# First fit to get MLE
mle_result <- fit(model)(df)
mle_params <- get_mle_params(mle_result)
# Create sampler and generate bootstrap samples
model_sampler <- sampler(model, df = df, par = c(lambda = 2))
boot_result <- model_sampler(n = 50)
# Bootstrap mean should be close to MLE
boot_mean <- colMeans(boot_result$replicates)
expect_equal(unname(boot_mean[1]), unname(mle_params[1]), tolerance = 0.5)
})
# =============================================================================
# LRT (LIKELIHOOD RATIO TEST) TESTS
# =============================================================================
test_that("lrt requires likelihood models as inputs", {
df <- data.frame(t = rexp(10, rate = 1))
expect_error(lrt(list(), exponential_lifetime("t"),
df, null_par = c(1), alt_par = c(2)))
})
test_that("lrt computes correct test statistic", {
set.seed(1004)
df <- data.frame(t = rexp(100, rate = 1))
model <- exponential_lifetime("t")
ll_func <- loglik(model)
null_par <- c(1.0)
alt_par <- c(1.05)
null_ll <- ll_func(df, null_par)
alt_ll <- ll_func(df, alt_par)
result <- lrt(model, model, df,
null_par = null_par, alt_par = alt_par, dof = 1)
expected_stat <- -2 * (null_ll - alt_ll)
expect_equal(result$stat, expected_stat, tolerance = 1e-10)
expect_equal(result$dof, 1)
expect_true(result$p.value >= 0 && result$p.value <= 1)
})
test_that("lrt p-value is computed correctly", {
set.seed(1005)
df <- data.frame(t = rexp(200, rate = 2))
model <- exponential_lifetime("t")
null_par <- c(2.0)
mle <- fit(model)(df)
alt_par <- coef(mle)
result <- lrt(model, model, df,
null_par = null_par, alt_par = alt_par, dof = 1)
# Test statistic should be non-negative
expect_true(result$stat >= 0)
# p-value should be in [0, 1]
expect_true(result$p.value >= 0 && result$p.value <= 1)
})
test_that("lrt auto-calculates degrees of freedom", {
set.seed(1006)
df <- data.frame(t = rexp(50, rate = 2))
model <- exponential_lifetime("t")
result <- lrt(model, model, df,
null_par = c(1),
alt_par = c(1, 0.5),
dof = NULL)
expect_equal(result$dof, 1)
expect_true(!is.null(result$stat))
expect_true(!is.null(result$p.value))
})
# =============================================================================
# EDGE CASES AND ERROR HANDLING
# =============================================================================
test_that("loglik handles data with single observation", {
df <- data.frame(t = 5.0)
model <- exponential_lifetime("t")
ll_func <- loglik(model)
expected_ll <- dexp(5.0, rate = 2, log = TRUE)
computed_ll <- ll_func(df, c(2))
expect_equal(computed_ll, expected_ll, tolerance = 1e-10)
})
# =============================================================================
# PRINT.LIKELIHOOD_MODEL TESTS
# =============================================================================
test_that("print.likelihood_model outputs model information", {
model <- exponential_lifetime("t")
# Capture output
output <- capture.output(print(model))
# Check that key information is printed
expect_true(any(grepl("Likelihood model", output)))
expect_true(any(grepl("Observation column", output)))
})
test_that("print.likelihood_model with show.loglik=TRUE shows loglik function", {
model <- exponential_lifetime("t")
output <- capture.output(print(model, show.loglik = TRUE))
expect_true(any(grepl("Log-likelihood function", output)))
})
test_that("print.likelihood_model returns model invisibly", {
model <- exponential_lifetime("t")
result <- print(model)
expect_identical(result, model)
})
# =============================================================================
# LRT WITH NULL DOF TESTS
# =============================================================================
test_that("lrt computes dof automatically when NULL", {
set.seed(1014)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
null_par <- c(1.5)
alt_par <- c(2.0)
# When dof is NULL, it should be computed as length(alt_par) - length(null_par)
result <- lrt(model, model, df,
null_par = null_par, alt_par = alt_par, dof = NULL)
# dof should be 0 (1 - 1)
expect_equal(result$dof, 0)
expect_true(!is.null(result$stat))
expect_true(!is.null(result$p.value))
})
# =============================================================================
# FIT WITH SANN METHOD TESTS
# =============================================================================
test_that("fit.likelihood_model works with SANN method (no gradient)", {
set.seed(1015)
df <- data.frame(t = rexp(100, rate = 2))
# Use the generic fit.likelihood_model (not exponential_lifetime's override)
# by creating a mock model that delegates to exponential loglik
mock_model <- structure(
list(ob_col = "t", censor_col = NULL),
class = c("mock_exp_sann", "likelihood_model")
)
loglik.mock_exp_sann <<- function(model, ...) {
function(df, par, ...) {
lambda <- par[1]
if (lambda <= 0) return(-.Machine$double.xmax / 2)
n <- nrow(df)
n * log(lambda) - lambda * sum(df[[model$ob_col]])
}
}
solver <- fit(mock_model)
result <- solver(df, par = c(1), method = "SANN",
control = list(maxit = 1000))
params_val <- get_mle_params(result)
expect_true(!is.null(params_val))
expect_true(!any(is.na(params_val)))
# Parameters should be reasonable
expect_lt(abs(params_val[1] - 2), 1.5)
rm(loglik.mock_exp_sann, envir = .GlobalEnv)
})
# =============================================================================
# FIM GENERIC TESTS
# =============================================================================
test_that("fim generic dispatches correctly", {
# fim is a generic that requires a method
# Test that it exists and errors appropriately for objects without methods
expect_error(fim(list()), "no applicable method")
})
# =============================================================================
# FISHER_MLE CLASS TESTS
# =============================================================================
test_that("fisher_mle creates object with correct structure", {
result <- fisher_mle(
par = c(mean = 0, sd = 1),
loglik_val = -50.5,
hessian = matrix(c(-100, 0, 0, -50), 2, 2),
nobs = 100
)
expect_true(inherits(result, "fisher_mle"))
expect_true(inherits(result, "mle_fit"))
# Test base R generic methods
expect_equal(coef(result), c(mean = 0, sd = 1))
expect_true(!is.null(vcov(result)))
expect_equal(nobs(result), 100)
# logLik should return a logLik object
ll <- logLik(result)
expect_true(inherits(ll, "logLik"))
expect_equal(as.numeric(ll), -50.5)
})
test_that("fisher_mle confint computes Wald confidence intervals", {
result <- fisher_mle(
par = c(mean = 5, sd = 2),
vcov = matrix(c(0.04, 0, 0, 0.02), 2, 2), # SE = 0.2 and ~0.14
loglik_val = -100,
nobs = 100
)
ci <- confint(result, level = 0.95)
# Check structure
expect_true(is.matrix(ci))
expect_equal(nrow(ci), 2)
expect_equal(ncol(ci), 2)
# Intervals should be centered around estimates
expect_lt(ci[1, 1], 5) # lower < estimate
expect_gt(ci[1, 2], 5) # upper > estimate
})
test_that("fisher_mle se returns standard errors", {
result <- fisher_mle(
par = c(a = 1, b = 2),
vcov = matrix(c(0.25, 0, 0, 0.16), 2, 2), # SE = 0.5 and 0.4
loglik_val = -50
)
se_vals <- se(result)
expect_equal(se_vals, c(0.5, 0.4))
})
test_that("fisher_mle AIC and BIC compute correctly via stats generics", {
result <- fisher_mle(
par = c(a = 1, b = 2),
loglik_val = -100,
nobs = 50
)
# AIC = -2*logL + 2*k = -2*(-100) + 2*2 = 204
expect_equal(stats::AIC(result), 204)
# BIC = -2*logL + k*log(n) = 200 + 2*log(50)
expected_bic <- 200 + 2 * log(50)
expect_equal(stats::BIC(result), expected_bic)
})
test_that("fisher_mle summary produces correct output", {
result <- fisher_mle(
par = c(mean = 5, sd = 2),
vcov = matrix(c(0.04, 0, 0, 0.02), 2, 2),
loglik_val = -100,
nobs = 100
)
summ <- summary(result)
expect_true(inherits(summ, "summary_fisher_mle"))
expect_true(!is.null(summ$coefficients))
expect_equal(summ$loglik, -100)
expect_equal(summ$nobs, 100)
})
test_that("fit.likelihood_model returns fisher_mle object", {
set.seed(2001)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
result <- fit(model)(df)
expect_true(inherits(result, "fisher_mle"))
expect_true(inherits(result, "mle_fit"))
expect_true(result$converged)
expect_equal(result$nobs, 100)
})
# =============================================================================
# FISHERIAN INFERENCE TESTS
# =============================================================================
test_that("support function computes log relative likelihood", {
set.seed(2002)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
mle_result <- fit(model)(df)
# Support at MLE should be 0
s_at_mle <- support(mle_result, coef(mle_result), df, model)
expect_equal(unname(s_at_mle), 0, tolerance = 1e-10)
# Support at other values should be negative
s_away <- support(mle_result, c(lambda = 0.5), df, model)
expect_true(s_away < 0)
})
test_that("relative_likelihood computes exp(support)", {
set.seed(2003)
df <- data.frame(t = rexp(50, rate = 2))
model <- exponential_lifetime("t")
mle_result <- fit(model)(df)
# Relative likelihood at MLE should be 1
rl_at_mle <- relative_likelihood(mle_result, coef(mle_result), df, model)
expect_equal(unname(rl_at_mle), 1, tolerance = 1e-10)
# Relative likelihood elsewhere should be between 0 and 1
rl_away <- relative_likelihood(mle_result, c(lambda = 0.5), df, model)
expect_true(rl_away > 0 && rl_away < 1)
})
test_that("likelihood_interval works for single parameter model", {
set.seed(2004)
df <- data.frame(t = rexp(200, rate = 2))
model <- exponential_lifetime("t")
mle_result <- fit(model)(df)
# Compute 1/8 likelihood interval
li <- likelihood_interval(mle_result, df, model, k = 8)
expect_true(inherits(li, "likelihood_interval"))
expect_equal(nrow(li), 1)
expect_equal(ncol(li), 2)
# Interval should contain MLE
mle_val <- unname(coef(mle_result))
expect_true(li[1, 1] < mle_val && mle_val < li[1, 2])
})
test_that("profile_loglik computes profile for single parameter", {
set.seed(2005)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
mle_result <- fit(model)(df)
# Profile for first parameter (lambda)
prof <- profile_loglik(mle_result, df, model, param = 1, n_grid = 20)
expect_true(inherits(prof, "profile_loglik"))
expect_equal(nrow(prof), 20)
expect_true("loglik" %in% names(prof))
expect_true("support" %in% names(prof))
expect_true("relative_likelihood" %in% names(prof))
# Maximum profile loglik should be near MLE
max_idx <- which.max(prof$loglik)
expect_equal(unname(prof[[1]][max_idx]), unname(coef(mle_result)[1]), tolerance = 0.5)
})
test_that("deviance computes correctly for fisher_mle", {
result <- fisher_mle(
par = c(a = 1),
loglik_val = -50,
nobs = 100
)
# Single model deviance = -2 * logL = 100
expect_equal(deviance(result), 100)
# Comparison deviance
null_result <- fisher_mle(
par = c(a = 0.5),
loglik_val = -55,
nobs = 100
)
# Deviance = 2*(logL_full - logL_null) = 2*(-50 - (-55)) = 10
expect_equal(deviance(result, null_result), 10)
})
test_that("evidence computes log likelihood ratio", {
set.seed(2006)
df <- data.frame(t = rexp(50, rate = 2))
model <- exponential_lifetime("t")
# Evidence for theta1 vs theta2
ev <- evidence(model, df, c(lambda = 2), c(lambda = 0.5))
# Should strongly favor true parameters (2) over (0.5)
expect_true(ev > 0)
expect_true(ev > log(8)) # Strong evidence
})
# =============================================================================
# FISHER_BOOT CLASS TESTS
# =============================================================================
test_that("fisher_boot inherits from fisher_mle", {
skip_if_not_installed("boot")
set.seed(2007)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
model_sampler <- sampler(model, df = df, par = c(lambda = 2))
boot_result <- model_sampler(n = 20)
expect_true(inherits(boot_result, "fisher_boot"))
expect_true(inherits(boot_result, "fisher_mle"))
expect_true(inherits(boot_result, "mle_fit"))
# Base methods should work
expect_equal(length(coef(boot_result)), 1)
expect_true(is.matrix(vcov(boot_result)))
})
test_that("fisher_boot bias computes from bootstrap replicates", {
skip_if_not_installed("boot")
set.seed(2008)
df <- data.frame(t = rexp(200, rate = 2))
model <- exponential_lifetime("t")
model_sampler <- sampler(model, df = df, par = c(lambda = 2))
boot_result <- model_sampler(n = 50)
# Bias should be computed from replicates
b <- bias(boot_result)
expect_equal(length(b), 1)
# Expected: mean(replicates) - original MLE
expected_bias <- colMeans(boot_result$replicates) - coef(boot_result)
expect_equal(b, expected_bias)
})
test_that("sampler generic works on fisher_mle for asymptotic sampling", {
result <- fisher_mle(
par = c(mean = 5, sd = 2),
vcov = matrix(c(0.04, 0, 0, 0.02), 2, 2),
loglik_val = -100,
nobs = 100
)
samp_fn <- sampler(result)
samples <- samp_fn(1000)
expect_equal(dim(samples), c(1000, 2))
# Samples should be centered near MLE
expect_equal(mean(samples[, 1]), 5, tolerance = 0.1)
expect_equal(mean(samples[, 2]), 2, tolerance = 0.1)
})
# =============================================================================
# EDGE CASE AND BUG FIX VALIDATION TESTS
# =============================================================================
test_that("lrt with dof=NULL computes dof from parameter lengths", {
set.seed(3001)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
# alt_par has 2 elements, null_par has 1 => dof should be 1
null_par <- c(2)
alt_par <- c(2, 0.5)
result <- lrt(model, model, df,
null_par = null_par, alt_par = alt_par, dof = NULL)
expect_equal(result$dof, 1)
expect_true(is.finite(result$stat))
expect_true(result$p.value >= 0 && result$p.value <= 1)
})
test_that("evidence returns 0 when comparing same parameters", {
set.seed(3003)
df <- data.frame(t = rexp(50, rate = 2))
model <- exponential_lifetime("t")
# Evidence for identical parameters should be exactly 0
ev <- evidence(model, df, c(lambda = 2), c(lambda = 2))
expect_equal(unname(ev), 0)
})
test_that("fisher_mle with NULL vcov: se returns NA, sampler errors", {
result <- fisher_mle(
par = c(a = 1, b = 2),
loglik_val = -50,
nobs = 100
# vcov and hessian both NULL
)
# se should return NA values
se_vals <- se(result)
expect_equal(length(se_vals), 2)
expect_true(all(is.na(se_vals)))
# sampler should error cleanly
expect_error(sampler(result), "Cannot create sampler: vcov is NULL")
})
# =============================================================================
# EXPONENTIAL_LIFETIME TESTS
# =============================================================================
test_that("exponential_lifetime creates model with correct structure", {
model <- exponential_lifetime("t")
expect_true(is_likelihood_model(model))
expect_equal(model$ob_col, "t")
expect_null(model$censor_col)
expect_true("exponential_lifetime" %in% class(model))
model_c <- exponential_lifetime("t", censor_col = "status")
expect_equal(model_c$censor_col, "status")
})
test_that("loglik matches manual d*log(lambda) - lambda*T", {
set.seed(4001)
lambda <- 2.0
x <- rexp(100, rate = lambda)
df <- data.frame(t = x)
model <- exponential_lifetime("t")
ll_func <- loglik(model)
# Manual calculation
n <- length(x)
total_time <- sum(x)
manual_ll <- n * log(lambda) - lambda * total_time
expect_equal(ll_func(df, lambda), manual_ll, tolerance = 1e-12)
})
test_that("loglik matches sum of dexp(x, rate, log=TRUE)", {
set.seed(4002)
lambda <- 1.5
x <- rexp(80, rate = lambda)
df <- data.frame(t = x)
model <- exponential_lifetime("t")
ll_func <- loglik(model)
expected_ll <- sum(dexp(x, rate = lambda, log = TRUE))
expect_equal(ll_func(df, lambda), expected_ll, tolerance = 1e-10)
})
test_that("closed-form MLE equals d/T exactly", {
set.seed(4003)
lambda_true <- 3.0
n <- 200
x <- rexp(n, rate = lambda_true)
df <- data.frame(t = x)
model <- exponential_lifetime("t")
result <- fit(model)(df)
# Closed-form MLE: lambda_hat = n / sum(x)
expected_mle <- n / sum(x)
expect_equal(unname(coef(result)), expected_mle, tolerance = 1e-12)
# Should be close to true value
expect_lt(abs(coef(result) - lambda_true), 0.5)
# MLE should have converged
expect_true(result$converged)
expect_equal(result$nobs, n)
})
test_that("analytical score matches numDeriv::grad", {
set.seed(4004)
lambda <- 2.5
x <- rexp(50, rate = lambda)
df <- data.frame(t = x)
model <- exponential_lifetime("t")
ll_func <- loglik(model)
score_func <- score(model)
# Test at several parameter values
for (lam in c(1.0, 2.0, 3.0, 5.0)) {
analytical <- score_func(df, lam)
numerical <- numDeriv::grad(function(p) ll_func(df, p), lam)
expect_equal(unname(analytical), numerical, tolerance = 1e-6,
info = paste("Score mismatch at lambda =", lam))
}
})
test_that("analytical hessian matches numDeriv::hessian", {
set.seed(4005)
lambda <- 2.0
x <- rexp(100, rate = lambda)
df <- data.frame(t = x)
model <- exponential_lifetime("t")
ll_func <- loglik(model)
hess_func <- hess_loglik(model)
for (lam in c(1.0, 2.0, 4.0)) {
analytical <- hess_func(df, lam)
numerical <- numDeriv::hessian(function(p) ll_func(df, p), lam)
expect_equal(as.numeric(analytical), as.numeric(numerical), tolerance = 1e-4,
info = paste("Hessian mismatch at lambda =", lam))
}
})
test_that("right-censored MLE is correct", {
set.seed(4006)
lambda_true <- 2.0
n <- 200
censor_time <- 0.4
x_raw <- rexp(n, rate = lambda_true)
x_obs <- pmin(x_raw, censor_time)
status <- ifelse(x_raw > censor_time, "right", "exact")
df <- data.frame(t = x_obs, status = status)
model <- exponential_lifetime("t", censor_col = "status")
result <- fit(model)(df)
lambda_hat <- unname(coef(result))
# Verify closed-form: d / T
d <- sum(status == "exact")
total_time <- sum(x_obs)
expect_equal(lambda_hat, d / total_time, tolerance = 1e-12)
# Should be reasonable (right-censoring still gives consistent estimator)
expect_lt(abs(lambda_hat - lambda_true), 0.8)
})
test_that("score is exactly zero at MLE", {
set.seed(4007)
x <- rexp(100, rate = 1.5)
df <- data.frame(t = x)
model <- exponential_lifetime("t")
result <- fit(model)(df)
# Score at MLE should be exactly 0 (closed-form solution)
score_func <- score(model)
s <- score_func(df, coef(result))
expect_equal(unname(s), 0, tolerance = 1e-12)
# Also stored in the MLE object
expect_equal(unname(score_val(result)), 0, tolerance = 1e-12)
})
test_that("FIM equals n/lambda^2", {
model <- exponential_lifetime("t")
fim_func <- fim(model)
# FIM for n observations at lambda
for (lam in c(0.5, 1.0, 2.0, 5.0)) {
for (n_obs in c(10, 100, 1000)) {
expected_fim <- matrix(n_obs / lam^2, 1, 1)
computed_fim <- fim_func(lam, n_obs)
expect_equal(as.numeric(computed_fim), as.numeric(expected_fim),
tolerance = 1e-12,
info = paste("FIM mismatch at lambda =", lam, "n =", n_obs))
}
}
})
test_that("full Fisherian inference works with exponential_lifetime", {
set.seed(4008)
lambda_true <- 2.0
n <- 200
x <- rexp(n, rate = lambda_true)
df <- data.frame(t = x)
model <- exponential_lifetime("t")
mle_result <- fit(model)(df)
# Support at MLE should be 0
s_at_mle <- support(mle_result, coef(mle_result), df, model)
expect_equal(unname(s_at_mle), 0, tolerance = 1e-10)
# Support away from MLE should be negative
s_away <- support(mle_result, c(lambda = 0.5), df, model)
expect_true(unname(s_away) < 0)
# Relative likelihood at MLE is 1
rl_at_mle <- relative_likelihood(mle_result, coef(mle_result), df, model)
expect_equal(unname(rl_at_mle), 1, tolerance = 1e-10)
# Likelihood interval should contain MLE
li <- likelihood_interval(mle_result, df, model, k = 8)
mle_val <- unname(coef(mle_result))
expect_true(!is.na(li[1, 1]) && !is.na(li[1, 2]))
expect_true(li[1, 1] < mle_val && mle_val < li[1, 2])
})
test_that("exponential_lifetime errors with no exact observations", {
df <- data.frame(t = c(1, 2, 3), status = rep("right", 3))
model <- exponential_lifetime("t", censor_col = "status")
expect_error(fit(model)(df), "no exact")
})
test_that("exponential_lifetime rdata generates correct data", {
model <- exponential_lifetime("t")
rdata_fn <- rdata(model)
set.seed(4010)
df <- rdata_fn(theta = 2.0, n = 100)
expect_equal(nrow(df), 100)
expect_true("t" %in% names(df))
expect_true(all(df$t > 0))
# With censoring
model_c <- exponential_lifetime("t", censor_col = "status")
rdata_fn_c <- rdata(model_c)
df_c <- rdata_fn_c(theta = 2.0, n = 100, censor_time = 0.5)
expect_equal(nrow(df_c), 100)
expect_true("t" %in% names(df_c))
expect_true("status" %in% names(df_c))
expect_true(all(df_c$t <= 0.5))
expect_true(all(df_c$status %in% c("exact", "right")))
})
test_that("assumptions include censoring note when censor_col is set", {
model <- exponential_lifetime("t")
a <- assumptions(model)
expect_false("non-informative right censoring" %in% a)
model_c <- exponential_lifetime("t", censor_col = "status")
a_c <- assumptions(model_c)
expect_true("non-informative right censoring" %in% a_c)
})
# =============================================================================
# ALGEBRAIC.MLE INTERFACE COMPATIBILITY TESTS
# =============================================================================
test_that("params() returns same as coef() on fisher_mle", {
result <- fisher_mle(
par = c(shape = 2.0, scale = 1.5),
loglik_val = -50,
hessian = matrix(c(-100, 0, 0, -50), 2, 2),
nobs = 100
)
expect_equal(params(result), coef(result))
expect_equal(params(result), c(shape = 2.0, scale = 1.5))
})
test_that("nparams() returns correct count", {
# Multivariate
result2 <- fisher_mle(
par = c(shape = 2.0, scale = 1.5),
loglik_val = -50,
nobs = 100
)
expect_equal(nparams(result2), 2L)
# Univariate
result1 <- fisher_mle(
par = c(lambda = 3.0),
loglik_val = -30,
nobs = 50
)
expect_equal(nparams(result1), 1L)
})
test_that("observed_fim() returns -hessian (positive definite)", {
H <- matrix(c(-100, -5, -5, -50), 2, 2)
result <- fisher_mle(
par = c(a = 1, b = 2),
loglik_val = -50,
hessian = H,
nobs = 100
)
fim_mat <- observed_fim(result)
expect_equal(fim_mat, -H)
# Should be positive definite (all eigenvalues > 0)
evals <- eigen(fim_mat)$values
expect_true(all(evals > 0))
})
test_that("observed_fim() returns NULL when hessian is NULL", {
result <- fisher_mle(
par = c(a = 1),
loglik_val = -50,
nobs = 100
# hessian and vcov both NULL
)
expect_null(observed_fim(result))
})
test_that("obs() returns NULL for fisher_mle (by design)", {
result <- fisher_mle(
par = c(a = 1),
loglik_val = -50,
nobs = 100
)
expect_null(obs(result))
})
test_that("mse() equals vcov under zero asymptotic bias", {
V <- matrix(c(0.04, 0.01, 0.01, 0.02), 2, 2)
result <- fisher_mle(
par = c(a = 1, b = 2),
vcov = V,
loglik_val = -50,
nobs = 100
)
# Bias is zero for fisher_mle, so MSE = vcov
expect_equal(mse(result), V)
})
test_that("mse() works for univariate fisher_mle", {
result <- fisher_mle(
par = c(lambda = 2.0),
vcov = matrix(0.04, 1, 1),
loglik_val = -30,
nobs = 50
)
# Scalar case: MSE = var + 0^2 = var
expect_equal(mse(result), matrix(0.04, 1, 1))
})
test_that("params()/nparams() work on fit() output from real models", {
set.seed(5001)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
result <- fit(model)(df)
p <- params(result)
expect_equal(length(p), 1)
expect_equal(p, coef(result))
expect_equal(nparams(result), 1L)
})
test_that("observed_fim() is positive definite on real fit output", {
set.seed(5002)
df <- data.frame(t = rexp(200, rate = 2))
model <- exponential_lifetime("t")
result <- fit(model)(df)
fim_mat <- observed_fim(result)
expect_true(!is.null(fim_mat))
expect_true(is.matrix(fim_mat))
evals <- eigen(fim_mat)$values
expect_true(all(evals > 0),
info = "Observed FIM should be positive definite at MLE")
})
test_that("rmap() fallthrough works on fisher_mle", {
skip_if_not_installed("algebraic.mle")
result <- fisher_mle(
par = c(shape = 2.0, scale = 1.5),
vcov = matrix(c(0.04, 0.01, 0.01, 0.02), 2, 2),
loglik_val = -50,
nobs = 100
)
# rmap transforms parameters via delta method
# Transform: mean lifetime = scale * gamma(1 + 1/shape)
mapped <- algebraic.mle::rmap(result, function(p) {
c(mean_life = p[2] * gamma(1 + 1 / p[1]))
})
expect_true(inherits(mapped, "mle_fit"))
expect_equal(length(params(mapped)), 1)
expect_true(params(mapped) > 0)
})
test_that("marginal() fallthrough works on fisher_mle", {
skip_if_not_installed("algebraic.mle")
result <- fisher_mle(
par = c(shape = 2.0, scale = 1.5),
vcov = matrix(c(0.04, 0.01, 0.01, 0.02), 2, 2),
loglik_val = -50,
nobs = 100
)
# Extract marginal for first parameter
m <- algebraic.mle::marginal(result, 1)
expect_true(inherits(m, "mle_fit"))
expect_equal(nparams(m), 1L)
expect_equal(unname(params(m)), 2.0)
})
test_that("expectation() fallthrough works on fisher_mle", {
skip_if_not_installed("algebraic.mle")
result <- fisher_mle(
par = c(shape = 2.0, scale = 1.5),
vcov = matrix(c(0.04, 0.01, 0.01, 0.02), 2, 2),
loglik_val = -50,
nobs = 100
)
set.seed(5003)
# Monte Carlo expectation of shape parameter
# Use qualified name to avoid conflict with testthat::expectation
# n goes in control list per algebraic.mle API
e <- algebraic.mle::expectation(result, function(p) p[1],
control = list(n = 5000L))
expect_equal(e, 2.0, tolerance = 0.1)
})
# =============================================================================
# FISHER_MLE PRINT AND SUMMARY COVERAGE TESTS
# =============================================================================
test_that("print.fisher_mle outputs expected text", {
result <- fisher_mle(
par = c(shape = 2.0, scale = 1.5),
vcov = matrix(c(0.04, 0.01, 0.01, 0.02), 2, 2),
loglik_val = -50,
nobs = 100
)
out <- capture.output(print(result))
expect_true(any(grepl("Maximum Likelihood Estimate", out)))
expect_true(any(grepl("Log-likelihood", out)))
expect_true(any(grepl("Observations:", out)))
})
test_that("print.fisher_mle shows warning when not converged", {
result <- fisher_mle(
par = c(a = 1),
vcov = matrix(0.1, 1, 1),
loglik_val = -50,
nobs = 10,
converged = FALSE
)
out <- capture.output(print(result))
expect_true(any(grepl("WARNING.*converge", out)))
})
test_that("print.fisher_mle works without nobs", {
result <- fisher_mle(
par = c(a = 1),
vcov = matrix(0.1, 1, 1),
loglik_val = -50
)
out <- capture.output(print(result))
expect_false(any(grepl("Observations:", out)))
})
test_that("print.summary_fisher_mle outputs expected text", {
result <- fisher_mle(
par = c(shape = 2.0, scale = 1.5),
vcov = matrix(c(0.04, 0.01, 0.01, 0.02), 2, 2),
loglik_val = -50,
nobs = 100
)
out <- capture.output(print(summary(result)))
expect_true(any(grepl("Maximum Likelihood Estimate", out)))
expect_true(any(grepl("AIC", out)))
expect_true(any(grepl("Log-likelihood", out)))
expect_true(any(grepl("Number of observations", out)))
})
test_that("print.summary_fisher_mle shows warning when not converged", {
result <- fisher_mle(
par = c(a = 1),
vcov = matrix(0.1, 1, 1),
loglik_val = -50,
nobs = 10,
converged = FALSE
)
out <- capture.output(print(summary(result)))
expect_true(any(grepl("WARNING.*converge", out)))
})
test_that("print.summary_fisher_mle works without nobs", {
result <- fisher_mle(
par = c(a = 1),
vcov = matrix(0.1, 1, 1),
loglik_val = -50
)
out <- capture.output(print(summary(result)))
expect_false(any(grepl("Number of observations", out)))
})
# =============================================================================
# FISHER_MLE ACCESSOR COVERAGE TESTS
# =============================================================================
test_that("logLik.fisher_mle returns the log-likelihood value", {
result <- fisher_mle(
par = c(a = 1, b = 2),
vcov = matrix(c(0.04, 0, 0, 0.02), 2, 2),
loglik_val = -123.45,
nobs = 50
)
expect_equal(as.numeric(logLik(result)), -123.45)
})
test_that("confint.fisher_mle works with character parm", {
result <- fisher_mle(
par = c(shape = 2.0, scale = 1.5),
vcov = matrix(c(0.04, 0.01, 0.01, 0.02), 2, 2),
loglik_val = -50,
nobs = 100
)
ci_all <- confint(result)
ci_shape <- confint(result, parm = "shape")
expect_equal(nrow(ci_shape), 1)
expect_equal(ci_shape[1, ], ci_all["shape", ])
})
test_that("fisher_mle warns when hessian is singular", {
# Singular hessian: second row is a multiple of first
H <- matrix(c(-1, -2, -2, -4), 2, 2)
expect_warning(
result <- fisher_mle(
par = c(a = 1, b = 2),
loglik_val = -50,
hessian = H,
nobs = 100
),
"not invertible"
)
expect_null(result$vcov)
})
test_that("BIC works via stats::BIC when nobs is available", {
result <- fisher_mle(
par = c(a = 1),
vcov = matrix(0.1, 1, 1),
loglik_val = -50,
nobs = 100
)
# BIC = -2*logL + k*log(n) = 100 + 1*log(100)
expect_equal(stats::BIC(result), 100 + log(100))
})
# =============================================================================
# FISHER_BOOT TESTS
# =============================================================================
test_that("confint.fisher_boot computes percentile CI", {
skip_if_not_installed("boot")
set.seed(6001)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
model_sampler <- sampler(model, df = df, par = c(lambda = 2))
boot_obj <- model_sampler(n = 200)
ci <- confint(boot_obj, level = 0.95, type = "perc")
expect_equal(nrow(ci), 1)
expect_equal(ncol(ci), 2)
expect_true(ci[1, 1] < ci[1, 2]) # lower < upper
})
test_that("confint.fisher_boot computes basic CI", {
skip_if_not_installed("boot")
set.seed(6002)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
model_sampler <- sampler(model, df = df, par = c(lambda = 2))
boot_obj <- model_sampler(n = 200)
ci <- confint(boot_obj, level = 0.95, type = "basic")
expect_equal(nrow(ci), 1)
expect_equal(ncol(ci), 2)
})
test_that("confint.fisher_boot computes norm CI", {
skip_if_not_installed("boot")
set.seed(6003)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
model_sampler <- sampler(model, df = df, par = c(lambda = 2))
boot_obj <- model_sampler(n = 200)
ci <- confint(boot_obj, level = 0.95, type = "norm")
expect_equal(nrow(ci), 1)
expect_equal(ncol(ci), 2)
})
test_that("confint.fisher_boot with integer parm", {
skip_if_not_installed("boot")
set.seed(6004)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
model_sampler <- sampler(model, df = df, par = c(lambda = 2))
boot_obj <- model_sampler(n = 200)
ci <- confint(boot_obj, parm = 1, level = 0.95, type = "perc")
expect_equal(nrow(ci), 1)
})
test_that("print.fisher_boot outputs expected text", {
skip_if_not_installed("boot")
set.seed(6005)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
model_sampler <- sampler(model, df = df, par = c(lambda = 2))
boot_obj <- model_sampler(n = 50)
out <- capture.output(print(boot_obj))
expect_true(any(grepl("Bootstrap MLE", out)))
expect_true(any(grepl("Bootstrap replicates:", out)))
expect_true(any(grepl("Bootstrap SE:", out)))
})
test_that("sampler.fisher_boot resamples from bootstrap distribution", {
skip_if_not_installed("boot")
set.seed(6006)
df <- data.frame(t = rexp(100, rate = 2))
model <- exponential_lifetime("t")
model_sampler <- sampler(model, df = df, par = c(lambda = 2))
boot_obj <- model_sampler(n = 200)
samp_fn <- sampler(boot_obj)
samples <- samp_fn(100)
expect_equal(nrow(samples), 100)
expect_equal(ncol(samples), 1)
expect_true(all(is.finite(samples)))
})
# =============================================================================
# SAMPLER.FISHER_MLE UNIVARIATE COVERAGE
# =============================================================================
test_that("sampler.fisher_mle works for univariate case", {
result <- fisher_mle(
par = c(rate = 2.0),
vcov = matrix(0.04, 1, 1),
loglik_val = -30,
nobs = 50
)
samp_fn <- sampler(result)
set.seed(6007)
samples <- samp_fn(500)
expect_equal(ncol(samples), 1)
expect_equal(nrow(samples), 500)
expect_equal(mean(samples), 2.0, tolerance = 0.1)
})
test_that("sampler.fisher_mle errors when vcov is NULL", {
result <- fisher_mle(
par = c(rate = 2.0),
loglik_val = -30,
nobs = 50
)
expect_error(sampler(result), "vcov is NULL")
})
# =============================================================================
# FISHERIAN INFERENCE COVERAGE TESTS
# =============================================================================
test_that("likelihood_interval works for single parameter model", {
set.seed(6009)
df <- data.frame(x = rexp(100, rate = 2))
model <- exponential_lifetime("x")
result <- fit(model)(df, par = c(rate = 1))
li <- likelihood_interval(result, data = df, model = model, k = 8)
expect_true(!is.null(li))
})
test_that("print.likelihood_interval outputs expected text", {
set.seed(6010)
df <- data.frame(x = rexp(100, rate = 2))
model <- exponential_lifetime("x")
result <- fit(model)(df, par = c(rate = 1))
li <- likelihood_interval(result, data = df, model = model, k = 8)
out <- capture.output(print(li))
expect_true(any(grepl("Likelihood Interval", out)))
})
test_that("profile_loglik errors for > 2 parameters", {
result <- fisher_mle(
par = c(a = 1, b = 2, c = 3),
vcov = diag(3) * 0.01,
loglik_val = -50,
nobs = 100
)
# Use a mock model for this error test
mock_model <- structure(list(), class = c("mock_3param", "likelihood_model"))
loglik.mock_3param <<- function(model, ...) function(df, par, ...) 0
df <- data.frame(x = 1:10)
expect_error(
profile_loglik(result, data = df, model = mock_model, param = 1:3),
"1 or 2 parameters"
)
rm(loglik.mock_3param, envir = .GlobalEnv)
})
test_that("print.profile_loglik outputs expected text", {
set.seed(6013)
df <- data.frame(t = rexp(50, rate = 2))
model <- exponential_lifetime("t")
result <- fit(model)(df)
pl <- profile_loglik(result, data = df, model = model,
param = 1, n_grid = 10)
out <- capture.output(print(pl))
expect_true(any(grepl("Profile Log-Likelihood", out)))
expect_true(any(grepl("Grid points:", out)))
})
test_that("deviance.fisher_mle errors on non-fisher_mle null model", {
result <- fisher_mle(
par = c(a = 1),
vcov = matrix(0.1, 1, 1),
loglik_val = -50,
nobs = 100
)
expect_error(deviance(result, null_model = list(loglik = -60)),
"fisher_mle")
})
# =============================================================================
# FIM AND MOCK MODEL COVERAGE TESTS
# =============================================================================
# Helper: register mock exponential loglik/rdata methods in the global env.
# Returns a model object that falls through to fim.likelihood_model (unlike
# exponential_lifetime, which has its own fim method).
make_mock_exp <- function(class_name) {
assign(paste0("loglik.", class_name), function(model, ...) {
function(df, par, ...) {
lambda <- par[1]
if (lambda <= 0) return(-.Machine$double.xmax / 2)
nrow(df) * log(lambda) - lambda * sum(df$x)
}
}, envir = .GlobalEnv)
assign(paste0("rdata.", class_name), function(model, ...) {
function(theta, n, ...) {
data.frame(x = rexp(n, rate = theta[1]))
}
}, envir = .GlobalEnv)
structure(list(), class = c(class_name, "likelihood_model"))
}
cleanup_mock_exp <- function(class_name) {
rm(list = paste0(c("loglik.", "rdata."), class_name), envir = .GlobalEnv)
}
test_that("fim.likelihood_model computes FIM via Monte Carlo (neg Hessian)", {
mock_model <- make_mock_exp("mock_exp")
set.seed(6014)
fim_fn <- fim(mock_model)
# True FIM for exp(rate=2) with n=50 is n/rate^2 = 50/4 = 12.5
fim_mat <- fim_fn(theta = c(2), n_obs = 50, n_samples = 500)
expect_true(is.matrix(fim_mat))
expect_equal(nrow(fim_mat), 1)
expect_equal(ncol(fim_mat), 1)
expect_equal(fim_mat[1, 1], 12.5, tolerance = 2)
cleanup_mock_exp("mock_exp")
})
test_that("fim.likelihood_model scales linearly with n_obs", {
mock_model <- make_mock_exp("mock_exp2")
set.seed(7001)
fim_fn <- fim(mock_model)
fim_10 <- fim_fn(theta = c(2), n_obs = 10, n_samples = 2000)
fim_50 <- fim_fn(theta = c(2), n_obs = 50, n_samples = 2000)
# FIM should scale as n_obs / lambda^2, so ratio ~ 50/10 = 5
expect_equal(fim_50[1, 1] / fim_10[1, 1], 5, tolerance = 0.5)
cleanup_mock_exp("mock_exp2")
})
test_that("fim.likelihood_model adds parameter names from theta", {
mock_model <- make_mock_exp("mock_exp3")
set.seed(7002)
fim_fn <- fim(mock_model)
fim_mat <- fim_fn(theta = c(rate = 2), n_obs = 10, n_samples = 100)
expect_equal(rownames(fim_mat), "rate")
expect_equal(colnames(fim_mat), "rate")
cleanup_mock_exp("mock_exp3")
})
# =============================================================================
# BIAS.FISHER_MLE TESTS
# =============================================================================
test_that("bias.fisher_mle returns zeros without model", {
result <- fisher_mle(
par = c(a = 1, b = 2),
vcov = diag(2) * 0.01,
loglik_val = -50,
nobs = 100
)
b <- bias(result)
expect_equal(b, c(0, 0))
# With theta but no model, still zeros
b2 <- bias(result, theta = c(1, 2))
expect_equal(b2, c(0, 0))
})
test_that("bias.fisher_mle estimates bias via MC when model provided", {
model <- exponential_lifetime("t")
set.seed(7020)
df <- data.frame(t = rexp(200, rate = 2))
result <- fit(model)(df)
# MC bias for exponential MLE should be near zero for large n
b <- bias(result, theta = c(lambda = 2), model = model, n_sim = 200)
expect_equal(length(b), 1)
# Bias should be small relative to the parameter value
expect_true(abs(b[1]) < 0.5)
})
# =============================================================================
# FIM ... FORWARDING + NaN GUARD TESTS
# =============================================================================
test_that("fim.likelihood_model forwards ... to hess_fn", {
mock_model <- structure(list(), class = c("mock_dots_fim", "likelihood_model"))
loglik.mock_dots_fim <<- function(model, ...) {
function(df, par, multiplier = 1, ...) {
multiplier * (nrow(df) * log(par[1]) - par[1] * sum(df$x))
}
}
rdata.mock_dots_fim <<- function(model, ...) {
function(theta, n, ...) {
data.frame(x = rexp(n, rate = theta[1]))
}
}
set.seed(8002)
fim_fn <- fim(mock_model)
# With multiplier=1 (default), FIM for exp(rate=2), n=10 is 10/4 = 2.5
fim_default <- fim_fn(theta = c(2), n_obs = 10, n_samples = 500)
# With multiplier=2, FIM should be ~2x
fim_doubled <- fim_fn(theta = c(2), n_obs = 10, n_samples = 500, multiplier = 2)
expect_equal(fim_doubled[1, 1] / fim_default[1, 1], 2, tolerance = 0.5)
rm(loglik.mock_dots_fim, rdata.mock_dots_fim, envir = .GlobalEnv)
})
test_that("fim.likelihood_model handles NaN from hess_loglik gracefully", {
mock_model <- structure(list(), class = c("mock_nan_fim", "likelihood_model"))
call_count <- 0L
hess_loglik.mock_nan_fim <<- function(model, ...) {
function(df, par, ...) {
# Return NaN matrix every 3rd call
call_count <<- call_count + 1L
if (call_count %% 3 == 0) return(matrix(NaN, 1, 1))
matrix(-nrow(df) / par[1]^2, 1, 1)
}
}
rdata.mock_nan_fim <<- function(model, ...) {
function(theta, n, ...) {
data.frame(x = rexp(n, rate = theta[1]))
}
}
set.seed(8003)
fim_fn <- fim(mock_model)
# Should still produce a valid FIM despite some NaN samples
expect_warning(
fim_mat <- fim_fn(theta = c(2), n_obs = 10, n_samples = 100),
"samples failed"
)
expect_true(is.matrix(fim_mat))
expect_true(all(is.finite(fim_mat)))
rm(hess_loglik.mock_nan_fim, rdata.mock_nan_fim, envir = .GlobalEnv)
call_count <- NULL
})
# =============================================================================
# MSE TESTS
# =============================================================================
test_that("mse.fisher_mle returns Vcov when bias is zero (asymptotic)", {
model <- exponential_lifetime("t")
set.seed(8005)
df <- data.frame(t = rexp(100, rate = 2))
result <- fit(model)(df)
# Without MC, bias = 0, so MSE = Vcov
mse_val <- mse(result, theta = c(lambda = 2))
expect_equal(mse_val, vcov(result))
})
test_that("mse.fisher_mle forwards model and n_sim to bias for MC-based MSE", {
model <- exponential_lifetime("t")
set.seed(8005)
df <- data.frame(t = rexp(100, rate = 2))
result <- fit(model)(df)
# With model, MSE accounts for finite-sample bias
mse_mc <- mse(result, theta = c(lambda = 2), model = model, n_sim = 100)
expect_true(is.finite(mse_mc))
})
# =============================================================================
# SAMPLER ... SCOPING TEST
# =============================================================================
test_that("sampler.likelihood_model bootstrap works correctly", {
model <- exponential_lifetime("t")
set.seed(8006)
df <- data.frame(t = rexp(50, rate = 2))
boot_sampler <- sampler(model, df, par = c(lambda = 2))
boot_result <- boot_sampler(50)
expect_s3_class(boot_result, "fisher_boot")
expect_equal(ncol(boot_result$replicates), 1)
expect_equal(nrow(boot_result$replicates), 50)
})
# =============================================================================
# DEFAULT METHOD ERROR MESSAGES
# =============================================================================
test_that("loglik.likelihood_model gives clear error for unimplemented class", {
fake_model <- structure(list(), class = c("fake_model", "likelihood_model"))
expect_error(loglik(fake_model), "must implement loglik")
})
test_that("rdata.likelihood_model gives clear error", {
fake_model <- structure(list(), class = c("fake_model2", "likelihood_model"))
loglik.fake_model2 <<- function(model, ...) function(df, par, ...) 0
expect_error(rdata(fake_model), "does not implement rdata")
rm(loglik.fake_model2, envir = .GlobalEnv)
})
# =============================================================================
# LRT RESULT CLASS + PRINT
# =============================================================================
test_that("lrt returns lrt_result class with print method", {
set.seed(8007)
df <- data.frame(t = rexp(50, rate = 2))
model <- exponential_lifetime("t")
result <- lrt(model, model, df,
null_par = c(2), alt_par = c(2), dof = 0)
expect_s3_class(result, "lrt_result")
expect_true("stat" %in% names(result))
expect_true("p.value" %in% names(result))
out <- capture.output(print(result))
expect_true(any(grepl("Likelihood Ratio Test", out)))
})
# =============================================================================
# EVIDENCE IS A GENERIC
# =============================================================================
test_that("evidence dispatches via S3", {
set.seed(8008)
df <- data.frame(t = rexp(50, rate = 2))
model <- exponential_lifetime("t")
e <- evidence(model, data = df, theta1 = c(lambda = 2), theta2 = c(lambda = 1))
expect_true(is.numeric(e))
expect_equal(length(e), 1)
# Verify it matches manual computation
ll <- loglik(model)
expected <- ll(df, c(lambda = 2)) - ll(df, c(lambda = 1))
expect_equal(e, expected)
})
# =============================================================================
# COVERAGE RECOVERY TESTS
# =============================================================================
# These tests target multivariate code paths that require a 2+ parameter model.
#
# Mock normal model: loglik.mock_norm, score.mock_norm, hess_loglik.mock_norm
# are registered in the global environment and cleaned up after each test
# block that needs them.
# Helper: set up and tear down mock_norm model
make_mock_norm <- function() {
model <- structure(list(ob_col = "x"), class = c("mock_norm", "likelihood_model"))
loglik.mock_norm <<- function(model, ...) {
function(df, par, ...) {
mu <- par[1]; sigma <- par[2]
if (sigma <= 0) return(-.Machine$double.xmax / 2)
sum(dnorm(df[[model$ob_col]], mu, sigma, log = TRUE))
}
}
model
}
cleanup_mock_norm <- function() {
to_rm <- intersect(c("loglik.mock_norm"), ls(envir = .GlobalEnv))
if (length(to_rm)) rm(list = to_rm, envir = .GlobalEnv)
}
# ---------------------------------------------------------------------------
# sampler.fisher_mle: multivariate branch (mvtnorm::rmvnorm) — line 543
# ---------------------------------------------------------------------------
test_that("sampler.fisher_mle uses mvtnorm for 2-param model", {
skip_if_not_installed("mvtnorm")
result <- fisher_mle(
par = c(mu = 5.0, sigma = 2.0),
vcov = matrix(c(0.04, 0.0, 0.0, 0.02), 2, 2),
loglik_val = -150,
nobs = 100
)
samp_fn <- sampler(result)
set.seed(9001)
samples <- samp_fn(500)
expect_equal(dim(samples), c(500, 2))
expect_equal(mean(samples[, 1]), 5.0, tolerance = 0.1)
expect_equal(mean(samples[, 2]), 2.0, tolerance = 0.1)
})
# ---------------------------------------------------------------------------
# likelihood_interval: multivariate (profile) branch — lines 149-213
# ---------------------------------------------------------------------------
test_that("likelihood_interval computes profile interval for 2-param model", {
skip_if_not_installed("mvtnorm")
model <- make_mock_norm()
on.exit(cleanup_mock_norm())
set.seed(9002)
df <- data.frame(x = rnorm(100, mean = 3, sd = 1.5))
# Fit via fit.likelihood_model (optim-based)
solver <- fit(model)
result <- solver(df, par = c(mu = 0, sigma = 1))
# Compute profile likelihood interval for mu (param = 1)
li <- likelihood_interval(result, data = df, model = model, k = 8, param = 1)
expect_true(inherits(li, "likelihood_interval"))
expect_equal(nrow(li), 1)
expect_equal(ncol(li), 2)
# Interval should contain the MLE for mu
mu_hat <- coef(result)[1]
expect_true(li[1, 1] < mu_hat && mu_hat < li[1, 2])
})
test_that("likelihood_interval computes profile intervals for both params", {
skip_if_not_installed("mvtnorm")
model <- make_mock_norm()
on.exit(cleanup_mock_norm())
set.seed(9003)
df <- data.frame(x = rnorm(80, mean = 2, sd = 1))
solver <- fit(model)
result <- solver(df, par = c(mu = 0, sigma = 1))
# Profile interval for all params (NULL => both)
li <- likelihood_interval(result, data = df, model = model, k = 8)
expect_equal(nrow(li), 2)
mu_hat <- coef(result)[1]
sigma_hat <- coef(result)[2]
expect_true(li[1, 1] < mu_hat && mu_hat < li[1, 2])
expect_true(li[2, 1] < sigma_hat && sigma_hat < li[2, 2])
})
test_that("likelihood_interval handles character param name for 2-param model", {
skip_if_not_installed("mvtnorm")
model <- make_mock_norm()
on.exit(cleanup_mock_norm())
set.seed(9004)
df <- data.frame(x = rnorm(60, mean = 1, sd = 2))
solver <- fit(model)
result <- solver(df, par = c(mu = 0, sigma = 1))
# Use character param name
li <- likelihood_interval(result, data = df, model = model, k = 8,
param = "mu")
expect_equal(nrow(li), 1)
mu_hat <- coef(result)["mu"]
expect_true(li[1, 1] < mu_hat && mu_hat < li[1, 2])
})
# ---------------------------------------------------------------------------
# profile_loglik: 1D profile over a 2-param model — lines 310-329
# ---------------------------------------------------------------------------
test_that("profile_loglik 1D profile over nuisance parameter works", {
skip_if_not_installed("mvtnorm")
model <- make_mock_norm()
on.exit(cleanup_mock_norm())
set.seed(9005)
df <- data.frame(x = rnorm(80, mean = 3, sd = 1.5))
solver <- fit(model)
result <- solver(df, par = c(mu = 0, sigma = 1))
# Profile over mu (param=1), optimising out sigma
prof <- profile_loglik(result, data = df, model = model, param = 1, n_grid = 15)
expect_true(inherits(prof, "profile_loglik"))
expect_equal(nrow(prof), 15)
expect_true("loglik" %in% names(prof))
expect_true("support" %in% names(prof))
expect_true("relative_likelihood" %in% names(prof))
# Profile should peak near MLE
mu_hat <- unname(coef(result)[1])
max_idx <- which.max(prof$loglik)
expect_equal(unname(prof[[1]][max_idx]), mu_hat, tolerance = 0.5)
})
# ---------------------------------------------------------------------------
# profile_loglik: 2D profile — lines 332-358
# ---------------------------------------------------------------------------
test_that("profile_loglik 2D profile produces correct shape data frame", {
skip_if_not_installed("mvtnorm")
model <- make_mock_norm()
on.exit(cleanup_mock_norm())
set.seed(9006)
df <- data.frame(x = rnorm(60, mean = 2, sd = 1))
solver <- fit(model)
result <- solver(df, par = c(mu = 0, sigma = 1))
prof <- profile_loglik(result, data = df, model = model,
param = 1:2, n_grid = 8)
expect_true(inherits(prof, "profile_loglik"))
expect_equal(nrow(prof), 8 * 8) # expand.grid of 8x8 grid
expect_true("loglik" %in% names(prof))
expect_true("support" %in% names(prof))
expect_true("relative_likelihood" %in% names(prof))
})
# ---------------------------------------------------------------------------
# print.profile_loglik: 2D case — covered when param has length 2
# ---------------------------------------------------------------------------
test_that("print.profile_loglik prints correctly for 2D profile", {
skip_if_not_installed("mvtnorm")
model <- make_mock_norm()
on.exit(cleanup_mock_norm())
set.seed(9007)
df <- data.frame(x = rnorm(40, mean = 1, sd = 1))
solver <- fit(model)
result <- solver(df, par = c(mu = 0, sigma = 1))
prof <- profile_loglik(result, data = df, model = model,
param = 1:2, n_grid = 5)
out <- capture.output(print(prof))
expect_true(any(grepl("Profile Log-Likelihood", out)))
expect_true(any(grepl("Grid points:", out)))
# Should mention both parameter names
expect_true(any(grepl("mu", out)))
})
# ---------------------------------------------------------------------------
# bias.fisher_mle: nobs NULL error — line 294
# ---------------------------------------------------------------------------
test_that("bias.fisher_mle errors when nobs is NULL and model is provided", {
model <- exponential_lifetime("t")
result <- fisher_mle(
par = c(lambda = 2.0),
vcov = matrix(0.04, 1, 1),
loglik_val = -50
# nobs intentionally omitted (NULL)
)
expect_error(
bias(result, theta = c(lambda = 2), model = model),
"nobs not available"
)
})
# ---------------------------------------------------------------------------
# bias.fisher_mle: warning when most MC replicates fail — lines 314-317
# ---------------------------------------------------------------------------
test_that("bias.fisher_mle warns when most MC replicates fail", {
# Create a model whose fit() always errors
bad_model <- structure(
list(ob_col = "t"),
class = c("mock_bad_fit", "likelihood_model")
)
loglik.mock_bad_fit <<- function(model, ...) {
function(df, par, ...) {
lambda <- par[1]
if (lambda <= 0) return(-.Machine$double.xmax / 2)
nrow(df) * log(lambda) - lambda * sum(df[[model$ob_col]])
}
}
rdata.mock_bad_fit <<- function(model, ...) {
function(theta, n, ...) {
data.frame(t = rexp(n, rate = theta[1]))
}
}
# Override fit to always error
fit.mock_bad_fit <<- function(object, ...) {
function(df, par = NULL, ...) {
stop("Intentional fit failure for testing")
}
}
on.exit({
rm(list = intersect(
c("loglik.mock_bad_fit", "rdata.mock_bad_fit", "fit.mock_bad_fit"),
ls(envir = .GlobalEnv)
), envir = .GlobalEnv)
})
set.seed(9008)
result <- fisher_mle(
par = c(lambda = 2.0),
vcov = matrix(0.04, 1, 1),
loglik_val = -50,
nobs = 50
)
# All replicates will fail, so we get a warning + NA return
expect_warning(
b <- bias(result, theta = c(lambda = 2), model = bad_model, n_sim = 20),
"MC replicates succeeded"
)
expect_true(all(is.na(b)))
})
# ---------------------------------------------------------------------------
# print.fisher_boot: bias line is covered when boot object has 2+ params
# ---------------------------------------------------------------------------
test_that("print.fisher_boot covers format(bias(x)) for multivariate bootstrap", {
skip_if_not_installed("boot")
# Build a 2-param mock model that fit.likelihood_model can use
model <- make_mock_norm()
on.exit(cleanup_mock_norm())
set.seed(9009)
df <- data.frame(x = rnorm(50, mean = 2, sd = 1))
boot_sampler <- sampler(model, df, par = c(mu = 0, sigma = 1))
boot_obj <- boot_sampler(30)
out <- capture.output(print(boot_obj))
expect_true(any(grepl("Bootstrap MLE", out)))
expect_true(any(grepl("Bootstrap bias:", out)))
# Should show 2 bias values
expect_equal(length(bias(boot_obj)), 2)
})
# ---------------------------------------------------------------------------
# core-generics.R line 247: "All Monte Carlo samples failed" error in fim
# ---------------------------------------------------------------------------
test_that("fim.likelihood_model errors when all MC samples fail", {
always_fail_model <- structure(
list(),
class = c("mock_always_fail_fim", "likelihood_model")
)
loglik.mock_always_fail_fim <<- function(model, ...) {
function(df, par, ...) 0
}
hess_loglik.mock_always_fail_fim <<- function(model, ...) {
function(df, par, ...) stop("Always fails")
}
rdata.mock_always_fail_fim <<- function(model, ...) {
function(theta, n, ...) data.frame(x = 1)
}
on.exit({
rm(list = intersect(
c("loglik.mock_always_fail_fim",
"hess_loglik.mock_always_fail_fim",
"rdata.mock_always_fail_fim"),
ls(envir = .GlobalEnv)
), envir = .GlobalEnv)
})
fim_fn <- fim(always_fail_model)
expect_error(
fim_fn(theta = c(1), n_obs = 10, n_samples = 5),
"All Monte Carlo samples failed"
)
})
# ---------------------------------------------------------------------------
# exponential_lifetime loglik: negative lambda guard — line 76
# ---------------------------------------------------------------------------
test_that("loglik.exponential_lifetime returns sentinel for non-positive lambda", {
model <- exponential_lifetime("t")
ll_fn <- loglik(model)
df <- data.frame(t = c(1, 2, 3))
# Negative lambda
val_neg <- ll_fn(df, c(-1))
expect_equal(val_neg, -.Machine$double.xmax / 2)
# Zero lambda
val_zero <- ll_fn(df, c(0))
expect_equal(val_zero, -.Machine$double.xmax / 2)
})
# ---------------------------------------------------------------------------
# fit.likelihood_model: auto-switch 1-param to BFGS — line 61
# (the SANN test uses a custom model; this test exercises the 1-param
# Nelder-Mead -> BFGS switch via fit.likelihood_model directly)
# ---------------------------------------------------------------------------
test_that("fit.likelihood_model auto-switches 1-param Nelder-Mead to BFGS", {
# Use make_mock_norm but override to 1-param normal (fixed sigma)
mock_1p <- structure(list(), class = c("mock_1param_bfgs", "likelihood_model"))
loglik.mock_1param_bfgs <<- function(model, ...) {
function(df, par, ...) {
mu <- par[1]
sum(dnorm(df$x, mu, sd = 1, log = TRUE))
}
}
on.exit(rm(list = intersect("loglik.mock_1param_bfgs", ls(.GlobalEnv)),
envir = .GlobalEnv))
set.seed(9010)
df <- data.frame(x = rnorm(50, mean = 3, sd = 1))
# Deliberately pass method = "Nelder-Mead" with 1 param; should switch to BFGS
solver <- fit(mock_1p)
result <- solver(df, par = c(mu = 0), method = "Nelder-Mead")
expect_true(inherits(result, "fisher_mle"))
expect_equal(unname(coef(result)[1]), 3, tolerance = 0.3)
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.