Skip to content

Commit 35ed1c4

Browse files
authored
Add rejection sampling using PMF Lognormal (#32)
* Add rejection sampling using PMF Lognormal * Add rejection sampling functions to pkgdown reference index
1 parent 4f89bfb commit 35ed1c4

10 files changed

Lines changed: 332 additions & 26 deletions

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ export(flow_frequency_sampler)
66
export(flow_frequency_sampler_expected)
77
export(hydrograph_setup)
88
export(mod_puls_routing)
9+
export(pmf_stage_lognormal)
910
export(qp3)
11+
export(rejection_sampling_stage)
1012
export(rfa_simulate)
1113
export(scale_hydrograph)
1214
export(stage2aep)
@@ -16,7 +18,9 @@ importFrom(stats,approx)
1618
importFrom(stats,approxfun)
1719
importFrom(stats,pnorm)
1820
importFrom(stats,qgamma)
21+
importFrom(stats,qlnorm)
1922
importFrom(stats,qnorm)
2023
importFrom(stats,quantile)
24+
importFrom(stats,rlnorm)
2125
importFrom(stats,runif)
2226
importFrom(utils,write.csv)

R/imports.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
#' @importFrom stats approx approxfun pnorm qgamma qnorm quantile runif
1+
#' @importFrom stats approx approxfun pnorm qgamma qnorm quantile runif qlnorm rlnorm
22
#' @importFrom utils write.csv
33
NULL

R/rejection_sampling.R

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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+
}

R/utils.R

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,3 @@ power_function <- function(x) {
88
10^(1:x)
99
}
1010

11-
#' Thin samples
12-
#' Thin a large sample of sorted z-scores and stages for efficiency.
13-
#' Retains 5,000 evenly-spaced points across the full range plus the last
14-
#' 10,000 points for dense coverage at the extreme tail.
15-
#'
16-
#' @param n Total number of samples
17-
#' @param z_sorted Sorted z-scores (ascending), length n
18-
#' @param stage_sorted Sorted stage values (ascending), length n
19-
#' @return A tibble with columns z_aep and stage
20-
thin_samples <- function(n, z_sorted, stage_sorted) {
21-
22-
thin_idx <- unique(c(
23-
round(seq(1, n, length.out = 5000)),
24-
(n - 10000):n
25-
))
26-
thin_idx <- sort(unique(thin_idx))
27-
thin_idx <- thin_idx[thin_idx >= 1 & thin_idx <= n]
28-
29-
data.frame(
30-
z_aep = z_sorted[thin_idx],
31-
stage = stage_sorted[thin_idx]
32-
)
33-
}

_pkgdown.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ reference:
4040
- interpolate_stage_matrix
4141
- power_function
4242
- theme_rfar_conceptual
43+
- pmf_stage_lognormal
44+
- rejection_sampling_stage
4345
- title: "Datasets"
4446
desc: "Example datasets included with rfaR"
4547
contents:

man/pmf_stage_lognormal.Rd

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/power_function.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/rejection_sampling_stage.Rd

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/rfaR-package.Rd

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)