--- title: "Exact and log-scale tail probabilities for Roy's largest root with rootWishartHD" author: "Stepan Grinek" output: rmarkdown::html_vignette: toc: true toc_depth: 2 vignette: > %\VignetteIndexEntry{Exact and log-scale tail probabilities for Roy's largest root with rootWishartHD} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4.6, fig.align = "center" ) has_mc <- requireNamespace("rWishart", quietly = TRUE) && requireNamespace("corpcor", quietly = TRUE) ``` ## Overview `rootWishartHD` computes distribution functions and **log-scale tail probabilities** for Roy's largest root in single- and double-Wishart (Jacobi ensemble) settings. It is derived from the [`rootWishart`](https://github.com/turgeonmaxime/rootWishart) and extends it with: * numerically robust **log-CDF** and **log-survival** evaluation that does not saturate to 0 or 1 in extreme tails; * **tail-aware arbitrary precision**, so probabilities remain accurate far into the upper tail; * scaling that pushes reliable evaluation from `p ~ 30-40` (the practical limit of the original package) up to `p ~ 500`. The methods follow Chiani (2014, 2016): the CDF of the largest root is a Pfaffian of a skew-symmetric matrix built from incomplete-beta entries. ```{r load} library(rootWishartHD) ``` The double-Wishart distribution is parametrized by `(s, m, n)` (Chiani's notation). For a MANOVA-type problem with dimension `p`, hypothesis degrees of freedom `q_df` and error degrees of freedom related to `m_df`, the mapping used throughout this vignette is: ```{r dsb} dsb_params <- function(p, m_df, q_df) { s <- q_df mC <- 0.5 * (abs(m_df - s) - 1) df2 <- p - m_df + q_df nC <- 0.5 * (abs(df2 - s) - 1) list(s = s, m = mC, n = nC) } ``` ## Basic usage: the CDF `doubleWishart()` returns the CDF `F(theta)` of the largest root statistic `theta = lambda / (1 + lambda)` on `(0, 1)`. `type = "double"` uses fast double precision; `type = "arbitrary"` uses the arbitrary-precision backend. ```{r cdf-basic} par0 <- dsb_params(p = 20, m_df = 14, q_df = 10) # s = 10 theta <- c(0.3, 0.5, 0.7, 0.9) doubleWishart(theta, s = par0$s, m = par0$m, n = par0$n, type = "double", verbose = FALSE) ``` ## Multiprecision backend The portable default build uses Boost's header-only `cpp_dec_float` backend from `BH` (`DW_USE_MPFR=0`). This is the CRAN-safe setting and does not require system MPFR/GMP libraries. To force the arbitrary-precision path at runtime, use ```{r multiprecision-option, eval=FALSE} options(rootWishartHD.force_multiprecision = TRUE) ``` Local source builds can opt in to MPFR/GMP with ```sh DW_USE_MPFR=1 R CMD INSTALL rootWishartHD_0.95.1.tar.gz ``` Runtime adaptive precision (`adaptive = TRUE`) requires an MPFR/GMP build. With the default `DW_USE_MPFR=0` build, adaptive requests are downgraded to fixed `cpp_dec_float` precision with a warning; increase fixed precision at install time, for example with `DW_MP_DIGITS=300 R CMD INSTALL .`. Check the compiled backend with: ```{r mpfr-enabled} rootWishartHD_mpfr_enabled() ``` If the deprecated `force_mpfr` interface is used but the package was built with `DW_USE_MPFR=0`, `rootWishartHD` warns once and falls back to Boost `cpp_dec_float`. ## Log scale to avoid tail saturation In moderate to high dimensions the CDF is extremely close to 0 or 1 over most of its support, so a plain double-precision CDF *saturates*: `1 - F` underflows to exactly 0 and the upper tail is lost. The log-survival function keeps the tail resolvable. ```{r logsf} # Upper-tail log-survival log(1 - F). 'lower' gives log F. logSF <- doubleWishart_log( c(0.85, 0.92, 0.97), s = par0$s, m = par0$m, n = par0$n, type = "arbitrary", tail = "upper", verbose = FALSE) data.frame(theta = c(0.85, 0.92, 0.97), logSF = logSF, SF = exp(logSF)) ``` The plain CDF would report `1` (survival `0`) for these points, whereas the log-scale survival values remain finite and accurate. ## Regulating precision The exact path exposes several precision knobs. The most important: | argument | meaning | |---|---| | `type` | `"double"` (fast) or `"arbitrary"` (multiprecision) | | `adaptive` | grow precision at runtime until the result converges | | `start_digits10` | starting decimal precision for the adaptive search | | `max_digits10` | cap on decimal precision | | `tol` | convergence tolerance in log-space | | `pf_method` | Pfaffian backend: `"gauss"`, `"lu"`, `"svd"`, `"schur"`, `"auto"` | | `scale_iter` | symmetric equilibration iterations before the Pfaffian | A practical **two-stage** strategy is to evaluate every point at moderate precision first, and only re-evaluate the points that underflow (return `-Inf`) at a much higher precision: ```{r two-stage} logsf_two_stage <- function(theta, s, m, n, stage1_max = 600L, stage2_max = 20000L) { v <- doubleWishart_log( theta, s = s, m = m, n = n, type = "arbitrary", tail = "upper", adaptive = TRUE, start_digits10 = 200L, max_digits10 = stage1_max, tol = 1e-8, pf_method = "gauss", verbose = FALSE) unresolved <- !is.finite(v) if (any(unresolved)) { v[unresolved] <- doubleWishart_log( theta[unresolved], s = s, m = m, n = n, type = "arbitrary", tail = "upper", adaptive = TRUE, start_digits10 = 200L, max_digits10 = stage2_max, tol = 1e-12, pf_method = "gauss", verbose = FALSE) } v } logsf_two_stage(c(0.9, 0.97), s = par0$s, m = par0$m, n = par0$n) ``` ## Validation: empirical vs analytic CDF We can check the analytic CDF against a Monte Carlo reference. The helper below draws the largest-root statistic by simulation (requires the `rWishart` and `corpcor` packages). ```{r mc-helper, eval = has_mc} one_draw_theta <- function(dfA, dfB, S) { A <- rWishart::rSingularWishart(1L, dfA, S)[, , 1] B <- rWishart::rSingularWishart(1L, dfB, S)[, , 1] eigA <- eigen(A, symmetric = TRUE) rankVr <- min(dfA, nrow(A)) V <- eigA$vectors[, 1:rankVr, drop = FALSE] vals <- pmax(eigA$values[1:rankVr], 1e-12 * max(eigA$values[1:rankVr])) Xp <- sweep(V, 2, sqrt(1 / vals), `*`) C <- crossprod(Xp, B %*% Xp) W <- Xp %*% corpcor::fast.svd(C)$u lmax <- max(crossprod(W, B %*% W)) lmax / (1 + lmax) } ``` ```{r mc-figure, eval = has_mc, fig.cap = "Empirical (Monte Carlo) vs analytic CDF for a small double-Wishart case."} set.seed(1) p <- 20; m_df <- 14; q_df <- 10 par1 <- dsb_params(p, m_df, q_df) # s = 10 S <- diag(1, p) theta_mc <- replicate(100, one_draw_theta(m_df, q_df, S)) theta_mc <- pmin(pmax(theta_mc, 1e-12), 1 - 1e-12) grid <- as.numeric(quantile(theta_mc, probs = seq(0.02, 0.98, length.out = 30))) F_ana <- doubleWishart(grid, s = par1$s, m = par1$m, n = par1$n, type = "double", verbose = FALSE) F_emp <- ecdf(theta_mc)(grid) plot(grid, F_emp, type = "s", col = "steelblue", lwd = 2, ylim = c(0, 1), xlab = expression(theta), ylab = expression(F(theta)), main = sprintf("s = %d: empirical vs analytic CDF", par1$s)) lines(grid, F_ana, col = "firebrick", lwd = 2, lty = 2) points(grid, F_ana, col = "firebrick", pch = 19, cex = 0.5) legend("bottomright", bty = "n", lwd = 2, lty = c(1, 2), col = c("steelblue", "firebrick"), legend = c("empirical (Monte Carlo)", "analytic (rootWishartHD)")) cat(sprintf("max |F_emp - F_ana| = %.4f\n", max(abs(F_emp - F_ana)))) ``` ```{r mc-skip, eval = !has_mc, echo = FALSE, results = "asis"} cat("> _The Monte Carlo validation figure is skipped because `rWishart` and/or", "`corpcor` are not installed._\n") ``` ## Performance across dimensions The table below summarises measured timings on a multi-core Linux machine (R 4.6, 16 PSOCK workers). `exact/pt` is the wall time for one adaptive upper-tail `logSF` evaluation; `MC sim` is the one-off cost of generating the Monte Carlo reference (cached on disk for reuse). The repository ships a benchmarking harness, `test_doubleWishartHD_sweep.R`, that reproduces these numbers and the figure above across a grid of settings. ```{r perf-table, echo = FALSE} perf <- data.frame( setting = c("p40", "p100", "p150", "p300", "p500"), p = c(40, 100, 150, 300, 500), s = c(20, 50, 35, 150, 498), `MC sim` = c("4.1 s", "not run", "2.5 min", "2.5 min", "3.1 min"), `exact/pt`= c("2.5 s", "5.8 s", "4.1 s", "22 s", "3.7 min"), check.names = FALSE, stringsAsFactors = FALSE ) knitr::kable(perf, caption = "Representative timings for one exact upper-tail logSF evaluation.") ``` A key internal optimisation makes the matrix construction `O(s)` incomplete-beta evaluations instead of `O(s^2)` (the `a_{ij}` entries depend only on `i + j`). For `s = 50` this reduced one exact evaluation from about **36 s to under 6 s** with bit-identical results. ## Reproducing the benchmarks The full numerical validation sweep is included with the package but is not run automatically, because it can be computationally expensive and may use arbitrary-precision arithmetic. ```{r validation-sweep, eval=FALSE} sweep_file <- system.file( "validation", "test_doubleWishartHD_sweep.R", package = "rootWishartHD" ) if (!nzchar(sweep_file)) { stop("Validation script not found. Reinstall rootWishartHD with inst/validation included.") } source(sweep_file) # performance sweep over selected settings, 8 workers res <- run_sweep(c("p40", "p150"), n_exact = 6, n_cores = 8) perf_table(res, "kable") # markdown performance table # empirical-vs-analytic CDF comparison + figure cmp <- cdf_compare("p40", n_grid = 25) plot_cdf_compare(cmp) # regulate precision: two-stage arbitrary-precision evaluation ex <- make_exact(start_digits10 = 150, stage1_max = 800, stage2_max = 20000) run_sweep("p200", exact = ex, plot = TRUE) # writes CDF PNGs to diag_figs/ ``` Monte Carlo draws are cached under `diag_cache/`. ## References * Chiani, M. (2014). Distribution of the largest root of a matrix for Roy's test in multivariate analysis of variance. *Journal of Multivariate Analysis*, 131, 69-81. \doi{10.1016/j.jmva.2014.04.002} * Chiani, M. (2016). Distribution of the largest eigenvalue for real Wishart and Gaussian random matrices and a simple approximation for the Tracy-Widom distribution. *Journal of Multivariate Analysis*, 143, 480-493. \doi{10.1016/j.jmva.2015.10.007}