|
| 1 | +#' Thin rejection sampling output for plotting |
| 2 | +#' |
| 3 | +#' Thins a large sample of sorted z-scores and stages for plotting efficiency. |
| 4 | +#' Retains 5,000 evenly-spaced points across the full range plus the last |
| 5 | +#' 10,000 points for dense tail coverage. |
| 6 | +#' |
| 7 | +#' @param n Integer. Total number of samples. |
| 8 | +#' @param z_sorted Numeric vector of sorted z-scores (ascending), length \code{n}. |
| 9 | +#' @param stage_sorted Numeric vector of sorted stage values (ascending), length \code{n}. |
| 10 | +#' |
| 11 | +#' @return A data frame with columns \code{z_aep} and \code{stage}. |
| 12 | +#' |
| 13 | +#' @keywords internal |
| 14 | +thin_samples <- function(n, z_sorted, stage_sorted) { |
| 15 | + thin_idx <- unique(c( |
| 16 | + round(seq(1, n, length.out = 5000)), |
| 17 | + (n - 10000):n |
| 18 | + )) |
| 19 | + thin_idx <- sort(unique(thin_idx)) |
| 20 | + thin_idx <- thin_idx[thin_idx >= 1 & thin_idx <= n] |
| 21 | + data.frame( |
| 22 | + z_aep = z_sorted[thin_idx], |
| 23 | + stage = stage_sorted[thin_idx] |
| 24 | + ) |
| 25 | +} |
| 26 | + |
| 27 | +#' Parameterize a Three-Parameter Lognormal PMF for Stage |
| 28 | +#' |
| 29 | +#' Computes the parameters of a three-parameter lognormal distribution for use |
| 30 | +#' as a probabilistic maximum stage (PMF) in rejection sampling. The shift |
| 31 | +#' parameter defines the lower bound of the distribution. |
| 32 | +#' |
| 33 | +#' @param pmf_shift Numeric. Lower bound (shift) of the lognormal distribution. |
| 34 | +#' Must be greater than \code{pmf_mean}. |
| 35 | +#' @param pmf_mean Numeric. Mean of the PMF stage distribution. Must be less |
| 36 | +#' than \code{pmf_shift}. |
| 37 | +#' @param pmf_sigma Numeric. Standard deviation on the log scale. Defaults to |
| 38 | +#' \code{0.5}. |
| 39 | +#' |
| 40 | +#' @return A named list with the following elements: |
| 41 | +#' \describe{ |
| 42 | +#' \item{pmf_shift}{Lower bound of the distribution.} |
| 43 | +#' \item{pmf_mean}{Mean of the distribution.} |
| 44 | +#' \item{pmf_sigma}{Standard deviation on the log scale.} |
| 45 | +#' \item{pmf_mu}{Derived location parameter on the log scale.} |
| 46 | +#' \item{pmf_p05}{5th percentile of the PMF stage distribution.} |
| 47 | +#' \item{pmf_p95}{95th percentile of the PMF stage distribution.} |
| 48 | +#' } |
| 49 | +#' |
| 50 | +#' @examples |
| 51 | +#' pmf <- pmf_stage_lognormal(pmf_shift = 1200, pmf_mean = 1150, pmf_sigma = 0.5) |
| 52 | +#' pmf$pmf_p05 |
| 53 | +#' pmf$pmf_p95 |
| 54 | +#' |
| 55 | +#' @export |
| 56 | +pmf_stage_lognormal <- function(pmf_shift, pmf_mean, pmf_sigma = 0.5){ |
| 57 | + if (pmf_mean >= pmf_shift) { |
| 58 | + cli::cli_abort(c( |
| 59 | + "{.arg pmf_mean} must be less than {.arg pmf_shift}.", |
| 60 | + "x" = "{.arg pmf_mean} is {.val {pmf_mean}} but {.arg pmf_shift} is {.val {pmf_shift}}." |
| 61 | + )) |
| 62 | + } |
| 63 | + |
| 64 | + # Derive mu ==== |
| 65 | + pmf_mu <- log(pmf_mean - pmf_shift) - (pmf_sigma^2 / 2) |
| 66 | + |
| 67 | + # Summary stats |
| 68 | + # pmf_median <- pmf_shift + exp(pmf_mu) |
| 69 | + # pmf_sd <- sqrt((exp(pmf_sigma^2) - 1) * exp(2 * pmf_mu + pmf_sigma^2)) |
| 70 | + pmf_p05 <- pmf_shift + qlnorm(0.05, pmf_mu, pmf_sigma) |
| 71 | + pmf_p95 <- pmf_shift + qlnorm(0.95, pmf_mu, pmf_sigma) |
| 72 | + |
| 73 | + # create pmf_stage_LN as a list |
| 74 | + pmf_stage_LN <- list(pmf_shift = pmf_shift, |
| 75 | + pmf_mean = pmf_mean, |
| 76 | + pmf_sigma = pmf_sigma, |
| 77 | + pmf_mu = pmf_mu, |
| 78 | + pmf_p05 = pmf_p05, |
| 79 | + pmf_p95 = pmf_p95) |
| 80 | + |
| 81 | + return(pmf_stage_LN) |
| 82 | +} |
| 83 | + |
| 84 | +#' Rejection Sampling of Stage Bounded by a Probabilistic Maximum Stage |
| 85 | +#' |
| 86 | +#' Draws \code{n_samples} stage values from a stage-frequency curve, rejecting |
| 87 | +#' any draws that exceed a probabilistic maximum stage (PMF) sampled from a |
| 88 | +#' three-parameter lognormal distribution. Accepted samples are ranked and |
| 89 | +#' assigned Weibull plotting positions for use in frequency analysis. |
| 90 | +#' |
| 91 | +#' @param pmf_stage_LN A named list returned by \code{\link{pmf_stage_lognormal}} |
| 92 | +#' containing the lognormal PMF parameters. |
| 93 | +#' @param stage_freq_df A data frame with AEP in column 1 and stage in column 2. |
| 94 | +#' Mutually exclusive with \code{aep} and \code{stage}. |
| 95 | +#' @param aep Numeric vector of annual exceedance probabilities. Must be |
| 96 | +#' supplied with \code{stage}. Mutually exclusive with \code{stage_freq_df}. |
| 97 | +#' @param stage Numeric vector of stages corresponding to \code{aep}. Must be |
| 98 | +#' supplied with \code{aep}. Mutually exclusive with \code{stage_freq_df}. |
| 99 | +#' @param n_samples Integer. Number of samples to draw. Defaults to \code{1e7}. |
| 100 | +#' |
| 101 | +#' @return A data frame of thinned accepted samples with columns for z-score |
| 102 | +#' and stage, suitable for plotting a probabilistically bounded stage-frequency |
| 103 | +#' curve. Output is produced by \code{thin_samples()}. |
| 104 | +#' |
| 105 | +#' @seealso \code{\link{pmf_stage_lognormal}} |
| 106 | +#' |
| 107 | +#' @examples |
| 108 | +#' \dontrun{ |
| 109 | +#' pmf <- pmf_stage_lognormal(pmf_shift = 1200, pmf_mean = 1150, pmf_sigma = 0.5) |
| 110 | +#' |
| 111 | +#' result <- rejection_sampling_stage( |
| 112 | +#' pmf_stage_LN = pmf, |
| 113 | +#' stage_freq_df = my_stage_freq_df, |
| 114 | +#' n_samples = 1e7 |
| 115 | +#' ) |
| 116 | +#' } |
| 117 | +#' |
| 118 | +#' @export |
| 119 | +rejection_sampling_stage <- function(pmf_stage_LN, stage_freq_df = NULL, aep = NULL, stage = NULL, n_samples = 1E7){ |
| 120 | + # Resolve inputs |
| 121 | + if (!is.null(stage_freq_df)) { |
| 122 | + if (!is.null(aep) || !is.null(stage)) { |
| 123 | + cli::cli_abort("Provide either {.arg stage_freq_df} or {.arg aep}/{.arg stage}, not both.") |
| 124 | + } |
| 125 | + aep <- stage_freq_df[[1]] |
| 126 | + stage <- stage_freq_df[[2]] |
| 127 | + |
| 128 | + } else if (!is.null(aep) && !is.null(stage)) { |
| 129 | + # use as-is |
| 130 | + |
| 131 | + } else { |
| 132 | + cli::cli_abort(c( |
| 133 | + "Must provide either {.arg stage_freq_df} or both {.arg aep} and {.arg stage}.", |
| 134 | + "i" = "Supply a data frame via {.arg stage_freq_df}, or pass numeric vectors to {.arg aep} and {.arg stage} directly." |
| 135 | + )) |
| 136 | + } |
| 137 | + |
| 138 | + # PMF Log-Normal Parameters ================================================== |
| 139 | + pmf_shift <- pmf_stage_LN[["pmf_shift"]] |
| 140 | + pmf_mu <- pmf_stage_LN[["pmf_mu"]] |
| 141 | + pmf_sigma <- pmf_stage_LN[["pmf_sigma"]] |
| 142 | + |
| 143 | + # Transform to z-space for interpolation function ============================ |
| 144 | + z_aep <- qnorm(1-aep) |
| 145 | + zaep_stage <- approxfun(z_aep, stage, rule = 2) |
| 146 | + |
| 147 | + # First sample rejection ===================================================== |
| 148 | + # Sample AEPs |
| 149 | + aep_draws <- runif(n_samples, min = 1e-9, max = 1 - 1e-9) |
| 150 | + |
| 151 | + # Return Stage corresponding to sampled AEP (as z-value) |
| 152 | + stage_draws <- zaep_stage(qnorm(1 - aep_draws)) |
| 153 | + |
| 154 | + # Randomly Sample PMF Stage from log-normal above |
| 155 | + pmf_draws <- pmf_shift + rlnorm(n_samples, pmf_mu, pmf_sigma) |
| 156 | + |
| 157 | + # Reject (aep,stage) where the stage exceeds sampled PMF Stage |
| 158 | + reject_idx <- which(stage_draws > pmf_draws) |
| 159 | + |
| 160 | + # Rejection Iterations ======================================================= |
| 161 | + iter <- 0 |
| 162 | + while (length(reject_idx) > 0) { |
| 163 | + # Number of rejects |
| 164 | + n_rej <- length(reject_idx) |
| 165 | + # New AEP sample |
| 166 | + aep_new <- runif(n_rej, min = 1e-9, max = 1 - 1e-9) |
| 167 | + # Corresponding stage sample |
| 168 | + stage_new <- zaep_stage(qnorm(1 - aep_new)) |
| 169 | + # New PMF Stage sample |
| 170 | + pmf_new <- pmf_shift + rlnorm(n_rej, pmf_mu, pmf_sigma) |
| 171 | + # Replace rejections with resampled aep,stage |
| 172 | + stage_draws[reject_idx] <- stage_new |
| 173 | + pmf_draws[reject_idx] <- pmf_new |
| 174 | + # Next rejection index |
| 175 | + reject_idx <- reject_idx[stage_new > pmf_new] |
| 176 | + # Count iterations |
| 177 | + iter <- iter + 1 |
| 178 | + if (iter >= 1E3) { |
| 179 | + cli::cli_warn(c( |
| 180 | + "Rejection sampling did not converge after {iter} iterations.", |
| 181 | + "i" = "{length(reject_idx)} sample{?s} remain unresolved.", |
| 182 | + "i" = "Check that {.arg pmf_stage_LN} is consistent with the provided stage-frequency curve." |
| 183 | + )) |
| 184 | + break |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + # Assign Weibull to the accepted samples ===================================== |
| 189 | + # Sort Stages |
| 190 | + stage_sorted <- sort(stage_draws) |
| 191 | + |
| 192 | + # Assign Weibull pp |
| 193 | + weibull_aep <- 1 - seq_len(n_samples) / (n_samples + 1) |
| 194 | + |
| 195 | + # Convert to z_aep |
| 196 | + z_sorted <- qnorm(1 - weibull_aep) |
| 197 | + |
| 198 | + # Thin for plotting purposes |
| 199 | + probabilistic_bounded <- thin_samples(n_samples, z_sorted, stage_sorted) |
| 200 | + return(probabilistic_bounded) |
| 201 | +} |
0 commit comments