--- title: "Validation on simulated fields" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Validation on simulated fields} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 6.5, fig.height = 4.0, dpi = 120) ``` ```{r setup} library(trialSizing) ``` ## The idea On real uniformity trials the optimal plot size is unknown -- that is the whole reason to estimate it. To *validate* the methods we need the opposite: data whose spatial structure is known by construction, so an estimate can be compared against a truth. This article simulates such fields and checks what the package recovers. The fields are drawn from a **Gaussian process with a known exponential variogram**. Two quantities are then known exactly: - the **range** of the variogram, `a`, the distance over which basic units stay correlated; and - the **lag-1 autocorrelation** between adjacent units, $\rho = \exp(-3/a)$, which follows from the effective-range convention the package uses ($\gamma$ reaches 95% of the sill at `a`). ## The generator A field is one draw from a multivariate normal whose covariance decays exponentially with distance. Building it needs nothing beyond base R: ```{r generator} gen_field <- function(nr, nc, range, mu = 100, psill = 350, nugget = 50) { xy <- expand.grid(r = seq_len(nr), c = seq_len(nc)) D <- as.matrix(dist(xy)) S <- psill * exp(-3 * D / range) # structured covariance, 95% decay at `range` diag(S) <- psill + nugget # nugget: variance with no spatial structure z <- mu + t(chol(S)) %*% rnorm(nr * nc) matrix(as.numeric(z), nrow = nr, ncol = nc) } ``` A larger `range` means smoother fields: neighbours resemble one another over a longer distance. The three fields below, on a 16 x 16 grid, share one colour scale, so the increasing spatial structure is visible directly. ```{r fields, fig.height = 3.2} set.seed(99) fields <- lapply(c(2, 4, 6), function(a) gen_field(16, 16, range = a)) names(fields) <- paste("range =", c(2, 4, 6)) plot(check_trial(fields), title = "Simulated fields (kriged surface)") ``` ## Recovering the autocorrelation The lag-1 autocorrelation is the quantity the ParanaĆ­ba method rests on, and the one a short trial estimates most stably. For each range we draw several fields, estimate $\rho$ with `calc_paranaiba()` (the average of its two directional walks), and compare it with the truth $\exp(-3/a)$. ```{r rho-recovery} set.seed(2026) ranges <- c(2, 4, 6) rho <- do.call(rbind, lapply(ranges, function(a) { do.call(rbind, lapply(1:10, function(i) { s <- suppressMessages(calc_paranaiba(gen_field(16, 16, a)))$summary data.frame(range = a, rho_true = exp(-3 / a), rho_est = mean(c(s$rho_row, s$rho_col))) })) })) agg <- aggregate(cbind(rho_true, rho_est) ~ range, rho, mean) agg$bias <- agg$rho_est - agg$rho_true round(agg, 3) ``` The estimate tracks the truth across the whole range, with a small downward bias -- the familiar attenuation of a lag-1 autocorrelation estimated from a short series. The relationship is tight across individual draws, not just on average: ```{r rho-plot, fig.height = 4.2} library(ggplot2) ggplot(rho, aes(rho_true, rho_est, colour = factor(range))) + geom_abline(slope = 1, intercept = 0, linetype = 2, colour = "grey50") + geom_point(size = 2.4, alpha = 0.8) + labs(title = "Estimated vs true lag-1 autocorrelation", subtitle = sprintf("correlation across all draws: %.2f", cor(rho$rho_true, rho$rho_est)), x = expression("true " * rho == exp(-3/a)), y = expression("estimated " * rho), colour = "range") + coord_equal() + theme_minimal(base_size = 12) ``` Points sit just below the identity line: the estimator is slightly conservative but unbiased in *ordering*, which is what matters when $\rho$ feeds a plot-size formula. ## Response of the CV-curve methods The plateau methods do not estimate $\rho$; they read the optimal plot size off the CV-versus-size curve built by `calc_cv_shapes()`. A field with more spatial structure keeps rewarding larger plots for longer, so the optimum should **grow with the range**. It does: ```{r cv-response} set.seed(7) cv <- do.call(rbind, lapply(ranges, function(a) { do.call(rbind, lapply(1:4, function(i) { tab <- suppressMessages(calc_cv_shapes(gen_field(16, 16, a))) lrp <- fit_lrp(tab, x = "x", cv = "cv", step = 0.25) qrp <- fit_qrp(tab, x = "x", cv = "cv", step = 0.25) data.frame(range = a, LRP = unname(lrp$parameters["Breakpoint"]), QRP = unname(qrp$parameters["Breakpoint"])) })) })) aggregate(cbind(LRP, QRP) ~ range, cv, mean) ``` Both optima increase monotonically with the generating range: the methods respond to a controlled change in spatial structure in the expected direction. ## What this does and does not show The simulation validates two things cleanly. First, the **autocorrelation machinery** underneath `check_trial()` and `calc_paranaiba()` recovers a known $\rho$. Second, the **CV-curve optima move in the right direction** as spatial structure is dialled up. It also makes an honest limitation visible. There is no single "true optimal plot size" to recover: the methods target different definitions and answer different questions. The plateau methods (LRP, QRP) grow with the range, while the ParanaĆ­ba optimum moves the *other* way -- it is largest at $\rho = 0$ and shrinks as dependence strengthens, by construction of its formula (see `vignette("paranaiba")`). That divergence is a documented property, not an error, and it is exactly what `vignette("compare")` is for. A second caveat worth stating: the *point estimate* of the variogram range from a single small trial is noisy, so it is not used as a validation target here. The nugget-to-sill ratio and the autocorrelation are the more stable readings, which is why `check_trial()` leads with them. ## Extending this to your own scenarios The generator is the hook. Vary its arguments to build the cases you care about: - raise `nugget` toward `psill` to simulate a field with **weak spatial dependence**, where every method should return a small optimum and `check_trial()` should report a high nugget-to-sill ratio; - change the grid dimensions to study how few basic units the methods tolerate; - replace the exponential covariance with a spherical or Gaussian one to check robustness to the variogram shape. Wrapping the loops above in a function that returns estimate-minus-truth is the whole of a validation harness. See `vignette("check_trial")` for the diagnostics and `vignette("compare")` for reading several methods against each other.