Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix logging of closure limits #34

Merged
merged 7 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: daedalus
Title: Model health, social, and economic costs of a pandemic using
_DAEDALUS_
Version: 0.0.14
Version: 0.0.15
Authors@R: c(
person("Pratik", "Gupte", , "p.gupte24@imperial.ac.uk", role = c("aut", "cre"),
comment = c(ORCID = "0000-0001-5294-7819")),
Expand All @@ -16,7 +16,7 @@ License: MIT + file LICENSE
URL: https://github.com/j-idea/daedalus
BugReports: https://github.com/j-idea/daedalus/issues
Depends:
R (>= 2.10)
R (>= 4.3)
Imports:
checkmate,
cli,
Expand Down
14 changes: 14 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# daedalus 0.0.15

This patch fixes an issue where closures were not ended in model runs where the closure began after the epidemic had stopped growing. This mostly affected edge cases of countries with large spare hospital capacity, and relatively late `response_time`s. In such cases the closure end time was assigned as the simulation end time, inflating costs related to closures. The fix:

- Prevents closures from being activated by the `hospital_capacity` trigger if the epidemic is not growing, even if the hospital capacity threshold is crossed;

- Prevents closures from being activated by the `response_time` trigger if the epidemic is not growing between stage 01 and stage 02. Closures are manually turned off if the epidemic is not growing ($R_t < 1.0$).

Tests for different response times check that the model behaves as expected.

## Miscellaneous changes

- The package now requires a minimum R version >= v4.3 due to the use of the new function `array2DF()`.

# daedalus 0.0.14

This patch adds vaccine investment scenarios to _daedalus_. All model runs must now include an assumption about the level of advance vaccine investment, defaulting to "none".
Expand Down
29 changes: 23 additions & 6 deletions R/closure.R
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,32 @@ make_response_threshold_event <- function(response_threshold) {
}

event_function <- function(time, state, parameters) {
# prevent flipping switch when checkEventFunc runs
# turn closure and excess hospitalisation switch on
# NOTE: prevent flipping switch when checkEventFunc runs
# NOTE: prevent response activation if epdedemic is not growing
state <- array(
state,
c(N_AGE_GROUPS, N_MODEL_COMPARTMENTS, N_ECON_STRATA, N_VACCINE_STRATA)
)
cm <- parameters[["contact_matrix"]] %*% diag(parameters[["demography"]])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If neither of these inputs never change is it better perhaps to compute in parameter set up once rather than adding this matrix multiplication to every root check?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to parameters.

rt <- r_eff(parameters[["r0"]], state, cm)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to get a "r_greater_than_one" check function written at some point that could be much faster than doing this accurately - would be good to get some profiles done at some point

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would that be a simple wrapper around this operation, or is there something more complex I'm missing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something more complex - we don't care about the eigenvalues very much - we just care about the sign of the leading one. (Much) faster algorithms exist for finding the leading eigenvalue and I think we can generalise that (or find an existing generalisation) if we just care about the sign. But no point doing any of that unless this is actually a timesink, hence the suggestion of profiling first

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have thoughts on this that open up another can of worms related to the $R_0$ to $\beta$ conversion used in this model.

I've worked out a relatively simple mathematical expression for the eigenvalue here: https://hackmd.io/xfzSdlY-SnGblvP8aOA7Fw, as a precursor to calculating $\beta$, but we don't really use this in the model as I wasn't able to explain it sufficiently to EPPI (and they have their own maths, which I think is equivalent). So this alternative exists if we can sit down and hash out whether the maths is correct.


# NOTE: to ensure only first hosp threshold crossing is logged
is_hosp_switch_on <- rlang::env_get(parameters[["mutables"]], "hosp_switch")

if (time != parameters[["min_time"]] && !is_hosp_switch_on) {
rlang::env_bind(
parameters[["mutables"]],
switch = TRUE, hosp_switch = TRUE,
closure_time_start = time
rlang::env_poke(
parameters[["mutables"]], "hosp_switch", TRUE
)

# NOTE: trigger response and log closure start time only if
# epidemic is growing
if (rt >= 1.0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also be checking somewhere here that switch is already FALSE? Or is it sufficient that hosp_switch is FALSE?

Copy link
Collaborator Author

@pratikunterwegs pratikunterwegs Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that switch and hosp_switch are equivalent flags in stage 01 (1:response_time).

The sequence of events is:

  • Hospitalisations cross the capacity threshold, but hosp_switch is FALSE;
  • hosp_switch is set to TRUE activating excess mortality;
  • IFF the epidemic is growing, the response is triggered and switch is turned on activating a response;
  • time is logged as closure_start_time;

I have split up the two switches in make_rt_end_event() as they are independent once the response is triggered by the default response_time (t = 30).

rlang::env_bind(
parameters[["mutables"]],
switch = TRUE,
closure_time_start = time
)
}
}
as.numeric(state)
}
Expand Down
22 changes: 22 additions & 0 deletions R/daedalus.R
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,28 @@ daedalus <- function(country,
]
initial_state <- as.numeric(initial_state)

### cancel closures if epidemic is not growing ###
state_temp <- array(
initial_state,
c(N_AGE_GROUPS, N_MODEL_COMPARTMENTS, N_ECON_STRATA, N_VACCINE_STRATA)
)
# close intervention IFF epidemic is not growing
# NOTE: this step placed here as a conditional on r_eff < 1.0 is not
# practical in a root-finding function (Error: 'root too near initial point')
is_epidemic_growing <- r_eff(
parameters[["r0"]],
state_temp,
parameters[["contact_matrix"]] %*% diag(parameters[["demography"]])
) >= 1.0

if (!is_epidemic_growing) {
rlang::env_bind(
parameters[["mutables"]],
switch = FALSE,
closure_time_end = response_time
)
}

# reset min time
parameters[["min_time"]] <- response_time

Expand Down
39 changes: 39 additions & 0 deletions tests/testthat/test-closures.R
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,43 @@ test_that("Closures: correct logging of time limits", {
response_data[["closure_info"]][["closure_time_end"]],
time_end
)

# Check that closures are terminated at the start time when
# the epidemic is not growing
dvx <- daedalus_vaccination("low", vax_start_time = 90)
tend <- 100
response_time <- seq(10, 80, 10)

# run scenario with very few susceptibles
invisible(
lapply(
response_time, function(x) {
data <- daedalus(
"United States", "influenza_1957",
initial_state_manual = list(
p_infectious = 0.9
),
response_strategy = "elimination",
response_time = x,
vaccine_investment = dvx,
time_end = tend
)

closure_info <- data$response_data$closure_info

expect_identical(
closure_info$closure_time_end,
closure_info$closure_time_start
)
expect_identical(
closure_info$closure_time_end,
x
)
expect_identical(
closure_info$closure_duration,
0.0
)
}
)
)
})