Skip to content

Commit 37edd45

Browse files
committed
Update sim_failures() to add optional sim window
1 parent 748e4fe commit 37edd45

4 files changed

Lines changed: 176 additions & 19 deletions

File tree

.Rproj.user/CB2BA49E/pcs/files-pane.pper

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"ascending": true
66
}
77
],
8-
"path": "~/Documents/ReliaGrowR"
8+
"path": "~/Documents/ReliaGrowR/R"
99
}

R/sim_failures.R

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#' Simulates which units in a non-failed population fail next using probability
44
#' proportional to size (PPS) sampling based on unit runtimes. Units with
55
#' longer runtimes have a proportionally higher probability of being selected.
6+
#' The full fleet is returned: selected units are labelled `"Failure"` and the
7+
#' remaining units are labelled `"Suspension"`.
68
#'
79
#' @srrstats {G1.0} This function implements probability proportional to size
810
#' (PPS) sampling, a standard statistical technique.
@@ -21,17 +23,30 @@
2123
#' runtime of each unit in the non-failed population.
2224
#' @param replace Logical scalar. If `TRUE`, sampling is done with replacement
2325
#' (a unit may be selected more than once). Default is `FALSE`.
24-
#' @return A data frame with `n` rows sorted by `runtime`, containing:
25-
#' \item{index}{Integer index of the selected unit in `runtimes`.}
26-
#' \item{runtime}{Runtime of the selected unit (reported failure time).}
26+
#' @param window `NULL` or a single positive numeric. The width of the
27+
#' observation window. When `NULL` (default), event times equal current
28+
#' runtimes. When provided, failure times are
29+
#' `runtime + Uniform(0, window)` and suspension times are
30+
#' `runtime + window`.
31+
#' @return A data frame with `length(runtimes)` rows sorted ascending by
32+
#' `runtime`, containing:
33+
#' \item{index}{Integer index of the unit in `runtimes`.}
34+
#' \item{runtime}{Simulated event time.}
35+
#' \item{type}{Character; `"Failure"` for selected units, `"Suspension"` for
36+
#' the rest.}
2737
#' @family data preparation
2838
#' @examples
2939
#' set.seed(42)
3040
#' runtimes <- c(100, 500, 200, 800, 300)
3141
#' result <- sim_failures(2, runtimes)
3242
#' print(result)
43+
#'
44+
#' # With an observation window
45+
#' set.seed(42)
46+
#' result_w <- sim_failures(2, runtimes, window = 50)
47+
#' print(result_w)
3348
#' @export
34-
sim_failures <- function(n, runtimes, replace = FALSE) {
49+
sim_failures <- function(n, runtimes, replace = FALSE, window = NULL) {
3550
# Validate n
3651
if (!is.numeric(n) || length(n) != 1) {
3752
stop("'n' must be a single numeric value.")
@@ -67,11 +82,41 @@ sim_failures <- function(n, runtimes, replace = FALSE) {
6782
stop("'n' cannot exceed the number of units in 'runtimes' when replace = FALSE.")
6883
}
6984

85+
# Validate window
86+
if (!is.null(window)) {
87+
if (!is.numeric(window) || length(window) != 1) {
88+
stop("'window' must be a single positive numeric value.")
89+
}
90+
if (is.na(window) || is.nan(window) || !is.finite(window)) {
91+
stop("'window' must be a finite positive numeric value.")
92+
}
93+
if (window <= 0) {
94+
stop("'window' must be a positive numeric value.")
95+
}
96+
}
97+
7098
# PPS sampling
7199
prob <- runtimes / sum(runtimes)
72-
idx <- sample(seq_along(runtimes), size = n, replace = replace, prob = prob)
100+
idx <- sample(seq_along(runtimes), size = n, replace = replace, prob = prob)
101+
102+
fail_idx <- unique(idx)
103+
susp_idx <- setdiff(seq_along(runtimes), fail_idx)
104+
105+
# Compute event times
106+
if (is.null(window)) {
107+
fail_times <- runtimes[fail_idx]
108+
susp_times <- runtimes[susp_idx]
109+
} else {
110+
fail_times <- runtimes[fail_idx] + runif(length(fail_idx), 0, window)
111+
susp_times <- runtimes[susp_idx] + window
112+
}
73113

74-
result <- data.frame(index = idx, runtime = runtimes[idx])
114+
result <- data.frame(
115+
index = c(fail_idx, susp_idx),
116+
runtime = c(fail_times, susp_times),
117+
type = c(rep("Failure", length(fail_idx)),
118+
rep("Suspension", length(susp_idx)))
119+
)
75120
result <- result[order(result$runtime), ]
76121
rownames(result) <- NULL
77122

man/sim_failures.Rd

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

tests/testthat/test-srr-sim_failures.R

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ test_that("sim_failures returns correct output structure", {
44
result <- sim_failures(2, runtimes)
55

66
expect_s3_class(result, "data.frame")
7-
expect_named(result, c("index", "runtime"))
8-
expect_equal(nrow(result), 2)
7+
expect_named(result, c("index", "runtime", "type"))
8+
expect_equal(nrow(result), 5)
99
expect_type(result$index, "integer")
1010
expect_type(result$runtime, "double")
11+
expect_type(result$type, "character")
12+
expect_true(all(result$type %in% c("Failure", "Suspension")))
1113
# sorted ascending by runtime
1214
expect_true(all(diff(result$runtime) >= 0))
1315
})
@@ -17,7 +19,8 @@ test_that("sim_failures PPS bias: higher-runtime units selected more often", {
1719
runtimes <- c(10, 1000)
1820
N <- 10000
1921
draws <- replicate(N, {
20-
sim_failures(1, runtimes, replace = FALSE)$index
22+
res <- sim_failures(1, runtimes, replace = FALSE)
23+
res$index[res$type == "Failure"]
2124
})
2225
# unit 2 has 100x the runtime of unit 1, so it should be chosen ~99% of the time
2326
prop_unit2 <- mean(draws == 2)
@@ -72,10 +75,10 @@ test_that("sim_failures errors: n > length(runtimes) without replacement", {
7275
sim_failures(4, c(100, 200, 300), replace = FALSE),
7376
"'n' cannot exceed the number of units in 'runtimes' when replace = FALSE."
7477
)
75-
# Same call with replace = TRUE should succeed
78+
# Same call with replace = TRUE should succeed (returns full fleet of 3 rows)
7679
set.seed(1)
7780
result <- sim_failures(4, c(100, 200, 300), replace = TRUE)
78-
expect_equal(nrow(result), 4)
81+
expect_equal(nrow(result), 3)
7982
})
8083

8184
test_that("sim_failures is reproducible with the same seed", {
@@ -90,12 +93,12 @@ test_that("sim_failures is reproducible with the same seed", {
9093
expect_equal(r1, r2)
9194
})
9295

93-
test_that("sim_failures edge case: n = 1 returns a 1-row data frame", {
96+
test_that("sim_failures edge case: n = 1 returns full fleet (3 rows)", {
9497
set.seed(7)
9598
runtimes <- c(100, 500, 200)
9699
result <- sim_failures(1, runtimes)
97-
expect_equal(nrow(result), 1)
98-
expect_named(result, c("index", "runtime"))
100+
expect_equal(nrow(result), 3)
101+
expect_named(result, c("index", "runtime", "type"))
99102
})
100103

101104
test_that("sim_failures edge case: n = length(runtimes) returns all units without replacement", {
@@ -105,3 +108,96 @@ test_that("sim_failures edge case: n = length(runtimes) returns all units withou
105108
expect_equal(nrow(result), length(runtimes))
106109
expect_equal(sort(result$index), seq_along(runtimes))
107110
})
111+
112+
test_that("sim_failures type labels: correct counts of Failure and Suspension", {
113+
set.seed(42)
114+
runtimes <- c(100, 500, 200, 800, 300)
115+
n <- 2
116+
result <- sim_failures(n, runtimes, replace = FALSE)
117+
118+
n_failures <- sum(result$type == "Failure")
119+
n_suspensions <- sum(result$type == "Suspension")
120+
121+
# With replace = FALSE and n unique draws, exactly n failures and rest suspensions
122+
expect_equal(n_failures, n)
123+
expect_equal(n_suspensions, length(runtimes) - n)
124+
})
125+
126+
test_that("sim_failures window: failure times are in (runtime, runtime + window]", {
127+
set.seed(42)
128+
runtimes <- c(100, 500, 200, 800, 300)
129+
w <- 50
130+
result <- sim_failures(2, runtimes, window = w)
131+
132+
failures <- result[result$type == "Failure", ]
133+
for (i in seq_len(nrow(failures))) {
134+
orig <- runtimes[failures$index[i]]
135+
expect_gt(failures$runtime[i], orig)
136+
expect_lte(failures$runtime[i], orig + w)
137+
}
138+
})
139+
140+
test_that("sim_failures window: suspension times equal runtime + window exactly", {
141+
set.seed(42)
142+
runtimes <- c(100, 500, 200, 800, 300)
143+
w <- 50
144+
result <- sim_failures(2, runtimes, window = w)
145+
146+
suspensions <- result[result$type == "Suspension", ]
147+
for (i in seq_len(nrow(suspensions))) {
148+
orig <- runtimes[suspensions$index[i]]
149+
expect_equal(suspensions$runtime[i], orig + w)
150+
}
151+
})
152+
153+
test_that("sim_failures window = NULL: runtimes unchanged", {
154+
set.seed(42)
155+
runtimes <- c(100, 500, 200, 800, 300)
156+
result <- sim_failures(2, runtimes, window = NULL)
157+
158+
for (i in seq_len(nrow(result))) {
159+
expect_equal(result$runtime[i], runtimes[result$index[i]])
160+
}
161+
})
162+
163+
test_that("sim_failures window validation errors", {
164+
runtimes <- c(100, 500, 200)
165+
166+
expect_error(
167+
sim_failures(1, runtimes, window = "50"),
168+
"'window' must be a single positive numeric value."
169+
)
170+
expect_error(
171+
sim_failures(1, runtimes, window = c(10, 20)),
172+
"'window' must be a single positive numeric value."
173+
)
174+
expect_error(
175+
sim_failures(1, runtimes, window = NA_real_),
176+
"'window' must be a finite positive numeric value."
177+
)
178+
expect_error(
179+
sim_failures(1, runtimes, window = Inf),
180+
"'window' must be a finite positive numeric value."
181+
)
182+
expect_error(
183+
sim_failures(1, runtimes, window = 0),
184+
"'window' must be a positive numeric value."
185+
)
186+
expect_error(
187+
sim_failures(1, runtimes, window = -5),
188+
"'window' must be a positive numeric value."
189+
)
190+
})
191+
192+
test_that("sim_failures reproducibility with window", {
193+
runtimes <- c(100, 500, 200, 800, 300)
194+
w <- 50
195+
196+
set.seed(42)
197+
r1 <- sim_failures(2, runtimes, window = w)
198+
199+
set.seed(42)
200+
r2 <- sim_failures(2, runtimes, window = w)
201+
202+
expect_equal(r1, r2)
203+
})

0 commit comments

Comments
 (0)