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) })